diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index a1d39d0e2..eff5665a0 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 c52ad34bb..fb3fbe761 100644 --- a/Aetheria.State.Unity.Smoke/Program.cs +++ b/Aetheria.State.Unity.Smoke/Program.cs @@ -9,6 +9,7 @@ var statePath = args.Length > 1 await using var client = await AetheriaRuntimeCatalogClient.OpenAsync(statePath); var catalog = client.ReadCatalog(); +var packageCatalog = AetheriaRuntimeCatalogStore.OpenReadOnly(statePath); var surface = await client.ReadCatalogSurfaceAsync(); if (catalog.Items.Count != 115) @@ -16,6 +17,15 @@ if (catalog.Items.Count != 115) throw new InvalidOperationException($"Expected 115 runtime catalog items, found {catalog.Items.Count}."); } +if (packageCatalog.Items.Count != catalog.Items.Count || + packageCatalog.Corporations.Count != catalog.Corporations.Count || + packageCatalog.NameFiles.Count != catalog.NameFiles.Count) +{ + throw new InvalidOperationException( + $"Package catalog store mismatch: {packageCatalog.Items.Count}/{packageCatalog.Corporations.Count}/{packageCatalog.NameFiles.Count} " + + $"!= {catalog.Items.Count}/{catalog.Corporations.Count}/{catalog.NameFiles.Count}."); +} + if (catalog.TradeItems.Count == 0) { throw new InvalidOperationException("Runtime catalog has no trade items."); @@ -28,6 +38,14 @@ if (catalog.EquipmentItems.Count == 0) var shaped = catalog.Items.FirstOrDefault(item => item.ShapeCells.Count > 0) ?? throw new InvalidOperationException("Runtime catalog has no typed shape masks."); +var packageShaped = packageCatalog.FindItemByLegacyId(shaped.LegacyId); +if (packageShaped == null || + packageShaped.Name != shaped.Name || + packageShaped.ShapeCells.Count != shaped.ShapeCells.Count) +{ + throw new InvalidOperationException($"Package catalog store did not read the expected typed item payload for {shaped.Name}."); +} + if (shaped.ShapeCells.Count != shaped.OccupiedCells) { throw new InvalidOperationException( @@ -90,6 +108,12 @@ if (catalog.GetManufacturer(manufactured) == null) throw new InvalidOperationException($"Runtime catalog manufacturer lookup failed for {manufactured.Name}."); } +var packageCorporation = packageCatalog.Corporations.FirstOrDefault(corporation => !string.IsNullOrWhiteSpace(corporation.GeonameFileLegacyId)); +if (packageCorporation == null || packageCatalog.GetNameFile(packageCorporation) == null) +{ + throw new InvalidOperationException("Package catalog store did not read corporation/name-file links."); +} + if (surface?.Schema != "gamecult.eve.surface.v1" || surface.Surface.Id != AetheriaCatalogSurfaceProjector.SurfaceId) { diff --git a/Aetheria.State.Unity/Aetheria.State.Unity.csproj b/Aetheria.State.Unity/Aetheria.State.Unity.csproj index fcf138bea..20570ddc7 100644 --- a/Aetheria.State.Unity/Aetheria.State.Unity.csproj +++ b/Aetheria.State.Unity/Aetheria.State.Unity.csproj @@ -9,6 +9,7 @@ + diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 5fb7824ad..39868a2a5 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -67,6 +67,7 @@ public class ActionGameManager : MonoBehaviour public static Galaxy CurrentGalaxy; public static bool IsTutorial; + public static AetheriaRuntimeCatalogSnapshot RuntimeCatalog { get; private set; } public GameSettings Settings; //public string StarterShipTemplate = "Longinus"; @@ -235,7 +236,15 @@ public class ActionGameManager : MonoBehaviour var stateBoot = AetheriaRuntimeStateBoot.Inspect(GameDataDirectory); Debug.Log($"Aetheria typed state file: {stateBoot.StateFilePath}"); if (!stateBoot.StateFileExists) + { Debug.LogWarning("Aetheria typed state file is missing. Run the Aetheria.State importer before treating legacy catalog data as runtime state."); + } + else + { + RuntimeCatalog = AetheriaRuntimeCatalogStore.OpenReadOnly(stateBoot.StateFilePath); + Debug.Log($"Aetheria typed runtime catalog: {RuntimeCatalog.Items.Count} items, {RuntimeCatalog.Corporations.Count} corporations, {RuntimeCatalog.NameFiles.Count} name files"); + } + Debug.Log($"Aetheria legacy item catalog: {LegacyAetherDatabasePath}"); ItemManager = new ItemManager( LegacyCatalog, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs new file mode 100644 index 000000000..99830826e --- /dev/null +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs @@ -0,0 +1,428 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MessagePack; + +#nullable enable + +namespace GameCult.Aetheria.State.Unity +{ + public static class AetheriaRuntimeCatalogStore + { + private const string ItemDefinitionSchema = "aetheria.item_definition"; + private const string CorporationSchema = "aetheria.corporation"; + private const string NameFileSchema = "aetheria.name_file"; + + public static AetheriaRuntimeCatalogSnapshot OpenReadOnly(string stateFilePath) + { + if (string.IsNullOrWhiteSpace(stateFilePath)) + throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath)); + + var catalog = ReadSchemaCatalog(stateFilePath); + var items = new List(); + var corporations = new List(); + var nameFiles = new List(); + + foreach (var record in ReadRecords(stateFilePath)) + { + if (!catalog.TryGetValue(record.SchemaId, out var schemaName)) + continue; + + if (schemaName == ItemDefinitionSchema) + items.Add(ReadItem(record.Payload)); + else if (schemaName == CorporationSchema) + corporations.Add(ReadCorporation(record.Payload)); + else if (schemaName == NameFileSchema) + nameFiles.Add(ReadNameFile(record.Payload)); + } + + return new AetheriaRuntimeCatalogSnapshot( + items.ToArray(), + corporations.ToArray(), + nameFiles.ToArray()); + } + + private static Dictionary ReadSchemaCatalog(string stateFilePath) + { + if (!File.Exists(stateFilePath)) + throw new FileNotFoundException("Aetheria typed state file was not found.", stateFilePath); + + var reader = new MessagePackReader(File.ReadAllBytes(stateFilePath)); + var snapshotFields = reader.ReadArrayHeader(); + if (snapshotFields < 2) + throw new InvalidDataException("CultCache snapshot is missing its embedded schema catalog."); + + reader.Skip(); + var schemaCount = reader.ReadArrayHeader(); + var catalog = new Dictionary(StringComparer.Ordinal); + for (var index = 0; index < schemaCount; index++) + { + var fieldCount = reader.ReadArrayHeader(); + var schemaId = fieldCount > 0 ? ReadString(ref reader) : ""; + var schemaName = fieldCount > 1 ? ReadString(ref reader) : ""; + for (var field = 2; field < fieldCount; field++) + reader.Skip(); + + if (!string.IsNullOrWhiteSpace(schemaId) && !string.IsNullOrWhiteSpace(schemaName)) + catalog[schemaId] = schemaName; + } + + return catalog; + } + + private static IReadOnlyList ReadRecords(string stateFilePath) + { + var records = new List(); + var reader = new MessagePackReader(File.ReadAllBytes(stateFilePath)); + var snapshotFields = reader.ReadArrayHeader(); + if (snapshotFields > 0) reader.Skip(); + if (snapshotFields > 1) reader.Skip(); + if (snapshotFields > 2) + { + var recordCount = reader.ReadArrayHeader(); + for (var index = 0; index < recordCount; index++) + records.Add(ReadPersistedRecord(ref reader)); + } + + var recordDirectory = stateFilePath + ".records"; + if (!Directory.Exists(recordDirectory)) + return records; + + foreach (var recordFile in Directory.EnumerateFiles(recordDirectory, "*.msgpack").OrderBy(path => path, StringComparer.Ordinal)) + { + var recordReader = new MessagePackReader(File.ReadAllBytes(recordFile)); + records.Add(ReadPersistedRecord(ref recordReader)); + } + + return records; + } + + private static PersistedRecord ReadPersistedRecord(ref MessagePackReader reader) + { + var fieldCount = reader.ReadArrayHeader(); + if (fieldCount > 0) reader.Skip(); + var schemaId = fieldCount > 1 ? ReadString(ref reader) : ""; + if (fieldCount > 2) reader.Skip(); + var payload = Array.Empty(); + if (fieldCount > 3) + { + var sequence = reader.ReadBytes(); + if (sequence.HasValue) + payload = sequence.Value.ToArray(); + } + for (var field = 4; field < fieldCount; field++) + reader.Skip(); + return new PersistedRecord(schemaId, payload); + } + + private static AetheriaRuntimeCatalogItem ReadItem(byte[] payload) + { + var reader = new MessagePackReader(payload); + var fields = reader.ReadArrayHeader(); + var name = ReadFieldString(ref reader, fields, 0); + var category = ReadFieldString(ref reader, fields, 1); + var legacyId = ReadFieldString(ref reader, fields, 2); + var description = ReadFieldString(ref reader, fields, 3); + var mass = ReadFieldDouble(ref reader, fields, 4); + var volume = ReadFieldDouble(ref reader, fields, 5); + SkipField(ref reader, fields, 6); + var manufacturerLegacyId = ReadFieldString(ref reader, fields, 7); + var price = ReadFieldInt32(ref reader, fields, 8); + var shapeWidth = ReadFieldInt32(ref reader, fields, 9); + var shapeHeight = ReadFieldInt32(ref reader, fields, 10); + var occupiedCells = ReadFieldInt32(ref reader, fields, 11); + var hardpointType = ReadFieldString(ref reader, fields, 12); + var hullType = ReadFieldString(ref reader, fields, 13); + var behaviorKinds = ReadFieldStringArray(ref reader, fields, 14); + SkipField(ref reader, fields, 15); + var maxStack = ReadFieldInt32(ref reader, fields, 16); + SkipField(ref reader, fields, 17); + SkipField(ref reader, fields, 18); + var durability = ReadFieldDouble(ref reader, fields, 19); + var weaponRange = ReadFieldString(ref reader, fields, 20); + var weaponCaliber = ReadFieldString(ref reader, fields, 21); + var weaponType = ReadFieldString(ref reader, fields, 22); + var weaponFireTypes = ReadFieldString(ref reader, fields, 23); + var weaponModifiers = ReadFieldString(ref reader, fields, 24); + var shapeCells = ReadFieldShapeCells(ref reader, fields, 25); + var interiorShapeWidth = ReadFieldInt32(ref reader, fields, 26); + var interiorShapeHeight = ReadFieldInt32(ref reader, fields, 27); + var interiorOccupiedCells = ReadFieldInt32(ref reader, fields, 28); + var interiorShapeCells = ReadFieldShapeCells(ref reader, fields, 29); + var hardpoints = ReadFieldHardpoints(ref reader, fields, 30); + var behaviorPayloads = ReadFieldBehaviorPayloads(ref reader, fields, 31); + SkipRemaining(ref reader, fields, 32); + + return new AetheriaRuntimeCatalogItem( + legacyId, + name, + category, + description, + manufacturerLegacyId, + price, + mass, + volume, + shapeWidth, + shapeHeight, + occupiedCells, + shapeCells, + interiorShapeWidth, + interiorShapeHeight, + interiorOccupiedCells, + interiorShapeCells, + hardpoints, + behaviorPayloads, + hardpointType, + hullType, + behaviorKinds, + maxStack, + durability, + weaponRange, + weaponCaliber, + weaponType, + weaponFireTypes, + weaponModifiers); + } + + private static AetheriaRuntimeCorporation ReadCorporation(byte[] payload) + { + var reader = new MessagePackReader(payload); + var fields = reader.ReadArrayHeader(); + var name = ReadFieldString(ref reader, fields, 0); + var legacyId = ReadFieldString(ref reader, fields, 1); + var shortName = ReadFieldString(ref reader, fields, 2); + var description = ReadFieldString(ref reader, fields, 3); + var geonameFileLegacyId = ReadFieldString(ref reader, fields, 4); + var bossHullLegacyId = ReadFieldString(ref reader, fields, 5); + var influenceDistance = ReadFieldInt32(ref reader, fields, 6); + var allegianceCount = ReadFieldInt32(ref reader, fields, 7); + SkipRemaining(ref reader, fields, 8); + return new AetheriaRuntimeCorporation( + legacyId, + name, + shortName, + description, + geonameFileLegacyId, + bossHullLegacyId, + influenceDistance, + allegianceCount); + } + + private static AetheriaRuntimeNameFile ReadNameFile(byte[] payload) + { + var reader = new MessagePackReader(payload); + var fields = reader.ReadArrayHeader(); + var name = ReadFieldString(ref reader, fields, 0); + var legacyId = ReadFieldString(ref reader, fields, 1); + var nameCount = ReadFieldInt32(ref reader, fields, 2); + var sampleNames = ReadFieldStringArray(ref reader, fields, 3); + SkipRemaining(ref reader, fields, 4); + return new AetheriaRuntimeNameFile(legacyId, name, nameCount, sampleNames); + } + + private static string ReadFieldString(ref MessagePackReader reader, int fields, int index) + { + return index >= fields ? "" : ReadString(ref reader); + } + + private static int ReadFieldInt32(ref MessagePackReader reader, int fields, int index) + { + return index >= fields ? 0 : reader.ReadInt32(); + } + + private static double ReadFieldDouble(ref MessagePackReader reader, int fields, int index) + { + return index >= fields ? 0 : reader.ReadDouble(); + } + + private static IReadOnlyList ReadFieldStringArray(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var values = new string[count]; + for (var item = 0; item < count; item++) + values[item] = ReadString(ref reader); + return values; + } + + private static IReadOnlyList ReadFieldShapeCells(ref MessagePackReader reader, int fields, int index) + { + return index >= fields ? Array.Empty() : ReadShapeCells(ref reader); + } + + private static IReadOnlyList ReadShapeCells(ref MessagePackReader reader) + { + var count = reader.ReadArrayHeader(); + var cells = new AetheriaRuntimeShapeCell[count]; + for (var cell = 0; cell < count; cell++) + { + var fields = reader.ReadArrayHeader(); + var x = ReadFieldInt32(ref reader, fields, 0); + var y = ReadFieldInt32(ref reader, fields, 1); + SkipRemaining(ref reader, fields, 2); + cells[cell] = new AetheriaRuntimeShapeCell(x, y); + } + + return cells; + } + + private static IReadOnlyList ReadFieldHardpoints(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var hardpoints = new AetheriaRuntimeHardpoint[count]; + for (var hardpoint = 0; hardpoint < count; hardpoint++) + { + var hardpointFields = reader.ReadArrayHeader(); + var type = ReadFieldString(ref reader, hardpointFields, 0); + var positionX = ReadFieldInt32(ref reader, hardpointFields, 1); + var positionY = ReadFieldInt32(ref reader, hardpointFields, 2); + var shapeWidth = ReadFieldInt32(ref reader, hardpointFields, 3); + var shapeHeight = ReadFieldInt32(ref reader, hardpointFields, 4); + var occupiedCells = ReadFieldInt32(ref reader, hardpointFields, 5); + var shapeCells = ReadFieldShapeCells(ref reader, hardpointFields, 6); + var transform = ReadFieldString(ref reader, hardpointFields, 7); + var rotation = ReadFieldString(ref reader, hardpointFields, 8); + var armor = ReadFieldDouble(ref reader, hardpointFields, 9); + SkipRemaining(ref reader, hardpointFields, 10); + hardpoints[hardpoint] = new AetheriaRuntimeHardpoint( + type, + positionX, + positionY, + shapeWidth, + shapeHeight, + occupiedCells, + shapeCells, + transform, + rotation, + armor); + } + + return hardpoints; + } + + private static IReadOnlyList ReadFieldBehaviorPayloads(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var payloads = new AetheriaRuntimeBehaviorPayload[count]; + for (var payload = 0; payload < count; payload++) + { + var payloadFields = reader.ReadArrayHeader(); + var unionKey = ReadFieldInt32(ref reader, payloadFields, 0); + var kind = ReadFieldString(ref reader, payloadFields, 1); + var group = ReadFieldInt32(ref reader, payloadFields, 2); + var fieldsValue = ReadFieldBehaviorFields(ref reader, payloadFields, 3); + SkipRemaining(ref reader, payloadFields, 4); + payloads[payload] = new AetheriaRuntimeBehaviorPayload(unionKey, kind, group, fieldsValue); + } + + return payloads; + } + + private static IReadOnlyList ReadFieldBehaviorFields(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var values = new AetheriaRuntimeBehaviorField[count]; + for (var field = 0; field < count; field++) + { + var fieldCount = reader.ReadArrayHeader(); + var key = ReadFieldInt32(ref reader, fieldCount, 0); + var value = ReadFieldBehaviorValue(ref reader, fieldCount, 1); + SkipRemaining(ref reader, fieldCount, 2); + values[field] = new AetheriaRuntimeBehaviorField(key, value); + } + + return values; + } + + private static AetheriaRuntimeBehaviorValue ReadFieldBehaviorValue(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) + return EmptyBehaviorValue(); + + var valueFields = reader.ReadArrayHeader(); + var kind = ReadFieldString(ref reader, valueFields, 0); + var stringValue = ReadFieldString(ref reader, valueFields, 1); + var numberValue = ReadFieldDouble(ref reader, valueFields, 2); + var boolValue = valueFields > 3 && reader.ReadBoolean(); + var legacyIdValue = ReadFieldString(ref reader, valueFields, 4); + var children = ReadFieldBehaviorValues(ref reader, valueFields, 5); + var mapEntries = ReadFieldBehaviorMapEntries(ref reader, valueFields, 6); + SkipRemaining(ref reader, valueFields, 7); + return new AetheriaRuntimeBehaviorValue(kind, stringValue, numberValue, boolValue, legacyIdValue, children, mapEntries); + } + + private static IReadOnlyList ReadFieldBehaviorValues(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var values = new AetheriaRuntimeBehaviorValue[count]; + for (var value = 0; value < count; value++) + values[value] = ReadFieldBehaviorValue(ref reader, 1, 0); + return values; + } + + private static IReadOnlyList ReadFieldBehaviorMapEntries(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var entries = new AetheriaRuntimeBehaviorMapEntry[count]; + for (var entry = 0; entry < count; entry++) + { + var entryFields = reader.ReadArrayHeader(); + var key = ReadFieldString(ref reader, entryFields, 0); + var value = ReadFieldBehaviorValue(ref reader, entryFields, 1); + SkipRemaining(ref reader, entryFields, 2); + entries[entry] = new AetheriaRuntimeBehaviorMapEntry(key, value); + } + + return entries; + } + + private static AetheriaRuntimeBehaviorValue EmptyBehaviorValue() + { + return new AetheriaRuntimeBehaviorValue( + "nil", + "", + 0, + false, + "", + Array.Empty(), + Array.Empty()); + } + + private static string ReadString(ref MessagePackReader reader) + { + return reader.ReadString() ?? ""; + } + + private static void SkipField(ref MessagePackReader reader, int fields, int index) + { + if (index < fields) + reader.Skip(); + } + + private static void SkipRemaining(ref MessagePackReader reader, int fields, int firstUnhandledIndex) + { + for (var field = firstUnhandledIndex; field < fields; field++) + reader.Skip(); + } + + private readonly struct PersistedRecord + { + public PersistedRecord(string schemaId, byte[] payload) + { + SchemaId = schemaId; + Payload = payload; + } + + public string SchemaId { get; } + + public byte[] Payload { get; } + } + } +} diff --git a/Packages/org.gamecult.aetheria.state/Runtime/GameCult.Aetheria.State.Unity.asmdef b/Packages/org.gamecult.aetheria.state/Runtime/GameCult.Aetheria.State.Unity.asmdef index 8d4d3aa6f..efe1f5133 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/GameCult.Aetheria.State.Unity.asmdef +++ b/Packages/org.gamecult.aetheria.state/Runtime/GameCult.Aetheria.State.Unity.asmdef @@ -1,7 +1,9 @@ { "name": "GameCult.Aetheria.State.Unity", "rootNamespace": "GameCult.Aetheria.State.Unity", - "references": [], + "references": [ + "MessagePack" + ], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index eed66dd02..07e7adfeb 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -27,10 +27,13 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. the Unity boot-time state-file existence probe through `AetheriaRuntimeStateBoot`. `ActionGameManager` consumes that package-owned boot report and warns when `aetheria-world.cc` is missing before treating - legacy catalog data as runtime state. `LegacyCatalogBoundary` no longer names - or owns typed state paths. Local run saves, loadouts, player settings files, - zone files, and generated keyboard layout caches no longer write bespoke - durable files. The legacy + legacy catalog data as runtime state. When the typed state file exists, + `ActionGameManager` opens a package-owned read-only runtime catalog snapshot + from the `.cc` directory store before constructing the temporary legacy + `ItemManager`. `LegacyCatalogBoundary` no longer names or owns typed state + paths. Local run saves, loadouts, player settings files, zone files, and + generated keyboard layout caches no longer write bespoke durable files. The + legacy `PlayerSettings` runtime object is no longer decorated as a MessagePack persistence shape. - Shared domain state is still partly built around `DatabaseEntry`, GUID @@ -121,11 +124,13 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. Unity-visible immutable catalog read-model contract for trade/equipment/behavior/hardpoint/manufacturer/corporation/name queries, typed item shape masks, typed interior masks, hardpoints, and behavior - payloads. The SDK-style `Aetheria.State.Unity` facade includes that package - source and owns only the `.cc` opener plus mapper from `Aetheria.State` - documents into the package contract. It reads the published Eve surface - without deserializing legacy `DatabaseEntry` objects. It is not a simulation - owner and does not write state. + payloads. It also owns a read-only Unity-compatible CultCache directory-store + opener for the known Aetheria catalog schemas, so Unity runtime code can read + `aetheria-world.cc` catalog records without `DatabaseEntry` or + `LegacyCatalogCache`. The SDK-style `Aetheria.State.Unity` facade includes + that package source and owns the full `Aetheria.State` mapper plus Eve surface + read path for .NET smokes. Neither is a simulation owner and neither writes + state. - `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 @@ -214,10 +219,12 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. `ILegacyCatalogReader` until typed runtime catalog boot replaces it; `Galaxy` has been moved off the legacy reader and now consumes `ItemManager`. The embedded Unity state package owns the read-only runtime catalog model - contract for future Unity consumers; the SDK-style `Aetheria.State.Unity` - facade maps typed `.cc` documents into that contract for current .NET smokes. - Neither writes state or owns simulation. The legacy cache no longer has a - public mutation surface. + contract and the read-only known-schema `.cc` catalog opener. `ActionGameManager` + now exposes that typed package snapshot at boot. The SDK-style + `Aetheria.State.Unity` facade maps typed `.cc` documents into the same + contract for full .NET smokes and Eve surface reads. Neither writes state or + owns simulation. The legacy cache no longer has a public mutation surface, but + it still materializes `ItemManager`. - Inputs: Unity gameplay code, legacy catalog files, typed state documents, and CultMesh server state. - Outputs: `Aetheria.State` emits `.cc` state and CultMesh documents. Legacy @@ -425,10 +432,11 @@ First Aetheria surfaces to publish: - Migration smoke proves stable old catalog payload fields can be read once and converted without old readers remaining on the live runtime path. - Unity runtime catalog smoke proves read-only catalog consumers can open the - typed `.cc` store through the SDK-style `Aetheria.State.Unity` opener and - receive package-owned runtime catalog read models without `DatabaseEntry` or - `LegacyCatalogCache`, including typed item masks, interior masks, and - hardpoint definitions, plus typed behavior payloads. + typed `.cc` store through both the SDK-style `Aetheria.State.Unity` opener and + the embedded package-owned read-only catalog opener, receiving the same + package-owned runtime catalog read models without `DatabaseEntry` or + `LegacyCatalogCache`, including typed item masks, interior masks, hardpoint + definitions, corporation/name-file links, and typed behavior payloads. - 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