Port historical asteroid mining to daemon

This commit is contained in:
Meta Vens 2026-07-11 18:40:40 +01:00
commit 922b0b06ac
7 changed files with 225 additions and 7 deletions

View file

@ -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()

View file

@ -82,14 +82,16 @@ namespace GameCult.Aetheria.State.Verse
public static IReadOnlyList<AetheriaRuntimeDaemonCommandDocument> AssignAndPlan(
AetheriaRuntimeRunCheckpointCommit run,
long frameId)
long frameId,
AetheriaRuntimeCatalogSnapshot? catalog = null,
double simulationTimeSeconds = 0)
{
if (run == null)
return Array.Empty<AetheriaRuntimeDaemonCommandDocument>();
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<AetheriaRuntimeDaemonCommandDocument> PlanAssignedTasks(
AetheriaRuntimeRunCheckpointCommit run,
long frameId)
long frameId,
AetheriaRuntimeCatalogSnapshot? catalog,
double simulationTimeSeconds)
{
var commands = new List<AetheriaRuntimeDaemonCommandDocument>();
foreach (var task in (run.AgentTasks ?? Array.Empty<AetheriaRuntimeAgentTaskCommit>())
@ -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<AetheriaRuntimeDaemonCommandDocument> 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<string>()).FirstOrDefault() ?? "";
var body = (zone.Bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && string.Equals(candidate.BodyKey, bodyKey, StringComparison.Ordinal));
if (tool == null || body == null)
{
Fail(task, agent);
return Array.Empty<AetheriaRuntimeDaemonCommandDocument>();
}
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<AetheriaRuntimeDaemonCommandDocument> PlanHaul(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeZoneSnapshotCommit zone,

View file

@ -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

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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(

View file

@ -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<AetheriaRuntimeEntitySnapshotCommit> entities,
AetheriaRuntimeDaemonIntentState intents,
AetheriaRuntimeCatalogSnapshot? catalog,
long frameId,
double simulationTimeSeconds,
double deltaSeconds)
{
foreach (var body in zone.Bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
foreach (var asteroid in body?.Asteroids ?? Array.Empty<AetheriaRuntimeAsteroidCommit>())
asteroid.RespawnTimer = Math.Max(0, asteroid.RespawnTimer - deltaSeconds);
foreach (var intent in intents?.Behaviors ?? Enumerable.Empty<AetheriaRuntimeDaemonBehaviorIntent>())
{
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<AetheriaRuntimeBodySnapshotCommit>())
.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<AetheriaRuntimeAsteroidMiningAccumulatorCommit>()).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<AetheriaRuntimeBodyResourceCommit>()).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<AetheriaRuntimeAsteroidMiningAccumulatorCommit>();
asteroid.RespawnTimer = RespawnTime(asteroid.Size);
}
}
}
private static void AddCargo(AetheriaRuntimeEntitySnapshotCommit entity, string itemKey)
{
var bays = (entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>()).ToList();
if (bays.Count == 0) bays.Add(new AetheriaRuntimeCargoBayLoadoutCommit());
var slots = (bays[0].Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).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);
}
}