using Aetheria.State.Documents; using GameCult.Aetheria.State.Unity; namespace Aetheria.State; public sealed class AetheriaEveCommandApplyReport { public int AppliedCatalogRefreshes { get; set; } public int AppliedOperationsRefreshes { get; set; } public int AppliedPlayerSettingsCommands { get; set; } public int AppliedVerseHostCommands { get; set; } public int RejectedCommands { get; set; } public string[] AcceptedPaths { get; set; } = []; public string[] RejectedPaths { get; set; } = []; public string LastRejectedCommand { get; set; } = ""; public string LastRejectedReason { get; set; } = ""; } public static class AetheriaEveCommandBridge { public const string CommandSchema = "gamecult.eve.command.v1"; public static string GetPendingDirectory(string stateFilePath) { if (string.IsNullOrWhiteSpace(stateFilePath)) throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath)); return stateFilePath + ".eve.pending"; } public static async Task ApplyPendingAsync( AetheriaStateNode node, bool deleteAccounted = true) { if (node == null) throw new ArgumentNullException(nameof(node)); var pendingDirectory = GetPendingDirectory(node.StatePath); var report = new AetheriaEveCommandApplyReport(); var accepted = new List(); var rejected = new List(); if (!Directory.Exists(pendingDirectory)) return report; foreach (var path in Directory.EnumerateFiles(pendingDirectory, "*.cc").OrderBy(path => path, StringComparer.Ordinal)) { var command = ReadCommand(path); var rejection = Validate(command); if (!string.IsNullOrWhiteSpace(rejection)) { RecordRejection(report, command, rejection, path, rejected, deleteAccounted); continue; } switch (command.Command) { case "aetheria.catalog.refresh": await node.PutCatalogSurfaceAsync( AetheriaCatalogSurfaceProjector.Build(node.ReadCatalogSnapshot(), command.IssuedAtUtc)).ConfigureAwait(false); report.AppliedCatalogRefreshes++; break; case "aetheria.operations.refresh": var commitStatus = await node.GetRuntimeCommitDrainStatusAsync().ConfigureAwait(false) ?? EmptyCommitDrainStatus(node.StatePath, command.IssuedAtUtc); var eveStatus = await node.GetEveCommandDrainStatusAsync().ConfigureAwait(false) ?? EmptyEveCommandDrainStatus(node.StatePath, command.IssuedAtUtc); var verseHostSettings = await node.GetVerseHostSettingsAsync().ConfigureAwait(false); var runtimeSession = string.IsNullOrWhiteSpace(commitStatus.RuntimeId) ? null : await node.GetRuntimeSessionAsync(commitStatus.RuntimeId).ConfigureAwait(false); await node.PutOperationsSurfaceAsync( AetheriaOperationsSurfaceProjector.Build( commitStatus, eveStatus, verseHostSettings, runtimeSession)).ConfigureAwait(false); report.AppliedOperationsRefreshes++; break; case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.Refresh: case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.SetPlayerName: case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleTemperatureUnit: case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.DecrementSignificantDigits: case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IncrementSignificantDigits: case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleNebulaQuality: case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.ToggleShowAsteroidsInMinimap: await ApplyPlayerSettingsCommandAsync(node, command).ConfigureAwait(false); report.AppliedPlayerSettingsCommands++; break; case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.Refresh: case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.CycleVisibility: await ApplyVerseHostCommandAsync(node, command).ConfigureAwait(false); report.AppliedVerseHostCommands++; break; } accepted.Add(path); if (deleteAccounted) File.Delete(path); } await node.FlushAsync().ConfigureAwait(false); report.AcceptedPaths = accepted.ToArray(); report.RejectedPaths = rejected.ToArray(); return report; } private static AetheriaRuntimeEveCommandDocument ReadCommand(string path) { return AetheriaRuntimePendingCultCacheStore.ReadEveCommand(path); } private static string Validate(AetheriaRuntimeEveCommandDocument command) { if (!string.Equals(command.Schema, CommandSchema, StringComparison.Ordinal)) return $"Unexpected Eve command schema '{command.Schema}'."; if (!string.Equals(command.ProviderId, AetheriaProviderAdvertisementProjector.ProviderId, StringComparison.Ordinal)) return $"Unexpected Eve provider '{command.ProviderId}'."; if (!KnownCommand(command.SurfaceId, command.Command)) return $"Command '{command.Command}' is not advertised for surface '{command.SurfaceId}'."; return ""; } private static bool KnownCommand(string surfaceId, string command) { return (string.Equals(surfaceId, AetheriaCatalogSurfaceProjector.SurfaceId, StringComparison.Ordinal) && string.Equals(command, "aetheria.catalog.refresh", StringComparison.Ordinal)) || (string.Equals(surfaceId, AetheriaOperationsSurfaceProjector.SurfaceId, StringComparison.Ordinal) && string.Equals(command, "aetheria.operations.refresh", StringComparison.Ordinal)) || (string.Equals(surfaceId, AetheriaPlayerSettingsSurfaceProjector.SurfaceId, StringComparison.Ordinal) && GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IsKnown(command)) || (string.Equals(surfaceId, GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.SurfaceId, StringComparison.Ordinal) && GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.IsKnown(command)); } private static async Task ApplyPlayerSettingsCommandAsync( AetheriaStateNode node, AetheriaRuntimeEveCommandDocument command) { var settings = await node.GetPlayerSettingsAsync().ConfigureAwait(false) ?? new AetheriaPlayerSettings(); settings.Gameplay ??= new AetheriaPlayerGameplaySettings(); settings.Graphics ??= new AetheriaPlayerGraphicsSettings(); var persistSettings = false; switch (command.Command) { case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.SetPlayerName: settings.PlayerName = command.Payload.TryGetValue("value", out var playerName) ? playerName ?? "" : ""; persistSettings = true; break; case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleTemperatureUnit: settings.Gameplay.TemperatureUnit = Cycle( settings.Gameplay.TemperatureUnit, "Kelvin", "Celsius", "Fahrenheit"); persistSettings = true; break; case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.DecrementSignificantDigits: settings.Gameplay.SignificantDigits = Math.Max(0, settings.Gameplay.SignificantDigits - 1); persistSettings = true; break; case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IncrementSignificantDigits: if (settings.Gameplay.SignificantDigits < int.MaxValue) settings.Gameplay.SignificantDigits++; persistSettings = true; break; case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.CycleNebulaQuality: settings.Graphics.NebulaQuality = Cycle( settings.Graphics.NebulaQuality, "Low", "Normal", "High", "Ultra"); persistSettings = true; break; case GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.ToggleShowAsteroidsInMinimap: settings.Graphics.ShowAsteroidsInMinimap = !settings.Graphics.ShowAsteroidsInMinimap; persistSettings = true; break; } if (persistSettings) { settings.LastUpdatedAtUtc = command.IssuedAtUtc; await node.PutPlayerSettingsAsync(settings).ConfigureAwait(false); } await node.PutPlayerSettingsSurfaceAsync( AetheriaPlayerSettingsSurfaceProjector.Build(settings, command.IssuedAtUtc)) .ConfigureAwait(false); } private static async Task ApplyVerseHostCommandAsync( AetheriaStateNode node, AetheriaRuntimeEveCommandDocument command) { var settings = await node.GetVerseHostSettingsAsync().ConfigureAwait(false) ?? new AetheriaVerseHostSettings(); var normalized = AetheriaVerseHostSettingsNormalizer.Normalize(settings); var persistSettings = false; switch (command.Command) { case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.CycleVisibility: normalized.Visibility = Cycle(normalized.Visibility, "private", "public"); persistSettings = true; break; } if (persistSettings) { normalized.LastUpdatedAtUtc = command.IssuedAtUtc; await node.PutVerseHostSettingsAsync(normalized).ConfigureAwait(false); } var commitStatus = await node.GetRuntimeCommitDrainStatusAsync().ConfigureAwait(false) ?? EmptyCommitDrainStatus(node.StatePath, command.IssuedAtUtc); var eveStatus = await node.GetEveCommandDrainStatusAsync().ConfigureAwait(false) ?? EmptyEveCommandDrainStatus(node.StatePath, command.IssuedAtUtc); var runtimeSession = string.IsNullOrWhiteSpace(commitStatus.RuntimeId) ? null : await node.GetRuntimeSessionAsync(commitStatus.RuntimeId).ConfigureAwait(false); await node.PutOperationsSurfaceAsync( AetheriaOperationsSurfaceProjector.Build( commitStatus, eveStatus, normalized, runtimeSession)).ConfigureAwait(false); await node.PutProviderAdvertisementAsync( AetheriaProviderAdvertisementProjector.Build(normalized, node.StatePath, command.IssuedAtUtc)).ConfigureAwait(false); } private static string Cycle(string current, params string[] values) { if (values.Length == 0) return current; var index = Array.FindIndex(values, value => string.Equals(value, current, StringComparison.Ordinal)); if (index < 0) return values[0]; return values[(index + 1) % values.Length]; } private static void RecordRejection( AetheriaEveCommandApplyReport report, AetheriaRuntimeEveCommandDocument command, string reason, string path, List rejected, bool deleteAccounted) { report.RejectedCommands++; report.LastRejectedCommand = string.IsNullOrWhiteSpace(command.Command) ? command.CommandId : command.Command; report.LastRejectedReason = reason; rejected.Add(path); if (deleteAccounted) File.Delete(path); } private static AetheriaRuntimeCommitDrainStatus EmptyCommitDrainStatus(string statePath, string now) { return new AetheriaRuntimeCommitDrainStatus { RuntimeId = "aetheria-state", StatePath = statePath, LastPollAtUtc = now, Status = "idle" }; } private static AetheriaEveCommandDrainStatus EmptyEveCommandDrainStatus(string statePath, string now) { return new AetheriaEveCommandDrainStatus { RuntimeId = "aetheria-state", StatePath = statePath, LastPollAtUtc = now, Status = "idle" }; } }