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; 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": var settings = ToPlayerSettings( command.PlayerSettings ?? throw MissingPayload(command), command.CreatedAtUtc); await node.PutPlayerSettingsAsync(settings).ConfigureAwait(false); await node.PutPlayerSettingsSurfaceAsync( AetheriaPlayerSettingsSurfaceProjector.Build(settings, 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 = AetheriaRuntimePendingCultCacheStore.ReadStateCommit(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 = entity.FactionKey ?? "", 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 = item.ItemKey ?? "", Quality = item.Quality, Durability = item.Durability, Quantity = item.Quantity, Enabled = item.Enabled, OverrideShutdown = item.OverrideShutdown }; } 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 (slots ?? Array.Empty()) .Select(slot => { var item = ToLoadoutItem(slot.Item); return new AetheriaEntityItemSlot { Position = new AetheriaGridCoord { X = slot.X, Y = slot.Y }, ItemKey = item.ItemKey, Quality = item.Quality, Durability = item.Durability, Quantity = item.Quantity, Enabled = item.Enabled, OverrideShutdown = item.OverrideShutdown }; }) .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, DiscoveredZoneIndices = ToIntArray(checkpoint.DiscoveredZoneIndices), ActionBarBindings = ToActionBarBindings(checkpoint.ActionBarBindings), FactionRelationships = ToFactionRelationships(checkpoint.FactionRelationships), UpdatedAtUtc = updatedAtUtc, GenerationSeed = checkpoint.GenerationSeed }; 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(), Orbits = ToOrbitSnapshots(zone.Orbits), Bodies = ToBodySnapshots(zone.Bodies, run.RunId, zone.ZoneIndex), DroppedPickups = ToDroppedPickups(zone.DroppedPickups) }) .ConfigureAwait(false); zoneKeys.Add(zoneKey.ToString()); } if (zoneKeys.Count > 0) run.ZoneKeys = zoneKeys.ToArray(); run.CurrentEntityKey = checkpoint.CurrentEntityKey ?? ""; await node.PutRunStateAsync(RunKey(run.RunId), run).ConfigureAwait(false); } private static AetheriaDroppedPickupSnapshot[] ToDroppedPickups( IReadOnlyList? pickups) { return (pickups ?? Array.Empty()) .Select(pickup => new AetheriaDroppedPickupSnapshot { PickupIndex = pickup.PickupIndex, Position = new AetheriaVector3 { X = pickup.PositionX, Y = pickup.PositionY, Z = pickup.PositionZ }, Velocity = new AetheriaVector3 { X = pickup.VelocityX, Y = pickup.VelocityY, Z = pickup.VelocityZ }, Item = ToLoadoutItem(pickup.Item) }) .ToArray(); } private static AetheriaOrbitSnapshot[] ToOrbitSnapshots( IReadOnlyList? orbits) { return (orbits ?? Array.Empty()) .Select(orbit => new AetheriaOrbitSnapshot { OrbitKey = orbit.OrbitKey ?? "", ParentOrbitKey = orbit.ParentOrbitKey ?? "", Distance = orbit.Distance, Phase = orbit.Phase, FixedPosition = new AetheriaVector2 { X = orbit.FixedPositionX, Y = orbit.FixedPositionY } }) .ToArray(); } private static AetheriaBodySnapshot[] ToBodySnapshots( IReadOnlyList? bodies, string runId, int zoneIndex) { return (bodies ?? Array.Empty()) .Select(body => new AetheriaBodySnapshot { BodyKey = body.BodyKey ?? "", Kind = body.Kind ?? "", Name = body.Name ?? "", OrbitKey = body.OrbitKey ?? "", Mass = body.Mass, Resources = ToBodyResources(body.Resources), BodyRadiusMultiplier = body.BodyRadiusMultiplier, GravityRadiusMultiplier = body.GravityRadiusMultiplier, GravityDepthMultiplier = body.GravityDepthMultiplier, GravityDepthExponent = body.GravityDepthExponent, Asteroids = ToAsteroidSnapshots(body.Asteroids, runId, zoneIndex), GasGiantVisual = ToGasGiantVisual(body.GasGiantVisual), SunVisual = ToSunVisual(body.SunVisual) }) .ToArray(); } private static AetheriaBodyResource[] ToBodyResources( IReadOnlyList? resources) { return (resources ?? Array.Empty()) .Select(resource => new AetheriaBodyResource { ItemKey = resource.ItemKey ?? "", Amount = resource.Amount }) .ToArray(); } private static AetheriaAsteroidSnapshot[] ToAsteroidSnapshots( IReadOnlyList? asteroids, string runId, int zoneIndex) { return (asteroids ?? Array.Empty()) .Select(asteroid => new AetheriaAsteroidSnapshot { Distance = asteroid.Distance, Phase = asteroid.Phase, Size = asteroid.Size, RotationSpeed = asteroid.RotationSpeed, Damage = asteroid.Damage, RespawnTimer = asteroid.RespawnTimer, MiningAccumulators = ToAsteroidMiningAccumulators(asteroid.MiningAccumulators, runId, zoneIndex) }) .ToArray(); } private static AetheriaAsteroidMiningAccumulatorSnapshot[] ToAsteroidMiningAccumulators( IReadOnlyList? accumulators, string runId, int zoneIndex) { return (accumulators ?? Array.Empty()) .Where(accumulator => accumulator.MinerEntityIndex >= 0) .Select(accumulator => new AetheriaAsteroidMiningAccumulatorSnapshot { MinerEntityKey = EntityKey(runId, zoneIndex, accumulator.MinerEntityIndex).ToString(), Amount = accumulator.Amount }) .ToArray(); } private static AetheriaGasGiantVisualState ToGasGiantVisual(AetheriaRuntimeGasGiantVisualCommit? visual) { if (visual == null) return new AetheriaGasGiantVisualState(); return new AetheriaGasGiantVisualState { FirstOffsetDomainRotationSpeed = visual.FirstOffsetDomainRotationSpeed, FirstOffsetRotationSpeed = visual.FirstOffsetRotationSpeed, SecondOffsetDomainRotationSpeed = visual.SecondOffsetDomainRotationSpeed, SecondOffsetRotationSpeed = visual.SecondOffsetRotationSpeed, AlbedoRotationSpeed = visual.AlbedoRotationSpeed, WaveRadiusMultiplier = visual.WaveRadiusMultiplier, WaveDepthMultiplier = visual.WaveDepthMultiplier, WaveDepthExponent = visual.WaveDepthExponent, WaveSpeedMultiplier = visual.WaveSpeedMultiplier, MaterialOverrides = (visual.MaterialOverrides ?? Array.Empty()).ToArray(), Colors = (visual.Colors ?? Array.Empty()) .Select(ToColor) .ToArray() }; } private static AetheriaSunVisualState ToSunVisual(AetheriaRuntimeSunVisualCommit? visual) { if (visual == null) return new AetheriaSunVisualState(); return new AetheriaSunVisualState { LightColor = new AetheriaVector3 { X = visual.LightColorX, Y = visual.LightColorY, Z = visual.LightColorZ }, FogTintColor = new AetheriaVector3 { X = visual.FogTintColorX, Y = visual.FogTintColorY, Z = visual.FogTintColorZ }, LightRadiusMultiplier = visual.LightRadiusMultiplier }; } private static AetheriaColor ToColor(AetheriaRuntimeColorCommit color) { return new AetheriaColor { X = color.X, Y = color.Y, Z = color.Z, W = color.W }; } 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 }, Velocity = new AetheriaVector2 { X = entity.VelocityX, Y = entity.VelocityY }, TargetEntityKey = entity.TargetEntityIndex >= 0 ? EntityKey(runId, zoneIndex, entity.TargetEntityIndex).ToString() : "", IsActive = entity.IsActive, HeatsinksEnabled = entity.HeatsinksEnabled, OverrideShutdown = entity.OverrideShutdown, TractorPower = entity.TractorPower, Heatstroke = entity.Heatstroke, Hypothermia = entity.Hypothermia, FactionKey = entity.FactionKey ?? "", HullItemKey = entity.HullItemKey ?? "", 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(), StatGrids = ToEntityStatGrids(entity.StatGrids), ActiveConsumables = ToActiveConsumables(entity.ActiveConsumables), BehaviorProgress = ToBehaviorProgress(entity.BehaviorProgress), WeaponStates = ToWeaponStates(entity.WeaponStates, runId, zoneIndex), BehaviorStates = ToBehaviorStates(entity.BehaviorStates), CargoContents = ToCargoBays(entity.CargoContents), DockingBayContents = ToCargoBays(entity.DockingBayContents), DockingBayAssignments = ToIntArray(entity.DockingBayAssignments), Visibility = entity.Visibility, VisibilitySourceCount = entity.VisibilitySourceCount, Contacts = ToEntityContacts(entity.Contacts, runId, zoneIndex) }; } private static AetheriaEntityContactSnapshot[] ToEntityContacts( IReadOnlyList? contacts, string runId, int zoneIndex) { return (contacts ?? Array.Empty()) .Where(contact => contact.TargetEntityIndex >= 0) .Select(contact => new AetheriaEntityContactSnapshot { TargetEntityKey = EntityKey(runId, zoneIndex, contact.TargetEntityIndex).ToString(), InfoGathered = contact.InfoGathered, Hostile = contact.Hostile, Visible = contact.Visible }) .ToArray(); } private static AetheriaBehaviorStateSnapshot[] ToBehaviorStates( IReadOnlyList? states) { return (states ?? Array.Empty()) .Select(state => new AetheriaBehaviorStateSnapshot { OwnerKind = state.OwnerKind ?? "", OwnerIndex = state.OwnerIndex, BehaviorIndex = state.BehaviorIndex, BehaviorKind = state.BehaviorKind ?? "", Pinging = state.Pinging, PingCooldown = state.PingCooldown, PingLerp = state.PingLerp, PingRadius = state.PingRadius, PingedEntityCount = state.PingedEntityCount, RadiatorTemperature = state.RadiatorTemperature, Emissivity = state.Emissivity, PumpedHeat = state.PumpedHeat, WasteHeat = state.WasteHeat, EnergyUsage = state.EnergyUsage, ReactorDraw = state.ReactorDraw, ReactorLoadRatio = state.ReactorLoadRatio, CapacitorCharge = state.CapacitorCharge, CapacitorCapacity = state.CapacitorCapacity, CapacitorEfficiency = state.CapacitorEfficiency, AetherDriveAxisX = state.AetherDriveAxisX, AetherDriveAxisY = state.AetherDriveAxisY, AetherDriveAxisZ = state.AetherDriveAxisZ, AetherDriveThrustX = state.AetherDriveThrustX, AetherDriveThrustY = state.AetherDriveThrustY, AetherDriveThrustZ = state.AetherDriveThrustZ, AetherDriveRpmX = state.AetherDriveRpmX, AetherDriveRpmY = state.AetherDriveRpmY, AetherDriveRpmZ = state.AetherDriveRpmZ, AetherDriveMaximumRpm = state.AetherDriveMaximumRpm, AetherDriveThrustDirectionX = state.AetherDriveThrustDirectionX, AetherDriveThrustDirectionY = state.AetherDriveThrustDirectionY, ResourceScannerTargetBodyKey = state.ResourceScannerTargetBodyKey ?? "", ResourceScannerAsteroidIndex = state.ResourceScannerAsteroidIndex, ResourceScannerScanTime = state.ResourceScannerScanTime, ResourceScannerRange = state.ResourceScannerRange, ResourceScannerMinimumDensity = state.ResourceScannerMinimumDensity, ResourceScannerScanDuration = state.ResourceScannerScanDuration, MiningToolAsteroidBeltKey = state.MiningToolAsteroidBeltKey ?? "", MiningToolAsteroidIndex = state.MiningToolAsteroidIndex, MiningToolRange = state.MiningToolRange, ThrusterAxis = state.ThrusterAxis, ThrusterThrust = state.ThrusterThrust, ThrusterTorque = state.ThrusterTorque, ShieldEfficiency = state.ShieldEfficiency, ShieldEnergyUsage = state.ShieldEnergyUsage, VelocityLimit = state.VelocityLimit, ThermotoggleTargetTemperature = state.ThermotoggleTargetTemperature, SwitchActivated = state.SwitchActivated, TriggerPulled = state.TriggerPulled, StatModifierApplied = state.StatModifierApplied, StatModifierExecuted = state.StatModifierExecuted, StatModifierTargetStatCount = state.StatModifierTargetStatCount, TurretControllerWeaponCount = state.TurretControllerWeaponCount, TurretControllerShotSpeed = state.TurretControllerShotSpeed, TurretControllerPredictShots = state.TurretControllerPredictShots }) .ToArray(); } private static AetheriaWeaponStateSnapshot[] ToWeaponStates( IReadOnlyList? states, string runId, int zoneIndex) { return (states ?? Array.Empty()) .Select(state => new AetheriaWeaponStateSnapshot { OwnerKind = state.OwnerKind ?? "", OwnerIndex = state.OwnerIndex, BehaviorIndex = state.BehaviorIndex, BehaviorKind = state.BehaviorKind ?? "", Firing = state.Firing, Ammo = state.Ammo, BurstRemaining = state.BurstRemaining, BurstTimer = state.BurstTimer, BurstInterval = state.BurstInterval, CooldownProgress = state.CooldownProgress, CoolingDown = state.CoolingDown, Charging = state.Charging, Charged = state.Charged, Charge = state.Charge, Reloading = state.Reloading, ReloadProgress = state.ReloadProgress, AmmoIntervalProgress = state.AmmoIntervalProgress, LockProgress = state.LockProgress, LockTargetEntityKey = state.LockTargetEntityIndex >= 0 ? EntityKey(runId, zoneIndex, state.LockTargetEntityIndex).ToString() : "" }) .ToArray(); } private static AetheriaBehaviorProgressSnapshot[] ToBehaviorProgress( IReadOnlyList? progress) { return (progress ?? Array.Empty()) .Select(entry => new AetheriaBehaviorProgressSnapshot { OwnerKind = entry.OwnerKind ?? "", OwnerIndex = entry.OwnerIndex, BehaviorIndex = entry.BehaviorIndex, BehaviorKind = entry.BehaviorKind ?? "", Progress = entry.Progress }) .ToArray(); } private static AetheriaActiveConsumableSnapshot[] ToActiveConsumables( IReadOnlyList? consumables) { return (consumables ?? Array.Empty()) .Select(consumable => new AetheriaActiveConsumableSnapshot { ItemKey = consumable.ItemKey ?? "", Quality = consumable.Quality, RemainingDuration = consumable.RemainingDuration, Duration = consumable.Duration }) .ToArray(); } private static AetheriaEntityStatGrid[] ToEntityStatGrids( IReadOnlyList? grids) { return (grids ?? Array.Empty()) .Select(grid => new AetheriaEntityStatGrid { Name = grid.Name ?? "", Width = grid.Width, Height = grid.Height, Values = (grid.Values ?? Array.Empty()).ToArray() }) .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 = binding.ItemKey ?? "", 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 = relationship.FactionKey ?? "", 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 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."); } }