1 #region Copyright & License Information 2 /* 3 * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) 4 * This file is part of OpenRA, which is free software. It is made 5 * available to you under the terms of the GNU General Public License 6 * as published by the Free Software Foundation, either version 3 of 7 * the License, or (at your option) any later version. For more 8 * information, see COPYING. 9 */ 10 #endregion 11 12 using System; 13 using System.Collections.Generic; 14 using System.Linq; 15 using Eluant; 16 using Eluant.ObjectBinding; 17 using OpenRA.Activities; 18 using OpenRA.Graphics; 19 using OpenRA.Primitives; 20 using OpenRA.Scripting; 21 using OpenRA.Traits; 22 23 namespace OpenRA 24 { 25 public sealed class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding, IEquatable<Actor>, IDisposable 26 { 27 internal struct SyncHash 28 { 29 public readonly ISync Trait; 30 readonly Func<object, int> hashFunction; SyncHashOpenRA.Actor.SyncHash31 public SyncHash(ISync trait) { Trait = trait; hashFunction = Sync.GetHashFunction(trait); } HashOpenRA.Actor.SyncHash32 public int Hash() { return hashFunction(Trait); } 33 } 34 35 public readonly ActorInfo Info; 36 37 public readonly World World; 38 39 public readonly uint ActorID; 40 41 public Player Owner { get; internal set; } 42 43 public bool IsInWorld { get; internal set; } 44 public bool WillDispose { get; private set; } 45 public bool Disposed { get; private set; } 46 47 Activity currentActivity; 48 public Activity CurrentActivity 49 { 50 get { return Activity.SkipDoneActivities(currentActivity); } 51 private set { currentActivity = value; } 52 } 53 54 public int Generation; 55 public Actor ReplacedByActor; 56 57 public IEffectiveOwner EffectiveOwner { get; private set; } 58 public IOccupySpace OccupiesSpace { get; private set; } 59 public ITargetable[] Targetables { get; private set; } 60 61 public bool IsIdle { get { return CurrentActivity == null; } } 62 public bool IsDead { get { return Disposed || (health != null && health.IsDead); } } 63 64 public CPos Location { get { return OccupiesSpace.TopLeft; } } 65 public WPos CenterPosition { get { return OccupiesSpace.CenterPosition; } } 66 67 public WRot Orientation 68 { 69 get 70 { 71 // TODO: Support non-zero pitch/roll in IFacing (IOrientation?) 72 var facingValue = facing != null ? facing.Facing : 0; 73 return new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(facingValue)); 74 } 75 } 76 77 internal SyncHash[] SyncHashes { get; private set; } 78 79 readonly IFacing facing; 80 readonly IHealth health; 81 readonly IRenderModifier[] renderModifiers; 82 readonly IRender[] renders; 83 readonly IMouseBounds[] mouseBounds; 84 readonly IVisibilityModifier[] visibilityModifiers; 85 readonly IDefaultVisibility defaultVisibility; 86 readonly INotifyBecomingIdle[] becomingIdles; 87 readonly INotifyIdle[] tickIdles; 88 readonly ITargetablePositions[] targetablePositions; 89 WPos[] staticTargetablePositions; 90 bool created; 91 Actor(World world, string name, TypeDictionary initDict)92 internal Actor(World world, string name, TypeDictionary initDict) 93 { 94 var init = new ActorInitializer(this, initDict); 95 96 World = world; 97 ActorID = world.NextAID(); 98 if (initDict.Contains<OwnerInit>()) 99 Owner = init.Get<OwnerInit, Player>(); 100 101 if (name != null) 102 { 103 name = name.ToLowerInvariant(); 104 105 if (!world.Map.Rules.Actors.ContainsKey(name)) 106 throw new NotImplementedException("No rules definition for unit " + name); 107 108 Info = world.Map.Rules.Actors[name]; 109 foreach (var trait in Info.TraitsInConstructOrder()) 110 { 111 AddTrait(trait.Create(init)); 112 113 // Some traits rely on properties provided by IOccupySpace in their initialization, 114 // so we must ready it now, we cannot wait until all traits have finished construction. 115 if (trait is IOccupySpaceInfo) 116 OccupiesSpace = Trait<IOccupySpace>(); 117 } 118 } 119 120 // PERF: Cache all these traits as soon as the actor is created. This is a fairly cheap one-off cost per 121 // actor that allows us to provide some fast implementations of commonly used methods that are relied on by 122 // performance-sensitive parts of the core game engine, such as pathfinding, visibility and rendering. 123 EffectiveOwner = TraitOrDefault<IEffectiveOwner>(); 124 facing = TraitOrDefault<IFacing>(); 125 health = TraitOrDefault<IHealth>(); 126 renderModifiers = TraitsImplementing<IRenderModifier>().ToArray(); 127 renders = TraitsImplementing<IRender>().ToArray(); 128 mouseBounds = TraitsImplementing<IMouseBounds>().ToArray(); 129 visibilityModifiers = TraitsImplementing<IVisibilityModifier>().ToArray(); 130 defaultVisibility = Trait<IDefaultVisibility>(); 131 becomingIdles = TraitsImplementing<INotifyBecomingIdle>().ToArray(); 132 tickIdles = TraitsImplementing<INotifyIdle>().ToArray(); 133 Targetables = TraitsImplementing<ITargetable>().ToArray(); 134 targetablePositions = TraitsImplementing<ITargetablePositions>().ToArray(); 135 world.AddFrameEndTask(w => 136 { 137 // Caching this in a AddFrameEndTask, because trait construction order might cause problems if done directly at creation time. 138 // All actors that can move or teleport should have IPositionable, if not it's pretty safe to assume the actor is completely immobile and 139 // all targetable positions can be cached if all ITargetablePositions have no conditional requirements. 140 if (!Info.HasTraitInfo<IPositionableInfo>() && targetablePositions.Any() && targetablePositions.All(tp => tp.AlwaysEnabled)) 141 staticTargetablePositions = targetablePositions.SelectMany(tp => tp.TargetablePositions(this)).ToArray(); 142 }); 143 144 SyncHashes = TraitsImplementing<ISync>().Select(sync => new SyncHash(sync)).ToArray(); 145 } 146 Created()147 internal void Created() 148 { 149 created = true; 150 151 foreach (var t in TraitsImplementing<INotifyCreated>()) 152 t.Created(this); 153 154 // The initial activity should run before any activities queued by INotifyCreated.Created 155 // However, we need to know which traits are enabled (via conditions), so wait for after the calls and insert the activity as the first 156 ICreationActivity creationActivity = null; 157 foreach (var ica in TraitsImplementing<ICreationActivity>()) 158 { 159 if (!ica.IsTraitEnabled()) 160 continue; 161 162 if (creationActivity != null) 163 throw new InvalidOperationException("More than one enabled ICreationActivity trait: {0} and {1}".F(creationActivity.GetType().Name, ica.GetType().Name)); 164 165 var activity = ica.GetCreationActivity(); 166 if (activity == null) 167 continue; 168 169 creationActivity = ica; 170 171 activity.Queue(CurrentActivity); 172 CurrentActivity = activity; 173 } 174 } 175 Tick()176 public void Tick() 177 { 178 var wasIdle = IsIdle; 179 CurrentActivity = ActivityUtils.RunActivity(this, CurrentActivity); 180 181 if (!wasIdle && IsIdle) 182 { 183 foreach (var n in becomingIdles) 184 n.OnBecomingIdle(this); 185 186 // If IsIdle is true, it means the last CurrentActivity.Tick returned null. 187 // If a next activity has been queued via OnBecomingIdle, we need to start running it now, 188 // to avoid an 'empty' null tick where the actor will (visibly, if moving) do nothing. 189 CurrentActivity = ActivityUtils.RunActivity(this, CurrentActivity); 190 } 191 else if (wasIdle) 192 foreach (var tickIdle in tickIdles) 193 tickIdle.TickIdle(this); 194 } 195 Render(WorldRenderer wr)196 public IEnumerable<IRenderable> Render(WorldRenderer wr) 197 { 198 // PERF: Avoid LINQ. 199 var renderables = Renderables(wr); 200 foreach (var modifier in renderModifiers) 201 renderables = modifier.ModifyRender(this, wr, renderables); 202 return renderables; 203 } 204 Renderables(WorldRenderer wr)205 IEnumerable<IRenderable> Renderables(WorldRenderer wr) 206 { 207 // PERF: Avoid LINQ. 208 // Implementations of Render are permitted to return both an eagerly materialized collection or a lazily 209 // generated sequence. 210 // For large amounts of renderables, a lazily generated sequence (e.g. as returned by LINQ, or by using 211 // `yield`) will avoid the need to allocate a large collection. 212 // For small amounts of renderables, allocating a small collection can often be faster and require less 213 // memory than creating the objects needed to represent a sequence. 214 foreach (var render in renders) 215 foreach (var renderable in render.Render(this, wr)) 216 yield return renderable; 217 } 218 ScreenBounds(WorldRenderer wr)219 public IEnumerable<Rectangle> ScreenBounds(WorldRenderer wr) 220 { 221 var bounds = Bounds(wr); 222 foreach (var modifier in renderModifiers) 223 bounds = modifier.ModifyScreenBounds(this, wr, bounds); 224 return bounds; 225 } 226 Bounds(WorldRenderer wr)227 IEnumerable<Rectangle> Bounds(WorldRenderer wr) 228 { 229 // PERF: Avoid LINQ. See comments for Renderables 230 foreach (var render in renders) 231 foreach (var r in render.ScreenBounds(this, wr)) 232 if (!r.IsEmpty) 233 yield return r; 234 } 235 MouseBounds(WorldRenderer wr)236 public Rectangle MouseBounds(WorldRenderer wr) 237 { 238 foreach (var mb in mouseBounds) 239 { 240 var bounds = mb.MouseoverBounds(this, wr); 241 if (!bounds.IsEmpty) 242 return bounds; 243 } 244 245 return Rectangle.Empty; 246 } 247 QueueActivity(bool queued, Activity nextActivity)248 public void QueueActivity(bool queued, Activity nextActivity) 249 { 250 if (!queued) 251 CancelActivity(); 252 253 QueueActivity(nextActivity); 254 } 255 QueueActivity(Activity nextActivity)256 public void QueueActivity(Activity nextActivity) 257 { 258 if (!created) 259 throw new InvalidOperationException("An activity was queued before the actor was created. Queue it inside the INotifyCreated.Created callback instead."); 260 261 if (CurrentActivity == null) 262 CurrentActivity = nextActivity; 263 else 264 CurrentActivity.Queue(nextActivity); 265 } 266 CancelActivity()267 public void CancelActivity() 268 { 269 if (CurrentActivity != null) 270 CurrentActivity.Cancel(this); 271 } 272 GetHashCode()273 public override int GetHashCode() 274 { 275 return (int)ActorID; 276 } 277 Equals(object obj)278 public override bool Equals(object obj) 279 { 280 var o = obj as Actor; 281 return o != null && Equals(o); 282 } 283 Equals(Actor other)284 public bool Equals(Actor other) 285 { 286 return ActorID == other.ActorID; 287 } 288 ToString()289 public override string ToString() 290 { 291 // PERF: Avoid format strings. 292 var name = Info.Name + " " + ActorID; 293 if (!IsInWorld) 294 name += " (not in world)"; 295 return name; 296 } 297 Trait()298 public T Trait<T>() 299 { 300 return World.TraitDict.Get<T>(this); 301 } 302 TraitOrDefault()303 public T TraitOrDefault<T>() 304 { 305 return World.TraitDict.GetOrDefault<T>(this); 306 } 307 TraitsImplementing()308 public IEnumerable<T> TraitsImplementing<T>() 309 { 310 return World.TraitDict.WithInterface<T>(this); 311 } 312 AddTrait(object trait)313 public void AddTrait(object trait) 314 { 315 World.TraitDict.AddTrait(this, trait); 316 } 317 Dispose()318 public void Dispose() 319 { 320 // If CurrentActivity isn't null, run OnActorDisposeOuter in case some cleanups are needed. 321 // This should be done before the FrameEndTask to avoid dependency issues. 322 if (CurrentActivity != null) 323 CurrentActivity.OnActorDisposeOuter(this); 324 325 // Allow traits/activities to prevent a race condition when they depend on disposing the actor (e.g. Transforms) 326 WillDispose = true; 327 328 World.AddFrameEndTask(w => 329 { 330 if (Disposed) 331 return; 332 333 if (IsInWorld) 334 World.Remove(this); 335 336 foreach (var t in TraitsImplementing<INotifyActorDisposing>()) 337 t.Disposing(this); 338 339 World.TraitDict.RemoveActor(this); 340 Disposed = true; 341 342 if (luaInterface != null) 343 luaInterface.Value.OnActorDestroyed(); 344 }); 345 } 346 347 // TODO: move elsewhere. ChangeOwner(Player newOwner)348 public void ChangeOwner(Player newOwner) 349 { 350 World.AddFrameEndTask(_ => ChangeOwnerSync(newOwner)); 351 } 352 353 /// <summary> 354 /// Change the actors owner without queuing a FrameEndTask. 355 /// This must only be called from inside an existing FrameEndTask. 356 /// </summary> ChangeOwnerSync(Player newOwner)357 public void ChangeOwnerSync(Player newOwner) 358 { 359 if (Disposed) 360 return; 361 362 var oldOwner = Owner; 363 var wasInWorld = IsInWorld; 364 365 // momentarily remove from world so the ownership queries don't get confused 366 if (wasInWorld) 367 World.Remove(this); 368 369 Owner = newOwner; 370 Generation++; 371 372 foreach (var t in TraitsImplementing<INotifyOwnerChanged>()) 373 t.OnOwnerChanged(this, oldOwner, newOwner); 374 375 foreach (var t in World.WorldActor.TraitsImplementing<INotifyOwnerChanged>()) 376 t.OnOwnerChanged(this, oldOwner, newOwner); 377 378 if (wasInWorld) 379 World.Add(this); 380 } 381 GetDamageState()382 public DamageState GetDamageState() 383 { 384 if (Disposed) 385 return DamageState.Dead; 386 387 return (health == null) ? DamageState.Undamaged : health.DamageState; 388 } 389 InflictDamage(Actor attacker, Damage damage)390 public void InflictDamage(Actor attacker, Damage damage) 391 { 392 if (Disposed || health == null) 393 return; 394 395 health.InflictDamage(this, attacker, damage, false); 396 } 397 Kill(Actor attacker, BitSet<DamageType> damageTypes = default(BitSet<DamageType>))398 public void Kill(Actor attacker, BitSet<DamageType> damageTypes = default(BitSet<DamageType>)) 399 { 400 if (Disposed || health == null) 401 return; 402 403 health.Kill(this, attacker, damageTypes); 404 } 405 CanBeViewedByPlayer(Player player)406 public bool CanBeViewedByPlayer(Player player) 407 { 408 // PERF: Avoid LINQ. 409 foreach (var visibilityModifier in visibilityModifiers) 410 if (!visibilityModifier.IsVisible(this, player)) 411 return false; 412 413 return defaultVisibility.IsVisible(this, player); 414 } 415 GetAllTargetTypes()416 public BitSet<TargetableType> GetAllTargetTypes() 417 { 418 // PERF: Avoid LINQ. 419 var targetTypes = default(BitSet<TargetableType>); 420 foreach (var targetable in Targetables) 421 targetTypes = targetTypes.Union(targetable.TargetTypes); 422 return targetTypes; 423 } 424 GetEnabledTargetTypes()425 public BitSet<TargetableType> GetEnabledTargetTypes() 426 { 427 // PERF: Avoid LINQ. 428 var targetTypes = default(BitSet<TargetableType>); 429 foreach (var targetable in Targetables) 430 if (targetable.IsTraitEnabled()) 431 targetTypes = targetTypes.Union(targetable.TargetTypes); 432 return targetTypes; 433 } 434 IsTargetableBy(Actor byActor)435 public bool IsTargetableBy(Actor byActor) 436 { 437 // PERF: Avoid LINQ. 438 foreach (var targetable in Targetables) 439 if (targetable.IsTraitEnabled() && targetable.TargetableBy(this, byActor)) 440 return true; 441 442 return false; 443 } 444 GetTargetablePositions()445 public IEnumerable<WPos> GetTargetablePositions() 446 { 447 if (staticTargetablePositions != null) 448 return staticTargetablePositions; 449 450 var enabledTargetablePositionTraits = targetablePositions.Where(Exts.IsTraitEnabled); 451 if (enabledTargetablePositionTraits.Any()) 452 return enabledTargetablePositionTraits.SelectMany(tp => tp.TargetablePositions(this)); 453 454 return new[] { CenterPosition }; 455 } 456 457 #region Scripting interface 458 459 Lazy<ScriptActorInterface> luaInterface; OnScriptBind(ScriptContext context)460 public void OnScriptBind(ScriptContext context) 461 { 462 if (luaInterface == null) 463 luaInterface = Exts.Lazy(() => new ScriptActorInterface(context, this)); 464 } 465 466 public LuaValue this[LuaRuntime runtime, LuaValue keyValue] 467 { 468 get { return luaInterface.Value[runtime, keyValue]; } 469 set { luaInterface.Value[runtime, keyValue] = value; } 470 } 471 Equals(LuaRuntime runtime, LuaValue left, LuaValue right)472 public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) 473 { 474 Actor a, b; 475 if (!left.TryGetClrValue(out a) || !right.TryGetClrValue(out b)) 476 return false; 477 478 return a == b; 479 } 480 ToString(LuaRuntime runtime)481 public LuaValue ToString(LuaRuntime runtime) 482 { 483 return "Actor ({0})".F(this); 484 } 485 HasScriptProperty(string name)486 public bool HasScriptProperty(string name) 487 { 488 return luaInterface.Value.ContainsKey(name); 489 } 490 491 #endregion 492 } 493 } 494