improvement on gm windows, ai

This commit is contained in:
2026-03-20 12:40:26 -04:00
parent ff078fe939
commit 3b56785f9a
39 changed files with 2594 additions and 358 deletions

View File

@@ -40,6 +40,7 @@ public sealed class ItemProductionDefinition
public sealed class BalanceDefinition
{
public float SimulationSpeedMultiplier { get; set; } = 1f;
public float YPlane { get; set; }
public float ArrivalThreshold { get; set; }
public float MiningRate { get; set; }
@@ -94,7 +95,8 @@ public sealed class AsteroidFieldDefinition
public sealed class ResourceNodeDefinition
{
public string SourceKind { get; set; } = "asteroid-belt";
public string SourceKind { get; set; } = "local-space";
public string? AnchorReference { get; set; }
public float Angle { get; set; }
public float RadiusOffset { get; set; }
public float InclinationDegrees { get; set; }

View File

@@ -457,6 +457,18 @@ internal sealed class ObjectiveStepFactory
}
}
internal sealed record StepExecutionAssessment(
FactionPlanStepStatus Status,
string StatusReason,
string? BlockingReason = null,
StepExecutionBinding? Binding = null,
IndustryExpansionProject? ExpectedProject = null);
internal sealed record StepExecutionBinding(
string Kind,
string? TargetId,
string Summary);
internal sealed class FactionObjectiveExecutor
{
internal void Execute(
@@ -466,6 +478,7 @@ internal sealed class FactionObjectiveExecutor
FactionPlanningState state)
{
var blackboard = commander.FactionBlackboard ?? throw new InvalidOperationException("Faction blackboard must exist before objectives are executed.");
var activeProject = FactionIndustryPlanner.GetActiveExpansionProject(world, commander.FactionId);
commander.ActiveGoalName = null;
commander.ActiveActionName = null;
@@ -480,8 +493,8 @@ internal sealed class FactionObjectiveExecutor
EvaluateObjective(world, commander, objective);
foreach (var step in objective.Steps.OrderByDescending(step => step.Priority))
{
EvaluateStep(world, commander, objective, step, blackboard, state);
EmitTasks(engine, world, commander, objective, step, blackboard, state, touchedTaskIds, assignedAssetIds);
var assessment = EvaluateStep(world, commander, objective, step, blackboard, state, activeProject);
EmitTasks(engine, world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds);
}
}
@@ -531,102 +544,300 @@ internal sealed class FactionObjectiveExecutor
objective.State = FactionObjectiveState.Active;
}
private static void EvaluateStep(
private static StepExecutionAssessment EvaluateStep(
SimulationWorld world,
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
FactionBlackboardRuntime blackboard,
FactionPlanningState state)
FactionPlanningState state,
IndustryExpansionProject? activeProject)
{
step.LastEvaluatedCycle = commander.PlanningCycle;
step.BlockingReason = null;
step.StatusReason = null;
step.ExecutionBindingKind = null;
step.ExecutionBindingTargetId = null;
step.ExecutionBindingSummary = null;
StepExecutionAssessment assessment;
if (step.DependencyStepIds.Count > 0 && HasIncompleteDependencies(commander, step))
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "Waiting for prerequisite objective steps to complete.";
return;
assessment = new StepExecutionAssessment(
FactionPlanStepStatus.Blocked,
"Blocked on prerequisite objective steps.",
BlockingReason: "Waiting for prerequisite objective steps to complete.");
}
else
{
assessment = step.Kind switch
{
FactionPlanStepKind.EnsureCommodityProduction => EvaluateCommodityStep(world, commander, step, blackboard, activeProject),
FactionPlanStepKind.EnsureShipyardSite => EvaluateShipyardStep(world, commander, step, state, activeProject),
FactionPlanStepKind.ProduceFleet => EvaluateFleetProductionStep(commander, step, blackboard, state),
FactionPlanStepKind.AttackFactionAssets => EvaluateAttackStep(commander, step, blackboard, state),
FactionPlanStepKind.EnsureWaterSupply => EvaluateWaterStep(world, commander, step, blackboard, activeProject),
FactionPlanStepKind.EnsureMiningCapacity => EvaluateCapacityStep(commander, step, blackboard.HasShipyard, state.MinerShipCount, 2, "mining"),
FactionPlanStepKind.EnsureConstructionCapacity => EvaluateCapacityStep(commander, step, blackboard.HasShipyard, state.ConstructorShipCount, 1, "construction"),
FactionPlanStepKind.EnsureTransportCapacity => EvaluateCapacityStep(commander, step, blackboard.HasShipyard, state.TransportShipCount, 1, "transport"),
FactionPlanStepKind.MonitorExpansionProject => EvaluateWarIndustryMonitorStep(world, commander, step, blackboard, activeProject),
_ => new StepExecutionAssessment(FactionPlanStepStatus.Failed, "Unknown step kind."),
};
}
switch (step.Kind)
{
case FactionPlanStepKind.EnsureCommodityProduction:
EvaluateCommodityStep(step, blackboard);
break;
case FactionPlanStepKind.EnsureShipyardSite:
if (state.HasShipFactory)
{
step.Status = FactionPlanStepStatus.Complete;
step.ProducedFacts.Add("shipyard-online");
}
else
{
step.Status = blackboard.HasActiveExpansionProject && string.Equals(blackboard.ActiveExpansionModuleId, step.ModuleId, StringComparison.Ordinal)
? FactionPlanStepStatus.Running
: FactionPlanStepStatus.Ready;
}
break;
case FactionPlanStepKind.ProduceFleet:
step.Status = state.MilitaryShipCount >= blackboard.TargetWarshipCount
? FactionPlanStepStatus.Complete
: FactionPlanStepStatus.Running;
break;
case FactionPlanStepKind.AttackFactionAssets:
if (blackboard.EnemyFactionCount <= 0)
{
step.Status = FactionPlanStepStatus.Complete;
}
else if (state.MilitaryShipCount < Math.Max(2, blackboard.TargetWarshipCount / 2))
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "Insufficient military strength to commit to a faction attack objective.";
}
else
{
step.Status = FactionPlanStepStatus.Running;
}
break;
case FactionPlanStepKind.EnsureWaterSupply:
step.Status = IsCommodityOperational(blackboard, "water", 300f)
? FactionPlanStepStatus.Complete
: blackboard.HasActiveExpansionProject && string.Equals(blackboard.ActiveExpansionCommodityId, "water", StringComparison.Ordinal)
? FactionPlanStepStatus.Running
: FactionPlanStepStatus.Ready;
break;
case FactionPlanStepKind.EnsureMiningCapacity:
step.Status = state.MinerShipCount >= 2 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
break;
case FactionPlanStepKind.EnsureConstructionCapacity:
step.Status = state.ConstructorShipCount >= 1 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
break;
case FactionPlanStepKind.EnsureTransportCapacity:
step.Status = state.TransportShipCount >= 1 ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
break;
case FactionPlanStepKind.MonitorExpansionProject:
step.Status = blackboard.HasWarIndustrySupplyChain ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Running;
break;
}
ApplyAssessment(step, assessment);
return assessment;
}
private static void EvaluateCommodityStep(
private static StepExecutionAssessment EvaluateCommodityStep(
SimulationWorld world,
CommanderRuntime commander,
FactionPlanStepRuntime step,
FactionBlackboardRuntime blackboard)
FactionBlackboardRuntime blackboard,
IndustryExpansionProject? activeProject)
{
if (string.IsNullOrWhiteSpace(step.CommodityId))
{
step.Status = FactionPlanStepStatus.Failed;
step.BlockingReason = "Commodity planning step is missing a target commodity.";
return;
return new StepExecutionAssessment(
FactionPlanStepStatus.Failed,
"Commodity step is missing a required commodity.",
BlockingReason: "Commodity planning step is missing a target commodity.");
}
var completed = IsCommodityOperational(blackboard, step.CommodityId, 240f);
step.Status = completed ? FactionPlanStepStatus.Complete : FactionPlanStepStatus.Ready;
if (completed)
if (IsCommodityOperational(blackboard, step.CommodityId, 240f))
{
step.ProducedFacts.Add($"commodity-online:{step.CommodityId}");
return new StepExecutionAssessment(
FactionPlanStepStatus.Complete,
$"Commodity {step.CommodityId} is operational in the faction economy.");
}
var expectedProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, step.CommodityId, ignoreActiveExpansionProject: true);
return EvaluateExpansionRequirement(step, expectedProject, activeProject);
}
private static StepExecutionAssessment EvaluateShipyardStep(
SimulationWorld world,
CommanderRuntime commander,
FactionPlanStepRuntime step,
FactionPlanningState state,
IndustryExpansionProject? activeProject)
{
if (state.HasShipFactory)
{
step.ProducedFacts.Add("shipyard-online");
return new StepExecutionAssessment(
FactionPlanStepStatus.Complete,
"Faction already has an online shipyard.");
}
var expectedProject = FactionIndustryPlanner.CreateShipyardFoundationProject(world, commander.FactionId, ignoreActiveExpansionProject: true);
return EvaluateExpansionRequirement(step, expectedProject, activeProject);
}
private static StepExecutionAssessment EvaluateWaterStep(
SimulationWorld world,
CommanderRuntime commander,
FactionPlanStepRuntime step,
FactionBlackboardRuntime blackboard,
IndustryExpansionProject? activeProject)
{
if (IsCommodityOperational(blackboard, "water", 300f))
{
step.ProducedFacts.Add("commodity-online:water");
return new StepExecutionAssessment(
FactionPlanStepStatus.Complete,
"Water supply is operational.");
}
var expectedProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, "water", ignoreActiveExpansionProject: true);
return EvaluateExpansionRequirement(step, expectedProject, activeProject);
}
private static StepExecutionAssessment EvaluateFleetProductionStep(
CommanderRuntime commander,
FactionPlanStepRuntime step,
FactionBlackboardRuntime blackboard,
FactionPlanningState state)
{
if (state.MilitaryShipCount >= blackboard.TargetWarshipCount)
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Complete,
"Target war fleet size has been reached.");
}
if (!blackboard.HasShipyard)
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Blocked,
"Fleet production requires an online shipyard.",
BlockingReason: "Fleet production requires an online shipyard.");
}
if (TryFindIssuedTaskBinding(commander, step, out var binding))
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Running,
"Military fleet production is already bound to an issued ship-production task.",
Binding: binding);
}
return new StepExecutionAssessment(
FactionPlanStepStatus.Ready,
"Shipyard is available; military fleet production can begin.");
}
private static StepExecutionAssessment EvaluateAttackStep(
CommanderRuntime commander,
FactionPlanStepRuntime step,
FactionBlackboardRuntime blackboard,
FactionPlanningState state)
{
if (blackboard.EnemyFactionCount <= 0)
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Complete,
"No hostile faction remains to attack.");
}
if (state.MilitaryShipCount < Math.Max(2, blackboard.TargetWarshipCount / 2))
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Blocked,
"Insufficient military strength to begin the attack objective.",
BlockingReason: "Insufficient military strength to commit to a faction attack objective.");
}
if (TryFindIssuedTaskBinding(commander, step, out var binding))
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Running,
"Attack objective is already bound to a matching combat task.",
Binding: binding);
}
return new StepExecutionAssessment(
FactionPlanStepStatus.Ready,
"Combat strength is available; attack execution can begin.");
}
private static StepExecutionAssessment EvaluateCapacityStep(
CommanderRuntime commander,
FactionPlanStepRuntime step,
bool hasShipyard,
int currentCount,
int requiredCount,
string shipRole)
{
if (currentCount >= requiredCount)
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Complete,
$"Faction already meets the required {shipRole} ship capacity.");
}
if (!hasShipyard)
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Blocked,
$"No shipyard is currently assigned to produce {shipRole} ships.",
BlockingReason: $"Ship capacity expansion for {shipRole} requires an online shipyard.");
}
if (TryFindIssuedTaskBinding(commander, step, out var binding))
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Running,
$"{shipRole} ship production is already bound to a matching issued task.",
Binding: binding);
}
return new StepExecutionAssessment(
FactionPlanStepStatus.Ready,
$"Shipyard capacity is available; {shipRole} ship production can begin.");
}
private static StepExecutionAssessment EvaluateWarIndustryMonitorStep(
SimulationWorld world,
CommanderRuntime commander,
FactionPlanStepRuntime step,
FactionBlackboardRuntime blackboard,
IndustryExpansionProject? activeProject)
{
if (blackboard.HasWarIndustrySupplyChain)
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Complete,
"War-industry supply chain is operational.");
}
foreach (var commodityId in new[] { "refinedmetals", "hullparts", "claytronics" })
{
if (IsCommodityOperational(blackboard, commodityId, 240f))
{
continue;
}
var expectedProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, commodityId, ignoreActiveExpansionProject: true);
return EvaluateExpansionRequirement(step, expectedProject, activeProject);
}
return new StepExecutionAssessment(
FactionPlanStepStatus.Ready,
"War-industry prerequisites are unresolved but no matching active project is bound yet.");
}
private static StepExecutionAssessment EvaluateExpansionRequirement(
FactionPlanStepRuntime step,
IndustryExpansionProject? expectedProject,
IndustryExpansionProject? activeProject)
{
if (expectedProject is null)
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Blocked,
"Unable to derive a valid expansion plan for the step outcome.",
BlockingReason: BuildMissingPlanReason(step));
}
if (activeProject is not null && ProjectsSemanticallyMatch(expectedProject, activeProject))
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Running,
$"Running on matching active expansion project {DescribeProject(activeProject)}.",
Binding: new StepExecutionBinding(
"expansion-project",
activeProject.SiteId,
$"Matched active project {DescribeProject(activeProject)}."),
ExpectedProject: activeProject);
}
if (activeProject is not null)
{
return new StepExecutionAssessment(
FactionPlanStepStatus.Blocked,
$"Blocked by unrelated active expansion {DescribeProject(activeProject)}; step requires {DescribeProject(expectedProject)}.",
BlockingReason: $"Active expansion {DescribeProject(activeProject)} does not satisfy required outcome {DescribeProject(expectedProject)}.",
ExpectedProject: expectedProject);
}
return new StepExecutionAssessment(
FactionPlanStepStatus.Ready,
$"Ready to start required expansion {DescribeProject(expectedProject)}.",
ExpectedProject: expectedProject);
}
private static void ApplyAssessment(
FactionPlanStepRuntime step,
StepExecutionAssessment assessment)
{
step.Status = assessment.Status;
step.StatusReason = assessment.StatusReason;
step.BlockingReason = assessment.BlockingReason;
step.ExecutionBindingKind = assessment.Binding?.Kind;
step.ExecutionBindingTargetId = assessment.Binding?.TargetId;
step.ExecutionBindingSummary = assessment.Binding?.Summary;
}
private static bool IsCommodityOperational(
@@ -653,8 +864,7 @@ internal sealed class FactionObjectiveExecutor
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
FactionBlackboardRuntime blackboard,
FactionPlanningState state,
StepExecutionAssessment assessment,
ISet<string> touchedTaskIds,
ISet<string> assignedAssetIds)
{
@@ -670,128 +880,12 @@ internal sealed class FactionObjectiveExecutor
switch (step.Kind)
{
case FactionPlanStepKind.EnsureCommodityProduction:
if (blackboard.HasActiveExpansionProject)
{
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: blackboard.ActiveExpansionCommodityId ?? step.CommodityId,
moduleId: blackboard.ActiveExpansionModuleId,
targetSystemId: blackboard.ActiveExpansionSystemId,
targetSiteId: blackboard.ActiveExpansionSiteId,
blockingReason: step.BlockingReason,
notes: step.Notes ?? "Expansion project already active for faction.");
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
step.Status = FactionPlanStepStatus.Running;
return;
}
if (step.CommodityId is null)
{
return;
}
var project = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, step.CommodityId);
if (project is not null)
{
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
step.TargetSiteId = project.SiteId;
step.Status = FactionPlanStepStatus.Running;
step.Notes = $"Queued expansion project for {project.CommodityId}.";
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: project.CommodityId,
moduleId: project.ModuleId,
targetSystemId: project.SystemId,
targetSiteId: project.SiteId,
blockingReason: null,
notes: step.Notes);
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
}
else
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = $"Unable to derive an expansion project for {step.CommodityId}.";
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: step.CommodityId,
moduleId: step.ModuleId,
targetSystemId: null,
targetSiteId: null,
blockingReason: step.BlockingReason,
notes: step.Notes);
}
EmitExpansionExecution(world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds);
break;
case FactionPlanStepKind.EnsureShipyardSite:
if (blackboard.HasActiveExpansionProject)
{
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: blackboard.ActiveExpansionCommodityId,
moduleId: blackboard.ActiveExpansionModuleId ?? step.ModuleId,
targetSystemId: blackboard.ActiveExpansionSystemId,
targetSiteId: blackboard.ActiveExpansionSiteId,
blockingReason: step.BlockingReason,
notes: step.Notes ?? "Shipyard support project waiting on current expansion site.");
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
step.Status = FactionPlanStepStatus.Running;
return;
}
var shipyardProject = FactionIndustryPlanner.CreateShipyardFoundationProject(world, commander.FactionId);
if (shipyardProject is not null)
{
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, shipyardProject);
step.TargetSiteId = shipyardProject.SiteId;
step.Status = FactionPlanStepStatus.Running;
step.Notes = "Queued shipyard foundation project.";
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: shipyardProject.CommodityId,
moduleId: shipyardProject.ModuleId,
targetSystemId: shipyardProject.SystemId,
targetSiteId: shipyardProject.SiteId,
blockingReason: null,
notes: step.Notes);
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
}
else
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "Unable to identify a viable shipyard foundation project.";
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: step.CommodityId,
moduleId: step.ModuleId,
targetSystemId: null,
targetSiteId: null,
blockingReason: step.BlockingReason,
notes: step.Notes);
}
EmitExpansionExecution(world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds);
break;
case FactionPlanStepKind.ProduceFleet:
if (!blackboard.HasShipyard)
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "Fleet production requires an online shipyard.";
}
UpsertShipProductionTask(
commander,
objective,
@@ -801,78 +895,30 @@ internal sealed class FactionObjectiveExecutor
blockingReason: step.BlockingReason,
notes: step.Notes ?? "Maintain military ship production until war fleet target is satisfied.");
AssignShipyardAssets(world, commander, objective, step);
PromoteShipProductionStepToRunning(step, "military");
break;
case FactionPlanStepKind.AttackFactionAssets:
UpsertAttackTask(commander, objective, step, touchedTaskIds);
AssignCombatAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
PromoteCombatStepToRunning(step);
break;
case FactionPlanStepKind.EnsureWaterSupply:
if (blackboard.HasActiveExpansionProject)
{
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: blackboard.ActiveExpansionCommodityId,
moduleId: blackboard.ActiveExpansionModuleId,
targetSystemId: blackboard.ActiveExpansionSystemId,
targetSiteId: blackboard.ActiveExpansionSiteId,
blockingReason: step.BlockingReason,
notes: step.Notes ?? "Water support project waiting on current expansion site.");
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
step.Status = FactionPlanStepStatus.Running;
return;
}
var waterProject = FactionIndustryPlanner.AnalyzeCommodityNeed(world, commander.FactionId, "water");
if (waterProject is not null)
{
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, waterProject);
step.Status = FactionPlanStepStatus.Running;
step.Notes = "Queued water expansion project.";
step.TargetSiteId = waterProject.SiteId;
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: waterProject.CommodityId,
moduleId: waterProject.ModuleId,
targetSystemId: waterProject.SystemId,
targetSiteId: waterProject.SiteId,
blockingReason: null,
notes: step.Notes);
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
}
else
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "Unable to derive an expansion project for water.";
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: "water",
moduleId: step.ModuleId,
targetSystemId: null,
targetSiteId: null,
blockingReason: step.BlockingReason,
notes: step.Notes);
}
EmitExpansionExecution(world, commander, objective, step, assessment, touchedTaskIds, assignedAssetIds);
break;
case FactionPlanStepKind.EnsureMiningCapacity:
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "mining", step.BlockingReason, "Maintain mining ship production until logistical capacity is healthy.");
AssignShipyardAssets(world, commander, objective, step);
PromoteShipProductionStepToRunning(step, "mining");
break;
case FactionPlanStepKind.EnsureConstructionCapacity:
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "construction", step.BlockingReason, "Maintain construction ship production until expansion support is healthy.");
AssignShipyardAssets(world, commander, objective, step);
PromoteShipProductionStepToRunning(step, "construction");
break;
case FactionPlanStepKind.EnsureTransportCapacity:
UpsertShipProductionTask(commander, objective, step, touchedTaskIds, "transport", step.BlockingReason, "Maintain transport ship production until logistical throughput is healthy.");
AssignShipyardAssets(world, commander, objective, step);
PromoteShipProductionStepToRunning(step, "transport");
break;
case FactionPlanStepKind.MonitorExpansionProject:
UpsertWarIndustryTask(commander, objective, step, touchedTaskIds);
@@ -880,6 +926,111 @@ internal sealed class FactionObjectiveExecutor
}
}
private static void EmitExpansionExecution(
SimulationWorld world,
CommanderRuntime commander,
FactionObjectiveRuntime objective,
FactionPlanStepRuntime step,
StepExecutionAssessment assessment,
ISet<string> touchedTaskIds,
ISet<string> assignedAssetIds)
{
var project = assessment.ExpectedProject;
if (project is null)
{
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: step.CommodityId,
moduleId: step.ModuleId,
targetSystemId: null,
targetSiteId: null,
blockingReason: step.BlockingReason,
notes: step.StatusReason);
return;
}
if (step.Status == FactionPlanStepStatus.Ready)
{
FactionIndustryPlanner.EnsureExpansionSite(world, commander.FactionId, project);
project = project with { SiteId = project.SiteId ?? FindMatchingSiteId(world, commander.FactionId, project) };
step.TargetSiteId = project.SiteId;
step.Status = FactionPlanStepStatus.Running;
step.StatusReason = $"Started required expansion {DescribeProject(project)}.";
step.ExecutionBindingKind = "expansion-project";
step.ExecutionBindingTargetId = project.SiteId;
step.ExecutionBindingSummary = $"Started site {project.SiteId ?? "pending"} for {DescribeProject(project)}.";
}
if (step.Status == FactionPlanStepStatus.Running)
{
step.TargetSiteId ??= project.SiteId;
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: project.CommodityId,
moduleId: project.ModuleId,
targetSystemId: project.SystemId,
targetSiteId: project.SiteId,
blockingReason: step.BlockingReason,
notes: step.StatusReason);
AssignConstructionAssets(world, commander, objective, step, step.IssuedTaskIds, assignedAssetIds);
return;
}
UpsertExpansionTask(
commander,
objective,
step,
touchedTaskIds,
commodityId: project.CommodityId,
moduleId: project.ModuleId,
targetSystemId: project.SystemId,
targetSiteId: project.SiteId,
blockingReason: step.BlockingReason,
notes: step.StatusReason);
}
private static void PromoteShipProductionStepToRunning(FactionPlanStepRuntime step, string shipRole)
{
if (step.Status != FactionPlanStepStatus.Ready || step.AssignedAssetIds.Count == 0)
{
return;
}
step.Status = FactionPlanStepStatus.Running;
step.StatusReason = $"{titleCase(shipRole)} ship production is bound to shipyard assets.";
step.ExecutionBindingKind = "shipyard-production";
step.ExecutionBindingTargetId = step.AssignedAssetIds.FirstOrDefault();
step.ExecutionBindingSummary = $"Using shipyard assets {string.Join(", ", step.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal))}.";
}
private static void PromoteCombatStepToRunning(FactionPlanStepRuntime step)
{
if (step.Status != FactionPlanStepStatus.Ready)
{
return;
}
if (step.AssignedAssetIds.Count <= 0)
{
step.Status = FactionPlanStepStatus.Blocked;
step.BlockingReason = "No combat ships were available for the attack step.";
step.StatusReason = step.BlockingReason;
return;
}
step.Status = FactionPlanStepStatus.Running;
step.StatusReason = "Attack step is bound to assigned combat ships.";
step.ExecutionBindingKind = "combat-assets";
step.ExecutionBindingTargetId = step.AssignedAssetIds.FirstOrDefault();
step.ExecutionBindingSummary = $"Using combat ships {string.Join(", ", step.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal))}.";
}
private static void ReconcileStaleTasks(CommanderRuntime commander, ISet<string> touchedTaskIds)
{
foreach (var task in commander.IssuedTasks)
@@ -1056,6 +1207,71 @@ internal sealed class FactionObjectiveExecutor
_ => FactionIssuedTaskState.Cancelled,
};
private static bool ProjectsSemanticallyMatch(
IndustryExpansionProject expectedProject,
IndustryExpansionProject activeProject) =>
string.Equals(expectedProject.CommodityId, activeProject.CommodityId, StringComparison.Ordinal)
&& string.Equals(expectedProject.ModuleId, activeProject.ModuleId, StringComparison.Ordinal)
&& string.Equals(expectedProject.SystemId, activeProject.SystemId, StringComparison.Ordinal)
&& string.Equals(expectedProject.CelestialId, activeProject.CelestialId, StringComparison.Ordinal);
private static string DescribeProject(IndustryExpansionProject project) =>
$"{project.CommodityId}/{project.ModuleId} @ {project.SystemId}:{project.CelestialId}";
private static string BuildMissingPlanReason(FactionPlanStepRuntime step) =>
step.Kind switch
{
FactionPlanStepKind.EnsureCommodityProduction => $"Unable to derive an expansion project for required commodity {step.CommodityId}.",
FactionPlanStepKind.EnsureWaterSupply => "Unable to derive an expansion project for water supply.",
FactionPlanStepKind.EnsureShipyardSite => "Unable to identify a viable shipyard foundation project.",
_ => "Unable to derive the required execution plan for this step.",
};
private static bool TryFindIssuedTaskBinding(
CommanderRuntime commander,
FactionPlanStepRuntime step,
out StepExecutionBinding binding)
{
var task = commander.IssuedTasks.FirstOrDefault(candidate =>
string.Equals(candidate.StepId, step.Id, StringComparison.Ordinal)
&& candidate.State == FactionIssuedTaskState.Active);
if (task is null)
{
binding = default!;
return false;
}
var targetId = task.TargetSiteId
?? task.TargetFactionId
?? task.AssignedAssetIds.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault();
binding = new StepExecutionBinding(
"issued-task",
targetId,
$"Reusing active issued task {task.Kind} ({task.Id}).");
return true;
}
private static string? FindMatchingSiteId(
SimulationWorld world,
string factionId,
IndustryExpansionProject project) =>
world.ConstructionSites
.Where(site =>
string.Equals(site.FactionId, factionId, StringComparison.Ordinal)
&& string.Equals(site.TargetKind, "station-foundation", StringComparison.Ordinal)
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed
&& string.Equals(site.TargetDefinitionId, project.CommodityId, StringComparison.Ordinal)
&& string.Equals(site.BlueprintId, project.ModuleId, StringComparison.Ordinal)
&& string.Equals(site.SystemId, project.SystemId, StringComparison.Ordinal)
&& string.Equals(site.CelestialId, project.CelestialId, StringComparison.Ordinal))
.Select(site => site.Id)
.FirstOrDefault();
private static string titleCase(string value) =>
string.IsNullOrWhiteSpace(value)
? value
: char.ToUpperInvariant(value[0]) + value[1..];
private static void AssignCombatAssets(
SimulationWorld world,
CommanderRuntime commander,

View File

@@ -88,6 +88,10 @@ public sealed record FactionPlanStepSnapshot(
string? ModuleId,
string? TargetFactionId,
string? TargetSiteId,
string? StatusReason,
string? ExecutionBindingKind,
string? ExecutionBindingTargetId,
string? ExecutionBindingSummary,
string? BlockingReason,
string? Notes,
int LastEvaluatedCycle,

View File

@@ -144,6 +144,10 @@ public sealed class FactionPlanStepRuntime
public string? ModuleId { get; set; }
public string? TargetFactionId { get; set; }
public string? TargetSiteId { get; set; }
public string? StatusReason { get; set; }
public string? ExecutionBindingKind { get; set; }
public string? ExecutionBindingTargetId { get; set; }
public string? ExecutionBindingSummary { get; set; }
public string? BlockingReason { get; set; }
public string? Notes { get; set; }
public int LastEvaluatedCycle { get; set; }

View File

@@ -7,9 +7,9 @@ internal static class FactionIndustryPlanner
private const float CommodityTargetLevelSeconds = 240f;
private const float WaterTargetLevelSeconds = 300f;
internal static IndustryExpansionProject? AnalyzeCommodityNeed(SimulationWorld world, string factionId, string commodityId)
internal static IndustryExpansionProject? AnalyzeCommodityNeed(SimulationWorld world, string factionId, string commodityId, bool ignoreActiveExpansionProject = false)
{
if (HasActiveExpansionProject(world, factionId))
if (!ignoreActiveExpansionProject && HasActiveExpansionProject(world, factionId))
{
return null;
}
@@ -41,9 +41,9 @@ internal static class FactionIndustryPlanner
supportStation.Id);
}
internal static IndustryExpansionProject? AnalyzeShipyardNeed(SimulationWorld world, string factionId)
internal static IndustryExpansionProject? AnalyzeShipyardNeed(SimulationWorld world, string factionId, bool ignoreActiveExpansionProject = false)
{
if (HasActiveExpansionProject(world, factionId))
if (!ignoreActiveExpansionProject && HasActiveExpansionProject(world, factionId))
{
return null;
}
@@ -79,16 +79,16 @@ internal static class FactionIndustryPlanner
if (!string.IsNullOrWhiteSpace(bottleneckCommodity))
{
return AnalyzeCommodityNeed(world, factionId, bottleneckCommodity);
return AnalyzeCommodityNeed(world, factionId, bottleneckCommodity, ignoreActiveExpansionProject);
}
return CreateShipyardFoundationProject(world, factionId);
return CreateShipyardFoundationProject(world, factionId, ignoreActiveExpansionProject);
}
internal static IndustryExpansionProject? CreateShipyardFoundationProject(SimulationWorld world, string factionId)
internal static IndustryExpansionProject? CreateShipyardFoundationProject(SimulationWorld world, string factionId, bool ignoreActiveExpansionProject = false)
{
const string shipyardModuleId = "module_gen_build_l_01";
if (HasActiveExpansionProject(world, factionId))
if (!ignoreActiveExpansionProject && HasActiveExpansionProject(world, factionId))
{
return null;
}

View File

@@ -18,6 +18,7 @@ builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSect
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));
builder.Services.AddFastEndpoints();
builder.Services.AddSingleton<WorldService>();
builder.Services.AddSingleton<TelemetryService>();
builder.Services.AddHostedService<SimulationHostedService>();
var app = builder.Build();

