#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.Collections.ObjectModel; using System.IO; using System.Linq; using Eluant; using Eluant.ObjectBinding; using OpenRA.Activities; using OpenRA.Graphics; using OpenRA.Primitives; using OpenRA.Scripting; using OpenRA.Traits; namespace OpenRA { [Flags] public enum SystemActors { Player = 0, EditorPlayer = 1, World = 2, EditorWorld = 4 } public sealed class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding, IEquatable, IDisposable { /// Value used to represent an invalid token. public const int InvalidConditionToken = -1; internal readonly struct SyncHash { public readonly ISync Trait; readonly Func hashFunction; public SyncHash(ISync trait) { Trait = trait; hashFunction = Sync.GetHashFunction(trait); } public int Hash() { return hashFunction(Trait); } } public readonly ActorInfo Info; public readonly World World; public readonly uint ActorID; public Player Owner { get; internal set; } public bool IsInWorld { get; internal set; } public bool WillDispose { get; private set; } public bool Disposed { get; private set; } Activity currentActivity; public Activity CurrentActivity { get => Activity.SkipDoneActivities(currentActivity); private set => currentActivity = value; } public int Generation; public Actor ReplacedByActor; public IEffectiveOwner EffectiveOwner { get; } public IOccupySpace OccupiesSpace { get; } public ITargetable[] Targetables { get; } public IEnumerable EnabledTargetablePositions { get; } readonly ICrushable[] crushables; public ICrushable[] Crushables { get => crushables ?? throw new InvalidOperationException($"Crushables for {Info.Name} are not initialized."); } public bool IsIdle => CurrentActivity == null; public bool IsDead => Disposed || (health != null && health.IsDead); public CPos Location => OccupiesSpace.TopLeft; public WPos CenterPosition => OccupiesSpace.CenterPosition; public WRot Orientation => facing?.Orientation ?? WRot.None; sealed class ConditionState { /// Delegates that have registered to be notified when this condition changes. public readonly List Notifiers = new(); /// Unique integers identifying granted instances of the condition. public readonly HashSet Tokens = new(); } readonly Dictionary conditionStates = new(); /// Each granted condition receives a unique token that is used when revoking. readonly Dictionary conditionTokens = new(); int nextConditionToken = 1; /// Cache of condition -> enabled state for quick evaluation of token counter conditions. readonly Dictionary conditionCache = new(); /// Read-only version of conditionCache that is passed to IConditionConsumers. readonly IReadOnlyDictionary readOnlyConditionCache; internal SyncHash[] SyncHashes { get; } readonly IFacing facing; readonly IHealth health; readonly IResolveOrder[] resolveOrders; readonly IRenderModifier[] renderModifiers; readonly IRender[] renders; readonly IMouseBounds[] mouseBounds; readonly IVisibilityModifier[] visibilityModifiers; readonly IDefaultVisibility defaultVisibility; readonly INotifyBecomingIdle[] becomingIdles; readonly INotifyIdle[] tickIdles; readonly IEnumerable enabledTargetableWorldPositions; bool created; internal Actor(World world, string name, TypeDictionary initDict) { var duplicateInit = initDict.WithInterface().GroupBy(i => i.GetType()) .FirstOrDefault(i => i.Count() > 1); if (duplicateInit != null) throw new InvalidDataException($"Duplicate initializer '{duplicateInit.Key.Name}'"); var init = new ActorInitializer(this, initDict); readOnlyConditionCache = new ReadOnlyDictionary(conditionCache); World = world; ActorID = world.NextAID(); var ownerInit = init.GetOrDefault(); if (ownerInit != null) Owner = ownerInit.Value(world); if (name != null) { name = name.ToLowerInvariant(); if (!world.Map.Rules.Actors.ContainsKey(name)) throw new NotImplementedException("No rules definition for unit " + name); Info = world.Map.Rules.Actors[name]; var resolveOrdersList = new List(); var renderModifiersList = new List(); var rendersList = new List(); var mouseBoundsList = new List(); var visibilityModifiersList = new List(); var becomingIdlesList = new List(); var tickIdlesList = new List(); var targetablesList = new List(); var targetablePositionsList = new List(); var syncHashesList = new List(); var crushablesList = new List(); foreach (var traitInfo in Info.TraitsInConstructOrder()) { var trait = traitInfo.Create(init); AddTrait(trait); // PERF: Cache all these traits as soon as the actor is created. This is a fairly cheap one-off cost per // actor that allows us to provide some fast implementations of commonly used methods that are relied on by // performance-sensitive parts of the core game engine, such as pathfinding, visibility and rendering. // Note: The blocks are required to limit the scope of the t's, so we make an exception to our normal style // rules for spacing in order to keep these assignments compact and readable. { if (trait is IOccupySpace t) OccupiesSpace = t; } { if (trait is IEffectiveOwner t) EffectiveOwner = t; } { if (trait is IFacing t) facing = t; } { if (trait is IHealth t) health = t; } { if (trait is IResolveOrder t) resolveOrdersList.Add(t); } { if (trait is IRenderModifier t) renderModifiersList.Add(t); } { if (trait is IRender t) rendersList.Add(t); } { if (trait is IMouseBounds t) mouseBoundsList.Add(t); } { if (trait is IVisibilityModifier t) visibilityModifiersList.Add(t); } { if (trait is IDefaultVisibility t) defaultVisibility = t; } { if (trait is INotifyBecomingIdle t) becomingIdlesList.Add(t); } { if (trait is INotifyIdle t) tickIdlesList.Add(t); } { if (trait is ITargetable t) targetablesList.Add(t); } { if (trait is ITargetablePositions t) targetablePositionsList.Add(t); } { if (trait is ISync t) syncHashesList.Add(new SyncHash(t)); } { if (trait is ICrushable t) crushablesList.Add(t); } } resolveOrders = resolveOrdersList.ToArray(); renderModifiers = renderModifiersList.ToArray(); renders = rendersList.ToArray(); mouseBounds = mouseBoundsList.ToArray(); visibilityModifiers = visibilityModifiersList.ToArray(); becomingIdles = becomingIdlesList.ToArray(); tickIdles = tickIdlesList.ToArray(); Targetables = targetablesList.ToArray(); var targetablePositions = targetablePositionsList.ToArray(); EnabledTargetablePositions = targetablePositions.Where(Exts.IsTraitEnabled); enabledTargetableWorldPositions = EnabledTargetablePositions.SelectMany(tp => tp.TargetablePositions(this)); SyncHashes = syncHashesList.ToArray(); crushables = crushablesList.ToArray(); } } internal void Initialize(bool addToWorld = true) { created = true; // Make sure traits are usable for condition notifiers foreach (var t in TraitsImplementing()) t.Created(this); var allObserverNotifiers = new HashSet(); foreach (var provider in TraitsImplementing()) { foreach (var variableUser in provider.GetVariableObservers()) { allObserverNotifiers.Add(variableUser.Notifier); foreach (var variable in variableUser.Variables) { var cs = conditionStates.GetOrAdd(variable); cs.Notifiers.Add(variableUser.Notifier); // Initialize conditions that have not yet been granted to 0 // NOTE: Some conditions may have already been granted by INotifyCreated calling GrantCondition, // and we choose to assign the token count to safely cover both cases instead of adding an if branch. conditionCache[variable] = cs.Tokens.Count; } } } // Update all traits with their initial condition state foreach (var notify in allObserverNotifiers) notify(this, readOnlyConditionCache); // TODO: Other traits may need initialization after being notified of initial condition state. // TODO: A post condition initialization notification phase may allow queueing activities instead. // The initial activity should run before any activities queued by INotifyCreated.Created // However, we need to know which traits are enabled (via conditions), so wait for after the calls and insert the activity as the first ICreationActivity creationActivity = null; foreach (var ica in TraitsImplementing()) { if (!ica.IsTraitEnabled()) continue; if (creationActivity != null) throw new InvalidOperationException($"More than one enabled ICreationActivity trait: {creationActivity.GetType().Name} and {ica.GetType().Name}"); var activity = ica.GetCreationActivity(); if (activity == null) continue; creationActivity = ica; activity.Queue(CurrentActivity); CurrentActivity = activity; } if (addToWorld) World.Add(this); } public void Tick() { var wasIdle = IsIdle; CurrentActivity = ActivityUtils.RunActivity(this, CurrentActivity); if (!wasIdle && IsIdle) { foreach (var n in becomingIdles) n.OnBecomingIdle(this); // If IsIdle is true, it means the last CurrentActivity.Tick returned null. // If a next activity has been queued via OnBecomingIdle, we need to start running it now, // to avoid an 'empty' null tick where the actor will (visibly, if moving) do nothing. CurrentActivity = ActivityUtils.RunActivity(this, CurrentActivity); } else if (wasIdle) foreach (var tickIdle in tickIdles) tickIdle.TickIdle(this); } public IEnumerable Render(WorldRenderer wr) { // PERF: Avoid LINQ. var renderables = Renderables(wr); foreach (var modifier in renderModifiers) renderables = modifier.ModifyRender(this, wr, renderables); return renderables; } IEnumerable Renderables(WorldRenderer wr) { // PERF: Avoid LINQ. // Implementations of Render are permitted to return both an eagerly materialized collection or a lazily // generated sequence. // For large amounts of renderables, a lazily generated sequence (e.g. as returned by LINQ, or by using // `yield`) will avoid the need to allocate a large collection. // For small amounts of renderables, allocating a small collection can often be faster and require less // memory than creating the objects needed to represent a sequence. foreach (var render in renders) foreach (var renderable in render.Render(this, wr)) yield return renderable; } public IEnumerable ScreenBounds(WorldRenderer wr) { var bounds = Bounds(wr); foreach (var modifier in renderModifiers) bounds = modifier.ModifyScreenBounds(this, wr, bounds); return bounds; } IEnumerable Bounds(WorldRenderer wr) { // PERF: Avoid LINQ. See comments for Renderables foreach (var render in renders) foreach (var r in render.ScreenBounds(this, wr)) if (!r.IsEmpty) yield return r; } public Polygon MouseBounds(WorldRenderer wr) { foreach (var mb in mouseBounds) { var bounds = mb.MouseoverBounds(this, wr); if (!bounds.IsEmpty) return bounds; } return Polygon.Empty; } public void QueueActivity(bool queued, Activity nextActivity) { if (!queued) CancelActivity(); QueueActivity(nextActivity); } public void QueueActivity(Activity nextActivity) { if (!created) throw new InvalidOperationException("An activity was queued before the actor was created. Queue it inside the INotifyCreated.Created callback instead."); if (CurrentActivity == null) CurrentActivity = nextActivity; else CurrentActivity.Queue(nextActivity); } public void CancelActivity() { CurrentActivity?.Cancel(this); } public override int GetHashCode() { return (int)ActorID; } public override bool Equals(object obj) { return obj is Actor o && Equals(o); } public bool Equals(Actor other) { return ActorID == other.ActorID; } public override string ToString() { // PERF: Avoid format strings. var name = Info.Name + " " + ActorID; if (!IsInWorld) name += " (not in world)"; return name; } public T Trait() { return World.TraitDict.Get(this); } public T TraitOrDefault() { return World.TraitDict.GetOrDefault(this); } public IEnumerable TraitsImplementing() { return World.TraitDict.WithInterface(this); } public void AddTrait(object trait) { World.TraitDict.AddTrait(this, trait); } public void Dispose() { // If CurrentActivity isn't null, run OnActorDisposeOuter in case some cleanups are needed. // This should be done before the FrameEndTask to avoid dependency issues. CurrentActivity?.OnActorDisposeOuter(this); // Allow traits/activities to prevent a race condition when they depend on disposing the actor (e.g. Transforms) WillDispose = true; World.AddFrameEndTask(w => { if (Disposed) return; if (IsInWorld) World.Remove(this); foreach (var t in TraitsImplementing()) t.Disposing(this); World.TraitDict.RemoveActor(this); Disposed = true; luaInterface?.Value.OnActorDestroyed(); }); } public void ResolveOrder(Order order) { foreach (var r in resolveOrders) r.ResolveOrder(this, order); } // TODO: move elsewhere. public void ChangeOwner(Player newOwner) { World.AddFrameEndTask(_ => ChangeOwnerSync(newOwner)); } /// /// Change the actors owner without queuing a FrameEndTask. /// This must only be called from inside an existing FrameEndTask. /// public void ChangeOwnerSync(Player newOwner) { if (Disposed) return; var oldOwner = Owner; var wasInWorld = IsInWorld; // momentarily remove from world so the ownership queries don't get confused if (wasInWorld) World.Remove(this); Owner = newOwner; Generation++; foreach (var t in TraitsImplementing()) t.OnOwnerChanged(this, oldOwner, newOwner); foreach (var t in World.WorldActor.TraitsImplementing()) t.OnOwnerChanged(this, oldOwner, newOwner); if (wasInWorld) World.Add(this); } public DamageState GetDamageState() { if (Disposed) return DamageState.Dead; return (health == null) ? DamageState.Undamaged : health.DamageState; } public void InflictDamage(Actor attacker, Damage damage) { if (Disposed || health == null) return; health.InflictDamage(this, attacker, damage, false); } public void Kill(Actor attacker, BitSet damageTypes = default) { if (Disposed || health == null) return; health.Kill(this, attacker, damageTypes); } public bool CanBeViewedByPlayer(Player player) { // PERF: Avoid LINQ. foreach (var visibilityModifier in visibilityModifiers) if (!visibilityModifier.IsVisible(this, player)) return false; return defaultVisibility.IsVisible(this, player); } public BitSet GetAllTargetTypes() { // PERF: Avoid LINQ. var targetTypes = default(BitSet); foreach (var targetable in Targetables) targetTypes = targetTypes.Union(targetable.TargetTypes); return targetTypes; } public BitSet GetEnabledTargetTypes() { // PERF: Avoid LINQ. var targetTypes = default(BitSet); foreach (var targetable in Targetables) if (targetable.IsTraitEnabled()) targetTypes = targetTypes.Union(targetable.TargetTypes); return targetTypes; } public bool IsTargetableBy(Actor byActor) { // PERF: Avoid LINQ. foreach (var targetable in Targetables) if (targetable.TargetableBy(this, byActor)) return true; return false; } public IEnumerable GetTargetablePositions() { if (EnabledTargetablePositions.Any()) return enabledTargetableWorldPositions; return new[] { CenterPosition }; } #region Conditions void UpdateConditionState(string condition, int token, bool isRevoke) { var conditionState = conditionStates.GetOrAdd(condition); if (isRevoke) conditionState.Tokens.Remove(token); else conditionState.Tokens.Add(token); conditionCache[condition] = conditionState.Tokens.Count; // Conditions may be granted or revoked before the state is initialized. // These notifications will be processed after INotifyCreated.Created. if (created) foreach (var notify in conditionState.Notifiers) notify(this, readOnlyConditionCache); } /// /// Grants a specified condition if it is valid. /// Otherwise, just returns InvalidConditionToken. /// /// The token that is used to revoke this condition. public int GrantCondition(string condition) { if (string.IsNullOrEmpty(condition)) return InvalidConditionToken; var token = nextConditionToken++; conditionTokens.Add(token, condition); UpdateConditionState(condition, token, false); return token; } /// /// Revokes a previously granted condition. /// /// The token ID returned by GrantCondition. /// The invalid token ID. public int RevokeCondition(int token) { if (!conditionTokens.TryGetValue(token, out var condition)) throw new InvalidOperationException($"Attempting to revoke condition with invalid token {token} for {this}."); conditionTokens.Remove(token); UpdateConditionState(condition, token, true); return InvalidConditionToken; } /// Returns whether the specified token is valid for RevokeCondition. public bool TokenValid(int token) { return conditionTokens.ContainsKey(token); } #endregion #region Scripting interface Lazy luaInterface; public void OnScriptBind(ScriptContext context) { luaInterface ??= Exts.Lazy(() => new ScriptActorInterface(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 Actor a) || !right.TryGetClrValue(out Actor b)) return false; return a == b; } public LuaValue ToString(LuaRuntime runtime) { return $"Actor ({this})"; } public bool HasScriptProperty(string name) { return luaInterface.Value.ContainsKey(name); } #endregion } }