763 lines
33 KiB
C#
763 lines
33 KiB
C#
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 void SyncBehaviorOrders(SimulationWorld world, ShipRuntime ship)
|
|
{
|
|
var desiredOrder = BuildManagedBehaviorOrder(world, ship);
|
|
ship.OrderQueue.RemoveWhere(order =>
|
|
order.SourceKind == ShipOrderSourceKind.Behavior
|
|
&& order.Id.StartsWith("behavior-", StringComparison.Ordinal)
|
|
&& (desiredOrder is null || !string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal)));
|
|
|
|
if (desiredOrder is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var existing = ship.OrderQueue.FindById(desiredOrder.Id);
|
|
if (existing is null)
|
|
{
|
|
ship.OrderQueue.AddOrReplaceManagedOrder(desiredOrder);
|
|
return;
|
|
}
|
|
|
|
if (ManagedOrdersEqual(existing, desiredOrder))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ship.OrderQueue.AddOrReplaceManagedOrder(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, DockAtStation, 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-at-station",
|
|
Kind = ShipOrderKinds.DockAtStation,
|
|
SourceKind = ShipOrderSourceKind.Behavior,
|
|
SourceId = behaviorKind,
|
|
Priority = 0,
|
|
InterruptCurrentPlan = false,
|
|
Label = $"Dock at {station.Label}",
|
|
TargetEntityId = station.Id,
|
|
TargetSystemId = station.SystemId,
|
|
DestinationStationId = station.Id,
|
|
Radius = ship.DefaultBehavior.Radius,
|
|
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
|
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
|
};
|
|
}
|
|
|
|
if (string.Equals(behaviorKind, Move, StringComparison.Ordinal))
|
|
{
|
|
ship.LastAccessFailureReason = null;
|
|
return new ShipOrderRuntime
|
|
{
|
|
Id = $"behavior-{ship.Id}-move",
|
|
Kind = ShipOrderKinds.Move,
|
|
SourceKind = ShipOrderSourceKind.Behavior,
|
|
SourceId = behaviorKind,
|
|
Priority = 0,
|
|
InterruptCurrentPlan = false,
|
|
Label = "Fly to position",
|
|
TargetSystemId = systemId,
|
|
TargetPosition = ship.DefaultBehavior.TargetPosition ?? ship.Position,
|
|
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,
|
|
AnchorId = opportunity.Node.AnchorId,
|
|
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 CreateManagedMoveOrder(
|
|
ship,
|
|
behaviorKind,
|
|
"Protect position",
|
|
targetSystemId,
|
|
targetPosition,
|
|
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 CreateManagedMoveOrder(
|
|
ship,
|
|
behaviorKind,
|
|
$"Guard {station.Label}",
|
|
station.SystemId,
|
|
GetFormationPosition(station.Position, ship.Id, MathF.Max(station.Radius + 18f, ship.DefaultBehavior.Radius)),
|
|
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 CreateManagedDockAtStationOrder(ship, behaviorKind, visitStation, $"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,
|
|
AnchorId = template.AnchorId,
|
|
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, ship.DefaultBehavior.PreferredAnchorId);
|
|
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}",
|
|
TargetEntityId = node.Id,
|
|
TargetSystemId = node.SystemId,
|
|
AnchorId = node.AnchorId,
|
|
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.AnchorId, right.AnchorId, 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 CreateManagedMoveOrder(ship, sourceKind, "Patrol waypoint", targetSystemId, targetPosition, MathF.Max(6f, ship.DefaultBehavior.Radius), orderIdSuffix: "patrol-move");
|
|
}
|
|
|
|
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 CreateManagedDockAtStationOrder(ShipRuntime ship, string behaviorKind, StationRuntime station, string label) =>
|
|
new()
|
|
{
|
|
Id = $"behavior-{ship.Id}-{behaviorKind}-dock-at-station",
|
|
Kind = ShipOrderKinds.DockAtStation,
|
|
SourceKind = ShipOrderSourceKind.Behavior,
|
|
SourceId = behaviorKind,
|
|
Priority = 0,
|
|
InterruptCurrentPlan = false,
|
|
Label = label,
|
|
TargetEntityId = station.Id,
|
|
TargetSystemId = station.SystemId,
|
|
DestinationStationId = station.Id,
|
|
Radius = ship.DefaultBehavior.Radius,
|
|
MaxSystemRange = ship.DefaultBehavior.MaxSystemRange,
|
|
KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly,
|
|
};
|
|
|
|
private static ShipOrderRuntime CreateManagedMoveOrder(
|
|
ShipRuntime ship,
|
|
string behaviorKind,
|
|
string label,
|
|
string targetSystemId,
|
|
Vector3 targetPosition,
|
|
float radius,
|
|
string? orderIdSuffix = null) =>
|
|
new()
|
|
{
|
|
Id = $"behavior-{ship.Id}-{orderIdSuffix ?? behaviorKind}",
|
|
Kind = ShipOrderKinds.Move,
|
|
SourceKind = ShipOrderSourceKind.Behavior,
|
|
SourceId = behaviorKind,
|
|
Priority = 0,
|
|
InterruptCurrentPlan = false,
|
|
Label = label,
|
|
TargetSystemId = targetSystemId,
|
|
TargetPosition = targetPosition,
|
|
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,
|
|
};
|
|
}
|