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

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