diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 1a9bf94e8..921f5fd50 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Aetheria.State.Unity.Smoke/Program.cs b/Aetheria.State.Unity.Smoke/Program.cs index 7964fabbb..1982b4d61 100644 --- a/Aetheria.State.Unity.Smoke/Program.cs +++ b/Aetheria.State.Unity.Smoke/Program.cs @@ -907,6 +907,7 @@ try packageZones[0].DroppedPickups[0].Item.ItemKey != "aetheria.item_definition:legacy:smoke:ore" || packageZones[0].DroppedPickups[0].Item.Quantity != 11 || packageEntities.Count != 1 || + packageEntities[0].RecordKey != "global:aetheria.run_state.smoke-run.zone.0.entity.0.v1" || packageEntities[0].Name != "Unity Smoke Ship" || packageEntities[0].FactionKey != "aetheria.corporation:legacy:smoke:faction" || packageEntities[0].Equipment[0].Quality != 0.9 || diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 3dc04a364..e83e28554 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -791,6 +791,24 @@ static void RequireMainMenuContinueRunState(string root) var actionGameManager = File.Exists(actionGameManagerPath) ? File.ReadAllText(actionGameManagerPath) : throw new InvalidOperationException("Cannot verify Continue run authority; ActionGameManager.cs is missing."); + var packageSnapshotPath = Path.Combine( + root, + "Packages", + "org.gamecult.aetheria.state", + "Runtime", + "AetheriaRuntimeCatalogSnapshot.cs"); + var packageSnapshot = File.Exists(packageSnapshotPath) + ? File.ReadAllText(packageSnapshotPath) + : throw new InvalidOperationException("Cannot verify Continue entity identity; package runtime snapshots are missing."); + var packageStorePath = Path.Combine( + root, + "Packages", + "org.gamecult.aetheria.state", + "Runtime", + "AetheriaRuntimeCatalogStore.cs"); + var packageStore = File.Exists(packageStorePath) + ? File.ReadAllText(packageStorePath) + : throw new InvalidOperationException("Cannot verify Continue entity readback; package runtime store is missing."); var requiredMenuSymbols = new[] { @@ -823,6 +841,11 @@ static void RequireMainMenuContinueRunState(string root) "AetheriaRuntimeRunStateSnapshot ContinueRunState", "ResolveStartZone(continuingRun)", "if (continuingRun != null)", + "RestoreCurrentEntityFromTypedRun(continuingRun)", + "ReadEntitySnapshots(RuntimeStateFilePath)", + "entity.RecordKey", + "CreateEntityConstructionBlueprint(entitySnapshot, true)", + "BindToEntity(entity)", "ContinueRunState = null", "RestoreDroppedPickupsFromTypedZoneState" }; @@ -837,6 +860,25 @@ static void RequireMainMenuContinueRunState(string root) "ActionGameManager no longer has the typed Continue boot path: " + string.Join(", ", missingGameplaySymbols)); } + + var requiredPackageSymbols = new[] + { + "public string RecordKey", + "ReadEntitySnapshotPayload(record.Key, record.Payload)" + }; + + var missingPackageSymbols = requiredPackageSymbols + .Where(symbol => + !packageSnapshot.Contains(symbol, StringComparison.Ordinal) && + !packageStore.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingPackageSymbols.Length > 0) + { + throw new InvalidOperationException( + "Unity package entity readback no longer preserves record identity for Continue: " + + string.Join(", ", missingPackageSymbols)); + } } static void RequirePropertiesPanelReadOnlyInspector(string root) diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 641c3fb8b..04457c1b4 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -250,6 +250,45 @@ public class ActionGameManager : MonoBehaviour return blueprint; } + private EntityConstructionBlueprint CreateEntityConstructionBlueprint(AetheriaRuntimeEntitySnapshot entity, bool isCurrentEntity) + { + if (entity == null) + return null; + + var hull = CreateEquippableLoadoutItem(new AetheriaRuntimeLoadoutItemSnapshot( + entity.HullItemKey, + 1, + 1, + 1, + true, + false)); + if (hull == null) + return null; + + EntityConstructionBlueprint blueprint = string.Equals(entity.Kind, "orbital", StringComparison.OrdinalIgnoreCase) + ? new OrbitalEntityConstructionBlueprint() + : new ShipConstructionBlueprint + { + Position = new float3((float)entity.PositionX, (float)entity.PositionY, (float)entity.PositionZ), + Direction = new float2((float)entity.DirectionX, (float)entity.DirectionY), + IsPlayerShip = isCurrentEntity + }; + + blueprint.Name = entity.Name ?? ""; + blueprint.FactionKey = entity.FactionKey ?? ""; + blueprint.Faction = ParseLegacyGuidFromReferenceKey(blueprint.FactionKey, "aetheria.corporation"); + blueprint.Hull = hull; + blueprint.Equipment = CreateEquippableSlots(entity.Equipment); + blueprint.CargoBays = CreateEquippableSlots(entity.CargoBays); + blueprint.DockingBays = CreateEquippableSlots(entity.DockingBays); + blueprint.CargoContents = CreateCargoBayContents(entity.CargoContents); + blueprint.DockingBayContents = CreateCargoBayContents(entity.DockingBayContents); + blueprint.DockingBayAssignments = entity.DockingBayAssignments.ToArray(); + blueprint.WeaponGroups = entity.WeaponGroups.Select(group => group.ToArray()).ToArray(); + blueprint.Children = Array.Empty(); + return blueprint; + } + private (int2 position, EquippableItem item)[] CreateEquippableSlots( IReadOnlyList slots) { @@ -259,6 +298,21 @@ public class ActionGameManager : MonoBehaviour .ToArray(); } + private (int2 position, EquippableItem item)[] CreateEquippableSlots( + IReadOnlyList slots) + { + return slots + .Select(slot => (position: new int2(slot.X, slot.Y), item: CreateEquippableLoadoutItem(new AetheriaRuntimeLoadoutItemSnapshot( + slot.ItemKey, + slot.Quality, + slot.Durability, + slot.Quantity, + slot.Enabled, + slot.OverrideShutdown)))) + .Where(slot => slot.item != null) + .ToArray(); + } + private (int2 position, ItemInstance item)[][] CreateCargoBayContents( IReadOnlyList bays) { @@ -2428,6 +2482,7 @@ public class ActionGameManager : MonoBehaviour if (continuingRun != null) { + RestoreCurrentEntityFromTypedRun(continuingRun); ContinueRunState = null; return; } @@ -2447,6 +2502,50 @@ public class ActionGameManager : MonoBehaviour } } + private void RestoreCurrentEntityFromTypedRun(AetheriaRuntimeRunStateSnapshot run) + { + if (run == null || + run.CurrentZoneIndex < 0 || + run.CurrentZoneEntityIndex < 0) + { + return; + } + + var entityKey = $"global:aetheria.run_state.{run.RunId}.zone.{run.CurrentZoneIndex}.entity.{run.CurrentZoneEntityIndex}.v1"; + AetheriaRuntimeEntitySnapshot entitySnapshot; + try + { + entitySnapshot = AetheriaRuntimeCatalogStore.ReadEntitySnapshots(RuntimeStateFilePath) + .FirstOrDefault(entity => string.Equals(entity.RecordKey, entityKey, StringComparison.Ordinal)); + } + catch (Exception ex) + { + Debug.LogError($"Failed to read typed current entity snapshot {entityKey}: {ex}"); + return; + } + + var blueprint = CreateEntityConstructionBlueprint(entitySnapshot, true); + if (blueprint == null) + { + Debug.LogWarning($"Typed current entity snapshot {entityKey} could not be lowered into a runtime entity."); + return; + } + + var entity = EntityConstructionBlueprintProjector.InstantiateFromBlueprint(ItemManager, Zone, blueprint, true); + if (entity == null) + return; + + entity.Position = new float3((float)entitySnapshot.PositionX, (float)entitySnapshot.PositionY, (float)entitySnapshot.PositionZ); + entity.Direction = new float2((float)entitySnapshot.DirectionX, (float)entitySnapshot.DirectionY); + entity.Velocity = new float2((float)entitySnapshot.VelocityX, (float)entitySnapshot.VelocityY); + entity.OverrideShutdown = entitySnapshot.OverrideShutdown; + entity.TractorPower = (float)entitySnapshot.TractorPower; + entity.Zone = Zone; + Zone.Entities.Add(entity); + entity.Activate(); + BindToEntity(entity); + } + private static GalaxyZone ResolveStartZone(AetheriaRuntimeRunStateSnapshot run) { if (CurrentGalaxy?.Zones == null || CurrentGalaxy.Zones.Length == 0) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs index 10020dfae..0731de73b 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs @@ -847,6 +847,7 @@ namespace GameCult.Aetheria.State.Unity public sealed class AetheriaRuntimeEntitySnapshot { public AetheriaRuntimeEntitySnapshot( + string recordKey, string name, string kind, double positionX, @@ -882,6 +883,7 @@ namespace GameCult.Aetheria.State.Unity int visibilitySourceCount, IReadOnlyList contacts) { + RecordKey = recordKey; Name = name; Kind = kind; PositionX = positionX; @@ -918,6 +920,7 @@ namespace GameCult.Aetheria.State.Unity Contacts = contacts; } + public string RecordKey { get; } public string Name { get; } public string Kind { get; } public double PositionX { get; } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs index f40842c1f..2e03036c6 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs @@ -162,7 +162,7 @@ namespace GameCult.Aetheria.State.Unity schemaName != EntitySnapshotSchema) continue; - entities.Add(ReadEntitySnapshotPayload(record.Payload)); + entities.Add(ReadEntitySnapshotPayload(record.Key, record.Payload)); } return entities.OrderBy(entity => entity.Name, StringComparer.Ordinal).ToArray(); @@ -534,7 +534,7 @@ namespace GameCult.Aetheria.State.Unity return pickups; } - private static AetheriaRuntimeEntitySnapshot ReadEntitySnapshotPayload(byte[] payload) + private static AetheriaRuntimeEntitySnapshot ReadEntitySnapshotPayload(string recordKey, byte[] payload) { var reader = new MessagePackReader(payload); var fields = reader.ReadArrayHeader(); @@ -570,6 +570,7 @@ namespace GameCult.Aetheria.State.Unity var contacts = ReadFieldEntityContacts(ref reader, fields, 29); SkipRemaining(ref reader, fields, 30); return new AetheriaRuntimeEntitySnapshot( + recordKey, name, kind, position.X, diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index c055f0a5f..b7a9a3a07 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -967,13 +967,23 @@ First Aetheria surfaces to publish: `AetheriaRuntimeCatalogStore`, selects the newest run, regenerates the galaxy from the saved generation seed, and passes the selected run to `ActionGameManager` so boot enters the saved current zone and lowers typed - pickup rows. Entity/player reconstruction from `AetheriaEntitySnapshot` - remains future Continue work, not a fake restored ship. + pickup rows. + - Done: restore the current player entity from typed state during Continue + boot. Package entity readback now preserves `AetheriaEntitySnapshot` record + identity, and `ActionGameManager` resolves the exact + `RunId + ZoneIndex + CurrentZoneEntityIndex` entity record, lowers it + through the existing construction blueprint path, restores hull, + equipment, cargo, docking contents, weapon groups, position, direction, + velocity, shutdown override, and tractor power, then binds the lowered + entity as the current player ship. - Remaining: add typed documents/mappers for runtime object graphs, - complete Continue/run entity reconstruction from typed state, typed - behavior factory construction, remaining behavior-private state not covered - by progress, weapon, sensor, radiator, reactor, or capacitor rows, and any - catalog fields not covered by the stable scalar/fingerprint/payload pass. + complete full Continue/run entity graph reconstruction from typed state + for non-current entities, child/docked entity activation, targets, + contacts, active consumables, behavior-private state not covered by + progress, weapon, sensor, radiator, reactor, or capacitor rows, + heatstroke/hypothermia runtime restore, typed behavior factory + construction, and any catalog fields not covered by the stable + scalar/fingerprint/payload pass. 4. Runtime cutover - Done: add a Unity-facing typed catalog read facade and smoke proving it can