diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 7b237ca28..c13e86833 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 7223d16b9..7964fabbb 100644 --- a/Aetheria.State.Unity.Smoke/Program.cs +++ b/Aetheria.State.Unity.Smoke/Program.cs @@ -885,6 +885,7 @@ try packageRuns[0].FactionRelationships.Count != 1 || packageRuns[0].FactionRelationships[0].FactionKey != "aetheria.corporation:legacy:smoke:faction" || packageZones.Count != 1 || + packageZones[0].RecordKey != "global:aetheria.run_state.smoke-run.zone.0.v1" || packageZones[0].Name != "Unity Smoke Zone" || packageZones[0].EntityKeys.Count != 1 || packageZones[0].Orbits.Count != 1 || diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index ce6bb4bdb..7cf2fe78e 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -475,7 +475,7 @@ Console.WriteLine("Weapon group authority: UI assignment flows through gameplay Console.WriteLine("Inventory transfer authority: UI transfer and drag/drop requests flow through gameplay checkpoint commits"); Console.WriteLine("Loot pickup authority: collision pickup requests flow through gameplay checkpoint commits"); Console.WriteLine("Entity destruction authority: hull-death observers flow through gameplay checkpoint commits"); -Console.WriteLine("Dropped pickup state: zone checkpoints carry typed dropped-pickup snapshots"); +Console.WriteLine("Dropped pickup state: zone checkpoints carry typed dropped-pickup snapshots and keyed live lowering"); Console.WriteLine("Trade purchase authority: UI buy requests flow through gameplay checkpoint commits"); Console.WriteLine("Inventory loadout restore authority: UI restore requests flow through gameplay checkpoint commits"); Console.WriteLine("Docked current-ship authority: UI selection requests flow through gameplay checkpoint commits"); @@ -1246,18 +1246,23 @@ static void RequireDroppedPickupCheckpointState(string root) }, [Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeCatalogStore.cs")] = new[] { + "ReadZoneStatePayload(record.Key, record.Payload)", "ReadFieldDroppedPickups", "AetheriaRuntimeDroppedPickupSnapshot" }, [Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeCatalogSnapshot.cs")] = new[] { + "public string RecordKey", "AetheriaRuntimeDroppedPickupSnapshot", "public IReadOnlyList DroppedPickups" }, [Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs")] = new[] { "ProjectDroppedPickups", - "DroppedPickups = ProjectDroppedPickups(zone)" + "DroppedPickups = ProjectDroppedPickups(zone)", + "RestoreDroppedPickupsFromTypedZoneState", + "AetheriaRuntimeCatalogStore.ReadZoneStates(RuntimeStateFilePath)", + "ZoneRenderer.DropItem(" }, [Path.Combine(root, "Assets", "Scripts", "Zone Display", "ZoneRenderer.cs")] = new[] { @@ -1266,6 +1271,7 @@ static void RequireDroppedPickupCheckpointState(string root) [Path.Combine(root, "Aetheria.State.Unity.Smoke", "Program.cs")] = new[] { "new AetheriaRuntimeDroppedPickupCommit", + "packageZones[0].RecordKey", "packageZones[0].DroppedPickups" } }; diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 613bea880..67b8ee0de 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -2316,6 +2316,7 @@ public class ActionGameManager : MonoBehaviour } ZoneRenderer.LoadZone(Zone); + RestoreDroppedPickupsFromTypedZoneState(galaxyZone); if (CurrentEntity != null) { @@ -2324,6 +2325,49 @@ public class ActionGameManager : MonoBehaviour } } + private void RestoreDroppedPickupsFromTypedZoneState(GalaxyZone galaxyZone) + { + if (RuntimeCatalog == null || + ZoneRenderer == null || + galaxyZone == null) + { + return; + } + + var zoneIndex = ZoneIndex(galaxyZone); + if (zoneIndex < 0) + return; + + var runId = IsTutorial ? "tutorial" : "local"; + var zoneKey = $"global:aetheria.run_state.{runId}.zone.{zoneIndex}.v1"; + AetheriaRuntimeZoneStateSnapshot zoneState; + try + { + zoneState = AetheriaRuntimeCatalogStore.ReadZoneStates(RuntimeStateFilePath) + .FirstOrDefault(zone => string.Equals(zone.RecordKey, zoneKey, StringComparison.Ordinal)); + } + catch (Exception ex) + { + Debug.LogError($"Failed to read typed dropped pickup state for zone {zoneKey}: {ex}"); + return; + } + + if (zoneState?.DroppedPickups == null || zoneState.DroppedPickups.Count == 0) + return; + + foreach (var pickup in zoneState.DroppedPickups.OrderBy(pickup => pickup.PickupIndex)) + { + var item = CreateLoadoutItem(pickup.Item); + if (item == null) + continue; + + ZoneRenderer.DropItem( + new Vector3((float)pickup.PositionX, (float)pickup.PositionY, (float)pickup.PositionZ), + new Vector3((float)pickup.VelocityX, (float)pickup.VelocityY, (float)pickup.VelocityZ), + item); + } + } + private void ToggleFullscreenMenu(GameObject menu) { if (EventSystem.current != null && EventSystem.current.currentSelectedGameObject != null && EventSystem.current.currentSelectedGameObject.GetComponent() != null) return; diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs index 482f59459..10020dfae 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs @@ -774,6 +774,7 @@ namespace GameCult.Aetheria.State.Unity public sealed class AetheriaRuntimeZoneStateSnapshot { public AetheriaRuntimeZoneStateSnapshot( + string recordKey, string name, double positionX, double positionY, @@ -785,6 +786,7 @@ namespace GameCult.Aetheria.State.Unity IReadOnlyList bodies, IReadOnlyList droppedPickups) { + RecordKey = recordKey; Name = name; PositionX = positionX; PositionY = positionY; @@ -797,6 +799,7 @@ namespace GameCult.Aetheria.State.Unity DroppedPickups = droppedPickups; } + public string RecordKey { get; } public string Name { get; } public double PositionX { get; } public double PositionY { get; } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs index eee6ec794..f40842c1f 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs @@ -143,7 +143,7 @@ namespace GameCult.Aetheria.State.Unity schemaName != ZoneStateSchema) continue; - zones.Add(ReadZoneStatePayload(record.Payload)); + zones.Add(ReadZoneStatePayload(record.Key, record.Payload)); } return zones.OrderBy(zone => zone.Name, StringComparer.Ordinal).ToArray(); @@ -479,7 +479,7 @@ namespace GameCult.Aetheria.State.Unity generationSeed); } - private static AetheriaRuntimeZoneStateSnapshot ReadZoneStatePayload(byte[] payload) + private static AetheriaRuntimeZoneStateSnapshot ReadZoneStatePayload(string recordKey, byte[] payload) { var reader = new MessagePackReader(payload); var fields = reader.ReadArrayHeader(); @@ -494,6 +494,7 @@ namespace GameCult.Aetheria.State.Unity var droppedPickups = ReadFieldDroppedPickups(ref reader, fields, 8); SkipRemaining(ref reader, fields, 9); return new AetheriaRuntimeZoneStateSnapshot( + recordKey, name, position.X, position.Y, diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 2868c12fb..adf13584a 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -958,11 +958,15 @@ First Aetheria surfaces to publish: checkpoints now carry pickup index, position, velocity, item key, quality, durability, quantity, and package readback through `AetheriaRuntimeZoneStateSnapshot.DroppedPickups`. + - Done: lower typed dropped-pickup zone rows back into live scene pickups on + zone load. `AetheriaRuntimeZoneStateSnapshot.RecordKey` preserves the exact + typed zone identity, and `ActionGameManager` restores only the matching + `RunId + ZoneIndex` pickup rows through `ZoneRenderer.DropItem`. - Remaining: add typed documents/mappers for runtime object graphs, - restore/lowering of dropped pickups back into live scene objects, 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 Continue/run restore 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. 4. Runtime cutover - Done: add a Unity-facing typed catalog read facade and smoke proving it can @@ -1423,8 +1427,8 @@ First Aetheria surfaces to publish: 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 now projected into typed zone - snapshot state; restoring/lowering those typed pickup rows back into live - scene objects remains future runtime object graph work. + snapshot state and lowered back into live scene pickups by exact typed zone + record key during zone load. - 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. @@ -1764,7 +1768,7 @@ follows the same rule: collision code reports the pickup opportunity, gameplay owns cargo storage and checkpoint. Entity destruction follows the same rule: instance code observes death, gameplay owns drop decisions, zone removal, and checkpoint. Dropped world pickups are typed zone-snapshot state, not only -renderer-local objects; restore/lowering work must consume that typed state +renderer-local objects; live lowering consumes the exact typed zone record key instead of rehydrating a parallel presentation list. 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