From c8b58fa44f053e5abd952cb7a5ac7daba782383f Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 16 Jun 2026 08:18:16 +0100 Subject: [PATCH] Persist docked entity graph in typed checkpoints --- .voidbot/state/aetheria.cc | Bin 148602 -> 148330 bytes Aetheria.State.Verify/Program.cs | 3 + Aetheria.State/README.md | 8 +- Assets/Scripts/Gameplay/ActionGameManager.cs | 122 ++++++++++++++++--- docs/aetheria-perfect-machine-map.md | 10 +- 5 files changed, 122 insertions(+), 21 deletions(-) diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 61e56102ad60305715c20e612b257c90b5f26f3d..065ffb53a0262943df078148ed56a49c88a99027 100644 GIT binary patch delta 1674 zcmZuyO=u)V6lN+xH@K*mK){1fJuHFg+02BUKS7!NY!F#oqVW(Gq0(J5U8cLLt*V}6 zK$u;2QTHf0GzfyAlA~U9VPz2y-o3_)c=aZD5(HmWb3m|zhke9XX?09T?8T!cv;aWMjq05kC@<`KITw>m=H<3W1jJ>LPCNFsNcB zF(l1J@k@iOO%)5JinODpV|&GlqY+XmNx((KPC;ihWI&N=u22V4p4K5mEof*oK3R<% z!@85rDnYg8Baz|@c)?1SphRG(8Zo0Ausyw`B+x8NIMUj-$vmmIQ8V}Mj+b|fP6qR| zU?gOS8zFg`CRQ>Z#h`MOBxQj-u}$_@7CPh`R6HJ#=TL;Qjb62&;&||uqsLASoWx9& z(u^@(b;Fhft}B~RgFr#5BW`HYI7feJZ1BayEf&);mnz5sSR7&LBBQA~l+!{3u}MF`AoLDjbVCdI8%pW@-XQWu0p_qhU(Wq|yPjp~as~ zvO2x^@n&vv?DTv*5OP%!PNX`9G{-SPe_a|CR2eJ!DZvUeV>Dn#Iv9#!T_vL&&0_u9 za4p1!22vywH-KH7Kql~f=!y;*_AbPZn;6wgQ4}}TfIk*JjF*c4TI68*@ly;Rp{i~O ztDRu6ahudCw(q;^95oP8>7sqMIHHy9{FFK;FL4DQ$)!Dn?8fgWXzOyz>%SExLDV&lm7R z?XG`X>k#woxldSp)gfz3-Tp@LQ-{3b9{%W%-7Dv%H;Z_w{G)qn*tQ%bWgsYh%s7dDodg_}Tq%>TK1XiJ9Tl*@itkJ^P${;nMBJ**lA~ z&f(&$`~BjjiA)7J8B?EhO_+{FRR9cf94! z!5@1smRDP@xBJDF`ZlSfNGb;Soy9a@iu72@B-Jb=K`te7+akmk;rKq_IwHV9Y^CyU z+kI+eOB0%gNCRvs3$!Ad${y2ggOvuFD2=3qCL-;P3x`eC*5^S?d2;N0vlVDA(jA(x zU7FG!ljM5U%eL)u$256OisXjr^Lm~l7afI)us)MTm9HQwrJ0I^CV3hngF&lU3{>b! z;*LRTs3VzVT|Qhwl0@`RG;cpQO`8h0*h*V!$MD&;axpa1#_d zk|OU_tbq=tWp2A9dvVS($x}@-s-qd{rf;y)EQ`%3GA8Zt zQ6Di%fjH7d4rPM=YYtKj6zK}taBoOZXpQjnWLY~^fI{zkJO#&=N7|oAqvm_m`ckvh zls|T6u0%Wz+o1^d5oZZ344Pb8cw9jUJdU9`-88s4j%(&-$>dNlQ`g+1V565Nc49iBAXpfh@dNQT9MuzwmMQ7xI+u)8BDTa$CKTkl zMJ}_5_PLNAW@W~Mt1wGJ&O!g(xhh7}3{RQbmNMd@U2Uv! zLaAVP$d#^U9txYHOZORpn2d=b0Wr~ysfal9!?>T47;fi~C zRQ#`%AKF1DY}lCW0n`=VDanuFwyPbaTDL!i(GPasyEr-$Z()UYIeM4uR?eaQX7YAt z5>+I7B8G>WP3T$clOSdkX6-GF=@<*Rs$WZWtYu>v1Xrbxo`*11o3ex7-f45bOEcUJ zgfynI6L@&n77xHO^9BI3rL2U^G)aN(*MDkKYw)Y z4HyGMs)j#SeAPEM+BJ@`A}hDHov3{MY4^#6g%g*sXDj-8Ih7&Fvk;CbKU#I~oc*>A zR3O&qjK* ProjectEntitySnapshot(zone, entity, index)) + Entities = entityGraph + .Select((entity, index) => ProjectEntitySnapshot(entityGraph, entity, index)) .ToArray(), DroppedPickups = ProjectDroppedPickups(zone) }; @@ -913,7 +915,29 @@ public class ActionGameManager : MonoBehaviour }; } - private AetheriaRuntimeEntitySnapshotCommit ProjectEntitySnapshot(Zone zone, Entity entity, int entityIndex) + private static List FlattenEntityGraph(Zone zone) + { + var entities = new List(); + if (zone == null) + return entities; + + var visited = new HashSet(); + foreach (var entity in zone.Entities) + AddEntityAndChildren(entity); + return entities; + + void AddEntityAndChildren(Entity entity) + { + if (entity == null || !visited.Add(entity)) + return; + + entities.Add(entity); + foreach (var child in entity.Children) + AddEntityAndChildren(child); + } + } + + private AetheriaRuntimeEntitySnapshotCommit ProjectEntitySnapshot(IReadOnlyList entityGraph, Entity entity, int entityIndex) { return new AetheriaRuntimeEntitySnapshotCommit { @@ -927,7 +951,7 @@ public class ActionGameManager : MonoBehaviour DirectionY = entity.Direction.y, VelocityX = entity.Velocity.x, VelocityY = entity.Velocity.y, - TargetEntityIndex = entity.Target?.Value == null ? -1 : zone.Entities.IndexOf(entity.Target.Value), + TargetEntityIndex = entity.Target?.Value == null ? -1 : entityGraph.IndexOf(entity.Target.Value), IsActive = entity.Active, HeatsinksEnabled = entity.HeatsinksEnabled, OverrideShutdown = entity.OverrideShutdown, @@ -946,9 +970,9 @@ public class ActionGameManager : MonoBehaviour .ToArray() ?? Array.Empty(), Visibility = entity.Visibility, VisibilitySourceCount = entity.VisibilitySources.Count, - Contacts = ProjectEntityContacts(zone, entity), + Contacts = ProjectEntityContacts(entityGraph, entity), ChildEntityIndices = entity.Children? - .Select(child => zone.Entities.IndexOf(child)) + .Select(child => entityGraph.IndexOf(child)) .Where(index => index >= 0) .ToArray() ?? Array.Empty(), WeaponGroups = entity.WeaponGroups? @@ -957,17 +981,17 @@ public class ActionGameManager : MonoBehaviour StatGrids = ProjectEntityStatGrids(entity), ActiveConsumables = ProjectActiveConsumables(entity), BehaviorProgress = ProjectBehaviorProgress(entity), - WeaponStates = ProjectWeaponStates(zone, entity), + WeaponStates = ProjectWeaponStates(entityGraph, entity), BehaviorStates = ProjectBehaviorStates(entity) }; } - private static AetheriaRuntimeEntityContactCommit[] ProjectEntityContacts(Zone zone, Entity entity) + private static AetheriaRuntimeEntityContactCommit[] ProjectEntityContacts(IReadOnlyList entityGraph, Entity entity) { return entity.EntityInfoGathered .Select(contact => { - var targetIndex = zone.Entities.IndexOf(contact.Key); + var targetIndex = entityGraph.IndexOf(contact.Key); entity.EntityHostility.TryGetValue(contact.Key, out var hostile); return new AetheriaRuntimeEntityContactCommit { @@ -1179,22 +1203,22 @@ public class ActionGameManager : MonoBehaviour } } - private static AetheriaRuntimeWeaponStateCommit[] ProjectWeaponStates(Zone zone, Entity entity) + private static AetheriaRuntimeWeaponStateCommit[] ProjectWeaponStates(IReadOnlyList entityGraph, Entity entity) { var states = new List(); for (var ownerIndex = 0; ownerIndex < entity.Equipment.Count; ownerIndex++) - states.AddRange(ProjectWeaponStates(zone, "equipment", ownerIndex, entity.Equipment[ownerIndex].Behaviors)); + states.AddRange(ProjectWeaponStates(entityGraph, "equipment", ownerIndex, entity.Equipment[ownerIndex].Behaviors)); var activeConsumables = entity.ActiveConsumables; for (var ownerIndex = 0; ownerIndex < activeConsumables.Count; ownerIndex++) - states.AddRange(ProjectWeaponStates(zone, "active_consumable", ownerIndex, activeConsumables[ownerIndex].Behaviors)); + states.AddRange(ProjectWeaponStates(entityGraph, "active_consumable", ownerIndex, activeConsumables[ownerIndex].Behaviors)); return states.ToArray(); } private static IEnumerable ProjectWeaponStates( - Zone zone, + IReadOnlyList entityGraph, string ownerKind, int ownerIndex, IReadOnlyList behaviors) @@ -1243,7 +1267,7 @@ public class ActionGameManager : MonoBehaviour if (weapon is LockWeapon lockWeapon) { state.LockProgress = lockWeapon.LockProgress; - state.LockTargetEntityIndex = lockWeapon.LockTarget == null ? -1 : zone.Entities.IndexOf(lockWeapon.LockTarget); + state.LockTargetEntityIndex = lockWeapon.LockTarget == null ? -1 : entityGraph.IndexOf(lockWeapon.LockTarget); } yield return state; @@ -2577,6 +2601,8 @@ public class ActionGameManager : MonoBehaviour foreach (var entity in restoredEntities.Values) entity.Activate(); + RestoreChildAndDockingRelationships(entitySnapshots, restoredEntities); + foreach (var entitySnapshot in entitySnapshots) { if (!restoredEntities.TryGetValue(entitySnapshot.RecordKey, out var entity)) @@ -2592,7 +2618,71 @@ public class ActionGameManager : MonoBehaviour } if (restoredEntities.TryGetValue(currentEntityKey, out var currentEntity)) - BindToEntity(currentEntity); + RestoreCurrentEntityBinding(currentEntity); + } + + private void RestoreChildAndDockingRelationships( + IReadOnlyList entitySnapshots, + IReadOnlyDictionary restoredEntities) + { + foreach (var entitySnapshot in entitySnapshots) + { + if (!restoredEntities.TryGetValue(entitySnapshot.RecordKey, out var parent)) + continue; + + foreach (var childKey in entitySnapshot.ChildEntityKeys) + { + if (!restoredEntities.TryGetValue(childKey, out var child) || + child == parent || + child.Parent == parent) + { + continue; + } + + child.RemoveParent(); + child.SetParent(parent); + } + + for (var bayIndex = 0; bayIndex < entitySnapshot.DockingBayAssignments.Count; bayIndex++) + { + var childIndex = entitySnapshot.DockingBayAssignments[bayIndex]; + if (childIndex < 0 || + childIndex >= entitySnapshot.ChildEntityKeys.Count || + bayIndex >= parent.DockingBays.Count || + !restoredEntities.TryGetValue(entitySnapshot.ChildEntityKeys[childIndex], out var child) || + !(child is Ship ship)) + { + continue; + } + + var dockingBay = parent.DockingBays[bayIndex]; + dockingBay.DockedShip = ship; + if (ship.Parent != parent) + { + ship.RemoveParent(); + ship.SetParent(parent); + } + if (Zone.Entities.Contains(ship)) + Zone.Entities.Remove(ship); + ship.Deactivate(); + } + } + } + + private void RestoreCurrentEntityBinding(Entity currentEntity) + { + if (currentEntity.Parent != null) + { + var dockingBay = currentEntity.Parent.DockingBays.FirstOrDefault(bay => bay.DockedShip == currentEntity); + if (dockingBay != null && currentEntity.Parent is OrbitalEntity) + { + CurrentEntity = currentEntity; + DoDock(currentEntity.Parent, dockingBay); + return; + } + } + + BindToEntity(currentEntity); } private void RestoreEntityContactsFromTypedSnapshot( diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 6002d0dcd..6773381d5 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -979,10 +979,16 @@ First Aetheria surfaces to publish: heatstroke/hypothermia exposure, active consumable item/timer rows, typed stat grids for temperature, thermal mass, armor, max armor, and hull-conductivity axes, then reconnects target/contact rows among restored - entities and binds the saved current entity as the player ship. + entities, restores child/docking relationships from typed child keys and + docking-bay assignments, and binds the saved current entity as the player + ship or docked current ship. + - Done: project run checkpoint entity snapshots from the flattened live + entity graph, not only `Zone.Entities`, so docked child ships can receive + typed entity records and stable child/docking references instead of + disappearing from the `.zone` replacement graph. - Remaining: add typed documents/mappers for runtime object graphs, complete full Continue/run entity graph reconstruction from typed state - for child/docked entity activation, restored active-consumable behavior runtime internals, + for restored active-consumable behavior runtime internals, behavior-private state not covered by progress, weapon, sensor, radiator, reactor, or capacitor rows, typed behavior factory construction, and any catalog fields not covered by the stable scalar/fingerprint/payload pass.