using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Aetheria.State.Documents; using GameCult.Aetheria.State.Unity; using GameCult.Caching; using MessagePack; namespace Aetheria.State; public sealed class AetheriaRuntimeCommitApplyReport { public int AppliedPlayerSettings { get; set; } public int AppliedLoadoutTemplates { get; set; } public int AppliedRunCheckpoints { get; set; } public string[] AppliedPaths { get; set; } = []; } public static class AetheriaRuntimeCommitLogApplier { public static async Task ApplyPendingAsync( AetheriaStateNode node, bool deleteApplied = true) { if (node == null) throw new ArgumentNullException(nameof(node)); var pendingDirectory = node.StatePath + ".pending"; var report = new AetheriaRuntimeCommitApplyReport(); var applied = 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); switch (command.Kind) { case "player_settings": await node.PutPlayerSettingsAsync( ToPlayerSettings(command.PlayerSettings ?? throw MissingPayload(command), command.CreatedAtUtc)) .ConfigureAwait(false); report.AppliedPlayerSettings++; break; case "loadout_template": var loadout = ToLoadoutTemplate( command.LoadoutTemplate ?? throw MissingPayload(command), command.CreatedAtUtc); await node.PutLoadoutTemplateAsync(LoadoutKey(loadout.Name), loadout).ConfigureAwait(false); report.AppliedLoadoutTemplates++; break; case "run_checkpoint": await ApplyRunCheckpointAsync( node, command.RunCheckpoint ?? throw MissingPayload(command), command.CreatedAtUtc) .ConfigureAwait(false); report.AppliedRunCheckpoints++; break; default: throw new InvalidDataException($"Unknown Aetheria runtime commit kind '{command.Kind}' in {path}."); } applied.Add(path); if (deleteApplied) File.Delete(path); } await node.FlushAsync().ConfigureAwait(false); report.AppliedPaths = applied.ToArray(); return report; } private static AetheriaRuntimeStateCommitDocument ReadCommand(string path) { var command = MessagePackSerializer.Deserialize(File.ReadAllBytes(path)); if (!string.Equals(command.Schema, AetheriaRuntimeStateCommitDocument.SchemaId, StringComparison.Ordinal)) throw new InvalidDataException($"Unexpected Aetheria runtime commit schema '{command.Schema}' in {path}."); return command; } private static AetheriaPlayerSettings ToPlayerSettings( AetheriaRuntimePlayerSettingsCommit settings, string updatedAtUtc) { return new AetheriaPlayerSettings { PlayerName = settings.PlayerName ?? "", TutorialPassed = settings.TutorialPassed, StoryFileHashes = (settings.StoryFileHashes ?? Array.Empty()) .Select(hash => new AetheriaStoryFileHash { StoryPath = hash.StoryPath ?? "", Hash = hash.Hash ?? "" }) .ToArray(), Gameplay = new AetheriaPlayerGameplaySettings { TemperatureUnit = settings.TemperatureUnit ?? "", SignificantDigits = settings.SignificantDigits }, Graphics = new AetheriaPlayerGraphicsSettings { NebulaQuality = settings.NebulaQuality ?? "", ShowAsteroidsInMinimap = settings.ShowAsteroidsInMinimap }, Input = new AetheriaPlayerInputSettings { BindingOverrides = (settings.BindingOverrides ?? Array.Empty()) .Select(binding => new AetheriaInputBindingOverride { ActionName = binding.ActionName ?? "", BindingIndex = binding.BindingIndex, BindingPath = binding.BindingPath ?? "" }) .ToArray(), ActionBarInputs = (settings.ActionBarInputs ?? Array.Empty()) .Select(input => input ?? "") .ToArray() }, LastUpdatedAtUtc = updatedAtUtc }; } private static AetheriaLoadoutTemplate ToLoadoutTemplate( AetheriaRuntimeLoadoutTemplateCommit loadout, string updatedAtUtc) { return new AetheriaLoadoutTemplate { Name = loadout.Name ?? "", OwnerPlayerKey = loadout.OwnerPlayerKey ?? "", RootEntity = ToEntityLoadout(loadout.RootEntity), CreatedAtUtc = updatedAtUtc, UpdatedAtUtc = updatedAtUtc }; } private static AetheriaEntityLoadout ToEntityLoadout(AetheriaRuntimeEntityLoadoutCommit? entity) { entity ??= new AetheriaRuntimeEntityLoadoutCommit(); return new AetheriaEntityLoadout { Name = entity.Name ?? "", Kind = entity.Kind ?? "", FactionKey = ReferenceKey("aetheria.corporation", entity.CorporationLegacyId ?? ""), Hull = ToLoadoutItem(entity.Hull), Equipment = ToItemSlots(entity.Equipment), CargoBays = ToItemSlots(entity.CargoBays), DockingBays = ToItemSlots(entity.DockingBays), CargoContents = ToCargoBays(entity.CargoContents), DockingBayContents = ToCargoBays(entity.DockingBayContents), DockingBayAssignments = ToIntArray(entity.DockingBayAssignments), WeaponGroups = ToIntArrayArray(entity.WeaponGroups), Children = (entity.Children ?? Array.Empty()) .Select(ToEntityLoadout) .ToArray() }; } private static AetheriaLoadoutItem ToLoadoutItem(AetheriaRuntimeLoadoutItemCommit? item) { item ??= new AetheriaRuntimeLoadoutItemCommit(); return new AetheriaLoadoutItem { ItemKey = ReferenceKey("aetheria.item_definition", item.ItemDefinitionLegacyId ?? ""), Quality = item.Quality, Durability = item.Durability, Quantity = item.Quantity }; } private static AetheriaLoadoutItemSlot[] ToItemSlots( IReadOnlyList? slots) { return (slots ?? Array.Empty()) .Select(slot => new AetheriaLoadoutItemSlot { Position = new AetheriaGridCoord { X = slot.X, Y = slot.Y }, Item = ToLoadoutItem(slot.Item) }) .ToArray(); } private static AetheriaEntityItemSlot[] ToEntityItemSlots( IReadOnlyList? slots) { return ToItemSlots(slots) .Select(slot => new AetheriaEntityItemSlot { Position = slot.Position, ItemKey = slot.Item.ItemKey }) .ToArray(); } private static AetheriaCargoBayLoadout[] ToCargoBays( IReadOnlyList? cargoBays) { return (cargoBays ?? Array.Empty()) .Select(bay => new AetheriaCargoBayLoadout { Items = ToItemSlots(bay.Items) }) .ToArray(); } private static async Task ApplyRunCheckpointAsync( AetheriaStateNode node, AetheriaRuntimeRunCheckpointCommit checkpoint, string updatedAtUtc) { var run = new AetheriaRunState { RunId = checkpoint.RunId ?? "", IsTutorial = checkpoint.IsTutorial, EntranceZoneIndex = checkpoint.EntranceZoneIndex, ExitZoneIndex = checkpoint.ExitZoneIndex, CurrentZoneIndex = checkpoint.CurrentZoneIndex, CurrentZoneEntityIndex = checkpoint.CurrentZoneEntityIndex, DiscoveredZoneIndices = ToIntArray(checkpoint.DiscoveredZoneIndices), ActionBarBindings = ToActionBarBindings(checkpoint.ActionBarBindings), FactionRelationships = ToFactionRelationships(checkpoint.FactionRelationships), UpdatedAtUtc = updatedAtUtc }; var zoneKeys = new List(); foreach (var zone in checkpoint.Zones ?? Array.Empty()) { var zoneKey = ZoneKey(run.RunId, zone.ZoneIndex); var entityKeys = new List(); foreach (var entity in zone.Entities ?? Array.Empty()) { var entityKey = EntityKey(run.RunId, zone.ZoneIndex, entity.EntityIndex); var snapshot = ToEntitySnapshot(entity, run.RunId, zone.ZoneIndex); await node.PutEntitySnapshotAsync(entityKey, snapshot).ConfigureAwait(false); entityKeys.Add(entityKey.ToString()); } await node.PutZoneStateAsync( zoneKey, new AetheriaZoneState { Name = zone.Name ?? "", Position = new AetheriaVector2 { X = zone.PositionX, Y = zone.PositionY }, AdjacentZoneIndices = ToIntArray(zone.AdjacentZoneIndices), FactionIndices = ToIntArray(zone.FactionIndices), OwnerFactionIndex = zone.OwnerFactionIndex, EntityKeys = entityKeys.ToArray() }) .ConfigureAwait(false); zoneKeys.Add(zoneKey.ToString()); } if (zoneKeys.Count > 0) run.ZoneKeys = zoneKeys.ToArray(); await node.PutRunStateAsync(RunKey(run.RunId), run).ConfigureAwait(false); } private static AetheriaEntitySnapshot ToEntitySnapshot( AetheriaRuntimeEntitySnapshotCommit entity, string runId, int zoneIndex) { return new AetheriaEntitySnapshot { Name = entity.Name ?? "", Kind = entity.Kind ?? "", Position = new AetheriaVector3 { X = entity.PositionX, Y = entity.PositionY, Z = entity.PositionZ }, Direction = new AetheriaVector2 { X = entity.DirectionX, Y = entity.DirectionY }, FactionKey = ReferenceKey("aetheria.corporation", entity.CorporationLegacyId ?? ""), HullItemKey = ReferenceKey("aetheria.item_definition", entity.HullItemDefinitionLegacyId ?? ""), Equipment = ToEntityItemSlots(entity.Equipment), CargoBays = ToEntityItemSlots(entity.CargoBays), DockingBays = ToEntityItemSlots(entity.DockingBays), ChildEntityKeys = (entity.ChildEntityIndices ?? Array.Empty()) .Select(childIndex => EntityKey(runId, zoneIndex, childIndex).ToString()) .ToArray(), WeaponGroups = ToIntArrayArray(entity.WeaponGroups) .Select(group => new AetheriaWeaponGroupSnapshot { EquipmentIndices = group }) .ToArray() }; } private static int[] ToIntArray(IReadOnlyList? values) { return (values ?? Array.Empty()).ToArray(); } private static int[][] ToIntArrayArray(IReadOnlyList>? values) { return (values ?? Array.Empty>()) .Select(ToIntArray) .ToArray(); } private static AetheriaActionBarBinding[] ToActionBarBindings( IReadOnlyList? bindings) { return (bindings ?? Array.Empty()) .Select(binding => new AetheriaActionBarBinding { ControlPath = binding.ControlPath ?? "", Kind = binding.Kind ?? "", TargetKey = ReferenceKey("aetheria.item_definition", binding.ItemDefinitionLegacyId ?? ""), EquipmentIndex = binding.EquipmentIndex, BehaviorIndex = binding.BehaviorIndex, WeaponGroup = binding.WeaponGroup }) .ToArray(); } private static AetheriaFactionRelationshipState[] ToFactionRelationships( IReadOnlyList? relationships) { return (relationships ?? Array.Empty()) .Select(relationship => new AetheriaFactionRelationshipState { FactionKey = ReferenceKey("aetheria.corporation", relationship.CorporationLegacyId ?? ""), Relationship = relationship.Relationship ?? "", Standing = relationship.Standing }) .ToArray(); } private static CultRecordKey LoadoutKey(string name) { return new CultRecordKey($"global:aetheria.loadout_template.{StableToken(name)}.v1"); } private static CultRecordKey RunKey(string runId) { return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.v1"); } private static CultRecordKey ZoneKey(string runId, int zoneIndex) { return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.zone.{zoneIndex}.v1"); } private static CultRecordKey EntityKey(string runId, int zoneIndex, int entityIndex) { return new CultRecordKey($"global:aetheria.run_state.{StableToken(runId)}.zone.{zoneIndex}.entity.{entityIndex}.v1"); } private static string ReferenceKey(string documentName, string legacyId) { return string.IsNullOrWhiteSpace(legacyId) ? "" : $"{documentName}:legacy:{legacyId}"; } private static string StableToken(string value) { var chars = (string.IsNullOrWhiteSpace(value) ? "unnamed" : value.Trim().ToLowerInvariant()) .Select(ch => char.IsLetterOrDigit(ch) ? ch : '-') .ToArray(); var token = new string(chars).Trim('-'); while (token.Contains("--", StringComparison.Ordinal)) token = token.Replace("--", "-", StringComparison.Ordinal); return string.IsNullOrWhiteSpace(token) ? "unnamed" : token; } private static InvalidDataException MissingPayload(AetheriaRuntimeStateCommitDocument command) { return new InvalidDataException($"Aetheria runtime commit '{command.CommandId}' has no {command.Kind} payload."); } }