From fa911f2d852b72763c23cf4933e290eabd231f07 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 13 Oct 2026 02:13:43 +0200 Subject: [PATCH] Zone.Entities read-only outside Zone: Admit and Remove are the only membership writers Entities is a MemberView with no mutating member; Admit installs the death watch with the membership and refuses a member already present, Remove takes it away with the membership. The Reset and duplicate-Add bypass paths of the death watch cannot be spelled any more. Tests move to Admit/Remove; Admit also activates, so the explicit Activate calls that followed a raw Add are dropped. Unity call sites route through Zone.Remove. Builds: unverified at this commit (verified in the job at the head). Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Editor/ImpactPresenterSmoke.cs | 6 +- Assets/Scripts/Gameplay/ActionGameManager.cs | 2 +- Assets/Scripts/Gameplay/GridObject.cs | 51 ------------- Assets/Scripts/Gameplay/GridObject.cs.meta | 3 - Assets/Scripts/Gameplay/TimedDestroy.cs | 19 ----- Assets/Scripts/Gameplay/TimedDestroy.cs.meta | 3 - Assets/Scripts/ServerShared/Entity.cs | 2 +- Assets/Scripts/ServerShared/Zone.cs | 73 +++++++++++-------- tests/Aetheria.Shared.Tests/BrownoutTests.cs | 3 +- .../ConditionRatioTests.cs | 3 +- .../Aetheria.Shared.Tests/ConsumableTests.cs | 3 +- .../ContactRevealTests.cs | 34 ++++----- tests/Aetheria.Shared.Tests/ContactTests.cs | 3 +- .../FireAuthorityTests.cs | 8 +- .../FireControlCut11Tests.cs | 10 +-- .../FireControlCut123Tests.cs | 6 +- .../FireControlCut124Tests.cs | 21 +++--- .../FireControlCut12Tests.cs | 6 +- .../FireControlCut4Tests.cs | 11 +-- .../FireControlCut5Tests.cs | 24 ++---- .../FireControlCut6Tests.cs | 6 +- .../FireControlCut6cTests.cs | 9 +-- .../FireControlCut6dTests.cs | 6 +- .../FireControlCut7Tests.cs | 6 +- .../FireControlCut9Tests.cs | 20 ++--- .../FireControlPerWeaponTests.cs | 6 +- .../Aetheria.Shared.Tests/FireControlTests.cs | 6 +- .../FloatingBodyTests.cs | 3 +- .../IffAndCombatTests.cs | 8 +- .../ImpactFeedbackTests.cs | 5 +- .../InputCapacitorTests.cs | 3 +- .../Aetheria.Shared.Tests/LockWarningTests.cs | 3 +- tests/Aetheria.Shared.Tests/LootTests.cs | 52 +++++++++---- tests/Aetheria.Shared.Tests/MineTests.cs | 6 +- .../Aetheria.Shared.Tests/MiningCut3Tests.cs | 6 +- .../Aetheria.Shared.Tests/MiningIndexTests.cs | 9 +-- .../MiningTargetQueriesTests.cs | 11 ++- .../MunitionPartsTests.cs | 3 +- tests/Aetheria.Shared.Tests/PaintModeTests.cs | 13 ++-- tests/Aetheria.Shared.Tests/PowerBusTests.cs | 3 +- .../Aetheria.Shared.Tests/PowerCurveTests.cs | 6 +- tests/Aetheria.Shared.Tests/PowerTierTests.cs | 3 +- .../RestoredHullsTests.cs | 6 +- .../ShieldReserveTests.cs | 3 +- tests/Aetheria.Shared.Tests/SignatureTests.cs | 3 +- tests/Aetheria.Shared.Tests/SolutionTests.cs | 3 +- .../StatResolverTests.cs | 9 +-- .../TargetingSystemTests.cs | 9 +-- .../ThrottleLockTests.cs | 3 +- .../Aetheria.Shared.Tests/VapourCloudTests.cs | 6 +- 50 files changed, 206 insertions(+), 320 deletions(-) delete mode 100644 Assets/Scripts/Gameplay/GridObject.cs delete mode 100644 Assets/Scripts/Gameplay/GridObject.cs.meta delete mode 100644 Assets/Scripts/Gameplay/TimedDestroy.cs delete mode 100644 Assets/Scripts/Gameplay/TimedDestroy.cs.meta diff --git a/Assets/Scripts/Editor/ImpactPresenterSmoke.cs b/Assets/Scripts/Editor/ImpactPresenterSmoke.cs index 42dc9d56d..b5f6f9714 100644 --- a/Assets/Scripts/Editor/ImpactPresenterSmoke.cs +++ b/Assets/Scripts/Editor/ImpactPresenterSmoke.cs @@ -64,10 +64,8 @@ public static class ImpactPresenterSmoke var hullRef = items.ItemData.RefOf(items.ItemData.GetByName("Smoke Hull")); var ship = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 1 }, new EntitySettings()); var other = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 2 }, new EntitySettings()); - zone.Entities.Add(ship); - zone.Entities.Add(other); - ship.Activate(); - other.Activate(); + zone.Admit(ship, piloted: false); + zone.Admit(other, piloted: false); // The scene's side: the tuning object with a pooled effect, and the game manager that names the player's ship. var feedbackObject = new GameObject("Smoke Impact Feedback"); diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 3fae48b73..aacf8b2b3 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -739,7 +739,7 @@ public class ActionGameManager : MonoBehaviour if (CurrentEntity != null) { CurrentEntity.Deactivate(); - CurrentEntity.Zone.Entities.Remove(CurrentEntity); + CurrentEntity.Zone.Remove(CurrentEntity); CurrentEntity.Zone = Zone; Zone.Admit(CurrentEntity, piloted: false); } diff --git a/Assets/Scripts/Gameplay/GridObject.cs b/Assets/Scripts/Gameplay/GridObject.cs deleted file mode 100644 index 82d64199a..000000000 --- a/Assets/Scripts/Gameplay/GridObject.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using CultMath; -using CultMath.UnityBridge; -using UnityEngine; -using static CultMath.math; -using Random = UnityEngine.Random; - -public class GridObject : MonoBehaviour -{ - public float RotationSpeed; - public float GridOffset; - public float GridAttraction; - public float Gravity; - public float Drag = .1f; - public float LaunchDrag; - - private float2 _selfVelocity; - private float _timeOffset; - - public Zone Zone { get; set; } - public Vector3 Velocity { get; set; } - - private void Start() - { - _timeOffset = Random.value * 100; - } - - void Update() - { - var t = transform; - t.localRotation = Quaternion.Euler(sin(Time.time - _timeOffset * RotationSpeed) * 90, 0, cos(Time.time - _timeOffset * RotationSpeed) * 90); - - if (Zone == null) return; - - var position = t.position; - var gridHeight = Zone.GetHeight(position.Flatland().ToCultMath()) + GridOffset; - Velocity += Vector3.up * (sign(gridHeight - position.y) * GridAttraction * Time.deltaTime); - Velocity *= max(0, 1 - LaunchDrag * Time.deltaTime); - var normal = Zone.GetNormal(position.Flatland().ToCultMath()); - var force = new float2(normal.x, normal.z); - var forceMagnitude = lengthsq(force); - if (forceMagnitude > .001f) - { - var fa = 1 / (1 - forceMagnitude) - 1; - _selfVelocity += normalize(force) * Zone.Settings.GravityStrength * fa * Gravity; - } - - _selfVelocity *= max(0, 1 - Drag * Time.deltaTime); - t.position = position + (Velocity + new Vector3(_selfVelocity.x, 0, _selfVelocity.y)) * Time.deltaTime; - } -} \ No newline at end of file diff --git a/Assets/Scripts/Gameplay/GridObject.cs.meta b/Assets/Scripts/Gameplay/GridObject.cs.meta deleted file mode 100644 index 335d81a85..000000000 --- a/Assets/Scripts/Gameplay/GridObject.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 737f47c158214f0995ba2835f040ed96 -timeCreated: 1618441439 \ No newline at end of file diff --git a/Assets/Scripts/Gameplay/TimedDestroy.cs b/Assets/Scripts/Gameplay/TimedDestroy.cs deleted file mode 100644 index fbbd86f8c..000000000 --- a/Assets/Scripts/Gameplay/TimedDestroy.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using UnityEngine; - -public class TimedDestroy : MonoBehaviour -{ - public float Duration; - - private float _startTime; - - private void Start() - { - _startTime = Time.time; - } - - public void Update() - { - if(Time.time - _startTime > Duration) Destroy(gameObject); - } -} \ No newline at end of file diff --git a/Assets/Scripts/Gameplay/TimedDestroy.cs.meta b/Assets/Scripts/Gameplay/TimedDestroy.cs.meta deleted file mode 100644 index 2f6bac661..000000000 --- a/Assets/Scripts/Gameplay/TimedDestroy.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 5d5539a23b814254b038a16f32f1d8a9 -timeCreated: 1618453298 \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 2716cb7c3..dc36a9622 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -1507,7 +1507,7 @@ public abstract class Entity { bay.DockedShip = ship; ship.SetParent(this); - Zone.Entities.Remove(ship); + Zone.Remove(ship); ship.Deactivate(); ship.Docked.OnNext(this); } diff --git a/Assets/Scripts/ServerShared/Zone.cs b/Assets/Scripts/ServerShared/Zone.cs index 5b4a5ccc9..a21329e0d 100644 --- a/Assets/Scripts/ServerShared/Zone.cs +++ b/Assets/Scripts/ServerShared/Zone.cs @@ -15,7 +15,29 @@ using float2 = CultMath.float2; public class Zone { public Action Log; - public ReactiveCollection Entities = new ReactiveCollection(); + + // Membership, in admission order. Admit and Remove are the only writers; everyone else reads this view, which has + // no mutating member, so the death watch below follows membership by construction. + private readonly ReactiveCollection _entities = new ReactiveCollection(); + public MemberView Entities { get; } + + public sealed class MemberView : IReadOnlyReactiveCollection + { + private readonly ReactiveCollection _members; + internal MemberView(ReactiveCollection members) => _members = members; + public int Count => _members.Count; + public Entity this[int index] => _members[index]; + public int IndexOf(Entity entity) => _members.IndexOf(entity); + public bool Contains(Entity entity) => _members.Contains(entity); + public IEnumerator GetEnumerator() => _members.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public UniRx.IObservable> ObserveAdd() => _members.ObserveAdd(); + public UniRx.IObservable ObserveCountChanged(bool notifyCurrentCount = false) => _members.ObserveCountChanged(notifyCurrentCount); + public UniRx.IObservable> ObserveMove() => _members.ObserveMove(); + public UniRx.IObservable> ObserveRemove() => _members.ObserveRemove(); + public UniRx.IObservable> ObserveReplace() => _members.ObserveReplace(); + public UniRx.IObservable ObserveReset() => _members.ObserveReset(); + } // Laid mines, in lay order. Zone owns their existence: Lay is the one adder, the mine step the one remover, // and every removal is a blast. Runtime state, never saved. @@ -105,30 +127,13 @@ public class Zone // The death ordinal of the loot dice: the nth death in this zone rolls on SimulationDice.For(CombatSeed, LootStream, n). private uint _deaths; - // One death subscription per member entity, owned here: a membership record and its subscription are created - // together, and only when the entity is not already a member. Leaving Entities (Remove) or resetting it (Clear) - // disposes it; nothing else touches it. + // One death subscription per member entity: Admit creates it together with the membership, Remove disposes it + // together with the membership, and nothing else touches either. private readonly Dictionary _deathWatches = new Dictionary(); - private void WatchDeath(Entity entity) - { - if (_deathWatches.ContainsKey(entity)) return; - _deathWatches.Add(entity, entity.Death.Subscribe(_ => { DropLoot(entity); Entities.Remove(entity); entity.Deactivate(); })); - } - - private void UnwatchDeath(Entity entity) - { - if (_deathWatches.Remove(entity, out var watch)) watch.Dispose(); - } - - private void UnwatchAll() - { - foreach (var watch in _deathWatches.Values) watch.Dispose(); - _deathWatches.Clear(); - } - public Zone(ItemManager itemManager, PlanetSettings settings, ZonePack pack, GalaxyZone galaxyZone, Galaxy galaxy) { + Entities = new MemberView(_entities); _time = pack.Time; GalaxyZone = galaxyZone; Galaxy = galaxy; @@ -144,11 +149,8 @@ public class Zone // ObserveAdd fires for all of them. Forbidden writer: no presentation may remove an entity from Zone. // The death watch lives exactly as long as the entity's membership in this zone: installed on admission, // disposed on removal (death, dock, jump), so an entity admitted again never holds a second one. - Entities.ObserveAdd().Subscribe(add => WatchDeath(add.Value)); - Entities.ObserveRemove().Subscribe(remove => UnwatchDeath(remove.Value)); - Entities.ObserveReset().Subscribe(_ => UnwatchAll()); // The one record of a removal for observers (ruling reveal-ends-on-perception): read by the next Sense pass. - Entities.ObserveRemove().Subscribe(remove => _departures.Add(new Departure(remove.Value, remove.Value.Position.xz, + _entities.ObserveRemove().Subscribe(remove => _departures.Add(new Departure(remove.Value, remove.Value.Position.xz, remove.Value.Visibility - remove.Value.SignatureOn(SignatureChannel.Emission)))); Targets.Add(new EntityTargets()); @@ -205,17 +207,30 @@ public class Zone // TODO: Associate planets with stored entities for planetary colonies } - // The one admission into this zone (docs/scenarios-cut.md, 2.2): the only writer of Entities.Add and the only + // The one admission into this zone (docs/scenarios-cut.md, 2.2): the only writer of membership and the only // creator of agents. Construction, new-run staging (RunStart), warp arrival, undock and the console's spawned // turret all join through here. A piloted entity gets the zone's Minion; an unpiloted one gets no agent at all. - // Only a ship can be piloted. The Death subscription above covers every join, whichever caller admits it. + // Only a ship can be piloted. The death watch is installed here, for every join, whichever caller admits it. + // An entity that is already a member is refused: it would hold a second watch and a second agent. public void Admit(Entity entity, bool piloted) { - Entities.Add(entity); + if (_entities.Contains(entity)) throw new InvalidOperationException("entity: already a member of this zone"); + _entities.Add(entity); + _deathWatches.Add(entity, entity.Death.Subscribe(_ => { DropLoot(entity); Remove(entity); entity.Deactivate(); })); entity.Activate(); if (piloted) Agents.Add(CreateAgent((Ship) entity)); } + // The one removal from this zone (death, dock, jump): membership and its death watch leave together. False, and + // nothing changes, when the entity is not a member. The caller deactivates; Remove does not. + public bool Remove(Entity entity) + { + if (!_entities.Remove(entity)) return false; + _deathWatches.Remove(entity, out var watch); + watch.Dispose(); + return true; + } + private Agent CreateAgent(Ship ship) { var agent = new Minion(ship); @@ -231,7 +246,7 @@ public class Zone { Radius = Pack.Radius, Mass = Pack.Mass, - Entities = Entities.Select(EntitySerializer.Pack).ToList(), + Entities = _entities.Select(EntitySerializer.Pack).ToList(), Orbits = Orbits.Keys.Select(key => new CultRecordRef(key)).ToList(), Planets = Planets.Keys.Select(key => new CultRecordRef(key)).ToList(), Time = _time, diff --git a/tests/Aetheria.Shared.Tests/BrownoutTests.cs b/tests/Aetheria.Shared.Tests/BrownoutTests.cs index a262d41e3..3fe4bfb51 100644 --- a/tests/Aetheria.Shared.Tests/BrownoutTests.cs +++ b/tests/Aetheria.Shared.Tests/BrownoutTests.cs @@ -113,11 +113,10 @@ public sealed class BrownoutTests : IDisposable // not flight, and a lone off-centre thruster could not fire without turning. Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Consumer")), new int2(2, 1))); - zone.Entities.Add(ship); + zone.Admit(ship, piloted: false); // Ship.Turn stays 0, so Ship.Update commands no rotation: no incidental torque thrust from the // ship trying to turn. Aim steers nothing, so this one is only a plausible aim. ship.Aim = float3(0, 0, 1); - ship.Activate(); return ship; } diff --git a/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs b/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs index 83da4a488..e294b942f 100644 --- a/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs +++ b/tests/Aetheria.Shared.Tests/ConditionRatioTests.cs @@ -96,9 +96,8 @@ public sealed class ConditionRatioTests : IDisposable // not flight, and a lone off-centre thruster could not fire without turning. Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Consumer")), new int2(2, 1))); - zone.Entities.Add(ship); + zone.Admit(ship, piloted: false); ship.Aim = float3(0, 0, 1); // Turn stays 0, as in BrownoutTests - ship.Activate(); consumer = ship.Equipment.Single(e => e.Data.Name == "Consumer"); Assert.True(ship.GetBehavior().Torque == 0f, "the lone thruster turns nothing"); diff --git a/tests/Aetheria.Shared.Tests/ConsumableTests.cs b/tests/Aetheria.Shared.Tests/ConsumableTests.cs index b5717e9d6..7ba593e6e 100644 --- a/tests/Aetheria.Shared.Tests/ConsumableTests.cs +++ b/tests/Aetheria.Shared.Tests/ConsumableTests.cs @@ -83,8 +83,7 @@ public sealed class ConsumableTests : IDisposable var ship = new Ship(items, zone, Gear(cache.Get(hullRef)), new EntitySettings()); Assert.True(ship.TryEquip(Gear(cache.Get(engineRef)), EngineCell)); Assert.True(ship.TryEquip(Gear(cache.Get(holdRef)), HoldCell)); - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); var data = cache.Get(consumableRef); return new Rig { diff --git a/tests/Aetheria.Shared.Tests/ContactRevealTests.cs b/tests/Aetheria.Shared.Tests/ContactRevealTests.cs index 0273bcdc4..9eeb90033 100644 --- a/tests/Aetheria.Shared.Tests/ContactRevealTests.cs +++ b/tests/Aetheria.Shared.Tests/ContactRevealTests.cs @@ -187,8 +187,7 @@ public sealed class ContactRevealTests : IDisposable Assert.True(ship.TryFindSpace(item, out var at)); Assert.True(ship.TryEquip(item, at)); } - _zone.Entities.Add(ship); - ship.Activate(); + _zone.Admit(ship, piloted: false); ship.Position = position; ship.Aim = float3(0, 0, 1); _zone.Update(0f); // brings the gear online @@ -514,8 +513,8 @@ public sealed class ContactRevealTests : IDisposable { switch (how) { - case "remove": zone.Entities.Remove(e); break; - case "warp": e.Deactivate(); zone.Entities.Remove(e); break; + case "remove": zone.Remove(e); break; + case "warp": e.Deactivate(); zone.Remove(e); break; case "death": e.Hull.Durability = 0f; e.HullDamage.OnNext(1f); break; default: throw new Exception("fixture: " + how); } @@ -554,7 +553,7 @@ public sealed class ContactRevealTests : IDisposable Assert.True(e.Visibility * o.PassiveInfo(e.Position.xz, 1f) <= _items.GameplaySettings.TargetDetectionInfoThreshold, "fixture: its last light is below o's gate"); Assert.Equal(2, times.Count); - _zone.Entities.Remove(e); + _zone.Remove(e); Assert.Equal(2, o.RevealsOf(e).Count()); var lapsed = false; @@ -640,7 +639,7 @@ public sealed class ContactRevealTests : IDisposable var gate = _items.GameplaySettings.TargetDetectionInfoThreshold / o.PassiveInfo(e.Position.xz, 1f); ShowOnly(e, gate * against, Guns(e)[0]); - _zone.Entities.Remove(e); + _zone.Remove(e); Tick(.1f); Assert.Equal(ended ? 0 : 1, o.RevealsOf(e).Count()); @@ -662,7 +661,7 @@ public sealed class ContactRevealTests : IDisposable ShowOnly(e, threshold * 8f, Guns(e)[0]); Assert.True(e.Visibility * o.PassiveInfo(e.Position.xz, 1f) == threshold, "fixture: the ship's light at removal is exactly the gate"); - _zone.Entities.Remove(e); + _zone.Remove(e); Tick(.1f); Assert.Single(o.RevealsOf(e)); @@ -679,7 +678,7 @@ public sealed class ContactRevealTests : IDisposable ShowOnly(e, 0f, Guns(e)[0]); e.SetSignature(new object(), SignatureChannel.Emission, 1000f, Guns(e)[0]); - _zone.Entities.Remove(e); + _zone.Remove(e); Tick(.1f); Assert.Single(o.RevealsOf(e)); @@ -696,8 +695,9 @@ public sealed class ContactRevealTests : IDisposable Tick(1f); // the gun cools down ShowOnly(e, 1000f, gun); // bright enough for any gate - _zone.Entities.Remove(e); - _zone.Entities.Add(e); + _zone.Remove(e); + e.Deactivate(); + _zone.Admit(e, piloted: false); Tick(.1f); Assert.Equal(at, Assert.Single(o.RevealsOf(e)).Time); @@ -813,7 +813,7 @@ public sealed class ContactRevealTests : IDisposable var first = Assert.Single(o.RevealsOf(locker)); // fixture: the lock paints every tick, and the first one is recorded Assert.True(locker.Visibility * o.PassiveInfo(locker.Position.xz, 1f) <= _items.GameplaySettings.TargetDetectionInfoThreshold, "fixture: its departure is below o's gate"); - _zone.Entities.Remove(locker); // dies, warps or docks while its latest paint is still in o's inbox + _zone.Remove(locker); // dies, warps or docks while its latest paint is still in o's inbox var inboxed = ((System.Collections.IList) typeof(Entity).GetField("_paintInbox", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(o)).Count; Assert.True(inboxed > 0 || o.IncomingPaints.Count > 0, "fixture: a paint is in flight"); Tick(.1f); @@ -865,7 +865,7 @@ public sealed class ContactRevealTests : IDisposable for (var tick = 0; tick < 100 && inbox.Count == 0; tick++) Tick(.1f); Assert.NotEmpty(inbox); // fixture: the ping crossed o and its paint waits to be published - _zone.Entities.Remove(p); + _zone.Remove(p); Tick(.1f); Assert.Single(o.IncomingPaints); @@ -1039,7 +1039,7 @@ public sealed class ContactRevealTests : IDisposable ShowOnly(e, 1f, gun); var threshold = _items.GameplaySettings.TargetDetectionInfoThreshold; Assert.True(e.Visibility * o.PassiveInfo(e.Position.xz, 1f) <= threshold, "fixture: from where o is, the departure is below the gate"); - _zone.Entities.Remove(e); + _zone.Remove(e); Tick(.1f); Assert.Single(o.RevealsOf(e)); @@ -1069,8 +1069,8 @@ public sealed class ContactRevealTests : IDisposable var threshold = _items.GameplaySettings.TargetDetectionInfoThreshold; Assert.True(a.Visibility * o.PassiveInfo(a.Position.xz, 1f) > threshold && b.Visibility * o.PassiveInfo(b.Position.xz, 1f) > threshold, "fixture: both departures pass the gate"); - _zone.Entities.Remove(firstThenSecond ? a : b); - _zone.Entities.Remove(firstThenSecond ? b : a); + _zone.Remove(firstThenSecond ? a : b); + _zone.Remove(firstThenSecond ? b : a); Tick(.1f); Assert.Empty(o.RevealsOf(a)); @@ -1096,7 +1096,7 @@ public sealed class ContactRevealTests : IDisposable Tick(.01f); Assert.Same(guns[1], Assert.Single(o.RevealsOf(e)).Item); // fixture: one gun ended as destroyed, one waiting e.Deactivate(); // the warp path: the ship's own subscriptions to the observer go with it - _zone.Entities.Remove(e); + _zone.Remove(e); for (var i = 0; i < 40; i++) Tick(.1f); // past BearingMemory (2 s) Assert.Equal(0, RevealBookSubjects(o)); // fixture: pruned @@ -1179,7 +1179,7 @@ public sealed class ContactRevealTests : IDisposable Assert.Contains(identified.VisibilitySources.Values, v => v.Item == gunItems[0] && v.Strength > 0f); var departed = _zone.Entities.Single(x => x.Equipment.Contains(gunItems[18])); foreach (var source in departed.VisibilitySources.Keys.ToArray()) departed.SetSignature(source, SignatureChannel.Reflection, 0f, gunItems[18]); - _zone.Entities.Remove(departed); + _zone.Remove(departed); Assert.Single((System.Collections.IList) typeof(Zone).GetProperty("Departures", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(_zone)); sense(); Assert.Single(o.RevealsOf(departed)); // fixture: awaiting its prune diff --git a/tests/Aetheria.Shared.Tests/ContactTests.cs b/tests/Aetheria.Shared.Tests/ContactTests.cs index b139edea7..e26996ce4 100644 --- a/tests/Aetheria.Shared.Tests/ContactTests.cs +++ b/tests/Aetheria.Shared.Tests/ContactTests.cs @@ -64,8 +64,7 @@ public sealed class ContactTests : IDisposable { var hullRef = Items.ItemData.RefOf(Items.ItemData.GetByName(hull)); var ship = new Ship(Items, Zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = Lot++ }, new EntitySettings()); - Zone.Entities.Add(ship); - ship.Activate(); + Zone.Admit(ship, piloted: false); ship.Position = at; ship.Velocity = velocity; return ship; diff --git a/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs b/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs index 9512be458..76e175e82 100644 --- a/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs +++ b/tests/Aetheria.Shared.Tests/FireAuthorityTests.cs @@ -176,10 +176,8 @@ public sealed class FireAuthorityTests : IDisposable // extra gear a test needs (a second aimable item, a shield, a reactor) must go on before Activate(). beforeActivate?.Invoke(items, shooter, target); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = float3(0, 0, targetRange); @@ -327,7 +325,7 @@ public sealed class FireAuthorityTests : IDisposable FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); var before = e.Target.Hull.Durability; - e.Zone.Entities.Remove(e.Target); + e.Zone.Remove(e.Target); e.Zone.Update(10f); // well past arrival Assert.Equal(before, e.Target.Hull.Durability); diff --git a/tests/Aetheria.Shared.Tests/FireControlCut11Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut11Tests.cs index ed352a535..54e43f21e 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut11Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut11Tests.cs @@ -177,10 +177,8 @@ public sealed class FireControlCut11Tests : IDisposable Assert.True(target.TryEquip(shield, spos)); } - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3(37, 0, -11); target.Position = float3(37, 0, 89); // 100 units straight ahead of the shooter @@ -280,7 +278,7 @@ public sealed class FireControlCut11Tests : IDisposable // What the HUD's arrival countdown reads: time left in flight at commit, 5 s - 4.7 s. Assert.Equal(.3f, committed.ArrivalIn, 3); - e.Zone.Entities.Remove(e.Target); // the target is gone before the shot arrives + e.Zone.Remove(e.Target); // the target is gone before the shot arrives e.Zone.Update(1f); Assert.NotNull(resolved); @@ -307,7 +305,7 @@ public sealed class FireControlCut11Tests : IDisposable e.Zone.Update(1f); SafeAssert.Empty(committed); - e.Zone.Entities.Remove(e.Target); + e.Zone.Remove(e.Target); e.Zone.Update(1f); foreach (var outcome in new[] { SafeAssert.Only(committed), SafeAssert.Only(resolved) }) diff --git a/tests/Aetheria.Shared.Tests/FireControlCut123Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut123Tests.cs index f935c2edf..a599d576d 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut123Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut123Tests.cs @@ -224,10 +224,8 @@ public sealed class FireControlCut123Tests : IDisposable cockpit = target.Equipment.Single(x => x.EquippableItem == ci); } - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); // Cut 11.3: the shooter sits off the origin. shooter.Position = float3(37, 0, -11); diff --git a/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs index a1d5445e8..cd6b876b1 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs @@ -242,10 +242,8 @@ public sealed partial class FireControlCut124Tests : IDisposable Assert.True(target.TryEquip(shield, spos)); } - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); // Off-origin (the fixture rule): the shooter sits away from the world origin, and the target sits // `targetRange` ahead of it along the shooter's own +z, so a stationary target's TravelDirection is @@ -515,8 +513,7 @@ public sealed partial class FireControlCut124Tests : IDisposable Data = e.Items.ItemData.RefOf(e.Items.ItemData.GetByName("Marker")), Durability = 1000000, Lot = lot + 1 }; Assert.True(ship.TryEquip(marker, new int2(1, 1))); - e.Zone.Entities.Add(ship); - ship.Activate(); + e.Zone.Admit(ship, piloted: false); ship.Position = position; return ship; } @@ -1465,7 +1462,7 @@ public sealed partial class FireControlCut124Tests : IDisposable FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); Assert.True(SafeAssert.OnlyShot(e.Zone).Target == e.Target, "fixture: the pushed-out round keeps its target"); e.Zone.Update(.01f); - e.Zone.Entities.Remove(e.Target); + e.Zone.Remove(e.Target); e.Zone.Update(3f); SafeAssert.NoShots(e.Zone); @@ -1510,7 +1507,7 @@ public sealed partial class FireControlCut124Tests : IDisposable var e = Build(TestSettings(), SolidShape(5, 4), velocity: 20f, fuse: WeaponFuse.Proximity, blastRadius: 4f, damage: 100f); FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); using var c = e.Zone.ShotCommitted.Subscribe(_ => throw new InvalidOperationException("observer")); - e.Zone.Entities.Remove(e.Target); + e.Zone.Remove(e.Target); Assert.ThrowsAny(() => e.Zone.Update(.01f)); var second = Record.Exception(() => e.Zone.Update(.01f)); @@ -2004,7 +2001,7 @@ public sealed partial class FireControlCut124Tests : IDisposable FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); for (var i = 0; i < updatesBeforeLeaving; i++) e.Zone.Update(.01f); - e.Zone.Entities.Remove(e.Shooter); + e.Zone.Remove(e.Shooter); e.Zone.Update(4f); SafeAssert.NoShots(e.Zone); @@ -2022,7 +2019,7 @@ public sealed partial class FireControlCut124Tests : IDisposable FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); e.Zone.Update(.01f); - e.Zone.Entities.Remove(e.Shooter); + e.Zone.Remove(e.Shooter); e.Zone.Update(6f); SafeAssert.NoShots(e.Zone); @@ -2044,7 +2041,7 @@ public sealed partial class FireControlCut124Tests : IDisposable for (var i = 0; i < 12; i++) FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); e.Zone.Update(.01f); - e.Zone.Entities.Remove(e.Shooter); + e.Zone.Remove(e.Shooter); e.Zone.Update(6f); SafeAssert.NoShots(e.Zone); @@ -2274,7 +2271,7 @@ public sealed partial class FireControlCut124Tests : IDisposable FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter); e.Zone.Update(committedFirst ? 4.6f : .01f); Assert.Equal(committedFirst, e.Zone.PendingShots[0].Committed); - e.Zone.Entities.Remove(e.Target); + e.Zone.Remove(e.Target); e.Zone.Update(6f); foreach (var s in subscriptions) s.Dispose(); diff --git a/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs index 2c0623273..c28242a15 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut12Tests.cs @@ -269,10 +269,8 @@ public sealed class FireControlCut12Tests : IDisposable Assert.True(target.TryEquip(shield, spos)); } - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); // Cut 11.3: the shooter sits off the origin -- a shooter at float3.zero makes `target - source` and // `target + source` the same vector, hiding a whole class of sign mutants. diff --git a/tests/Aetheria.Shared.Tests/FireControlCut4Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut4Tests.cs index c73d486e3..8ff7eef2f 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut4Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut4Tests.cs @@ -154,10 +154,8 @@ public sealed class FireControlCut4Tests : IDisposable var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 9 }; Assert.True(target.TryEquip(targetGun, new int2(0, 0))); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = float3(0, 0, targetRange); @@ -233,8 +231,7 @@ public sealed class FireControlCut4Tests : IDisposable Assert.True(shooter.TryEquip(gun, new int2(0, 0))); var weapon = (InstantWeapon) shooter.Equipment.Single(e => e.EquippableItem == gun).Behaviors.Single(b => b is Weapon); - zone.Entities.Add(shooter); - shooter.Activate(); + zone.Admit(shooter, piloted: false); zone.Update(0f); // warm up resolved stats // No target, no OnFire subscriber -- exactly a headless shooter with no target and no presentation. @@ -276,7 +273,7 @@ public sealed class FireControlCut4Tests : IDisposable var ship = new Ship(items, zone, hull, new EntitySettings()); ship.Position = position; ship.Direction = direction; - zone.Entities.Add(ship); + zone.Admit(ship, piloted: false); return ship; } diff --git a/tests/Aetheria.Shared.Tests/FireControlCut5Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut5Tests.cs index 327b6e9e9..0023f0c08 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut5Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut5Tests.cs @@ -184,10 +184,8 @@ public sealed class FireControlCut5Tests : IDisposable beforeActivate?.Invoke(items, shooter, target); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = float3(0, 0, targetRange); @@ -339,10 +337,8 @@ public sealed class FireControlCut5Tests : IDisposable var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 5 }; Assert.True(target.TryEquip(targetGun, new int2(0, 0))); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = targetPosition; @@ -427,10 +423,8 @@ public sealed class FireControlCut5Tests : IDisposable var targetGun = new EquippableItem { Data = gunRef, Durability = 1, Lot = 4 }; Assert.True(target.TryEquip(targetGun, new int2(0, 0))); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); return (items, shooter, target, weaponItem); } @@ -576,8 +570,7 @@ public sealed class FireControlCut5Tests : IDisposable Assert.True(target.TryEquip(medium, mediumPos)); var mediumItem = target.Equipment.Single(e => e.EquippableItem == medium); - zone.Entities.Add(target); - target.Activate(); + zone.Admit(target, piloted: false); return (items, target, bigItem, mediumItem); } @@ -592,8 +585,7 @@ public sealed class FireControlCut5Tests : IDisposable var (items, target, big, medium) = BuildRevealTarget(settings); var observerHullRef = items.ItemData.RefOf(items.ItemData.GetByName("Hull")); var observer = new Ship(items, target.Zone, new EquippableItem { Data = observerHullRef, Durability = 1000, Lot = 1 }, new EntitySettings()); - target.Zone.Entities.Add(observer); - observer.Activate(); + target.Zone.Admit(observer, piloted: false); observer.SetTarget(target); // With both items alive: Big (index 0) reveals at .2, Medium (index 1) at .8. diff --git a/tests/Aetheria.Shared.Tests/FireControlCut6Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut6Tests.cs index 8f581a33d..56b2f38da 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut6Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut6Tests.cs @@ -156,10 +156,8 @@ public sealed class FireControlCut6Tests : IDisposable var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 5 }; Assert.True(target.TryEquip(targetGun, new int2(0, 0))); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = float3(0, 0, targetRange); diff --git a/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs b/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs index 586c658b4..0421b1b25 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut6cTests.cs @@ -138,10 +138,8 @@ public sealed class FireControlCut6cTests : IDisposable var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 9 }; Assert.True(target.TryEquip(targetGun, new int2(0, 0))); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = float3(0, 0, targetRange); @@ -267,8 +265,7 @@ public sealed class FireControlCut6cTests : IDisposable var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test", Owner = null }, null); var entity = EntitySerializer.Unpack(items, zone, pack); - zone.Entities.Add(entity); - entity.Activate(); + zone.Admit(entity, piloted: false); zone.Update(0f); Assert.Null(entity.GetBehavior()); diff --git a/tests/Aetheria.Shared.Tests/FireControlCut6dTests.cs b/tests/Aetheria.Shared.Tests/FireControlCut6dTests.cs index fdf54d5f2..5ae16a285 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut6dTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut6dTests.cs @@ -156,10 +156,8 @@ public sealed class FireControlCut6dTests : IDisposable Assert.True(target.TryEquip(marker, markerCell.Value)); } - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = float3(0, 0, targetRange); diff --git a/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs index 56b5cc2be..67eaa672e 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut7Tests.cs @@ -133,10 +133,8 @@ public sealed class FireControlCut7Tests : IDisposable beforeActivate?.Invoke(items, shooter, target); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = float3(0, 0, 100); diff --git a/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs b/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs index 2a94080ed..02cab4a7b 100644 --- a/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlCut9Tests.cs @@ -129,10 +129,8 @@ public sealed class FireControlCut9Tests : IDisposable Assert.True(target.TryFindSpace(targetMarker, out var targetMarkerPos)); Assert.True(target.TryEquip(targetMarker, targetMarkerPos)); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = float3(0, 0, 100); @@ -340,10 +338,8 @@ public sealed class FireControlCut9Tests : IDisposable var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 5 }; Assert.True(target.TryEquip(targetGun, new int2(0, 0))); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = float3(0, 0, 100); @@ -567,10 +563,8 @@ public sealed class FireControlCut9Tests : IDisposable var bystanderGun = new EquippableItem { Data = bystanderGunRef, Durability = 1, Lot = 4 }; Assert.True(bystander.TryEquip(bystanderGun, new int2(0, 0))); // _orderedEquipment must not be empty/null for Update to run - zone.Entities.Add(locker); - zone.Entities.Add(bystander); - locker.Activate(); - bystander.Activate(); + zone.Admit(locker, piloted: false); + zone.Admit(bystander, piloted: false); locker.Position = float3.zero; bystander.Position = float3(0, 0, 100); @@ -603,7 +597,7 @@ public sealed class FireControlCut9Tests : IDisposable // the target dies or leaves while the locker is inactive -- its own Zone.Entities.ObserveRemove // subscription (the only thing that would have nulled Target) was disposed by Deactivate above, so // nothing reacts to this removal yet. - s.Zone.Entities.Remove(s.Bystander); + s.Zone.Remove(s.Bystander); s.Locker.Activate(); // undock Assert.True(s.Locker.Active); diff --git a/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs b/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs index 5617eaa0b..924e868e8 100644 --- a/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlPerWeaponTests.cs @@ -238,10 +238,8 @@ public sealed class FireControlPerWeaponTests : IDisposable var target = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = lot++ }, new EntitySettings()); Assert.True(target.TryEquip(Make("Gun", lot++, 1), new int2(0, 0))); - zone.Entities.Add(shooter); - zone.Entities.Add(target); - shooter.Activate(); - target.Activate(); + zone.Admit(shooter, piloted: false); + zone.Admit(target, piloted: false); shooter.Position = float3(-53, 0, -101); target.Position = shooter.Position + float3(0, 0, targetRange); diff --git a/tests/Aetheria.Shared.Tests/FireControlTests.cs b/tests/Aetheria.Shared.Tests/FireControlTests.cs index 8b0f16e9f..813bde945 100644 --- a/tests/Aetheria.Shared.Tests/FireControlTests.cs +++ b/tests/Aetheria.Shared.Tests/FireControlTests.cs @@ -95,8 +95,7 @@ public sealed class FireControlTests : IDisposable weapons[i] = ship.Equipment.Single(e => e.EquippableItem == gun); } - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); return (items, ship, weapons); } @@ -195,8 +194,7 @@ public sealed class FireControlTests : IDisposable // gun so the target is a real, ordinarily-constructed ship rather than a workaround-shaped one. var targetGunRef = items.ItemData.RefOf(items.ItemData.GetByName("Gun")); Assert.True(target.TryEquip(new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 1 }, new int2(0, 0))); - zone.Entities.Add(target); - target.Activate(); + zone.Admit(target, piloted: false); shooter.Position = float3.zero; target.Position = targetPosition; diff --git a/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs b/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs index 77cd3b48e..5b98ba649 100644 --- a/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs +++ b/tests/Aetheria.Shared.Tests/FloatingBodyTests.cs @@ -239,9 +239,8 @@ public sealed class FloatingBodyTests : IDisposable Lot = items.Lots.Add(new Lot { Design = cache.RefOf(design), Origin = new Attributed(), Quality = .5f, Roles = new List() }) }; var ship = new Ship(items, zone, hull, new EntitySettings()); - zone.Entities.Add(ship); + zone.Admit(ship, piloted: false); ship.Aim = float3(0, 0, 1); - ship.Activate(); ship.Position = float3(x, 0, 0); return ship; } diff --git a/tests/Aetheria.Shared.Tests/IffAndCombatTests.cs b/tests/Aetheria.Shared.Tests/IffAndCombatTests.cs index 3e1012889..7fe1d9127 100644 --- a/tests/Aetheria.Shared.Tests/IffAndCombatTests.cs +++ b/tests/Aetheria.Shared.Tests/IffAndCombatTests.cs @@ -82,8 +82,7 @@ public sealed class IffAndCombatTests : IDisposable Assert.True(ship.TryEquip(gun, HardpointCell)); } - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); return ship; } @@ -132,8 +131,9 @@ public sealed class IffAndCombatTests : IDisposable a.SetIff(b, true); Assert.True(a.EntityHostility[b]); - zone.Entities.Remove(b); // b leaves the zone: a's override on b must be cleared - zone.Entities.Add(b); // b returns as a "new" contact from a's perspective + zone.Remove(b); // b leaves the zone: a's override on b must be cleared + b.Deactivate(); + zone.Admit(b, piloted: false); // b returns as a "new" contact from a's perspective Assert.False(a.EntityHostility[b]); // recomputed fresh from the derived (non-hostile) rule Assert.False(a.IsHostileTo(b)); diff --git a/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs b/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs index 203a5347a..3e52fc3c1 100644 --- a/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs +++ b/tests/Aetheria.Shared.Tests/ImpactFeedbackTests.cs @@ -33,8 +33,7 @@ public sealed partial class FireControlCut124Tests { var hullRef = e.Items.ItemData.RefOf(e.Items.ItemData.GetByName("Hull")); var ship = new Ship(e.Items, e.Zone, new EquippableItem { Data = hullRef, Durability = 100000, Lot = lot }, new EntitySettings()); - e.Zone.Entities.Add(ship); - ship.Activate(); + e.Zone.Admit(ship, piloted: false); ship.Position = position; return ship; } @@ -135,7 +134,7 @@ public sealed partial class FireControlCut124Tests var gone = Build(TestSettings(), SolidShape(5, 4), velocity: 50f, damage: 100f); var goneAbsorbs = new[] { gone.Target, gone.Shooter }.SelectMany(x => Collect(x, subs)).ToList(); FireControl.Fire(gone.Weapon, gone.WeaponItem, gone.Shooter); - gone.Zone.Entities.Remove(gone.Target); + gone.Zone.Remove(gone.Target); for (var i = 0; i < 40; i++) gone.Zone.Update(.1f); foreach (var s in subs) s.Dispose(); diff --git a/tests/Aetheria.Shared.Tests/InputCapacitorTests.cs b/tests/Aetheria.Shared.Tests/InputCapacitorTests.cs index d72113458..3a7b9e2d5 100644 --- a/tests/Aetheria.Shared.Tests/InputCapacitorTests.cs +++ b/tests/Aetheria.Shared.Tests/InputCapacitorTests.cs @@ -97,8 +97,7 @@ public sealed class InputCapacitorTests : IDisposable Assert.True(ship.TryEquip(Mint(cache, items, reactorData))); - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); return (ship, items, ship.GetBehavior()); } diff --git a/tests/Aetheria.Shared.Tests/LockWarningTests.cs b/tests/Aetheria.Shared.Tests/LockWarningTests.cs index 3b2710660..3327ffdcc 100644 --- a/tests/Aetheria.Shared.Tests/LockWarningTests.cs +++ b/tests/Aetheria.Shared.Tests/LockWarningTests.cs @@ -105,8 +105,7 @@ public sealed class LockWarningTests : IDisposable var gearRef = _items.ItemData.RefOf(_items.ItemData.GetByName(launcher)); Assert.True(ship.TryEquip(new EquippableItem { Data = gearRef, Durability = 1, Lot = _nextLot++ }, HardpointCell)); } - _zone.Entities.Add(ship); - ship.Activate(); + _zone.Admit(ship, piloted: false); ship.Position = position; _zone.Update(0f); // brings the gear online return ship; diff --git a/tests/Aetheria.Shared.Tests/LootTests.cs b/tests/Aetheria.Shared.Tests/LootTests.cs index ab2c19511..d0414f293 100644 --- a/tests/Aetheria.Shared.Tests/LootTests.cs +++ b/tests/Aetheria.Shared.Tests/LootTests.cs @@ -133,8 +133,7 @@ public sealed class LootTests : IDisposable Assert.True(ship.CargoBays.Single().TryStore(lab.Ore(7))); Assert.True(ship.CargoBays.Single().TryStore(lab.Make("Spare"))); } - lab.Zone.Entities.Add(ship); - ship.Activate(); + lab.Zone.Admit(ship, piloted: false); ship.Position = at; return ship; } @@ -153,8 +152,7 @@ public sealed class LootTests : IDisposable Assert.True(station.TryEquip(spare, pos)); Assert.True(station.CargoBays.Single().TryStore(lab.Ore(3))); } - lab.Zone.Entities.Add(station); - station.Activate(); + lab.Zone.Admit(station, piloted: false); station.Position = at; return station; } @@ -393,10 +391,10 @@ public sealed class LootTests : IDisposable var lab = Build(probability: 1f); var ship = Spawn(lab, float3(3, 0, 4)); // What Entity.TryDock then Entity.TryUndock do to the zone: Entities.Remove + Deactivate, then Zone.Admit. - lab.Zone.Entities.Remove(ship); + lab.Zone.Remove(ship); ship.Deactivate(); lab.Zone.Admit(ship, piloted: false); - lab.Zone.Entities.Remove(ship); + lab.Zone.Remove(ship); lab.Zone.Admit(ship, piloted: false); Kill(ship); var dropped = lab.Zone.FloatingItems.Select(f => f.Item).ToList(); @@ -412,7 +410,7 @@ public sealed class LootTests : IDisposable var ship = Spawn(lab, float3(3, 0, 4)); // What ActionGameManager does on a jump. ship.Deactivate(); - lab.Zone.Entities.Remove(ship); + lab.Zone.Remove(ship); ship.Zone = other; other.Admit(ship, piloted: false); Kill(ship); @@ -509,9 +507,10 @@ public sealed class LootTests : IDisposable Assert.True(lab.Zone.FloatingItems.Select(f => Math.Round(Math.Atan2(f.Body.Velocity.y, f.Body.Velocity.x), 3)).Distinct().Count() > 1); } - // A member added twice holds one death subscription, so it drops each item once, as a single death would. + // Membership has one owner (aetheria-release zone-entities-encapsulation): a second admission of a member is + // refused, so it never holds a second death watch and the zone never lists it twice. [Fact] - public void AMemberAddedTwiceStillDropsEachItemOnce() + public void AMemberAdmittedTwiceIsRefusedAndStillDropsEachItemOnce() { var once = Build("Once", probability: 1f); Kill(Spawn(once, float3(3, 0, 4))); @@ -520,20 +519,45 @@ public sealed class LootTests : IDisposable var lab = Build("Twice", probability: 1f); var ship = Spawn(lab, float3(3, 0, 4)); - lab.Zone.Entities.Add(ship); + Assert.Throws(() => lab.Zone.Admit(ship, piloted: false)); + Assert.Equal(1, lab.Zone.Entities.Count(e => e == ship)); Kill(ship); Assert.Equal(baseline, lab.Zone.FloatingItems.Count); } - // Clearing Entities removes every member, so a death afterwards drops nothing in this zone. + // Remove is the one removal: a non-member is refused with nothing changed, and a member's removal takes its death + // watch with it, so a death signal afterwards drops nothing and a re-admission watches exactly once. [Fact] - public void AClearedZoneDropsNothingWhenAFormerMemberDies() + public void RemovingANonMemberChangesNothingAndARemovedMembersWatchGoesWithIt() { - var lab = Build("Cleared", probability: 1f); + var lab = Build(probability: 1f); var ship = Spawn(lab, float3(3, 0, 4)); - lab.Zone.Entities.Clear(); + var stranger = Spawn(Build("Elsewhere", probability: 1f), float3(1, 0, 1)); + var members = lab.Zone.Entities.Count; + Assert.False(lab.Zone.Remove(stranger)); + Assert.Equal(members, lab.Zone.Entities.Count); + + Assert.True(lab.Zone.Remove(ship)); + Assert.False(lab.Zone.Remove(ship)); Kill(ship); Assert.Empty(lab.Zone.FloatingItems); + + ship.Deactivate(); + lab.Zone.Admit(ship, piloted: false); + Kill(ship); + var dropped = lab.Zone.FloatingItems.Select(f => f.Item).ToList(); + Assert.NotEmpty(dropped); + Assert.Equal(dropped.Count, dropped.Distinct().Count()); + } + + // The membership view has no mutating member: a caller can read it and subscribe to it, and nothing else. + [Fact] + public void TheMembershipViewOffersNoWayToMutateMembership() + { + var lab = Build(); + Spawn(lab, float3(3, 0, 4)); + Assert.IsNotAssignableFrom>(lab.Zone.Entities); + Assert.IsNotAssignableFrom(lab.Zone.Entities); } // The pickup is whole or nothing at the arithmetic boundary: two stacks of 90 hold 20 more in all and no free diff --git a/tests/Aetheria.Shared.Tests/MineTests.cs b/tests/Aetheria.Shared.Tests/MineTests.cs index f132de9d2..71321e99d 100644 --- a/tests/Aetheria.Shared.Tests/MineTests.cs +++ b/tests/Aetheria.Shared.Tests/MineTests.cs @@ -130,8 +130,7 @@ public sealed class MineTests : IDisposable var targeting = lab.Make("Targeting", 1); Assert.True(layer.TryFindSpace(targeting, out var tpos)); Assert.True(layer.TryEquip(targeting, tpos)); - zone.Entities.Add(layer); - layer.Activate(); + zone.Admit(layer, piloted: false); layer.Position = layerAt ?? float3(-53, 0, -101); lab.Layer = layer; zone.Update(1f); @@ -152,8 +151,7 @@ public sealed class MineTests : IDisposable Assert.True(ship.TryFindSpace(gear, out var spos)); Assert.True(ship.TryEquip(gear, spos)); } - lab.Zone.Entities.Add(ship); - ship.Activate(); + lab.Zone.Admit(ship, piloted: false); ship.Position = at; if (shield != null) ship.Shield.Item.Enabled.Value = true; return ship; diff --git a/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs b/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs index ea29e418b..d8cfd9858 100644 --- a/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs +++ b/tests/Aetheria.Shared.Tests/MiningCut3Tests.cs @@ -222,10 +222,9 @@ public sealed partial class MiningCut3Tests : IDisposable s.Cache.Upsert(gun); Assert.True(ship.TryEquip(Mint(s, gun))); } - s.Zone.Entities.Add(ship); + s.Zone.Admit(ship, piloted: false); Hold(ship, at); ship.Aim = float3(0, 0, 1); - ship.Activate(); return ship; } @@ -867,10 +866,9 @@ public sealed partial class MiningCut3Tests : IDisposable var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings()); Assert.True(ship.TryEquip(Mint(s, s.Eye))); foreach (var weapon in weapons) Assert.True(ship.TryEquip(Mint(s, weapon))); - s.Zone.Entities.Add(ship); + s.Zone.Admit(ship, piloted: false); Hold(ship, eyeAt); ship.Aim = float3(0, 0, 1); - ship.Activate(); Tick((ship, eyeAt)); // weapons read their ranges when they run return ship; } diff --git a/tests/Aetheria.Shared.Tests/MiningIndexTests.cs b/tests/Aetheria.Shared.Tests/MiningIndexTests.cs index 785854911..3c14484f5 100644 --- a/tests/Aetheria.Shared.Tests/MiningIndexTests.cs +++ b/tests/Aetheria.Shared.Tests/MiningIndexTests.cs @@ -54,10 +54,9 @@ public sealed partial class MiningCut3Tests var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings()); Assert.True(ship.TryEquip(Mint(s, eye))); Assert.True(ship.TryEquip(Mint(s, gun))); - s.Zone.Entities.Add(ship); + s.Zone.Admit(ship, piloted: false); Hold(ship, at); ship.Aim = float3(0, 0, 1); - ship.Activate(); Tick((ship, at)); // equipment comes online and weapons read their ranges on the first tick return ship; } @@ -177,7 +176,7 @@ public sealed partial class MiningCut3Tests Assert.True(expected.ToHashSet().SetEquals(offered), $"sensitivity {sensitivity}: {expected.Count} visible, {offered.Count} offered"); visibleSeen += expected.Count; if (examined < inReach.Count) pruned++; - s.Zone.Entities.Remove(observer); + s.Zone.Remove(observer); } Assert.True(visibleSeen > 50 && pruned > 4, $"degenerate: {visibleSeen} visible, {pruned} queries pruned"); } @@ -422,7 +421,7 @@ public sealed partial class MiningCut3Tests var brightest = all.Max(c => s.Zone.ChunkVisibility(c)); var unit = Observer(s, float2(5000, 0), 350f, 1f, Falloff()); var sensitivity = 120f / (brightest * unit.DetectionReachPerVisibility()); - s.Zone.Entities.Remove(unit); + s.Zone.Remove(unit); var offered = new List(); long examined = 0, inReach = 0; @@ -437,7 +436,7 @@ public sealed partial class MiningCut3Tests observer.VisibleChunksInReach(offered); examined += s.Zone.Targets.Examined - before; inReach += all.Count(c => length(At(s, c) - at) <= 350f); - s.Zone.Entities.Remove(observer); + s.Zone.Remove(observer); } Assert.True(examined < .265 * inReach, $"examined {examined} of {inReach} rocks in reach: the visibility bound has loosened"); } diff --git a/tests/Aetheria.Shared.Tests/MiningTargetQueriesTests.cs b/tests/Aetheria.Shared.Tests/MiningTargetQueriesTests.cs index 64304e066..374f9292e 100644 --- a/tests/Aetheria.Shared.Tests/MiningTargetQueriesTests.cs +++ b/tests/Aetheria.Shared.Tests/MiningTargetQueriesTests.cs @@ -33,7 +33,7 @@ public sealed partial class MiningCut3Tests { var observer = Observer(s, at, reach, sensitivity, Falloff()); if (inReach.Count(observer.ChunkVisible) >= share * inReach.Count) return (s, observer); - s.Zone.Entities.Remove(observer); + s.Zone.Remove(observer); } throw new InvalidOperationException("no sensitivity sees the rocks"); } @@ -42,7 +42,7 @@ public sealed partial class MiningCut3Tests private static Ship Sight(Scene s, Ship observer, float2 at, bool enemy = false) { var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings()); - s.Zone.Entities.Add(ship); + s.Zone.Admit(ship, piloted: false); Hold(ship, at); observer.VisibleEntities.Add(ship); if (enemy) observer.VisibleEnemies.Add(ship); @@ -265,7 +265,7 @@ public sealed partial class MiningCut3Tests observer.Aim = float3(cos(heading), 0, sin(heading)); Check($"the reticle at {heading}", Cost(observer.TargetUnderReticle, TargetRef.None), reticle); } - s.Zone.Entities.Remove(observer); + s.Zone.Remove(observer); } } Assert.True(failures.Count == 0, string.Join("; ", failures)); @@ -336,10 +336,9 @@ public sealed partial class MiningCut3Tests var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings()); Assert.True(ship.TryEquip(Mint(s, s.Eye))); Assert.True(ship.TryEquip(Mint(s, launcher))); - s.Zone.Entities.Add(ship); + s.Zone.Admit(ship, piloted: false); Hold(ship, at); ship.Aim = float3(0, 0, 1); - ship.Activate(); Tick((ship, at)); Assert.Contains(ship.Weapons, w => w.Range == 400f); Assert.True(AllChunks(s).Any(c => length(At(s, c) - at) <= 400f && ship.ChunkVisible(c)), "there are rocks within the launcher's range"); @@ -375,7 +374,7 @@ public sealed partial class MiningCut3Tests private static Ship Unseen(Scene s, float2 at) { var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings()); - s.Zone.Entities.Add(ship); + s.Zone.Admit(ship, piloted: false); Hold(ship, at); return ship; } diff --git a/tests/Aetheria.Shared.Tests/MunitionPartsTests.cs b/tests/Aetheria.Shared.Tests/MunitionPartsTests.cs index 4eddd2731..614a402a5 100644 --- a/tests/Aetheria.Shared.Tests/MunitionPartsTests.cs +++ b/tests/Aetheria.Shared.Tests/MunitionPartsTests.cs @@ -266,8 +266,7 @@ public sealed class MunitionPartsTests : IDisposable Assert.True(ship.TryEquip(new EquippableItem { Data = cache.RefOf(gear), Durability = gear.Durability, Lot = 1 }, new int2(x, y)), name); } Assert.True(ship.TryEquip(new EquippableItem { Data = cache.RefOf(gt), Durability = gt.Durability, Lot = 2 }, hardpoint.Position)); - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); // Settle at the item's optimal temperature, as ConditionRatioTests does, so heat and wear read their identity and only // the role's quality moves a stat. var item = ship.Equipment.Single(e => e.Data.Name == "GT 3K"); diff --git a/tests/Aetheria.Shared.Tests/PaintModeTests.cs b/tests/Aetheria.Shared.Tests/PaintModeTests.cs index 2928e2907..00245412f 100644 --- a/tests/Aetheria.Shared.Tests/PaintModeTests.cs +++ b/tests/Aetheria.Shared.Tests/PaintModeTests.cs @@ -125,8 +125,7 @@ public sealed class PaintModeTests : IDisposable var gearRef2 = _items.ItemData.RefOf(_items.ItemData.GetByName(sensor2)); Assert.True(ship.TryEquip(new EquippableItem { Data = gearRef2, Durability = 1, Lot = _nextLot++ }, new int2(1, 0))); } - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); ship.Position = position; ship.SetSignature(new object(), SignatureChannel.Reflection, 5f, null); zone.Update(0f); // brings the gear online @@ -334,7 +333,7 @@ public sealed class PaintModeTests : IDisposable if (tick == 2 && illuminatorDies) { Assert.True(SensorOf(a).PingRadius < 100f, "fixture: A dies before its wavefront reaches T"); - _zone.Entities.Remove(a); + _zone.Remove(a); } track.Add(Track(c, t)); } @@ -425,7 +424,7 @@ public sealed class PaintModeTests : IDisposable for (var tick = 1; tick <= 24; tick++) { Tick(.1f); - if (tick == removePingerAfterTick) _zone.Entities.Remove(a); + if (tick == removePingerAfterTick) _zone.Remove(a); Near(tick <= 3 ? 0f : .21213f, Track(c, t)); Assert.Equal(tick == 4 ? 1 : 0, t.IncomingPaints.Count); } @@ -686,7 +685,7 @@ public sealed class PaintModeTests : IDisposable SensorOf(a).Ping(); for (var tick = 1; tick <= 3; tick++) Tick(.1f); Assert.True(SensorOf(a).PingRadius > 50f); // the front had passed 50 m by the move - a.Deactivate(); _zone.Entities.Remove(a); a.Zone = z2; z2.Admit(a, false); + a.Deactivate(); _zone.Remove(a); a.Zone = z2; z2.Admit(a, false); var paints = 0; for (var tick = 1; tick <= 24; tick++) { z2.Update(.1f); paints += near.IncomingPaints.Count + far.IncomingPaints.Count; } Assert.Equal(0, paints); @@ -706,8 +705,8 @@ public sealed class PaintModeTests : IDisposable var paints = 0; for (var tick = 1; tick <= 30; tick++) { - if (tick == 5) { _zone.Entities.Remove(t); t.Deactivate(); } - if (tick == 8) { _zone.Entities.Add(t); t.Activate(); } + if (tick == 5) { _zone.Remove(t); t.Deactivate(); } + if (tick == 8) { _zone.Admit(t, piloted: false); } Tick(.1f); paints += t.IncomingPaints.Count; } diff --git a/tests/Aetheria.Shared.Tests/PowerBusTests.cs b/tests/Aetheria.Shared.Tests/PowerBusTests.cs index c0817c6a0..2ab76f599 100644 --- a/tests/Aetheria.Shared.Tests/PowerBusTests.cs +++ b/tests/Aetheria.Shared.Tests/PowerBusTests.cs @@ -101,8 +101,7 @@ public sealed class PowerBusTests : IDisposable if (reactorFirst) { EquipReactor(); EquipCapacitor(); } else { EquipCapacitor(); EquipReactor(); } - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); var reactor = ship.GetBehavior(); var capacitor = ship.GetBehavior(); diff --git a/tests/Aetheria.Shared.Tests/PowerCurveTests.cs b/tests/Aetheria.Shared.Tests/PowerCurveTests.cs index 183238e55..e0e03b4ea 100644 --- a/tests/Aetheria.Shared.Tests/PowerCurveTests.cs +++ b/tests/Aetheria.Shared.Tests/PowerCurveTests.cs @@ -103,8 +103,7 @@ public sealed class PowerCurveTests : IDisposable Assert.True(ship.TryEquip(Mint(cache, items, reactorData))); Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Engine")))); - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); return ship; } @@ -408,9 +407,8 @@ public sealed class PowerCurveTests : IDisposable var ship = new Ship(items, zone, hullItem, new EntitySettings()); Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Drain")), HardpointCell)); Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Booster")), SecondHardpointCell)); - zone.Entities.Add(ship); - var error = Assert.Throws(() => ship.Activate()); + var error = Assert.Throws(() => zone.Admit(ship, piloted: false)); Assert.Contains("Drain", error.Message); // the item whose request would be corrupted Assert.Contains("Booster", error.Message); // the item whose modifier would corrupt it } diff --git a/tests/Aetheria.Shared.Tests/PowerTierTests.cs b/tests/Aetheria.Shared.Tests/PowerTierTests.cs index 256b9bb26..ecdbd4b2d 100644 --- a/tests/Aetheria.Shared.Tests/PowerTierTests.cs +++ b/tests/Aetheria.Shared.Tests/PowerTierTests.cs @@ -72,8 +72,7 @@ public sealed class PowerTierTests : IDisposable ((ReactorData) reactorData.Behaviors[0]).Charge = Constant(reactorCharge); Assert.True(ship.TryEquip(Mint(cache, items, reactorData))); - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); var reactor = ship.GetBehavior(); ship.Update(0f); diff --git a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs index 4086e4b4a..7bca4194a 100644 --- a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs +++ b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs @@ -314,9 +314,8 @@ public sealed class RestoredHullsTests Assert.True(ship.TryEquip(fillerItem, filled.Position)); } - zone.Entities.Add(ship); + zone.Admit(ship, piloted: false); ship.Position = float3.zero; - ship.Activate(); zone.Update(0f); // warm-up: resolves equipment stats before the driven ticks read them return ship; } @@ -618,9 +617,8 @@ public sealed class RestoredHullsTests var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test", Owner = null }, null); var ship = (Ship) EntitySerializer.Unpack(items, zone, pack); - zone.Entities.Add(ship); + zone.Admit(ship, piloted: false); ship.Position = float3.zero; - ship.Activate(); zone.Update(0f); ship.MovementDirection = float2(0, 1); // full forward diff --git a/tests/Aetheria.Shared.Tests/ShieldReserveTests.cs b/tests/Aetheria.Shared.Tests/ShieldReserveTests.cs index 58f0654d9..d84519034 100644 --- a/tests/Aetheria.Shared.Tests/ShieldReserveTests.cs +++ b/tests/Aetheria.Shared.Tests/ShieldReserveTests.cs @@ -95,8 +95,7 @@ public sealed class ShieldReserveTests : IDisposable var shieldData = cache.GetByName("Shield"); Assert.True(ship.TryEquip(Mint(cache, items, shieldData))); - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); return (ship, ship.GetBehavior()); } diff --git a/tests/Aetheria.Shared.Tests/SignatureTests.cs b/tests/Aetheria.Shared.Tests/SignatureTests.cs index c9b7efb8a..2e8ea58ed 100644 --- a/tests/Aetheria.Shared.Tests/SignatureTests.cs +++ b/tests/Aetheria.Shared.Tests/SignatureTests.cs @@ -159,8 +159,7 @@ public sealed class SignatureTests : IDisposable Assert.True(ship.TryEquip(item, at)); } - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); ship.Aim = float3(0, 0, 1); zone.Update(1f); return rig; diff --git a/tests/Aetheria.Shared.Tests/SolutionTests.cs b/tests/Aetheria.Shared.Tests/SolutionTests.cs index d9e55cc9c..d3a18141c 100644 --- a/tests/Aetheria.Shared.Tests/SolutionTests.cs +++ b/tests/Aetheria.Shared.Tests/SolutionTests.cs @@ -189,8 +189,7 @@ public sealed partial class FireControlCut124Tests var e = Build(TestSettings(), SolidShape(5, 4), hardpointArc: SolutionArc, weaponRange: 200f); var hull = e.Items.ItemData.RefOf(e.HullData); var decoy = new Ship(e.Items, e.Zone, new EquippableItem { Data = hull, Durability = 1000000, Lot = 400 }, new EntitySettings()); - e.Zone.Entities.Add(decoy); - decoy.Activate(); + e.Zone.Admit(decoy, piloted: false); var at = radians(40f); decoy.Position = e.Shooter.Position + float3(sin(at), 0, cos(at)) * 100f; e.Shooter.VisibleEntities.Add(decoy); diff --git a/tests/Aetheria.Shared.Tests/StatResolverTests.cs b/tests/Aetheria.Shared.Tests/StatResolverTests.cs index c7d32f337..aefb239c1 100644 --- a/tests/Aetheria.Shared.Tests/StatResolverTests.cs +++ b/tests/Aetheria.Shared.Tests/StatResolverTests.cs @@ -71,8 +71,7 @@ public sealed class StatResolverTests : IDisposable Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Battery")), HardpointCell)); if (withBooster) Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Booster")), SecondHardpointCell)); - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); return ship; } @@ -287,9 +286,8 @@ public sealed class StatResolverTests : IDisposable var hullItem = Mint(cache, items, cache.GetByName("Skiff")); var ship = new Ship(items, zone, hullItem, new EntitySettings()); Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName("Loop")), HardpointCell)); - zone.Entities.Add(ship); - var error = Assert.Throws(() => ship.Activate()); + var error = Assert.Throws(() => zone.Admit(ship, piloted: false)); Assert.Contains(ship.Name, error.Message); // the entity, by name ("Skiff", HullData.Name) Assert.Contains(nameof(CapacitorData), error.Message); Assert.Contains(nameof(CapacitorData.Capacity), error.Message); @@ -513,8 +511,7 @@ public sealed class StatResolverTests : IDisposable Assert.True(ship.TryEquip(boost, cells[i + 1])); } ship.OverrideShutdown = true; - zone.Entities.Add(ship); - ship.Activate(); + zone.Admit(ship, piloted: false); Func mint = null; if (consumable != null) diff --git a/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs b/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs index b621f7b87..c965069d4 100644 --- a/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs +++ b/tests/Aetheria.Shared.Tests/TargetingSystemTests.cs @@ -147,8 +147,7 @@ public sealed class TargetingSystemTests : IDisposable targetingItem = target.Equipment.Single(e => e.EquippableItem == targeting); } - zone.Entities.Add(target); - target.Activate(); + zone.Admit(target, piloted: false); return (items, target, weapon, wideItem, narrowItem, targetingItem); } @@ -160,8 +159,7 @@ public sealed class TargetingSystemTests : IDisposable var hullRef = items.ItemData.RefOf(items.ItemData.GetByName("TestHull")); var hull = new EquippableItem { Data = hullRef, Durability = 1, Lot = 1 }; var observer = new Ship(items, zone, hull, new EntitySettings()); - zone.Entities.Add(observer); - observer.Activate(); + zone.Admit(observer, piloted: false); return observer; } @@ -351,9 +349,8 @@ public sealed class TargetingSystemTests : IDisposable targeting.Lot = items.Lots.Add(new Lot { Design = cache.RefOf(targetingData), Origin = new Attributed(), Quality = .5f, Roles = new System.Collections.Generic.List() }); Assert.True(ship.TryEquip(targeting)); - zone.Entities.Add(ship); + zone.Admit(ship, piloted: false); ship.Aim = float3(0, 0, 1); - ship.Activate(); return ship; } diff --git a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs index 58c1fe356..8511e371a 100644 --- a/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs +++ b/tests/Aetheria.Shared.Tests/ThrottleLockTests.cs @@ -105,9 +105,8 @@ public sealed class ThrottleLockTests : IDisposable } Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(1, 3))); Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(3, 3))); - zone.Entities.Add(ship); + zone.Admit(ship, piloted: false); ship.Aim = float3(0, 0, 1); - ship.Activate(); // A thruster is live from its first performance update; the allocator reads that every tick. foreach (var item in ship.Equipment) item.UpdatePerformance(); diff --git a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs index 58ede257f..51e270f0c 100644 --- a/tests/Aetheria.Shared.Tests/VapourCloudTests.cs +++ b/tests/Aetheria.Shared.Tests/VapourCloudTests.cs @@ -130,10 +130,8 @@ public sealed class VapourCloudTests : IDisposable // Held off until a test pulls the trigger: the dump vents on its first run. lab.Dump.Enabled.Value = false; - zone.Entities.Add(lab.Observer); - zone.Entities.Add(lab.Target); - lab.Observer.Activate(); - lab.Target.Activate(); + zone.Admit(lab.Observer, piloted: false); + zone.Admit(lab.Target, piloted: false); lab.Observer.Position = float3(0, 0, 0); lab.Observer.Aim = float3(0, 0, 1); lab.Target.Position = float3(0, 0, distance);