diff --git a/OpenRA.Game/GameInformation.cs b/OpenRA.Game/GameInformation.cs index 1401ad6dec..c315948345 100644 --- a/OpenRA.Game/GameInformation.cs +++ b/OpenRA.Game/GameInformation.cs @@ -19,6 +19,9 @@ namespace OpenRA { public class GameInformation { + [TranslationReference("name", "number")] + const string EnumeratedBotName = "enumerated-bot-name"; + public string Mod; public string Version; @@ -118,6 +121,7 @@ namespace OpenRA Name = runtimePlayer.PlayerName, IsHuman = !runtimePlayer.IsBot, IsBot = runtimePlayer.IsBot, + BotType = runtimePlayer.BotType, FactionName = runtimePlayer.Faction.Name, FactionId = runtimePlayer.Faction.InternalName, DisplayFactionName = runtimePlayer.DisplayFaction.Name, @@ -143,6 +147,20 @@ namespace OpenRA return player; } + public string ResolvedPlayerName(Player player) + { + if (player.IsBot) + { + var number = Players.Where(p => p.BotType == player.BotType).ToList().IndexOf(player) + 1; + return TranslationProvider.GetString(EnumeratedBotName, + Translation.Arguments( + "name", TranslationProvider.GetString(player.Name), + "number", number)); + } + + return player.Name; + } + public class Player { #region Start-up information @@ -153,6 +171,7 @@ namespace OpenRA public string Name; public bool IsHuman; public bool IsBot; + public string BotType; /// The faction's display name. public string FactionName; diff --git a/OpenRA.Game/Network/Order.cs b/OpenRA.Game/Network/Order.cs index 399fa515cd..5e76b9d673 100644 --- a/OpenRA.Game/Network/Order.cs +++ b/OpenRA.Game/Network/Order.cs @@ -465,7 +465,7 @@ namespace OpenRA public override string ToString() { return $"OrderString: \"{OrderString}\" \n\t Type: \"{Type}\". \n\t Subject: \"{Subject}\". \n\t Target: \"{Target}\"." + - $"\n\t TargetString: \"{TargetString}\".\n\t IsImmediate: {IsImmediate}.\n\t Player(PlayerName): {Player?.PlayerName}\n"; + $"\n\t TargetString: \"{TargetString}\".\n\t IsImmediate: {IsImmediate}.\n\t Player(PlayerName): {Player?.ResolvedPlayerName}\n"; } } } diff --git a/OpenRA.Game/Player.cs b/OpenRA.Game/Player.cs index 7336c7dc6f..18b0bfeb96 100644 --- a/OpenRA.Game/Player.cs +++ b/OpenRA.Game/Player.cs @@ -38,6 +38,9 @@ namespace OpenRA public class Player : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding { + [TranslationReference("name", "number")] + const string EnumeratedBotName = "enumerated-bot-name"; + public readonly Actor PlayerActor; public readonly string PlayerName; public readonly string InternalName; @@ -80,6 +83,9 @@ namespace OpenRA readonly IUnlocksRenderPlayer[] unlockRenderPlayer; readonly INotifyPlayerDisconnected[] notifyDisconnected; + readonly IReadOnlyCollection botInfos; + string resolvedPlayerName; + // Each player is identified with a unique bit in the set // Cache masks for the player's index and ally/enemy player indices for performance. public LongBitSet PlayerMask; @@ -97,6 +103,16 @@ namespace OpenRA } } + /// The chosen player name including localized and enumerated bot names. + public string ResolvedPlayerName + { + get + { + resolvedPlayerName ??= ResolvePlayerName(); + return resolvedPlayerName; + } + } + public static FactionInfo ResolveFaction( string factionName, IEnumerable factionInfos, MersenneTwister playerRandom, bool requireSelectable = true) { @@ -133,18 +149,6 @@ namespace OpenRA return factions.FirstOrDefault(f => f.InternalName == factionName) ?? factions.First(); } - public static string ResolvePlayerName(Session.Client client, IEnumerable clients, IEnumerable botInfos) - { - if (client.Bot != null) - { - var botInfo = botInfos.First(b => b.Type == client.Bot); - var botsOfSameType = clients.Where(c => c.Bot == client.Bot).ToArray(); - return botsOfSameType.Length == 1 ? botInfo.Name : $"{botInfo.Name} {botsOfSameType.IndexOf(client) + 1}"; - } - - return client.Name; - } - public Player(World world, Session.Client client, PlayerReference pr, MersenneTwister playerRandom) { World = world; @@ -152,6 +156,7 @@ namespace OpenRA PlayerReference = pr; inMissionMap = world.Map.Visibility.HasFlag(MapVisibility.MissionSelector); + botInfos = World.Map.Rules.Actors[SystemActors.Player].TraitInfos(); // Real player or host-created bot if (client != null) @@ -159,7 +164,7 @@ namespace OpenRA ClientIndex = client.Index; color = client.Color; Color = color; - PlayerName = ResolvePlayerName(client, world.LobbyInfo.Clients, world.Map.Rules.Actors[SystemActors.Player].TraitInfos()); + PlayerName = client.Name; BotType = client.Bot; Faction = ResolveFaction(world, client.Faction, playerRandom, !pr.LockFaction); @@ -224,7 +229,21 @@ namespace OpenRA public override string ToString() { - return $"{PlayerName} ({ClientIndex})"; + return $"{ResolvedPlayerName} ({ClientIndex})"; + } + + string ResolvePlayerName() + { + if (IsBot) + { + var botInfo = botInfos.First(b => b.Type == BotType); + var botsOfSameType = World.Players.Where(c => c.BotType == BotType).ToArray(); + return TranslationProvider.GetString(EnumeratedBotName, + Translation.Arguments("name", TranslationProvider.GetString(botInfo.Name), + "number", botsOfSameType.IndexOf(this) + 1)); + } + + return PlayerName; } public PlayerRelationship RelationshipWith(Player other) @@ -310,7 +329,7 @@ namespace OpenRA public LuaValue ToString(LuaRuntime runtime) { - return $"Player ({PlayerName})"; + return $"Player ({ResolvedPlayerName})"; } #endregion diff --git a/OpenRA.Game/Scripting/ScriptPlayerInterface.cs b/OpenRA.Game/Scripting/ScriptPlayerInterface.cs index 305a29c7c2..065aa3bf2a 100644 --- a/OpenRA.Game/Scripting/ScriptPlayerInterface.cs +++ b/OpenRA.Game/Scripting/ScriptPlayerInterface.cs @@ -16,9 +16,9 @@ namespace OpenRA.Scripting readonly Player player; protected override string DuplicateKeyError(string memberName) => - $"Player '{player.PlayerName}' defines the command '{memberName}' on multiple traits"; + $"Player '{player.ResolvedPlayerName}' defines the command '{memberName}' on multiple traits"; protected override string MemberNotFoundError(string memberName) => - $"Player '{player.PlayerName}' does not define a property '{memberName}'"; + $"Player '{player.ResolvedPlayerName}' does not define a property '{memberName}'"; public ScriptPlayerInterface(ScriptContext context, Player player) : base(context) diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index 462c6ea35e..ed07e9ffb5 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -1323,7 +1323,7 @@ namespace OpenRA.Server foreach (var cmpi in Map.WorldActorInfo.TraitInfos()) cmpi.CreateServerPlayers(Map, LobbyInfo, worldPlayers, playerRandom); - gameInfo = new GameInformation + gameInfo = new GameInformation() { Mod = Game.ModData.Manifest.Id, Version = Game.ModData.Manifest.Metadata.Version, diff --git a/OpenRA.Mods.Common/Scripting/Properties/PlayerProperties.cs b/OpenRA.Mods.Common/Scripting/Properties/PlayerProperties.cs index a0e53b0525..985d4ae45a 100644 --- a/OpenRA.Mods.Common/Scripting/Properties/PlayerProperties.cs +++ b/OpenRA.Mods.Common/Scripting/Properties/PlayerProperties.cs @@ -28,7 +28,7 @@ namespace OpenRA.Mods.Common.Scripting public string InternalName => Player.InternalName; [Desc("The player's name.")] - public string Name => Player.PlayerName; + public string Name => Player.ResolvedPlayerName; [Desc("The player's color.")] public Color Color => Player.GetColor(Player); diff --git a/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs index be43996aa4..ff61177d95 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/SupportPowerBotModule.cs @@ -83,14 +83,14 @@ namespace OpenRA.Mods.Common.Traits { if (powerDecision == null) { - AIUtils.BotDebug("{0} couldn't find powerDecision for {1}", player.PlayerName, sp.Info.OrderName); + AIUtils.BotDebug($"{player.ResolvedPlayerName} couldn't find powerDecision for {sp.Info.OrderName}"); continue; } var attackLocation = FindCoarseAttackLocationToSupportPower(sp); if (attackLocation == null) { - AIUtils.BotDebug("{0} can't find suitable coarse attack location for support power {1}. Delaying rescan.", player.PlayerName, sp.Info.OrderName); + AIUtils.BotDebug($"{player.ResolvedPlayerName} can't find suitable coarse attack location for support power {sp.Info.OrderName}. Delaying rescan."); waitingPowers[sp] += powerDecision.GetNextScanTime(world); continue; @@ -100,14 +100,14 @@ namespace OpenRA.Mods.Common.Traits attackLocation = FindFineAttackLocationToSupportPower(sp, (CPos)attackLocation); if (attackLocation == null) { - AIUtils.BotDebug("{0} can't find suitable final attack location for support power {1}. Delaying rescan.", player.PlayerName, sp.Info.OrderName); + AIUtils.BotDebug($"{player.ResolvedPlayerName} can't find suitable final attack location for support power {sp.Info.OrderName}. Delaying rescan."); waitingPowers[sp] += powerDecision.GetNextScanTime(world); continue; } // Valid target found, delay by a few ticks to avoid rescanning before power fires via order - AIUtils.BotDebug("{0} found new target location {1} for support power {2}.", player.PlayerName, attackLocation, sp.Info.OrderName); + AIUtils.BotDebug($"{player.ResolvedPlayerName} found new target location {attackLocation} for support power {sp.Info.OrderName}."); waitingPowers[sp] += 10; // Note: SelectDirectionalTarget uses uint.MaxValue in ExtraData to indicate that the player did not pick a direction. @@ -131,7 +131,7 @@ namespace OpenRA.Mods.Common.Traits var powerDecision = powerDecisions[readyPower.Info.OrderName]; if (powerDecision == null) { - AIUtils.BotDebug("{0} couldn't find powerDecision for {1}", player.PlayerName, readyPower.Info.OrderName); + AIUtils.BotDebug($"{player.ResolvedPlayerName} couldn't find powerDecision for {readyPower.Info.OrderName}"); return null; } @@ -181,7 +181,7 @@ namespace OpenRA.Mods.Common.Traits var powerDecision = powerDecisions[readyPower.Info.OrderName]; if (powerDecision == null) { - AIUtils.BotDebug("{0} couldn't find powerDecision for {1}", player.PlayerName, readyPower.Info.OrderName); + AIUtils.BotDebug($"{player.ResolvedPlayerName} couldn't find powerDecision for {readyPower.Info.OrderName}"); return null; } diff --git a/OpenRA.Mods.Common/Traits/Player/ConquestVictoryConditions.cs b/OpenRA.Mods.Common/Traits/Player/ConquestVictoryConditions.cs index 0d8eae8401..644fa16032 100644 --- a/OpenRA.Mods.Common/Traits/Player/ConquestVictoryConditions.cs +++ b/OpenRA.Mods.Common/Traits/Player/ConquestVictoryConditions.cs @@ -105,7 +105,7 @@ namespace OpenRA.Mods.Common.Traits if (info.SuppressNotifications) return; - TextNotificationsManager.AddSystemLine(PlayerIsDefeated, Translation.Arguments("player", player.PlayerName)); + TextNotificationsManager.AddSystemLine(PlayerIsDefeated, Translation.Arguments("player", player.ResolvedPlayerName)); Game.RunAfterDelay(info.NotificationDelay, () => { if (Game.IsCurrentWorld(player.World) && player == player.World.LocalPlayer) @@ -121,7 +121,7 @@ namespace OpenRA.Mods.Common.Traits if (info.SuppressNotifications) return; - TextNotificationsManager.AddSystemLine(PlayerIsVictorious, Translation.Arguments("player", player.PlayerName)); + TextNotificationsManager.AddSystemLine(PlayerIsVictorious, Translation.Arguments("player", player.ResolvedPlayerName)); Game.RunAfterDelay(info.NotificationDelay, () => { if (Game.IsCurrentWorld(player.World) && player == player.World.LocalPlayer) diff --git a/OpenRA.Mods.Common/Traits/Player/DeveloperMode.cs b/OpenRA.Mods.Common/Traits/Player/DeveloperMode.cs index 4b80f68b77..69d2e00522 100644 --- a/OpenRA.Mods.Common/Traits/Player/DeveloperMode.cs +++ b/OpenRA.Mods.Common/Traits/Player/DeveloperMode.cs @@ -275,7 +275,7 @@ namespace OpenRA.Mods.Common.Traits return; } - var arguments = Translation.Arguments("cheat", order.OrderString, "player", self.Owner.PlayerName, "suffix", debugSuffix); + var arguments = Translation.Arguments("cheat", order.OrderString, "player", self.Owner.ResolvedPlayerName, "suffix", debugSuffix); TextNotificationsManager.Debug(TranslationProvider.GetString(CheatUsed, arguments)); } diff --git a/OpenRA.Mods.Common/Traits/Player/ModularBot.cs b/OpenRA.Mods.Common/Traits/Player/ModularBot.cs index 83d857d00c..e4af985ea4 100644 --- a/OpenRA.Mods.Common/Traits/Player/ModularBot.cs +++ b/OpenRA.Mods.Common/Traits/Player/ModularBot.cs @@ -25,8 +25,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("Internal id for this bot.")] public readonly string Type = null; + [TranslationReference] [Desc("Human-readable name this bot uses.")] - public readonly string Name = "Unnamed Bot"; + public readonly string Name = null; [Desc("Minimum portion of pending orders to issue each tick (e.g. 5 issues at least 1/5th of all pending orders). " + "Excess orders remain queued for subsequent ticks.")] diff --git a/OpenRA.Mods.Common/Traits/Player/StrategicVictoryConditions.cs b/OpenRA.Mods.Common/Traits/Player/StrategicVictoryConditions.cs index 9e6dc647c9..06004f517b 100644 --- a/OpenRA.Mods.Common/Traits/Player/StrategicVictoryConditions.cs +++ b/OpenRA.Mods.Common/Traits/Player/StrategicVictoryConditions.cs @@ -151,7 +151,7 @@ namespace OpenRA.Mods.Common.Traits if (info.SuppressNotifications) return; - TextNotificationsManager.AddSystemLine(PlayerIsDefeated, Translation.Arguments("player", player.PlayerName)); + TextNotificationsManager.AddSystemLine(PlayerIsDefeated, Translation.Arguments("player", player.ResolvedPlayerName)); Game.RunAfterDelay(info.NotificationDelay, () => { if (Game.IsCurrentWorld(player.World) && player == player.World.LocalPlayer) @@ -167,7 +167,7 @@ namespace OpenRA.Mods.Common.Traits if (info.SuppressNotifications) return; - TextNotificationsManager.AddSystemLine(PlayerIsVictorious, Translation.Arguments("player", player.PlayerName)); + TextNotificationsManager.AddSystemLine(PlayerIsVictorious, Translation.Arguments("player", player.ResolvedPlayerName)); Game.RunAfterDelay(info.NotificationDelay, () => { if (Game.IsCurrentWorld(player.World) && player == player.World.LocalPlayer) diff --git a/OpenRA.Mods.Common/Traits/Render/WithNameTagDecoration.cs b/OpenRA.Mods.Common/Traits/Render/WithNameTagDecoration.cs index 6574d82121..e97e0d1953 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithNameTagDecoration.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithNameTagDecoration.cs @@ -54,7 +54,7 @@ namespace OpenRA.Mods.Common.Traits.Render font = Game.Renderer.Fonts[info.Font]; this.info = info; - name = self.Owner.PlayerName; + name = self.Owner.ResolvedPlayerName; if (name.Length > info.MaxLength) name = name[..info.MaxLength]; } @@ -73,7 +73,7 @@ namespace OpenRA.Mods.Common.Traits.Render void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) { - name = self.Owner.PlayerName; + name = self.Owner.ResolvedPlayerName; if (name.Length > Info.MaxLength) name = name[..Info.MaxLength]; } diff --git a/OpenRA.Mods.Common/Traits/World/CreateMapPlayers.cs b/OpenRA.Mods.Common/Traits/World/CreateMapPlayers.cs index 95f390aded..e268a56f44 100644 --- a/OpenRA.Mods.Common/Traits/World/CreateMapPlayers.cs +++ b/OpenRA.Mods.Common/Traits/World/CreateMapPlayers.cs @@ -55,7 +55,7 @@ namespace OpenRA.Mods.Common.Traits var player = new GameInformation.Player { ClientIndex = client.Index, - Name = Player.ResolvePlayerName(client, lobbyInfo.Clients, bots), + Name = client.Name, IsHuman = client.Bot == null, IsBot = client.Bot != null, FactionName = resolvedFaction.Name, diff --git a/OpenRA.Mods.Common/Traits/World/ValidateOrder.cs b/OpenRA.Mods.Common/Traits/World/ValidateOrder.cs index bb1757f240..9011a5f72b 100644 --- a/OpenRA.Mods.Common/Traits/World/ValidateOrder.cs +++ b/OpenRA.Mods.Common/Traits/World/ValidateOrder.cs @@ -30,7 +30,9 @@ namespace OpenRA.Mods.Common.Traits if (subjectClient == null) { - Log.Write("debug", $"Tick {world.WorldTick}: Order sent to {order.Subject.Owner.PlayerName}: resolved ClientIndex `{subjectClientId}` doesn't exist"); + Log.Write("debug", $"Tick {world.WorldTick}: " + + $"Order sent to {order.Subject.Owner.ResolvedPlayerName}: " + + $"resolved ClientIndex `{subjectClientId}` doesn't exist"); return false; } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs index d714ee4b9a..f8d4ecce5a 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs @@ -81,7 +81,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic TextNotificationsManager.MutedPlayers.Add(c.Index, false); tabCompletion.Commands = chatTraits.OfType().ToArray().SelectMany(x => x.Commands.Keys); - tabCompletion.Names = orderManager.LobbyInfo.Clients.Select(c => c.Name).Distinct().ToList(); + tabCompletion.Names = orderManager.LobbyInfo.Clients.Where(c => !c.IsBot).Select(c => c.Name).Distinct().ToList(); if (logicArgs.TryGetValue("Templates", out var templateIds)) { diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ObserverShroudSelectorLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ObserverShroudSelectorLogic.cs index 48542417ed..d5517ab410 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ObserverShroudSelectorLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ObserverShroudSelectorLogic.cs @@ -65,7 +65,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic public CameraOption(ObserverShroudSelectorLogic logic, Player p) { Player = p; - Label = p.PlayerName; + Label = p.ResolvedPlayerName; Color = p.Color; Faction = p.Faction.InternalName; IsSelected = () => p.World.RenderPlayer == p; diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ObserverStatsLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ObserverStatsLogic.cs index 57bf407a32..87d8273f9d 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ObserverStatsLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ObserverStatsLogic.cs @@ -259,7 +259,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic incomeGraph.GetSeries = () => players.Select(p => new LineGraphSeries( - p.PlayerName, + p.ResolvedPlayerName, p.Color, (p.PlayerActor.TraitOrDefault() ?? new PlayerStatistics(p.PlayerActor)).IncomeSamples.Select(s => (float)s))); } @@ -271,7 +271,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic armyValueGraph.GetSeries = () => players.Select(p => new LineGraphSeries( - p.PlayerName, + p.ResolvedPlayerName, p.Color, (p.PlayerActor.TraitOrDefault() ?? new PlayerStatistics(p.PlayerActor)).ArmySamples.Select(s => (float)s))); } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/WorldTooltipLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/WorldTooltipLogic.cs index 6cde3ad671..bd6644955d 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/WorldTooltipLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/WorldTooltipLogic.cs @@ -112,7 +112,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (showOwner) { flagFaction = o.Faction.InternalName; - ownerName = o.PlayerName; + ownerName = o.ResolvedPlayerName; widget.Bounds.Height = doubleHeight; widget.Bounds.Width = Math.Max(widget.Bounds.Width, owner.Bounds.X + ownerFont.Measure(ownerName).X + label.Bounds.X); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index e4d174ab4e..353ad82e6e 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -853,7 +853,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic while (players.Children.Count > idx) players.RemoveChild(players.Children[idx]); - tabCompletion.Names = orderManager.LobbyInfo.Clients.Select(c => c.Name).Distinct().ToList(); + tabCompletion.Names = orderManager.LobbyInfo.Clients.Where(c => !c.IsBot).Select(c => c.Name).Distinct().ToList(); } void UpdateDiscordStatus() diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs index 8ddca8d643..0f59fdaec8 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs @@ -75,7 +75,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic foreach (var b in map.PlayerActorInfo.TraitInfos()) { var botController = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.IsAdmin); - bots.Add(new SlotDropDownOption(b.Name, + bots.Add(new SlotDropDownOption(TranslationProvider.GetString(b.Name), $"slot_bot {slot.PlayerReference} {botController.Index} {b.Type}", () => client != null && client.Bot == b.Type)); } @@ -422,11 +422,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic public static void SetupNameWidget(Widget parent, Session.Client c, OrderManager orderManager, WorldRenderer worldRenderer) { - var name = parent.Get("NAME"); - name.IsVisible = () => true; - var font = Game.Renderer.Fonts[name.Font]; - var label = WidgetUtils.TruncateText(c.Name, name.Bounds.Width, font); - name.GetText = () => label; + var label = parent.Get("NAME"); + label.IsVisible = () => true; + var font = Game.Renderer.Fonts[label.Font]; + var name = c.IsBot ? TranslationProvider.GetString(c.Name) : c.Name; + var text = WidgetUtils.TruncateText(name, label.Bounds.Width, font); + label.GetText = () => text; SetupProfileWidget(parent, c, orderManager, worldRenderer); } @@ -444,7 +445,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic var closed = TranslationProvider.GetString(Closed); var open = TranslationProvider.GetString(Open); - slot.GetText = () => truncated.Update(c != null ? c.Name : s.Closed ? closed : open); + slot.GetText = () => truncated.Update(c != null ? + c.IsBot ? TranslationProvider.GetString(c.Name) : c.Name + : s.Closed ? closed : open); + slot.OnMouseDown = _ => ShowSlotDropDown(slot, s, c, orderManager, map, modData); // Ensure Name selector (if present) is hidden diff --git a/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs index 763199512c..66344c3500 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs @@ -407,7 +407,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic var ddb = panel.GetOrNull("FLT_PLAYER_DROPDOWNBUTTON"); if (ddb != null) { - var options = replays.SelectMany(r => r.GameInfo.Players.Select(p => p.Name)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + var options = replays.SelectMany(r => r.GameInfo.Players.Select(p => r.GameInfo.ResolvedPlayerName(p))) + .Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + options.Sort(StringComparer.OrdinalIgnoreCase); options.Insert(0, null); // no filter @@ -666,7 +668,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (!string.IsNullOrEmpty(filter.PlayerName)) { var player = replay.GameInfo.Players.FirstOrDefault( - p => string.Equals(filter.PlayerName, p.Name, StringComparison.CurrentCultureIgnoreCase)); + p => string.Equals(filter.PlayerName, replay.GameInfo.ResolvedPlayerName(p), StringComparison.CurrentCultureIgnoreCase)); if (player == null) return false; @@ -751,7 +753,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var label = item.Get("LABEL"); var font = Game.Renderer.Fonts[label.Font]; - var name = WidgetUtils.TruncateText(o.Name, label.Bounds.Width, font); + var name = WidgetUtils.TruncateText(replay.GameInfo.ResolvedPlayerName(o), label.Bounds.Width, font); label.GetText = () => name; label.GetColor = () => color; diff --git a/OpenRA.Mods.Common/Widgets/MapPreviewWidget.cs b/OpenRA.Mods.Common/Widgets/MapPreviewWidget.cs index a8156027d8..8d96a0c9e3 100644 --- a/OpenRA.Mods.Common/Widgets/MapPreviewWidget.cs +++ b/OpenRA.Mods.Common/Widgets/MapPreviewWidget.cs @@ -30,7 +30,7 @@ namespace OpenRA.Mods.Common.Widgets public SpawnOccupant(Session.Client client) { Color = client.Color; - PlayerName = client.Name; + PlayerName = client.IsBot ? TranslationProvider.GetString(client.Name) : client.Name;; Team = client.Team; Faction = client.Faction; SpawnPoint = client.SpawnPoint; @@ -39,7 +39,7 @@ namespace OpenRA.Mods.Common.Widgets public SpawnOccupant(GameInformation.Player player) { Color = player.Color; - PlayerName = player.Name; + PlayerName = player.IsBot ? TranslationProvider.GetString(player.Name) : player.Name; Team = player.Team; Faction = player.FactionId; SpawnPoint = player.SpawnPoint; @@ -48,7 +48,7 @@ namespace OpenRA.Mods.Common.Widgets public SpawnOccupant(GameClient player, bool suppressFaction) { Color = player.Color; - PlayerName = player.Name; + PlayerName = player.IsBot ? TranslationProvider.GetString(player.Name) : player.Name; Team = player.Team; Faction = !suppressFaction ? player.Faction : null; SpawnPoint = player.SpawnPoint; diff --git a/OpenRA.Mods.Common/Widgets/SupportPowerTimerWidget.cs b/OpenRA.Mods.Common/Widgets/SupportPowerTimerWidget.cs index 1b92511efc..9541eeb563 100644 --- a/OpenRA.Mods.Common/Widgets/SupportPowerTimerWidget.cs +++ b/OpenRA.Mods.Common/Widgets/SupportPowerTimerWidget.cs @@ -59,7 +59,7 @@ namespace OpenRA.Mods.Common.Widgets { var self = p.Instances[0].Self; var time = WidgetUtils.FormatTime(p.RemainingTicks, false, self.World.Timestep); - var text = TranslationProvider.GetString(Format, Translation.Arguments("player", self.Owner.PlayerName, "support-power", p.Name, "time", time)); + var text = TranslationProvider.GetString(Format, Translation.Arguments("player", self.Owner.ResolvedPlayerName, "support-power", p.Name, "time", time)); var color = !p.Ready || Game.LocalTick % 50 < 25 ? self.OwnerColor() : Color.White; diff --git a/OpenRA.Mods.Common/Widgets/WidgetUtils.cs b/OpenRA.Mods.Common/Widgets/WidgetUtils.cs index a599d2fbf4..589b8e3352 100644 --- a/OpenRA.Mods.Common/Widgets/WidgetUtils.cs +++ b/OpenRA.Mods.Common/Widgets/WidgetUtils.cs @@ -20,6 +20,15 @@ namespace OpenRA.Mods.Common.Widgets { public static class WidgetUtils { + [TranslationReference] + const string Gone = "label-client-state-disconnected"; + + [TranslationReference] + const string Won = "label-win-state-won"; + + [TranslationReference] + const string Lost = "label-win-state-lost"; + public static string GetStatefulImageName( string baseName, bool disabled = false, bool pressed = false, bool hover = false, bool focused = false) { @@ -324,19 +333,29 @@ namespace OpenRA.Mods.Common.Widgets { var client = p.World.LobbyInfo.ClientWithIndex(p.ClientIndex); var nameFont = Game.Renderer.Fonts[label.Font]; - var name = new CachedTransform<(string Name, WinState WinState, Session.ClientState ClientState), string>(c => + var name = new CachedTransform<(WinState WinState, Session.ClientState ClientState), string>(c => { - var suffix = c.WinState == WinState.Undefined ? "" : " (" + c.WinState + ")"; - if (c.ClientState == Session.ClientState.Disconnected) - suffix = " (Gone)"; + var text = p.ResolvedPlayerName; - return TruncateText(c.Name, label.Bounds.Width - nameFont.Measure(suffix).X, nameFont) + suffix; + var suffix = ""; + if (c.WinState == WinState.Won) + suffix = $" ({TranslationProvider.GetString(Won)})"; + else if (c.WinState == WinState.Lost) + suffix = $" ({TranslationProvider.GetString(Lost)})"; + + if (client.State == Session.ClientState.Disconnected) + suffix = $" ({TranslationProvider.GetString(Gone)})"; + + text += suffix; + + var size = nameFont.Measure(text) - nameFont.Measure(p.ResolvedPlayerName); + return TruncateText(text, label.Bounds.Width - size.X, nameFont); }); label.GetText = () => { var clientState = client != null ? client.State : Session.ClientState.Ready; - return name.Update((p.PlayerName, p.WinState, clientState)); + return name.Update((p.WinState, clientState)); }; } diff --git a/mods/cnc/languages/campaign/en.ftl b/mods/cnc/languages/campaign/en.ftl index 5735ca32a7..37fd87ec2a 100644 --- a/mods/cnc/languages/campaign/en.ftl +++ b/mods/cnc/languages/campaign/en.ftl @@ -8,6 +8,10 @@ options-difficulty = .normal = Normal .hard = Hard +## player +bot-campaign-ai = + .name = Campaign Player AI + ## campaign-maprules.yaml actor-moneycrate-name = Money Crate diff --git a/mods/cnc/languages/rules/en.ftl b/mods/cnc/languages/rules/en.ftl index 00f22bd720..63c5a290e2 100644 --- a/mods/cnc/languages/rules/en.ftl +++ b/mods/cnc/languages/rules/en.ftl @@ -559,3 +559,13 @@ actor-truck = Builds fast Unarmed .name = Supply Truck + +## ai.yaml +bot-cabal = + .name = Cabal + +bot-watson = + .name = Watson + +bot-hal9001 = + .name = HAL 9001 diff --git a/mods/cnc/rules/ai.yaml b/mods/cnc/rules/ai.yaml index 3d97c08375..a2aa944617 100644 --- a/mods/cnc/rules/ai.yaml +++ b/mods/cnc/rules/ai.yaml @@ -1,12 +1,12 @@ Player: ModularBot@Cabal: - Name: Cabal + Name: bot-cabal.name Type: cabal ModularBot@Watson: - Name: Watson + Name: bot-watson.name Type: watson ModularBot@HAL9001: - Name: HAL 9001 + Name: bot-hal9001.name Type: hal9001 GrantConditionOnBotOwner@cabal: Condition: enable-cabal-ai diff --git a/mods/cnc/rules/campaign-maprules.yaml b/mods/cnc/rules/campaign-maprules.yaml index ed475b42d8..91bdc90cb4 100644 --- a/mods/cnc/rules/campaign-maprules.yaml +++ b/mods/cnc/rules/campaign-maprules.yaml @@ -51,7 +51,7 @@ Player: DeveloperMode: CheckboxVisible: False ModularBot@CampaignAI: - Name: Campaign Player AI + Name: bot-campaign-ai.name Type: campaign airstrike.proxy: diff --git a/mods/common/languages/en.ftl b/mods/common/languages/en.ftl index 98c99c2c2b..de537b506a 100644 --- a/mods/common/languages/en.ftl +++ b/mods/common/languages/en.ftl @@ -860,3 +860,14 @@ notification-desync-compare-logs = Out of sync in frame { $frame }. ## SupportPowerTimerWidget support-power-timer = { $player }'s { $support-power }: { $time } + +## WidgetUtils +label-win-state-won = Won +label-win-state-lost = Lost + +## Player +enumerated-bot-name = + { $name } { $number -> + *[zero] {""} + [other] { $number } + } diff --git a/mods/d2k/languages/campaign/en.ftl b/mods/d2k/languages/campaign/en.ftl index 0b5e7b03e9..f25a5dbe9f 100644 --- a/mods/d2k/languages/campaign/en.ftl +++ b/mods/d2k/languages/campaign/en.ftl @@ -8,6 +8,10 @@ options-difficulty = .normal = Normal .hard = Hard +## player +bot-campaign-ai = + .name = Campaign Player AI + ## campaign-tooltips.yaml neutral-prefix = Neutral diff --git a/mods/d2k/languages/rules/en.ftl b/mods/d2k/languages/rules/en.ftl index 9cbbda72ee..a5f95da060 100644 --- a/mods/d2k/languages/rules/en.ftl +++ b/mods/d2k/languages/rules/en.ftl @@ -569,3 +569,13 @@ meta-destroyabletile = meta-destroyedtile = .generic-name = Passage (repairable) .name = Passage (repairable) + +## ai.yaml +bot-omnius = + .name = Omnius + +bot-vidius = + .name = Vidious + +bot-gladius = + .name = Gladius diff --git a/mods/d2k/rules/ai.yaml b/mods/d2k/rules/ai.yaml index a0a1ab54d8..82a7fb290a 100644 --- a/mods/d2k/rules/ai.yaml +++ b/mods/d2k/rules/ai.yaml @@ -1,12 +1,12 @@ Player: ModularBot@Omnius: - Name: Omnius + Name: bot-omnius.name Type: omnius ModularBot@Vidius: - Name: Vidious + Name: bot-vidius.name Type: vidious ModularBot@Gladius: - Name: Gladius + Name: bot-gladius.name Type: gladius GrantConditionOnBotOwner@omnius: Condition: enable-omnius-ai diff --git a/mods/d2k/rules/campaign-rules.yaml b/mods/d2k/rules/campaign-rules.yaml index a695bcf994..24531d8435 100644 --- a/mods/d2k/rules/campaign-rules.yaml +++ b/mods/d2k/rules/campaign-rules.yaml @@ -20,7 +20,7 @@ Player: DeveloperMode: CheckboxVisible: False ModularBot@CampaignAI: - Name: Campaign Player AI + Name: bot-campaign-ai.name Type: campaign World: diff --git a/mods/ra/languages/campaign/en.ftl b/mods/ra/languages/campaign/en.ftl index 1dee249463..47b778cc07 100644 --- a/mods/ra/languages/campaign/en.ftl +++ b/mods/ra/languages/campaign/en.ftl @@ -8,6 +8,10 @@ options-difficulty = .normal = Normal .hard = Hard +## player +bot-campaign-ai = + .name = Campaign Player AI + ## campaign-rules.yaml actor-crate-name = Crate diff --git a/mods/ra/languages/rules/en.ftl b/mods/ra/languages/rules/en.ftl index e696aad80f..1fba7d784c 100644 --- a/mods/ra/languages/rules/en.ftl +++ b/mods/ra/languages/rules/en.ftl @@ -849,3 +849,16 @@ actor-powerproxy-paratroopers = .name = Paratroopers .description = A Badger drops a squad of infantry anywhere on the map. + +## ai.yaml +bot-rush-ai = + .name = Rush AI + +bot-normal-ai = + .name = Normal AI + +bot-turtle-ai = + .name = Turtle AI + +bot-naval-ai = + .name = Naval AI diff --git a/mods/ra/rules/ai.yaml b/mods/ra/rules/ai.yaml index 89364491a2..938e793923 100644 --- a/mods/ra/rules/ai.yaml +++ b/mods/ra/rules/ai.yaml @@ -1,15 +1,15 @@ Player: ModularBot@RushAI: - Name: Rush AI + Name: bot-rush-ai.name Type: rush ModularBot@NormalAI: - Name: Normal AI + Name: bot-normal-ai.name Type: normal ModularBot@TurtleAI: - Name: Turtle AI + Name: bot-turtle-ai.name Type: turtle ModularBot@NavalAI: - Name: Naval AI + Name: bot-naval-ai.name Type: naval GrantConditionOnBotOwner@rush: Condition: enable-rush-ai diff --git a/mods/ra/rules/campaign-rules.yaml b/mods/ra/rules/campaign-rules.yaml index 553e862801..c415a2cbde 100644 --- a/mods/ra/rules/campaign-rules.yaml +++ b/mods/ra/rules/campaign-rules.yaml @@ -29,7 +29,7 @@ Player: DeveloperMode: CheckboxVisible: False ModularBot@CampaignAI: - Name: Campaign Player AI + Name: bot-campaign-ai.name Type: campaign World: diff --git a/mods/ts/languages/rules/en.ftl b/mods/ts/languages/rules/en.ftl index fee22feb2c..dfb12afe13 100644 --- a/mods/ts/languages/rules/en.ftl +++ b/mods/ts/languages/rules/en.ftl @@ -751,3 +751,7 @@ actor-cahosp = .name = Civilian Hospital .captured-desc = Provides infantry with self-healing. .capturable-desc = Capture to enable self-healing for infantry. + +## ai.yaml +bot-test-ai = + .name = Test AI diff --git a/mods/ts/rules/ai.yaml b/mods/ts/rules/ai.yaml index 951e43d797..cb4249df1c 100644 --- a/mods/ts/rules/ai.yaml +++ b/mods/ts/rules/ai.yaml @@ -1,6 +1,6 @@ Player: - ModularBot@TestAI: - Name: Test AI + ModularBot@bot-test-ai: + Name: bot-test-ai.name Type: test GrantConditionOnBotOwner@test: Condition: enable-test-ai