using System; using System.IO; using System.Threading.Tasks; using Aetheria.State.Documents; using GameCult.Aetheria.State.Unity; using GameCult.Caching; using GameCult.Caching.MessagePack; using GameCult.Mesh; using GameCult.Networking; namespace Aetheria.State; public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable { private readonly CultMeshNode _node; private AetheriaStateNode(string statePath, CultMeshNode node) { StatePath = statePath; _node = node; } public string StatePath { get; } public CultMeshNode MeshNode => _node; public CultCache Cache => _node.Cache; public CultNetDatabase Database => _node.Database; public static async Task OpenAsync( string statePath, string runtimeId = "aetheria-local", bool startServer = false) { if (string.IsNullOrWhiteSpace(statePath)) { throw new ArgumentException("State path must be non-empty.", nameof(statePath)); } Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(statePath)) ?? "."); var cacheRegistry = AetheriaDocumentRegistry.CreateCultCacheRegistry(); var node = await CultMesh.CreateNodeAsync( statePath, new CultMeshNodeOptions { StartServer = startServer, EnableDurableShardLogs = true, CacheOptions = new CultCacheOpenOptions { Registry = cacheRegistry, PullOnOpen = true, StoreFlushOnDispose = true, UseDirectoryStore = true }, DatabaseOptions = new CultNetDatabaseOptions { RuntimeId = runtimeId, DocumentRegistry = AetheriaDocumentRegistry.CreateCultNetRegistry(cacheRegistry) } }).ConfigureAwait(false); return new AetheriaStateNode(statePath, node); } public Task> PutWorldAsync(AetheriaWorldState world) { return Database.PutAsync(new CultRecordKey("global:aetheria.world_state.v1"), world); } public Task GetWorldAsync() { return Database.GetAsync(new CultRecordKey("global:aetheria.world_state.v1")); } public Task> PutMigrationLedgerAsync(AetheriaMigrationLedger ledger) { return Database.PutAsync(new CultRecordKey("global:aetheria.migration_ledger.v1"), ledger); } public Task GetMigrationLedgerAsync() { return Database.GetAsync(new CultRecordKey("global:aetheria.migration_ledger.v1")); } public Task> PutLegacyCatalogQuarantineAsync( AetheriaLegacyCatalogQuarantine quarantine) { return Database.PutAsync(new CultRecordKey("global:aetheria.legacy_catalog_quarantine.v1"), quarantine); } public Task GetLegacyCatalogQuarantineAsync() { return Database.GetAsync( new CultRecordKey("global:aetheria.legacy_catalog_quarantine.v1")); } public Task> PutItemDefinitionAsync( CultRecordKey key, AetheriaItemDefinition item) { return Database.PutAsync(key, item); } public Task> PutLegacyItemDefinitionAsync(AetheriaItemDefinition item) { return PutItemDefinitionAsync(AetheriaCatalogKeys.ItemDefinitionFromLegacyId(item.LegacyId), item); } public Task GetItemDefinitionAsync(CultRecordKey key) { return Database.GetAsync(key); } public Task GetItemDefinitionByLegacyIdAsync(string legacyId) { return GetItemDefinitionAsync(AetheriaCatalogKeys.ItemDefinitionFromLegacyId(legacyId)); } public Task> PutCorporationAsync(CultRecordKey key, AetheriaCorporation corporation) { return Database.PutAsync(key, corporation); } public Task> PutLegacyCorporationAsync(AetheriaCorporation corporation) { return PutCorporationAsync(AetheriaCatalogKeys.CorporationFromLegacyId(corporation.LegacyId), corporation); } public Task GetCorporationAsync(CultRecordKey key) { return Database.GetAsync(key); } public Task GetCorporationByLegacyIdAsync(string legacyId) { return GetCorporationAsync(AetheriaCatalogKeys.CorporationFromLegacyId(legacyId)); } public Task> PutNameFileAsync(CultRecordKey key, AetheriaNameFile nameFile) { return Database.PutAsync(key, nameFile); } public Task> PutLegacyNameFileAsync(AetheriaNameFile nameFile) { return PutNameFileAsync(AetheriaCatalogKeys.NameFileFromLegacyId(nameFile.LegacyId), nameFile); } public Task GetNameFileAsync(CultRecordKey key) { return Database.GetAsync(key); } public Task GetNameFileByLegacyIdAsync(string legacyId) { return GetNameFileAsync(AetheriaCatalogKeys.NameFileFromLegacyId(legacyId)); } public AetheriaCatalogSnapshot ReadCatalogSnapshot() { return new AetheriaCatalogSnapshot( Cache.GetAll(), Cache.GetAll(), Cache.GetAll()); } public Task> PutCatalogSurfaceAsync(EveSurfaceState surface) { return Database.PutAsync(new CultRecordKey(AetheriaCatalogSurfaceProjector.SurfaceKey), surface); } public Task GetCatalogSurfaceAsync() { return Database.GetAsync(new CultRecordKey(AetheriaCatalogSurfaceProjector.SurfaceKey)); } public Task> PutOperationsSurfaceAsync(EveSurfaceState surface) { return Database.PutAsync(new CultRecordKey(AetheriaOperationsSurfaceProjector.SurfaceKey), surface); } public Task GetOperationsSurfaceAsync() { return Database.GetAsync(new CultRecordKey(AetheriaOperationsSurfaceProjector.SurfaceKey)); } public Task> PutPlayerSettingsSurfaceAsync(EveSurfaceState surface) { return Database.PutAsync(new CultRecordKey(AetheriaPlayerSettingsSurfaceProjector.SurfaceKey), surface); } public Task GetPlayerSettingsSurfaceAsync() { return Database.GetAsync(new CultRecordKey(AetheriaPlayerSettingsSurfaceProjector.SurfaceKey)); } public Task> PutProviderAdvertisementAsync( EveProviderAdvertisementState advertisement) { return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.AdvertisementKey), advertisement); } public Task GetProviderAdvertisementAsync() { return Database.GetAsync( new CultRecordKey(AetheriaProviderAdvertisementProjector.AdvertisementKey)); } public Task> PutDaemonProviderAdvertisementAsync( AetheriaRuntimeDaemonProviderAdvertisementDocument advertisement) { return Database.PutAsync(new CultRecordKey("daemon:aetheria.provider_advertisement.v1"), advertisement); } public Task GetDaemonProviderAdvertisementAsync() { return Database.GetAsync( new CultRecordKey("daemon:aetheria.provider_advertisement.v1")); } public Task> PutDaemonHealthAsync( AetheriaRuntimeDaemonHealthDocument health) { return Database.PutAsync(new CultRecordKey("daemon:aetheria.health.v1"), health); } public Task GetDaemonHealthAsync() { return Database.GetAsync( new CultRecordKey("daemon:aetheria.health.v1")); } public Task> PutDaemonCommandBoundaryAsync( AetheriaRuntimeDaemonCommandBoundaryDocument boundary) { return Database.PutAsync(new CultRecordKey("daemon:aetheria.command_boundary.v1"), boundary); } public Task GetDaemonCommandBoundaryAsync() { return Database.GetAsync( new CultRecordKey("daemon:aetheria.command_boundary.v1")); } public Task> SubmitDaemonCommandAsync( AetheriaRuntimeDaemonCommandDocument command) { 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.IssuedAtUtc)) command.IssuedAtUtc = DateTime.UtcNow.ToString("O"); return Database.PutAsync(DaemonCommandKey(command.CommandId), command); } public IReadOnlyList ReadObservedDaemonCommands() { return Cache .GetAll() .OrderBy(command => command.IssuedAtUtc ?? "", StringComparer.Ordinal) .ThenBy(command => command.CommandId ?? "", StringComparer.Ordinal) .ToArray(); } private static CultRecordKey DaemonCommandKey(string commandId) { return new CultRecordKey($"daemon:commands:{StableToken(commandId)}:gamecult.aetheria.daemon_command.v1"); } public Task> SubmitEveCommandAsync( AetheriaRuntimeEveCommandDocument command) { if (command == null) throw new ArgumentNullException(nameof(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"); return Database.PutAsync(EveCommandKey(command.CommandId), command); } public IReadOnlyList ReadObservedEveCommands() { return Cache .GetAll() .OrderBy(command => command.IssuedAtUtc ?? "", StringComparer.Ordinal) .ThenBy(command => command.CommandId ?? "", StringComparer.Ordinal) .ToArray(); } private static CultRecordKey EveCommandKey(string commandId) { return new CultRecordKey($"eve:commands:{StableToken(commandId)}:gamecult.eve.command.v1"); } public Task> PutDaemonFrameAsync( AetheriaRuntimeDaemonFrameDocument frame) { return Database.PutAsync(new CultRecordKey("daemon:aetheria.frame.latest.v1"), frame); } public Task GetDaemonFrameAsync() { return Database.GetAsync( new CultRecordKey("daemon:aetheria.frame.latest.v1")); } public Task> PutDaemonSoaViewAsync( AetheriaRuntimeDaemonSoaViewDocument view) { return Database.PutAsync(new CultRecordKey("daemon:aetheria.soa_view.latest.v1"), view); } public Task GetDaemonSoaViewAsync() { return Database.GetAsync( new CultRecordKey("daemon:aetheria.soa_view.latest.v1")); } public Task> PutDaemonGameSurfaceAsync(EveSurfaceState surface) { return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameSurfaceKey), surface); } public Task GetDaemonGameSurfaceAsync() { return Database.GetAsync( new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameSurfaceKey)); } public Task> PutDaemonGameTuiSurfaceAsync(EveSurfaceState surface) { return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameTuiSurfaceKey), surface); } public Task GetDaemonGameTuiSurfaceAsync() { return Database.GetAsync( new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonGameTuiSurfaceKey)); } public Task> PutDaemonEditorSurfaceAsync(EveSurfaceState surface) { return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorSurfaceKey), surface); } public Task GetDaemonEditorSurfaceAsync() { return Database.GetAsync( new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorSurfaceKey)); } public Task> PutDaemonEditorTuiSurfaceAsync(EveSurfaceState surface) { return Database.PutAsync(new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorTuiSurfaceKey), surface); } public Task GetDaemonEditorTuiSurfaceAsync() { return Database.GetAsync( new CultRecordKey(AetheriaProviderAdvertisementProjector.DaemonEditorTuiSurfaceKey)); } public Task> PutRuntimeSessionAsync(AetheriaRuntimeSession session) { return Database.PutAsync(new CultRecordKey($"runtime:{session.RuntimeId}:aetheria.runtime_session.v1"), session); } public Task GetRuntimeSessionAsync(string runtimeId) { return Database.GetAsync( new CultRecordKey($"runtime:{runtimeId}:aetheria.runtime_session.v1")); } public Task> PutPlayerSettingsAsync(AetheriaPlayerSettings settings) { return Database.PutAsync(new CultRecordKey("global:aetheria.player_settings.v1"), settings); } public Task GetPlayerSettingsAsync() { return Database.GetAsync(new CultRecordKey("global:aetheria.player_settings.v1")); } public Task> PutLoadoutTemplateAsync( CultRecordKey key, AetheriaLoadoutTemplate loadout) { return Database.PutAsync(key, loadout); } public Task GetLoadoutTemplateAsync(CultRecordKey key) { return Database.GetAsync(key); } public Task> PutRunStateAsync(CultRecordKey key, AetheriaRunState run) { return Database.PutAsync(key, run); } public Task GetRunStateAsync(CultRecordKey key) { return Database.GetAsync(key); } public Task> PutZoneStateAsync(CultRecordKey key, AetheriaZoneState zone) { return Database.PutAsync(key, zone); } public Task GetZoneStateAsync(CultRecordKey key) { return Database.GetAsync(key); } public Task> PutEntitySnapshotAsync( CultRecordKey key, AetheriaEntitySnapshot entity) { return Database.PutAsync(key, entity); } public Task GetEntitySnapshotAsync(CultRecordKey key) { return Database.GetAsync(key); } public Task> PutVerseHostSettingsAsync( AetheriaVerseHostSettings settings) { return Database.PutAsync(new CultRecordKey("global:aetheria.verse_host_settings.v1"), settings); } public Task GetVerseHostSettingsAsync() { return Database.GetAsync( new CultRecordKey("global:aetheria.verse_host_settings.v1")); } public Task> PutEveCommandAcceptanceStatusAsync( AetheriaEveCommandAcceptanceStatus status) { return Database.PutAsync(new CultRecordKey("global:aetheria.eve_command_acceptance_status.v1"), status); } public Task GetEveCommandAcceptanceStatusAsync() { return Database.GetAsync( new CultRecordKey("global:aetheria.eve_command_acceptance_status.v1")); } public Task FlushAsync(bool soft = false) { return _node.FlushAsync(soft); } private 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 void Dispose() { _node.Dispose(); } public async ValueTask DisposeAsync() { await FlushAsync().ConfigureAwait(false); Dispose(); } }