diff --git a/Aetheria.State.Import/Program.cs b/Aetheria.State.Import/Program.cs index 5dfaf765f..a5ee15b0c 100644 --- a/Aetheria.State.Import/Program.cs +++ b/Aetheria.State.Import/Program.cs @@ -2,7 +2,6 @@ using System.Security.Cryptography; using Aetheria.State; using Aetheria.State.Documents; using Aetheria.State.Migration; -using GameCult.Caching; using MessagePack; var root = args.Length > 0 ? Path.GetFullPath(args[0]) : Directory.GetCurrentDirectory(); @@ -110,17 +109,17 @@ await node.PutMigrationLedgerAsync(new AetheriaMigrationLedger foreach (var item in itemDefinitions) { - await node.PutItemDefinitionAsync(new CultRecordKey($"legacy:item:{item.LegacyId}"), item); + await node.PutLegacyItemDefinitionAsync(item); } foreach (var corporation in corporations) { - await node.PutCorporationAsync(new CultRecordKey($"legacy:faction:{corporation.LegacyId}"), corporation); + await node.PutLegacyCorporationAsync(corporation); } foreach (var nameFile in parsedNameFiles) { - await node.PutNameFileAsync(new CultRecordKey($"legacy:name-file:{nameFile.LegacyId}"), nameFile); + await node.PutLegacyNameFileAsync(nameFile); } await node.FlushAsync(); diff --git a/Aetheria.State.Smoke/Program.cs b/Aetheria.State.Smoke/Program.cs index 77f984163..104b88e58 100644 --- a/Aetheria.State.Smoke/Program.cs +++ b/Aetheria.State.Smoke/Program.cs @@ -6,9 +6,9 @@ using GameCult.Caching; var root = args.Length > 0 ? args[0] : Directory.GetCurrentDirectory(); var statePath = AetheriaStatePaths.ResolveDefaultStatePath(root); var now = DateTimeOffset.UtcNow.ToString("O"); -var itemKey = new CultRecordKey("item:smoke-aether-drive"); -var factionKey = new CultRecordKey("faction:smoke"); -var nameFileKey = new CultRecordKey("name-file:smoke"); +var itemLegacyId = "smoke:aether-drive"; +var factionLegacyId = "smoke:faction"; +var nameFileLegacyId = "smoke:name-file"; var runKey = new CultRecordKey("run:smoke"); await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-state-smoke")) @@ -22,13 +22,12 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s UpdatedAtUtc = now }); - await node.PutItemDefinitionAsync( - itemKey, + await node.PutLegacyItemDefinitionAsync( new AetheriaItemDefinition { Name = "Smoke Aether Drive", Category = "ship-module", - LegacyId = "smoke:aether-drive", + LegacyId = itemLegacyId, Description = "Typed CultCache smoke document for the rebuild spine.", Mass = 12.5, Volume = 4.0, @@ -70,17 +69,17 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s Notes = ["Smoke proves legacy catalog quarantine state is typed and durable."] }); - await node.PutCorporationAsync(factionKey, new AetheriaCorporation + await node.PutLegacyCorporationAsync(new AetheriaCorporation { Name = "Smoke Faction", - LegacyId = "smoke:faction", + LegacyId = factionLegacyId, Description = "Typed faction/corporation document for legacy catalog migration smoke." }); - await node.PutNameFileAsync(nameFileKey, new AetheriaNameFile + await node.PutLegacyNameFileAsync(new AetheriaNameFile { Name = "Smoke Names", - LegacyId = "smoke:name-file", + LegacyId = nameFileLegacyId, NameCount = 2, SampleNames = ["Ada", "Grace"] }); @@ -117,9 +116,9 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aetheria-state-smoke-reopen")) { var world = await reopened.GetWorldAsync(); - var item = await reopened.GetItemDefinitionAsync(itemKey); - var faction = await reopened.GetCorporationAsync(factionKey); - var nameFile = await reopened.GetNameFileAsync(nameFileKey); + var item = await reopened.GetItemDefinitionByLegacyIdAsync(itemLegacyId); + var faction = await reopened.GetCorporationByLegacyIdAsync(factionLegacyId); + var nameFile = await reopened.GetNameFileByLegacyIdAsync(nameFileLegacyId); var quarantine = await reopened.GetLegacyCatalogQuarantineAsync(); var playerSettings = await reopened.GetPlayerSettingsAsync(); var runState = await reopened.GetRunStateAsync(runKey); @@ -134,7 +133,7 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether throw new InvalidOperationException("Item definition did not survive flush/reopen."); } - if (faction?.LegacyId != "smoke:faction") + if (faction?.LegacyId != factionLegacyId) { throw new InvalidOperationException("Faction/corporation document did not survive flush/reopen."); } diff --git a/Aetheria.State/AetheriaCatalogKeys.cs b/Aetheria.State/AetheriaCatalogKeys.cs new file mode 100644 index 000000000..19770a4fa --- /dev/null +++ b/Aetheria.State/AetheriaCatalogKeys.cs @@ -0,0 +1,31 @@ +using GameCult.Caching; + +namespace Aetheria.State; + +public static class AetheriaCatalogKeys +{ + public static CultRecordKey ItemDefinitionFromLegacyId(string legacyId) + { + return CreateLegacyKey("item", legacyId); + } + + public static CultRecordKey CorporationFromLegacyId(string legacyId) + { + return CreateLegacyKey("faction", legacyId); + } + + public static CultRecordKey NameFileFromLegacyId(string legacyId) + { + return CreateLegacyKey("name-file", legacyId); + } + + private static CultRecordKey CreateLegacyKey(string catalogKind, string legacyId) + { + if (string.IsNullOrWhiteSpace(legacyId)) + { + throw new ArgumentException("Legacy catalog id must be non-empty.", nameof(legacyId)); + } + + return new CultRecordKey($"legacy:{catalogKind}:{legacyId.Trim()}"); + } +} diff --git a/Aetheria.State/AetheriaStateNode.cs b/Aetheria.State/AetheriaStateNode.cs index b21b4e1be..ba980bb6b 100644 --- a/Aetheria.State/AetheriaStateNode.cs +++ b/Aetheria.State/AetheriaStateNode.cs @@ -95,31 +95,61 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable return Database.PutAsync(key, item); } + public Task> PutLegacyItemDefinitionAsync(AetheriaItemDefinition item) + { + return PutItemDefinitionAsync(AetheriaCatalogKeys.ItemDefinitionFromLegacyId(item.LegacyId), item); + } + public Task GetItemDefinitionAsync(CultRecordKey key) { return Database.GetAsync(key); } + public Task GetItemDefinitionByLegacyIdAsync(string legacyId) + { + return GetItemDefinitionAsync(AetheriaCatalogKeys.ItemDefinitionFromLegacyId(legacyId)); + } + public Task> PutCorporationAsync(CultRecordKey key, AetheriaCorporation corporation) { return Database.PutAsync(key, corporation); } + public Task> PutLegacyCorporationAsync(AetheriaCorporation corporation) + { + return PutCorporationAsync(AetheriaCatalogKeys.CorporationFromLegacyId(corporation.LegacyId), corporation); + } + public Task GetCorporationAsync(CultRecordKey key) { return Database.GetAsync(key); } + public Task GetCorporationByLegacyIdAsync(string legacyId) + { + return GetCorporationAsync(AetheriaCatalogKeys.CorporationFromLegacyId(legacyId)); + } + public Task> PutNameFileAsync(CultRecordKey key, AetheriaNameFile nameFile) { return Database.PutAsync(key, nameFile); } + public Task> PutLegacyNameFileAsync(AetheriaNameFile nameFile) + { + return PutNameFileAsync(AetheriaCatalogKeys.NameFileFromLegacyId(nameFile.LegacyId), nameFile); + } + public Task GetNameFileAsync(CultRecordKey key) { return Database.GetAsync(key); } + public Task GetNameFileByLegacyIdAsync(string legacyId) + { + return GetNameFileAsync(AetheriaCatalogKeys.NameFileFromLegacyId(legacyId)); + } + public Task> PutPlayerSettingsAsync(AetheriaPlayerSettings settings) { return Database.PutAsync(new CultRecordKey("global:aetheria.player_settings.v1"), settings); diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 6db77c5c6..00e6db599 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -50,7 +50,8 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. MessagePack union fields into typed item/faction/name-file documents without compiling the old Unity domain model into `Aetheria.State`. The current checked-in catalog maps to 115 item definitions, 12 factions, and 12 name - files. + files. `Aetheria.State` now owns the canonical legacy-ID record key mapping + for migrated item, corporation, and name-file documents. - `GameData/aetheria-world.cc` is now materialized from the importer as the project-local typed state file for the checked-in catalog. The importer stores relative provenance in the state document, not machine-local absolute paths. @@ -100,7 +101,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. CultMesh server state. - Outputs: `Aetheria.State` emits `.cc` state and CultMesh documents. Legacy catalog reads emit in-memory domain objects only; the old local save and - editor catalog write paths have been cut. + editor catalog write paths have been cut. Migrated catalog documents are + addressed through `AetheriaCatalogKeys` and `AetheriaStateNode` legacy-ID + methods, not importer-local string concatenation. - Derived state: UI panels, generated keyboard layouts, editor rows, and serialized legacy payloads are projections, migration inputs, or disabled session-local DTOs, not durable authority.