diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index ac74ae404..cd1f5a766 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 b0d279608..fee7070e6 100644 --- a/Aetheria.State.Unity.Smoke/Program.cs +++ b/Aetheria.State.Unity.Smoke/Program.cs @@ -144,8 +144,59 @@ try TutorialPassed = true, ActionBarInputs = new[] { "/1" } }); + AetheriaRuntimeStateCommitLog.QueueRunCheckpoint( + commitSmokeStatePath, + new AetheriaRuntimeRunCheckpointCommit + { + RunId = "smoke-run", + CurrentZoneIndex = 0, + CurrentZoneEntityIndex = 0, + DiscoveredZoneIndices = new[] { 0 }, + Zones = new[] + { + new AetheriaRuntimeZoneSnapshotCommit + { + ZoneIndex = 0, + Name = "Unity Smoke Zone", + PositionX = 12.0, + PositionY = -4.0, + AdjacentZoneIndices = new[] { 1 }, + OwnerFactionIndex = 0, + Entities = new[] + { + new AetheriaRuntimeEntitySnapshotCommit + { + EntityIndex = 0, + Name = "Unity Smoke Ship", + Kind = "ship", + PositionX = 1.0, + PositionY = 2.0, + PositionZ = 3.0, + DirectionX = 0.0, + DirectionY = 1.0, + HullItemLegacyId = "smoke:hull", + Equipment = new[] + { + new AetheriaRuntimeLoadoutItemSlotCommit + { + X = 0, + Y = 0, + Item = new AetheriaRuntimeLoadoutItemCommit + { + ItemLegacyId = "smoke:weapon", + Quality = 0.9, + Durability = 0.8 + } + } + }, + WeaponGroups = new[] { new[] { 0 } } + } + } + } + } + }); var pending = AetheriaRuntimeStateCommitLog.ReadPending(commitSmokeStatePath); - if (pending.Count != 1 || + if (pending.Count != 2 || pending[0].Kind != AetheriaRuntimeCommitKind.PlayerSettings || pending[0].Schema != AetheriaRuntimeStateCommitLog.CommitSchema || pending[0].Path != commit.Path) @@ -156,12 +207,20 @@ try await using var commitNode = await AetheriaStateNode.OpenAsync(commitSmokeStatePath, "aetheria-unity-runtime-commit-smoke"); var report = await AetheriaRuntimeCommitLogApplier.ApplyPendingAsync(commitNode); var settings = await commitNode.GetPlayerSettingsAsync(); + var run = await commitNode.GetRunStateAsync(new("global:aetheria.run_state.smoke-run.v1")); + var zone = await commitNode.GetZoneStateAsync(new("global:aetheria.run_state.smoke-run.zone.0.v1")); + var entity = await commitNode.GetEntitySnapshotAsync(new("global:aetheria.run_state.smoke-run.zone.0.entity.0.v1")); if (report.AppliedPlayerSettings != 1 || + report.AppliedRunCheckpoints != 1 || settings?.PlayerName != "Unity smoke" || settings.Input.ActionBarInputs.Length != 1 || + run?.ZoneKeys.Length != 1 || + zone?.EntityKeys.Length != 1 || + entity?.Equipment.Length != 1 || + entity.WeaponGroups.Length != 1 || AetheriaRuntimeStateCommitLog.ReadPending(commitSmokeStatePath).Count != 0) { - throw new InvalidOperationException("Runtime state commit log did not apply queued settings through the typed state node."); + throw new InvalidOperationException("Runtime state commit log did not apply queued settings/run snapshots through the typed state node."); } } finally @@ -176,4 +235,4 @@ Console.WriteLine($"Interior/hardpoint sample: {interior.Name} {interior.Interio Console.WriteLine($"Behavior payload sample: {behaviorHost.Name} {behaviorPayload.Kind}/{behaviorPayload.Fields.Count}"); Console.WriteLine($"Behavior sample: {behaviorKind}"); Console.WriteLine($"Eve surface: {surface.Surface.Id}"); -Console.WriteLine("Runtime state commit log smoke: player settings command queued, applied, and cleared"); +Console.WriteLine("Runtime state commit log smoke: settings and run zone/entity snapshots queued, applied, and cleared"); diff --git a/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs b/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs index d70db4b43..35b45e10d 100644 --- a/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs +++ b/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs @@ -50,6 +50,31 @@ public static class AetheriaRuntimeCommitLogApplier break; case "run_checkpoint": var run = command.RunState ?? throw MissingPayload(command); + var zoneKeys = new List(); + foreach (var zone in command.ZoneSnapshots) + { + var zoneKey = ZoneKey(run.RunId, zone.ZoneIndex); + var entityKeys = new List(); + foreach (var entity in zone.Entities) + { + var entityKey = EntityKey(run.RunId, zone.ZoneIndex, entity.EntityIndex); + entity.Snapshot.ChildEntityKeys = entity.Snapshot.ChildEntityKeys + .Select(key => key.StartsWith("pending:entity:", StringComparison.Ordinal) + && int.TryParse(key["pending:entity:".Length..], out var childIndex) + ? EntityKey(run.RunId, zone.ZoneIndex, childIndex).ToString() + : key) + .ToArray(); + await node.PutEntitySnapshotAsync(entityKey, entity.Snapshot).ConfigureAwait(false); + entityKeys.Add(entityKey.ToString()); + } + + zone.State.EntityKeys = entityKeys.ToArray(); + await node.PutZoneStateAsync(zoneKey, zone.State).ConfigureAwait(false); + zoneKeys.Add(zoneKey.ToString()); + } + + if (zoneKeys.Count > 0) + run.ZoneKeys = zoneKeys.ToArray(); await node.PutRunStateAsync(RunKey(run.RunId), run).ConfigureAwait(false); report.AppliedRunCheckpoints++; break; @@ -98,7 +123,8 @@ public static class AetheriaRuntimeCommitLogApplier kind, commandId, createdAtUtc, - ReadRunState(ref reader, createdAtUtc), + ReadRunState(ref reader, createdAtUtc, out var zoneSnapshots), + zoneSnapshots, path), _ => throw new InvalidDataException($"Unknown Aetheria runtime commit kind '{kind}' in {path}.") }; @@ -184,10 +210,13 @@ public static class AetheriaRuntimeCommitLogApplier }; } - private static AetheriaRunState ReadRunState(ref MessagePackReader reader, string updatedAtUtc) + private static AetheriaRunState ReadRunState( + ref MessagePackReader reader, + string updatedAtUtc, + out AetheriaPendingZoneSnapshot[] zoneSnapshots) { var fields = reader.ReadArrayHeader(); - return new AetheriaRunState + var run = new AetheriaRunState { RunId = ReadFieldString(ref reader, fields, 0), IsTutorial = ReadFieldBool(ref reader, fields, 1), @@ -198,6 +227,97 @@ public static class AetheriaRuntimeCommitLogApplier DiscoveredZoneIndices = ReadIntArray(ref reader, fields, 6), UpdatedAtUtc = updatedAtUtc }; + zoneSnapshots = ReadZoneSnapshots(ref reader, fields, 7); + return run; + } + + private static AetheriaPendingZoneSnapshot[] ReadZoneSnapshots(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return []; + var count = reader.ReadArrayHeader(); + var zones = new AetheriaPendingZoneSnapshot[count]; + for (var item = 0; item < count; item++) + { + var itemFields = reader.ReadArrayHeader(); + var zoneIndex = ReadFieldInt32(ref reader, itemFields, 0); + var state = new AetheriaZoneState + { + Name = ReadFieldString(ref reader, itemFields, 1), + Position = new AetheriaVector2 + { + X = ReadFieldDouble(ref reader, itemFields, 2), + Y = ReadFieldDouble(ref reader, itemFields, 3) + }, + AdjacentZoneIndices = ReadIntArray(ref reader, itemFields, 4), + FactionIndices = ReadIntArray(ref reader, itemFields, 5), + OwnerFactionIndex = ReadFieldInt32(ref reader, itemFields, 6) + }; + zones[item] = new AetheriaPendingZoneSnapshot( + zoneIndex, + state, + ReadEntitySnapshots(ref reader, itemFields, 7)); + } + + return zones; + } + + private static AetheriaPendingEntitySnapshot[] ReadEntitySnapshots(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return []; + var count = reader.ReadArrayHeader(); + var entities = new AetheriaPendingEntitySnapshot[count]; + for (var item = 0; item < count; item++) + { + var itemFields = reader.ReadArrayHeader(); + var entityIndex = ReadFieldInt32(ref reader, itemFields, 0); + var snapshot = new AetheriaEntitySnapshot + { + Name = ReadFieldString(ref reader, itemFields, 1), + Kind = ReadFieldString(ref reader, itemFields, 2), + Position = new AetheriaVector3 + { + X = ReadFieldDouble(ref reader, itemFields, 3), + Y = ReadFieldDouble(ref reader, itemFields, 4), + Z = ReadFieldDouble(ref reader, itemFields, 5) + }, + Direction = new AetheriaVector2 + { + X = ReadFieldDouble(ref reader, itemFields, 6), + Y = ReadFieldDouble(ref reader, itemFields, 7) + }, + FactionKey = ReferenceKey("aetheria.corporation", ReadFieldString(ref reader, itemFields, 8)), + HullItemKey = ReferenceKey("aetheria.item_definition", ReadFieldString(ref reader, itemFields, 9)), + Equipment = ReadEntityItemSlots(ref reader, itemFields, 10), + CargoBays = ReadEntityItemSlots(ref reader, itemFields, 11), + DockingBays = ReadEntityItemSlots(ref reader, itemFields, 12), + ChildEntityKeys = ReadIntArray(ref reader, itemFields, 13) + .Select(childIndex => $"pending:entity:{childIndex}") + .ToArray(), + WeaponGroups = ReadWeaponGroups(ref reader, itemFields, 14) + }; + entities[item] = new AetheriaPendingEntitySnapshot(entityIndex, snapshot); + } + + return entities; + } + + private static AetheriaEntityItemSlot[] ReadEntityItemSlots(ref MessagePackReader reader, int fields, int index) + { + var slots = ReadItemSlots(ref reader, fields, index); + return slots + .Select(slot => new AetheriaEntityItemSlot + { + Position = slot.Position, + ItemKey = slot.Item.ItemKey + }) + .ToArray(); + } + + private static AetheriaWeaponGroupSnapshot[] ReadWeaponGroups(ref MessagePackReader reader, int fields, int index) + { + return ReadIntArrayArray(ref reader, fields, index) + .Select(group => new AetheriaWeaponGroupSnapshot { EquipmentIndices = group }) + .ToArray(); } private static AetheriaStoryFileHash[] ReadStoryFileHashes(ref MessagePackReader reader, int fields, int index) @@ -350,6 +470,16 @@ public static class AetheriaRuntimeCommitLogApplier return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.v1"); } + private static CultRecordKey ZoneKey(string runId, int zoneIndex) + { + return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.zone.{zoneIndex}.v1"); + } + + private static CultRecordKey EntityKey(string runId, int zoneIndex, int entityIndex) + { + return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.zone.{zoneIndex}.entity.{entityIndex}.v1"); + } + private static string ReferenceKey(string documentName, string legacyId) { return string.IsNullOrWhiteSpace(legacyId) ? "" : $"{documentName}:legacy:{legacyId}"; @@ -380,7 +510,8 @@ public static class AetheriaRuntimeCommitLogApplier string path, AetheriaPlayerSettings? playerSettings, AetheriaLoadoutTemplate? loadoutTemplate, - AetheriaRunState? runState) + AetheriaRunState? runState, + AetheriaPendingZoneSnapshot[] zoneSnapshots) { Kind = kind; CommandId = commandId; @@ -389,11 +520,12 @@ public static class AetheriaRuntimeCommitLogApplier PlayerSettings = playerSettings; LoadoutTemplate = loadoutTemplate; RunState = runState; + ZoneSnapshots = zoneSnapshots; } public static PendingCommand Empty(string kind, string commandId, string createdAtUtc, string path) { - return new PendingCommand(kind, commandId, createdAtUtc, path, null, null, null); + return new PendingCommand(kind, commandId, createdAtUtc, path, null, null, null, []); } public static PendingCommand ForPlayerSettings( @@ -403,7 +535,7 @@ public static class AetheriaRuntimeCommitLogApplier AetheriaPlayerSettings settings, string path) { - return new PendingCommand(kind, commandId, createdAtUtc, path, settings, null, null); + return new PendingCommand(kind, commandId, createdAtUtc, path, settings, null, null, []); } public static PendingCommand ForLoadoutTemplate( @@ -413,7 +545,7 @@ public static class AetheriaRuntimeCommitLogApplier AetheriaLoadoutTemplate loadout, string path) { - return new PendingCommand(kind, commandId, createdAtUtc, path, null, loadout, null); + return new PendingCommand(kind, commandId, createdAtUtc, path, null, loadout, null, []); } public static PendingCommand ForRunState( @@ -421,9 +553,10 @@ public static class AetheriaRuntimeCommitLogApplier string commandId, string createdAtUtc, AetheriaRunState run, + AetheriaPendingZoneSnapshot[] zoneSnapshots, string path) { - return new PendingCommand(kind, commandId, createdAtUtc, path, null, null, run); + return new PendingCommand(kind, commandId, createdAtUtc, path, null, null, run, zoneSnapshots); } public string Kind { get; } @@ -433,5 +566,35 @@ public static class AetheriaRuntimeCommitLogApplier public AetheriaPlayerSettings? PlayerSettings { get; } public AetheriaLoadoutTemplate? LoadoutTemplate { get; } public AetheriaRunState? RunState { get; } + public AetheriaPendingZoneSnapshot[] ZoneSnapshots { get; } + } + + private readonly struct AetheriaPendingZoneSnapshot + { + public AetheriaPendingZoneSnapshot( + int zoneIndex, + AetheriaZoneState state, + AetheriaPendingEntitySnapshot[] entities) + { + ZoneIndex = zoneIndex; + State = state; + Entities = entities; + } + + public int ZoneIndex { get; } + public AetheriaZoneState State { get; } + public AetheriaPendingEntitySnapshot[] Entities { get; } + } + + private readonly struct AetheriaPendingEntitySnapshot + { + public AetheriaPendingEntitySnapshot(int entityIndex, AetheriaEntitySnapshot snapshot) + { + EntityIndex = entityIndex; + Snapshot = snapshot; + } + + public int EntityIndex { get; } + public AetheriaEntitySnapshot Snapshot { get; } } } diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index ca57b710d..303002216 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -336,15 +336,86 @@ public class ActionGameManager : MonoBehaviour .Select(ZoneIndex) .Where(index => index >= 0) .OrderBy(index => index) - .ToArray() ?? Array.Empty() + .ToArray() ?? Array.Empty(), + Zones = Zone == null ? Array.Empty() : new[] { ProjectZoneSnapshot(Zone) } }; } + private AetheriaRuntimeZoneSnapshotCommit ProjectZoneSnapshot(Zone zone) + { + var galaxyZone = zone.GalaxyZone; + return new AetheriaRuntimeZoneSnapshotCommit + { + ZoneIndex = ZoneIndex(galaxyZone), + Name = galaxyZone?.Name ?? "", + PositionX = galaxyZone?.Position.x ?? 0, + PositionY = galaxyZone?.Position.y ?? 0, + AdjacentZoneIndices = galaxyZone?.AdjacentZones + .Select(ZoneIndex) + .Where(index => index >= 0) + .OrderBy(index => index) + .ToArray() ?? Array.Empty(), + FactionIndices = galaxyZone?.Factions? + .Select(FactionIndex) + .Where(index => index >= 0) + .OrderBy(index => index) + .ToArray() ?? Array.Empty(), + OwnerFactionIndex = FactionIndex(galaxyZone?.Owner), + Entities = zone.Entities + .Select((entity, index) => ProjectEntitySnapshot(zone, entity, index)) + .ToArray() + }; + } + + private AetheriaRuntimeEntitySnapshotCommit ProjectEntitySnapshot(Zone zone, Entity entity, int entityIndex) + { + return new AetheriaRuntimeEntitySnapshotCommit + { + EntityIndex = entityIndex, + Name = entity.Name ?? "", + Kind = entity is Ship ? "ship" : entity is OrbitalEntity ? "orbital" : "entity", + PositionX = entity.Position.x, + PositionY = entity.Position.y, + PositionZ = entity.Position.z, + DirectionX = entity.Direction.x, + DirectionY = entity.Direction.y, + FactionLegacyId = entity.Faction?.ID.ToString("D") ?? "", + HullItemLegacyId = entity.Hull?.Data?.LinkID.ToString("D") ?? "", + Equipment = ProjectEquippedSlots(entity.Equipment), + CargoBays = ProjectEquippedSlots(entity.CargoBays), + DockingBays = ProjectEquippedSlots(entity.DockingBays), + ChildEntityIndices = entity.Children? + .Select(child => zone.Entities.IndexOf(child)) + .Where(index => index >= 0) + .ToArray() ?? Array.Empty(), + WeaponGroups = entity.WeaponGroups? + .Select(group => (IReadOnlyList)group.items.Select(item => entity.Equipment.IndexOf(item)).Where(index => index >= 0).ToArray()) + .ToArray() ?? Array.Empty>() + }; + } + + private static AetheriaRuntimeLoadoutItemSlotCommit[] ProjectEquippedSlots(IEnumerable slots) + { + return slots? + .Select(slot => new AetheriaRuntimeLoadoutItemSlotCommit + { + X = slot.Position.x, + Y = slot.Position.y, + Item = ProjectLoadoutItem(slot.EquippableItem) + }) + .ToArray() ?? Array.Empty(); + } + private static int ZoneIndex(GalaxyZone zone) { return CurrentGalaxy?.Zones == null || zone == null ? -1 : Array.IndexOf(CurrentGalaxy.Zones, zone); } + private static int FactionIndex(Faction faction) + { + return CurrentGalaxy?.Factions == null || faction == null ? -1 : Array.IndexOf(CurrentGalaxy.Factions, faction); + } + public void SaveLoadout(EntityPack pack) { Loadouts.RemoveAll(loadout => loadout.Name == pack.Name); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitLog.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitLog.cs index 57735cfff..8d51405ea 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitLog.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitLog.cs @@ -117,6 +117,38 @@ namespace GameCult.Aetheria.State.Unity public int CurrentZoneIndex { get; set; } = -1; public int CurrentZoneEntityIndex { get; set; } = -1; public IReadOnlyList DiscoveredZoneIndices { get; set; } = Array.Empty(); + public IReadOnlyList Zones { get; set; } = Array.Empty(); + } + + public sealed class AetheriaRuntimeZoneSnapshotCommit + { + public int ZoneIndex { get; set; } = -1; + public string Name { get; set; } = ""; + public double PositionX { get; set; } + public double PositionY { get; set; } + public IReadOnlyList AdjacentZoneIndices { get; set; } = Array.Empty(); + public IReadOnlyList FactionIndices { get; set; } = Array.Empty(); + public int OwnerFactionIndex { get; set; } = -1; + public IReadOnlyList Entities { get; set; } = Array.Empty(); + } + + public sealed class AetheriaRuntimeEntitySnapshotCommit + { + public int EntityIndex { get; set; } = -1; + public string Name { get; set; } = ""; + public string Kind { get; set; } = ""; + public double PositionX { get; set; } + public double PositionY { get; set; } + public double PositionZ { get; set; } + public double DirectionX { get; set; } + public double DirectionY { get; set; } + public string FactionLegacyId { get; set; } = ""; + public string HullItemLegacyId { get; set; } = ""; + public IReadOnlyList Equipment { get; set; } = Array.Empty(); + public IReadOnlyList CargoBays { get; set; } = Array.Empty(); + public IReadOnlyList DockingBays { get; set; } = Array.Empty(); + public IReadOnlyList ChildEntityIndices { get; set; } = Array.Empty(); + public IReadOnlyList> WeaponGroups { get; set; } = Array.Empty>(); } public static class AetheriaRuntimeStateCommitLog @@ -321,7 +353,7 @@ namespace GameCult.Aetheria.State.Unity private static void WriteRunCheckpoint(ref MessagePackWriter writer, AetheriaRuntimeRunCheckpointCommit run) { - writer.WriteArrayHeader(7); + writer.WriteArrayHeader(8); writer.Write(run.RunId ?? ""); writer.Write(run.IsTutorial); writer.Write(run.EntranceZoneIndex); @@ -329,6 +361,50 @@ namespace GameCult.Aetheria.State.Unity writer.Write(run.CurrentZoneIndex); writer.Write(run.CurrentZoneEntityIndex); WriteInts(ref writer, run.DiscoveredZoneIndices); + WriteZoneSnapshots(ref writer, run.Zones); + } + + private static void WriteZoneSnapshots(ref MessagePackWriter writer, IReadOnlyList? zones) + { + zones ??= Array.Empty(); + writer.WriteArrayHeader(zones.Count); + foreach (var zone in zones) + { + writer.WriteArrayHeader(8); + writer.Write(zone.ZoneIndex); + writer.Write(zone.Name ?? ""); + writer.Write(zone.PositionX); + writer.Write(zone.PositionY); + WriteInts(ref writer, zone.AdjacentZoneIndices); + WriteInts(ref writer, zone.FactionIndices); + writer.Write(zone.OwnerFactionIndex); + WriteEntitySnapshots(ref writer, zone.Entities); + } + } + + private static void WriteEntitySnapshots(ref MessagePackWriter writer, IReadOnlyList? entities) + { + entities ??= Array.Empty(); + writer.WriteArrayHeader(entities.Count); + foreach (var entity in entities) + { + writer.WriteArrayHeader(15); + writer.Write(entity.EntityIndex); + writer.Write(entity.Name ?? ""); + writer.Write(entity.Kind ?? ""); + writer.Write(entity.PositionX); + writer.Write(entity.PositionY); + writer.Write(entity.PositionZ); + writer.Write(entity.DirectionX); + writer.Write(entity.DirectionY); + writer.Write(entity.FactionLegacyId ?? ""); + writer.Write(entity.HullItemLegacyId ?? ""); + WriteItemSlots(ref writer, entity.Equipment); + WriteItemSlots(ref writer, entity.CargoBays); + WriteItemSlots(ref writer, entity.DockingBays); + WriteInts(ref writer, entity.ChildEntityIndices); + WriteIntLists(ref writer, entity.WeaponGroups); + } } private static void WriteStrings(ref MessagePackWriter writer, IReadOnlyList? values) diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index a80f08516..094f43626 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -32,9 +32,11 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. from the `.cc` directory store before constructing the temporary legacy `ItemManager`. Player settings saves, loadout saves, shutdown checkpoints, and wormhole transition checkpoints now queue typed `.cc.pending` Verse - commit commands through the embedded runtime state package. `Aetheria.State` - owns applying those command envelopes into canonical `AetheriaPlayerSettings`, - `AetheriaLoadoutTemplate`, and `AetheriaRunState` documents through + commit commands through the embedded runtime state package. Run checkpoint + commands include the current zone plus its runtime entity snapshots. + `Aetheria.State` owns applying those command envelopes into canonical + `AetheriaPlayerSettings`, `AetheriaLoadoutTemplate`, `AetheriaRunState`, + `AetheriaZoneState`, and `AetheriaEntitySnapshot` documents through `AetheriaStateNode`. Local run saves, loadouts, player settings files, zone files, and generated keyboard layout caches no longer write bespoke durable files. The legacy @@ -67,9 +69,10 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. state, and entity snapshots. The state smoke writes a run referencing a zone, a zone referencing an entity snapshot, and an entity snapshot carrying position, direction, faction, hull, equipment slots, weapon groups, and a stat - grid. This proves the `.zone` replacement graph is durable typed state; Unity - `ZonePack` and `EntityPack` are still runtime construction projections until - the runtime package is wired. + grid. This proves the `.zone` replacement graph is durable typed state. Unity + now queues current-zone/current-entity-collection snapshots through the + runtime commit log during run checkpoints; `ZonePack` and `EntityPack` remain + runtime construction/loadout projections rather than durable file formats. - `Aetheria.State` now defines `AetheriaLoadoutTemplate` as the typed Verse replacement for bespoke `.loadout` files. It stores structured hull, equipment, cargo bay, docking bay, child-entity, assignment, and weapon-group @@ -157,7 +160,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. loadout-template, and run-checkpoint command envelopes under `aetheria-world.cc.pending`. This log is command-only: it cannot decide canonical state, and the `Aetheria.State` node applicator deletes applied - commands after writing typed documents. + commands after writing typed documents. Run checkpoint envelopes now carry + current-zone and entity snapshots so the old `.zone` file path has a live + typed runtime projection path. - `AetheriaCatalogSurfaceProjector` now emits the first provider-owned Eve surface from typed catalog state. The importer materializes a `gamecult.eve.surface.v1` catalog operator document at @@ -298,11 +303,11 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. local save and editor catalog write paths have been cut. Migrated catalog documents are addressed through `AetheriaCatalogKeys` and `AetheriaStateNode` legacy-ID methods, not importer-local string concatenation. Player settings, loadout - templates, and lightweight run checkpoints have a shared Unity commit-log - primitive plus a typed state node applicator. Run, zone, and entity snapshot - state also have typed node put/get paths; full zone/entity runtime snapshot - projection still needs to move through the same command/apply spine. The old - `SaveState`/`SaveZone` command surface is gone. + templates, and run checkpoints have a shared Unity commit-log primitive plus + a typed state node applicator. Run checkpoints now carry the current zone and + its entity snapshots through that same command/apply spine. Whole-galaxy + unloaded-zone projection still needs to be added once lazy generation has a + typed state owner. The old `SaveState`/`SaveZone` command surface is gone. The catalog operator UI is emitted as a typed Eve surface document derived from the catalog snapshot. - Derived state: UI panels, generated keyboard layouts, editor rows, typed @@ -513,6 +518,9 @@ First Aetheria surfaces to publish: - Done: replace warning-only player settings, loadout, shutdown, and wormhole save paths with typed `.cc.pending` Verse commit commands and a state-node applicator that writes canonical typed documents. + - Done: extend run checkpoint commands to carry current-zone and entity + snapshots into canonical `AetheriaZoneState` and `AetheriaEntitySnapshot` + documents. - Done: stop legacy catalog pull/read paths from writing entries back to their source backing store. - Done: delete legacy catalog backing-store write/realtime APIs. @@ -551,9 +559,9 @@ First Aetheria surfaces to publish: package-owned runtime catalog read models without a MessagePack catalog cache, including typed item masks, interior masks, hardpoint definitions, corporation/name-file links, and typed behavior payloads. It also proves the - Unity runtime state commit log can queue a player-settings command, the - `Aetheria.State` node can apply it into canonical typed state, and the - command is cleared after application. + Unity runtime state commit log can queue player-settings and run snapshot + commands, the `Aetheria.State` node can apply them into canonical typed + settings/run/zone/entity state, and commands are cleared after application. - Unity play smoke proves runtime UI reads from Eve surfaces and sends commands through the shared state service. - UI Toolkit lowering parity compares Aetheria surfaces against the Eve browser