From 3818670e48b968792ef11b93049595353cf8a869 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 13 Jun 2026 09:15:59 +0100 Subject: [PATCH] Add Unity runtime catalog facade --- .gitignore | 4 + .voidbot/state/aetheria.cc | Bin 20742 -> 21528 bytes .../Aetheria.State.Unity.Smoke.csproj | 13 + Aetheria.State.Unity.Smoke/Program.cs | 57 ++++ .../Aetheria.State.Unity.csproj | 13 + .../AetheriaRuntimeCatalogClient.cs | 40 +++ .../AetheriaRuntimeCatalogSnapshot.cs | 265 ++++++++++++++++++ Aetheria.State/README.md | 11 + docs/aetheria-perfect-machine-map.md | 16 +- 9 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 Aetheria.State.Unity.Smoke/Aetheria.State.Unity.Smoke.csproj create mode 100644 Aetheria.State.Unity.Smoke/Program.cs create mode 100644 Aetheria.State.Unity/Aetheria.State.Unity.csproj create mode 100644 Aetheria.State.Unity/AetheriaRuntimeCatalogClient.cs create mode 100644 Aetheria.State.Unity/AetheriaRuntimeCatalogSnapshot.cs diff --git a/.gitignore b/.gitignore index 4041747ce..4b2b86558 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,10 @@ Aetheria.State.Import/bin/ Aetheria.State.Import/obj/ Aetheria.State.Smoke/bin/ Aetheria.State.Smoke/obj/ +Aetheria.State.Unity/bin/ +Aetheria.State.Unity/obj/ +Aetheria.State.Unity.Smoke/bin/ +Aetheria.State.Unity.Smoke/obj/ Aetheria.State.Verify/bin/ Aetheria.State.Verify/obj/ diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index a6a58b9d7a9d06cc531f9bb0bf71fe3cc9f157a8..876056dddbb20e4ba2c784daf7095c711c53a9ca 100644 GIT binary patch delta 1818 zcmb7FO>Z1U5XE+cFy;gVD*;0x5`~c6u{K_ZcYkb1wGao6(^J*;UcFba_Vy1zCPpVT8 z7hO=uqA~&GE-Em+3?o>}6SKy`cyy_zf_)4=2@}TCpC@*zOOGIt843*q5mJIjI1~OB z-Ns7P^*yPQCm`(bAXKFh$Jl!OBzp0In0qqR$XX)F7mR2zA9766~_D2UkdvHq*E+jOu}G zzlkAT>uj@*zNCumR}Yr`o-AV%7&dzH86*LSJ0rFn|Woh2MP&C@*b9U^2dF+!4-Ql%hj z4|;9|8|W&Q#oFFI{dnJ}dMG+(84|}ZcdBT>TU?vqE0TcNZKPSk$*hj(53k}a17S`v zArp{tx{lg0B3)Sg#>RZ*?obMHHY)O&+#`uc?m1$`4p*9#-;15B+lZN?HbzSd3zui@ zAE%GkJYf%*x7mD!WsqkPL5*OKn|KUx z)zH!%!z%~-^~a`G-oCA{bFic^ldYq+FNS%(I@~ODSu19GE#WBa_m@xfUn+n2iV(VU z1LfTL(JjWyHPP{M99(PtX7GaRToE)33M9+_6N&xn9K3OGX=6B;N480&k*f;jusRfO zta=b*l96s@z>< zn(;jeJOTUnDr~>=nvW@*(r7ID<)v%TTr3t?^}W~_ZL;dD+Gn5{jSa$vpj9Ev>&UWd zeYr6%RR#OgrIU80)q1#kSr(Qq-n?U2*PHkK*0qU8Z{se0Q|zaD*`PWNpj#G$UzTU4 zhc6JN+QTceqep6i9;sP-cKzF7NOR!=JyK)u*n&NrnzbLk&>w{~8}1z&EWh~O^aB;D JX7j?$e*i&4nl%6b delta 1208 zcmY*ZO=}cc6t$y>A%2XI(alKiLdd4m9b=+N#KCbEcKk#qLCDJ6Rky1yUG-k&y;tef zY#KKM?q(BQml zt4k}ZOAC$jXO}nstNogPn0T;TTU;z_djHOzT6ONwfm0KgJ`QFN+?lxse~=5wFoMex z^1(xgkq#i{z@THTp4&8z(PW>t*2@FzCiFG<%SOrxm!>_eME+-7Z{U}Bq|PL-<{^Sv%u{+sIHrv}>`vCHa!!nQ}4Xaxp^ zbkoC#+hm%B8qE$HYR1~<4wHcndgN>a);D_5^X8}cm8zWM(mMoJa?q5 zsQ>BPddLD}WkD7VaIf@DE6bn}wXLm-ehxZ?PGf;aE?_E@jgV{%xf+p-V;W*ABHzOG z(z*Qqv&ZryvnTEj{}tR4(}tLk=wQ22DKy0jYR;2{q;cu}&-OO|Oe1%l+rrN4VnDJa zTbIARJl+3rlXe`8NO-B!+ahvLo37+cfkP9J^a=ert)dJz*2J|cwbeK>k~%ilL23&J zs$|nv%1M!>lHgc{p~!XUg)9ke)5b7Amg6)kuYk-dT(ct+3wa{B13t@Kxib~MKaOg- zO5-IePjtOQ9s6b9iNUSeQwLkE8<;?v_~ma!R0?lJF98`}I2ij;60K$zYmEE~Fd00a z`)&U(uV|ZnQXW+^jt?F!@MM1Y%3OZ$;*sg`>u_de^Qpr?SezW(zj(Vg`2PFU-_mMbvu6#HywBC(t%kvAv$K+r>vlj2Euax!IYyI8YQu)XXZd`poxm#Rb+57=Y C*RD7K diff --git a/Aetheria.State.Unity.Smoke/Aetheria.State.Unity.Smoke.csproj b/Aetheria.State.Unity.Smoke/Aetheria.State.Unity.Smoke.csproj new file mode 100644 index 000000000..6516b3064 --- /dev/null +++ b/Aetheria.State.Unity.Smoke/Aetheria.State.Unity.Smoke.csproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + enable + latest + enable + + + + + + diff --git a/Aetheria.State.Unity.Smoke/Program.cs b/Aetheria.State.Unity.Smoke/Program.cs new file mode 100644 index 000000000..690fe91c5 --- /dev/null +++ b/Aetheria.State.Unity.Smoke/Program.cs @@ -0,0 +1,57 @@ +using Aetheria.State; +using Aetheria.State.Unity; + +var root = args.Length > 0 ? Path.GetFullPath(args[0]) : Directory.GetCurrentDirectory(); +var statePath = args.Length > 1 + ? Path.GetFullPath(args[1]) + : AetheriaStatePaths.ResolveDefaultStatePath(root); + +await using var client = await AetheriaRuntimeCatalogClient.OpenAsync(statePath); +var catalog = client.ReadCatalog(); +var surface = await client.ReadCatalogSurfaceAsync(); + +if (catalog.Items.Count != 115) +{ + throw new InvalidOperationException($"Expected 115 runtime catalog items, found {catalog.Items.Count}."); +} + +if (catalog.TradeItems.Count == 0) +{ + throw new InvalidOperationException("Runtime catalog has no trade items."); +} + +if (catalog.EquipmentItems.Count == 0) +{ + throw new InvalidOperationException("Runtime catalog has no equipment items."); +} + +var behaviorKind = catalog.Items.SelectMany(item => item.BehaviorKinds).FirstOrDefault() + ?? throw new InvalidOperationException("Runtime catalog has no behavior kind fingerprints."); +if (!catalog.FindItemsByBehavior(behaviorKind).Any()) +{ + throw new InvalidOperationException($"Runtime catalog behavior lookup failed for {behaviorKind}."); +} + +var equipment = catalog.EquipmentItems.First(); +if (!catalog.FindItemsByHardpoint(equipment.HardpointType).Any()) +{ + throw new InvalidOperationException($"Runtime catalog hardpoint lookup failed for {equipment.HardpointType}."); +} + +var manufactured = catalog.Items.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item.ManufacturerLegacyId)) + ?? throw new InvalidOperationException("Runtime catalog has no manufactured item."); +if (catalog.GetManufacturer(manufactured) == null) +{ + throw new InvalidOperationException($"Runtime catalog manufacturer lookup failed for {manufactured.Name}."); +} + +if (surface?.Schema != "gamecult.eve.surface.v1" || + surface.Surface.Id != AetheriaCatalogSurfaceProjector.SurfaceId) +{ + throw new InvalidOperationException("Runtime catalog client did not read the typed Eve surface."); +} + +Console.WriteLine($"Aetheria Unity runtime catalog smoke passed: {statePath}"); +Console.WriteLine($"Items/trade/equipment: {catalog.Items.Count}/{catalog.TradeItems.Count}/{catalog.EquipmentItems.Count}"); +Console.WriteLine($"Behavior sample: {behaviorKind}"); +Console.WriteLine($"Eve surface: {surface.Surface.Id}"); diff --git a/Aetheria.State.Unity/Aetheria.State.Unity.csproj b/Aetheria.State.Unity/Aetheria.State.Unity.csproj new file mode 100644 index 000000000..d0c760e7b --- /dev/null +++ b/Aetheria.State.Unity/Aetheria.State.Unity.csproj @@ -0,0 +1,13 @@ + + + netstandard2.1 + enable + latest + enable + Unity-facing runtime read facade over typed Aetheria CultCache state. + + + + + + diff --git a/Aetheria.State.Unity/AetheriaRuntimeCatalogClient.cs b/Aetheria.State.Unity/AetheriaRuntimeCatalogClient.cs new file mode 100644 index 000000000..7cc8deec3 --- /dev/null +++ b/Aetheria.State.Unity/AetheriaRuntimeCatalogClient.cs @@ -0,0 +1,40 @@ +using Aetheria.State.Documents; + +namespace Aetheria.State.Unity; + +public sealed class AetheriaRuntimeCatalogClient : IAsyncDisposable, IDisposable +{ + private readonly AetheriaStateNode _node; + + private AetheriaRuntimeCatalogClient(AetheriaStateNode node) + { + _node = node; + } + + public static async Task OpenAsync(string statePath) + { + var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-unity-runtime-catalog").ConfigureAwait(false); + return new AetheriaRuntimeCatalogClient(node); + } + + public AetheriaRuntimeCatalogSnapshot ReadCatalog() + { + var catalog = _node.ReadCatalogSnapshot(); + return AetheriaRuntimeCatalogSnapshot.FromCatalog(catalog); + } + + public async Task ReadCatalogSurfaceAsync() + { + return await _node.GetCatalogSurfaceAsync().ConfigureAwait(false); + } + + public ValueTask DisposeAsync() + { + return _node.DisposeAsync(); + } + + public void Dispose() + { + _node.Dispose(); + } +} diff --git a/Aetheria.State.Unity/AetheriaRuntimeCatalogSnapshot.cs b/Aetheria.State.Unity/AetheriaRuntimeCatalogSnapshot.cs new file mode 100644 index 000000000..ee10febc2 --- /dev/null +++ b/Aetheria.State.Unity/AetheriaRuntimeCatalogSnapshot.cs @@ -0,0 +1,265 @@ +using Aetheria.State.Documents; + +namespace Aetheria.State.Unity; + +public sealed class AetheriaRuntimeCatalogSnapshot +{ + private readonly Dictionary _itemsByLegacyId; + private readonly Dictionary _corporationsByLegacyId; + private readonly Dictionary _nameFilesByLegacyId; + + private AetheriaRuntimeCatalogSnapshot( + AetheriaRuntimeCatalogItem[] items, + AetheriaRuntimeCorporation[] corporations, + AetheriaRuntimeNameFile[] nameFiles) + { + Items = items; + Corporations = corporations; + NameFiles = nameFiles; + TradeItems = items.Where(item => item.Price > 0).ToArray(); + EquipmentItems = items.Where(item => !string.IsNullOrWhiteSpace(item.HardpointType)).ToArray(); + + _itemsByLegacyId = items + .Where(item => !string.IsNullOrWhiteSpace(item.LegacyId)) + .ToDictionary(item => item.LegacyId, StringComparer.OrdinalIgnoreCase); + _corporationsByLegacyId = corporations + .Where(corporation => !string.IsNullOrWhiteSpace(corporation.LegacyId)) + .ToDictionary(corporation => corporation.LegacyId, StringComparer.OrdinalIgnoreCase); + _nameFilesByLegacyId = nameFiles + .Where(nameFile => !string.IsNullOrWhiteSpace(nameFile.LegacyId)) + .ToDictionary(nameFile => nameFile.LegacyId, StringComparer.OrdinalIgnoreCase); + } + + public IReadOnlyList Items { get; } + + public IReadOnlyList TradeItems { get; } + + public IReadOnlyList EquipmentItems { get; } + + public IReadOnlyList Corporations { get; } + + public IReadOnlyList NameFiles { get; } + + public static AetheriaRuntimeCatalogSnapshot FromCatalog(AetheriaCatalogSnapshot catalog) + { + return new AetheriaRuntimeCatalogSnapshot( + catalog.Items.Select(AetheriaRuntimeCatalogItem.FromState).ToArray(), + catalog.Corporations.Select(AetheriaRuntimeCorporation.FromState).ToArray(), + catalog.NameFiles.Select(AetheriaRuntimeNameFile.FromState).ToArray()); + } + + public AetheriaRuntimeCatalogItem? FindItemByLegacyId(string legacyId) + { + return TryGet(_itemsByLegacyId, legacyId); + } + + public AetheriaRuntimeCorporation? FindCorporationByLegacyId(string legacyId) + { + return TryGet(_corporationsByLegacyId, legacyId); + } + + public AetheriaRuntimeNameFile? FindNameFileByLegacyId(string legacyId) + { + return TryGet(_nameFilesByLegacyId, legacyId); + } + + public IEnumerable FindItemsByBehavior(string behaviorKind) + { + return string.IsNullOrWhiteSpace(behaviorKind) + ? [] + : Items.Where(item => item.BehaviorKinds.Contains(behaviorKind, StringComparer.OrdinalIgnoreCase)); + } + + public IEnumerable FindItemsByHardpoint(string hardpointType) + { + return string.IsNullOrWhiteSpace(hardpointType) + ? [] + : Items.Where(item => string.Equals(item.HardpointType, hardpointType, StringComparison.OrdinalIgnoreCase)); + } + + public AetheriaRuntimeCorporation? GetManufacturer(AetheriaRuntimeCatalogItem item) + { + return FindCorporationByLegacyId(item.ManufacturerLegacyId); + } + + public AetheriaRuntimeNameFile? GetNameFile(AetheriaRuntimeCorporation corporation) + { + return FindNameFileByLegacyId(corporation.GeonameFileLegacyId); + } + + private static T? TryGet(IReadOnlyDictionary dictionary, string key) where T : class + { + return string.IsNullOrWhiteSpace(key) ? null : dictionary.TryGetValue(key, out var value) ? value : null; + } +} + +public sealed class AetheriaRuntimeCatalogItem +{ + public AetheriaRuntimeCatalogItem( + string legacyId, + string name, + string category, + string description, + string manufacturerLegacyId, + int price, + double mass, + double volume, + int shapeWidth, + int shapeHeight, + int occupiedCells, + string hardpointType, + string hullType, + IReadOnlyList behaviorKinds, + int maxStack, + double durability, + string weaponRange, + string weaponCaliber, + string weaponType, + string weaponFireTypes, + string weaponModifiers) + { + LegacyId = legacyId; + Name = name; + Category = category; + Description = description; + ManufacturerLegacyId = manufacturerLegacyId; + Price = price; + Mass = mass; + Volume = volume; + ShapeWidth = shapeWidth; + ShapeHeight = shapeHeight; + OccupiedCells = occupiedCells; + HardpointType = hardpointType; + HullType = hullType; + BehaviorKinds = behaviorKinds; + MaxStack = maxStack; + Durability = durability; + WeaponRange = weaponRange; + WeaponCaliber = weaponCaliber; + WeaponType = weaponType; + WeaponFireTypes = weaponFireTypes; + WeaponModifiers = weaponModifiers; + } + + public string LegacyId { get; } + public string Name { get; } + public string Category { get; } + public string Description { get; } + public string ManufacturerLegacyId { get; } + public int Price { get; } + public double Mass { get; } + public double Volume { get; } + public int ShapeWidth { get; } + public int ShapeHeight { get; } + public int OccupiedCells { get; } + public string HardpointType { get; } + public string HullType { get; } + public IReadOnlyList BehaviorKinds { get; } + public int MaxStack { get; } + public double Durability { get; } + public string WeaponRange { get; } + public string WeaponCaliber { get; } + public string WeaponType { get; } + public string WeaponFireTypes { get; } + public string WeaponModifiers { get; } + + public static AetheriaRuntimeCatalogItem FromState(AetheriaItemDefinition item) + { + return new AetheriaRuntimeCatalogItem( + item.LegacyId, + item.Name, + item.Category, + item.Description, + item.ManufacturerLegacyId, + item.Price, + item.Mass, + item.Volume, + item.ShapeWidth, + item.ShapeHeight, + item.OccupiedCells, + item.HardpointType, + item.HullType, + item.BehaviorKinds, + item.MaxStack, + item.Durability, + item.WeaponRange, + item.WeaponCaliber, + item.WeaponType, + item.WeaponFireTypes, + item.WeaponModifiers); + } +} + +public sealed class AetheriaRuntimeCorporation +{ + public AetheriaRuntimeCorporation( + string legacyId, + string name, + string shortName, + string description, + string geonameFileLegacyId, + string bossHullLegacyId, + int influenceDistance, + int allegianceCount) + { + LegacyId = legacyId; + Name = name; + ShortName = shortName; + Description = description; + GeonameFileLegacyId = geonameFileLegacyId; + BossHullLegacyId = bossHullLegacyId; + InfluenceDistance = influenceDistance; + AllegianceCount = allegianceCount; + } + + public string LegacyId { get; } + public string Name { get; } + public string ShortName { get; } + public string Description { get; } + public string GeonameFileLegacyId { get; } + public string BossHullLegacyId { get; } + public int InfluenceDistance { get; } + public int AllegianceCount { get; } + + public static AetheriaRuntimeCorporation FromState(AetheriaCorporation corporation) + { + return new AetheriaRuntimeCorporation( + corporation.LegacyId, + corporation.Name, + corporation.ShortName, + corporation.Description, + corporation.GeonameFileLegacyId, + corporation.BossHullLegacyId, + corporation.InfluenceDistance, + corporation.AllegianceCount); + } +} + +public sealed class AetheriaRuntimeNameFile +{ + public AetheriaRuntimeNameFile( + string legacyId, + string name, + int nameCount, + IReadOnlyList sampleNames) + { + LegacyId = legacyId; + Name = name; + NameCount = nameCount; + SampleNames = sampleNames; + } + + public string LegacyId { get; } + public string Name { get; } + public int NameCount { get; } + public IReadOnlyList SampleNames { get; } + + public static AetheriaRuntimeNameFile FromState(AetheriaNameFile nameFile) + { + return new AetheriaRuntimeNameFile( + nameFile.LegacyId, + nameFile.Name, + nameFile.NameCount, + nameFile.SampleNames); + } +} diff --git a/Aetheria.State/README.md b/Aetheria.State/README.md index f26d567e4..a222c9f6c 100644 --- a/Aetheria.State/README.md +++ b/Aetheria.State/README.md @@ -70,6 +70,13 @@ summary, trade-catalog, and corporation views. This is a typed CultCache provider document and a local mirror of the Eve contract until the shared Eve Unity UI Toolkit lowering package exists. +`Aetheria.State.Unity` is the Unity-facing runtime read facade over typed state. +It opens `aetheria-world.cc`, emits immutable catalog read models for trade, +equipment, behavior, hardpoint, manufacturer, corporation, and name-file +queries, and can read the published Eve catalog surface. It does not deserialize +legacy `DatabaseEntry` objects and does not write state. Unity can use this as +the first package boundary once CultLib/Eve runtime packaging is available. + `Aetheria.State.Verify` opens a materialized state file and checks that the typed migration ledger matches the actual item, corporation, and name-file records in the `.cc` store. It also verifies the canonical legacy-ID lookup API @@ -78,6 +85,10 @@ corporation catalog facts survived materialization. The verifier also exercises the typed catalog snapshot query surface and the published Eve catalog surface. Use it after import when `GameData/aetheria-world.cc` changes. +`Aetheria.State.Unity.Smoke` opens the materialized state through the runtime +facade and proves read-only Unity-facing catalog access without the legacy +catalog reader. + Current rebuild notes: - `MessagePack` 3.1.4 is pulled transitively through CultLib and is currently diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 2cccbc56e..8a448c386 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -71,6 +71,12 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. `.cc` records. It exposes trade-item, manufacturer, corporation prefix, and corporation name-file queries, plus equipment, hardpoint, and behavior queries without touching `DatabaseEntry`. +- `Aetheria.State.Unity` is the Unity-facing runtime read facade over typed + catalog state. It opens `aetheria-world.cc`, emits immutable catalog read + models for trade/equipment/behavior/hardpoint/manufacturer/corporation/name + queries, and reads the published Eve surface without deserializing legacy + `DatabaseEntry` objects. It is not a simulation owner and does not write + 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 @@ -133,7 +139,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. `LegacyCatalogBoundary` is the only named owner for old MessagePack catalog reads and legacy `DatabaseLink` resolution inside the Unity runtime until catalog migration lands. Runtime code receives `ILegacyCatalogReader`, not the - concrete cache. The legacy cache no longer has a public mutation surface. + concrete cache. `Aetheria.State.Unity` owns a read-only typed catalog facade + for future Unity consumers; it does not write state or own simulation. The + legacy cache no longer has a public mutation surface. - Inputs: Unity gameplay code, legacy catalog files, typed state documents, and CultMesh server state. - Outputs: `Aetheria.State` emits `.cc` state and CultMesh documents. Legacy @@ -250,6 +258,9 @@ First Aetheria surfaces to publish: catalog fields not covered by the stable scalar/fingerprint pass. 4. Runtime cutover + - Done: add a Unity-facing typed catalog read facade and smoke proving it can + read the materialized `.cc` catalog plus Eve surface without the legacy + catalog reader. - Replace `ActionGameManager` cache bootstrap with the new state runtime. - Convert domain references from GUID/base-class patterns to typed record refs. @@ -308,6 +319,9 @@ First Aetheria surfaces to publish: typed state without old readers remaining on the live runtime path. - 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 `Aetheria.State.Unity` without `DatabaseEntry` or + `LegacyCatalogCache`. - 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