From 922b0b06ac0aa0c40c2c4eee0b0dcdfc9d889d7a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 11 Jul 2026 18:40:40 +0100 Subject: [PATCH] Port historical asteroid mining to daemon --- Aetheria.State.Daemon.Smoke/Program.cs | 39 +++++++ .../Runtime/AetheriaRuntimeAgentTasks.cs | 62 +++++++++- .../AetheriaRuntimeDaemonIntentState.cs | 2 + .../AetheriaRuntimeDaemonOperations.cs | 4 +- .../AetheriaRuntimeDaemonSimulation.cs | 6 +- .../AetheriaRuntimeDaemonTickRunner.cs | 11 +- .../AetheriaRuntimeMiningSimulation.cs | 108 ++++++++++++++++++ 7 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMiningSimulation.cs diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index d6cb571f0..5352416fb 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -21,6 +21,45 @@ internal sealed class AetheriaDaemonYmirSmokeChecks RejectedHaulTransferDoesNotAdvanceTask(); AgentPatrolsHistoricalOrbitCircuitThroughMovementCommands(); TickReconcilesAndEvaluatesCatalogBehaviors(); + AgentMinesAsteroidThroughEquippedBehavior(); + } + + private static void AgentMinesAsteroidThroughEquippedBehavior() + { + var miner = Entity(0, 0, "workers"); + miner.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Mine]; + miner.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "mining-tool", Enabled = true, Quality = 1, Durability = 1 } }]; + var catalog = new AetheriaRuntimeCatalogSnapshot( + [CatalogItem("mining-tool", new AetheriaRuntimeBehaviorPayload(0, "MiningTool", 0, + [new AetheriaRuntimeBehaviorField(1, PerformanceStat(1000)), new AetheriaRuntimeBehaviorField(2, PerformanceStat(1000000000)), new AetheriaRuntimeBehaviorField(3, PerformanceStat(2)), new AetheriaRuntimeBehaviorField(4, PerformanceStat(1000))]))], + [], []); + var asteroid = new AetheriaRuntimeAsteroidCommit { Distance = 0, Size = 6 }; + var zone = new AetheriaRuntimeZoneSnapshotCommit + { + ZoneIndex = 0, + Entities = [miner], + Bodies = [new AetheriaRuntimeBodySnapshotCommit { BodyKey = "belt", Kind = "asteroid_belt", Asteroids = [asteroid], Resources = [new AetheriaRuntimeBodyResourceCommit { ItemKey = "iron", Amount = 1 }] }] + }; + var run = new AetheriaRuntimeRunCheckpointCommit + { + GenerationSeed = 7, + CurrentZoneIndex = 0, + CurrentEntityKey = "zone.0.entity.0", + Zones = [zone], + AgentTasks = [new AetheriaRuntimeAgentTaskCommit { TaskId = "mine-1", CorporationKey = "workers", TaskType = AetheriaRuntimeAgentTaskTypes.Mine, ZoneIndex = 0, Status = AetheriaRuntimeAgentTaskStatuses.Queued, TargetBodyKeys = ["belt"] }] + }; + + var result = AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-mining-smoke.cc"), run, + new AetheriaRuntimeDaemonTickOptions { FrameId = 1, FixedDeltaSeconds = 1, SimulationTimeSeconds = 1, Catalog = catalog, BuildPublications = false }); + + Require(run.AgentTasks[0].Phase == "mining", "mining task must activate its equipped tool"); + Require(result.OperationResult.Intents.Behaviors.Count == 1, + $"mining behavior command must become daemon intent (applied={string.Join(',', result.Frame.AppliedCommandIds)}, rejected={string.Join(',', result.Frame.RejectedCommandIds)})"); + var committedMiner = run.Zones[0].Entities.Single(entity => entity.EntityIndex == 0); + var committedAsteroid = run.Zones[0].Bodies[0].Asteroids[0]; + Require(committedAsteroid.RespawnTimer > 0, $"mining damage must deplete the asteroid and start historical respawn (damage={committedAsteroid.Damage}, respawn={committedAsteroid.RespawnTimer})"); + Require(committedMiner.CargoContents.SelectMany(bay => bay.Items).Any(slot => slot.Item.ItemKey == "iron" && slot.Item.Quantity > 0), + "historical mining yield must enter daemon-owned cargo"); } private static void TickReconcilesAndEvaluatesCatalogBehaviors() diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs index d6a757265..d0e98040d 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs @@ -82,14 +82,16 @@ namespace GameCult.Aetheria.State.Verse public static IReadOnlyList AssignAndPlan( AetheriaRuntimeRunCheckpointCommit run, - long frameId) + long frameId, + AetheriaRuntimeCatalogSnapshot? catalog = null, + double simulationTimeSeconds = 0) { if (run == null) return Array.Empty(); ReleaseInvalidAssignments(run); AssignQueuedTasks(run, frameId); - return PlanAssignedTasks(run, frameId); + return PlanAssignedTasks(run, frameId, catalog, simulationTimeSeconds); } public static void Reconcile( @@ -164,7 +166,9 @@ namespace GameCult.Aetheria.State.Verse private static IReadOnlyList PlanAssignedTasks( AetheriaRuntimeRunCheckpointCommit run, - long frameId) + long frameId, + AetheriaRuntimeCatalogSnapshot? catalog, + double simulationTimeSeconds) { var commands = new List(); foreach (var task in (run.AgentTasks ?? Array.Empty()) @@ -181,6 +185,11 @@ namespace GameCult.Aetheria.State.Verse commands.AddRange(PlanHaul(run, zone!, task, agent, frameId)); continue; } + if (string.Equals(task.TaskType, AetheriaRuntimeAgentTaskTypes.Mine, StringComparison.Ordinal)) + { + commands.AddRange(PlanMining(zone!, task, agent, frameId, catalog, simulationTimeSeconds)); + continue; + } if (string.Equals(task.TaskType, AetheriaRuntimeAgentTaskTypes.Defend, StringComparison.Ordinal)) { commands.AddRange(PlanPatrol(zone!, task, agent, frameId)); @@ -223,6 +232,53 @@ namespace GameCult.Aetheria.State.Verse return commands; } + private static IReadOnlyList PlanMining( + AetheriaRuntimeZoneSnapshotCommit zone, + AetheriaRuntimeAgentTaskCommit task, + AetheriaRuntimeEntitySnapshotCommit agent, + long frameId, + AetheriaRuntimeCatalogSnapshot? catalog, + double simulationTimeSeconds) + { + var tool = AetheriaRuntimeEquippedBehaviorQueries.Find(agent, catalog, "MiningTool").FirstOrDefault(); + var bodyKey = (task.TargetBodyKeys ?? Array.Empty()).FirstOrDefault() ?? ""; + var body = (zone.Bodies ?? Array.Empty()) + .FirstOrDefault(candidate => candidate != null && string.Equals(candidate.BodyKey, bodyKey, StringComparison.Ordinal)); + if (tool == null || body == null) + { + Fail(task, agent); + return Array.Empty(); + } + + var asteroid = AetheriaRuntimeDaemonRenderQueries.QueryAsteroidInstancePoses(zone, bodyKey, simulationTimeSeconds) + .Where(pose => pose.AsteroidIndex >= 0 && pose.AsteroidIndex < body.Asteroids.Count && body.Asteroids[pose.AsteroidIndex].RespawnTimer <= 0) + .OrderBy(pose => Math.Pow(pose.PositionX - agent.PositionX, 2) + Math.Pow(pose.PositionZ - agent.PositionZ, 2)) + .FirstOrDefault(); + if (asteroid.BodyKey == null) + return new[] { Movement(task, agent, frameId, 0, 0, 0) }; + + var dx = asteroid.PositionX - agent.PositionX; + var dz = asteroid.PositionZ - agent.PositionZ; + var distance = Math.Sqrt(dx * dx + dz * dz); + var range = Math.Max(0.01, tool.EvaluateStat(4)); + if (distance > range) + return new[] { Movement(task, agent, frameId, dx, dz, 1) }; + + task.Phase = "mining"; + return new[] + { + Movement(task, agent, frameId, 0, 0, 0), + Command(task, agent, frameId, AetheriaRuntimeDaemonCommandKinds.SetBehaviorActive, "mine", command => + { + command.EquipmentIndex = tool.EquipmentIndex; + command.BehaviorIndex = tool.BehaviorIndex; + command.ScalarValue = 1; + command.TextValue = bodyKey; + command.PositionX = asteroid.AsteroidIndex; + }) + }; + } + private static IReadOnlyList PlanHaul( AetheriaRuntimeRunCheckpointCommit run, AetheriaRuntimeZoneSnapshotCommit zone, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs index 6d0253464..39885db52 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs @@ -56,6 +56,8 @@ namespace GameCult.Aetheria.State.Verse public int EquipmentIndex { get; set; } = -1; public int BehaviorIndex { get; set; } = -1; public bool Active { get; set; } + public string TargetBodyKey { get; set; } = ""; + public int TargetAsteroidIndex { get; set; } = -1; } public sealed class AetheriaRuntimeDaemonConsumableIntent diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs index de675da65..e06f232e7 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs @@ -1095,7 +1095,9 @@ namespace GameCult.Aetheria.State.Verse ActorEntityKey = actor, EquipmentIndex = command.EquipmentIndex, BehaviorIndex = command.BehaviorIndex, - Active = command.ScalarValue > 0.5 + Active = command.ScalarValue > 0.5, + TargetBodyKey = command.TextValue ?? "", + TargetAsteroidIndex = (int)command.PositionX }); return true; } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs index c54d8f115..17d326a88 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs @@ -17,7 +17,10 @@ namespace GameCult.Aetheria.State.Verse AetheriaRuntimeDaemonIntentState intents, double deltaSeconds, AetheriaRuntimeDaemonSimulationSettings settings, - IAetheriaRuntimeProjectilePhysics projectilePhysics) + IAetheriaRuntimeProjectilePhysics projectilePhysics, + AetheriaRuntimeCatalogSnapshot? catalog = null, + long frameId = 0, + double simulationTimeSeconds = 0) { if (run == null || deltaSeconds <= 0) return; @@ -44,6 +47,7 @@ namespace GameCult.Aetheria.State.Verse StepTargetPursuit(entities, settings); StepMovement(entities, deltaSeconds); StepCombat(run, zone, entities, intents, deltaSeconds, settings, projectilePhysics); + AetheriaRuntimeMiningSimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds); StepTractorSalvage(run, zone, entities, settings); RefreshContacts(entities, settings); } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonTickRunner.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonTickRunner.cs index a1b6cd624..28ae90e8c 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonTickRunner.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonTickRunner.cs @@ -134,7 +134,11 @@ namespace GameCult.Aetheria.State.Verse .ToArray(), operationResult.Intents); } - var agentCommands = AetheriaRuntimeAgentScheduler.AssignAndPlan(operationResult.Run, options.FrameId); + var agentCommands = AetheriaRuntimeAgentScheduler.AssignAndPlan( + operationResult.Run, + options.FrameId, + options.Catalog, + options.SimulationTimeSeconds); if (agentCommands.Count > 0) { var agentResult = AetheriaRuntimeDaemonOperations.Execute( @@ -157,7 +161,10 @@ namespace GameCult.Aetheria.State.Verse operationResult.Intents, options.FixedDeltaSeconds, options.SimulationSettings, - options.ProjectilePhysics); + options.ProjectilePhysics, + options.Catalog, + options.FrameId, + options.SimulationTimeSeconds); StampZoneSimulationTime(operationResult.Run, options.SimulationTimeSeconds); var frame = AetheriaRuntimeDaemonFrameDocument.Create( diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMiningSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMiningSimulation.cs new file mode 100644 index 000000000..257795a49 --- /dev/null +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeMiningSimulation.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +#nullable enable + +namespace GameCult.Aetheria.State.Verse +{ + public static class AetheriaRuntimeMiningSimulation + { + public const double MiningDifficulty = 500; + + public static void Step( + AetheriaRuntimeRunCheckpointCommit run, + AetheriaRuntimeZoneSnapshotCommit zone, + IReadOnlyList entities, + AetheriaRuntimeDaemonIntentState intents, + AetheriaRuntimeCatalogSnapshot? catalog, + long frameId, + double simulationTimeSeconds, + double deltaSeconds) + { + foreach (var body in zone.Bodies ?? Array.Empty()) + foreach (var asteroid in body?.Asteroids ?? Array.Empty()) + asteroid.RespawnTimer = Math.Max(0, asteroid.RespawnTimer - deltaSeconds); + + foreach (var intent in intents?.Behaviors ?? Enumerable.Empty()) + { + if (!intent.Active || !TryEntityIndex(intent.ActorEntityKey, out var minerIndex)) + continue; + var miner = entities.FirstOrDefault(entity => entity.EntityIndex == minerIndex && entity.IsActive); + var tool = AetheriaRuntimeEquippedBehaviorQueries.Find(miner, catalog, "MiningTool") + .FirstOrDefault(candidate => candidate.EquipmentIndex == intent.EquipmentIndex && candidate.BehaviorIndex == intent.BehaviorIndex); + var body = (zone.Bodies ?? Array.Empty()) + .FirstOrDefault(candidate => candidate != null && string.Equals(candidate.BodyKey, intent.TargetBodyKey, StringComparison.Ordinal)); + if (miner == null || tool == null || body == null || intent.TargetAsteroidIndex < 0 || intent.TargetAsteroidIndex >= body.Asteroids.Count) + continue; + var asteroid = body.Asteroids[intent.TargetAsteroidIndex]; + if (asteroid.RespawnTimer > 0) + continue; + var pose = AetheriaRuntimeDaemonRenderQueries.QueryAsteroidInstancePoses(zone, body.BodyKey, simulationTimeSeconds) + .FirstOrDefault(candidate => candidate.AsteroidIndex == intent.TargetAsteroidIndex); + var range = Math.Max(0, tool.EvaluateStat(4)); + if (Math.Pow(pose.PositionX - miner.PositionX, 2) + Math.Pow(pose.PositionZ - miner.PositionZ, 2) > range * range) + continue; + + var damage = Math.Max(0, tool.EvaluateStat(1)) * deltaSeconds; + var efficiency = Math.Max(0, tool.EvaluateStat(2)); + var penetration = Math.Max(0.001, tool.EvaluateStat(3)); + asteroid.Damage += damage; + var accumulators = (asteroid.MiningAccumulators ?? Array.Empty()).ToList(); + var accumulator = accumulators.FirstOrDefault(value => value.MinerEntityIndex == minerIndex); + if (accumulator == null) + { + accumulator = new AetheriaRuntimeAsteroidMiningAccumulatorCommit { MinerEntityIndex = minerIndex }; + accumulators.Add(accumulator); + } + accumulator.Amount += damage; + asteroid.MiningAccumulators = accumulators; + + var resources = (body.Resources ?? Array.Empty()).Where(resource => resource.Amount > 0).ToArray(); + if (resources.Length > 0) + { + var resource = resources.OrderByDescending(value => Math.Pow(value.Amount, 1.0 / penetration) * Roll(run.GenerationSeed, frameId, minerIndex, intent.TargetAsteroidIndex, value.ItemKey)).First(); + if (efficiency * Roll(run.GenerationSeed, frameId, minerIndex, intent.TargetAsteroidIndex, "yield") * accumulator.Amount * resources.Length / MiningDifficulty > 1) + { + accumulator.Amount = 0; + AddCargo(miner, resource.ItemKey); + } + } + + if (asteroid.Damage >= Hitpoints(asteroid.Size)) + { + asteroid.Damage = 0; + asteroid.MiningAccumulators = Array.Empty(); + asteroid.RespawnTimer = RespawnTime(asteroid.Size); + } + } + } + + private static void AddCargo(AetheriaRuntimeEntitySnapshotCommit entity, string itemKey) + { + var bays = (entity.CargoContents ?? Array.Empty()).ToList(); + if (bays.Count == 0) bays.Add(new AetheriaRuntimeCargoBayLoadoutCommit()); + var slots = (bays[0].Items ?? Array.Empty()).ToList(); + var existing = slots.FirstOrDefault(slot => string.Equals(slot?.Item?.ItemKey, itemKey, StringComparison.Ordinal)); + if (existing != null) existing.Item.Quantity++; + else slots.Add(new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = itemKey, Quantity = 1 } }); + bays[0].Items = slots; + entity.CargoContents = bays; + } + + private static double Hitpoints(double size) => 10 + 190 * Math.Pow(Clamp((size - 3) / 3), 2); + private static double RespawnTime(double size) => 10 + 90 * Math.Pow(Clamp((size - 3) / 3), 1.5); + private static double Clamp(double value) => Math.Max(0, Math.Min(1, value)); + private static double Roll(uint seed, long frame, int miner, int asteroid, string salt) + { + unchecked + { + uint hash = seed ^ (uint)frame * 16777619u ^ (uint)miner * 2246822519u ^ (uint)asteroid * 3266489917u; + foreach (var c in salt ?? "") hash = (hash ^ c) * 16777619u; + hash ^= hash >> 16; hash *= 2246822519u; hash ^= hash >> 13; + return (hash + 1.0) / (uint.MaxValue + 2.0); + } + } + private static bool TryEntityIndex(string key, out int value) => int.TryParse((key ?? "").Split('.').LastOrDefault(), out value); + } +}