Adapt Aetheria state documents across schema aliases

This commit is contained in:
Meta Vens 2026-06-27 14:21:50 +01:00
commit 51ccf071e0
3 changed files with 57 additions and 7 deletions

View file

@ -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<TDocument>()`, `client.Aetheria().DocumentBySchema(schemaVersion)`, `client.Aetheria().LatestAsync<TDocument>()`, and `client.Watch<TDocument>()` as the C# stepping stone toward CultMesh-native type/schema lookup.
- Shared projected documents are reachable through `client.Aetheria().Document<TDocument>()`, including same-schema CLR aliases, `client.Aetheria().DocumentBySchema(schemaVersion)`, `client.Aetheria().LatestAsync<TDocument>()`, and `client.Watch<TDocument>()` 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.

View file

@ -604,6 +604,8 @@ Assert-Contains $clientState "TryGetDocument<TDocument>" "Aetheria state facade
Assert-Contains $clientState "TryGetDocumentBySchema" "Aetheria state facade must support safe schema document lookup."
Assert-Contains $clientState "Document<TDocument>()" "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<TAlias>" "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<TDocument>()" "Aetheria state facade must support one-call typed document reads."
Assert-Contains $clientState "Watch<TDocument>()" "Aetheria state facade must support reactive typed document lookup."
Assert-Contains $clientState "LatestAsync()" "Aetheria state documents must support one-shot typed reads."

View file

@ -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<AetheriaRuntimeStateQueryParameters, TDocument> Feed => _feed.Feed;
internal CultMeshVerseContext Context => _feed.Context;
public Task<TDocument> 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<TDocument>(feed.Bind(verse));
}
public AetheriaRuntimeProjectedDocument<TAlias> AsSchemaAlias<TAlias>()
{
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<AetheriaRuntimeStateQueryParameters, TAlias>(
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<TAlias>(feed.Bind(Context));
}
private static void ValidateSchemaAlias(Type sourceType, Type aliasType)
{
var source = sourceType.GetCustomAttribute<CultDocumentAttribute>();
var alias = aliasType.GetCustomAttribute<CultDocumentAttribute>();
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<CultMeshProjectionSource> Sources { get; }
AetheriaRuntimeProjectedDocument<TAlias> AsSchemaAlias<TAlias>();
}
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<TDocument> schemaTypedDocument)
_documentsBySchema.TryGetValue(schemaVersion, out var schemaDocument))
{
document = schemaTypedDocument;
if (schemaDocument is AetheriaRuntimeProjectedDocument<TDocument> schemaTypedDocument)
{
document = schemaTypedDocument;
return true;
}
document = schemaDocument.AsSchemaAlias<TDocument>();
return true;
}