diff --git a/Aetheria.State/docs/cultmesh-ergonomics-staged-migration-plan.md b/Aetheria.State/docs/cultmesh-ergonomics-staged-migration-plan.md index 74d2728be..046a1bb36 100644 --- a/Aetheria.State/docs/cultmesh-ergonomics-staged-migration-plan.md +++ b/Aetheria.State/docs/cultmesh-ergonomics-staged-migration-plan.md @@ -277,8 +277,10 @@ of transitional `Current*Async()`, `StationRefitAsync()`, `SectorMapAsync()`, also indexes projected state documents by shared document type so callers can retrieve or watch a known typed document with one call while CultMesh owns the bound live feed. The facade also indexes those handles by `CultDocument` -schema name/version, so a caller can resolve the shared schema identity even -when exact local CLR type identity is not the public contract. +schema name/version and can adapt a handle to a different local CLR document +type with the same schema identity, so caller code can start treating shared +schema identity as the contract even before CultMesh owns alias conversion +directly. Goal: every client-facing state access becomes a single CultMesh document, collection, query, or pointer call. The caller names the domain state it wants and gets a typed reactive value. It does not manually inspect daemon frames, current-state projections, station rows, record keys, local facade indexes, or transport route details. @@ -338,7 +340,7 @@ const refit = await client.aetheria() Work: -- Keep pushing shared CultMesh typed reactive document and collection handles down until type/schema lookup, including safe conversion for schema-compatible CLR aliases, is owned by CultMesh rather than by the Aetheria facade registry. +- Keep pushing shared CultMesh typed reactive document and collection handles down until type/schema lookup, including the current Aetheria facade shim for schema-compatible CLR aliases, is owned by CultMesh rather than by the Aetheria facade registry. - Generate Aetheria current/station/zone/catalog accessors from schema metadata. - Move derived-state joins, such as current docking bay from current docking plus station refit plus entity records, into generated/internal query executors. - Replace remaining Unity menu/HUD/render calls to `TryResolve*` and `_observedFacadeIndex` with typed handles or generated/internal render adapters. @@ -349,7 +351,7 @@ Work: Gates: - `InventoryMenu`, `InventoryPanel`, `TradeMenu`, `LocalMenu`, `SchematicDisplay`, `SectorRenderer`, `MapRenderer`, and `ZoneRenderer` do not manually join state projections to obtain domain values. -- Shared projected documents are reachable through `client.Aetheria().Document()`, `client.Aetheria().DocumentBySchema(schemaVersion)`, `client.Aetheria().LatestAsync()`, and `client.Watch()` as the C# stepping stone toward CultMesh-native type/schema lookup. +- Shared projected documents are reachable through `client.Aetheria().Document()`, including same-schema CLR aliases, `client.Aetheria().DocumentBySchema(schemaVersion)`, `client.Aetheria().LatestAsync()`, and `client.Watch()` as the C# stepping stone toward CultMesh-native type/schema lookup. - Client-facing code contains no `_observedFacadeIndex.TryResolve*` outside Unity render adapter internals. - Client-facing code contains no `ResolveClient().Current*Async().GetAwaiter().GetResult()` or equivalent blocking state reads. - `TryGetTypedCurrentDockingBayFacade` and similar transitional helpers are deleted rather than renamed. diff --git a/Aetheria.State/scripts/verify-stage7d-unity-parity.ps1 b/Aetheria.State/scripts/verify-stage7d-unity-parity.ps1 index 627b4ee7d..c57873570 100644 --- a/Aetheria.State/scripts/verify-stage7d-unity-parity.ps1 +++ b/Aetheria.State/scripts/verify-stage7d-unity-parity.ps1 @@ -604,6 +604,8 @@ Assert-Contains $clientState "TryGetDocument" "Aetheria state facade Assert-Contains $clientState "TryGetDocumentBySchema" "Aetheria state facade must support safe schema document lookup." Assert-Contains $clientState "Document()" "Aetheria state facade must support direct typed document lookup." Assert-Contains $clientState "DocumentBySchema(string schemaVersion)" "Aetheria state facade must support direct schema document lookup." +Assert-Contains $clientState "AsSchemaAlias" "Aetheria state facade must adapt same-schema projected documents across CLR aliases until CultMesh owns alias conversion." +Assert-Contains $clientState "CultDocumentMessagePackSerialization" "Aetheria projected document aliasing must use the canonical CultCache document serializer." Assert-Contains $clientState "LatestAsync()" "Aetheria state facade must support one-call typed document reads." Assert-Contains $clientState "Watch()" "Aetheria state facade must support reactive typed document lookup." Assert-Contains $clientState "LatestAsync()" "Aetheria state documents must support one-shot typed reads." diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs index 213fadf2f..55eb53168 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaClientState.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; using GameCult.Caching; +using GameCult.Caching.MessagePack; using GameCult.Mesh; using R3; @@ -44,6 +45,8 @@ namespace GameCult.Aetheria.State.Verse public CultMeshLiveFeed Feed => _feed.Feed; + internal CultMeshVerseContext Context => _feed.Context; + public Task LatestAsync() { return _feed.SnapshotAsync(AetheriaRuntimeStateQueryParameters.Empty); @@ -88,6 +91,42 @@ namespace GameCult.Aetheria.State.Verse var verse = CultMesh.Verse("aetheria.local", runtimeId); return new AetheriaRuntimeProjectedDocument(feed.Bind(verse)); } + + public AetheriaRuntimeProjectedDocument AsSchemaAlias() + { + ValidateSchemaAlias(typeof(TDocument), typeof(TAlias)); + + TAlias Convert(TDocument document) + { + if (document is TAlias alreadyTyped) + return alreadyTyped; + + var payload = CultDocumentMessagePackSerialization.SerializeUntyped(document!, typeof(TDocument)); + return (TAlias)CultDocumentMessagePackSerialization.DeserializeUntyped(typeof(TAlias), payload); + } + + var feed = new CultMeshLiveFeed( + DocumentId, + async (parameters, _context) => Convert(await _feed.SnapshotAsync(parameters).ConfigureAwait(false)), + (parameters, _context) => _feed.Watch(parameters).Select(Convert), + sources: Sources, + routeHint: RouteHint); + + return new AetheriaRuntimeProjectedDocument(feed.Bind(Context)); + } + + private static void ValidateSchemaAlias(Type sourceType, Type aliasType) + { + var source = sourceType.GetCustomAttribute(); + var alias = aliasType.GetCustomAttribute(); + if (source == null || alias == null || + !string.Equals(source.SchemaName, alias.SchemaName, StringComparison.Ordinal) || + !string.Equals(source.SchemaVersion, alias.SchemaVersion, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"{aliasType.FullName} cannot alias {sourceType.FullName}; CultDocument schema name/version must match exactly."); + } + } } public interface IAetheriaRuntimeProjectedDocument @@ -103,6 +142,8 @@ namespace GameCult.Aetheria.State.Verse CultMeshRouteHint RouteHint { get; } System.Collections.Generic.IReadOnlyList Sources { get; } + + AetheriaRuntimeProjectedDocument AsSchemaAlias(); } public sealed class AetheriaClientState @@ -182,10 +223,15 @@ namespace GameCult.Aetheria.State.Verse var schemaVersion = SchemaVersionFor(typeof(TDocument)); if (!string.IsNullOrWhiteSpace(schemaVersion) && - _documentsBySchema.TryGetValue(schemaVersion, out var schemaDocument) && - schemaDocument is AetheriaRuntimeProjectedDocument schemaTypedDocument) + _documentsBySchema.TryGetValue(schemaVersion, out var schemaDocument)) { - document = schemaTypedDocument; + if (schemaDocument is AetheriaRuntimeProjectedDocument schemaTypedDocument) + { + document = schemaTypedDocument; + return true; + } + + document = schemaDocument.AsSchemaAlias(); return true; }