View File

@@ -21,7 +21,7 @@ internal sealed class ShipBehaviorStateMachine
new PatrolShipBehaviorState(),
new AttackTargetShipBehaviorState(),
new TradeHaulShipBehaviorState(),
new ResourceHarvestShipBehaviorState("auto-mine", "ore", "mining"),
new ResourceHarvestShipBehaviorState("auto-mine", null, "mining"),
new ConstructStationShipBehaviorState(),
};

View File

@@ -58,10 +58,10 @@ internal sealed class PatrolShipBehaviorState : IShipBehaviorState
internal sealed class ResourceHarvestShipBehaviorState : IShipBehaviorState
{
private readonly string resourceItemId;
private readonly string? resourceItemId;
private readonly string requiredModule;
public ResourceHarvestShipBehaviorState(string kind, string resourceItemId, string requiredModule)
public ResourceHarvestShipBehaviorState(string kind, string? resourceItemId, string requiredModule)
{
Kind = kind;
this.resourceItemId = resourceItemId;

View File

@@ -291,11 +291,18 @@ internal sealed class ShipControlService
};
}
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string resourceItemId, string requiredModule)
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string? resourceItemId, string requiredModule)
{
var behavior = ship.DefaultBehavior;
var cargoItemId = ship.Inventory.Keys.FirstOrDefault();
var targetResourceItemId = SelectMiningResourceItem(world, ship, cargoItemId ?? behavior.ItemId ?? resourceItemId);
if (string.IsNullOrWhiteSpace(targetResourceItemId))
{
behavior.Phase = null;
ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold);
return;
}
if (!string.Equals(behavior.ItemId, targetResourceItemId, StringComparison.Ordinal))
{
behavior.ItemId = targetResourceItemId;
@@ -426,22 +433,22 @@ internal sealed class ShipControlService
}
}
private static string SelectMiningResourceItem(SimulationWorld world, ShipRuntime ship, string fallbackItemId)
private static string? SelectMiningResourceItem(SimulationWorld world, ShipRuntime ship, string? fallbackItemId)
{
var candidateItemId = world.MarketOrders
.Where(order =>
string.Equals(order.FactionId, ship.FactionId, StringComparison.Ordinal)
&& order.Kind == MarketOrderKinds.Buy
&& order.ConstructionSiteId is null
&& order.State != MarketOrderStateKinds.Cancelled
&& order.RemainingAmount > 0.01f)
.SelectMany(order => FactionIndustryPlanner.ResolveRootResourceItems(world, order.ItemId)
.Select(itemId => new
{
ItemId = itemId,
Score = order.RemainingAmount * MathF.Max(0.25f, order.Valuation),
}))
.Where(entry =>
CanShipMineItem(world, ship, entry.ItemId)
&& world.Nodes.Any(node => string.Equals(node.ItemId, entry.ItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
.Select(order => new
{
ItemId = order.ItemId,
Score = order.RemainingAmount * MathF.Max(0.25f, order.Valuation),
})
.Where(entry => CanShipMineItem(world, ship, entry.ItemId))
.Where(entry => world.Nodes.Any(node => string.Equals(node.ItemId, entry.ItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
.GroupBy(entry => entry.ItemId, StringComparer.Ordinal)
.Select(group => new
{
@@ -457,7 +464,8 @@ internal sealed class ShipControlService
return candidateItemId;
}
if (CanShipMineItem(world, ship, fallbackItemId)
if (!string.IsNullOrWhiteSpace(fallbackItemId)
&& CanShipMineItem(world, ship, fallbackItemId)
&& world.Nodes.Any(node => string.Equals(node.ItemId, fallbackItemId, StringComparison.Ordinal) && node.OreRemaining > 0.01f))
{
return fallbackItemId;

View File

@@ -1,5 +1,6 @@
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
using static SpaceGame.Api.Stations.Simulation.StationSimulationService;
namespace SpaceGame.Api.Ships.Simulation;
@@ -331,7 +332,8 @@ internal sealed partial class ShipTaskExecutionService
}
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
moved = MathF.Min(moved, GetInventoryAmount(station.Inventory, required.Key));
var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key));
moved = MathF.Min(moved, available);
if (moved <= 0.01f)
{
continue;
@@ -356,7 +358,8 @@ internal sealed partial class ShipTaskExecutionService
}
var moved = MathF.Min(remaining, world.Balance.TransferRate * deltaSeconds);
moved = MathF.Min(moved, GetInventoryAmount(station.Inventory, required.Key));
var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key));
moved = MathF.Min(moved, available);
if (moved <= 0.01f)
{
continue;

View File

@@ -30,14 +30,15 @@ public sealed class SimulationEngine
{
var nowUtc = DateTimeOffset.UtcNow;
var events = new List<SimulationEventRecord>();
var simulationDeltaSeconds = deltaSeconds * MathF.Max(world.Balance.SimulationSpeedMultiplier, 0.01f);
world.OrbitalTimeSeconds += deltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
_orbitalStateUpdater.Update(world);
_infrastructureSimulation.UpdateClaims(world, events);
_infrastructureSimulation.UpdateConstructionSites(world, events);
_commanderPlanning.UpdateCommanders(this, world, deltaSeconds, events);
_stationLifecycle.UpdateStations(world, deltaSeconds, events);
_commanderPlanning.UpdateCommanders(this, world, simulationDeltaSeconds, events);
_stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events);
foreach (var ship in world.Ships.ToList())
{
@@ -54,10 +55,10 @@ public sealed class SimulationEngine
_shipControl.RefreshControlLayers(ship, world);
_shipControl.PlanControllerTask(this, ship, world);
var controllerEvent = _shipTaskExecution.UpdateControllerTask(ship, world, deltaSeconds);
var controllerEvent = _shipTaskExecution.UpdateControllerTask(ship, world, simulationDeltaSeconds);
_shipControl.AdvanceControlState(this, ship, world, controllerEvent);
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds);
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(simulationDeltaSeconds);
_shipControl.TrackHistory(ship, controllerEvent);
_shipControl.EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events);
}
@@ -75,7 +76,7 @@ public sealed class SimulationEngine
public void PrimeDeltaBaseline(SimulationWorld world) =>
_projection.PrimeDeltaBaseline(world);
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string resourceItemId, string requiredModule) =>
internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string? resourceItemId, string requiredModule) =>
_shipControl.PlanResourceHarvest(ship, world, resourceItemId, requiredModule);
internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world) =>

View File

@@ -895,6 +895,10 @@ internal sealed class SimulationProjectionService
step.ModuleId,
step.TargetFactionId,
step.TargetSiteId,
step.StatusReason,
step.ExecutionBindingKind,
step.ExecutionBindingTargetId,
step.ExecutionBindingSummary,
step.BlockingReason,
step.Notes,
step.LastEvaluatedCycle,

View File

@@ -232,8 +232,13 @@ internal sealed class InfrastructureSimulationService
"power" => "energycells",
"refinery" => "refinedmetals",
"water" => "water",
"graphene" => "graphene",
"siliconwafers" => "siliconwafers",
"hullparts" => "hullparts",
"claytronics" => "claytronics",
"quantumtubes" => "quantumtubes",
"antimattercells" => "antimattercells",
"superfluidcoolant" => "superfluidcoolant",
_ => null,
};
@@ -724,6 +729,11 @@ internal sealed class InfrastructureSimulationService
private static float GetTargetLevelSeconds(string commodityId) =>
string.Equals(commodityId, "energycells", StringComparison.Ordinal) ? EnergyTargetLevelSeconds :
string.Equals(commodityId, "water", StringComparison.Ordinal) ? 300f :
string.Equals(commodityId, "graphene", StringComparison.Ordinal) ? 240f :
string.Equals(commodityId, "siliconwafers", StringComparison.Ordinal) ? 240f :
string.Equals(commodityId, "quantumtubes", StringComparison.Ordinal) ? 240f :
string.Equals(commodityId, "antimattercells", StringComparison.Ordinal) ? 240f :
string.Equals(commodityId, "superfluidcoolant", StringComparison.Ordinal) ? 240f :
CommodityTargetLevelSeconds;
internal static void PrepareNextConstructionSiteStep(SimulationWorld world, StationRuntime station, ConstructionSiteRuntime site)

