From 729ab0e8d84cfa78130c645db7aaa2c29811cbcf Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 16 Jun 2026 04:47:42 +0100 Subject: [PATCH] Route entity destruction through checkpoints --- .voidbot/state/aetheria.cc | Bin 145232 -> 145611 bytes Aetheria.State.Verify/Program.cs | 61 +++++++++++++++++++ Assets/Scripts/Gameplay/ActionGameManager.cs | 37 +++++++++++ Assets/Scripts/Gameplay/EntityInstance.cs | 28 ++------- docs/aetheria-perfect-machine-map.md | 10 ++- 5 files changed, 111 insertions(+), 25 deletions(-) diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index fc4964d6ff00da048bca4adcaaa8a466c0b3290c..d9942f39662e0f853d34cd84c8dde79bc8f88dc6 100644 GIT binary patch delta 1505 zcmZuxO^6&t6t-vMvKt5n<0?ypJOk>+%=DOKH=7?%W}`0gpyQ&jA%~jkH`C?xRJB#r z8D|mJCFCTiv4sY2{(~nmf$_rAaPZr+`` z@#oaHUsZMIxD`r*R6Wotv1njTNd%2ub)H4QWB`#;wiaj=(;r+IdY;0)z3b2uHUtK? zA7cPKMBa&&kk30StEng;cG0v#q%MDUmmtAzcngkG6+JYe zumzGOM_Ba9dM`-^wA+zN+m{{jDNi_(dF>%P;(Y1>me3LqBi zypzNRm_{&G5MiB=QeCXa#Fjd-+)CAa(?JFkv!0~CoBi#Tu96`ZT0$FJBgc#xI!R4y zl{pIYZixYvXE_vnaO_rz?)gW@(&~ZV6jLlJ48)zPf!_hs2AN7Qi+WIkws9)sG(^%s%M7{!F^@ z(BACbNpCv+<>^P$U5mS)dhMlh14mn_$-W5Nl%z5x>A`buH^i&#nmq?Ym+7K z41Vl*KTOVjH_ov*fLDh3sTHxT<*^)msJ+j=pDz2PHz#ICclF4EyQ`TwSh-r3jt`IT jWcEh2qDJNU{7PrPRVy2#(!yc)a&WO$xi($aj;{U-9)c@n delta 1175 zcmZ8hO=}fN6t!PsNR$DY;6m`@y3$C}d5LHeKY|)v45Ki_8IUZ*O7(rO3#+R-Rn>Vs zHzML*>BU>Pnek%=+>9XjWDrD{%^(9&+yvbC2LdkCjSFveUbl(cTUFcim5 z+Ti0qYuhI4$Lf<|y*|BgC)n9p8?6oGuWGl-^3;qk=eqA&uQG9Xs;CV1@2edcelpVk z^V-zXd959!f?v&2;95#yg;t<6NUfS^pk;(Z3!IszhDaFXPPUJC_WuzKFY1=E;9@M_ zCz)f;KW8ce9wWb+YN1^CkV__#)&DNW0%df8Ddv%tZh^s68)t!fBqfSYeJxYM3GDpS zAMCy`r;|iD&-9mq=qR*e!DFya8{*~}6adsqQ*S`!glqRWFS2&QXDP#sF0=giOFOSa zOSqW2GfQkG0gtUq5dP&*c?DMAZzgLnVS=V*&}0dwl0d(y#dVTuMO}+V)(I&gLhFof zud{1uBcpOY)^L7&1~%gyt!-qIu>6FhU^O%{ml*^ukty=T#yX?7id;&BCB_lWD~O1t zo7CsrGLHZDlNVAlj)^CsjmJwRx_s})J)PG#g5m3{Z_nxi#2~3;bd+&~bA?XfocHz& zYuoU&Y9Hiqau#^_t7%{K@*WJsaAd?|YSP*CBGA*e2!|p26l{_!gws(}PU4l7G|3{y zY*`SHi;d2jai!!K^yR9B3sy8d5}4J=0g%;qN~h`B*91!F!0QQ6bkd( zk4Ew@Zw5Qh?*@mu{|CXXzEh{pVuOw>%zc;3jXB#8DlnPaK;3r#-Wr%g-52%X>G2(x z=5Z;bZ&ju{zjJy2flXpUX~lSDRPR@IR%CGUN52Io!kMEJ3lAJ?k`C9p`;y?_f$nw} z+0u(=)4)L>8f08k !actionGameManager.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingSymbols.Length > 0) + { + throw new InvalidOperationException( + "Entity destruction no longer has a gameplay-owned checkpoint commit primitive: " + + string.Join(", ", missingSymbols)); + } + + var entityInstancePath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "EntityInstance.cs"); + var entityInstance = File.Exists(entityInstancePath) + ? File.ReadAllText(entityInstancePath) + : throw new InvalidOperationException("Cannot verify entity destruction authority; EntityInstance.cs is missing."); + + var forbiddenSymbols = new[] + { + "ZoneRenderer.DropItem(", + "entity.Zone.Entities.Remove(entity)", + "Random.onUnitSphere", + "LootDropProbability" + }; + + var hits = File.ReadLines(entityInstancePath) + .Select((line, index) => new { LineNumber = index + 1, Line = line }) + .Where(line => forbiddenSymbols.Any(symbol => line.Line.Contains(symbol, StringComparison.Ordinal))) + .Select(line => $"{Path.GetRelativePath(root, entityInstancePath)}:{line.LineNumber}: {line.Line.Trim()}") + .ToArray(); + + if (hits.Length > 0) + { + throw new InvalidOperationException( + "EntityInstance still owns direct destruction/drop mutation: " + + string.Join("; ", hits)); + } + + if (!entityInstance.Contains("ActionGameManager.Instance?.CommitEntityDestroyed", StringComparison.Ordinal)) + { + throw new InvalidOperationException("EntityInstance no longer routes destruction through ActionGameManager."); + } +} + static void RequireInventoryLoadoutRestoreCommitAuthority(string root) { var actionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs"); diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 7acac87c9..06cea354e 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -1587,6 +1587,43 @@ public class ActionGameManager : MonoBehaviour return false; } + public bool CommitEntityDestroyed(Entity entity, ZoneRenderer zoneRenderer) + { + if (entity?.Zone == null) + return false; + + if (zoneRenderer != null) + { + foreach (var gear in entity.Equipment) + { + if (gear == entity.EquippedHull || Random.value >= zoneRenderer.Settings.LootDropProbability) + continue; + + zoneRenderer.DropItem( + entity.Position, + Random.onUnitSphere * zoneRenderer.Settings.LootDropVelocity, + gear.EquippableItem); + } + + foreach (var cargo in entity.CargoBays) + { + foreach (var item in cargo.Cargo.Keys.ToArray()) + { + zoneRenderer.DropItem( + entity.Position, + Random.onUnitSphere * zoneRenderer.Settings.LootDropVelocity, + item); + } + } + } + + if (!entity.Zone.Entities.Remove(entity)) + return false; + + QueueRunCheckpoint("entity-destroyed"); + return true; + } + public bool CommitCargoItemTransfer( EquippedCargoBay origin, EquippedCargoBay destination, diff --git a/Assets/Scripts/Gameplay/EntityInstance.cs b/Assets/Scripts/Gameplay/EntityInstance.cs index a0f0b860f..950dae92e 100644 --- a/Assets/Scripts/Gameplay/EntityInstance.cs +++ b/Assets/Scripts/Gameplay/EntityInstance.cs @@ -8,7 +8,6 @@ using UnityEngine; using Unity.Mathematics; using static Unity.Mathematics.math; using float2 = Unity.Mathematics.float2; -using Random = UnityEngine.Random; public class EntityInstance : MonoBehaviour { @@ -421,37 +420,18 @@ public class EntityInstance : MonoBehaviour _subscriptions.Add(Entity.HullDamage.Subscribe(_ => { - if (Entity.Hull.Durability < .01f) + if (!_destroyed && Entity.Hull.Durability < .01f) { if (!this) return; - _destroyed = true; - foreach (var gear in Entity.Equipment) - { - if (gear != Entity.EquippedHull && Random.value < ZoneRenderer.Settings.LootDropProbability) - { - ZoneRenderer.DropItem( - Entity.Position, - Random.onUnitSphere * ZoneRenderer.Settings.LootDropVelocity, - gear.EquippableItem); - } - } + if (ActionGameManager.Instance?.CommitEntityDestroyed(entity, ZoneRenderer) != true) + return; - foreach (var cargo in Entity.CargoBays) - { - foreach (var item in cargo.Cargo.Keys) - { - ZoneRenderer.DropItem( - Entity.Position, - Random.onUnitSphere * ZoneRenderer.Settings.LootDropVelocity, - item); - } - } + _destroyed = true; if (DestroyEffect != null) { var t = Instantiate(DestroyEffect).transform; t.position = transform.position; } - entity.Zone.Entities.Remove(entity); } })); diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 3be2d7a57..f8dc024aa 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -1414,6 +1414,12 @@ First Aetheria surfaces to publish: after a successful commit, but it no longer stores items into cargo bays directly; gameplay owns the cargo mutation and queues the typed run checkpoint. + - Done: route entity destruction through + `ActionGameManager.CommitEntityDestroyed`. `EntityInstance` observes hull + death and may spawn the local destruction effect, but gameplay owns + equipment/cargo drop decisions, zone entity removal, and the typed run + checkpoint. Dropped world pickups are still presentation/runtime objects + until the runtime object graph pass gives them typed documents. - Replace the next concrete uGUI screen with an Eve-owned surface. - Move the staged packages into the Eve repo once its worktree is clean, then import them back into Aetheria from Eve instead of carrying a local copy. @@ -1749,7 +1755,9 @@ instantiation, credits, dock assignment, current entity, and checkpoint. Docked current-ship selection follows the same rule: UI requests selection, gameplay owns `CurrentEntity`, `DockingBay.DockedShip`, and checkpoint. Loot pickup follows the same rule: collision code reports the pickup opportunity, gameplay -owns cargo storage and checkpoint. Any +owns cargo storage and checkpoint. Entity destruction follows the same rule: +instance code observes death, gameplay owns drop decisions, zone removal, and +checkpoint. Any predicate that still needs legacy DTO objects must earn that dependency by using behavior objects or simulation-only methods that typed facets do not yet expose.