Expose Aetheria projected state as CultMesh handles

This commit is contained in:
Meta Vens 2026-06-27 18:07:51 +01:00
commit dc6ac7f446
4 changed files with 49 additions and 214 deletions

View file

@ -596,21 +596,19 @@ Assert-Contains $verseClient "AetheriaClientState Aetheria()" "AetheriaRuntimeVe
Assert-Contains $verseClient "CreateAetheriaStateFacade" "AetheriaRuntimeVerseClient must own projected state facade creation."
$clientState = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaClientState.cs"
Assert-Contains $clientState "CultMeshDocumentHandle<TDocument>" "Aetheria state documents must wrap CultMesh typed document handles."
Assert-Contains $clientState "CultMeshDocumentHandle<TDocument>" "Aetheria state documents must expose CultMesh typed document handles directly."
Assert-Contains $clientState "CultMeshDocumentCatalog" "Aetheria state facade must delegate type/schema lookup to CultMesh."
Assert-Contains $clientState "CultMesh.Documents" "Aetheria state facade must build its projected document catalog through CultMesh."
Assert-Contains $clientState "IAetheriaRuntimeProjectedDocument" "Aetheria projected documents must expose schema metadata independent of exact CLR type."
Assert-Contains $clientState "_documentsByType" "Aetheria state facade must index projected documents by shared document type."
Assert-Contains $clientState "ICultMeshDocumentHandle" "Aetheria schema lookup must expose CultMesh schema metadata independent of exact CLR type."
Assert-NotContains $clientState "AetheriaRuntimeProjectedDocument" "Aetheria state facade must not wrap CultMesh document handles in a private projected-document abstraction."
Assert-NotContains $clientState "_documentsByType" "Aetheria state facade must not maintain a private type index beside CultMeshDocumentCatalog."
Assert-Contains $clientState "TryGetDocument<TDocument>" "Aetheria state facade must support safe typed document lookup."
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 expose CultMesh same-schema aliases."
Assert-NotContains $clientState "CultDocumentMessagePackSerialization" "Aetheria projected document aliasing must be owned by CultMesh, not the Aetheria facade."
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."
Assert-Contains $clientState "Watch()" "Aetheria state documents must support reactive typed reads."
$projection = Read-Text "Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeRtsProjection.cs"
Assert-Contains $projection "ProjectZoneDetails" "Shared projection code must own the zone-details feed."

View file

