diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index a67348424..4fedda7f7 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Aetheria.State.Smoke/Program.cs b/Aetheria.State.Smoke/Program.cs index 7f61561b5..d85150b74 100644 --- a/Aetheria.State.Smoke/Program.cs +++ b/Aetheria.State.Smoke/Program.cs @@ -434,7 +434,17 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s Distance = 7, Phase = 0.75, Size = 2, - RotationSpeed = 0.5 + RotationSpeed = 0.5, + Damage = 1.25, + RespawnTimer = 6.5, + MiningAccumulators = + [ + new AetheriaAsteroidMiningAccumulatorSnapshot + { + MinerEntityKey = entityKey.ToString(), + Amount = 0.75 + } + ] } ] } @@ -621,7 +631,12 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether zoneState.Bodies.Length != 1 || zoneState.Bodies[0].Kind != "asteroid_belt" || zoneState.Bodies[0].Resources.Length != 1 || - zoneState.Bodies[0].Asteroids.Length != 1) + zoneState.Bodies[0].Asteroids.Length != 1 || + zoneState.Bodies[0].Asteroids[0].Damage != 1.25 || + zoneState.Bodies[0].Asteroids[0].RespawnTimer != 6.5 || + zoneState.Bodies[0].Asteroids[0].MiningAccumulators.Length != 1 || + zoneState.Bodies[0].Asteroids[0].MiningAccumulators[0].MinerEntityKey != entityKey.ToString() || + zoneState.Bodies[0].Asteroids[0].MiningAccumulators[0].Amount != 0.75) { throw new InvalidOperationException("Zone state did not survive flush/reopen."); } diff --git a/Aetheria.State.Unity.Smoke/Program.cs b/Aetheria.State.Unity.Smoke/Program.cs index 78a679bb8..db0952155 100644 --- a/Aetheria.State.Unity.Smoke/Program.cs +++ b/Aetheria.State.Unity.Smoke/Program.cs @@ -296,7 +296,17 @@ try Distance = 7.0, Phase = 0.75, Size = 2.0, - RotationSpeed = 0.5 + RotationSpeed = 0.5, + Damage = 1.25, + RespawnTimer = 6.5, + MiningAccumulators = new[] + { + new AetheriaRuntimeAsteroidMiningAccumulatorCommit + { + MinerEntityIndex = 0, + Amount = 0.75 + } + } } } } @@ -683,6 +693,11 @@ try zone.Bodies[0].Resources.Length == 1 && zone.Bodies[0].Resources[0].ItemKey == "aetheria.item_definition:legacy:smoke:ore" && zone.Bodies[0].Asteroids.Length == 1 && + zone.Bodies[0].Asteroids[0].Damage == 1.25 && + zone.Bodies[0].Asteroids[0].RespawnTimer == 6.5 && + zone.Bodies[0].Asteroids[0].MiningAccumulators.Length == 1 && + zone.Bodies[0].Asteroids[0].MiningAccumulators[0].MinerEntityKey == "global:aetheria.run_state.smoke-run.zone.0.entity.0.v1" && + zone.Bodies[0].Asteroids[0].MiningAccumulators[0].Amount == 0.75 && entity?.Equipment.Length == 1 && entity.Equipment[0].Quality == 0.9 && entity.Equipment[0].Durability == 0.8 && @@ -818,6 +833,9 @@ try packageZones[0].Bodies.Count != 1 || packageZones[0].Bodies[0].ResourceCount != 1 || packageZones[0].Bodies[0].AsteroidCount != 1 || + packageZones[0].Bodies[0].DamagedAsteroidCount != 1 || + packageZones[0].Bodies[0].RespawningAsteroidCount != 1 || + packageZones[0].Bodies[0].AsteroidMiningAccumulatorCount != 1 || packageEntities.Count != 1 || packageEntities[0].Name != "Unity Smoke Ship" || packageEntities[0].Equipment[0].Quality != 0.9 || diff --git a/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs b/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs index 1d54b123f..32f9553f8 100644 --- a/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs +++ b/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs @@ -270,7 +270,7 @@ public static class AetheriaRuntimeCommitLogApplier OwnerFactionIndex = zone.OwnerFactionIndex, EntityKeys = entityKeys.ToArray(), Orbits = ToOrbitSnapshots(zone.Orbits), - Bodies = ToBodySnapshots(zone.Bodies) + Bodies = ToBodySnapshots(zone.Bodies, run.RunId, zone.ZoneIndex) }) .ConfigureAwait(false); zoneKeys.Add(zoneKey.ToString()); @@ -301,7 +301,9 @@ public static class AetheriaRuntimeCommitLogApplier } private static AetheriaBodySnapshot[] ToBodySnapshots( - IReadOnlyList? bodies) + IReadOnlyList? bodies, + string runId, + int zoneIndex) { return (bodies ?? Array.Empty()) .Select(body => new AetheriaBodySnapshot @@ -316,7 +318,7 @@ public static class AetheriaRuntimeCommitLogApplier GravityRadiusMultiplier = body.GravityRadiusMultiplier, GravityDepthMultiplier = body.GravityDepthMultiplier, GravityDepthExponent = body.GravityDepthExponent, - Asteroids = ToAsteroidSnapshots(body.Asteroids), + Asteroids = ToAsteroidSnapshots(body.Asteroids, runId, zoneIndex), GasGiantVisual = ToGasGiantVisual(body.GasGiantVisual), SunVisual = ToSunVisual(body.SunVisual) }) @@ -336,7 +338,9 @@ public static class AetheriaRuntimeCommitLogApplier } private static AetheriaAsteroidSnapshot[] ToAsteroidSnapshots( - IReadOnlyList? asteroids) + IReadOnlyList? asteroids, + string runId, + int zoneIndex) { return (asteroids ?? Array.Empty()) .Select(asteroid => new AetheriaAsteroidSnapshot @@ -344,7 +348,25 @@ public static class AetheriaRuntimeCommitLogApplier Distance = asteroid.Distance, Phase = asteroid.Phase, Size = asteroid.Size, - RotationSpeed = asteroid.RotationSpeed + 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(); } diff --git a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs index 2c074ddac..3705f65d7 100644 --- a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs +++ b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs @@ -347,6 +347,26 @@ public sealed class AetheriaAsteroidSnapshot [Key(3)] public double RotationSpeed { get; set; } + + [Key(4)] + public double Damage { get; set; } + + [Key(5)] + public double RespawnTimer { get; set; } + + [Key(6)] + public AetheriaAsteroidMiningAccumulatorSnapshot[] MiningAccumulators { get; set; } = []; +} + +[MessagePackObject] +public sealed class AetheriaAsteroidMiningAccumulatorSnapshot +{ + [Key(0)] + [CultReference(typeof(AetheriaEntitySnapshot))] + public string MinerEntityKey { get; set; } = ""; + + [Key(1)] + public double Amount { get; set; } } [MessagePackObject] diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 08a761b0c..48ca71776 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -625,7 +625,7 @@ public class ActionGameManager : MonoBehaviour .ToArray() ?? Array.Empty(), OwnerFactionIndex = FactionIndex(galaxyZone?.Owner), Orbits = ProjectZoneOrbits(zone.Blueprint), - Bodies = ProjectZoneBodies(zone.Blueprint), + Bodies = ProjectZoneBodies(zone), Entities = zone.Entities .Select((entity, index) => ProjectEntitySnapshot(zone, entity, index)) .ToArray() @@ -648,16 +648,17 @@ public class ActionGameManager : MonoBehaviour .ToArray() ?? Array.Empty(); } - private static AetheriaRuntimeBodySnapshotCommit[] ProjectZoneBodies(RuntimeZoneBlueprint blueprint) + private static AetheriaRuntimeBodySnapshotCommit[] ProjectZoneBodies(Zone zone) { - return blueprint?.Planets? + return zone?.Blueprint?.Planets? .OrderBy(body => body.ID) - .Select(ProjectZoneBody) + .Select(body => ProjectZoneBody(zone, body)) .ToArray() ?? Array.Empty(); } - private static AetheriaRuntimeBodySnapshotCommit ProjectZoneBody(BodyData body) + private static AetheriaRuntimeBodySnapshotCommit ProjectZoneBody(Zone zone, BodyData body) { + zone.AsteroidBelts.TryGetValue(body.ID, out var asteroidBelt); return new AetheriaRuntimeBodySnapshotCommit { BodyLegacyId = LegacyId(body.ID), @@ -678,7 +679,7 @@ public class ActionGameManager : MonoBehaviour GravityDepthMultiplier = body.GravityDepthMultiplier, GravityDepthExponent = body.GravityDepthExponent, Asteroids = body is AsteroidBeltData belt - ? ProjectAsteroids(belt.Asteroids) + ? ProjectAsteroids(zone, belt, asteroidBelt) : Array.Empty(), GasGiantVisual = body is GasGiantData gas ? ProjectGasGiantVisual(gas) @@ -689,15 +690,31 @@ public class ActionGameManager : MonoBehaviour }; } - private static AetheriaRuntimeAsteroidCommit[] ProjectAsteroids(Asteroid[] asteroids) + private static AetheriaRuntimeAsteroidCommit[] ProjectAsteroids( + Zone zone, + AsteroidBeltData beltData, + AsteroidBelt asteroidBelt) { - return asteroids? - .Select(asteroid => new AetheriaRuntimeAsteroidCommit + return beltData.Asteroids? + .Select((asteroid, asteroidIndex) => new AetheriaRuntimeAsteroidCommit { Distance = asteroid.Distance, Phase = asteroid.Phase, Size = asteroid.Size, - RotationSpeed = asteroid.RotationSpeed + RotationSpeed = asteroid.RotationSpeed, + Damage = asteroidBelt != null && asteroidBelt.Damage.TryGetValue(asteroidIndex, out var damage) ? damage : 0, + RespawnTimer = asteroidBelt != null && asteroidBelt.RespawnTimers.TryGetValue(asteroidIndex, out var respawnTimer) ? respawnTimer : 0, + MiningAccumulators = asteroidBelt == null + ? Array.Empty() + : asteroidBelt.MiningAccumulator + .Where(pair => pair.Key.Item2 == asteroidIndex) + .Select(pair => new AetheriaRuntimeAsteroidMiningAccumulatorCommit + { + MinerEntityIndex = zone.Entities.IndexOf(pair.Key.Item1), + Amount = pair.Value + }) + .Where(accumulator => accumulator.MinerEntityIndex >= 0) + .ToArray() }) .ToArray() ?? Array.Empty(); } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs index 3baffd7eb..e858486bb 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs @@ -935,7 +935,17 @@ namespace GameCult.Aetheria.State.Unity public sealed class AetheriaRuntimeBodySnapshot { - public AetheriaRuntimeBodySnapshot(string bodyId, string kind, string name, string orbitId, double mass, int resourceCount, int asteroidCount) + public AetheriaRuntimeBodySnapshot( + string bodyId, + string kind, + string name, + string orbitId, + double mass, + int resourceCount, + int asteroidCount, + int damagedAsteroidCount, + int respawningAsteroidCount, + int asteroidMiningAccumulatorCount) { BodyId = bodyId; Kind = kind; @@ -944,6 +954,9 @@ namespace GameCult.Aetheria.State.Unity Mass = mass; ResourceCount = resourceCount; AsteroidCount = asteroidCount; + DamagedAsteroidCount = damagedAsteroidCount; + RespawningAsteroidCount = respawningAsteroidCount; + AsteroidMiningAccumulatorCount = asteroidMiningAccumulatorCount; } public string BodyId { get; } @@ -953,6 +966,9 @@ namespace GameCult.Aetheria.State.Unity public double Mass { get; } public int ResourceCount { get; } public int AsteroidCount { get; } + public int DamagedAsteroidCount { get; } + public int RespawningAsteroidCount { get; } + public int AsteroidMiningAccumulatorCount { get; } } public sealed class AetheriaRuntimeEntityItemSlotSnapshot diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs index 643c3778d..c2cf52761 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs @@ -702,7 +702,7 @@ namespace GameCult.Aetheria.State.Unity 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); + var asteroidSummary = ReadFieldAsteroidSummary(ref reader, bodyFields, 10); SkipRemaining(ref reader, bodyFields, 11); bodies[body] = new AetheriaRuntimeBodySnapshot( bodyId, @@ -711,12 +711,51 @@ namespace GameCult.Aetheria.State.Unity orbitId, mass, resourceCount, - asteroidCount); + asteroidSummary.Count, + asteroidSummary.DamagedCount, + asteroidSummary.RespawningCount, + asteroidSummary.MiningAccumulatorCount); } return bodies; } + private static AsteroidSummary ReadFieldAsteroidSummary(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return new AsteroidSummary(0, 0, 0, 0); + var count = reader.ReadArrayHeader(); + var damagedCount = 0; + var respawningCount = 0; + var miningAccumulatorCount = 0; + for (var asteroid = 0; asteroid < count; asteroid++) + { + var asteroidFields = reader.ReadArrayHeader(); + SkipFields(ref reader, asteroidFields, 0, 4); + if (ReadFieldDouble(ref reader, asteroidFields, 4) > 0) damagedCount++; + if (ReadFieldDouble(ref reader, asteroidFields, 5) > 0) respawningCount++; + miningAccumulatorCount += CountAndSkipFieldArray(ref reader, asteroidFields, 6); + SkipRemaining(ref reader, asteroidFields, 7); + } + + return new AsteroidSummary(count, damagedCount, respawningCount, miningAccumulatorCount); + } + + private readonly struct AsteroidSummary + { + public AsteroidSummary(int count, int damagedCount, int respawningCount, int miningAccumulatorCount) + { + Count = count; + DamagedCount = damagedCount; + RespawningCount = respawningCount; + MiningAccumulatorCount = miningAccumulatorCount; + } + + public int Count { get; } + public int DamagedCount { get; } + public int RespawningCount { get; } + public int MiningAccumulatorCount { get; } + } + private static IReadOnlyList ReadFieldEntityItemSlots(ref MessagePackReader reader, int fields, int index) { if (index >= fields) return Array.Empty(); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs index 795d83d31..655e93363 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs @@ -401,6 +401,25 @@ namespace GameCult.Aetheria.State.Unity [Key(3)] public double RotationSpeed { get; set; } + + [Key(4)] + public double Damage { get; set; } + + [Key(5)] + public double RespawnTimer { get; set; } + + [Key(6)] + public IReadOnlyList MiningAccumulators { get; set; } = Array.Empty(); + } + + [MessagePackObject] + public sealed class AetheriaRuntimeAsteroidMiningAccumulatorCommit + { + [Key(0)] + public int MinerEntityIndex { get; set; } = -1; + + [Key(1)] + public double Amount { get; set; } } [MessagePackObject] diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index eebfb8175..e90de0da0 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -146,8 +146,10 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. parent IDs, distance, phase, fixed positions, body kind/name/orbit, resources, gravity/body multipliers, asteroid belt entries, and gas/sun visual parameters now survive through the typed pending commit lane into - `AetheriaZoneState`. `RuntimeZoneBlueprint` feeds this projector, but it is - no longer the only place generated celestial graph facts can live. + `AetheriaZoneState`. Asteroid belt runtime damage, respawn timers, and + miner accumulators also persist on typed asteroid rows. `RuntimeZoneBlueprint` + feeds static generated geometry, but it is no longer the only place generated + celestial graph facts can live. - Run checkpoint entity snapshots include typed simulation stat grids for temperature, thermal mass, armor, max armor, and hull-conductivity axes. They also carry public runtime session state: velocity, target entity @@ -840,6 +842,8 @@ First Aetheria surfaces to publish: entity snapshot documents, so the `.zone` replacement graph is not only writable through the state node but also inspectable by runtime package consumers. + - Done: preserve asteroid belt runtime state on typed asteroid rows: + damage, respawn timers, and miner accumulator references. - Done: persist the actual galaxy generation seed in typed run state, so future Continue/restore work has a reproducible topology body before applying current-zone/entity snapshots.