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, 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 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, 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 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, }; }