@ -40,14 +40,14 @@ namespace GameCult.Aetheria.State.Verse
public AetheriaClientState Aetheria() => State;
public AetheriaRuntimeProjectedDocument<TDocument> Document<TDocument>()
public CultMeshDocumentHandle<TDocument> Document<TDocument>()
where TDocument : class
{
ThrowIfDisposed();
return State.Document<TDocument>();
}
public IAetheriaRuntimeProjectedDocument DocumentBySchema(string schemaVersion)
public ICultMeshDocumentHandle DocumentBySchema(string schemaVersion)
{
ThrowIfDisposed();
return State.DocumentBySchema(schemaVersion);

View file

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GameCult.Mesh;
using R3;
@ -9,209 +7,52 @@ using R3;
namespace GameCult.Aetheria.State.Verse
{
public sealed class AetheriaRuntimeProjectedDocument<TDocument> : IAetheriaRuntimeProjectedDocument
where TDocument : class
{
private readonly CultMeshDocumentHandle<TDocument> _handle;
internal AetheriaRuntimeProjectedDocument(
CultMeshDocumentHandle<TDocument> handle)
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
public string DocumentId => _handle.DocumentId;
public Type DocumentType => _handle.DocumentType;
public string SchemaName => _handle.SchemaName;
public string SchemaVersion => _handle.SchemaVersion;
public CultMeshRouteHint RouteHint => _handle.RouteHint;
public System.Collections.Generic.IReadOnlyList<CultMeshProjectionSource> Sources => _handle.Sources;
public bool CanReplace => _handle.CanReplace;
public CultMeshDocumentHandle<TDocument> Handle => _handle;
public ICultMeshDocumentHandle UntypedHandle => _handle;
public Task<TDocument> LatestAsync()
{
return _handle.LatestAsync();
}
public TDocument Latest()
{
return _handle.LatestAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}
public Observable<TDocument> Watch()
{
return _handle.Watch();
}
public IDisposable Watch(Action<TDocument> onNext)
{
if (onNext == null) throw new ArgumentNullException(nameof(onNext));
return _handle.Watch(onNext);
}
public Task ReplaceAsync(TDocument value)
{
return _handle.ReplaceAsync(value);
}
internal static AetheriaRuntimeProjectedDocument<TDocument> Create(
string documentId,
string runtimeId,
Func<Task<TDocument>> latest,
Observable<AetheriaRuntimeDaemonFrameDocument> frameChanges,
Func<AetheriaRuntimeDaemonFrameDocument, Task<TDocument>> projectFrame,
params CultMeshProjectionSource[] sources)
{
if (string.IsNullOrWhiteSpace(documentId))
throw new ArgumentException("Document id must be non-empty.", nameof(documentId));
if (string.IsNullOrWhiteSpace(runtimeId))
throw new ArgumentException("Runtime id must be non-empty.", nameof(runtimeId));
if (latest == null) throw new ArgumentNullException(nameof(latest));
if (frameChanges == null) throw new ArgumentNullException(nameof(frameChanges));
if (projectFrame == null) throw new ArgumentNullException(nameof(projectFrame));
var verse = CultMesh.Verse("aetheria.local", runtimeId);
var handle = CultMesh.Document(
documentId,
verse,
_ => latest(),
_ => frameChanges
.SelectAwait(async (frame, cancellationToken) =>
await projectFrame(frame).ConfigureAwait(false)),
sources: sources,
routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria typed projected state"));
return new AetheriaRuntimeProjectedDocument<TDocument>(handle);
}
public AetheriaRuntimeProjectedDocument<TAlias> AsSchemaAlias<TAlias>() where TAlias : class
{
return new AetheriaRuntimeProjectedDocument<TAlias>(_handle.AsSchemaAlias<TAlias>());
}
}
public interface IAetheriaRuntimeProjectedDocument
{
Type DocumentType { get; }
string DocumentId { get; }
string SchemaName { get; }
string SchemaVersion { get; }
CultMeshRouteHint RouteHint { get; }
System.Collections.Generic.IReadOnlyList<CultMeshProjectionSource> Sources { get; }
bool CanReplace { get; }
ICultMeshDocumentHandle UntypedHandle { get; }
AetheriaRuntimeProjectedDocument<TAlias> AsSchemaAlias<TAlias>() where TAlias : class;
}
internal sealed class AetheriaRuntimeUntypedProjectedDocument : IAetheriaRuntimeProjectedDocument
{
private readonly ICultMeshDocumentHandle _handle;
public AetheriaRuntimeUntypedProjectedDocument(ICultMeshDocumentHandle handle)
{
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
}
public Type DocumentType => _handle.DocumentType;
public string DocumentId => _handle.DocumentId;
public string SchemaName => _handle.SchemaName;
public string SchemaVersion => _handle.SchemaVersion;
public CultMeshRouteHint RouteHint => _handle.RouteHint;
public System.Collections.Generic.IReadOnlyList<CultMeshProjectionSource> Sources => _handle.Sources;
public bool CanReplace => _handle.CanReplace;
public ICultMeshDocumentHandle UntypedHandle => _handle;
public AetheriaRuntimeProjectedDocument<TAlias> AsSchemaAlias<TAlias>() where TAlias : class
{
return new AetheriaRuntimeProjectedDocument<TAlias>(_handle.AsSchemaAlias<TAlias>());
}
}
public sealed class AetheriaClientState
{
private readonly IReadOnlyDictionary<Type, object> _documentsByType;
private readonly CultMeshDocumentCatalog _documents;
internal AetheriaClientState(
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentZoneDocument> currentZone,
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentEntityDocument> currentEntity,
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentDockingDocument> currentDocking,
AetheriaRuntimeProjectedDocument<AetheriaRuntimeZoneContactsDocument> zoneContacts,
AetheriaRuntimeProjectedDocument<AetheriaRuntimeStationRefitDocument> stationRefit,
AetheriaRuntimeProjectedDocument<AetheriaRuntimeSectorMapDocument> sectorMap,
AetheriaRuntimeProjectedDocument<AetheriaRuntimeZoneRenderDocument> zoneRender)
CultMeshDocumentHandle<AetheriaRuntimeCurrentZoneDocument> currentZone,
CultMeshDocumentHandle<AetheriaRuntimeCurrentEntityDocument> currentEntity,
CultMeshDocumentHandle<AetheriaRuntimeCurrentDockingDocument> currentDocking,
CultMeshDocumentHandle<AetheriaRuntimeZoneContactsDocument> zoneContacts,
CultMeshDocumentHandle<AetheriaRuntimeStationRefitDocument> stationRefit,
CultMeshDocumentHandle<AetheriaRuntimeSectorMapDocument> sectorMap,
CultMeshDocumentHandle<AetheriaRuntimeZoneRenderDocument> zoneRender)
{
Current = new AetheriaClientCurrentState(currentZone, currentEntity, currentDocking);
ZoneContacts = zoneContacts ?? throw new ArgumentNullException(nameof(zoneContacts));
StationRefit = stationRefit ?? throw new ArgumentNullException(nameof(stationRefit));
SectorMap = sectorMap ?? throw new ArgumentNullException(nameof(sectorMap));
ZoneRender = zoneRender ?? throw new ArgumentNullException(nameof(zoneRender));
_documentsByType = new Dictionary<Type, object>
{
[typeof(AetheriaRuntimeCurrentZoneDocument)] = Current.Zone,
[typeof(AetheriaRuntimeCurrentEntityDocument)] = Current.Entity,
[typeof(AetheriaRuntimeCurrentDockingDocument)] = Current.Docking,
[typeof(AetheriaRuntimeZoneContactsDocument)] = ZoneContacts,
[typeof(AetheriaRuntimeStationRefitDocument)] = StationRefit,
[typeof(AetheriaRuntimeSectorMapDocument)] = SectorMap,
[typeof(AetheriaRuntimeZoneRenderDocument)] = ZoneRender
};
_documents = CultMesh.Documents(_documentsByType
.Values
.OfType<IAetheriaRuntimeProjectedDocument>()
.Select(document => document.UntypedHandle));
_documents = CultMesh.Documents(
Current.Zone,
Current.Entity,
Current.Docking,
ZoneContacts,
StationRefit,
SectorMap,
ZoneRender);
}
public AetheriaClientCurrentState Current { get; }
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeZoneContactsDocument> ZoneContacts { get; }
public CultMeshDocumentHandle<AetheriaRuntimeZoneContactsDocument> ZoneContacts { get; }
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeStationRefitDocument> StationRefit { get; }
public CultMeshDocumentHandle<AetheriaRuntimeStationRefitDocument> StationRefit { get; }
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeSectorMapDocument> SectorMap { get; }
public CultMeshDocumentHandle<AetheriaRuntimeSectorMapDocument> SectorMap { get; }
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeZoneRenderDocument> ZoneRender { get; }
public CultMeshDocumentHandle<AetheriaRuntimeZoneRenderDocument> ZoneRender { get; }
public bool TryGetDocumentBySchema(
string schemaVersion,
out IAetheriaRuntimeProjectedDocument document)
out ICultMeshDocumentHandle document)
{
if (_documents.TryGetDocumentBySchema(schemaVersion, out var handle))
{
document = new AetheriaRuntimeUntypedProjectedDocument(handle);
return true;
}
document = null!;
return false;
return _documents.TryGetDocumentBySchema(schemaVersion, out document);
}
public IAetheriaRuntimeProjectedDocument DocumentBySchema(string schemaVersion)
public ICultMeshDocumentHandle DocumentBySchema(string schemaVersion)
{
if (TryGetDocumentBySchema(schemaVersion, out var document))
return document;
@ -221,20 +62,13 @@ namespace GameCult.Aetheria.State.Verse
}
public bool TryGetDocument<TDocument>(
out AetheriaRuntimeProjectedDocument<TDocument> document)
out CultMeshDocumentHandle<TDocument> document)
where TDocument : class
{
if (_documents.TryGetDocument<TDocument>(out var handle))
{
document = new AetheriaRuntimeProjectedDocument<TDocument>(handle);
return true;
}
document = null!;
return false;
return _documents.TryGetDocument(out document);
}
public AetheriaRuntimeProjectedDocument<TDocument> Document<TDocument>()
public CultMeshDocumentHandle<TDocument> Document<TDocument>()
where TDocument : class
{
if (TryGetDocument<TDocument>(out var document))
@ -253,7 +87,7 @@ namespace GameCult.Aetheria.State.Verse
public TDocument Latest<TDocument>()
where TDocument : class
{
return Document<TDocument>().Latest();
return Document<TDocument>().LatestAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}
public Observable<TDocument> Watch<TDocument>()
@ -265,27 +99,27 @@ namespace GameCult.Aetheria.State.Verse
public IDisposable Watch<TDocument>(Action<TDocument> onNext)
where TDocument : class
{
if (onNext == null) throw new ArgumentNullException(nameof(onNext));
return Document<TDocument>().Watch(onNext);
}
}
public sealed class AetheriaClientCurrentState
{
internal AetheriaClientCurrentState(
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentZoneDocument> zone,
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentEntityDocument> entity,
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentDockingDocument> docking)
CultMeshDocumentHandle<AetheriaRuntimeCurrentZoneDocument> zone,
CultMeshDocumentHandle<AetheriaRuntimeCurrentEntityDocument> entity,
CultMeshDocumentHandle<AetheriaRuntimeCurrentDockingDocument> docking)
{
Zone = zone ?? throw new ArgumentNullException(nameof(zone));
Entity = entity ?? throw new ArgumentNullException(nameof(entity));
Docking = docking ?? throw new ArgumentNullException(nameof(docking));
}
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentZoneDocument> Zone { get; }
public CultMeshDocumentHandle<AetheriaRuntimeCurrentZoneDocument> Zone { get; }
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentEntityDocument> Entity { get; }
public CultMeshDocumentHandle<AetheriaRuntimeCurrentEntityDocument> Entity { get; }
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentDockingDocument> Docking { get; }
public CultMeshDocumentHandle<AetheriaRuntimeCurrentDockingDocument> Docking { get; }
}
}

