Refactor runtime bootstrap and ship control flows
This commit is contained in:
784
apps/backend/Ships/AI/ShipAiService.BehaviorQueue.cs
Normal file
784
apps/backend/Ships/AI/ShipAiService.BehaviorQueue.cs
Normal file
@@ -0,0 +1,784 @@
|
||||
using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
|
||||
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
|
||||
using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService;
|
||||
|
||||
namespace SpaceGame.Api.Ships.AI;
|
||||
|
||||
public sealed partial class ShipAiService
|
||||
{
|
||||
private static ShipOrderRuntime? GetTopOrder(ShipRuntime ship) =>
|
||||
ship.OrderQueue
|
||||
.Where(order => order.Status is OrderStatus.Queued or OrderStatus.Active)
|
||||
.OrderByDescending(GetOrderSourcePriority)
|
||||
.ThenByDescending(order => order.Priority)
|
||||
.ThenBy(order => order.CreatedAtUtc)
|
||||
.FirstOrDefault();
|
||||
|
||||
private static int GetOrderSourcePriority(ShipOrderRuntime order) => order.SourceKind switch
|
||||
{
|
||||
ShipOrderSourceKind.Player => 300,
|
||||
ShipOrderSourceKind.Commander => 200,
|
||||
ShipOrderSourceKind.Behavior => 100,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private void SyncBehaviorOrders(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
var desiredOrder = BuildManagedBehaviorOrder(world, ship);
|
||||
ship.OrderQueue.RemoveAll(order =>
|
||||
order.SourceKind == ShipOrderSourceKind.Behavior
|
||||
&& (desiredOrder is null || !string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal)));
|
||||
|
||||
if (desiredOrder is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = ship.OrderQueue.FirstOrDefault(order => string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal));
|
||||
if (existing is null)
|
||||
{
|
||||
ship.OrderQueue.Add(desiredOrder);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ManagedOrdersEqual(existing, desiredOrder))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ship.OrderQueue.Remove(existing);
|
||||
ship.OrderQueue.Add(desiredOrder);
|
||||
}
|
||||
|
||||
private ShipOrderRuntime? BuildManagedBehaviorOrder(SimulationWorld world, ShipRuntime ship)
|
||||
{
|
||||
var assignment = ResolveAssignment(world, ship);
|
||||
var behaviorKind = assignment?.BehaviorKind ?? ship.DefaultBehavior.Kind;
|
||||
var systemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId;
|
||||
|
||||
if (string.Equals(behaviorKind, HoldPosition, StringComparison.Ordinal))
|
||||
{
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-hold-position",
|
||||
Kind = ShipOrderKinds.HoldPosition,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = "Hold position",
|
||||
TargetSystemId = systemId,
|
||||
TargetPosition = ship.DefaultBehavior.TargetPosition ?? ship.Position,
|
||||
WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
|
||||
Radius = ship.DefaultBehavior.Radius,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, DockAndWait, StringComparison.Ordinal))
|
||||
{
|
||||
var station = ResolveStation(world, assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId ?? ship.DefaultBehavior.HomeStationId);
|
||||
if (station is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "station-missing";
|
||||
return null;
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-dock-and-wait",
|
||||
Kind = ShipOrderKinds.DockAndWait,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = $"Dock and wait at {station.Label}",
|
||||
TargetEntityId = station.Id,
|
||||
TargetSystemId = station.SystemId,
|
||||
DestinationStationId = station.Id,
|
||||
WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
|
||||
Radius = ship.DefaultBehavior.Radius,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, FlyAndWait, StringComparison.Ordinal))
|
||||
{
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-fly-and-wait",
|
||||
Kind = ShipOrderKinds.FlyAndWait,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = "Fly and wait",
|
||||
TargetSystemId = systemId,
|
||||
TargetPosition = ship.DefaultBehavior.TargetPosition ?? ship.Position,
|
||||
WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
|
||||
Radius = ship.DefaultBehavior.Radius,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, FollowShip, StringComparison.Ordinal))
|
||||
{
|
||||
var targetShip = world.Ships.FirstOrDefault(candidate =>
|
||||
candidate.Id == (assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId)
|
||||
&& candidate.Health > 0f);
|
||||
if (targetShip is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "target-ship-missing";
|
||||
return null;
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-follow-ship",
|
||||
Kind = ShipOrderKinds.FollowShip,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = $"Follow {targetShip.Definition.Name}",
|
||||
TargetEntityId = targetShip.Id,
|
||||
TargetSystemId = targetShip.SystemId,
|
||||
WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
|
||||
Radius = MathF.Max(16f, ship.DefaultBehavior.Radius),
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, FlyToObject, StringComparison.Ordinal))
|
||||
{
|
||||
var targetEntityId = assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId;
|
||||
var target = ResolveObjectTarget(world, targetEntityId);
|
||||
if (target is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "target-missing";
|
||||
return null;
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-fly-to-object",
|
||||
Kind = ShipOrderKinds.FlyToObject,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = "Fly to object",
|
||||
TargetEntityId = targetEntityId,
|
||||
TargetSystemId = target.Value.SystemId,
|
||||
TargetPosition = target.Value.Position,
|
||||
WaitSeconds = MathF.Max(1f, ship.DefaultBehavior.WaitSeconds),
|
||||
Radius = MathF.Max(8f, ship.DefaultBehavior.Radius),
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, Patrol, StringComparison.Ordinal))
|
||||
{
|
||||
return BuildManagedPatrolOrder(world, ship, assignment, behaviorKind);
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, AttackTarget, StringComparison.Ordinal))
|
||||
{
|
||||
var targetEntityId = assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId;
|
||||
if (string.IsNullOrWhiteSpace(targetEntityId))
|
||||
{
|
||||
return BuildManagedPatrolOrder(world, ship, assignment, behaviorKind);
|
||||
}
|
||||
|
||||
var target = ResolveObjectTarget(world, targetEntityId);
|
||||
ship.LastAccessFailureReason = target is null ? "target-missing" : null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-attack-target",
|
||||
Kind = ShipOrderKinds.AttackTarget,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = "Attack target",
|
||||
TargetEntityId = targetEntityId,
|
||||
TargetSystemId = target?.SystemId ?? assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId,
|
||||
TargetPosition = target?.Position ?? ship.Position,
|
||||
WaitSeconds = 0f,
|
||||
Radius = 26f,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, ConstructStation, StringComparison.Ordinal))
|
||||
{
|
||||
var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == (assignment?.TargetEntityId ?? ship.DefaultBehavior.PreferredConstructionSiteId))
|
||||
?? world.ConstructionSites
|
||||
.Where(candidate => candidate.FactionId == ship.FactionId && candidate.State is ConstructionSiteStateKinds.Active or ConstructionSiteStateKinds.Planned)
|
||||
.OrderBy(candidate => candidate.Id, StringComparer.Ordinal)
|
||||
.FirstOrDefault();
|
||||
if (site is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "no-construction-site";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ResolveSupportStation(world, ship, site) is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "support-station-missing";
|
||||
return null;
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-construct-station",
|
||||
Kind = ShipOrderKinds.BuildAtSite,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = $"Build {site.BlueprintId}",
|
||||
TargetEntityId = site.Id,
|
||||
TargetSystemId = site.SystemId,
|
||||
ConstructionSiteId = site.Id,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, AdvancedAutoMine, StringComparison.Ordinal)
|
||||
|| string.Equals(behaviorKind, ExpertAutoMine, StringComparison.Ordinal))
|
||||
{
|
||||
var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
|
||||
if (homeStation is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "no-home-station";
|
||||
return null;
|
||||
}
|
||||
|
||||
var opportunity = SelectMiningOpportunity(world, ship, homeStation, assignment, behaviorKind);
|
||||
if (opportunity is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "no-mineable-node";
|
||||
return null;
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-{behaviorKind}-mine-and-deliver",
|
||||
Kind = ShipOrderKinds.MineAndDeliverRun,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = opportunity.Summary,
|
||||
TargetEntityId = opportunity.Node.Id,
|
||||
TargetSystemId = opportunity.Node.SystemId,
|
||||
DestinationStationId = opportunity.DropOffStation.Id,
|
||||
ItemId = opportunity.Node.ItemId,
|
||||
NodeId = opportunity.Node.Id,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, ProtectPosition, StringComparison.Ordinal))
|
||||
{
|
||||
var targetSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId;
|
||||
var targetPosition = ship.DefaultBehavior.TargetPosition ?? assignment?.TargetPosition ?? ship.Position;
|
||||
var threat = SelectThreatTarget(world, ship, targetSystemId, targetPosition, MathF.Max(90f, ship.DefaultBehavior.Radius));
|
||||
if (threat is not null)
|
||||
{
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedAttackOrder(ship, behaviorKind, "Protect position", threat.EntityId, threat.SystemId, threat.Position);
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedFlyAndWaitOrder(
|
||||
ship,
|
||||
behaviorKind,
|
||||
"Protect position",
|
||||
targetSystemId,
|
||||
targetPosition,
|
||||
MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
|
||||
MathF.Max(6f, ship.DefaultBehavior.Radius));
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, ProtectShip, StringComparison.Ordinal))
|
||||
{
|
||||
var guardTarget = world.Ships.FirstOrDefault(candidate =>
|
||||
candidate.Id == (assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId)
|
||||
&& candidate.Health > 0f);
|
||||
if (guardTarget is null)
|
||||
{
|
||||
return BuildManagedPatrolOrder(world, ship, assignment, Patrol);
|
||||
}
|
||||
|
||||
var threat = SelectThreatTarget(
|
||||
world,
|
||||
ship,
|
||||
guardTarget.SystemId,
|
||||
guardTarget.Position,
|
||||
MathF.Max(90f, ship.DefaultBehavior.Radius),
|
||||
excludeEntityId: guardTarget.Id);
|
||||
if (threat is not null)
|
||||
{
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedAttackOrder(ship, behaviorKind, $"Protect {guardTarget.Definition.Name}", threat.EntityId, threat.SystemId, threat.Position);
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedFollowShipOrder(
|
||||
ship,
|
||||
behaviorKind,
|
||||
$"Escort {guardTarget.Definition.Name}",
|
||||
guardTarget,
|
||||
MathF.Max(18f, ship.DefaultBehavior.Radius * 0.5f),
|
||||
MathF.Max(2f, ship.DefaultBehavior.WaitSeconds));
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, ProtectStation, StringComparison.Ordinal))
|
||||
{
|
||||
var station = ResolveStation(world, assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId ?? assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
|
||||
if (station is null)
|
||||
{
|
||||
return BuildManagedPatrolOrder(world, ship, assignment, Patrol);
|
||||
}
|
||||
|
||||
var threat = SelectThreatTarget(world, ship, station.SystemId, station.Position, MathF.Max(station.Radius + 80f, ship.DefaultBehavior.Radius));
|
||||
if (threat is not null)
|
||||
{
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedAttackOrder(ship, behaviorKind, $"Protect {station.Label}", threat.EntityId, threat.SystemId, threat.Position);
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedFlyAndWaitOrder(
|
||||
ship,
|
||||
behaviorKind,
|
||||
$"Guard {station.Label}",
|
||||
station.SystemId,
|
||||
GetFormationPosition(station.Position, ship.Id, MathF.Max(station.Radius + 18f, ship.DefaultBehavior.Radius)),
|
||||
MathF.Max(2f, ship.DefaultBehavior.WaitSeconds),
|
||||
MathF.Max(6f, ship.DefaultBehavior.Radius));
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, Police, StringComparison.Ordinal))
|
||||
{
|
||||
var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
|
||||
var policeSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? homeStation?.SystemId ?? ship.SystemId;
|
||||
var areaPosition = homeStation?.Position ?? ship.DefaultBehavior.TargetPosition ?? ship.Position;
|
||||
var contact = SelectPoliceContact(world, ship, policeSystemId, areaPosition, MathF.Max(80f, ship.DefaultBehavior.Radius));
|
||||
if (contact is null)
|
||||
{
|
||||
return BuildManagedPatrolOrder(world, ship, assignment, Patrol);
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return contact.Engage
|
||||
? CreateManagedAttackOrder(ship, behaviorKind, "Police engage", contact.EntityId, contact.SystemId, contact.Position)
|
||||
: CreateManagedFollowTargetOrder(ship, behaviorKind, "Police inspect", contact.EntityId, contact.SystemId, contact.Position, MathF.Max(14f, ship.DefaultBehavior.Radius * 0.5f), MathF.Max(2f, ship.DefaultBehavior.WaitSeconds));
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, LocalAutoTrade, StringComparison.Ordinal)
|
||||
|| string.Equals(behaviorKind, AdvancedAutoTrade, StringComparison.Ordinal)
|
||||
|| string.Equals(behaviorKind, FillShortages, StringComparison.Ordinal)
|
||||
|| string.Equals(behaviorKind, FindBuildTasks, StringComparison.Ordinal)
|
||||
|| string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal))
|
||||
{
|
||||
var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
|
||||
var route = SelectTradeRoute(world, ship, homeStation, behaviorKind, ship.DefaultBehavior.KnownStationsOnly);
|
||||
if (route is not null)
|
||||
{
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedTradeRouteOrder(ship, behaviorKind, route);
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal)
|
||||
&& SelectKnownStationVisit(world, ship, homeStation) is { } visitStation)
|
||||
{
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedDockAndWaitOrder(ship, behaviorKind, visitStation, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), $"Revisit {visitStation.Label}");
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = "no-trade-route";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, SupplyFleet, StringComparison.Ordinal))
|
||||
{
|
||||
var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
|
||||
var plan = SelectFleetSupplyPlan(world, ship, homeStation);
|
||||
if (plan is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "no-fleet-to-supply";
|
||||
return null;
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-supply-fleet",
|
||||
Kind = ShipOrderKinds.SupplyFleetRun,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = plan.Summary,
|
||||
TargetEntityId = plan.TargetShip.Id,
|
||||
TargetSystemId = plan.TargetShip.SystemId,
|
||||
SourceStationId = plan.SourceStation.Id,
|
||||
ItemId = plan.ItemId,
|
||||
Radius = plan.Radius,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, AutoSalvage, StringComparison.Ordinal))
|
||||
{
|
||||
var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId);
|
||||
if (homeStation is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "no-home-station";
|
||||
return null;
|
||||
}
|
||||
|
||||
var salvage = SelectSalvageOpportunity(world, ship, homeStation);
|
||||
if (salvage is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "no-salvage-target";
|
||||
return null;
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-auto-salvage",
|
||||
Kind = ShipOrderKinds.SalvageRun,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = salvage.Summary,
|
||||
TargetEntityId = salvage.Wreck.Id,
|
||||
TargetSystemId = salvage.Wreck.SystemId,
|
||||
TargetPosition = salvage.Wreck.Position,
|
||||
SourceStationId = homeStation.Id,
|
||||
ItemId = salvage.Wreck.ItemId,
|
||||
Radius = MathF.Max(8f, ship.DefaultBehavior.Radius * 0.25f),
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(behaviorKind, RepeatOrders, StringComparison.Ordinal))
|
||||
{
|
||||
if (ship.DefaultBehavior.RepeatOrders.Count == 0)
|
||||
{
|
||||
ship.LastAccessFailureReason = "no-repeat-orders";
|
||||
return null;
|
||||
}
|
||||
|
||||
var template = ship.DefaultBehavior.RepeatOrders[ship.DefaultBehavior.RepeatIndex % ship.DefaultBehavior.RepeatOrders.Count];
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-repeat-{ship.DefaultBehavior.RepeatIndex}",
|
||||
Kind = template.Kind,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = template.Label,
|
||||
TargetEntityId = template.TargetEntityId,
|
||||
TargetSystemId = template.TargetSystemId,
|
||||
TargetPosition = template.TargetPosition,
|
||||
SourceStationId = template.SourceStationId,
|
||||
DestinationStationId = template.DestinationStationId,
|
||||
ItemId = template.ItemId,
|
||||
NodeId = template.NodeId,
|
||||
ConstructionSiteId = template.ConstructionSiteId,
|
||||
ModuleId = template.ModuleId,
|
||||
WaitSeconds = template.WaitSeconds,
|
||||
Radius = template.Radius,
|
||||
MaxSystemRange = template.MaxSystemRange ?? ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = template.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
if (!string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var itemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId;
|
||||
if (string.IsNullOrWhiteSpace(itemId))
|
||||
{
|
||||
ship.LastAccessFailureReason = "missing-item";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f)
|
||||
{
|
||||
var buyer = SelectLocalAutoMineBuyer(world, ship, systemId, itemId);
|
||||
if (buyer is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "no-suitable-buyer";
|
||||
return null;
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-local-auto-mine-sell",
|
||||
Kind = ShipOrderKinds.SellMinedCargo,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = $"Sell {itemId} in {systemId}",
|
||||
TargetEntityId = buyer.Id,
|
||||
TargetSystemId = buyer.SystemId,
|
||||
DestinationStationId = buyer.Id,
|
||||
ItemId = itemId,
|
||||
WaitSeconds = 0f,
|
||||
Radius = 0f,
|
||||
MaxSystemRange = 0,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
var node = SelectLocalMiningNode(world, ship, systemId, itemId);
|
||||
if (node is null)
|
||||
{
|
||||
ship.LastAccessFailureReason = "no-mineable-node";
|
||||
return null;
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return new ShipOrderRuntime
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-local-auto-mine-mine",
|
||||
Kind = ShipOrderKinds.MineLocal,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = $"Mine {itemId} in {systemId}",
|
||||
TargetSystemId = node.SystemId,
|
||||
NodeId = node.Id,
|
||||
ItemId = node.ItemId,
|
||||
WaitSeconds = 0f,
|
||||
Radius = 0f,
|
||||
MaxSystemRange = 0,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ManagedOrdersEqual(ShipOrderRuntime left, ShipOrderRuntime right) =>
|
||||
string.Equals(left.Id, right.Id, StringComparison.Ordinal)
|
||||
&& string.Equals(left.Kind, right.Kind, StringComparison.Ordinal)
|
||||
&& left.SourceKind == right.SourceKind
|
||||
&& string.Equals(left.SourceId, right.SourceId, StringComparison.Ordinal)
|
||||
&& left.Priority == right.Priority
|
||||
&& left.InterruptCurrentPlan == right.InterruptCurrentPlan
|
||||
&& string.Equals(left.Label, right.Label, StringComparison.Ordinal)
|
||||
&& string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal)
|
||||
&& string.Equals(left.TargetSystemId, right.TargetSystemId, StringComparison.Ordinal)
|
||||
&& left.TargetPosition == right.TargetPosition
|
||||
&& string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal)
|
||||
&& string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal)
|
||||
&& string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal)
|
||||
&& left.WaitSeconds.Equals(right.WaitSeconds)
|
||||
&& left.Radius.Equals(right.Radius)
|
||||
&& left.MaxSystemRange == right.MaxSystemRange
|
||||
&& left.KnownStationsOnly == right.KnownStationsOnly;
|
||||
|
||||
private ShipOrderRuntime BuildManagedPatrolOrder(SimulationWorld world, ShipRuntime ship, CommanderAssignmentRuntime? assignment, string sourceKind)
|
||||
{
|
||||
var patrolSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId;
|
||||
var protectPosition = ship.DefaultBehavior.TargetPosition ?? assignment?.TargetPosition ?? ship.Position;
|
||||
var patrolThreat = SelectThreatTarget(world, ship, patrolSystemId, protectPosition, MathF.Max(60f, ship.DefaultBehavior.Radius));
|
||||
if (patrolThreat is not null)
|
||||
{
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedAttackOrder(ship, sourceKind, "Patrol intercept", patrolThreat.EntityId, patrolThreat.SystemId, patrolThreat.Position, orderIdSuffix: "patrol-attack");
|
||||
}
|
||||
|
||||
Vector3 targetPosition;
|
||||
string targetSystemId;
|
||||
if (ship.DefaultBehavior.PatrolPoints.Count > 0)
|
||||
{
|
||||
var index = ship.DefaultBehavior.PatrolIndex % ship.DefaultBehavior.PatrolPoints.Count;
|
||||
targetPosition = ship.DefaultBehavior.PatrolPoints[index];
|
||||
ship.DefaultBehavior.PatrolIndex = (index + 1) % ship.DefaultBehavior.PatrolPoints.Count;
|
||||
targetSystemId = ship.DefaultBehavior.AreaSystemId ?? ship.SystemId;
|
||||
}
|
||||
else if (ResolveStation(world, ship.DefaultBehavior.HomeStationId ?? assignment?.HomeStationId) is { } homeStation)
|
||||
{
|
||||
var patrolRadius = homeStation.Radius + 90f;
|
||||
targetPosition = new Vector3(homeStation.Position.X + patrolRadius, homeStation.Position.Y, homeStation.Position.Z);
|
||||
targetSystemId = homeStation.SystemId;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetPosition = ship.Position;
|
||||
targetSystemId = ship.SystemId;
|
||||
}
|
||||
|
||||
ship.LastAccessFailureReason = null;
|
||||
return CreateManagedFlyAndWaitOrder(ship, sourceKind, "Patrol waypoint", targetSystemId, targetPosition, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), MathF.Max(6f, ship.DefaultBehavior.Radius), orderIdSuffix: "patrol-fly-and-wait");
|
||||
}
|
||||
|
||||
private static ShipOrderRuntime CreateManagedAttackOrder(
|
||||
ShipRuntime ship,
|
||||
string behaviorKind,
|
||||
string label,
|
||||
string targetEntityId,
|
||||
string targetSystemId,
|
||||
Vector3 targetPosition,
|
||||
string? orderIdSuffix = null) =>
|
||||
new()
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-{orderIdSuffix ?? behaviorKind}",
|
||||
Kind = ShipOrderKinds.AttackTarget,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = label,
|
||||
TargetEntityId = targetEntityId,
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = targetPosition,
|
||||
WaitSeconds = 0f,
|
||||
Radius = 26f,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
|
||||
private static ShipOrderRuntime CreateManagedTradeRouteOrder(ShipRuntime ship, string behaviorKind, TradeRoutePlan route) =>
|
||||
new()
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-{behaviorKind}-trade-route",
|
||||
Kind = ShipOrderKinds.TradeRoute,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = route.Summary,
|
||||
SourceStationId = route.SourceStation.Id,
|
||||
DestinationStationId = route.DestinationStation.Id,
|
||||
ItemId = route.ItemId,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
|
||||
private static ShipOrderRuntime CreateManagedDockAndWaitOrder(ShipRuntime ship, string behaviorKind, StationRuntime station, float waitSeconds, string label) =>
|
||||
new()
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-{behaviorKind}-dock-and-wait",
|
||||
Kind = ShipOrderKinds.DockAndWait,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = label,
|
||||
TargetEntityId = station.Id,
|
||||
TargetSystemId = station.SystemId,
|
||||
DestinationStationId = station.Id,
|
||||
WaitSeconds = waitSeconds,
|
||||
Radius = ship.DefaultBehavior.Radius,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
|
||||
private static ShipOrderRuntime CreateManagedFlyAndWaitOrder(
|
||||
ShipRuntime ship,
|
||||
string behaviorKind,
|
||||
string label,
|
||||
string targetSystemId,
|
||||
Vector3 targetPosition,
|
||||
float waitSeconds,
|
||||
float radius,
|
||||
string? orderIdSuffix = null) =>
|
||||
new()
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-{orderIdSuffix ?? behaviorKind}",
|
||||
Kind = ShipOrderKinds.FlyAndWait,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = label,
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = targetPosition,
|
||||
WaitSeconds = waitSeconds,
|
||||
Radius = radius,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
|
||||
private static ShipOrderRuntime CreateManagedFollowShipOrder(
|
||||
ShipRuntime ship,
|
||||
string behaviorKind,
|
||||
string label,
|
||||
ShipRuntime targetShip,
|
||||
float radius,
|
||||
float waitSeconds) =>
|
||||
new()
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-{behaviorKind}",
|
||||
Kind = ShipOrderKinds.FollowShip,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = label,
|
||||
TargetEntityId = targetShip.Id,
|
||||
TargetSystemId = targetShip.SystemId,
|
||||
WaitSeconds = waitSeconds,
|
||||
Radius = radius,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
|
||||
private static ShipOrderRuntime CreateManagedFollowTargetOrder(
|
||||
ShipRuntime ship,
|
||||
string behaviorKind,
|
||||
string label,
|
||||
string targetEntityId,
|
||||
string targetSystemId,
|
||||
Vector3 targetPosition,
|
||||
float radius,
|
||||
float waitSeconds) =>
|
||||
new()
|
||||
{
|
||||
Id = $"behavior-{ship.Id}-{behaviorKind}",
|
||||
Kind = ShipOrderKinds.FollowShip,
|
||||
SourceKind = ShipOrderSourceKind.Behavior,
|
||||
SourceId = behaviorKind,
|
||||
Priority = 0,
|
||||
InterruptCurrentPlan = false,
|
||||
Label = label,
|
||||
TargetEntityId = targetEntityId,
|
||||
TargetSystemId = targetSystemId,
|
||||
TargetPosition = targetPosition,
|
||||
WaitSeconds = waitSeconds,
|
||||
Radius = radius,
|
||||
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
||||
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user