using Aetheria.State.Documents; using GameCult.Aetheria.State.Unity; namespace Aetheria.State; public sealed class AetheriaEveCommandAcceptanceReport { public int AcceptedCatalogRefreshes { get; set; } public int AcceptedOperationsRefreshes { get; set; } public int AcceptedPlayerSettingsCommands { get; set; } public int AcceptedInputSettingsCommands { get; set; } public int AcceptedLoadoutTemplateCommands { get; set; } public int AcceptedVerseHostCommands { get; set; } public int RejectedCommands { get; set; } public string[] AcceptedCommandIds { get; set; } = []; public string[] RejectedCommandIds { get; set; } = []; public string[] AccountedCommandIds { 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 async Task AcceptObservedAsync( AetheriaStateNode node, IEnumerable? accountedCommandIds = null) { if (node == null) throw new ArgumentNullException(nameof(node)); var report = new AetheriaEveCommandAcceptanceReport(); var accepted = new List(); var rejected = new List(); var accounted = new HashSet(accountedCommandIds ?? Array.Empty(), StringComparer.Ordinal); foreach (var command in node.ReadObservedEveCommands() .Where(command => !string.IsNullOrWhiteSpace(command.CommandId)) .Where(command => !accounted.Contains(command.CommandId))) { var rejection = Validate(command); if (!string.IsNullOrWhiteSpace(rejection)) { RecordRejection(report, command, rejection, rejected); continue; } switch (command.Command) { case AetheriaRuntimeCatalogCommands.Refresh: await node.PutCatalogSurfaceAsync( AetheriaCatalogSurfaceProjector.Build(node.ReadCatalogSnapshot(), command.IssuedAtUtc)).ConfigureAwait(false); report.AcceptedCatalogRefreshes++; break; case AetheriaRuntimeOperationsCommands.Refresh: var eveStatus = await node.GetEveCommandAcceptanceStatusAsync().ConfigureAwait(false) ?? EmptyEveCommandAcceptanceStatus(node.StatePath, command.IssuedAtUtc); var verseHostSettings = await node.GetVerseHostSettingsAsync().ConfigureAwait(false); var runtimeSession = await node.GetRuntimeSessionAsync(eveStatus.RuntimeId).ConfigureAwait(false); await node.PutOperationsSurfaceAsync( AetheriaOperationsSurfaceProjector.Build( eveStatus, verseHostSettings, runtimeSession)).ConfigureAwait(false); report.AcceptedOperationsRefreshes++; 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 ExecutePlayerSettingsCommandAsync(node, command).ConfigureAwait(false); report.AcceptedPlayerSettingsCommands++; break; case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.Refresh: case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetBindingOverride: case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetActionBarEnabled: await ExecuteInputSettingsCommandAsync(node, command).ConfigureAwait(false); report.AcceptedInputSettingsCommands++; break; case GameCult.Aetheria.State.Unity.AetheriaRuntimeLoadoutTemplateCommands.Save: await ExecuteLoadoutTemplateCommandAsync(node, command).ConfigureAwait(false); report.AcceptedLoadoutTemplateCommands++; break; case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.Refresh: case GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.CycleVisibility: await ExecuteVerseHostCommandAsync(node, command).ConfigureAwait(false); report.AcceptedVerseHostCommands++; break; } accepted.Add(command.CommandId ?? ""); } await node.FlushAsync().ConfigureAwait(false); report.AcceptedCommandIds = accepted.ToArray(); report.RejectedCommandIds = rejected.ToArray(); report.AccountedCommandIds = accounted .Concat(report.AcceptedCommandIds) .Concat(report.RejectedCommandIds) .Where(commandId => !string.IsNullOrWhiteSpace(commandId)) .Distinct(StringComparer.Ordinal) .ToArray(); return report; } 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, AetheriaRuntimeCatalogCommands.SurfaceId, StringComparison.Ordinal) && AetheriaRuntimeCatalogCommands.IsKnown(command)) || (string.Equals(surfaceId, AetheriaRuntimeOperationsCommands.SurfaceId, StringComparison.Ordinal) && AetheriaRuntimeOperationsCommands.IsKnown(command)) || (string.Equals(surfaceId, AetheriaPlayerSettingsSurfaceProjector.SurfaceId, StringComparison.Ordinal) && GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommands.IsKnown(command)) || (string.Equals(surfaceId, GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SurfaceId, StringComparison.Ordinal) && GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.IsKnown(command)) || (string.Equals(surfaceId, GameCult.Aetheria.State.Unity.AetheriaRuntimeLoadoutTemplateCommands.SurfaceId, StringComparison.Ordinal) && GameCult.Aetheria.State.Unity.AetheriaRuntimeLoadoutTemplateCommands.IsKnown(command)) || (string.Equals(surfaceId, GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.SurfaceId, StringComparison.Ordinal) && GameCult.Aetheria.State.Unity.AetheriaRuntimeVerseHostCommands.IsKnown(command)); } private static async Task ExecutePlayerSettingsCommandAsync( 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.PlayerSettings.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 ExecuteInputSettingsCommandAsync( AetheriaStateNode node, AetheriaRuntimeEveCommandDocument command) { var settings = await node.GetPlayerSettingsAsync().ConfigureAwait(false) ?? new AetheriaPlayerSettings(); settings.Input ??= new AetheriaPlayerInputSettings(); var persistSettings = false; switch (command.Command) { case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetBindingOverride: var actionName = command.InputSettings.ActionName ?? ""; var inputSystemPath = command.InputSettings.InputSystemPath ?? ""; var bindingIndex = command.InputSettings.BindingIndex; if (!string.IsNullOrWhiteSpace(actionName) && bindingIndex >= 0) { settings.Input.BindingOverrides = settings.Input.BindingOverrides .Where(binding => !string.Equals(binding.ActionName, actionName, StringComparison.Ordinal) || binding.BindingIndex != bindingIndex) .Concat(new[] { new AetheriaInputBindingOverride { ActionName = actionName, BindingIndex = bindingIndex, BindingPath = inputSystemPath } }) .OrderBy(binding => binding.ActionName, StringComparer.Ordinal) .ThenBy(binding => binding.BindingIndex) .ToArray(); persistSettings = true; } break; case GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommands.SetActionBarEnabled: var inputPath = command.InputSettings.InputSystemPath ?? ""; var enabled = command.InputSettings.Enabled; if (!string.IsNullOrWhiteSpace(inputPath)) { var inputs = settings.Input.ActionBarInputs .Where(path => !string.Equals(path, inputPath, StringComparison.Ordinal)) .ToList(); if (enabled) inputs.Add(inputPath); settings.Input.ActionBarInputs = inputs .OrderBy(path => path, StringComparer.Ordinal) .ToArray(); persistSettings = true; } break; } if (persistSettings) { settings.LastUpdatedAtUtc = command.IssuedAtUtc; await node.PutPlayerSettingsAsync(settings).ConfigureAwait(false); } } private static async Task ExecuteVerseHostCommandAsync( 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 eveStatus = await node.GetEveCommandAcceptanceStatusAsync().ConfigureAwait(false) ?? EmptyEveCommandAcceptanceStatus(node.StatePath, command.IssuedAtUtc); var runtimeSession = await node.GetRuntimeSessionAsync(eveStatus.RuntimeId).ConfigureAwait(false); await node.PutOperationsSurfaceAsync( AetheriaOperationsSurfaceProjector.Build( eveStatus, normalized, runtimeSession)).ConfigureAwait(false); await node.PutProviderAdvertisementAsync( AetheriaProviderAdvertisementProjector.Build(normalized, node.StatePath, command.IssuedAtUtc)).ConfigureAwait(false); } private static async Task ExecuteLoadoutTemplateCommandAsync( AetheriaStateNode node, AetheriaRuntimeEveCommandDocument command) { var commit = command.LoadoutTemplate ?? throw new InvalidOperationException( $"Loadout template command '{command.CommandId}' is missing its typed payload."); var loadout = AetheriaRuntimeStateMapper.ToLoadoutTemplate(commit, command.IssuedAtUtc); await node.PutLoadoutTemplateAsync( AetheriaRuntimeStateMapper.LoadoutKey(loadout.Name), loadout).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( AetheriaEveCommandAcceptanceReport report, AetheriaRuntimeEveCommandDocument command, string reason, List rejected) { report.RejectedCommands++; report.LastRejectedCommand = string.IsNullOrWhiteSpace(command.Command) ? command.CommandId : command.Command; report.LastRejectedReason = reason; rejected.Add(command.CommandId ?? ""); } private static AetheriaEveCommandAcceptanceStatus EmptyEveCommandAcceptanceStatus(string statePath, string now) { return new AetheriaEveCommandAcceptanceStatus { RuntimeId = "aetheria-state", StatePath = statePath, LastPollAtUtc = now, Status = "idle" }; } }