feat: production chain

This commit is contained in:
2026-03-15 22:46:47 -04:00
parent 651556c916
commit 5ba1287f85
65 changed files with 3718 additions and 687 deletions

View File

@@ -13,6 +13,8 @@ public sealed partial class SimulationEngine
world.Seed,
sequence,
world.TickIntervalMs,
world.OrbitalTimeSeconds,
new OrbitalSimulationSnapshot(_orbitalSimulation.SimulatedSecondsPerRealSecond),
world.GeneratedAtUtc,
world.Systems.Select(system => new SystemSnapshot(
system.Definition.Id,
@@ -59,11 +61,12 @@ public sealed partial class SimulationEngine
node.Id,
node.SystemId,
node.LocalPosition,
node.AnchorNodeId,
node.SourceKind,
node.OreRemaining,
node.MaxOre,
node.ItemId)).ToList(),
world.Stations.Select(ToStationDelta).Select(station => new StationSnapshot(
world.Stations.Select(station => ToStationDelta(world, station)).Select(station => new StationSnapshot(
station.Id,
station.Label,
station.Category,
@@ -74,8 +77,13 @@ public sealed partial class SimulationEngine
station.AnchorNodeId,
station.Color,
station.DockedShips,
station.DockedShipIds,
station.DockingPads,
station.FuelStored,
station.FuelCapacity,
station.EnergyStored,
station.EnergyCapacity,
station.CurrentProcesses,
station.Inventory,
station.FactionId,
station.CommanderId,
@@ -135,7 +143,7 @@ public sealed partial class SimulationEngine
policy.DockingAccessPolicy,
policy.ConstructionAccessPolicy,
policy.OperationalRangePolicy)).ToList(),
world.Ships.Select(ToShipDelta).Select(ship => new ShipSnapshot(
world.Ships.Select(ship => ToShipDelta(world, ship)).Select(ship => new ShipSnapshot(
ship.Id,
ship.Label,
ship.Role,
@@ -154,12 +162,14 @@ public sealed partial class SimulationEngine
ship.CommanderId,
ship.PolicySetId,
ship.CargoCapacity,
ship.CargoItemId,
ship.WorkerPopulation,
ship.EnergyStored,
ship.Inventory,
ship.FactionId,
ship.Health,
ship.History,
ship.CurrentAction,
ship.SpatialState)).ToList(),
world.Factions.Select(ToFactionDelta).Select(faction => new FactionSnapshot(
faction.Id,
@@ -193,7 +203,7 @@ public sealed partial class SimulationEngine
foreach (var station in world.Stations)
{
station.LastDeltaSignature = BuildStationSignature(station);
station.LastDeltaSignature = BuildStationSignature(world, station);
}
foreach (var claim in world.Claims)
@@ -218,7 +228,7 @@ public sealed partial class SimulationEngine
foreach (var ship in world.Ships)
{
ship.LastDeltaSignature = BuildShipSignature(ship);
ship.LastDeltaSignature = BuildShipSignature(world, ship);
}
foreach (var faction in world.Factions)
@@ -286,14 +296,14 @@ public sealed partial class SimulationEngine
var deltas = new List<StationDelta>();
foreach (var station in world.Stations)
{
var signature = BuildStationSignature(station);
var signature = BuildStationSignature(world, station);
if (signature == station.LastDeltaSignature)
{
continue;
}
station.LastDeltaSignature = signature;
deltas.Add(ToStationDelta(station));
deltas.Add(ToStationDelta(world, station));
}
return deltas;
@@ -371,19 +381,19 @@ public sealed partial class SimulationEngine
return deltas;
}
private static IReadOnlyList<ShipDelta> BuildShipDeltas(SimulationWorld world)
private IReadOnlyList<ShipDelta> BuildShipDeltas(SimulationWorld world)
{
var deltas = new List<ShipDelta>();
foreach (var ship in world.Ships)
{
var signature = BuildShipSignature(ship);
var signature = BuildShipSignature(world, ship);
if (signature == ship.LastDeltaSignature)
{
continue;
}
ship.LastDeltaSignature = signature;
deltas.Add(ToShipDelta(ship));
deltas.Add(ToShipDelta(world, ship));
}
return deltas;
@@ -407,7 +417,8 @@ public sealed partial class SimulationEngine
return deltas;
}
private static string BuildNodeSignature(ResourceNodeRuntime node) => $"{node.SystemId}|{node.OreRemaining:0.###}";
private static string BuildNodeSignature(ResourceNodeRuntime node) =>
$"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.AnchorNodeId}|{node.OreRemaining:0.###}";
private static string BuildSpatialNodeSignature(NodeRuntime node) =>
$"{node.SystemId}|{node.Kind.ToContractValue()}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.BubbleId}|{node.ParentNodeId}|{node.OccupyingStructureId}|{node.OrbitReferenceId}";
@@ -415,8 +426,34 @@ public sealed partial class SimulationEngine
private static string BuildLocalBubbleSignature(LocalBubbleRuntime bubble) =>
$"{bubble.SystemId}|{bubble.NodeId}|{bubble.Radius:0.###}|{string.Join(",", bubble.OccupantShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantStationIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantClaimIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal))}";
private static string BuildStationSignature(StationRuntime station) =>
$"{station.SystemId}|{station.NodeId}|{station.BubbleId}|{station.AnchorNodeId}|{station.CommanderId}|{station.PolicySetId}|{BuildInventorySignature(station.Inventory)}|{station.EnergyStored:0.###}|{station.DockedShipIds.Count}|{station.DockingPadAssignments.Count}|{station.Population:0.###}|{station.PopulationCapacity:0.###}|{station.WorkforceRequired:0.###}|{station.WorkforceEffectiveRatio:0.###}|{string.Join(",", station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal))}|{string.Join(",", station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal))}|{station.ActiveConstruction?.ModuleId ?? "none"}|{station.ActiveConstruction?.ProgressSeconds.ToString("0.###") ?? "0"}";
private static string BuildStationSignature(SimulationWorld world, StationRuntime station)
{
var processes = ToStationActionProgressSnapshots(world, station);
return string.Join("|",
station.SystemId,
station.NodeId ?? "none",
station.BubbleId ?? "none",
station.AnchorNodeId ?? "none",
station.CommanderId ?? "none",
station.PolicySetId ?? "none",
BuildInventorySignature(station.Inventory),
GetInventoryAmount(station.Inventory, "fuel").ToString("0.###"),
GetStationFuelCapacity(station).ToString("0.###"),
station.EnergyStored.ToString("0.###"),
GetStationEnergyCapacity(station).ToString("0.###"),
string.Join(",", processes.Select(process => $"{process.Lane}:{process.Label}:{process.Progress:0.###}")),
string.Join(",", station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal)),
station.DockingPadAssignments.Count.ToString(),
station.Population.ToString("0.###"),
station.PopulationCapacity.ToString("0.###"),
station.WorkforceRequired.ToString("0.###"),
station.WorkforceEffectiveRatio.ToString("0.###"),
string.Join(",", station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal)),
string.Join(",", station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal)),
station.ActiveConstruction?.ModuleId ?? "none",
station.ActiveConstruction?.ProgressSeconds.ToString("0.###") ?? "0",
string.Join(",", station.ProductionLaneTimers.OrderBy(entry => entry.Key, StringComparer.Ordinal).Select(entry => $"{entry.Key}:{entry.Value:0.###}")));
}
private static string BuildClaimSignature(ClaimRuntime claim) =>
$"{claim.FactionId}|{claim.SystemId}|{claim.NodeId}|{claim.BubbleId}|{claim.State}|{claim.Health:0.###}|{claim.ActivatesAtUtc:O}";
@@ -430,7 +467,7 @@ public sealed partial class SimulationEngine
private static string BuildPolicySignature(PolicySetRuntime policy) =>
$"{policy.OwnerKind}|{policy.OwnerId}|{policy.TradeAccessPolicy}|{policy.DockingAccessPolicy}|{policy.ConstructionAccessPolicy}|{policy.OperationalRangePolicy}";
private static string BuildShipSignature(ShipRuntime ship) =>
private static string BuildShipSignature(SimulationWorld world, ShipRuntime ship) =>
string.Join("|",
ship.SystemId,
ship.Position.X.ToString("0.###"),
@@ -442,10 +479,10 @@ public sealed partial class SimulationEngine
ship.TargetPosition.X.ToString("0.###"),
ship.TargetPosition.Y.ToString("0.###"),
ship.TargetPosition.Z.ToString("0.###"),
ship.State,
ship.State.ToContractValue(),
ship.Order?.Kind ?? "none",
ship.DefaultBehavior.Kind,
ship.ControllerTask.Kind,
ship.ControllerTask.Kind.ToContractValue(),
ship.SpatialState.CurrentNodeId ?? "none",
ship.SpatialState.CurrentBubbleId ?? "none",
ship.DockedStationId ?? "none",
@@ -463,8 +500,14 @@ public sealed partial class SimulationEngine
ship.SpatialState.Transit?.Progress.ToString("0.###") ?? "0",
GetShipCargoAmount(ship).ToString("0.###"),
GetInventoryAmount(ship.Inventory, "fuel").ToString("0.###"),
ship.TrackedActionKey ?? "none",
ship.TrackedActionTotal.ToString("0.###"),
ship.ControllerTask.TargetEntityId is not null && world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is { } site
? GetRemainingConstructionDelivery(world, site).ToString("0.###")
: "0",
ship.EnergyStored.ToString("0.###"),
ship.Health.ToString("0.###"));
ship.Health.ToString("0.###"),
ship.ActionTimer.ToString("0.###"));
private static string BuildInventorySignature(IReadOnlyDictionary<string, float> inventory) =>
string.Join(",",
@@ -480,6 +523,7 @@ public sealed partial class SimulationEngine
node.Id,
node.SystemId,
ToDto(node.Position),
node.AnchorNodeId,
node.SourceKind,
node.OreRemaining,
node.MaxOre,
@@ -505,7 +549,7 @@ public sealed partial class SimulationEngine
bubble.OccupantClaimIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
bubble.OccupantConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal).ToList());
private static StationDelta ToStationDelta(StationRuntime station) => new(
private static StationDelta ToStationDelta(SimulationWorld world, StationRuntime station) => new(
station.Id,
station.Definition.Label,
station.Definition.Category,
@@ -516,8 +560,13 @@ public sealed partial class SimulationEngine
station.AnchorNodeId,
station.Definition.Color,
station.DockedShipIds.Count,
station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
GetDockingPadCount(station),
GetInventoryAmount(station.Inventory, "fuel"),
GetStationFuelCapacity(station),
station.EnergyStored,
GetStationEnergyCapacity(station),
ToStationActionProgressSnapshots(world, station),
ToInventoryEntries(station.Inventory),
station.FactionId,
station.CommanderId,
@@ -529,6 +578,23 @@ public sealed partial class SimulationEngine
station.InstalledModules.OrderBy(moduleId => moduleId, StringComparer.Ordinal).ToList(),
station.MarketOrderIds.OrderBy(orderId => orderId, StringComparer.Ordinal).ToList());
private static IReadOnlyList<StationActionProgressSnapshot> ToStationActionProgressSnapshots(SimulationWorld world, StationRuntime station) =>
GetStationProductionLanes(station)
.Select(laneKey =>
{
var recipe = SelectProductionRecipe(world, station, laneKey);
var timer = GetStationProductionTimer(station, laneKey);
return recipe is null || station.EnergyStored <= 0.01f || timer <= 0.01f
? null
: new StationActionProgressSnapshot(
laneKey,
recipe.Label,
Math.Clamp(timer / MathF.Max(recipe.Duration, 0.1f), 0f, 1f));
})
.Where(snapshot => snapshot is not null)
.Cast<StationActionProgressSnapshot>()
.ToList();
private static ClaimDelta ToClaimDelta(ClaimRuntime claim) => new(
claim.Id,
claim.FactionId,
@@ -582,7 +648,7 @@ public sealed partial class SimulationEngine
policy.ConstructionAccessPolicy,
policy.OperationalRangePolicy);
private static ShipDelta ToShipDelta(ShipRuntime ship) => new(
private ShipDelta ToShipDelta(SimulationWorld world, ShipRuntime ship) => new(
ship.Id,
ship.Definition.Label,
ship.Definition.Role,
@@ -591,24 +657,75 @@ public sealed partial class SimulationEngine
ToDto(ship.Position),
ToDto(ship.Velocity),
ToDto(ship.TargetPosition),
ship.State,
ship.State.ToContractValue(),
ship.Order?.Kind,
ship.DefaultBehavior.Kind,
ship.ControllerTask.Kind,
ship.ControllerTask.Kind.ToContractValue(),
ship.SpatialState.CurrentNodeId,
ship.SpatialState.CurrentBubbleId,
ship.DockedStationId,
ship.CommanderId,
ship.PolicySetId,
ship.Definition.CargoCapacity,
ship.Definition.CargoItemId,
ship.WorkerPopulation,
ship.EnergyStored,
ToInventoryEntries(ship.Inventory),
ship.FactionId,
ship.Health,
ship.History.ToList(),
ToShipActionProgressSnapshot(world, ship),
ToShipSpatialStateSnapshot(ship.SpatialState));
private static ShipActionProgressSnapshot? ToShipActionProgressSnapshot(SimulationWorld world, ShipRuntime ship)
{
var progress = ship.State switch
{
ShipState.SpoolingFtl => CreateShipActionProgress("FTL spool", ship.ActionTimer, MathF.Max(ship.Definition.SpoolTime, 0.1f)),
ShipState.Ftl => ship.SpatialState.Transit is null ? null : new ShipActionProgressSnapshot("FTL", Math.Clamp(ship.SpatialState.Transit.Progress, 0f, 1f)),
ShipState.SpoolingWarp => CreateShipActionProgress("Warp spool", ship.ActionTimer, MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f)),
ShipState.Warping => ship.SpatialState.Transit is null ? null : new ShipActionProgressSnapshot("Warp", Math.Clamp(ship.SpatialState.Transit.Progress, 0f, 1f)),
ShipState.Mining => CreateShipActionProgress("Mining", ship.ActionTimer, MathF.Max(world.Balance.MiningCycleSeconds, 0.1f)),
ShipState.Docking => CreateShipActionProgress("Docking", ship.ActionTimer, MathF.Max(world.Balance.DockingDuration, 0.1f)),
ShipState.Undocking => CreateShipActionProgress("Undocking", ship.ActionTimer, MathF.Max(world.Balance.UndockingDuration, 0.1f)),
ShipState.Transferring => CreateShipRemainingActionProgress("Transfer", ship.TrackedActionTotal, GetShipCargoAmount(ship)),
ShipState.Refueling => CreateShipRemainingActionProgress(
"Refuel",
ship.TrackedActionTotal,
MathF.Max(0f, GetShipRefuelTarget(ship, world) - GetInventoryAmount(ship.Inventory, "fuel"))),
ShipState.Loading => CreateShipRemainingActionProgress(
"Load workers",
ship.TrackedActionTotal,
MathF.Max(0f, ship.TrackedActionTotal - ship.WorkerPopulation)),
ShipState.Unloading => CreateShipRemainingActionProgress(
"Unload workers",
ship.TrackedActionTotal,
ship.WorkerPopulation),
ShipState.DeliveringConstruction => ship.ControllerTask.TargetEntityId is null
? null
: world.ConstructionSites.FirstOrDefault(site => site.Id == ship.ControllerTask.TargetEntityId) is not { } site
? null
: CreateShipRemainingActionProgress("Deliver materials", ship.TrackedActionTotal, GetRemainingConstructionDelivery(world, site)),
_ => null,
};
return progress;
}
private static ShipActionProgressSnapshot CreateShipActionProgress(string label, float elapsedSeconds, float requiredSeconds) =>
new(label, Math.Clamp(elapsedSeconds / requiredSeconds, 0f, 1f));
private static ShipActionProgressSnapshot? CreateShipRemainingActionProgress(string label, float totalAmount, float remainingAmount)
{
if (totalAmount <= 0.01f)
{
return null;
}
var progress = 1f - Math.Clamp(remainingAmount / totalAmount, 0f, 1f);
return new ShipActionProgressSnapshot(label, progress);
}
private static IReadOnlyList<InventoryEntry> ToInventoryEntries(IReadOnlyDictionary<string, float> inventory) =>
inventory
.Where(entry => entry.Value > 0.001f)
@@ -647,9 +764,9 @@ public sealed partial class SimulationEngine
private static void EmitShipStateEvents(
ShipRuntime ship,
string previousState,
ShipState previousState,
string previousBehavior,
string previousTask,
ControllerTaskKind previousTask,
string controllerEvent,
ICollection<SimulationEventRecord> events)
{
@@ -657,7 +774,7 @@ public sealed partial class SimulationEngine
if (previousState != ship.State)
{
events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Label} {previousState} -> {ship.State}", occurredAtUtc));
events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Label} {previousState.ToContractValue()} -> {ship.State.ToContractValue()}", occurredAtUtc));
}
if (previousBehavior != ship.DefaultBehavior.Kind)
@@ -667,7 +784,7 @@ public sealed partial class SimulationEngine
if (previousTask != ship.ControllerTask.Kind)
{
events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Label} task {previousTask} -> {ship.ControllerTask.Kind}", occurredAtUtc));
events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Label} task {previousTask.ToContractValue()} -> {ship.ControllerTask.Kind.ToContractValue()}", occurredAtUtc));
}
if (controllerEvent != "none")