View File

@@ -24,12 +24,25 @@ internal sealed class StationSimulationService
var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics");
var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals");
var iceReserve = role == "water" ? 260f : 0f;
var methaneReserve = role == "graphene" ? 320f : 0f;
var hydrogenReserve = role == "antimattercells" ? 320f : 0f;
var heliumReserve = role == "superfluidcoolant" ? 320f : 0f;
var siliconReserve = role == "siliconwafers" ? 240f : 0f;
var grapheneInputReserve = role == "quantumtubes" ? 160f : 0f;
var superfluidCoolantInputReserve = role == "quantumtubes" ? 120f : 0f;
var antimatterCellsInputReserve = role == "claytronics" ? 120f : 0f;
var quantumTubesInputReserve = role == "claytronics" ? 120f : 0f;
var energyReserve = role switch
{
"power" => 120f,
"refinery" => 160f,
"hullparts" => 180f,
"claytronics" => 220f,
"graphene" => 160f,
"siliconwafers" => 160f,
"antimattercells" => 160f,
"superfluidcoolant" => 160f,
"quantumtubes" => 160f,
"water" => 140f,
_ => 60f,
} + constructionEnergyReserve;
@@ -43,6 +56,11 @@ internal sealed class StationSimulationService
var oreReserve = role == "refinery" ? 260f : 0f;
var hullpartsReserve = MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
var claytronicsReserve = MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f);
var grapheneReserve = role == "graphene" ? 120f : 0f;
var siliconWafersReserve = role == "siliconwafers" ? 120f : 0f;
var antimatterCellsReserve = role == "antimattercells" ? 120f : 0f;
var superfluidCoolantReserve = role == "superfluidcoolant" ? 120f : 0f;
var quantumTubesReserve = role == "quantumtubes" ? 120f : 0f;
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
&& FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military")
? 90f
@@ -51,22 +69,98 @@ internal sealed class StationSimulationService
AddDemandOrder(desiredOrders, station, "water", ScaleReserveByEconomy(economy, "water", waterReserve), valuationBase: ScaleDemandValuation(economy, "water", 1.1f));
AddDemandOrder(desiredOrders, station, "energycells", ScaleReserveByEconomy(economy, "energycells", energyReserve), valuationBase: ScaleDemandValuation(economy, "energycells", 1.0f));
AddDemandOrder(desiredOrders, station, "ice", ScaleReserveByEconomy(economy, "ice", iceReserve), valuationBase: ScaleDemandValuation(economy, "ice", 1.0f));
AddDemandOrder(desiredOrders, station, "methane", ScaleReserveByEconomy(economy, "methane", methaneReserve), valuationBase: ScaleDemandValuation(economy, "methane", 1.0f));
AddDemandOrder(desiredOrders, station, "hydrogen", ScaleReserveByEconomy(economy, "hydrogen", hydrogenReserve), valuationBase: ScaleDemandValuation(economy, "hydrogen", 1.0f));
AddDemandOrder(desiredOrders, station, "helium", ScaleReserveByEconomy(economy, "helium", heliumReserve), valuationBase: ScaleDemandValuation(economy, "helium", 1.0f));
AddDemandOrder(desiredOrders, station, "ore", ScaleReserveByEconomy(economy, "ore", oreReserve), valuationBase: ScaleDemandValuation(economy, "ore", 1.0f));
AddDemandOrder(desiredOrders, station, "silicon", ScaleReserveByEconomy(economy, "silicon", siliconReserve), valuationBase: ScaleDemandValuation(economy, "silicon", 1.0f));
AddDemandOrder(desiredOrders, station, "refinedmetals", ScaleReserveByEconomy(economy, "refinedmetals", MathF.Max(refinedReserve, constructionRefinedReserve)), valuationBase: ScaleDemandValuation(economy, "refinedmetals", 1.15f));
AddDemandOrder(desiredOrders, station, "hullparts", ScaleReserveByEconomy(economy, "hullparts", hullpartsReserve + shipPartsReserve), valuationBase: ScaleDemandValuation(economy, "hullparts", 1.3f));
AddDemandOrder(desiredOrders, station, "claytronics", ScaleReserveByEconomy(economy, "claytronics", claytronicsReserve), valuationBase: ScaleDemandValuation(economy, "claytronics", 1.35f));
AddDemandOrder(desiredOrders, station, "graphene", ScaleReserveByEconomy(economy, "graphene", grapheneReserve), valuationBase: ScaleDemandValuation(economy, "graphene", 1.05f));
AddDemandOrder(desiredOrders, station, "siliconwafers", ScaleReserveByEconomy(economy, "siliconwafers", siliconWafersReserve), valuationBase: ScaleDemandValuation(economy, "siliconwafers", 1.05f));
AddDemandOrder(desiredOrders, station, "antimattercells", ScaleReserveByEconomy(economy, "antimattercells", antimatterCellsReserve), valuationBase: ScaleDemandValuation(economy, "antimattercells", 1.05f));
AddDemandOrder(desiredOrders, station, "superfluidcoolant", ScaleReserveByEconomy(economy, "superfluidcoolant", superfluidCoolantReserve), valuationBase: ScaleDemandValuation(economy, "superfluidcoolant", 1.05f));
AddDemandOrder(desiredOrders, station, "graphene", ScaleReserveByEconomy(economy, "graphene", grapheneInputReserve), valuationBase: ScaleDemandValuation(economy, "graphene", 1.1f));
AddDemandOrder(desiredOrders, station, "superfluidcoolant", ScaleReserveByEconomy(economy, "superfluidcoolant", superfluidCoolantInputReserve), valuationBase: ScaleDemandValuation(economy, "superfluidcoolant", 1.1f));
AddDemandOrder(desiredOrders, station, "antimattercells", ScaleReserveByEconomy(economy, "antimattercells", antimatterCellsInputReserve), valuationBase: ScaleDemandValuation(economy, "antimattercells", 1.1f));
AddDemandOrder(desiredOrders, station, "quantumtubes", ScaleReserveByEconomy(economy, "quantumtubes", quantumTubesInputReserve), valuationBase: ScaleDemandValuation(economy, "quantumtubes", 1.1f));
AddDemandOrder(desiredOrders, station, "quantumtubes", ScaleReserveByEconomy(economy, "quantumtubes", quantumTubesReserve), valuationBase: ScaleDemandValuation(economy, "quantumtubes", 1.05f));
AddSupplyOrder(desiredOrders, station, "water", ScaleSupplyTriggerByEconomy(economy, "water", waterReserve * 1.5f), reserveFloor: waterReserve, valuationBase: ScaleSupplyValuation(economy, "water", 0.65f));
AddSupplyOrder(desiredOrders, station, "energycells", ScaleSupplyTriggerByEconomy(economy, "energycells", energyReserve * 1.4f), reserveFloor: energyReserve, valuationBase: ScaleSupplyValuation(economy, "energycells", 0.7f));
AddSupplyOrder(desiredOrders, station, "ice", ScaleSupplyTriggerByEconomy(economy, "ice", iceReserve * 1.4f), reserveFloor: iceReserve, valuationBase: ScaleSupplyValuation(economy, "ice", 0.5f));
AddSupplyOrder(desiredOrders, station, "methane", ScaleSupplyTriggerByEconomy(economy, "methane", methaneReserve * 1.4f), reserveFloor: methaneReserve, valuationBase: ScaleSupplyValuation(economy, "methane", 0.7f));
AddSupplyOrder(desiredOrders, station, "hydrogen", ScaleSupplyTriggerByEconomy(economy, "hydrogen", hydrogenReserve * 1.4f), reserveFloor: hydrogenReserve, valuationBase: ScaleSupplyValuation(economy, "hydrogen", 0.7f));
AddSupplyOrder(desiredOrders, station, "helium", ScaleSupplyTriggerByEconomy(economy, "helium", heliumReserve * 1.4f), reserveFloor: heliumReserve, valuationBase: ScaleSupplyValuation(economy, "helium", 0.7f));
AddSupplyOrder(desiredOrders, station, "ore", ScaleSupplyTriggerByEconomy(economy, "ore", oreReserve * 1.4f), reserveFloor: oreReserve, valuationBase: ScaleSupplyValuation(economy, "ore", 0.7f));
AddSupplyOrder(desiredOrders, station, "silicon", ScaleSupplyTriggerByEconomy(economy, "silicon", siliconReserve * 1.4f), reserveFloor: siliconReserve, valuationBase: ScaleSupplyValuation(economy, "silicon", 0.7f));
AddSupplyOrder(desiredOrders, station, "refinedmetals", ScaleSupplyTriggerByEconomy(economy, "refinedmetals", MathF.Max(refinedReserve, constructionRefinedReserve) * 1.4f), reserveFloor: MathF.Max(refinedReserve, constructionRefinedReserve), valuationBase: ScaleSupplyValuation(economy, "refinedmetals", 0.95f));
AddSupplyOrder(desiredOrders, station, "hullparts", ScaleSupplyTriggerByEconomy(economy, "hullparts", MathF.Max(hullpartsReserve * 1.35f, hullpartsReserve + 40f)), reserveFloor: hullpartsReserve, valuationBase: ScaleSupplyValuation(economy, "hullparts", 1.05f));
AddSupplyOrder(desiredOrders, station, "claytronics", ScaleSupplyTriggerByEconomy(economy, "claytronics", MathF.Max(claytronicsReserve * 1.35f, claytronicsReserve + 30f)), reserveFloor: claytronicsReserve, valuationBase: ScaleSupplyValuation(economy, "claytronics", 1.1f));
AddSupplyOrder(desiredOrders, station, "graphene", ScaleSupplyTriggerByEconomy(economy, "graphene", MathF.Max(grapheneReserve * 1.35f, grapheneReserve + 30f)), reserveFloor: grapheneReserve, valuationBase: ScaleSupplyValuation(economy, "graphene", 0.9f));
AddSupplyOrder(desiredOrders, station, "siliconwafers", ScaleSupplyTriggerByEconomy(economy, "siliconwafers", MathF.Max(siliconWafersReserve * 1.35f, siliconWafersReserve + 30f)), reserveFloor: siliconWafersReserve, valuationBase: ScaleSupplyValuation(economy, "siliconwafers", 0.9f));
AddSupplyOrder(desiredOrders, station, "antimattercells", ScaleSupplyTriggerByEconomy(economy, "antimattercells", MathF.Max(antimatterCellsReserve * 1.35f, antimatterCellsReserve + 30f)), reserveFloor: antimatterCellsReserve, valuationBase: ScaleSupplyValuation(economy, "antimattercells", 0.9f));
AddSupplyOrder(desiredOrders, station, "superfluidcoolant", ScaleSupplyTriggerByEconomy(economy, "superfluidcoolant", MathF.Max(superfluidCoolantReserve * 1.35f, superfluidCoolantReserve + 30f)), reserveFloor: superfluidCoolantReserve, valuationBase: ScaleSupplyValuation(economy, "superfluidcoolant", 0.9f));
AddSupplyOrder(desiredOrders, station, "quantumtubes", ScaleSupplyTriggerByEconomy(economy, "quantumtubes", MathF.Max(quantumTubesReserve * 1.35f, quantumTubesReserve + 30f)), reserveFloor: quantumTubesReserve, valuationBase: ScaleSupplyValuation(economy, "quantumtubes", 0.9f));
ReconcileStationMarketOrders(world, station, desiredOrders);
}
internal static float GetStationReserveFloor(SimulationWorld world, StationRuntime station, string itemId)
{
var role = DetermineStationRole(station);
var site = GetConstructionSiteForStation(world, station.Id);
var constructionEnergyReserve = GetConstructionDemandForItem(world, site, "energycells");
var constructionHullpartsReserve = GetConstructionDemandForItem(world, site, "hullparts");
var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics");
var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals");
var shipPartsReserve = HasStationModules(station, "module_gen_build_l_01")
&& FactionCommanderHasIssuedTask(world, station.FactionId, FactionIssuedTaskKind.ProduceShips, "military")
? 90f
: 0f;
return itemId switch
{
"water" => MathF.Max(30f, station.Population * 3f),
"energycells" => role switch
{
"power" => 120f,
"refinery" => 160f,
"hullparts" => 180f,
"claytronics" => 220f,
"graphene" => 160f,
"siliconwafers" => 160f,
"antimattercells" => 160f,
"superfluidcoolant" => 160f,
"quantumtubes" => 160f,
"water" => 140f,
_ => 60f,
} + constructionEnergyReserve,
"ice" => role == "water" ? 260f : 0f,
"methane" => role == "graphene" ? 320f : 0f,
"hydrogen" => role == "antimattercells" ? 320f : 0f,
"helium" => role == "superfluidcoolant" ? 320f : 0f,
"ore" => role == "refinery" ? 260f : 0f,
"silicon" => role == "siliconwafers" ? 240f : 0f,
"refinedmetals" => MathF.Max(role switch
{
"hullparts" => 220f,
"shipyard" => 260f,
"refinery" => 80f,
_ => 0f,
}, constructionRefinedReserve),
"hullparts" => MathF.Max(constructionHullpartsReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f) + shipPartsReserve,
"claytronics" => MathF.Max(constructionClayReserve, HasStationModules(station, "module_gen_build_l_01") ? 120f : 0f),
"graphene" => MathF.Max(role == "graphene" ? 120f : 0f, role == "quantumtubes" ? 160f : 0f),
"siliconwafers" => role == "siliconwafers" ? 120f : 0f,
"antimattercells" => MathF.Max(role == "antimattercells" ? 120f : 0f, role == "claytronics" ? 120f : 0f),
"superfluidcoolant" => MathF.Max(role == "superfluidcoolant" ? 120f : 0f, role == "quantumtubes" ? 120f : 0f),
"quantumtubes" => MathF.Max(role == "quantumtubes" ? 120f : 0f, role == "claytronics" ? 120f : 0f),
_ => 0f,
};
}
internal void RunStationProduction(SimulationWorld world, StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
{
var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId);
@@ -295,6 +389,11 @@ internal sealed class StationSimulationService
"refinery" or "refinedmetals" => "refinery",
"hullparts" or "hull" => "hullparts",
"claytronics" or "clay" => "claytronics",
"graphene" => "graphene",
"siliconwafers" or "silicon-wafers" or "silicon" => "siliconwafers",
"antimattercells" or "antimatter-cells" => "antimattercells",
"superfluidcoolant" or "superfluid-coolant" => "superfluidcoolant",
"quantumtubes" or "quantum-tubes" => "quantumtubes",
"shipyard" or "ship-production" => "shipyard",
_ => "general",
};
@@ -318,6 +417,31 @@ internal sealed class StationSimulationService
return "water";
}
if (HasStationModules(station, "module_gen_prod_superfluidcoolant_01"))
{
return "superfluidcoolant";
}
if (HasStationModules(station, "module_gen_prod_quantumtubes_01"))
{
return "quantumtubes";
}
if (HasStationModules(station, "module_gen_prod_antimattercells_01"))
{
return "antimattercells";
}
if (HasStationModules(station, "module_gen_prod_siliconwafers_01"))
{
return "siliconwafers";
}
if (HasStationModules(station, "module_gen_prod_graphene_01"))
{
return "graphene";
}
if (HasStationModules(station, "module_gen_prod_claytronics_01"))
{
return "claytronics";

View File

@@ -0,0 +1,16 @@
using FastEndpoints;
using SpaceGame.Api.Universe.Simulation;
namespace SpaceGame.Api.Universe.Api;
public sealed class GetBalanceHandler(WorldService worldService) : EndpointWithoutRequest
{
public override void Configure()
{
Get("/api/balance");
AllowAnonymous();
}
public override Task HandleAsync(CancellationToken cancellationToken) =>
SendOkAsync(worldService.GetBalance(), cancellationToken);
}

View File

@@ -0,0 +1,49 @@
using System.Runtime.InteropServices;
using FastEndpoints;
using SpaceGame.Api.Universe.Simulation;
namespace SpaceGame.Api.Universe.Api;
public sealed class GetTelemetryHandler(TelemetryService telemetry, WorldService worldService) : EndpointWithoutRequest
{
public override void Configure()
{
Get("/api/telemetry");
AllowAnonymous();
}
public override Task HandleAsync(CancellationToken cancellationToken)
{
var status = worldService.GetStatus();
var connections = worldService.GetConnectionStats();
var uptime = telemetry.Uptime;
return SendOkAsync(new
{
process = new
{
uptimeSeconds = uptime.TotalSeconds,
cpuPercent = Math.Round(telemetry.CpuPercent, 1),
workingSetMb = Math.Round(telemetry.WorkingSetBytes / 1_048_576.0, 1),
gcMemoryMb = Math.Round(telemetry.GcMemoryBytes / 1_048_576.0, 1),
threadCount = telemetry.ThreadCount,
processorCount = Environment.ProcessorCount,
},
simulation = new
{
sequence = status.Sequence,
connectedClients = connections.ConnectedClients,
deltaHistoryCount = connections.DeltaHistoryCount,
tickIntervalMs = 200,
},
runtime = new
{
frameworkDescription = RuntimeInformation.FrameworkDescription,
osDescription = RuntimeInformation.OSDescription,
gcGen0 = GC.CollectionCount(0),
gcGen1 = GC.CollectionCount(1),
gcGen2 = GC.CollectionCount(2),
},
}, cancellationToken);
}
}

View File

@@ -0,0 +1,20 @@
using FastEndpoints;
using SpaceGame.Api.Definitions;
using SpaceGame.Api.Universe.Simulation;
namespace SpaceGame.Api.Universe.Api;
public sealed class UpdateBalanceHandler(WorldService worldService) : Endpoint<BalanceDefinition>
{
public override void Configure()
{
Put("/api/balance");
AllowAnonymous();
}
public override Task HandleAsync(BalanceDefinition req, CancellationToken cancellationToken)
{
var applied = worldService.UpdateBalance(req);
return SendOkAsync(applied, cancellationToken);
}
}

View File

@@ -5,7 +5,7 @@ public sealed class SimulationWorld
{
public required string Label { get; init; }
public required int Seed { get; init; }
public required BalanceDefinition Balance { get; init; }
public required BalanceDefinition Balance { get; set; }
public required List<SystemRuntime> Systems { get; init; }
public required List<ResourceNodeRuntime> Nodes { get; init; }
public required List<CelestialRuntime> Celestials { get; init; }

View File

@@ -224,6 +224,23 @@ internal sealed class SpatialBuilder
private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
{
if (!string.IsNullOrWhiteSpace(definition.AnchorReference))
{
var anchorId = definition.AnchorReference.ToLowerInvariant() switch
{
var reference when reference.StartsWith("star-", StringComparison.Ordinal)
=> $"node-{graph.SystemId}-{reference}",
var reference when reference.StartsWith("planet-", StringComparison.Ordinal)
=> $"node-{graph.SystemId}-{reference}",
_ => null,
};
if (anchorId is not null)
{
return graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, anchorId, StringComparison.Ordinal));
}
}
if (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0)
{
return null;

View File

@@ -9,7 +9,7 @@ internal sealed class SystemGenerationService
internal List<SolarSystemDefinition> InjectSpecialSystems(IReadOnlyList<SolarSystemDefinition> authoredSystems) =>
authoredSystems
.Select(CloneSystemDefinition)
.Select((system, index) => EnsureStrategicResourceCoverage(CloneSystemDefinition(system), index))
.ToList();
internal List<SolarSystemDefinition> ExpandSystems(
@@ -126,6 +126,7 @@ internal sealed class SystemGenerationService
.Select(node => new ResourceNodeDefinition
{
SourceKind = node.SourceKind,
AnchorReference = node.AnchorReference,
Angle = node.Angle,
RadiusOffset = node.RadiusOffset,
InclinationDegrees = node.InclinationDegrees,
@@ -137,7 +138,7 @@ internal sealed class SystemGenerationService
})
.ToList();
return new SolarSystemDefinition
return EnsureStrategicResourceCoverage(new SolarSystemDefinition
{
Id = id,
Label = label,
@@ -161,7 +162,7 @@ internal sealed class SystemGenerationService
},
ResourceNodes = resourceNodes,
Planets = planets,
};
}, generatedIndex + 1024);
}
private static SolarSystemDefinition CloneSystemDefinition(SolarSystemDefinition definition)
@@ -182,6 +183,7 @@ internal sealed class SystemGenerationService
ResourceNodes = definition.ResourceNodes.Select(node => new ResourceNodeDefinition
{
SourceKind = node.SourceKind,
AnchorReference = node.AnchorReference,
Angle = node.Angle,
RadiusOffset = node.RadiusOffset,
InclinationDegrees = node.InclinationDegrees,
@@ -223,6 +225,7 @@ internal sealed class SystemGenerationService
nodes.AddRange(template.ResourceNodes.Select(node => new ResourceNodeDefinition
{
SourceKind = node.SourceKind,
AnchorReference = node.AnchorReference,
Angle = node.Angle,
RadiusOffset = node.RadiusOffset,
InclinationDegrees = node.InclinationDegrees,
@@ -234,10 +237,30 @@ internal sealed class SystemGenerationService
}));
}
nodes.AddRange(BuildAsteroidBeltNodes(generatedIndex, planets));
return nodes;
}
private static SolarSystemDefinition EnsureStrategicResourceCoverage(SolarSystemDefinition system, int seed)
{
for (var index = 0; index < system.ResourceNodes.Count; index += 1)
{
system.ResourceNodes[index] = SanitizeResourceNode(system.ResourceNodes[index], system.Planets, seed, index);
}
var requiredItems = new[] { "ore", "silicon", "ice", "hydrogen", "helium", "methane" };
foreach (var itemId in requiredItems)
{
if (system.ResourceNodes.Any(node => string.Equals(node.ItemId, itemId, StringComparison.Ordinal)))
{
continue;
}
system.ResourceNodes.Add(BuildStrategicResourceNode(itemId, system.Planets, seed, system.ResourceNodes.Count));
}
return system;
}
private static List<Vector3> BuildGalaxyPositions(IReadOnlyCollection<Vector3> occupiedPositions, int count)
{
var allPositions = occupiedPositions.ToList();
@@ -303,25 +326,124 @@ internal sealed class SystemGenerationService
return $"gen-{ordinal}-{slug}";
}
private static IEnumerable<ResourceNodeDefinition> BuildAsteroidBeltNodes(int generatedIndex, IReadOnlyList<PlanetDefinition> planets)
private static ResourceNodeDefinition BuildStrategicResourceNode(
string itemId,
IReadOnlyList<PlanetDefinition> planets,
int seed,
int ordinal)
{
var nodeCount = 4 + (generatedIndex % 4);
var oreAmount = 1000f;
for (var index = 0; index < nodeCount; index += 1)
var anchorPlanetIndex = ResolveStrategicResourceAnchorPlanetIndex(itemId, planets);
return new ResourceNodeDefinition
{
yield return new ResourceNodeDefinition
SourceKind = "local-space",
AnchorReference = ResolveStrategicAnchorReference(itemId, planets, ordinal),
Angle = (MathF.PI * 2f * ((ordinal % 7) / 7f)) + Jitter(seed, 400 + ordinal, 0.35f),
RadiusOffset = 150000f + Jitter(seed, 460 + ordinal, 42000f),
InclinationDegrees = Jitter(seed, 520 + ordinal, 10f),
AnchorPlanetIndex = anchorPlanetIndex,
OreAmount = itemId switch
{
SourceKind = "asteroid-belt",
Angle = ((MathF.PI * 2f) / nodeCount) * index + Jitter(generatedIndex, 180 + index, 0.22f),
RadiusOffset = 120000f + Jitter(generatedIndex, 200 + index, 36000f),
InclinationDegrees = Jitter(generatedIndex, 280 + index, 12f),
AnchorPlanetIndex = ResolveAsteroidAnchorPlanetIndex(planets),
OreAmount = oreAmount,
ItemId = "ore",
ShardCount = 6 + (index % 4),
};
"ore" => 12000f,
"silicon" => 10000f,
"ice" => 9000f,
_ => 8000f,
},
ItemId = itemId,
ShardCount = itemId switch
{
"ore" or "silicon" or "ice" => 8,
_ => 6,
},
};
}
private static ResourceNodeDefinition SanitizeResourceNode(
ResourceNodeDefinition node,
IReadOnlyList<PlanetDefinition> planets,
int seed,
int ordinal)
{
node.SourceKind = "local-space";
node.AnchorReference ??= ResolveLegacyAnchorReference(node, planets, seed, ordinal);
return node;
}
private static string ResolveLegacyAnchorReference(
ResourceNodeDefinition node,
IReadOnlyList<PlanetDefinition> planets,
int seed,
int ordinal)
{
if (node.AnchorMoonIndex is int moonIndex && node.AnchorPlanetIndex is int planetIndex && planetIndex >= 0)
{
return $"planet-{planetIndex + 1}-moon-{moonIndex + 1}";
}
if (node.AnchorPlanetIndex is int anchoredPlanetIndex && anchoredPlanetIndex >= 0)
{
return $"planet-{anchoredPlanetIndex + 1}";
}
return ResolveStrategicAnchorReference(node.ItemId, planets, ordinal + seed);
}
private static string ResolveStrategicAnchorReference(string itemId, IReadOnlyList<PlanetDefinition> planets, int ordinal)
{
if (itemId is "hydrogen" or "helium" or "methane")
{
var gasGiantIndex = planets
.Select((planet, index) => (planet, index))
.FirstOrDefault(entry => entry.planet.PlanetType is "gas-giant" or "ice-giant")
.index;
return gasGiantIndex > 0 || (planets.Count > 0 && planets[0].PlanetType is "gas-giant" or "ice-giant")
? $"planet-{gasGiantIndex + 1}"
: "star-1";
}
if (itemId == "ice")
{
var moonAnchor = planets
.Select((planet, index) => (planet, index))
.FirstOrDefault(entry => entry.planet.Moons.Count > 0 && entry.planet.PlanetType is "ice" or "ice-giant" or "oceanic");
if (moonAnchor.planet is not null && moonAnchor.planet.Moons.Count > 0)
{
return $"planet-{moonAnchor.index + 1}-moon-1";
}
}
var anchorPlanetIndex = ResolveStrategicResourceAnchorPlanetIndex(itemId, planets);
var lagrange = (ordinal % 3) switch
{
0 => "l1",
1 => "l4",
_ => "l5",
};
return $"planet-{anchorPlanetIndex + 1}-{lagrange}";
}
private static int ResolveStrategicResourceAnchorPlanetIndex(string itemId, IReadOnlyList<PlanetDefinition> planets)
{
if (planets.Count == 0)
{
return 0;
}
bool MatchesPlanetType(PlanetDefinition planet) => itemId switch
{
"hydrogen" or "helium" or "methane" => planet.PlanetType is "gas-giant" or "ice-giant",
"ice" => planet.PlanetType is "ice" or "ice-giant" or "oceanic",
_ => planet.PlanetType is not "gas-giant" and not "ice-giant",
};
for (var index = 0; index < planets.Count; index += 1)
{
if (MatchesPlanetType(planets[index]))
{
return index;
}
}
return ResolveAsteroidAnchorPlanetIndex(planets);
}
private static int ResolveAsteroidAnchorPlanetIndex(IReadOnlyList<PlanetDefinition> planets)