View file

@ -685,7 +685,7 @@ namespace GameCult.Aetheria.State.Verse
frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectZoneRender(frame)),
AetheriaRuntimeDaemonSchemas.ZoneRender));
AetheriaRuntimeProjectedDocument<TDocument> ProjectedDocument<TDocument>(
CultMeshDocumentHandle<TDocument> ProjectedDocument<TDocument>(
string documentId,
Func<AetheriaRuntimeDaemonFrameDocument, Task<TDocument>> project,
string schemaId,
@ -703,13 +703,16 @@ namespace GameCult.Aetheria.State.Verse
.Append(CultMesh.ProjectionSource(documentId, schemaId, "projected Aetheria client document"))
.ToArray();
return AetheriaRuntimeProjectedDocument<TDocument>.Create(
var verse = CultMesh.Verse("aetheria.local", RuntimeId);
return CultMesh.Document(
documentId,
RuntimeId,
async () => await project(await RequireFrameAsync().ConfigureAwait(false)).ConfigureAwait(false),
frameChanges,
project,
sources);
verse,
async _ => await project(await RequireFrameAsync().ConfigureAwait(false)).ConfigureAwait(false),
_ => frameChanges
.SelectAwait(async (frame, cancellationToken) =>
await project(frame).ConfigureAwait(false)),
sources: sources,
routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria typed projected state"));
}
async Task<AetheriaRuntimeStationRefitDocument> ProjectStationRefitAsync(