Add typed Aetheria client state facade
This commit is contained in:
parent
d263118e45
commit
2266ae096e
5 changed files with 262 additions and 16 deletions
|
|
@ -144,6 +144,43 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.IsFalse(AetheriaRuntimeDaemonFrameStore.TryReadFrame(statePath, out _));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AetheriaClientStateFacadeReadsProjectedDocumentsThroughCultMesh()
|
||||
{
|
||||
var statePath = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"aetheria-client-state-facade-tests",
|
||||
Path.GetRandomFileName(),
|
||||
"state.cc");
|
||||
var frame = AetheriaRuntimeDaemonFrameDocument.Create(
|
||||
RunWithTwoEntities(),
|
||||
"daemon",
|
||||
"session-state",
|
||||
7,
|
||||
1.25,
|
||||
0.02);
|
||||
PublishLatestFrameThroughVerseClient(statePath, frame);
|
||||
|
||||
using var client = AetheriaClient
|
||||
.OpenAsync(statePath, "unity-test", pullOnOpen: true)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
var currentEntity = client.State.Current.Entity.LatestAsync().GetAwaiter().GetResult();
|
||||
var legacyCurrentEntity = client.CurrentEntityAsync().GetAwaiter().GetResult();
|
||||
|
||||
Assert.AreEqual("aetheria.current.entity", client.State.Current.Entity.DocumentId);
|
||||
Assert.AreEqual(AetheriaRuntimeDaemonSchemas.CurrentEntity, currentEntity.Schema);
|
||||
Assert.AreEqual("zone.0.entity.0", currentEntity.EntityKey);
|
||||
Assert.AreEqual(0, currentEntity.EntityIndex);
|
||||
Assert.AreEqual("Player", currentEntity.Entity?.Name);
|
||||
Assert.AreEqual(legacyCurrentEntity.EntityKey, currentEntity.EntityKey);
|
||||
Assert.IsTrue(client.State.Current.Entity.Sources.Any(source =>
|
||||
source.SourceId == AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString()));
|
||||
Assert.IsTrue(client.State.StationRefit.Sources.Any(source =>
|
||||
source.SourceId == "catalog:aetheria.runtime"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TickRunnerAppliesObservedCommandsAndPublishesFrame()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonEditorSurfaceBuilder.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeDaemonOperationClient.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaClient.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaClientState.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaControl.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaUi.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeVerseClient.cs" />
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Mesh;
|
||||
using R3;
|
||||
|
||||
#nullable enable
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
private readonly AetheriaRuntimeDaemonOperationsClient _operations;
|
||||
private readonly AetheriaControl _control;
|
||||
private readonly AetheriaUi _ui;
|
||||
private readonly AetheriaClientState _state;
|
||||
private bool _disposed;
|
||||
|
||||
private AetheriaClient(AetheriaRuntimeVerseClient verse, string clientId, string sessionId)
|
||||
|
|
@ -27,12 +29,14 @@ namespace GameCult.Aetheria.State.Verse
|
|||
_operations = new AetheriaRuntimeDaemonOperationsClient(SendOperation);
|
||||
_control = new AetheriaControl(_operations);
|
||||
_ui = new AetheriaUi(this);
|
||||
_state = CreateStateFacade();
|
||||
}
|
||||
|
||||
public string StatePath => _verse.StatePath;
|
||||
public string RuntimeId => _verse.RuntimeId;
|
||||
public AetheriaControl Control => _control;
|
||||
public AetheriaUi Ui => _ui;
|
||||
public AetheriaClientState State => _state;
|
||||
|
||||
internal AetheriaRuntimeDaemonOperationsClient Operations => _operations;
|
||||
|
||||
|
|
@ -144,45 +148,37 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public async Task<AetheriaRuntimeCurrentZoneDocument> CurrentZoneAsync()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var frame = await RequireFrameAsync().ConfigureAwait(false);
|
||||
return AetheriaRuntimeRtsProjection.ProjectCurrentZone(frame);
|
||||
return await State.Current.Zone.LatestAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AetheriaRuntimeCurrentEntityDocument> CurrentEntityAsync()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var frame = await RequireFrameAsync().ConfigureAwait(false);
|
||||
return AetheriaRuntimeRtsProjection.ProjectCurrentEntity(frame);
|
||||
return await State.Current.Entity.LatestAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AetheriaRuntimeCurrentDockingDocument> CurrentDockingAsync()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var frame = await RequireFrameAsync().ConfigureAwait(false);
|
||||
return AetheriaRuntimeRtsProjection.ProjectCurrentDocking(frame);
|
||||
return await State.Current.Docking.LatestAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AetheriaRuntimeZoneContactsDocument> ZoneContactsAsync()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var frame = await RequireFrameAsync().ConfigureAwait(false);
|
||||
return AetheriaRuntimeRtsProjection.ProjectZoneContacts(frame);
|
||||
return await State.ZoneContacts.LatestAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AetheriaRuntimeStationRefitDocument> StationRefitAsync()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var frame = await RequireFrameAsync().ConfigureAwait(false);
|
||||
var loadoutTemplates = await _verse.GetLoadoutTemplatesAsync().ConfigureAwait(false);
|
||||
var catalog = _verse.OpenRuntimeCatalog();
|
||||
return AetheriaRuntimeRtsProjection.ProjectStationRefit(frame, loadoutTemplates, catalog);
|
||||
return await State.StationRefit.LatestAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AetheriaRuntimeSectorMapDocument> SectorMapAsync()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var frame = await RequireFrameAsync().ConfigureAwait(false);
|
||||
return AetheriaRuntimeRtsProjection.ProjectSectorMap(frame);
|
||||
return await State.SectorMap.LatestAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AetheriaRuntimeZoneDetailsDocument> ZoneDetailsAsync(int zoneIndex)
|
||||
|
|
@ -195,8 +191,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public async Task<AetheriaRuntimeZoneRenderDocument> ZoneRenderAsync()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var frame = await RequireFrameAsync().ConfigureAwait(false);
|
||||
return AetheriaRuntimeRtsProjection.ProjectZoneRender(frame);
|
||||
return await State.ZoneRender.LatestAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AetheriaRuntimeStarbridgeSessionSummaryDocument> StarbridgeSessionSummaryAsync(
|
||||
|
|
@ -390,6 +385,79 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return frame;
|
||||
}
|
||||
|
||||
private AetheriaClientState CreateStateFacade()
|
||||
{
|
||||
var frameChanges = _verse.WatchLatestFrames()
|
||||
.Where(change => change.Document != null)
|
||||
.Select(change => change.Document!);
|
||||
|
||||
return new AetheriaClientState(
|
||||
ProjectedDocument(
|
||||
"aetheria.current.zone",
|
||||
frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectCurrentZone(frame)),
|
||||
AetheriaRuntimeDaemonSchemas.CurrentZone),
|
||||
ProjectedDocument(
|
||||
"aetheria.current.entity",
|
||||
frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectCurrentEntity(frame)),
|
||||
AetheriaRuntimeDaemonSchemas.CurrentEntity),
|
||||
ProjectedDocument(
|
||||
"aetheria.current.docking",
|
||||
frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectCurrentDocking(frame)),
|
||||
AetheriaRuntimeDaemonSchemas.CurrentDocking),
|
||||
ProjectedDocument(
|
||||
"aetheria.zone.contacts",
|
||||
frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectZoneContacts(frame)),
|
||||
AetheriaRuntimeDaemonSchemas.ZoneContacts),
|
||||
ProjectedDocument(
|
||||
"aetheria.station.refit",
|
||||
ProjectStationRefitAsync,
|
||||
AetheriaRuntimeDaemonSchemas.StationRefit,
|
||||
CultMesh.ProjectionSource("catalog:aetheria.runtime"),
|
||||
CultMesh.ProjectionSource("loadout-templates:aetheria.runtime")),
|
||||
ProjectedDocument(
|
||||
"aetheria.sector.map",
|
||||
frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectSectorMap(frame)),
|
||||
AetheriaRuntimeDaemonSchemas.SectorMap),
|
||||
ProjectedDocument(
|
||||
"aetheria.zone.render",
|
||||
frame => Task.FromResult(AetheriaRuntimeRtsProjection.ProjectZoneRender(frame)),
|
||||
AetheriaRuntimeDaemonSchemas.ZoneRender));
|
||||
|
||||
AetheriaRuntimeProjectedDocument<TDocument> ProjectedDocument<TDocument>(
|
||||
string documentId,
|
||||
Func<AetheriaRuntimeDaemonFrameDocument, Task<TDocument>> project,
|
||||
string schemaId,
|
||||
params CultMeshProjectionSource[] additionalSources)
|
||||
{
|
||||
var sources = new[]
|
||||
{
|
||||
CultMesh.ProjectionSource(
|
||||
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString(),
|
||||
AetheriaRuntimeDaemonSchemas.Frame,
|
||||
"latest authoritative daemon frame")
|
||||
}
|
||||
.Concat(additionalSources ?? Array.Empty<CultMeshProjectionSource>())
|
||||
.Append(CultMesh.ProjectionSource(documentId, schemaId, "projected Aetheria client document"))
|
||||
.ToArray();
|
||||
|
||||
return AetheriaRuntimeProjectedDocument<TDocument>.Create(
|
||||
documentId,
|
||||
RuntimeId,
|
||||
async () => await project(await RequireFrameAsync().ConfigureAwait(false)).ConfigureAwait(false),
|
||||
frameChanges,
|
||||
project,
|
||||
sources);
|
||||
}
|
||||
|
||||
async Task<AetheriaRuntimeStationRefitDocument> ProjectStationRefitAsync(
|
||||
AetheriaRuntimeDaemonFrameDocument frame)
|
||||
{
|
||||
var loadoutTemplates = await _verse.GetLoadoutTemplatesAsync().ConfigureAwait(false);
|
||||
var catalog = _verse.OpenRuntimeCatalog();
|
||||
return AetheriaRuntimeRtsProjection.ProjectStationRefit(frame, loadoutTemplates, catalog);
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Mesh;
|
||||
using R3;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public readonly struct AetheriaRuntimeStateQueryParameters
|
||||
{
|
||||
public static readonly AetheriaRuntimeStateQueryParameters Empty = new AetheriaRuntimeStateQueryParameters();
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeProjectedDocument<TDocument>
|
||||
{
|
||||
private static readonly CultMeshPollingWatchOptions<TDocument> DefaultWatchOptions =
|
||||
new CultMeshPollingWatchOptions<TDocument>(TimeSpan.FromMilliseconds(100));
|
||||
|
||||
private readonly CultMeshBoundLiveFeed<AetheriaRuntimeStateQueryParameters, TDocument> _feed;
|
||||
|
||||
internal AetheriaRuntimeProjectedDocument(
|
||||
CultMeshBoundLiveFeed<AetheriaRuntimeStateQueryParameters, TDocument> feed)
|
||||
{
|
||||
_feed = feed ?? throw new ArgumentNullException(nameof(feed));
|
||||
}
|
||||
|
||||
public string DocumentId => _feed.FeedId;
|
||||
|
||||
public CultMeshRouteHint RouteHint => _feed.RouteHint;
|
||||
|
||||
public System.Collections.Generic.IReadOnlyList<CultMeshProjectionSource> Sources => _feed.Sources;
|
||||
|
||||
public CultMeshLiveFeed<AetheriaRuntimeStateQueryParameters, TDocument> Feed => _feed.Feed;
|
||||
|
||||
public Task<TDocument> LatestAsync()
|
||||
{
|
||||
return _feed.SnapshotAsync(AetheriaRuntimeStateQueryParameters.Empty);
|
||||
}
|
||||
|
||||
public Observable<TDocument> Watch()
|
||||
{
|
||||
return _feed.Watch(AetheriaRuntimeStateQueryParameters.Empty);
|
||||
}
|
||||
|
||||
public IDisposable Watch(Action<TDocument> onNext)
|
||||
{
|
||||
if (onNext == null) throw new ArgumentNullException(nameof(onNext));
|
||||
return Watch().Subscribe(onNext);
|
||||
}
|
||||
|
||||
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 feed = new CultMeshLiveFeed<AetheriaRuntimeStateQueryParameters, TDocument>(
|
||||
documentId,
|
||||
(_parameters, _context) => latest(),
|
||||
(_parameters, context) => frameChanges
|
||||
.SelectAwait(async (frame, cancellationToken) =>
|
||||
await projectFrame(frame).ConfigureAwait(false)),
|
||||
sources: sources,
|
||||
routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria typed projected state"));
|
||||
|
||||
var verse = CultMesh.Verse("aetheria.local", runtimeId);
|
||||
return new AetheriaRuntimeProjectedDocument<TDocument>(feed.Bind(verse));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AetheriaClientState
|
||||
{
|
||||
internal AetheriaClientState(
|
||||
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentZoneDocument> currentZone,
|
||||
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentEntityDocument> currentEntity,
|
||||
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentDockingDocument> currentDocking,
|
||||
AetheriaRuntimeProjectedDocument<AetheriaRuntimeZoneContactsDocument> zoneContacts,
|
||||
AetheriaRuntimeProjectedDocument<AetheriaRuntimeStationRefitDocument> stationRefit,
|
||||
AetheriaRuntimeProjectedDocument<AetheriaRuntimeSectorMapDocument> sectorMap,
|
||||
AetheriaRuntimeProjectedDocument<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));
|
||||
}
|
||||
|
||||
public AetheriaClientCurrentState Current { get; }
|
||||
|
||||
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeZoneContactsDocument> ZoneContacts { get; }
|
||||
|
||||
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeStationRefitDocument> StationRefit { get; }
|
||||
|
||||
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeSectorMapDocument> SectorMap { get; }
|
||||
|
||||
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeZoneRenderDocument> ZoneRender { get; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaClientCurrentState
|
||||
{
|
||||
internal AetheriaClientCurrentState(
|
||||
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentZoneDocument> zone,
|
||||
AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentEntityDocument> entity,
|
||||
AetheriaRuntimeProjectedDocument<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 AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentEntityDocument> Entity { get; }
|
||||
|
||||
public AetheriaRuntimeProjectedDocument<AetheriaRuntimeCurrentDockingDocument> Docking { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 80b87956450e46e4a9b7ed79b9d8aee8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in a new issue