diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 4ee1484af..d7232dd37 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -12904,10 +12904,16 @@ static void RequireMainMenuContinueRunState(string root) "Unity still owns daemon entity snapshot projection; package runtime should lower typed daemon entity snapshots."); } + var zoneRenderDocumentsPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeRtsViewportDocuments.cs"); + var zoneRenderDocuments = File.Exists(zoneRenderDocumentsPath) + ? File.ReadAllText(zoneRenderDocumentsPath) + : throw new InvalidOperationException("Cannot verify Continue entity snapshot access; AetheriaRuntimeRtsViewportDocuments.cs is missing."); var daemonEntitySnapshotProjectorPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeEntitySnapshotProjector.cs"); - var daemonEntitySnapshotProjector = File.Exists(daemonEntitySnapshotProjectorPath) - ? File.ReadAllText(daemonEntitySnapshotProjectorPath) - : throw new InvalidOperationException("Cannot verify Continue entity projection; AetheriaRuntimeEntitySnapshotProjector.cs is missing."); + if (File.Exists(daemonEntitySnapshotProjectorPath)) + { + throw new InvalidOperationException( + "AetheriaRuntimeEntitySnapshotProjector is dead access chaff; AetheriaRuntimeZoneRenderDocument owns managed entity snapshot access."); + } var observedEntityRestorerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "AetheriaUnityObservedEntityRestorer.cs"); var observedEntityRestorer = File.Exists(observedEntityRestorerPath) ? File.ReadAllText(observedEntityRestorerPath) @@ -13007,27 +13013,27 @@ static void RequireMainMenuContinueRunState(string root) string.Join(", ", gameplayCatalogPolicyHits)); } - var requiredEntitySnapshotProjectorSymbols = new[] + var requiredZoneRenderSnapshotSymbols = new[] { - "public static class AetheriaRuntimeEntitySnapshotProjector", - "public static AetheriaRuntimeEntitySnapshot[] CreateSnapshots(", - "public static string DaemonEntityRecordKey(string runId, int zoneIndex, int entityIndex)", + "public AetheriaRuntimeEntitySnapshot[] CreateEntitySnapshots()", + "public static AetheriaRuntimeEntitySnapshot[] CreateEntitySnapshots(", + "public static string EntityRecordKey(string runId, int zoneIndex, int entityIndex)", "new AetheriaRuntimeEntitySnapshot(", "entity.EntityIndex,", - "CreateItemSlots(entity.Equipment)", + "CreateEntityItemSlots(entity.Equipment)", "CreateWeaponStates(runId, zoneIndex, entity.WeaponStates)", "CreateBehaviorStates(entity.BehaviorStates)", "CreateCargoBays(entity.CargoContents)", "new AetheriaRuntimeEntityContactSnapshot(" }; - var missingEntitySnapshotProjectorSymbols = requiredEntitySnapshotProjectorSymbols - .Where(symbol => !daemonEntitySnapshotProjector.Contains(symbol, StringComparison.Ordinal)) + var missingZoneRenderSnapshotSymbols = requiredZoneRenderSnapshotSymbols + .Where(symbol => !zoneRenderDocuments.Contains(symbol, StringComparison.Ordinal)) .ToArray(); - if (missingEntitySnapshotProjectorSymbols.Length > 0) + if (missingZoneRenderSnapshotSymbols.Length > 0) { throw new InvalidOperationException( - "Daemon entity snapshot projection must live in the runtime package outside ActionGameManager and Unity Assets: " + - string.Join(", ", missingEntitySnapshotProjectorSymbols)); + "Managed daemon entity snapshot access must live on AetheriaRuntimeZoneRenderDocument instead of a projector-shaped helper: " + + string.Join(", ", missingZoneRenderSnapshotSymbols)); } var forbiddenManagerSnapshotProjectionSymbols = new[] @@ -13204,7 +13210,7 @@ static void RequireMainMenuContinueRunState(string root) "private bool TryRestoreEntityGraphFromZoneRender(", "if (string.IsNullOrWhiteSpace(render.RunId))", "Aetheria zone-render feed does not identify a run id.", - "AetheriaRuntimeEntitySnapshotProjector.CreateSnapshots(runId, render.ZoneIndex, render.EntitySnapshots)", + "render.CreateEntitySnapshots()", ".OrderBy(entity => entity.EntityIndex)", "_entityRestorer.TryApplyInPlace(", "zoneRenderer?.ApplyZoneRender(render)", @@ -13827,6 +13833,7 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) var daemonOperationClientPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeDaemonOperationClient.cs"); var daemonRuntimeOperationsPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeDaemonOperations.cs"); var daemonIntentPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeDaemonIntentState.cs"); + var zoneRenderDocumentsPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeRtsViewportDocuments.cs"); var daemonObserverPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "AetheriaDaemonObserver.cs"); var pilotCommandSenderPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "AetheriaUnityPilotCommandSender.cs"); var pilotFrameControllerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "AetheriaUnityPilotFrameController.cs"); @@ -13897,9 +13904,14 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) "Unity still owns daemon entity snapshot projection; package runtime should lower typed daemon entity snapshots."); } - var daemonEntitySnapshotProjector = File.Exists(daemonEntitySnapshotProjectorPath) - ? File.ReadAllText(daemonEntitySnapshotProjectorPath) - : throw new InvalidOperationException("Cannot verify Unity observer authority; AetheriaRuntimeEntitySnapshotProjector.cs is missing."); + var zoneRenderDocuments = File.Exists(zoneRenderDocumentsPath) + ? File.ReadAllText(zoneRenderDocumentsPath) + : throw new InvalidOperationException("Cannot verify Unity observer authority; AetheriaRuntimeRtsViewportDocuments.cs is missing."); + if (File.Exists(daemonEntitySnapshotProjectorPath)) + { + throw new InvalidOperationException( + "AetheriaRuntimeEntitySnapshotProjector is dead access chaff; AetheriaRuntimeZoneRenderDocument owns managed entity snapshot access."); + } var observedEntityRestorer = File.Exists(observedEntityRestorerPath) ? File.ReadAllText(observedEntityRestorerPath) : throw new InvalidOperationException("Cannot verify Unity observer authority; AetheriaUnityObservedEntityRestorer.cs is missing."); @@ -14030,15 +14042,15 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) string.Join(", ", missingSceneWiringObserverSymbols)); } - if (!daemonEntitySnapshotProjector.Contains("new AetheriaRuntimeEntitySnapshot(", StringComparison.Ordinal) || - !daemonEntitySnapshotProjector.Contains("CreateWeaponStates(runId, zoneIndex, entity.WeaponStates)", StringComparison.Ordinal) || - !daemonEntitySnapshotProjector.Contains("entity.EntityIndex,", StringComparison.Ordinal)) + if (!zoneRenderDocuments.Contains("new AetheriaRuntimeEntitySnapshot(", StringComparison.Ordinal) || + !zoneRenderDocuments.Contains("CreateWeaponStates(runId, zoneIndex, entity.WeaponStates)", StringComparison.Ordinal) || + !zoneRenderDocuments.Contains("entity.EntityIndex,", StringComparison.Ordinal)) { throw new InvalidOperationException( - "Unity observer entity projection no longer carries typed daemon entity indices through the dedicated daemon snapshot projector."); + "Unity observer entity snapshot access no longer carries typed daemon entity indices through AetheriaRuntimeZoneRenderDocument."); } - if (daemonEntitySnapshotProjector.Contains("EntityIndexFromRecordKey", StringComparison.Ordinal) || + if (zoneRenderDocuments.Contains("EntityIndexFromRecordKey", StringComparison.Ordinal) || observedFrameApplier.Contains("EntityIndexFromRecordKey", StringComparison.Ordinal) || observedEntityRestorer.Contains("EntityIndexFromRecordKey", StringComparison.Ordinal)) { @@ -14115,13 +14127,35 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) "Unity observer zone context lowering no longer flows through the dedicated observed zone context factory."); } + var requiredZoneRenderSnapshotSymbols = new[] + { + "public AetheriaRuntimeEntitySnapshot[] CreateEntitySnapshots()", + "public static AetheriaRuntimeEntitySnapshot[] CreateEntitySnapshots(", + "public static string EntityRecordKey(string runId, int zoneIndex, int entityIndex)", + "new AetheriaRuntimeEntitySnapshot(", + "CreateEntityItemSlots(entity.Equipment)", + "CreateWeaponStates(runId, zoneIndex, entity.WeaponStates)", + "CreateBehaviorStates(entity.BehaviorStates)", + "CreateCargoBays(entity.CargoContents)", + "new AetheriaRuntimeEntityContactSnapshot(" + }; + var missingZoneRenderSnapshotSymbols = requiredZoneRenderSnapshotSymbols + .Where(symbol => !zoneRenderDocuments.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + if (missingZoneRenderSnapshotSymbols.Length > 0) + { + throw new InvalidOperationException( + "Managed daemon entity snapshot access must live on AetheriaRuntimeZoneRenderDocument instead of a projector-shaped helper: " + + string.Join(", ", missingZoneRenderSnapshotSymbols)); + } + var requiredObservedFrameApplierSymbols = new[] { "public sealed class AetheriaUnityObservedFrameApplier", "public bool ApplyLatestZoneRender()", "observer.LastRenderView?.ZoneRender", "private bool TryRestoreEntityGraphFromZoneRender(", - "AetheriaRuntimeEntitySnapshotProjector.CreateSnapshots(runId, render.ZoneIndex, render.EntitySnapshots)", + "render.CreateEntitySnapshots()", ".OrderBy(entity => entity.EntityIndex)", "_entityRestorer.Replace(entitySnapshots, currentEntityKey, _getZone())", "_entityRestorer.TryApplyInPlace(", diff --git a/Assets/Scripts/Gameplay/AetheriaUnityObservedFrameApplier.cs b/Assets/Scripts/Gameplay/AetheriaUnityObservedFrameApplier.cs index 2980b2249..ab457c46b 100644 --- a/Assets/Scripts/Gameplay/AetheriaUnityObservedFrameApplier.cs +++ b/Assets/Scripts/Gameplay/AetheriaUnityObservedFrameApplier.cs @@ -90,7 +90,7 @@ public sealed class AetheriaUnityObservedFrameApplier return false; } - var entitySnapshots = AetheriaRuntimeEntitySnapshotProjector.CreateSnapshots(runId, render.ZoneIndex, render.EntitySnapshots) + var entitySnapshots = render.CreateEntitySnapshots() .OrderBy(entity => entity.EntityIndex) .ToArray(); diff --git a/GameCult.Aetheria.State.Unity.csproj b/GameCult.Aetheria.State.Unity.csproj index 0b0db4484..b9688c828 100644 --- a/GameCult.Aetheria.State.Unity.csproj +++ b/GameCult.Aetheria.State.Unity.csproj @@ -123,7 +123,6 @@ - diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEntitySnapshotProjector.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEntitySnapshotProjector.cs deleted file mode 100644 index 116bf7469..000000000 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEntitySnapshotProjector.cs +++ /dev/null @@ -1,255 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -#nullable enable - -namespace GameCult.Aetheria.State.Verse -{ - public static class AetheriaRuntimeEntitySnapshotProjector - { - public static AetheriaRuntimeEntitySnapshot[] CreateSnapshots( - string runId, - AetheriaRuntimeZoneSnapshotCommit? zone) - { - if (zone == null) - return Array.Empty(); - - return CreateSnapshots(runId, zone.ZoneIndex, zone.Entities); - } - - public static AetheriaRuntimeEntitySnapshot[] CreateSnapshots( - string runId, - int zoneIndex, - IReadOnlyList? entities) - { - return (entities ?? Array.Empty()) - .Where(entity => entity != null) - .Select(entity => CreateSnapshot(runId, zoneIndex, entity)) - .ToArray(); - } - - public static string DaemonEntityRecordKey(string runId, int zoneIndex, int entityIndex) - { - return string.IsNullOrWhiteSpace(runId) - ? "" - : $"global:aetheria.run_state.{runId}.zone.{zoneIndex}.entity.{entityIndex}.v1"; - } - - private static AetheriaRuntimeEntitySnapshot CreateSnapshot( - string runId, - int zoneIndex, - AetheriaRuntimeEntitySnapshotCommit entity) - { - return new AetheriaRuntimeEntitySnapshot( - DaemonEntityRecordKey(runId, zoneIndex, entity.EntityIndex), - entity.EntityIndex, - entity.Name ?? "", - entity.Kind ?? "", - entity.PositionX, - entity.PositionY, - entity.PositionZ, - entity.DirectionX, - entity.DirectionY, - entity.FactionKey ?? "", - entity.HullItemKey ?? "", - CreateItemSlots(entity.Equipment), - CreateItemSlots(entity.CargoBays), - CreateItemSlots(entity.DockingBays), - (entity.ChildEntityIndices ?? Array.Empty()) - .Where(index => index >= 0) - .Select(index => DaemonEntityRecordKey(runId, zoneIndex, index)) - .ToArray(), - (entity.WeaponGroups ?? Array.Empty>()) - .Select(group => (IReadOnlyList)(group ?? Array.Empty()).ToArray()) - .ToArray(), - (entity.StatGrids ?? Array.Empty()) - .Select(grid => new AetheriaRuntimeEntityStatGridSnapshot( - grid.Name ?? "", - grid.Width, - grid.Height, - (grid.Values ?? Array.Empty()).ToArray())) - .ToArray(), - entity.VelocityX, - entity.VelocityY, - entity.TargetEntityIndex < 0 ? "" : DaemonEntityRecordKey(runId, zoneIndex, entity.TargetEntityIndex), - entity.IsActive, - entity.HeatsinksEnabled, - entity.OverrideShutdown, - entity.TractorPower, - entity.Heatstroke, - entity.Hypothermia, - (entity.ActiveConsumables ?? Array.Empty()) - .Select(consumable => new AetheriaRuntimeActiveConsumableSnapshot( - consumable.ItemKey ?? "", - consumable.Quality, - consumable.RemainingDuration, - consumable.Duration)) - .ToArray(), - (entity.BehaviorProgress ?? Array.Empty()) - .Select(progress => new AetheriaRuntimeBehaviorProgressSnapshot( - progress.OwnerKind ?? "", - progress.OwnerIndex, - progress.BehaviorIndex, - progress.BehaviorKind ?? "", - progress.Progress)) - .ToArray(), - CreateWeaponStates(runId, zoneIndex, entity.WeaponStates), - CreateBehaviorStates(entity.BehaviorStates), - CreateCargoBays(entity.CargoContents), - CreateCargoBays(entity.DockingBayContents), - (entity.DockingBayAssignments ?? Array.Empty()).ToArray(), - entity.Visibility, - entity.VisibilitySourceCount, - (entity.Contacts ?? Array.Empty()) - .Where(contact => contact != null && contact.TargetEntityIndex >= 0) - .Select(contact => new AetheriaRuntimeEntityContactSnapshot( - DaemonEntityRecordKey(runId, zoneIndex, contact.TargetEntityIndex), - contact.InfoGathered, - contact.Hostile, - contact.Visible)) - .ToArray(), - entity.ShutdownPerformance); - } - - private static AetheriaRuntimeEntityItemSlotSnapshot[] CreateItemSlots( - IReadOnlyList? slots) - { - return (slots ?? Array.Empty()) - .Where(slot => slot?.Item != null) - .Select(slot => new AetheriaRuntimeEntityItemSlotSnapshot( - slot.X, - slot.Y, - slot.Item!.ItemKey ?? "", - slot.Item.Quality, - slot.Item.Durability, - slot.Item.Quantity, - slot.Item.Enabled, - slot.Item.OverrideShutdown, - slot.Item.Temperature)) - .ToArray(); - } - - private static AetheriaRuntimeCargoBayLoadoutSnapshot[] CreateCargoBays( - IReadOnlyList? cargoBays) - { - return (cargoBays ?? Array.Empty()) - .Select(bay => new AetheriaRuntimeCargoBayLoadoutSnapshot( - CreateLoadoutSlots(bay?.Items))) - .ToArray(); - } - - private static AetheriaRuntimeLoadoutItemSlotSnapshot[] CreateLoadoutSlots( - IReadOnlyList? slots) - { - return (slots ?? Array.Empty()) - .Where(slot => slot?.Item != null) - .Select(slot => new AetheriaRuntimeLoadoutItemSlotSnapshot( - slot.X, - slot.Y, - new AetheriaRuntimeLoadoutItemSnapshot( - slot.Item!.ItemKey ?? "", - slot.Item.Quality, - slot.Item.Durability, - slot.Item.Quantity, - slot.Item.Enabled, - slot.Item.OverrideShutdown, - slot.Item.Temperature))) - .ToArray(); - } - - private static AetheriaRuntimeWeaponStateSnapshot[] CreateWeaponStates( - string runId, - int zoneIndex, - IReadOnlyList? weaponStates) - { - return (weaponStates ?? Array.Empty()) - .Where(state => state != null) - .Select(state => new AetheriaRuntimeWeaponStateSnapshot( - state.OwnerKind ?? "", - state.OwnerIndex, - state.BehaviorIndex, - state.BehaviorKind ?? "", - state.Firing, - state.Ammo, - state.BurstRemaining, - state.BurstTimer, - state.BurstInterval, - state.CooldownProgress, - state.CoolingDown, - state.Charging, - state.Charged, - state.Charge, - state.Reloading, - state.ReloadProgress, - state.AmmoIntervalProgress, - state.LockProgress, - state.LockTargetEntityIndex < 0 ? "" : DaemonEntityRecordKey(runId, zoneIndex, state.LockTargetEntityIndex))) - .ToArray(); - } - - private static AetheriaRuntimeBehaviorStateSnapshot[] CreateBehaviorStates( - IReadOnlyList? behaviorStates) - { - return (behaviorStates ?? Array.Empty()) - .Where(state => state != null) - .Select(state => new AetheriaRuntimeBehaviorStateSnapshot( - state.OwnerKind ?? "", - state.OwnerIndex, - state.BehaviorIndex, - state.BehaviorKind ?? "", - state.Pinging, - state.PingCooldown, - state.PingLerp, - state.PingRadius, - state.PingedEntityCount, - state.RadiatorTemperature, - state.Emissivity, - state.PumpedHeat, - state.WasteHeat, - state.EnergyUsage, - state.ReactorDraw, - state.ReactorLoadRatio, - state.CapacitorCharge, - state.CapacitorCapacity, - state.CapacitorEfficiency, - state.AetherDriveAxisX, - state.AetherDriveAxisY, - state.AetherDriveAxisZ, - state.AetherDriveThrustX, - state.AetherDriveThrustY, - state.AetherDriveThrustZ, - state.AetherDriveRpmX, - state.AetherDriveRpmY, - state.AetherDriveRpmZ, - state.AetherDriveMaximumRpm, - state.AetherDriveThrustDirectionX, - state.AetherDriveThrustDirectionY, - state.ResourceScannerTargetBodyKey ?? "", - state.ResourceScannerAsteroidIndex, - state.ResourceScannerScanTime, - state.ResourceScannerRange, - state.ResourceScannerMinimumDensity, - state.ResourceScannerScanDuration, - state.MiningToolAsteroidBeltKey ?? "", - state.MiningToolAsteroidIndex, - state.MiningToolRange, - state.ThrusterAxis, - state.ThrusterThrust, - state.ThrusterTorque, - state.ShieldEfficiency, - state.ShieldEnergyUsage, - state.VelocityLimit, - state.ThermotoggleTargetTemperature, - state.SwitchActivated, - state.TriggerPulled, - state.StatModifierApplied, - state.StatModifierExecuted, - state.StatModifierTargetStatCount, - state.TurretControllerWeaponCount, - state.TurretControllerShotSpeed, - state.TurretControllerPredictShots)) - .ToArray(); - } - } -} diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEntitySnapshotProjector.cs.meta b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEntitySnapshotProjector.cs.meta deleted file mode 100644 index d01e8c8a1..000000000 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEntitySnapshotProjector.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 1625cd76fccf6284bb9d97aa5b755a59 \ No newline at end of file diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs index 9a156254a..6d24030fc 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsViewportDocuments.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using GameCult.Caching; using MessagePack; @@ -879,6 +880,255 @@ namespace GameCult.Aetheria.State.Verse public IReadOnlyList Bodies { get; set; } = Array.Empty(); + public static string EntityRecordKey(string runId, int zoneIndex, int entityIndex) + { + return string.IsNullOrWhiteSpace(runId) + ? "" + : $"global:aetheria.run_state.{runId}.zone.{zoneIndex}.entity.{entityIndex}.v1"; + } + + public AetheriaRuntimeEntitySnapshot[] CreateEntitySnapshots() + { + return CreateEntitySnapshots(RunId, ZoneIndex, EntitySnapshots); + } + + public static AetheriaRuntimeEntitySnapshot[] CreateEntitySnapshots( + string runId, + AetheriaRuntimeZoneSnapshotCommit? zone) + { + if (zone == null) + return Array.Empty(); + + return CreateEntitySnapshots(runId, zone.ZoneIndex, zone.Entities); + } + + public static AetheriaRuntimeEntitySnapshot[] CreateEntitySnapshots( + string runId, + int zoneIndex, + IReadOnlyList? entities) + { + return (entities ?? Array.Empty()) + .Where(entity => entity != null) + .Select(entity => CreateEntitySnapshot(runId, zoneIndex, entity)) + .ToArray(); + } + + private static AetheriaRuntimeEntitySnapshot CreateEntitySnapshot( + string runId, + int zoneIndex, + AetheriaRuntimeEntitySnapshotCommit entity) + { + return new AetheriaRuntimeEntitySnapshot( + EntityRecordKey(runId, zoneIndex, entity.EntityIndex), + entity.EntityIndex, + entity.Name ?? "", + entity.Kind ?? "", + entity.PositionX, + entity.PositionY, + entity.PositionZ, + entity.DirectionX, + entity.DirectionY, + entity.FactionKey ?? "", + entity.HullItemKey ?? "", + CreateEntityItemSlots(entity.Equipment), + CreateEntityItemSlots(entity.CargoBays), + CreateEntityItemSlots(entity.DockingBays), + (entity.ChildEntityIndices ?? Array.Empty()) + .Where(index => index >= 0) + .Select(index => EntityRecordKey(runId, zoneIndex, index)) + .ToArray(), + (entity.WeaponGroups ?? Array.Empty>()) + .Select(group => (IReadOnlyList)(group ?? Array.Empty()).ToArray()) + .ToArray(), + (entity.StatGrids ?? Array.Empty()) + .Select(grid => new AetheriaRuntimeEntityStatGridSnapshot( + grid.Name ?? "", + grid.Width, + grid.Height, + (grid.Values ?? Array.Empty()).ToArray())) + .ToArray(), + entity.VelocityX, + entity.VelocityY, + entity.TargetEntityIndex < 0 ? "" : EntityRecordKey(runId, zoneIndex, entity.TargetEntityIndex), + entity.IsActive, + entity.HeatsinksEnabled, + entity.OverrideShutdown, + entity.TractorPower, + entity.Heatstroke, + entity.Hypothermia, + (entity.ActiveConsumables ?? Array.Empty()) + .Select(consumable => new AetheriaRuntimeActiveConsumableSnapshot( + consumable.ItemKey ?? "", + consumable.Quality, + consumable.RemainingDuration, + consumable.Duration)) + .ToArray(), + (entity.BehaviorProgress ?? Array.Empty()) + .Select(progress => new AetheriaRuntimeBehaviorProgressSnapshot( + progress.OwnerKind ?? "", + progress.OwnerIndex, + progress.BehaviorIndex, + progress.BehaviorKind ?? "", + progress.Progress)) + .ToArray(), + CreateWeaponStates(runId, zoneIndex, entity.WeaponStates), + CreateBehaviorStates(entity.BehaviorStates), + CreateCargoBays(entity.CargoContents), + CreateCargoBays(entity.DockingBayContents), + (entity.DockingBayAssignments ?? Array.Empty()).ToArray(), + entity.Visibility, + entity.VisibilitySourceCount, + (entity.Contacts ?? Array.Empty()) + .Where(contact => contact != null && contact.TargetEntityIndex >= 0) + .Select(contact => new AetheriaRuntimeEntityContactSnapshot( + EntityRecordKey(runId, zoneIndex, contact.TargetEntityIndex), + contact.InfoGathered, + contact.Hostile, + contact.Visible)) + .ToArray(), + entity.ShutdownPerformance); + } + + private static AetheriaRuntimeEntityItemSlotSnapshot[] CreateEntityItemSlots( + IReadOnlyList? slots) + { + return (slots ?? Array.Empty()) + .Where(slot => slot?.Item != null) + .Select(slot => new AetheriaRuntimeEntityItemSlotSnapshot( + slot.X, + slot.Y, + slot.Item!.ItemKey ?? "", + slot.Item.Quality, + slot.Item.Durability, + slot.Item.Quantity, + slot.Item.Enabled, + slot.Item.OverrideShutdown, + slot.Item.Temperature)) + .ToArray(); + } + + private static AetheriaRuntimeCargoBayLoadoutSnapshot[] CreateCargoBays( + IReadOnlyList? cargoBays) + { + return (cargoBays ?? Array.Empty()) + .Select(bay => new AetheriaRuntimeCargoBayLoadoutSnapshot( + CreateLoadoutSlots(bay?.Items))) + .ToArray(); + } + + private static AetheriaRuntimeLoadoutItemSlotSnapshot[] CreateLoadoutSlots( + IReadOnlyList? slots) + { + return (slots ?? Array.Empty()) + .Where(slot => slot?.Item != null) + .Select(slot => new AetheriaRuntimeLoadoutItemSlotSnapshot( + slot.X, + slot.Y, + new AetheriaRuntimeLoadoutItemSnapshot( + slot.Item!.ItemKey ?? "", + slot.Item.Quality, + slot.Item.Durability, + slot.Item.Quantity, + slot.Item.Enabled, + slot.Item.OverrideShutdown, + slot.Item.Temperature))) + .ToArray(); + } + + private static AetheriaRuntimeWeaponStateSnapshot[] CreateWeaponStates( + string runId, + int zoneIndex, + IReadOnlyList? weaponStates) + { + return (weaponStates ?? Array.Empty()) + .Where(state => state != null) + .Select(state => new AetheriaRuntimeWeaponStateSnapshot( + state.OwnerKind ?? "", + state.OwnerIndex, + state.BehaviorIndex, + state.BehaviorKind ?? "", + state.Firing, + state.Ammo, + state.BurstRemaining, + state.BurstTimer, + state.BurstInterval, + state.CooldownProgress, + state.CoolingDown, + state.Charging, + state.Charged, + state.Charge, + state.Reloading, + state.ReloadProgress, + state.AmmoIntervalProgress, + state.LockProgress, + state.LockTargetEntityIndex < 0 ? "" : EntityRecordKey(runId, zoneIndex, state.LockTargetEntityIndex))) + .ToArray(); + } + + private static AetheriaRuntimeBehaviorStateSnapshot[] CreateBehaviorStates( + IReadOnlyList? behaviorStates) + { + return (behaviorStates ?? Array.Empty()) + .Where(state => state != null) + .Select(state => new AetheriaRuntimeBehaviorStateSnapshot( + state.OwnerKind ?? "", + state.OwnerIndex, + state.BehaviorIndex, + state.BehaviorKind ?? "", + state.Pinging, + state.PingCooldown, + state.PingLerp, + state.PingRadius, + state.PingedEntityCount, + state.RadiatorTemperature, + state.Emissivity, + state.PumpedHeat, + state.WasteHeat, + state.EnergyUsage, + state.ReactorDraw, + state.ReactorLoadRatio, + state.CapacitorCharge, + state.CapacitorCapacity, + state.CapacitorEfficiency, + state.AetherDriveAxisX, + state.AetherDriveAxisY, + state.AetherDriveAxisZ, + state.AetherDriveThrustX, + state.AetherDriveThrustY, + state.AetherDriveThrustZ, + state.AetherDriveRpmX, + state.AetherDriveRpmY, + state.AetherDriveRpmZ, + state.AetherDriveMaximumRpm, + state.AetherDriveThrustDirectionX, + state.AetherDriveThrustDirectionY, + state.ResourceScannerTargetBodyKey ?? "", + state.ResourceScannerAsteroidIndex, + state.ResourceScannerScanTime, + state.ResourceScannerRange, + state.ResourceScannerMinimumDensity, + state.ResourceScannerScanDuration, + state.MiningToolAsteroidBeltKey ?? "", + state.MiningToolAsteroidIndex, + state.MiningToolRange, + state.ThrusterAxis, + state.ThrusterThrust, + state.ThrusterTorque, + state.ShieldEfficiency, + state.ShieldEnergyUsage, + state.VelocityLimit, + state.ThermotoggleTargetTemperature, + state.SwitchActivated, + state.TriggerPulled, + state.StatModifierApplied, + state.StatModifierExecuted, + state.StatModifierTargetStatCount, + state.TurretControllerWeaponCount, + state.TurretControllerShotSpeed, + state.TurretControllerPredictShots)) + .ToArray(); + } + } [MessagePackObject]