diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 58050242d..5de02a6b1 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Aetheria.State.Unity.Smoke/Program.cs b/Aetheria.State.Unity.Smoke/Program.cs index 865b1104a..60875a3df 100644 --- a/Aetheria.State.Unity.Smoke/Program.cs +++ b/Aetheria.State.Unity.Smoke/Program.cs @@ -580,6 +580,9 @@ try var packageSettings = AetheriaRuntimeCatalogStore.ReadPlayerSettings(commitSmokeStatePath); var packageLoadouts = AetheriaRuntimeCatalogStore.ReadLoadoutTemplates(commitSmokeStatePath); + var packageRuns = AetheriaRuntimeCatalogStore.ReadRunStates(commitSmokeStatePath); + var packageZones = AetheriaRuntimeCatalogStore.ReadZoneStates(commitSmokeStatePath); + var packageEntities = AetheriaRuntimeCatalogStore.ReadEntitySnapshots(commitSmokeStatePath); if (!typedNodeApplied || packageSettings?.PlayerName != "Unity smoke" || packageSettings.TutorialPassed != true || @@ -587,11 +590,40 @@ try packageLoadouts.Count != 1 || packageLoadouts[0].RootEntity.Hull.ItemKey != "aetheria.item_definition:legacy:smoke:hull" || packageLoadouts[0].RootEntity.Equipment[0].X != 1 || - packageLoadouts[0].RootEntity.CargoContents[0].Items[0].Item.Quantity != 5) + packageLoadouts[0].RootEntity.CargoContents[0].Items[0].Item.Quantity != 5 || + packageRuns.Count != 1 || + packageRuns[0].RunId != "smoke-run" || + packageRuns[0].ZoneKeys.Count != 1 || + packageRuns[0].ActionBarBindings.Count != 2 || + packageRuns[0].ActionBarBindings[1].TargetKey != "aetheria.item_definition:legacy:smoke:weapon" || + packageRuns[0].FactionRelationships.Count != 1 || + packageRuns[0].FactionRelationships[0].FactionKey != "aetheria.corporation:legacy:smoke:faction" || + packageZones.Count != 1 || + packageZones[0].Name != "Unity Smoke Zone" || + packageZones[0].EntityKeys.Count != 1 || + packageZones[0].Orbits.Count != 1 || + packageZones[0].Orbits[0].FixedPositionX != 5.0 || + packageZones[0].Bodies.Count != 1 || + packageZones[0].Bodies[0].ResourceCount != 1 || + packageZones[0].Bodies[0].AsteroidCount != 1 || + packageEntities.Count != 1 || + packageEntities[0].Name != "Unity Smoke Ship" || + packageEntities[0].VelocityX != 4.0 || + packageEntities[0].VelocityY != -2.0 || + packageEntities[0].TargetEntityKey != "global:aetheria.run_state.smoke-run.zone.0.entity.0.v1" || + packageEntities[0].ActiveConsumables.Count != 1 || + packageEntities[0].ActiveConsumables[0].ItemKey != "aetheria.item_definition:legacy:smoke:consumable" || + packageEntities[0].BehaviorProgress.Count != 2 || + packageEntities[0].WeaponStates.Count != 3 || + packageEntities[0].WeaponStates[2].AmmoIntervalProgress != 0.6 || + packageEntities[0].BehaviorStates.Count != 4 || + packageEntities[0].BehaviorStates[3].CapacitorCharge != 7.5 || + packageEntities[0].StatGrids.Count != 2 || + packageEntities[0].StatGrids.All(grid => grid.Name != "temperature" || grid.Values.Count != 2 || grid.Values[1] != 281.0)) { throw new InvalidOperationException( "Runtime state commit log did not apply queued settings/loadout/run snapshots through the typed state node. " + - $"typedNodeApplied={typedNodeApplied}, packageSettings={(packageSettings == null ? "null" : $"{packageSettings.PlayerName}/{packageSettings.TutorialPassed}/{packageSettings.ActionBarInputs.Count}")}, packageLoadouts={packageLoadouts.Count}"); + $"typedNodeApplied={typedNodeApplied}, packageSettings={(packageSettings == null ? "null" : $"{packageSettings.PlayerName}/{packageSettings.TutorialPassed}/{packageSettings.ActionBarInputs.Count}")}, packageLoadouts={packageLoadouts.Count}, packageRuns={packageRuns.Count}, packageZones={packageZones.Count}, packageEntities={packageEntities.Count}"); } var eveCommand = AetheriaRuntimeEveCommandLog.QueueCommand( diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs index 75879f5fb..4aead2bad 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs @@ -673,4 +673,436 @@ namespace GameCult.Aetheria.State.Unity public IReadOnlyList Items { get; } } + + public sealed class AetheriaRuntimeRunStateSnapshot + { + public AetheriaRuntimeRunStateSnapshot( + string runId, + bool isTutorial, + int entranceZoneIndex, + int exitZoneIndex, + int currentZoneIndex, + int currentZoneEntityIndex, + IReadOnlyList discoveredZoneIndices, + IReadOnlyList zoneKeys, + IReadOnlyList actionBarBindings, + IReadOnlyList factionRelationships, + string updatedAtUtc) + { + RunId = runId; + IsTutorial = isTutorial; + EntranceZoneIndex = entranceZoneIndex; + ExitZoneIndex = exitZoneIndex; + CurrentZoneIndex = currentZoneIndex; + CurrentZoneEntityIndex = currentZoneEntityIndex; + DiscoveredZoneIndices = discoveredZoneIndices; + ZoneKeys = zoneKeys; + ActionBarBindings = actionBarBindings; + FactionRelationships = factionRelationships; + UpdatedAtUtc = updatedAtUtc; + } + + public string RunId { get; } + public bool IsTutorial { get; } + public int EntranceZoneIndex { get; } + public int ExitZoneIndex { get; } + public int CurrentZoneIndex { get; } + public int CurrentZoneEntityIndex { get; } + public IReadOnlyList DiscoveredZoneIndices { get; } + public IReadOnlyList ZoneKeys { get; } + public IReadOnlyList ActionBarBindings { get; } + public IReadOnlyList FactionRelationships { get; } + public string UpdatedAtUtc { get; } + } + + public sealed class AetheriaRuntimeZoneStateSnapshot + { + public AetheriaRuntimeZoneStateSnapshot( + string name, + double positionX, + double positionY, + IReadOnlyList adjacentZoneIndices, + IReadOnlyList factionIndices, + int ownerFactionIndex, + IReadOnlyList entityKeys, + IReadOnlyList orbits, + IReadOnlyList bodies) + { + Name = name; + PositionX = positionX; + PositionY = positionY; + AdjacentZoneIndices = adjacentZoneIndices; + FactionIndices = factionIndices; + OwnerFactionIndex = ownerFactionIndex; + EntityKeys = entityKeys; + Orbits = orbits; + Bodies = bodies; + } + + public string Name { get; } + public double PositionX { get; } + public double PositionY { get; } + public IReadOnlyList AdjacentZoneIndices { get; } + public IReadOnlyList FactionIndices { get; } + public int OwnerFactionIndex { get; } + public IReadOnlyList EntityKeys { get; } + public IReadOnlyList Orbits { get; } + public IReadOnlyList Bodies { get; } + } + + public sealed class AetheriaRuntimeEntitySnapshot + { + public AetheriaRuntimeEntitySnapshot( + string name, + string kind, + double positionX, + double positionY, + double positionZ, + double directionX, + double directionY, + string factionKey, + string hullItemKey, + IReadOnlyList equipment, + IReadOnlyList cargoBays, + IReadOnlyList dockingBays, + IReadOnlyList childEntityKeys, + IReadOnlyList> weaponGroups, + IReadOnlyList statGrids, + double velocityX, + double velocityY, + string targetEntityKey, + bool isActive, + bool heatsinksEnabled, + bool overrideShutdown, + double tractorPower, + double heatstroke, + double hypothermia, + IReadOnlyList activeConsumables, + IReadOnlyList behaviorProgress, + IReadOnlyList weaponStates, + IReadOnlyList behaviorStates) + { + Name = name; + Kind = kind; + PositionX = positionX; + PositionY = positionY; + PositionZ = positionZ; + DirectionX = directionX; + DirectionY = directionY; + FactionKey = factionKey; + HullItemKey = hullItemKey; + Equipment = equipment; + CargoBays = cargoBays; + DockingBays = dockingBays; + ChildEntityKeys = childEntityKeys; + WeaponGroups = weaponGroups; + StatGrids = statGrids; + VelocityX = velocityX; + VelocityY = velocityY; + TargetEntityKey = targetEntityKey; + IsActive = isActive; + HeatsinksEnabled = heatsinksEnabled; + OverrideShutdown = overrideShutdown; + TractorPower = tractorPower; + Heatstroke = heatstroke; + Hypothermia = hypothermia; + ActiveConsumables = activeConsumables; + BehaviorProgress = behaviorProgress; + WeaponStates = weaponStates; + BehaviorStates = behaviorStates; + } + + public string Name { get; } + public string Kind { get; } + public double PositionX { get; } + public double PositionY { get; } + public double PositionZ { get; } + public double DirectionX { get; } + public double DirectionY { get; } + public string FactionKey { get; } + public string HullItemKey { get; } + public IReadOnlyList Equipment { get; } + public IReadOnlyList CargoBays { get; } + public IReadOnlyList DockingBays { get; } + public IReadOnlyList ChildEntityKeys { get; } + public IReadOnlyList> WeaponGroups { get; } + public IReadOnlyList StatGrids { get; } + public double VelocityX { get; } + public double VelocityY { get; } + public string TargetEntityKey { get; } + public bool IsActive { get; } + public bool HeatsinksEnabled { get; } + public bool OverrideShutdown { get; } + public double TractorPower { get; } + public double Heatstroke { get; } + public double Hypothermia { get; } + public IReadOnlyList ActiveConsumables { get; } + public IReadOnlyList BehaviorProgress { get; } + public IReadOnlyList WeaponStates { get; } + public IReadOnlyList BehaviorStates { get; } + } + + public sealed class AetheriaRuntimeActionBarBindingSnapshot + { + public AetheriaRuntimeActionBarBindingSnapshot(string controlPath, string kind, string targetKey, int equipmentIndex, int behaviorIndex, int weaponGroup) + { + ControlPath = controlPath; + Kind = kind; + TargetKey = targetKey; + EquipmentIndex = equipmentIndex; + BehaviorIndex = behaviorIndex; + WeaponGroup = weaponGroup; + } + + public string ControlPath { get; } + public string Kind { get; } + public string TargetKey { get; } + public int EquipmentIndex { get; } + public int BehaviorIndex { get; } + public int WeaponGroup { get; } + } + + public sealed class AetheriaRuntimeFactionRelationshipSnapshot + { + public AetheriaRuntimeFactionRelationshipSnapshot(string factionKey, string relationship, double standing) + { + FactionKey = factionKey; + Relationship = relationship; + Standing = standing; + } + + public string FactionKey { get; } + public string Relationship { get; } + public double Standing { get; } + } + + public sealed class AetheriaRuntimeOrbitSnapshot + { + public AetheriaRuntimeOrbitSnapshot(string orbitId, string parentId, double distance, double phase, double fixedPositionX, double fixedPositionY) + { + OrbitId = orbitId; + ParentId = parentId; + Distance = distance; + Phase = phase; + FixedPositionX = fixedPositionX; + FixedPositionY = fixedPositionY; + } + + public string OrbitId { get; } + public string ParentId { get; } + public double Distance { get; } + public double Phase { get; } + public double FixedPositionX { get; } + public double FixedPositionY { get; } + } + + public sealed class AetheriaRuntimeBodySnapshot + { + public AetheriaRuntimeBodySnapshot(string bodyId, string kind, string name, string orbitId, double mass, int resourceCount, int asteroidCount) + { + BodyId = bodyId; + Kind = kind; + Name = name; + OrbitId = orbitId; + Mass = mass; + ResourceCount = resourceCount; + AsteroidCount = asteroidCount; + } + + public string BodyId { get; } + public string Kind { get; } + public string Name { get; } + public string OrbitId { get; } + public double Mass { get; } + public int ResourceCount { get; } + public int AsteroidCount { get; } + } + + public sealed class AetheriaRuntimeEntityItemSlotSnapshot + { + public AetheriaRuntimeEntityItemSlotSnapshot(int x, int y, string itemKey) + { + X = x; + Y = y; + ItemKey = itemKey; + } + + public int X { get; } + public int Y { get; } + public string ItemKey { get; } + } + + public sealed class AetheriaRuntimeEntityStatGridSnapshot + { + public AetheriaRuntimeEntityStatGridSnapshot(string name, int width, int height, IReadOnlyList values) + { + Name = name; + Width = width; + Height = height; + Values = values; + } + + public string Name { get; } + public int Width { get; } + public int Height { get; } + public IReadOnlyList Values { get; } + } + + public sealed class AetheriaRuntimeActiveConsumableSnapshot + { + public AetheriaRuntimeActiveConsumableSnapshot(string itemKey, double quality, double remainingDuration, double duration) + { + ItemKey = itemKey; + Quality = quality; + RemainingDuration = remainingDuration; + Duration = duration; + } + + public string ItemKey { get; } + public double Quality { get; } + public double RemainingDuration { get; } + public double Duration { get; } + } + + public sealed class AetheriaRuntimeBehaviorProgressSnapshot + { + public AetheriaRuntimeBehaviorProgressSnapshot(string ownerKind, int ownerIndex, int behaviorIndex, string behaviorKind, double progress) + { + OwnerKind = ownerKind; + OwnerIndex = ownerIndex; + BehaviorIndex = behaviorIndex; + BehaviorKind = behaviorKind; + Progress = progress; + } + + public string OwnerKind { get; } + public int OwnerIndex { get; } + public int BehaviorIndex { get; } + public string BehaviorKind { get; } + public double Progress { get; } + } + + public sealed class AetheriaRuntimeWeaponStateSnapshot + { + public AetheriaRuntimeWeaponStateSnapshot( + string ownerKind, + int ownerIndex, + int behaviorIndex, + string behaviorKind, + bool firing, + int ammo, + int burstRemaining, + double burstTimer, + double burstInterval, + double cooldownProgress, + bool coolingDown, + bool charging, + bool charged, + double charge, + bool reloading, + double reloadProgress, + double ammoIntervalProgress) + { + OwnerKind = ownerKind; + OwnerIndex = ownerIndex; + BehaviorIndex = behaviorIndex; + BehaviorKind = behaviorKind; + Firing = firing; + Ammo = ammo; + BurstRemaining = burstRemaining; + BurstTimer = burstTimer; + BurstInterval = burstInterval; + CooldownProgress = cooldownProgress; + CoolingDown = coolingDown; + Charging = charging; + Charged = charged; + Charge = charge; + Reloading = reloading; + ReloadProgress = reloadProgress; + AmmoIntervalProgress = ammoIntervalProgress; + } + + public string OwnerKind { get; } + public int OwnerIndex { get; } + public int BehaviorIndex { get; } + public string BehaviorKind { get; } + public bool Firing { get; } + public int Ammo { get; } + public int BurstRemaining { get; } + public double BurstTimer { get; } + public double BurstInterval { get; } + public double CooldownProgress { get; } + public bool CoolingDown { get; } + public bool Charging { get; } + public bool Charged { get; } + public double Charge { get; } + public bool Reloading { get; } + public double ReloadProgress { get; } + public double AmmoIntervalProgress { get; } + } + + public sealed class AetheriaRuntimeBehaviorStateSnapshot + { + public AetheriaRuntimeBehaviorStateSnapshot( + string ownerKind, + int ownerIndex, + int behaviorIndex, + string behaviorKind, + bool pinging, + double pingCooldown, + double pingLerp, + double pingRadius, + int pingedEntityCount, + double radiatorTemperature, + double emissivity, + double pumpedHeat, + double wasteHeat, + double energyUsage, + double reactorDraw, + double reactorLoadRatio, + double capacitorCharge, + double capacitorCapacity, + double capacitorEfficiency) + { + OwnerKind = ownerKind; + OwnerIndex = ownerIndex; + BehaviorIndex = behaviorIndex; + BehaviorKind = behaviorKind; + Pinging = pinging; + PingCooldown = pingCooldown; + PingLerp = pingLerp; + PingRadius = pingRadius; + PingedEntityCount = pingedEntityCount; + RadiatorTemperature = radiatorTemperature; + Emissivity = emissivity; + PumpedHeat = pumpedHeat; + WasteHeat = wasteHeat; + EnergyUsage = energyUsage; + ReactorDraw = reactorDraw; + ReactorLoadRatio = reactorLoadRatio; + CapacitorCharge = capacitorCharge; + CapacitorCapacity = capacitorCapacity; + CapacitorEfficiency = capacitorEfficiency; + } + + public string OwnerKind { get; } + public int OwnerIndex { get; } + public int BehaviorIndex { get; } + public string BehaviorKind { get; } + public bool Pinging { get; } + public double PingCooldown { get; } + public double PingLerp { get; } + public double PingRadius { get; } + public int PingedEntityCount { get; } + public double RadiatorTemperature { get; } + public double Emissivity { get; } + public double PumpedHeat { get; } + public double WasteHeat { get; } + public double EnergyUsage { get; } + public double ReactorDraw { get; } + public double ReactorLoadRatio { get; } + public double CapacitorCharge { get; } + public double CapacitorCapacity { get; } + public double CapacitorEfficiency { get; } + } } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs index faf3227ec..d4257595c 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs @@ -18,6 +18,9 @@ namespace GameCult.Aetheria.State.Unity private const string EveSurfaceSchema = "gamecult.eve.surface"; private const string PlayerSettingsSchema = "aetheria.player_settings"; private const string LoadoutTemplateSchema = "aetheria.loadout_template"; + private const string RunStateSchema = "aetheria.run_state"; + private const string ZoneStateSchema = "aetheria.zone_state"; + private const string EntitySnapshotSchema = "aetheria.entity_snapshot"; private const string PlayerSettingsKey = "global:aetheria.player_settings.v1"; public static AetheriaRuntimeCatalogSnapshot OpenReadOnly(string stateFilePath) @@ -108,6 +111,63 @@ namespace GameCult.Aetheria.State.Unity .ToArray(); } + public static IReadOnlyList ReadRunStates(string stateFilePath) + { + if (string.IsNullOrWhiteSpace(stateFilePath)) + throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath)); + + var catalog = ReadSchemaCatalog(stateFilePath); + var runs = new List(); + foreach (var record in ReadRecords(stateFilePath)) + { + if (!catalog.TryGetValue(record.SchemaId, out var schemaName) || + schemaName != RunStateSchema) + continue; + + runs.Add(ReadRunStatePayload(record.Payload)); + } + + return runs.OrderBy(run => run.RunId, StringComparer.Ordinal).ToArray(); + } + + public static IReadOnlyList ReadZoneStates(string stateFilePath) + { + if (string.IsNullOrWhiteSpace(stateFilePath)) + throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath)); + + var catalog = ReadSchemaCatalog(stateFilePath); + var zones = new List(); + foreach (var record in ReadRecords(stateFilePath)) + { + if (!catalog.TryGetValue(record.SchemaId, out var schemaName) || + schemaName != ZoneStateSchema) + continue; + + zones.Add(ReadZoneStatePayload(record.Payload)); + } + + return zones.OrderBy(zone => zone.Name, StringComparer.Ordinal).ToArray(); + } + + public static IReadOnlyList ReadEntitySnapshots(string stateFilePath) + { + if (string.IsNullOrWhiteSpace(stateFilePath)) + throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath)); + + var catalog = ReadSchemaCatalog(stateFilePath); + var entities = new List(); + foreach (var record in ReadRecords(stateFilePath)) + { + if (!catalog.TryGetValue(record.SchemaId, out var schemaName) || + schemaName != EntitySnapshotSchema) + continue; + + entities.Add(ReadEntitySnapshotPayload(record.Payload)); + } + + return entities.OrderBy(entity => entity.Name, StringComparer.Ordinal).ToArray(); + } + private static Dictionary ReadSchemaCatalog(string stateFilePath) { if (!File.Exists(stateFilePath)) @@ -387,6 +447,440 @@ namespace GameCult.Aetheria.State.Unity updatedAtUtc); } + private static AetheriaRuntimeRunStateSnapshot ReadRunStatePayload(byte[] payload) + { + var reader = new MessagePackReader(payload); + var fields = reader.ReadArrayHeader(); + var runId = ReadFieldString(ref reader, fields, 0); + var isTutorial = ReadFieldBool(ref reader, fields, 1); + var entranceZoneIndex = ReadFieldInt32(ref reader, fields, 2); + var exitZoneIndex = ReadFieldInt32(ref reader, fields, 3); + var currentZoneIndex = ReadFieldInt32(ref reader, fields, 4); + var currentZoneEntityIndex = ReadFieldInt32(ref reader, fields, 5); + var discoveredZoneIndices = ReadFieldInt32Array(ref reader, fields, 6); + var zoneKeys = ReadFieldStringArray(ref reader, fields, 7); + var actionBarBindings = ReadFieldActionBarBindings(ref reader, fields, 8); + var factionRelationships = ReadFieldFactionRelationships(ref reader, fields, 9); + var updatedAtUtc = ReadFieldString(ref reader, fields, 10); + SkipRemaining(ref reader, fields, 11); + return new AetheriaRuntimeRunStateSnapshot( + runId, + isTutorial, + entranceZoneIndex, + exitZoneIndex, + currentZoneIndex, + currentZoneEntityIndex, + discoveredZoneIndices, + zoneKeys, + actionBarBindings, + factionRelationships, + updatedAtUtc); + } + + private static AetheriaRuntimeZoneStateSnapshot ReadZoneStatePayload(byte[] payload) + { + var reader = new MessagePackReader(payload); + var fields = reader.ReadArrayHeader(); + var name = ReadFieldString(ref reader, fields, 0); + var position = ReadFieldVector2(ref reader, fields, 1); + var adjacentZoneIndices = ReadFieldInt32Array(ref reader, fields, 2); + var factionIndices = ReadFieldInt32Array(ref reader, fields, 3); + var ownerFactionIndex = ReadFieldInt32(ref reader, fields, 4); + var entityKeys = ReadFieldStringArray(ref reader, fields, 5); + var orbits = ReadFieldOrbitSnapshots(ref reader, fields, 6); + var bodies = ReadFieldBodySnapshots(ref reader, fields, 7); + SkipRemaining(ref reader, fields, 8); + return new AetheriaRuntimeZoneStateSnapshot( + name, + position.X, + position.Y, + adjacentZoneIndices, + factionIndices, + ownerFactionIndex, + entityKeys, + orbits, + bodies); + } + + private static AetheriaRuntimeEntitySnapshot ReadEntitySnapshotPayload(byte[] payload) + { + var reader = new MessagePackReader(payload); + var fields = reader.ReadArrayHeader(); + var name = ReadFieldString(ref reader, fields, 0); + var kind = ReadFieldString(ref reader, fields, 1); + var position = ReadFieldVector3(ref reader, fields, 2); + var direction = ReadFieldVector2(ref reader, fields, 3); + var factionKey = ReadFieldString(ref reader, fields, 4); + var hullItemKey = ReadFieldString(ref reader, fields, 5); + var equipment = ReadFieldEntityItemSlots(ref reader, fields, 6); + var cargoBays = ReadFieldEntityItemSlots(ref reader, fields, 7); + var dockingBays = ReadFieldEntityItemSlots(ref reader, fields, 8); + var childEntityKeys = ReadFieldStringArray(ref reader, fields, 9); + var weaponGroups = ReadFieldEntityWeaponGroups(ref reader, fields, 10); + var statGrids = ReadFieldEntityStatGrids(ref reader, fields, 11); + var velocity = ReadFieldVector2(ref reader, fields, 12); + var targetEntityKey = ReadFieldString(ref reader, fields, 13); + var isActive = ReadFieldBool(ref reader, fields, 14); + var heatsinksEnabled = ReadFieldBool(ref reader, fields, 15); + var overrideShutdown = ReadFieldBool(ref reader, fields, 16); + var tractorPower = ReadFieldDouble(ref reader, fields, 17); + var heatstroke = ReadFieldDouble(ref reader, fields, 18); + var hypothermia = ReadFieldDouble(ref reader, fields, 19); + var activeConsumables = ReadFieldActiveConsumables(ref reader, fields, 20); + var behaviorProgress = ReadFieldBehaviorProgress(ref reader, fields, 21); + var weaponStates = ReadFieldWeaponStates(ref reader, fields, 22); + var behaviorStates = ReadFieldBehaviorStates(ref reader, fields, 23); + SkipRemaining(ref reader, fields, 24); + return new AetheriaRuntimeEntitySnapshot( + name, + kind, + position.X, + position.Y, + position.Z, + direction.X, + direction.Y, + factionKey, + hullItemKey, + equipment, + cargoBays, + dockingBays, + childEntityKeys, + weaponGroups, + statGrids, + velocity.X, + velocity.Y, + targetEntityKey, + isActive, + heatsinksEnabled, + overrideShutdown, + tractorPower, + heatstroke, + hypothermia, + activeConsumables, + behaviorProgress, + weaponStates, + behaviorStates); + } + + private static Vector2Value ReadFieldVector2(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return new Vector2Value(0, 0); + var vectorFields = reader.ReadArrayHeader(); + var x = ReadFieldDouble(ref reader, vectorFields, 0); + var y = ReadFieldDouble(ref reader, vectorFields, 1); + SkipRemaining(ref reader, vectorFields, 2); + return new Vector2Value(x, y); + } + + private static Vector3Value ReadFieldVector3(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return new Vector3Value(0, 0, 0); + var vectorFields = reader.ReadArrayHeader(); + var x = ReadFieldDouble(ref reader, vectorFields, 0); + var y = ReadFieldDouble(ref reader, vectorFields, 1); + var z = ReadFieldDouble(ref reader, vectorFields, 2); + SkipRemaining(ref reader, vectorFields, 3); + return new Vector3Value(x, y, z); + } + + private static IReadOnlyList ReadFieldActionBarBindings(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var bindings = new AetheriaRuntimeActionBarBindingSnapshot[count]; + for (var binding = 0; binding < count; binding++) + { + var bindingFields = reader.ReadArrayHeader(); + var controlPath = ReadFieldString(ref reader, bindingFields, 0); + var kind = ReadFieldString(ref reader, bindingFields, 1); + var targetKey = ReadFieldString(ref reader, bindingFields, 2); + var equipmentIndex = ReadFieldInt32(ref reader, bindingFields, 3); + var behaviorIndex = ReadFieldInt32(ref reader, bindingFields, 4); + var weaponGroup = ReadFieldInt32(ref reader, bindingFields, 5); + SkipRemaining(ref reader, bindingFields, 6); + bindings[binding] = new AetheriaRuntimeActionBarBindingSnapshot( + controlPath, + kind, + targetKey, + equipmentIndex, + behaviorIndex, + weaponGroup); + } + + return bindings; + } + + private static IReadOnlyList ReadFieldFactionRelationships(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var relationships = new AetheriaRuntimeFactionRelationshipSnapshot[count]; + for (var relationshipIndex = 0; relationshipIndex < count; relationshipIndex++) + { + var relationshipFields = reader.ReadArrayHeader(); + var factionKey = ReadFieldString(ref reader, relationshipFields, 0); + var relationship = ReadFieldString(ref reader, relationshipFields, 1); + var standing = ReadFieldDouble(ref reader, relationshipFields, 2); + SkipRemaining(ref reader, relationshipFields, 3); + relationships[relationshipIndex] = new AetheriaRuntimeFactionRelationshipSnapshot(factionKey, relationship, standing); + } + + return relationships; + } + + private static IReadOnlyList ReadFieldOrbitSnapshots(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var orbits = new AetheriaRuntimeOrbitSnapshot[count]; + for (var orbit = 0; orbit < count; orbit++) + { + var orbitFields = reader.ReadArrayHeader(); + var orbitId = ReadFieldString(ref reader, orbitFields, 0); + var parentId = ReadFieldString(ref reader, orbitFields, 1); + var distance = ReadFieldDouble(ref reader, orbitFields, 2); + var phase = ReadFieldDouble(ref reader, orbitFields, 3); + var fixedPosition = ReadFieldVector2(ref reader, orbitFields, 4); + SkipRemaining(ref reader, orbitFields, 5); + orbits[orbit] = new AetheriaRuntimeOrbitSnapshot( + orbitId, + parentId, + distance, + phase, + fixedPosition.X, + fixedPosition.Y); + } + + return orbits; + } + + private static IReadOnlyList ReadFieldBodySnapshots(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var bodies = new AetheriaRuntimeBodySnapshot[count]; + for (var body = 0; body < count; body++) + { + var bodyFields = reader.ReadArrayHeader(); + var bodyId = ReadFieldString(ref reader, bodyFields, 0); + var kind = ReadFieldString(ref reader, bodyFields, 1); + var name = ReadFieldString(ref reader, bodyFields, 2); + var orbitId = ReadFieldString(ref reader, bodyFields, 3); + var mass = ReadFieldDouble(ref reader, bodyFields, 4); + var resourceCount = CountAndSkipFieldArray(ref reader, bodyFields, 5); + SkipFields(ref reader, bodyFields, 6, 10); + var asteroidCount = CountAndSkipFieldArray(ref reader, bodyFields, 10); + SkipRemaining(ref reader, bodyFields, 11); + bodies[body] = new AetheriaRuntimeBodySnapshot( + bodyId, + kind, + name, + orbitId, + mass, + resourceCount, + asteroidCount); + } + + return bodies; + } + + private static IReadOnlyList ReadFieldEntityItemSlots(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var slots = new AetheriaRuntimeEntityItemSlotSnapshot[count]; + for (var slot = 0; slot < count; slot++) + { + var slotFields = reader.ReadArrayHeader(); + var position = ReadFieldGridCoord(ref reader, slotFields, 0); + var itemKey = ReadFieldString(ref reader, slotFields, 1); + SkipRemaining(ref reader, slotFields, 2); + slots[slot] = new AetheriaRuntimeEntityItemSlotSnapshot(position.X, position.Y, itemKey); + } + + return slots; + } + + private static IReadOnlyList ReadFieldEntityStatGrids(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var grids = new AetheriaRuntimeEntityStatGridSnapshot[count]; + for (var grid = 0; grid < count; grid++) + { + var gridFields = reader.ReadArrayHeader(); + var name = ReadFieldString(ref reader, gridFields, 0); + var width = ReadFieldInt32(ref reader, gridFields, 1); + var height = ReadFieldInt32(ref reader, gridFields, 2); + var values = ReadFieldDoubleArray(ref reader, gridFields, 3); + SkipRemaining(ref reader, gridFields, 4); + grids[grid] = new AetheriaRuntimeEntityStatGridSnapshot(name, width, height, values); + } + + return grids; + } + + private static IReadOnlyList> ReadFieldEntityWeaponGroups(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty>(); + var count = reader.ReadArrayHeader(); + var groups = new IReadOnlyList[count]; + for (var group = 0; group < count; group++) + { + var groupFields = reader.ReadArrayHeader(); + groups[group] = ReadFieldInt32Array(ref reader, groupFields, 0); + SkipRemaining(ref reader, groupFields, 1); + } + + return groups; + } + + private static IReadOnlyList ReadFieldActiveConsumables(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var consumables = new AetheriaRuntimeActiveConsumableSnapshot[count]; + for (var consumable = 0; consumable < count; consumable++) + { + var consumableFields = reader.ReadArrayHeader(); + var itemKey = ReadFieldString(ref reader, consumableFields, 0); + var quality = ReadFieldDouble(ref reader, consumableFields, 1, 1); + var remainingDuration = ReadFieldDouble(ref reader, consumableFields, 2); + var duration = ReadFieldDouble(ref reader, consumableFields, 3); + SkipRemaining(ref reader, consumableFields, 4); + consumables[consumable] = new AetheriaRuntimeActiveConsumableSnapshot(itemKey, quality, remainingDuration, duration); + } + + return consumables; + } + + private static IReadOnlyList ReadFieldBehaviorProgress(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var progressRows = new AetheriaRuntimeBehaviorProgressSnapshot[count]; + for (var progress = 0; progress < count; progress++) + { + var progressFields = reader.ReadArrayHeader(); + var ownerKind = ReadFieldString(ref reader, progressFields, 0); + var ownerIndex = ReadFieldInt32(ref reader, progressFields, 1); + var behaviorIndex = ReadFieldInt32(ref reader, progressFields, 2); + var behaviorKind = ReadFieldString(ref reader, progressFields, 3); + var progressValue = ReadFieldDouble(ref reader, progressFields, 4); + SkipRemaining(ref reader, progressFields, 5); + progressRows[progress] = new AetheriaRuntimeBehaviorProgressSnapshot( + ownerKind, + ownerIndex, + behaviorIndex, + behaviorKind, + progressValue); + } + + return progressRows; + } + + private static IReadOnlyList ReadFieldWeaponStates(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var weaponStates = new AetheriaRuntimeWeaponStateSnapshot[count]; + for (var weapon = 0; weapon < count; weapon++) + { + var weaponFields = reader.ReadArrayHeader(); + var ownerKind = ReadFieldString(ref reader, weaponFields, 0); + var ownerIndex = ReadFieldInt32(ref reader, weaponFields, 1); + var behaviorIndex = ReadFieldInt32(ref reader, weaponFields, 2); + var behaviorKind = ReadFieldString(ref reader, weaponFields, 3); + var firing = ReadFieldBool(ref reader, weaponFields, 4); + var ammo = ReadFieldInt32(ref reader, weaponFields, 5); + var burstRemaining = ReadFieldInt32(ref reader, weaponFields, 6); + var burstTimer = ReadFieldDouble(ref reader, weaponFields, 7); + var burstInterval = ReadFieldDouble(ref reader, weaponFields, 8); + var cooldownProgress = ReadFieldDouble(ref reader, weaponFields, 9); + var coolingDown = ReadFieldBool(ref reader, weaponFields, 10); + var charging = ReadFieldBool(ref reader, weaponFields, 11); + var charged = ReadFieldBool(ref reader, weaponFields, 12); + var charge = ReadFieldDouble(ref reader, weaponFields, 13); + var reloading = ReadFieldBool(ref reader, weaponFields, 14); + var reloadProgress = ReadFieldDouble(ref reader, weaponFields, 15); + var ammoIntervalProgress = ReadFieldDouble(ref reader, weaponFields, 16); + SkipRemaining(ref reader, weaponFields, 17); + weaponStates[weapon] = new AetheriaRuntimeWeaponStateSnapshot( + ownerKind, + ownerIndex, + behaviorIndex, + behaviorKind, + firing, + ammo, + burstRemaining, + burstTimer, + burstInterval, + cooldownProgress, + coolingDown, + charging, + charged, + charge, + reloading, + reloadProgress, + ammoIntervalProgress); + } + + return weaponStates; + } + + private static IReadOnlyList ReadFieldBehaviorStates(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var behaviorStates = new AetheriaRuntimeBehaviorStateSnapshot[count]; + for (var behavior = 0; behavior < count; behavior++) + { + var behaviorFields = reader.ReadArrayHeader(); + var ownerKind = ReadFieldString(ref reader, behaviorFields, 0); + var ownerIndex = ReadFieldInt32(ref reader, behaviorFields, 1); + var behaviorIndex = ReadFieldInt32(ref reader, behaviorFields, 2); + var behaviorKind = ReadFieldString(ref reader, behaviorFields, 3); + var pinging = ReadFieldBool(ref reader, behaviorFields, 4); + var pingCooldown = ReadFieldDouble(ref reader, behaviorFields, 5); + var pingLerp = ReadFieldDouble(ref reader, behaviorFields, 6); + var pingRadius = ReadFieldDouble(ref reader, behaviorFields, 7); + var pingedEntityCount = ReadFieldInt32(ref reader, behaviorFields, 8); + var radiatorTemperature = ReadFieldDouble(ref reader, behaviorFields, 9); + var emissivity = ReadFieldDouble(ref reader, behaviorFields, 10); + var pumpedHeat = ReadFieldDouble(ref reader, behaviorFields, 11); + var wasteHeat = ReadFieldDouble(ref reader, behaviorFields, 12); + var energyUsage = ReadFieldDouble(ref reader, behaviorFields, 13); + var reactorDraw = ReadFieldDouble(ref reader, behaviorFields, 14); + var reactorLoadRatio = ReadFieldDouble(ref reader, behaviorFields, 15); + var capacitorCharge = ReadFieldDouble(ref reader, behaviorFields, 16); + var capacitorCapacity = ReadFieldDouble(ref reader, behaviorFields, 17); + var capacitorEfficiency = ReadFieldDouble(ref reader, behaviorFields, 18); + SkipRemaining(ref reader, behaviorFields, 19); + behaviorStates[behavior] = new AetheriaRuntimeBehaviorStateSnapshot( + ownerKind, + ownerIndex, + behaviorIndex, + behaviorKind, + pinging, + pingCooldown, + pingLerp, + pingRadius, + pingedEntityCount, + radiatorTemperature, + emissivity, + pumpedHeat, + wasteHeat, + energyUsage, + reactorDraw, + reactorLoadRatio, + capacitorCharge, + capacitorCapacity, + capacitorEfficiency); + } + + return behaviorStates; + } + private static string ReadFieldString(ref MessagePackReader reader, int fields, int index) { return index >= fields ? "" : ReadString(ref reader); @@ -650,6 +1144,16 @@ namespace GameCult.Aetheria.State.Unity return values; } + private static IReadOnlyList ReadFieldDoubleArray(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var values = new double[count]; + for (var item = 0; item < count; item++) + values[item] = reader.ReadDouble(); + return values; + } + private static IReadOnlyList ReadFieldShapeCells(ref MessagePackReader reader, int fields, int index) { return index >= fields ? Array.Empty() : ReadShapeCells(ref reader); @@ -990,6 +1494,15 @@ namespace GameCult.Aetheria.State.Unity reader.Skip(); } + private static int CountAndSkipFieldArray(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return 0; + var count = reader.ReadArrayHeader(); + for (var item = 0; item < count; item++) + reader.Skip(); + return count; + } + private static void SkipRemaining(ref MessagePackReader reader, int fields, int firstUnhandledIndex) { for (var field = firstUnhandledIndex; field < fields; field++) @@ -1053,6 +1566,35 @@ namespace GameCult.Aetheria.State.Unity public IReadOnlyList ActionBarInputs { get; } } + private readonly struct Vector2Value + { + public Vector2Value(double x, double y) + { + X = x; + Y = y; + } + + public double X { get; } + + public double Y { get; } + } + + private readonly struct Vector3Value + { + public Vector3Value(double x, double y, double z) + { + X = x; + Y = y; + Z = z; + } + + public double X { get; } + + public double Y { get; } + + public double Z { get; } + } + private readonly struct GridCoord { public GridCoord(int x, int y) diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 19cf7228b..dcb113cbf 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -37,7 +37,12 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. `Aetheria.State` owns applying those command documents into canonical `AetheriaPlayerSettings`, `AetheriaLoadoutTemplate`, `AetheriaRunState`, `AetheriaZoneState`, and `AetheriaEntitySnapshot` documents through - `AetheriaStateNode`. Local run saves, loadouts, player settings files, zone + `AetheriaStateNode`. The embedded Unity state package can also read typed + run, zone, and entity snapshot documents back from the `.cc` store as + read-only runtime DTOs, including action-bar bindings, faction + relationships, zone entity/orbit/body references, entity session scalars, + active consumables, behavior progress, weapon state, behavior state, and + stat-grid rows. Local run saves, loadouts, player settings files, zone files, and generated keyboard layout caches no longer write bespoke durable files. The legacy `RuntimePlayerSettings` runtime object is no longer named or decorated as a @@ -782,6 +787,10 @@ First Aetheria surfaces to publish: - Done: cut live simulation grids out of `RuntimeEntityBlueprint`; loadout and construction templates no longer capture or restore temperature, armor, max-armor, or hull-conductivity state. + - Done: add Unity package readback for canonical typed run, zone, and + entity snapshot documents, so the `.zone` replacement graph is not only + writable through the state node but also inspectable by runtime package + consumers. - Remaining: add typed documents/mappers for runtime object graphs, typed behavior factory construction, remaining behavior-private state not covered by progress, weapon, sensor, radiator, reactor, or capacitor rows,