diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 61e56102a..065ffb53a 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index d3b2482d8..f3c6d3553 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -846,6 +846,9 @@ static void RequireMainMenuContinueRunState(string root) "entity.RecordKey", "ReplaceZoneEntitiesFromTypedSnapshots", "Zone.Agents.Clear()", + "FlattenEntityGraph(Zone)", + "RestoreChildAndDockingRelationships", + "RestoreCurrentEntityBinding", "RestoreEntityContactsFromTypedSnapshot", "entity.Target.Value = target", "string.Equals(entitySnapshot.RecordKey, currentEntityKey", diff --git a/Aetheria.State/README.md b/Aetheria.State/README.md index f48c7e8b2..a539445e9 100644 --- a/Aetheria.State/README.md +++ b/Aetheria.State/README.md @@ -177,11 +177,13 @@ orbit/body rows into `AetheriaZoneState`, so `ZoneConstructionBlueprint` is a one-shot construction input rather than the retained surface carrying celestial graph facts. Entity checkpoints persist hull simulation grids into `AetheriaEntitySnapshot`, so `EntityConstructionBlueprint` is no longer the only surface carrying those live -values. Continue restore lowers the current zone entity graph from exact typed -entity snapshot records, including position, velocity, target/contact state, +values. Run checkpoints project the flattened live entity graph, including +docked children, into exact typed entity snapshot records. Continue restore +lowers the current zone entity graph from those records, including position, +velocity, target/contact state, child/docking relationships, shutdown/heatsink state, thermal exposure, active consumable timers, and typed stat grids for temperature, thermal mass, armor, max armor, and hull conductivity. Broader runtime object graphs -such as child/docked activation and behavior-private simulation state remain legacy until dedicated typed runtime documents exist. Live behavior +such as behavior-private simulation state remain legacy until dedicated typed runtime documents exist. Live behavior instances expose typed behavior kind and group through `Behavior.Kind` and `Behavior.Group`; behavior construction now reads typed payload fields through `RuntimeBehaviorDefinition` instead of projecting through config DTOs. Stat diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 295359e7c..b25f29b2a 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -619,6 +619,7 @@ public class ActionGameManager : MonoBehaviour private AetheriaRuntimeRunCheckpointCommit ProjectRunCheckpoint() { + var entityGraph = FlattenEntityGraph(Zone); return new AetheriaRuntimeRunCheckpointCommit { RunId = IsTutorial ? "tutorial" : "local", @@ -626,7 +627,7 @@ public class ActionGameManager : MonoBehaviour EntranceZoneIndex = ZoneIndex(CurrentGalaxy?.Entrance), ExitZoneIndex = ZoneIndex(CurrentGalaxy?.Exit), CurrentZoneIndex = ZoneIndex(Zone?.GalaxyZone), - CurrentZoneEntityIndex = Zone?.Entities?.IndexOf(CurrentEntity) ?? -1, + CurrentZoneEntityIndex = entityGraph.IndexOf(CurrentEntity), GenerationSeed = CurrentGalaxy?.GenerationSeed ?? 0, DiscoveredZoneIndices = CurrentGalaxy?.DiscoveredZones .Select(ZoneIndex) @@ -696,6 +697,7 @@ public class ActionGameManager : MonoBehaviour private AetheriaRuntimeZoneSnapshotCommit ProjectZoneSnapshot(Zone zone) { var galaxyZone = zone.GalaxyZone; + var entityGraph = FlattenEntityGraph(zone); return new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = ZoneIndex(galaxyZone), @@ -715,8 +717,8 @@ public class ActionGameManager : MonoBehaviour OwnerFactionIndex = FactionIndex(galaxyZone?.Owner), Orbits = ProjectZoneOrbits(zone), Bodies = ProjectZoneBodies(zone), - Entities = zone.Entities - .Select((entity, index) => 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.