#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Linq; using Eluant; using Eluant.ObjectBinding; using OpenRA.Graphics; using OpenRA.Network; using OpenRA.Primitives; using OpenRA.Scripting; using OpenRA.Support; using OpenRA.Traits; using OpenRA.Widgets; namespace OpenRA { [Flags] public enum PowerState { Normal = 1, Low = 2, Critical = 4 } public enum WinState { Undefined, Won, Lost } public class PlayerBitMask { } public class Player : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding { [FluentReference("name", "number")] const string EnumeratedBotName = "enumerated-bot-name"; public readonly Actor PlayerActor; public readonly string PlayerName; public readonly string InternalName; public readonly FactionInfo Faction; public readonly bool NonCombatant = false; public readonly bool Playable = true; public readonly int ClientIndex; public readonly CPos HomeLocation; public readonly int Handicap; public readonly PlayerReference PlayerReference; public readonly bool IsBot; public readonly string BotType; public readonly Shroud Shroud; public readonly FrozenActorLayer FrozenActorLayer; readonly Color color; /// Returns player color with relationship colors applied. public Color Color { get; private set; } /// The faction (including Random, etc.) that was selected in the lobby. public readonly FactionInfo DisplayFaction; /// The spawn point index that was assigned for client-based players. public readonly int SpawnPoint; /// The spawn point index (including 0 for Random) that was selected in the lobby for client-based players. public readonly int DisplaySpawnPoint; public WinState WinState = WinState.Undefined; public bool HasObjectives = false; // Players in mission maps must not leave the player view public bool Spectating => !inMissionMap && (spectating || WinState != WinState.Undefined); public World World { get; } readonly bool inMissionMap; readonly bool spectating; 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; public LongBitSet AlliedPlayersMask = default; public LongBitSet EnemyPlayersMask = default; public bool UnlockedRenderPlayer { get { if (unlockRenderPlayer.Any(x => x.RenderPlayerUnlocked)) return true; return WinState != WinState.Undefined && !inMissionMap; } } /// 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) { var selectableFactions = factionInfos .Where(f => !requireSelectable || f.Selectable) .ToList(); var selected = selectableFactions.FirstOrDefault(f => f.InternalName == factionName) ?? selectableFactions.Random(playerRandom); // Don't loop infinite for (var i = 0; i <= 10 && selected.RandomFactionMembers.Count > 0; i++) { var faction = selected.RandomFactionMembers.Random(playerRandom); selected = selectableFactions.FirstOrDefault(f => f.InternalName == faction); if (selected == null) throw new YamlException($"Unknown faction: {faction}"); } return selected; } static FactionInfo ResolveFaction(World world, string factionName, MersenneTwister playerRandom, bool requireSelectable) { var factionInfos = world.Map.Rules.Actors[SystemActors.World].TraitInfos(); return ResolveFaction(factionName, factionInfos, playerRandom, requireSelectable); } static FactionInfo ResolveDisplayFaction(World world, string factionName) { var factions = world.Map.Rules.Actors[SystemActors.World].TraitInfos(); return factions.FirstOrDefault(f => f.InternalName == factionName) ?? factions.First(); } public Player(World world, Session.Client client, PlayerReference pr, MersenneTwister playerRandom) { World = world; InternalName = pr.Name; 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) { ClientIndex = client.Index; color = client.Color; Color = color; PlayerName = client.Name; BotType = client.Bot; Faction = ResolveFaction(world, client.Faction, playerRandom, !pr.LockFaction); DisplayFaction = ResolveDisplayFaction(world, client.Faction); var assignSpawnPoints = world.WorldActor.TraitOrDefault(); HomeLocation = assignSpawnPoints?.AssignHomeLocation(world, client, playerRandom) ?? pr.HomeLocation; SpawnPoint = assignSpawnPoints?.SpawnPointForPlayer(this) ?? client.SpawnPoint; DisplaySpawnPoint = client.SpawnPoint; Handicap = client.Handicap; } else { // Map player ClientIndex = world.LobbyInfo.Clients.FirstOrDefault(c => c.IsAdmin)?.Index ?? 0; // Owned by the host (TODO: fix this) color = pr.Color; Color = pr.Color; PlayerName = pr.Name; NonCombatant = pr.NonCombatant; Playable = pr.Playable; spectating = pr.Spectating; BotType = pr.Bot; Faction = ResolveFaction(world, pr.Faction, playerRandom, false); DisplayFaction = ResolveDisplayFaction(world, pr.Faction); HomeLocation = pr.HomeLocation; SpawnPoint = DisplaySpawnPoint = 0; Handicap = pr.Handicap; } if (!spectating) PlayerMask = new LongBitSet(InternalName); // Set this property before running any Created callbacks on the player actor IsBot = BotType != null; // Special case handling is required for the Player actor: // Since Actor.Created would be called before PlayerActor is assigned here // querying player traits in INotifyCreated.Created would crash. // Therefore assign the uninitialized actor and run the Created callbacks // by calling Initialize ourselves. var playerActorType = world.Type == WorldType.Editor ? SystemActors.EditorPlayer : SystemActors.Player; PlayerActor = new Actor(world, playerActorType.ToString(), new TypeDictionary { new OwnerInit(this) }); PlayerActor.Initialize(true); Shroud = PlayerActor.Trait(); FrozenActorLayer = PlayerActor.TraitOrDefault(); // Enable the bot logic on the host if (IsBot && Game.IsHost) { var logic = PlayerActor.TraitsImplementing().FirstOrDefault(b => b.Info.Type == BotType); if (logic == null) Log.Write("debug", $"Invalid bot type: {BotType}"); else logic.Activate(this); } unlockRenderPlayer = PlayerActor.TraitsImplementing().ToArray(); notifyDisconnected = PlayerActor.TraitsImplementing().ToArray(); } public override string ToString() { 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 FluentProvider.GetString(EnumeratedBotName, FluentBundle.Arguments("name", FluentProvider.GetString(botInfo.Name), "number", botsOfSameType.IndexOf(this) + 1)); } return PlayerName; } public PlayerRelationship RelationshipWith(Player other) { if (this == other) return PlayerRelationship.Ally; // Observers are considered allies to active combatants if (other == null || other.Spectating) return NonCombatant ? PlayerRelationship.Neutral : PlayerRelationship.Ally; if (AlliedPlayersMask.Overlaps(other.PlayerMask)) return PlayerRelationship.Ally; if (EnemyPlayersMask.Overlaps(other.PlayerMask)) return PlayerRelationship.Enemy; return PlayerRelationship.Neutral; } /// Returns true if player is null. public bool IsAlliedWith(Player p) { return RelationshipWith(p) == PlayerRelationship.Ally; } /// Returns , ignoring player relationship colors. public static Color GetColor(Player p) => p.color; public static void SetupRelationshipColors(Player[] players, Player viewer, WorldRenderer worldRenderer, bool firstRun) { foreach (var p in players) { p.Color = PlayerRelationshipColor(p, viewer); worldRenderer.UpdatePalettesForPlayer(p.InternalName, p.Color, !firstRun); } } public static Color PlayerRelationshipColor(Player player, Player viewer) { if (!Game.Settings.Game.UsePlayerStanceColors || viewer == null || viewer.Spectating) return player.color; if (viewer == player) return ChromeMetrics.Get("PlayerStanceColorSelf"); if (player.IsAlliedWith(viewer)) return ChromeMetrics.Get("PlayerStanceColorAllies"); if (player.NonCombatant) return ChromeMetrics.Get("PlayerStanceColorNeutrals"); return ChromeMetrics.Get("PlayerStanceColorEnemies"); } internal void PlayerDisconnected(Player p) { foreach (var np in notifyDisconnected) np.PlayerDisconnected(PlayerActor, p); } #region Scripting interface Lazy luaInterface; public void OnScriptBind(ScriptContext context) { luaInterface ??= Exts.Lazy(() => new ScriptPlayerInterface(context, this)); } public LuaValue this[LuaRuntime runtime, LuaValue keyValue] { get => luaInterface.Value[runtime, keyValue]; set => luaInterface.Value[runtime, keyValue] = value; } public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) { if (!left.TryGetClrValue(out Player a) || !right.TryGetClrValue(out Player b)) return false; return a == b; } public LuaValue ToString(LuaRuntime runtime) { return $"Player ({ResolvedPlayerName})"; } #endregion } }