Refactor runtime bootstrap and ship control flows

This commit is contained in:
2026-04-03 01:12:26 -04:00
parent 0bb72bee35
commit 706e1cda8f
129 changed files with 9588 additions and 3548 deletions

View File

@@ -0,0 +1,319 @@
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
namespace SpaceGame.Api.Ships.AI;
public sealed partial class ShipAiService
{
private ShipPlanRuntime BuildBehaviorFallbackPlan(SimulationWorld world, ShipRuntime ship)
{
var (behaviorKind, sourceId) = ResolveBehaviorSource(world, ship);
var failureReason = ship.LastAccessFailureReason;
if (string.Equals(behaviorKind, Idle, StringComparison.Ordinal))
{
return CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "Idle");
}
if (IsBehaviorBlockingFailure(behaviorKind, failureReason))
{
return CreateBlockedPlan(
ship,
AiPlanSourceKind.DefaultBehavior,
sourceId,
DescribeBehaviorFallbackSummary(world, ship, behaviorKind, failureReason),
failureReason!);
}
return CreateIdlePlan(
ship,
AiPlanSourceKind.DefaultBehavior,
sourceId,
DescribeBehaviorFallbackSummary(world, ship, behaviorKind, failureReason));
}
private static bool IsBehaviorBlockingFailure(string behaviorKind, string? failureReason) => failureReason switch
{
"missing-item" => true,
"no-suitable-buyer" => true,
"no-mineable-node" when string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal) => true,
_ => false,
};
private static string DescribeBehaviorFallbackSummary(SimulationWorld world, ShipRuntime ship, string behaviorKind, string? failureReason)
{
var assignment = ResolveAssignment(world, ship);
var systemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId;
var itemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId ?? "resource";
return failureReason switch
{
"missing-item" => "No mining ware configured",
"no-suitable-buyer" => $"No buyer for {itemId} in {systemId}",
"no-mineable-node" when string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal) => $"No {itemId} to mine in {systemId}",
"no-mineable-node" => "No mineable node",
"no-home-station" => "No home station",
"no-trade-route" => "No trade route",
"no-fleet-to-supply" => "No fleet to supply",
"station-missing" => "No station to dock",
"target-ship-missing" => "No ship to follow",
"target-missing" => "No object target",
"no-salvage-target" => "No salvage target",
"no-repeat-orders" => "No repeat orders",
"no-construction-site" => "No construction site",
"support-station-missing" => "No support station",
_ => "Idle",
};
}
private ShipPlanRuntime BuildTradePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, TradeRoutePlan route, string summary)
{
return CreatePlan(
ship,
sourceKind,
sourceId,
ShipOrderKinds.TradeRoute,
summary,
[
CreateStep("step-acquire", "acquire-cargo", $"Load {route.ItemId} at {route.SourceStation.Label}",
[
CreateSubTask("sub-acquire-travel", ShipTaskKinds.Travel, $"Travel to {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, MathF.Max(route.SourceStation.Radius + 12f, 12f), 0f),
CreateSubTask("sub-acquire-dock", ShipTaskKinds.Dock, $"Dock at {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, 4f, 0f),
CreateSubTask("sub-acquire-load", ShipTaskKinds.LoadCargo, $"Load {route.ItemId}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, 0f, ship.Definition.GetTotalCargoCapacity(), itemId: route.ItemId),
CreateSubTask("sub-acquire-undock", ShipTaskKinds.Undock, $"Undock from {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, MathF.Max(4f, 12f), 0f)
]),
CreateStep("step-deliver", "deliver-cargo", $"Deliver {route.ItemId} to {route.DestinationStation.Label}",
[
CreateSubTask("sub-route-travel", ShipTaskKinds.Travel, $"Travel to {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, MathF.Max(route.DestinationStation.Radius + 12f, 12f), 0f),
CreateSubTask("sub-route-dock", ShipTaskKinds.Dock, $"Dock at {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, 4f, 0f),
CreateSubTask("sub-route-unload", ShipTaskKinds.UnloadCargo, $"Unload {route.ItemId}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, 0f, ship.Definition.GetTotalCargoCapacity(), itemId: route.ItemId),
CreateSubTask("sub-route-undock", ShipTaskKinds.Undock, $"Undock from {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, MathF.Max(4f, 12f), 0f)
])
]);
}
private ShipPlanRuntime BuildFleetSupplyPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, FleetSupplyPlan plan)
{
return CreatePlan(
ship,
sourceKind,
sourceId,
SupplyFleet,
plan.Summary,
[
CreateStep("step-fleet-acquire", "acquire-cargo", $"Load {plan.ItemId} at {plan.SourceStation.Label}",
[
CreateSubTask("sub-fleet-source-travel", ShipTaskKinds.Travel, $"Travel to {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, MathF.Max(plan.SourceStation.Radius + 12f, 12f), 0f),
CreateSubTask("sub-fleet-source-dock", ShipTaskKinds.Dock, $"Dock at {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 4f, 0f),
CreateSubTask("sub-fleet-source-load", ShipTaskKinds.LoadCargo, $"Load {plan.ItemId}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 0f, plan.Amount, itemId: plan.ItemId),
CreateSubTask("sub-fleet-source-undock", ShipTaskKinds.Undock, $"Undock from {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 12f, 0f),
]),
CreateStep("step-fleet-deliver", "deliver-fleet", $"Deliver {plan.ItemId} to {plan.TargetShip.Definition.Name}",
[
CreateSubTask("sub-fleet-follow", ShipTaskKinds.FollowTarget, $"Rendezvous with {plan.TargetShip.Definition.Name}", plan.TargetShip.SystemId, plan.TargetShip.Position, plan.TargetShip.Id, plan.Radius, 6f),
CreateSubTask("sub-fleet-transfer", ShipTaskKinds.TransferCargoToShip, $"Transfer {plan.ItemId}", plan.TargetShip.SystemId, plan.TargetShip.Position, plan.TargetShip.Id, plan.Radius, plan.Amount, itemId: plan.ItemId),
])
]);
}
private ShipPlanRuntime BuildConstructionPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ConstructionSiteRuntime site, StationRuntime supportStation, string summary)
{
var targetPosition = site.StationId is null ? supportStation.Position : supportStation.Position;
return CreatePlan(
ship,
sourceKind,
sourceId,
"construction-support",
summary,
[
CreateStep("step-construction-deliver", "deliver-materials", $"Deliver materials to {site.Id}",
[
CreateSubTask("sub-construction-travel", ShipTaskKinds.Travel, $"Travel to support station {supportStation.Label}", supportStation.SystemId, targetPosition, supportStation.Id, MathF.Max(supportStation.Radius + 18f, 18f), 0f),
CreateSubTask("sub-construction-deliver", ShipTaskKinds.DeliverConstruction, $"Deliver materials to {site.Id}", site.SystemId, site.CelestialId is null ? supportStation.Position : supportStation.Position, site.Id, 12f, 0f)
]),
CreateStep("step-construction-build", "build-site", $"Build {site.Id}",
[
CreateSubTask("sub-construction-build", ShipTaskKinds.BuildConstructionSite, $"Build {site.Id}", site.SystemId, site.CelestialId is null ? supportStation.Position : supportStation.Position, site.Id, 12f, 0f)
])
]);
}
private ShipPlanRuntime BuildAttackPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string? targetSystemId, string summary)
{
return CreatePlan(
ship,
sourceKind,
sourceId,
ShipOrderKinds.AttackTarget,
summary,
[
CreateStep("step-attack", ShipOrderKinds.AttackTarget, summary,
[
CreateSubTask("sub-attack", ShipTaskKinds.AttackTarget, summary, targetSystemId ?? ship.SystemId, ship.Position, targetEntityId, 26f, 0f)
])
]);
}
private ShipPlanRuntime BuildDockAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, StationRuntime station, float waitSeconds, string summary)
{
return CreatePlan(
ship,
sourceKind,
sourceId,
ShipOrderKinds.DockAndWait,
summary,
[
CreateStep("step-dock-wait-travel", "travel", $"Travel to {station.Label}",
[
CreateSubTask("sub-dock-wait-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(station.Radius + 12f, 12f), 0f),
CreateSubTask("sub-dock-wait-dock", ShipTaskKinds.Dock, $"Dock at {station.Label}", station.SystemId, station.Position, station.Id, 4f, 0f),
CreateSubTask("sub-dock-wait-hold", ShipTaskKinds.HoldPosition, $"Wait at {station.Label}", station.SystemId, station.Position, station.Id, 0f, waitSeconds),
])
]);
}
private ShipPlanRuntime BuildFlyAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, float waitSeconds, string summary)
{
return CreatePlan(
ship,
sourceKind,
sourceId,
ShipOrderKinds.FlyAndWait,
summary,
[
CreateStep("step-fly-wait", ShipOrderKinds.FlyAndWait, summary,
[
CreateSubTask("sub-fly-wait-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, null, 6f, 0f),
CreateSubTask("sub-fly-wait-hold", ShipTaskKinds.HoldPosition, summary, targetSystemId, targetPosition, null, 0f, waitSeconds),
])
]);
}
private ShipPlanRuntime BuildFlyToObjectPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, string targetEntityId, string summary)
{
return CreatePlan(
ship,
sourceKind,
sourceId,
ShipOrderKinds.FlyToObject,
summary,
[
CreateStep("step-fly-object", ShipOrderKinds.FlyToObject, summary,
[
CreateSubTask("sub-fly-object-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, targetEntityId, 8f, 0f),
CreateSubTask("sub-fly-object-hold", ShipTaskKinds.HoldPosition, summary, targetSystemId, targetPosition, targetEntityId, 0f, MathF.Max(1f, ship.DefaultBehavior.WaitSeconds)),
])
]);
}
private ShipPlanRuntime BuildFollowShipPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ShipRuntime targetShip, float radius, float durationSeconds, string summary)
{
return BuildFollowPlan(ship, sourceKind, sourceId, targetShip.Id, targetShip.SystemId, targetShip.Position, radius, durationSeconds, summary);
}
private ShipPlanRuntime BuildFollowPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string targetSystemId, Vector3 targetPosition, float radius, float durationSeconds, string summary)
{
return CreatePlan(
ship,
sourceKind,
sourceId,
ShipOrderKinds.FollowShip,
summary,
[
CreateStep("step-follow", "follow-target", summary,
[
CreateSubTask("sub-follow", ShipTaskKinds.FollowTarget, summary, targetSystemId, targetPosition, targetEntityId, radius, durationSeconds),
])
]);
}
private ShipPlanRuntime CreateIdlePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string summary)
{
return CreatePlan(
ship,
sourceKind,
sourceId,
Idle,
summary,
[
CreateStep("step-idle", ShipOrderKinds.HoldPosition, summary,
[
CreateSubTask("sub-idle", ShipTaskKinds.HoldPosition, summary, ship.SystemId, ship.Position, null, 0f, 1.5f)
])
]);
}
private ShipPlanRuntime CreateBlockedPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string summary, string blockingReason)
{
var subTask = CreateSubTask("sub-blocked", ShipTaskKinds.HoldPosition, summary, ship.SystemId, ship.Position, null, 0f, 0f);
subTask.Status = WorkStatus.Blocked;
subTask.BlockingReason = blockingReason;
var step = CreateStep("step-blocked", "blocked", summary, [subTask]);
step.Status = AiPlanStepStatus.Blocked;
step.BlockingReason = blockingReason;
var plan = CreatePlan(ship, sourceKind, sourceId, "blocked", summary, [step]);
plan.Status = AiPlanStatus.Blocked;
plan.FailureReason = blockingReason;
return plan;
}
private static ShipPlanRuntime CreatePlan(
ShipRuntime ship,
AiPlanSourceKind sourceKind,
string sourceId,
string kind,
string summary,
IReadOnlyList<ShipPlanStepRuntime> steps)
{
var plan = new ShipPlanRuntime
{
Id = $"plan-{ship.Id}-{Guid.NewGuid():N}",
SourceKind = sourceKind,
SourceId = sourceId,
Kind = kind,
Summary = summary,
};
plan.Steps.AddRange(steps);
return plan;
}
private static ShipPlanStepRuntime CreateStep(string id, string kind, string summary, IReadOnlyList<ShipSubTaskRuntime> subTasks)
{
var step = new ShipPlanStepRuntime
{
Id = id,
Kind = kind,
Summary = summary,
};
step.SubTasks.AddRange(subTasks);
return step;
}
private static ShipSubTaskRuntime CreateSubTask(
string id,
string kind,
string summary,
string targetSystemId,
Vector3 targetPosition,
string? targetEntityId,
float threshold,
float amount,
string? itemId = null,
string? moduleId = null,
string? targetNodeId = null) =>
new()
{
Id = id,
Kind = kind,
Summary = summary,
TargetSystemId = targetSystemId,
TargetPosition = targetPosition,
TargetEntityId = targetEntityId,
TargetNodeId = targetNodeId,
ItemId = itemId,
ModuleId = moduleId,
Threshold = threshold,
Amount = amount,
};
}