View File

@@ -34,7 +34,8 @@ internal sealed class WorldBuilder(
systemsById,
spatialLayout.SystemGraphs,
spatialLayout.Celestials,
catalog.ModuleDefinitions);
catalog.ModuleDefinitions,
catalog.ItemDefinitions);
seedingService.InitializeStationStockpiles(stations);
var refinery = seedingService.SelectRefineryStation(stations, scenario);
@@ -106,7 +107,8 @@ internal sealed class WorldBuilder(
IReadOnlyDictionary<string, SystemRuntime> systemsById,
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
IReadOnlyCollection<CelestialRuntime> celestials,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions)
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
{
var stations = new List<StationRuntime>();
var stationIdCounter = 0;
@@ -136,9 +138,7 @@ internal sealed class WorldBuilder(
stations.Add(station);
placement.AnchorCelestial.OccupyingStructureId = station.Id;
var startingModules = plan.StartingModules.Count > 0
? plan.StartingModules
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_solid_m_01", "module_arg_stor_liquid_m_01"];
var startingModules = BuildStartingModules(plan, moduleDefinitions, itemDefinitions);
foreach (var moduleId in startingModules)
{
@@ -149,6 +149,91 @@ internal sealed class WorldBuilder(
return stations;
}
private static IReadOnlyList<string> BuildStartingModules(
InitialStationDefinition plan,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
{
var startingModules = new List<string>(plan.StartingModules.Count > 0
? plan.StartingModules
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_container_m_01"]);
EnsureStartingModule(startingModules, "module_arg_dock_m_01_lowtech");
var objectiveModuleId = GetObjectiveStartingModuleId(plan.Objective);
if (!string.IsNullOrWhiteSpace(objectiveModuleId))
{
EnsureStartingModule(startingModules, objectiveModuleId);
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal))
{
EnsureStartingModule(startingModules, "module_gen_prod_energycells_01");
}
foreach (var storageModuleId in GetRequiredStartingStorageModules(objectiveModuleId, moduleDefinitions, itemDefinitions))
{
EnsureStartingModule(startingModules, storageModuleId);
}
}
return startingModules;
}
private static string? GetObjectiveStartingModuleId(string? objective) =>
StationSimulationService.NormalizeStationObjective(objective) switch
{
"power" => "module_gen_prod_energycells_01",
"refinery" => "module_gen_ref_ore_01",
"graphene" => "module_gen_prod_graphene_01",
"siliconwafers" => "module_gen_prod_siliconwafers_01",
"hullparts" => "module_gen_prod_hullparts_01",
"claytronics" => "module_gen_prod_claytronics_01",
"quantumtubes" => "module_gen_prod_quantumtubes_01",
"antimattercells" => "module_gen_prod_antimattercells_01",
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
"water" => "module_gen_prod_water_01",
_ => null,
};
private static IEnumerable<string> GetRequiredStartingStorageModules(
string moduleId,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
{
if (!moduleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
{
yield break;
}
foreach (var wareId in moduleDefinition.Production
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
.Concat(moduleDefinition.Products)
.Distinct(StringComparer.Ordinal))
{
if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition))
{
continue;
}
var storageModuleId = itemDefinition.CargoKind switch
{
"solid" => "module_arg_stor_solid_m_01",
"liquid" => "module_arg_stor_liquid_m_01",
_ => "module_arg_stor_container_m_01",
};
yield return storageModuleId;
}
}
private static void EnsureStartingModule(List<string> modules, string moduleId)
{
if (!modules.Contains(moduleId, StringComparer.Ordinal))
{
modules.Add(moduleId);
}
}
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById)

View File

@@ -65,33 +65,6 @@ internal sealed class WorldSeedingService
foreach (var station in stations)
{
InitializeStationPopulation(station);
if (station.InstalledModules.Contains("module_gen_prod_energycells_01", StringComparer.Ordinal))
{
station.Inventory["energycells"] = MathF.Max(GetInventoryAmount(station.Inventory, "energycells"), 240f);
}
if (station.InstalledModules.Contains("module_gen_prod_refinedmetals_01", StringComparer.Ordinal))
{
station.Inventory["ore"] = MathF.Max(GetInventoryAmount(station.Inventory, "ore"), 220f);
}
if (station.InstalledModules.Contains("module_gen_prod_hullparts_01", StringComparer.Ordinal))
{
station.Inventory["refinedmetals"] = MathF.Max(GetInventoryAmount(station.Inventory, "refinedmetals"), 240f);
station.Inventory["graphene"] = MathF.Max(GetInventoryAmount(station.Inventory, "graphene"), 80f);
}
if (station.InstalledModules.Contains("module_gen_prod_claytronics_01", StringComparer.Ordinal))
{
station.Inventory["antimattercells"] = MathF.Max(GetInventoryAmount(station.Inventory, "antimattercells"), 90f);
station.Inventory["microchips"] = MathF.Max(GetInventoryAmount(station.Inventory, "microchips"), 120f);
station.Inventory["quantumtubes"] = MathF.Max(GetInventoryAmount(station.Inventory, "quantumtubes"), 90f);
}
if (station.Population > 0f)
{
station.Inventory["water"] = MathF.Max(60f, station.Population * 1.5f);
}
}
}
@@ -145,6 +118,11 @@ internal sealed class WorldSeedingService
foreach (var station in world.Stations)
{
if (HasSatisfiedStarterObjectiveLayout(world, station))
{
continue;
}
var moduleId = InfrastructureSimulationService.GetNextStationModuleToBuild(station, world);
if (moduleId is null || station.CelestialId is null)
{
@@ -200,6 +178,78 @@ internal sealed class WorldSeedingService
return (sites, orders);
}
private static bool HasSatisfiedStarterObjectiveLayout(SimulationWorld world, StationRuntime station)
{
var role = StationSimulationService.DetermineStationRole(station);
var objectiveModuleId = role switch
{
"power" => "module_gen_prod_energycells_01",
"refinery" => "module_gen_prod_refinedmetals_01",
"graphene" => "module_gen_prod_graphene_01",
"siliconwafers" => "module_gen_prod_siliconwafers_01",
"hullparts" => "module_gen_prod_hullparts_01",
"claytronics" => "module_gen_prod_claytronics_01",
"quantumtubes" => "module_gen_prod_quantumtubes_01",
"antimattercells" => "module_gen_prod_antimattercells_01",
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
"water" => "module_gen_prod_water_01",
_ => null,
};
if (objectiveModuleId is null)
{
return false;
}
if (!station.InstalledModules.Contains("module_arg_dock_m_01_lowtech", StringComparer.Ordinal)
|| !station.InstalledModules.Contains(objectiveModuleId, StringComparer.Ordinal))
{
return false;
}
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal)
&& !station.InstalledModules.Contains("module_gen_prod_energycells_01", StringComparer.Ordinal))
{
return false;
}
foreach (var storageModuleId in GetRequiredStorageModulesForInstalledObjective(world, objectiveModuleId))
{
if (!station.InstalledModules.Contains(storageModuleId, StringComparer.Ordinal))
{
return false;
}
}
return true;
}
private static IEnumerable<string> GetRequiredStorageModulesForInstalledObjective(SimulationWorld world, string moduleId)
{
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
{
yield break;
}
foreach (var wareId in moduleDefinition.Production
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
.Concat(moduleDefinition.Products)
.Distinct(StringComparer.Ordinal))
{
if (!world.ItemDefinitions.TryGetValue(wareId, out var itemDefinition))
{
continue;
}
yield return itemDefinition.CargoKind switch
{
"solid" => "module_arg_stor_solid_m_01",
"liquid" => "module_arg_stor_liquid_m_01",
_ => "module_arg_stor_container_m_01",
};
}
}
internal List<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
{
var policies = new List<PolicySetRuntime>(factions.Count);
@@ -385,6 +435,13 @@ internal sealed class WorldSeedingService
Color = "#ff8f70",
Credits = MinimumFactionCredits,
},
"nadir-syndicate" => new FactionRuntime
{
Id = factionId,
Label = "Nadir Syndicate",
Color = "#91e6a8",
Credits = MinimumFactionCredits,
},
_ => new FactionRuntime
{
Id = factionId,

View File

@@ -0,0 +1,44 @@
using System.Diagnostics;
namespace SpaceGame.Api.Universe.Simulation;
public sealed class TelemetryService : IDisposable
{
private readonly Process _process = Process.GetCurrentProcess();
private readonly Timer _timer;
private double _cpuPercent;
private DateTime _lastSampleTime;
private TimeSpan _lastCpuTime;
public TelemetryService()
{
_process.Refresh();
_lastSampleTime = DateTime.UtcNow;
_lastCpuTime = _process.TotalProcessorTime;
_timer = new Timer(Sample, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
}
private void Sample(object? _)
{
_process.Refresh();
var now = DateTime.UtcNow;
var cpu = _process.TotalProcessorTime;
var elapsed = (now - _lastSampleTime).TotalSeconds;
var cpuUsed = (cpu - _lastCpuTime).TotalSeconds;
Volatile.Write(ref _cpuPercent, elapsed > 0 ? cpuUsed / elapsed / Environment.ProcessorCount * 100.0 : 0);
_lastSampleTime = now;
_lastCpuTime = cpu;
}
public double CpuPercent => Volatile.Read(ref _cpuPercent);
public long WorkingSetBytes => _process.WorkingSet64;
public long GcMemoryBytes => GC.GetTotalMemory(false);
public int ThreadCount => _process.Threads.Count;
public TimeSpan Uptime => DateTime.UtcNow - _process.StartTime.ToUniversalTime();
public void Dispose()
{
_timer.Dispose();
_process.Dispose();
}
}

View File

@@ -18,6 +18,7 @@ public sealed class WorldService(
private readonly Queue<WorldDelta> _history = [];
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
private long _sequence;
private BalanceDefinition? _balanceOverride;
public WorldSnapshot GetSnapshot()
{
@@ -35,6 +36,44 @@ public sealed class WorldService(
}
}
public (int ConnectedClients, int DeltaHistoryCount) GetConnectionStats()
{
lock (_sync)
{
return (_subscribers.Count, _history.Count);
}
}
public BalanceDefinition GetBalance()
{
lock (_sync)
{
var b = _world.Balance;
return new BalanceDefinition
{
SimulationSpeedMultiplier = b.SimulationSpeedMultiplier,
YPlane = b.YPlane,
ArrivalThreshold = b.ArrivalThreshold,
MiningRate = b.MiningRate,
MiningCycleSeconds = b.MiningCycleSeconds,
TransferRate = b.TransferRate,
DockingDuration = b.DockingDuration,
UndockingDuration = b.UndockingDuration,
UndockDistance = b.UndockDistance,
};
}
}
public BalanceDefinition UpdateBalance(BalanceDefinition balance)
{
lock (_sync)
{
_balanceOverride = SanitizeBalance(balance);
ApplyBalance(_world, _balanceOverride);
return GetBalance();
}
}
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
@@ -96,6 +135,10 @@ public sealed class WorldService(
lock (_sync)
{
_world = _loader.Load();
if (_balanceOverride is not null)
{
ApplyBalance(_world, _balanceOverride);
}
_sequence += 1;
_history.Clear();
@@ -127,6 +170,39 @@ public sealed class WorldService(
}
}
private static void ApplyBalance(SimulationWorld world, BalanceDefinition balance) =>
world.Balance = new BalanceDefinition
{
SimulationSpeedMultiplier = balance.SimulationSpeedMultiplier,
YPlane = balance.YPlane,
ArrivalThreshold = balance.ArrivalThreshold,
MiningRate = balance.MiningRate,
MiningCycleSeconds = balance.MiningCycleSeconds,
TransferRate = balance.TransferRate,
DockingDuration = balance.DockingDuration,
UndockingDuration = balance.UndockingDuration,
UndockDistance = balance.UndockDistance,
};
private static BalanceDefinition SanitizeBalance(BalanceDefinition candidate)
{
static float finiteOr(float value, float fallback) =>
float.IsFinite(value) ? value : fallback;
return new BalanceDefinition
{
SimulationSpeedMultiplier = MathF.Max(0.01f, finiteOr(candidate.SimulationSpeedMultiplier, 1f)),
YPlane = MathF.Max(0f, finiteOr(candidate.YPlane, 0f)),
ArrivalThreshold = MathF.Max(0.1f, finiteOr(candidate.ArrivalThreshold, 16f)),
MiningRate = MathF.Max(0f, finiteOr(candidate.MiningRate, 10f)),
MiningCycleSeconds = MathF.Max(0.1f, finiteOr(candidate.MiningCycleSeconds, 10f)),
TransferRate = MathF.Max(0f, finiteOr(candidate.TransferRate, 56f)),
DockingDuration = MathF.Max(0.1f, finiteOr(candidate.DockingDuration, 1.2f)),
UndockingDuration = MathF.Max(0.1f, finiteOr(candidate.UndockingDuration, 1.2f)),
UndockDistance = MathF.Max(0f, finiteOr(candidate.UndockDistance, 42f)),
};
}
private static bool HasMeaningfulDelta(WorldDelta delta) =>
delta.RequiresSnapshotRefresh
|| delta.Events.Count > 0

View File

@@ -6,7 +6,7 @@
}
},
"WorldGeneration": {
"TargetSystemCount": 3,
"TargetSystemCount": 10,
"IncludeSolSystem": true
},
"OrbitalSimulation": {