Aetheria/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeVerseClient.cs

1103 lines
52 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using GameCult.Eve.Surface;
using GameCult.Mesh;
using GameCult.Networking;
using R3;
using EveUiCommandRequest = GameCult.Eve.Surface.EveSurfaceCommandRequest;
using EveSurfaceDocument = GameCult.Eve.Surface.EveSurfaceDocument;
#nullable enable
namespace GameCult.Aetheria.State.Verse
{
public static class AetheriaRuntimeVerseRecordKeys
{
public static CultRecordKey DaemonProviderAdvertisement { get; } =
new CultRecordKey("daemon:aetheria.provider_advertisement.v1");
public static CultRecordKey EveProviderAdvertisement { get; } =
new CultRecordKey("eve:provider:aetheria.daemon");
public const string EveCommandRecordPrefix = "eve:commands:aetheria.daemon";
public const string EveReceiptRecordPrefix = "eve:receipts:aetheria.daemon";
public static CultRecordKey DaemonHealth { get; } =
new CultRecordKey("daemon:aetheria.health.v1");
public static CultRecordKey DaemonCommandBoundary { get; } =
new CultRecordKey("daemon:aetheria.command_boundary.v1");
public static CultRecordKey DaemonAssetManifest { get; } =
new CultRecordKey("daemon:aetheria.asset_manifest.latest.v1");
public static CultRecordKey EveAssetCatalog { get; } =
new CultRecordKey("eve:assets:aetheria.daemon");
public static CultRecordKey VerseAuthorityPolicy { get; } =
new CultRecordKey(AetheriaRuntimeVerseAuthorityPolicyDocument.DocumentKey);
public static CultRecordKey DaemonFrameLatest { get; } =
new CultRecordKey("daemon:aetheria.frame.latest.v1");
public static CultRecordKey DaemonSoaViewLatest { get; } =
new CultRecordKey("daemon:aetheria.soa_view.latest.v1");
public static CultRecordKey StarbridgeScenarioLatest { get; } =
new CultRecordKey("starbridge:aetheria.scenario.latest.v1");
public static CultRecordKey StarbridgeSessionLatest { get; } =
new CultRecordKey("starbridge:aetheria.session.latest.v1");
public static CultRecordKey StarbridgeSessionSummary { get; } =
new CultRecordKey("daemon:aetheria.starbridge.session.latest.v1");
public static CultRecordKey StarbridgePlayerSeat(string seatId)
{
return new CultRecordKey(AetheriaRuntimeStarbridgePlayerSeatDocument.RecordKey(seatId));
}
public static CultRecordKey DaemonGameSurface { get; } =
new CultRecordKey("eve:surface:aetheria.daemon.game");
public static CultRecordKey DaemonGameTuiSurface { get; } =
new CultRecordKey("eve:surface:aetheria.daemon.game.tui");
public static CultRecordKey DaemonEditorSurface { get; } =
new CultRecordKey("eve:surface:aetheria.daemon.editor");
public static CultRecordKey DaemonEditorTuiSurface { get; } =
new CultRecordKey("eve:surface:aetheria.daemon.editor.tui");
public static CultRecordKey MainMenuSurface { get; } =
new CultRecordKey("eve:surface:aetheria.main_menu.root");
public static CultRecordKey MainMenuSettingsSurface { get; } =
new CultRecordKey("eve:surface:aetheria.main_menu.settings");
public static CultRecordKey MainMenuInputSettingsSurface { get; } =
new CultRecordKey("eve:surface:aetheria.main_menu.input_settings");
public static CultRecordKey MainMenuPlayerSettingsSurface { get; } =
new CultRecordKey("eve:surface:aetheria.main_menu.player_settings");
public static CultRecordKey MainMenuVerseSettingsSurface { get; } =
new CultRecordKey("eve:surface:aetheria.main_menu.verse_settings");
public static CultRecordKey InventoryPanelSurface { get; } =
new CultRecordKey("eve:surface:aetheria.inventory.panel");
public static CultRecordKey InventoryDropdownSurface { get; } =
new CultRecordKey("eve:surface:aetheria.inventory.panel.dropdown");
public static CultRecordKey MapMenuSurface { get; } =
new CultRecordKey("eve:surface:aetheria.map.zone_details");
public static CultRecordKey TradeMenuSurface { get; } =
new CultRecordKey("eve:surface:aetheria.trade.menu");
public static CultRecordKey DaemonCommand(string commandId)
{
return new CultRecordKey($"daemon:commands:{StableToken(commandId)}:gamecult.aetheria.daemon_command.v1");
}
public static CultRecordKey EveCommand(string commandId)
{
return new CultRecordKey($"eve:commands:{StableToken(commandId)}:gamecult.eve.command.v1");
}
public static CultRecordKey EveReceiptForCommand(string commandId)
{
if (string.IsNullOrWhiteSpace(commandId))
throw new ArgumentException("Eve receipt command id must be non-empty.", nameof(commandId));
return new CultRecordKey($"{EveReceiptRecordPrefix}:{commandId}");
}
public static string StableToken(string value)
{
if (string.IsNullOrWhiteSpace(value))
return "empty";
var chars = value
.Select(character => char.IsLetterOrDigit(character) ? character : '-')
.ToArray();
var token = new string(chars).Trim('-').ToLowerInvariant();
while (token.Contains("--", StringComparison.Ordinal))
token = token.Replace("--", "-", StringComparison.Ordinal);
return string.IsNullOrWhiteSpace(token) ? "empty" : token;
}
}
public static class AetheriaRuntimeVerseContractRegistry
{
private static readonly Type[] RuntimeDocumentTypes =
{
typeof(AetheriaRuntimeDaemonProviderAdvertisementDocument),
typeof(AetheriaRuntimeDaemonHealthDocument),
typeof(AetheriaRuntimeDaemonCommandBoundaryDocument),
typeof(AetheriaRuntimeVerseAuthorityPolicyDocument),
typeof(AetheriaRuntimeDaemonFrameDocument),
typeof(AetheriaRuntimeDaemonSoaViewDocument),
typeof(AetheriaRuntimeCatalogSnapshot),
typeof(AetheriaRuntimeLoadoutTemplatesDocument),
typeof(AetheriaRuntimeAssetManifestDocument),
typeof(AetheriaRuntimeObjectsViewportDocument),
typeof(AetheriaRuntimeGravityViewportDocument),
typeof(AetheriaRuntimeCurrentZoneDocument),
typeof(AetheriaRuntimeCurrentEntityDocument),
typeof(AetheriaRuntimeCurrentDockingDocument),
typeof(AetheriaRuntimeZoneContactsDocument),
typeof(AetheriaRuntimeStationRefitDocument),
typeof(AetheriaRuntimeSectorMapDocument),
typeof(AetheriaRuntimeZoneDetailsDocument),
typeof(AetheriaRuntimeZoneRenderDocument),
typeof(AetheriaRuntimeSelectedObjectDocument),
typeof(AetheriaRuntimeInventoryDocument),
typeof(AetheriaRuntimeStarbridgeScenarioDocument),
typeof(AetheriaRuntimeStarbridgeSessionDocument),
typeof(AetheriaRuntimeStarbridgeSessionSummaryDocument),
typeof(AetheriaRuntimeStarbridgePlayerSeatDocument),
typeof(AetheriaRuntimePlayerSettingsDocument),
typeof(AetheriaRuntimeVerseHostSettingsDocument),
typeof(AetheriaRuntimeSurfaceDocument),
typeof(EveProviderAdvertisementDocument),
typeof(EveSurfaceDocument),
typeof(EveSurfaceCommandRequest),
typeof(EveCommandReceiptDocument),
typeof(EveAssetCatalogDocument),
typeof(AetheriaRuntimeDaemonCommandDocument),
typeof(AetheriaRuntimeEveCommandDocument),
typeof(AetheriaRuntimeCommittedCommandFactDocument)
};
public static CultDocumentRegistry CreateCultCacheRegistry()
{
return CultMesh.CreateCultCacheDocumentRegistry(RuntimeDocumentTypes);
}
public static CultNetDocumentRegistry CreateCultNetRegistry(CultDocumentRegistry? cacheRegistry = null)
{
var registry = cacheRegistry ?? CreateCultCacheRegistry();
return CultMesh.CreateCultNetDocumentRegistry(RuntimeDocumentTypes, registry);
}
}
public sealed class AetheriaRuntimeVerseClient : IDisposable
{
public const string DefaultRuntimeId = "aetheria-verse-client";
private const string RemoteShardId = "primary";
private readonly CultMeshNode _node;
private AetheriaClientState? _aetheriaState;
private CultMeshReactiveDocument<AetheriaRuntimeDaemonFrameDocument>? _managedDaemonFrame;
private CultMeshReactiveDocument<AetheriaRuntimeCatalogSnapshot>? _managedCatalog;
private CultMeshReactiveDocument<AetheriaRuntimeLoadoutTemplatesDocument>? _managedLoadoutTemplates;
private CultMeshReactiveDocument<AetheriaRuntimePlayerSettingsDocument>? _managedPlayerSettings;
private CultMeshReactiveDocument<AetheriaRuntimeStarbridgeScenarioDocument>? _managedStarbridgeScenario;
private CultMeshReactiveDocument<AetheriaRuntimeStarbridgeSessionDocument>? _managedStarbridgeSession;
private bool _disposed;
private AetheriaRuntimeVerseClient(string statePath, string runtimeId, CultMeshNode node)
{
StatePath = statePath;
RuntimeId = runtimeId;
_node = node ?? throw new ArgumentNullException(nameof(node));
}
public string StatePath { get; }
public string RuntimeId { get; }
public CultMeshNode Node => _node;
public CultCache Cache => _node.Cache;
public CultNetDatabase Database => _node.Database;
public string RemoteEndpoint { get; private set; } = "";
public bool IsRemoteReplica => !string.IsNullOrWhiteSpace(RemoteEndpoint);
public static async Task<AetheriaRuntimeVerseClient> OpenAsync(
string statePath,
string runtimeId = DefaultRuntimeId,
bool startServer = false,
bool pullOnOpen = true)
{
if (string.IsNullOrWhiteSpace(statePath))
throw new ArgumentException("State path must be non-empty.", nameof(statePath));
var fullPath = Path.GetFullPath(statePath);
var effectiveRuntimeId = string.IsNullOrWhiteSpace(runtimeId) ? DefaultRuntimeId : runtimeId;
var registry = AetheriaRuntimeVerseContractRegistry.CreateCultCacheRegistry();
var node = await CultMesh.CreateNodeAsync(
fullPath,
new CultMeshNodeOptions
{
StartServer = startServer,
EnableDurableShardLogs = true,
CacheOptions = new CultCacheOpenOptions
{
Registry = registry,
PullOnOpen = pullOnOpen,
StoreFlushOnDispose = true,
UseDirectoryStore = true
},
DatabaseOptions = new CultNetDatabaseOptions
{
RuntimeId = effectiveRuntimeId,
DocumentRegistry = AetheriaRuntimeVerseContractRegistry.CreateCultNetRegistry(registry)
}
})
.ConfigureAwait(false);
return new AetheriaRuntimeVerseClient(fullPath, effectiveRuntimeId, node);
}
public static async Task<AetheriaRuntimeVerseClient> OpenRemoteAsync(
string replicaStatePath,
string endpoint,
string runtimeId = DefaultRuntimeId,
bool pullOnOpen = false,
bool synchronizeOnOpen = true)
{
if (string.IsNullOrWhiteSpace(endpoint))
throw new ArgumentException("CultMesh endpoint must be non-empty.", nameof(endpoint));
var fullPath = Path.GetFullPath(replicaStatePath ?? throw new ArgumentNullException(nameof(replicaStatePath)));
var effectiveRuntimeId = string.IsNullOrWhiteSpace(runtimeId) ? DefaultRuntimeId : runtimeId;
var registry = AetheriaRuntimeVerseContractRegistry.CreateCultCacheRegistry();
var shard = new CultNetShardDescriptor(
RemoteShardId,
"aetheria-daemon",
epoch: 1,
isPrimary: false,
primaryEndpoints: new[] { endpoint.Trim() });
var node = await CultMesh.CreateNodeAsync(
fullPath,
new CultMeshNodeOptions
{
StartServer = false,
EnableDurableShardLogs = true,
CacheOptions = new CultCacheOpenOptions
{
Registry = registry,
PullOnOpen = pullOnOpen,
StoreFlushOnDispose = true,
UseDirectoryStore = true
},
DatabaseOptions = new CultNetDatabaseOptions
{
RuntimeId = effectiveRuntimeId,
Shards = new[] { shard },
DocumentRegistry = AetheriaRuntimeVerseContractRegistry.CreateCultNetRegistry(registry)
}
})
.ConfigureAwait(false);
var client = new AetheriaRuntimeVerseClient(fullPath, effectiveRuntimeId, node)
{
RemoteEndpoint = endpoint.Trim()
};
if (synchronizeOnOpen)
await client.RefreshRemoteAsync().ConfigureAwait(false);
return client;
}
public async Task<int> RefreshRemoteAsync(
IReadOnlyList<string>? recordKeys = null,
TimeSpan? connectTimeout = null,
TimeSpan? responseTimeout = null)
{
ThrowIfDisposed();
if (!IsRemoteReplica)
throw new InvalidOperationException("Remote refresh requires a client opened with OpenRemoteAsync.");
var snapshot = CultMesh.SnapshotEndpoint(
RemoteEndpoint,
new CultMeshSnapshotEndpointOptions
{
Context = CultMesh.Verse("aetheria.remote", RuntimeId).Context,
DocumentRegistry = AetheriaRuntimeVerseContractRegistry.CreateCultNetRegistry(),
Request = new CultMeshSnapshotRequestOptions
{
RecordKeys = recordKeys,
ShardId = RemoteShardId,
ShardEpoch = 1,
ConnectTimeout = connectTimeout ?? TimeSpan.FromSeconds(5),
ResponseTimeout = responseTimeout ?? TimeSpan.FromSeconds(10),
MessageIdPrefix = "aetheria-unity",
RudpRuntimeId = RuntimeId,
RudpMaxFragmentBytes = 1200
}
});
var result = await snapshot.SyncSnapshotAsync(_node).ConfigureAwait(false);
await _node.FlushAsync().ConfigureAwait(false);
return result.AppliedCount;
}
public AetheriaClientState Aetheria()
{
ThrowIfDisposed();
return _aetheriaState ??= CreateAetheriaState();
}
public CultMeshMutableStatePointer<TDocument> MutableDocument<TDocument>(
CultRecordKey key)
where TDocument : class
{
ThrowIfDisposed();
if (string.IsNullOrWhiteSpace(key.Value))
throw new ArgumentException("Value must be non-empty.", nameof(key));
return MutableDocumentPointer<TDocument>(key);
}
public CultMeshDocumentHandle<TDocument> Document<TDocument>(
CultRecordKey key,
string? documentId = null)
where TDocument : class
{
ThrowIfDisposed();
if (string.IsNullOrWhiteSpace(key.Value))
throw new ArgumentException("Value must be non-empty.", nameof(key));
var descriptor = CultDocumentRegistry.Shared.GetRequired<TDocument>();
return CultMesh.Document<TDocument>(
Database,
key,
CultMesh.Verse("aetheria.local", RuntimeId),
documentId,
new[]
{
CultMesh.ProjectionSource(key.ToString(), descriptor.SchemaId, "Aetheria Verse database document")
},
new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria Verse database"));
}
public Observable<CultNetDatabaseChange<TDocument>> WatchRecord<TDocument>(
CultRecordKey key)
where TDocument : class
{
ThrowIfDisposed();
if (string.IsNullOrWhiteSpace(key.Value))
throw new ArgumentException("Value must be non-empty.", nameof(key));
return Database.WatchRecord<TDocument>(key);
}
internal async Task<AetheriaRuntimeDaemonCommandEnvelope> SubmitDaemonCommandAsync(
AetheriaRuntimeDaemonCommandDocument command,
bool flush = true)
{
ThrowIfDisposed();
if (command == null) throw new ArgumentNullException(nameof(command));
command.Schema = AetheriaRuntimeDaemonSchemas.Command;
if (string.IsNullOrWhiteSpace(command.CommandId))
command.CommandId = Guid.NewGuid().ToString("N");
if (string.IsNullOrWhiteSpace(command.ClientId))
command.ClientId = RuntimeId;
if (string.IsNullOrWhiteSpace(command.IssuedAtUtc))
command.IssuedAtUtc = DateTime.UtcNow.ToString("O");
await Database.PutAsync(AetheriaRuntimeVerseRecordKeys.DaemonCommand(command.CommandId), command)
.ConfigureAwait(false);
if (flush)
await FlushAsync().ConfigureAwait(false);
return AetheriaRuntimeDaemonOperationClient.ToEnvelope(command);
}
internal async Task<AetheriaRuntimeEveCommandEnvelope> SubmitEveCommandAsync(
AetheriaRuntimeEveCommandDocument command,
bool flush = true)
{
ThrowIfDisposed();
if (command == null) throw new ArgumentNullException(nameof(command));
AetheriaRuntimeEveCommandClient.NormalizeDocument(command);
command.Schema = AetheriaRuntimeEveCommandDocument.SchemaId;
if (string.IsNullOrWhiteSpace(command.CommandId))
command.CommandId = Guid.NewGuid().ToString("N");
if (string.IsNullOrWhiteSpace(command.IssuedAtUtc))
command.IssuedAtUtc = DateTime.UtcNow.ToString("O");
await Database.PutAsync(AetheriaRuntimeVerseRecordKeys.EveCommand(command.CommandId), command)
.ConfigureAwait(false);
if (flush)
await FlushAsync().ConfigureAwait(false);
return AetheriaRuntimeEveCommandClient.ToEnvelope(command);
}
internal Task<AetheriaRuntimeEveCommandEnvelope> SubmitInputSettingsCommandAsync(
AetheriaRuntimeEveCommandKind command,
AetheriaRuntimeInputSettingsCommandBody body,
string clientId,
bool flush = true)
{
ThrowIfDisposed();
return SubmitEveCommandAsync(
AetheriaRuntimeEveCommandClient.ToDocument(
AetheriaRuntimeEveCommandClient.CreateInputSettingsCommand(command, body, clientId)),
flush);
}
internal Task<AetheriaRuntimeEveCommandEnvelope> SubmitLoadoutTemplateCommandAsync(
AetheriaRuntimeLoadoutTemplateCommit loadoutTemplate,
string clientId,
bool flush = true)
{
ThrowIfDisposed();
return SubmitEveCommandAsync(
AetheriaRuntimeEveCommandClient.ToDocument(
AetheriaRuntimeEveCommandClient.CreateLoadoutTemplateCommand(loadoutTemplate, clientId)),
flush);
}
internal Task<AetheriaRuntimeEveCommandEnvelope> SubmitKnownSurfaceCommandAsync(
EveUiCommandRequest request,
string clientId,
bool flush = true)
{
ThrowIfDisposed();
if (!AetheriaRuntimeEveCommandClient.TryCreateKnownSurfaceCommand(request, out var envelope))
{
throw new InvalidOperationException(
$"Unknown Aetheria Eve surface command: {request?.ProviderId}/{request?.SurfaceId}/{request?.Command}");
}
var document = AetheriaRuntimeEveCommandClient.ToDocument(envelope!);
if (!string.IsNullOrWhiteSpace(clientId))
document.ClientId = clientId;
return SubmitEveCommandAsync(document, flush);
}
public Task FlushAsync(bool soft = false)
{
ThrowIfDisposed();
return _node.FlushAsync(soft);
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_aetheriaState?.Dispose();
_aetheriaState = null;
_managedDaemonFrame?.Dispose();
_managedCatalog?.Dispose();
_managedLoadoutTemplates?.Dispose();
_managedPlayerSettings?.Dispose();
_managedStarbridgeScenario?.Dispose();
_managedStarbridgeSession?.Dispose();
_node.Dispose();
}
private void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(AetheriaRuntimeVerseClient));
}
private AetheriaClientState CreateAetheriaState()
{
var frameChanges = WatchRecord<AetheriaRuntimeDaemonFrameDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
.Where(change => change.Document != null)
.Select(change => change.Document!);
var catalogDocument = BootstrapCatalogDocument(
"aetheria.catalog.runtime",
() => Task.FromResult(BootstrapRuntimeCatalogSnapshot()),
AetheriaRuntimeCatalogSnapshot.SchemaId,
CatalogBootstrapSource("catalog:aetheria.runtime"));
var loadoutTemplatesDocument = BootstrapCatalogDocument(
"aetheria.catalog.loadout_templates",
() => Task.FromResult(BootstrapLoadoutTemplatesDocument()),
AetheriaRuntimeLoadoutTemplatesDocument.SchemaId,
CatalogBootstrapSource("catalog:aetheria.loadout_templates"));
var playerSettingsDocument = BootstrapCatalogDocument(
"aetheria.settings.player",
() => Task.FromResult(BootstrapPlayerSettingsDocument()),
AetheriaRuntimePlayerSettingsDocument.SchemaId,
CatalogBootstrapSource("catalog:aetheria.player_settings"));
var verseHostSettingsDocument = BootstrapCatalogDocument(
"aetheria.settings.verse_host",
() => Task.FromResult(BootstrapVerseHostSettingsDocument()),
AetheriaRuntimeVerseHostSettingsDocument.SchemaId,
CatalogBootstrapSource("catalog:aetheria.verse_host_settings"));
var starbridgeScenarioDocument = Document<AetheriaRuntimeStarbridgeScenarioDocument>(
AetheriaRuntimeVerseRecordKeys.StarbridgeScenarioLatest);
var starbridgeSessionDocument = Document<AetheriaRuntimeStarbridgeSessionDocument>(
AetheriaRuntimeVerseRecordKeys.StarbridgeSessionLatest);
var latestFrameDocument = Document<AetheriaRuntimeDaemonFrameDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest);
CultMeshReactiveDocument<AetheriaRuntimeDaemonFrameDocument>? managedDaemonFrame = null;
CultMeshReactiveDocument<AetheriaRuntimeCatalogSnapshot>? managedCatalog = null;
CultMeshReactiveDocument<AetheriaRuntimeLoadoutTemplatesDocument>? managedLoadoutTemplates = null;
CultMeshReactiveDocument<AetheriaRuntimePlayerSettingsDocument>? managedPlayerSettings = null;
CultMeshReactiveDocument<AetheriaRuntimeStarbridgeScenarioDocument>? managedStarbridgeScenario = null;
CultMeshReactiveDocument<AetheriaRuntimeStarbridgeSessionDocument>? managedStarbridgeSession = null;
var state = new AetheriaClientState(
Document<AetheriaRuntimeDaemonProviderAdvertisementDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonProviderAdvertisement),
Document<AetheriaRuntimeDaemonHealthDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonHealth),
Document<AetheriaRuntimeDaemonCommandBoundaryDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonCommandBoundary),
Document<AetheriaRuntimeAssetManifestDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonAssetManifest),
Document<AetheriaRuntimeVerseAuthorityPolicyDocument>(
AetheriaRuntimeVerseRecordKeys.VerseAuthorityPolicy),
latestFrameDocument,
Document<AetheriaRuntimeDaemonSoaViewDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonSoaViewLatest),
Document<EveSurfaceDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonGameSurface),
Document<EveSurfaceDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonGameTuiSurface),
Document<EveSurfaceDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonEditorSurface),
Document<EveSurfaceDocument>(
AetheriaRuntimeVerseRecordKeys.DaemonEditorTuiSurface),
catalogDocument,
loadoutTemplatesDocument,
playerSettingsDocument,
verseHostSettingsDocument,
ManagedFrameDocument(
"aetheria.current.zone",
frame => Task.FromResult(AetheriaRuntimeGameDocuments.CurrentZone(frame)),
AetheriaRuntimeDaemonSchemas.CurrentZone),
ManagedFrameDocument(
"aetheria.current.entity",
frame => Task.FromResult(AetheriaRuntimeGameDocuments.CurrentEntity(frame)),
AetheriaRuntimeDaemonSchemas.CurrentEntity),
ManagedFrameDocument(
"aetheria.current.docking",
frame => Task.FromResult(AetheriaRuntimeGameDocuments.CurrentDocking(frame)),
AetheriaRuntimeDaemonSchemas.CurrentDocking),
ManagedFrameDocument(
"aetheria.zone.contacts",
frame => Task.FromResult(AetheriaRuntimeGameDocuments.ZoneContacts(frame)),
AetheriaRuntimeDaemonSchemas.ZoneContacts),
ManagedFrameDocument(
"aetheria.station.refit",
StationRefitAsync,
AetheriaRuntimeDaemonSchemas.StationRefit,
CultMesh.ProjectionSource("catalog:aetheria.runtime"),
CultMesh.ProjectionSource("loadout-templates:aetheria.runtime")),
ManagedFrameDocument(
"aetheria.sector.map",
frame => Task.FromResult(AetheriaRuntimeGameDocuments.SectorMap(frame)),
AetheriaRuntimeDaemonSchemas.SectorMap),
ManagedFrameDocument(
"aetheria.zone.render",
frame => Task.FromResult(AetheriaRuntimeGameDocuments.ZoneRender(frame)),
AetheriaRuntimeDaemonSchemas.ZoneRender),
viewport => ManagedFrameDocument(
ViewportDocumentId("aetheria.viewport.map", viewport),
frame => Task.FromResult(AetheriaRuntimeGameDocuments.Viewport(frame, viewport)),
AetheriaRuntimeDaemonSchemas.GameViewport),
viewport => ManagedFrameDocument(
ViewportDocumentId("aetheria.viewport.objects", viewport),
frame => Task.FromResult(AetheriaRuntimeGameDocuments.ObjectsViewport(frame, viewport)),
AetheriaRuntimeDaemonSchemas.ObjectsViewport),
viewport => ManagedFrameDocument(
ViewportDocumentId("aetheria.viewport.gravity", viewport),
frame => Task.FromResult(AetheriaRuntimeGameDocuments.GravityViewport(frame, viewport)),
AetheriaRuntimeDaemonSchemas.GravityViewport),
viewport => ManagedFrameDocument(
ViewportDocumentId("aetheria.viewport.render_splats", viewport),
frame => Task.FromResult(AetheriaRuntimeGameDocuments.RenderSplatsViewport(frame, viewport)),
AetheriaRuntimeDaemonSchemas.RenderSplatsViewport),
zoneIndex => ManagedFrameDocument(
IndexedDocumentId("aetheria.zone.details", zoneIndex),
frame => Task.FromResult(AetheriaRuntimeGameDocuments.ZoneDetails(frame, zoneIndex)),
AetheriaRuntimeDaemonSchemas.ZoneDetails),
zoneIndex => ManagedFrameDocument(
IndexedDocumentId("aetheria.zone.details.surface", zoneIndex),
frame => ZoneDetailsSurfaceAsync(frame, zoneIndex),
AetheriaRuntimeZoneDetailsSurfaceBuilder.SurfaceId,
CultMesh.ProjectionSource("catalog:aetheria.runtime"),
CultMesh.ProjectionSource("catalog:aetheria.player_settings")),
request => ManagedFrameDocument(
InventoryPanelSurfaceDocumentId(request),
frame => InventoryPanelSurfaceAsync(frame, request),
AetheriaRuntimeInventoryPanelSurfaceBuilder.SurfaceId,
CultMesh.ProjectionSource("catalog:aetheria.runtime"),
CultMesh.ProjectionSource("catalog:aetheria.player_settings"),
CultMesh.ProjectionSource("loadout-templates:aetheria.runtime")),
request => ManagedFrameDocument(
InventoryDropdownSurfaceDocumentId(request),
frame => InventoryDropdownSurfaceAsync(frame, request),
AetheriaRuntimeInventoryDropdownSurfaceBuilder.SurfaceId,
CultMesh.ProjectionSource("catalog:aetheria.runtime"),
CultMesh.ProjectionSource("loadout-templates:aetheria.runtime")),
(surfaceId, canOpenRuntimeInputScreen, inGame) => ManagedMainMenuSurfaceDocument(
surfaceId,
canOpenRuntimeInputScreen,
inGame),
entityIndex => ManagedFrameDocument(
IndexedDocumentId("aetheria.object.selected", entityIndex),
frame => Task.FromResult(AetheriaRuntimeGameDocuments.SelectedObject(frame, entityIndex)),
AetheriaRuntimeDaemonSchemas.SelectedObject),
entityIndex => ManagedFrameDocument(
IndexedDocumentId("aetheria.inventory", entityIndex),
frame => Task.FromResult(AetheriaRuntimeGameDocuments.Inventory(frame, entityIndex)),
AetheriaRuntimeDaemonSchemas.Inventory),
starbridgeScenarioDocument,
starbridgeSessionDocument,
ManagedFrameDocument(
"aetheria.starbridge.summary",
StarbridgeSummaryAsync,
AetheriaRuntimeDaemonSchemas.StarbridgeSessionSummary,
CultMesh.ProjectionSource(AetheriaRuntimeVerseRecordKeys.StarbridgeScenarioLatest.ToString()),
CultMesh.ProjectionSource(AetheriaRuntimeVerseRecordKeys.StarbridgeSessionLatest.ToString()),
CultMesh.ProjectionSource("catalog:aetheria.runtime")),
seatId => Document<AetheriaRuntimeStarbridgePlayerSeatDocument>(
AetheriaRuntimeVerseRecordKeys.StarbridgePlayerSeat(seatId)));
managedPlayerSettings = state.PlayerSettings.Reactive();
_managedPlayerSettings = managedPlayerSettings;
return state;
CultMeshDocumentHandle<TDocument> ManagedFrameDocument<TDocument>(
string documentId,
Func<AetheriaRuntimeDaemonFrameDocument, Task<TDocument>> derive,
string schemaId,
params CultMeshProjectionSource[] additionalSources)
where TDocument : class
{
var sources = new[]
{
CultMesh.ProjectionSource(
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString(),
AetheriaRuntimeDaemonSchemas.Frame,
"latest authoritative daemon frame")
}
.Concat(additionalSources ?? Array.Empty<CultMeshProjectionSource>())
.Append(CultMesh.ProjectionSource(documentId, schemaId, "managed Aetheria client document"))
.ToArray();
var verse = CultMesh.Verse("aetheria.local", RuntimeId);
return CultMesh.Document(
documentId,
verse,
async _ => await derive(RequireManagedFrame()).ConfigureAwait(false),
_ => frameChanges
.SelectAwait(async (frame, cancellationToken) =>
await derive(frame).ConfigureAwait(false)),
sources: sources,
routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria managed typed state"));
}
AetheriaRuntimeDaemonFrameDocument RequireManagedFrame()
{
managedDaemonFrame ??= latestFrameDocument.Reactive();
_managedDaemonFrame = managedDaemonFrame;
return managedDaemonFrame?.Current
?? throw new InvalidOperationException("Aetheria Verse client has no daemon frame yet.");
}
AetheriaRuntimeCatalogSnapshot RequireManagedCatalog()
{
managedCatalog ??= catalogDocument.Reactive();
_managedCatalog = managedCatalog;
return managedCatalog?.Current
?? throw new InvalidOperationException("Aetheria Verse client has no runtime catalog document yet.");
}
AetheriaRuntimeLoadoutTemplatesDocument RequireManagedLoadoutTemplates()
{
managedLoadoutTemplates ??= loadoutTemplatesDocument.Reactive();
_managedLoadoutTemplates = managedLoadoutTemplates;
return managedLoadoutTemplates?.Current
?? throw new InvalidOperationException("Aetheria Verse client has no loadout templates document yet.");
}
AetheriaRuntimePlayerSettingsDocument RequireManagedPlayerSettings()
{
return managedPlayerSettings?.Current
?? throw new InvalidOperationException("Aetheria Verse client has no player settings document yet.");
}
CultMeshDocumentHandle<TDocument> BootstrapCatalogDocument<TDocument>(
string documentId,
Func<Task<TDocument>> bootstrap,
string schemaId,
params CultMeshProjectionSource[] additionalSources)
where TDocument : class
{
var sources = (additionalSources ?? Array.Empty<CultMeshProjectionSource>())
.Append(CultMesh.ProjectionSource(documentId, schemaId, "managed Aetheria catalog document"))
.ToArray();
var verse = CultMesh.Verse("aetheria.local", RuntimeId);
return CultMesh.Document(
documentId,
verse,
async _ => await bootstrap().ConfigureAwait(false),
_ => frameChanges
.SelectAwait(async (_, cancellationToken) =>
await bootstrap().ConfigureAwait(false)),
sources: sources,
routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria managed catalog state"));
}
CultMeshDocumentHandle<AetheriaRuntimeSurfaceDocument> ManagedMainMenuSurfaceDocument(
string surfaceId,
bool canOpenRuntimeInputScreen,
bool inGame)
{
surfaceId = string.IsNullOrWhiteSpace(surfaceId)
? AetheriaRuntimeMainMenuCommands.RootSurfaceId
: surfaceId;
var documentId = string.Join(
".",
"aetheria.main_menu.surface",
AetheriaRuntimeVerseRecordKeys.StableToken(surfaceId),
canOpenRuntimeInputScreen ? "input-open" : "input-closed",
inGame ? "in-game" : "title");
var descriptor = CultDocumentRegistry.Shared.GetRequired<AetheriaRuntimeSurfaceDocument>();
var sources = new[]
{
CultMesh.ProjectionSource(documentId, descriptor.SchemaId, "managed Aetheria main menu surface"),
CultMesh.ProjectionSource("catalog:aetheria.player_settings"),
CultMesh.ProjectionSource("catalog:aetheria.verse_host_settings"),
CultMesh.ProjectionSource(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString())
};
var verse = CultMesh.Verse("aetheria.local", RuntimeId);
return CultMesh.Document(
documentId,
verse,
_ => Task.FromResult(BuildMainMenuSurface(surfaceId, canOpenRuntimeInputScreen, inGame)),
_ => frameChanges
.Select(_ => BuildMainMenuSurface(surfaceId, canOpenRuntimeInputScreen, inGame)),
sources: sources,
routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria managed main menu surface"));
}
Task<AetheriaRuntimeStationRefitDocument> StationRefitAsync(
AetheriaRuntimeDaemonFrameDocument frame)
{
return Task.FromResult(AetheriaRuntimeGameDocuments.StationRefit(
frame,
RequireManagedLoadoutTemplates().Templates,
RequireManagedCatalog()));
}
Task<AetheriaRuntimeStarbridgeSessionSummaryDocument> StarbridgeSummaryAsync(
AetheriaRuntimeDaemonFrameDocument frame)
{
return Task.FromResult(AetheriaRuntimeStarbridgeDocuments.SessionSummary(
frame,
CurrentStarbridgeScenario(),
CurrentStarbridgeSession(),
RequireManagedCatalog()));
}
AetheriaRuntimeStarbridgeScenarioDocument? CurrentStarbridgeScenario()
{
try
{
managedStarbridgeScenario ??= starbridgeScenarioDocument.Reactive();
_managedStarbridgeScenario = managedStarbridgeScenario;
return managedStarbridgeScenario.Current;
}
catch (KeyNotFoundException)
{
return null;
}
catch (MessagePack.FormatterNotRegisteredException)
{
return null;
}
}
AetheriaRuntimeStarbridgeSessionDocument? CurrentStarbridgeSession()
{
try
{
managedStarbridgeSession ??= starbridgeSessionDocument.Reactive();
_managedStarbridgeSession = managedStarbridgeSession;
return managedStarbridgeSession.Current;
}
catch (KeyNotFoundException)
{
return null;
}
catch (MessagePack.FormatterNotRegisteredException)
{
return null;
}
}
Task<AetheriaRuntimeSurfaceDocument> ZoneDetailsSurfaceAsync(
AetheriaRuntimeDaemonFrameDocument frame,
int zoneIndex)
{
return Task.FromResult(AetheriaRuntimeZoneDetailsSurfaceBuilder.BuildFromDocuments(
AetheriaRuntimeGameDocuments.ZoneDetails(frame, zoneIndex),
AetheriaRuntimeGameDocuments.SectorMap(frame),
RequireManagedCatalog(),
RequireManagedPlayerSettings(),
DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)));
}
Task<AetheriaRuntimeSurfaceDocument> InventoryPanelSurfaceAsync(
AetheriaRuntimeDaemonFrameDocument frame,
AetheriaRuntimeInventoryPanelSurfaceRequest request)
{
request ??= new AetheriaRuntimeInventoryPanelSurfaceRequest();
var entityIndex = request.DisplayedEntityIndex >= 0
? request.DisplayedEntityIndex
: request.DisplayedCargoEntityIndex;
var inventory = entityIndex < 0
? new AetheriaRuntimeInventoryDocument()
: AetheriaRuntimeGameDocuments.Inventory(frame, entityIndex);
return Task.FromResult(AetheriaRuntimeInventoryPanelSurfaceBuilder.BuildFromDocuments(
AetheriaRuntimeGameDocuments.CurrentEntity(frame),
AetheriaRuntimeGameDocuments.StationRefit(
frame,
RequireManagedLoadoutTemplates().Templates,
RequireManagedCatalog()),
inventory,
RequireManagedCatalog(),
RequireManagedPlayerSettings(),
request,
InventoryDropdownSurfaceDocumentId(ToDropdownRequest(request)),
DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)));
}
Task<AetheriaRuntimeSurfaceDocument> InventoryDropdownSurfaceAsync(
AetheriaRuntimeDaemonFrameDocument frame,
AetheriaRuntimeInventoryDropdownSurfaceRequest request)
{
return Task.FromResult(AetheriaRuntimeInventoryDropdownSurfaceBuilder.BuildFromDocuments(
AetheriaRuntimeGameDocuments.StationRefit(
frame,
RequireManagedLoadoutTemplates().Templates,
RequireManagedCatalog()),
request,
DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)));
}
AetheriaRuntimeSurfaceDocument BuildMainMenuSurface(
string surfaceId,
bool canOpenRuntimeInputScreen,
bool inGame)
{
var updatedAtUtc = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture);
var stateBoot = AetheriaRuntimeStateBoot.Inspect(
new DirectoryInfo(Path.GetDirectoryName(StatePath) ?? "."),
StatePath);
if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.RootSurfaceId, StringComparison.Ordinal))
{
return AetheriaRuntimeMainMenuSurfaceBuilder.BuildRoot(
stateBoot,
TryCurrentDaemonFrame(),
TryCurrentVerseHostSettings(),
TryCurrentPlayerSettings(),
canOpenRuntimeInputScreen,
inGame,
updatedAtUtc);
}
if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.SettingsSurfaceId, StringComparison.Ordinal))
return AetheriaRuntimeMainMenuSurfaceBuilder.BuildSettings(updatedAtUtc);
if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.InputSettingsSurfaceId, StringComparison.Ordinal))
{
return AetheriaRuntimeMainMenuSurfaceBuilder.BuildInputSettings(
stateBoot,
TryCurrentPlayerSettings(),
canOpenRuntimeInputScreen,
inGame,
updatedAtUtc);
}
if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.PlayerSettingsSurfaceId, StringComparison.Ordinal))
{
return AetheriaRuntimeMainMenuSurfaceBuilder.BuildPlayerSettings(
TryCurrentPlayerSettings(),
updatedAtUtc);
}
if (string.Equals(surfaceId, AetheriaRuntimeMainMenuCommands.VerseSettingsSurfaceId, StringComparison.Ordinal))
{
return AetheriaRuntimeMainMenuSurfaceBuilder.BuildVerseSettings(
AetheriaRuntimeClientTargetSurfaceBuilder.Build(
stateBoot,
TryCurrentVerseHostSettings(),
updatedAtUtc));
}
throw new ArgumentException($"Unknown Aetheria main menu surface id '{surfaceId}'.", nameof(surfaceId));
}
AetheriaRuntimePlayerSettingsDocument? TryCurrentPlayerSettings()
{
try
{
managedPlayerSettings ??= playerSettingsDocument.Reactive();
_managedPlayerSettings = managedPlayerSettings;
return managedPlayerSettings.Current;
}
catch (Exception)
{
return null;
}
}
AetheriaRuntimeVerseHostSettingsDocument? TryCurrentVerseHostSettings()
{
try
{
return verseHostSettingsDocument.Latest();
}
catch (Exception)
{
return null;
}
}
AetheriaRuntimeDaemonFrameDocument? TryCurrentDaemonFrame()
{
try
{
return latestFrameDocument.Latest();
}
catch (Exception)
{
return null;
}
}
static string IndexedDocumentId(string prefix, int index)
{
return $"{prefix}.{index.ToString(CultureInfo.InvariantCulture)}";
}
static string ViewportDocumentId(string prefix, AetheriaRuntimeViewportBounds viewport)
{
var normalized = AetheriaRuntimeGameDocuments.Normalize(viewport);
return string.Join(
".",
prefix,
ViewportToken(normalized.MinX),
ViewportToken(normalized.MinY),
ViewportToken(normalized.MaxX),
ViewportToken(normalized.MaxY));
}
static string InventoryPanelSurfaceDocumentId(AetheriaRuntimeInventoryPanelSurfaceRequest? request)
{
request ??= new AetheriaRuntimeInventoryPanelSurfaceRequest();
return string.Join(
".",
"aetheria.inventory.panel.surface",
AetheriaRuntimeVerseRecordKeys.StableToken(request.DisplayedEntityKey),
request.DisplayedEntityIndex.ToString(CultureInfo.InvariantCulture),
AetheriaRuntimeVerseRecordKeys.StableToken(request.DisplayedCargoEntityKey),
request.DisplayedCargoEntityIndex.ToString(CultureInfo.InvariantCulture),
request.DisplayedCargoIndex.ToString(CultureInfo.InvariantCulture),
request.ThermalView ? "thermal" : "inventory");
}
static string InventoryDropdownSurfaceDocumentId(AetheriaRuntimeInventoryDropdownSurfaceRequest? request)
{
request ??= new AetheriaRuntimeInventoryDropdownSurfaceRequest();
return string.Join(
".",
"aetheria.inventory.dropdown.surface",
AetheriaRuntimeVerseRecordKeys.StableToken(request.DisplayedEntityKey),
AetheriaRuntimeVerseRecordKeys.StableToken(request.DisplayedCargoEntityKey),
request.DisplayedCargoIndex.ToString(CultureInfo.InvariantCulture),
request.CanSaveLoadout ? "save" : "readonly");
}
static AetheriaRuntimeInventoryDropdownSurfaceRequest ToDropdownRequest(
AetheriaRuntimeInventoryPanelSurfaceRequest? request)
{
request ??= new AetheriaRuntimeInventoryPanelSurfaceRequest();
return new AetheriaRuntimeInventoryDropdownSurfaceRequest
{
CurrentView = request.ViewTitle ?? "",
DisplayedEntityKey = request.DisplayedEntityKey ?? "",
DisplayedCargoEntityKey = request.DisplayedCargoEntityKey ?? "",
DisplayedCargoIndex = request.DisplayedCargoIndex,
CanSaveLoadout = !string.IsNullOrWhiteSpace(request.DisplayedEntityKey)
};
}
static string ViewportToken(double value)
{
return value
.ToString("0.###", CultureInfo.InvariantCulture)
.Replace('-', 'n')
.Replace('.', 'p');
}
}
private CultMeshMutableStatePointer<T> MutableDocumentPointer<T>(CultRecordKey key) where T : class
{
return CultMesh.MutableStatePointer(
key.ToString(),
_ => Database.GetAsync<T>(key),
_ => Database.WatchRecord<T>(key)
.Where(change => change.Document != null)
.Select(change => change.Document!),
async (_, value) => { await Database.PutAsync(key, value).ConfigureAwait(false); },
sources: new[]
{
CultMesh.ProjectionSource(key.ToString())
});
}
private static CultMeshProjectionSource CatalogBootstrapSource(string sourceId)
{
return CultMesh.ProjectionSource(
sourceId,
description: "managed Aetheria document bootstrap seed");
}
private AetheriaRuntimeCatalogSnapshot BootstrapRuntimeCatalogSnapshot()
{
return AetheriaRuntimeCatalogStore.OpenReadOnly(StatePath);
}
public IReadOnlyList<AetheriaRuntimeCommittedCommandFactDocument> CommittedCommandFacts()
{
ThrowIfDisposed();
return Cache.GetAll<AetheriaRuntimeCommittedCommandFactDocument>().ToArray();
}
private AetheriaRuntimeLoadoutTemplatesDocument BootstrapLoadoutTemplatesDocument()
{
return new AetheriaRuntimeLoadoutTemplatesDocument(
AetheriaRuntimeCatalogStore.ReadLoadoutTemplates(StatePath));
}
private AetheriaRuntimePlayerSettingsDocument BootstrapPlayerSettingsDocument()
{
return AetheriaRuntimePlayerSettingsDocument.FromSnapshot(
AetheriaRuntimeCatalogStore.ReadPlayerSettings(StatePath));
}
private AetheriaRuntimeVerseHostSettingsDocument BootstrapVerseHostSettingsDocument()
{
return AetheriaRuntimeVerseHostSettingsDocument.FromSnapshot(
AetheriaRuntimeCatalogStore.ReadVerseHostSettings(StatePath));
}
}
}