using SpaceGame.Api.Contracts; using SpaceGame.Api.Simulation.AI; using SpaceGame.Api.Simulation.Engine; using SpaceGame.Api.Simulation.Model; using static SpaceGame.Api.Simulation.Systems.InfrastructureSimulationService; using static SpaceGame.Api.Simulation.Support.SimulationRuntimeSupport; namespace SpaceGame.Api.Simulation.Systems; internal sealed class ShipControlService { private static readonly ShipBehaviorStateMachine _shipBehaviorStateMachine = ShipBehaviorStateMachine.CreateDefault(); private static CommanderRuntime? GetShipCommander(SimulationWorld world, ShipRuntime ship) => ship.CommanderId is null ? null : world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId && candidate.Kind == CommanderKind.Ship); private static void SyncCommanderToShip(ShipRuntime ship, CommanderRuntime commander) { if (commander.ActiveBehavior is not null) { ship.DefaultBehavior.Kind = commander.ActiveBehavior.Kind; ship.DefaultBehavior.AreaSystemId = commander.ActiveBehavior.AreaSystemId; ship.DefaultBehavior.ModuleId = commander.ActiveBehavior.ModuleId; ship.DefaultBehavior.NodeId = commander.ActiveBehavior.NodeId; ship.DefaultBehavior.Phase = commander.ActiveBehavior.Phase; ship.DefaultBehavior.PatrolIndex = commander.ActiveBehavior.PatrolIndex; ship.DefaultBehavior.StationId = commander.ActiveBehavior.StationId; } if (commander.ActiveOrder is null) { ship.Order = null; } else { ship.Order = new ShipOrderRuntime { Kind = commander.ActiveOrder.Kind, Status = commander.ActiveOrder.Status, DestinationSystemId = commander.ActiveOrder.DestinationSystemId, DestinationPosition = commander.ActiveOrder.DestinationPosition, }; } if (commander.ActiveTask is not null) { ship.ControllerTask = new ControllerTaskRuntime { Kind = ParseControllerTaskKind(commander.ActiveTask.Kind), Status = commander.ActiveTask.Status, CommanderId = commander.Id, TargetEntityId = commander.ActiveTask.TargetEntityId, TargetNodeId = commander.ActiveTask.TargetNodeId, TargetPosition = commander.ActiveTask.TargetPosition, TargetSystemId = commander.ActiveTask.TargetSystemId, Threshold = commander.ActiveTask.Threshold, }; } } private static void SyncShipToCommander(ShipRuntime ship, CommanderRuntime commander) { commander.ActiveBehavior ??= new CommanderBehaviorRuntime { Kind = ship.DefaultBehavior.Kind }; commander.ActiveBehavior.Kind = ship.DefaultBehavior.Kind; commander.ActiveBehavior.AreaSystemId = ship.DefaultBehavior.AreaSystemId; commander.ActiveBehavior.ModuleId = ship.DefaultBehavior.ModuleId; commander.ActiveBehavior.NodeId = ship.DefaultBehavior.NodeId; commander.ActiveBehavior.Phase = ship.DefaultBehavior.Phase; commander.ActiveBehavior.PatrolIndex = ship.DefaultBehavior.PatrolIndex; commander.ActiveBehavior.StationId = ship.DefaultBehavior.StationId; if (ship.Order is null) { commander.ActiveOrder = null; } else { commander.ActiveOrder ??= new CommanderOrderRuntime { Kind = ship.Order.Kind, DestinationSystemId = ship.Order.DestinationSystemId, DestinationPosition = ship.Order.DestinationPosition, }; commander.ActiveOrder.Status = ship.Order.Status; commander.ActiveOrder.TargetEntityId = ship.ControllerTask.TargetEntityId; commander.ActiveOrder.DestinationNodeId = ship.ControllerTask.TargetNodeId ?? ship.SpatialState.DestinationNodeId; } commander.ActiveTask ??= new CommanderTaskRuntime { Kind = ship.ControllerTask.Kind.ToContractValue() }; commander.ActiveTask.Kind = ship.ControllerTask.Kind.ToContractValue(); commander.ActiveTask.Status = ship.ControllerTask.Status; commander.ActiveTask.TargetEntityId = ship.ControllerTask.TargetEntityId; commander.ActiveTask.TargetNodeId = ship.ControllerTask.TargetNodeId; commander.ActiveTask.TargetPosition = ship.ControllerTask.TargetPosition; commander.ActiveTask.TargetSystemId = ship.ControllerTask.TargetSystemId; commander.ActiveTask.Threshold = ship.ControllerTask.Threshold; } internal void RefreshControlLayers(ShipRuntime ship, SimulationWorld world) { var commander = GetShipCommander(world, ship); if (commander is not null) { SyncCommanderToShip(ship, commander); } if (ship.Order is not null && ship.Order.Status == OrderStatus.Queued) { ship.Order.Status = OrderStatus.Accepted; if (commander?.ActiveOrder is not null) { commander.ActiveOrder.Status = ship.Order.Status; } } if (commander is not null) { SyncShipToCommander(ship, commander); } } internal void PlanControllerTask(SimulationEngine engine, ShipRuntime ship, SimulationWorld world) { var commander = GetShipCommander(world, ship); if (ship.Order is not null) { ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Travel, Status = WorkStatus.Active, CommanderId = commander?.Id, TargetSystemId = ship.Order.DestinationSystemId, TargetNodeId = ship.SpatialState.DestinationNodeId, TargetPosition = ship.Order.DestinationPosition, Threshold = world.Balance.ArrivalThreshold, }; SyncCommanderTask(commander, ship.ControllerTask); return; } _shipBehaviorStateMachine.Plan(engine, ship, world); SyncCommanderTask(commander, ship.ControllerTask); } internal void PlanResourceHarvest(ShipRuntime ship, SimulationWorld world, string resourceItemId, string requiredModule) { var behavior = ship.DefaultBehavior; var refinery = SelectBestBuyStation(world, ship, resourceItemId, behavior.StationId); behavior.StationId = refinery?.Id; var node = behavior.NodeId is null ? world.Nodes .Where(candidate => (behavior.AreaSystemId is null || candidate.SystemId == behavior.AreaSystemId) && candidate.ItemId == resourceItemId && candidate.OreRemaining > 0.01f) .OrderByDescending(candidate => candidate.OreRemaining) .FirstOrDefault() : world.Nodes.FirstOrDefault(candidate => candidate.Id == behavior.NodeId && candidate.OreRemaining > 0.01f); if (refinery is null || node is null || !HasShipCapabilities(ship.Definition, requiredModule)) { behavior.Kind = "idle"; ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); return; } behavior.NodeId ??= node.Id; if (GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f && behavior.Phase is "travel-to-node" or "extract") { behavior.Phase = "travel-to-station"; } if (ship.DockedStationId == refinery.Id) { if (GetShipCargoAmount(ship) > 0.01f) { behavior.Phase = "unload"; } else if (behavior.Phase is "dock" or "unload") { behavior.Phase = "undock"; } } else if (behavior.Phase is not "travel-to-station" and not "dock" and not "travel-to-node" and not "extract") { behavior.Phase = "travel-to-station"; } switch (behavior.Phase) { case "extract": var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Extract, TargetEntityId = node.Id, TargetSystemId = node.SystemId, TargetPosition = extractionPosition, Threshold = 5f, }; break; case "travel-to-station": ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Travel, TargetEntityId = refinery.Id, TargetSystemId = refinery.SystemId, TargetPosition = refinery.Position, Threshold = refinery.Radius + 8f, }; break; case "dock": ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Dock, TargetEntityId = refinery.Id, TargetSystemId = refinery.SystemId, TargetPosition = refinery.Position, Threshold = refinery.Radius + 4f, }; break; case "unload": ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Unload, TargetEntityId = refinery.Id, TargetSystemId = refinery.SystemId, TargetPosition = refinery.Position, Threshold = 0f, }; break; case "undock": ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Undock, TargetEntityId = refinery.Id, TargetSystemId = refinery.SystemId, TargetPosition = new Vector3(refinery.Position.X + world.Balance.UndockDistance, refinery.Position.Y, refinery.Position.Z), Threshold = 8f, }; break; default: ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Travel, TargetEntityId = node.Id, TargetSystemId = node.SystemId, TargetPosition = node.Position, Threshold = 18f, }; behavior.Phase = "travel-to-node"; break; } } internal static StationRuntime? SelectBestBuyStation(SimulationWorld world, ShipRuntime ship, string itemId, string? preferredStationId) { var preferred = preferredStationId is null ? null : world.Stations.FirstOrDefault(station => station.Id == preferredStationId); var bestOrder = world.MarketOrders .Where(order => order.Kind == MarketOrderKinds.Buy && order.ConstructionSiteId is null && order.State != MarketOrderStateKinds.Cancelled && order.ItemId == itemId && order.RemainingAmount > 0.01f) .Select(order => (Order: order, Station: world.Stations.FirstOrDefault(station => station.Id == order.StationId))) .Where(entry => entry.Station is not null) .OrderByDescending(entry => { var distancePenalty = entry.Station!.SystemId == ship.SystemId ? 0f : 0.2f; return entry.Order.Valuation - distancePenalty; }) .FirstOrDefault(); return bestOrder.Station ?? preferred; } private static ControllerTaskRuntime CreateStationSupportTask(SimulationWorld world, ShipRuntime ship, StationRuntime station, string? phase) => phase switch { "dock" => new ControllerTaskRuntime { Kind = ControllerTaskKind.Dock, TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = station.Position, Threshold = 8f, }, "load" => new ControllerTaskRuntime { Kind = ControllerTaskKind.Load, TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = station.Position, Threshold = 8f, }, "unload" => new ControllerTaskRuntime { Kind = ControllerTaskKind.Unload, TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = station.Position, Threshold = 8f, }, "undock" => new ControllerTaskRuntime { Kind = ControllerTaskKind.Undock, TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = new Vector3(station.Position.X + world.Balance.UndockDistance, station.Position.Y, station.Position.Z), Threshold = 8f, }, _ => CreateIdleTask(world.Balance.ArrivalThreshold), }; internal void PlanStationConstruction(ShipRuntime ship, SimulationWorld world) { var behavior = ship.DefaultBehavior; var station = world.Stations.FirstOrDefault(candidate => candidate.Id == behavior.StationId); var site = station is null ? null : GetConstructionSiteForStation(world, station.Id); if (station is null) { behavior.Kind = "idle"; ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); return; } var moduleId = site?.BlueprintId ?? GetNextStationModuleToBuild(station, world); behavior.ModuleId = moduleId; if (moduleId is null) { ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); return; } if (ship.DockedStationId is not null) { var dockedStation = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId); if (dockedStation is not null) { dockedStation.DockedShipIds.Remove(ship.Id); ReleaseDockingPad(dockedStation, ship.Id); } ship.DockedStationId = null; ship.AssignedDockingPadIndex = null; ship.Position = GetConstructionHoldPosition(station, ship.Id); ship.TargetPosition = ship.Position; } var constructionHoldPosition = GetConstructionHoldPosition(station, ship.Id); var isAtConstructionHold = ship.SystemId == station.SystemId && ship.Position.DistanceTo(constructionHoldPosition) <= 10f; if (isAtConstructionHold) { if (site is not null && site.State == ConstructionSiteStateKinds.Active && !IsConstructionSiteReady(world, site)) { behavior.Phase = "deliver-to-site"; } else if (site is not null && site.State == ConstructionSiteStateKinds.Active && IsConstructionSiteReady(world, site)) { behavior.Phase = "build-site"; } else if (site is not null) { behavior.Phase = "wait-for-materials"; } else if (CanStartModuleConstruction(station, world.ModuleRecipes[moduleId])) { behavior.Phase = "construct-module"; } else { behavior.Phase = "wait-for-materials"; } } else if (behavior.Phase != "travel-to-station") { behavior.Phase = "travel-to-station"; } switch (behavior.Phase) { case "construct-module": ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.ConstructModule, TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = constructionHoldPosition, Threshold = 10f, }; break; case "deliver-to-site": ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.DeliverConstruction, TargetEntityId = site?.Id, TargetSystemId = station.SystemId, TargetPosition = constructionHoldPosition, Threshold = 10f, }; break; case "build-site": ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.BuildConstructionSite, TargetEntityId = site?.Id, TargetSystemId = station.SystemId, TargetPosition = constructionHoldPosition, Threshold = 10f, }; break; case "wait-for-materials": ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = constructionHoldPosition, Threshold = 0f, }; break; default: ship.ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Travel, TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = constructionHoldPosition, Threshold = 10f, }; behavior.Phase = "travel-to-station"; break; } } internal void AdvanceControlState(SimulationEngine engine, ShipRuntime ship, SimulationWorld world, string controllerEvent) { var commander = GetShipCommander(world, ship); if (ship.Order is not null && controllerEvent == "arrived") { ship.Order = null; ship.ControllerTask.Kind = ControllerTaskKind.Idle; if (commander is not null) { commander.ActiveOrder = null; commander.ActiveTask = new CommanderTaskRuntime { Kind = ShipTaskKinds.Idle, Status = WorkStatus.Completed, TargetSystemId = ship.SystemId, Threshold = 0f, }; } return; } _shipBehaviorStateMachine.ApplyEvent(engine, ship, world, controllerEvent); if (commander is not null) { SyncShipToCommander(ship, commander); if (commander.ActiveTask is not null) { commander.ActiveTask.Status = controllerEvent == "none" ? WorkStatus.Active : WorkStatus.Completed; } } } internal void TrackHistory(ShipRuntime ship, string controllerEvent) { var signature = $"{ship.State.ToContractValue()}|{ship.DefaultBehavior.Kind}|{ship.ControllerTask.Kind.ToContractValue()}|{ship.ControllerTask.TargetSystemId}|{ship.ControllerTask.TargetEntityId}|{GetShipCargoAmount(ship):0.0}|{controllerEvent}"; if (signature == ship.LastSignature) { return; } ship.LastSignature = signature; var target = ship.ControllerTask.TargetEntityId ?? ship.ControllerTask.TargetSystemId ?? "none"; var eventSummary = controllerEvent == "none" ? string.Empty : $" event={controllerEvent}"; ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} behavior={ship.DefaultBehavior.Kind} task={ship.ControllerTask.Kind.ToContractValue()} target={target} cargo={GetShipCargoAmount(ship):0.#}{eventSummary}"); if (ship.History.Count > 18) { ship.History.RemoveAt(0); } } internal void EmitShipStateEvents( ShipRuntime ship, ShipState previousState, string previousBehavior, ControllerTaskKind previousTask, string controllerEvent, ICollection events) { var occurredAtUtc = DateTimeOffset.UtcNow; if (previousState != ship.State) { events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Label} {previousState.ToContractValue()} -> {ship.State.ToContractValue()}", occurredAtUtc)); } if (previousBehavior != ship.DefaultBehavior.Kind) { events.Add(new SimulationEventRecord("ship", ship.Id, "behavior-changed", $"{ship.Definition.Label} behavior {previousBehavior} -> {ship.DefaultBehavior.Kind}", occurredAtUtc)); } if (previousTask != ship.ControllerTask.Kind) { events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Label} task {previousTask.ToContractValue()} -> {ship.ControllerTask.Kind.ToContractValue()}", occurredAtUtc)); } if (controllerEvent != "none") { events.Add(new SimulationEventRecord("ship", ship.Id, controllerEvent, $"{ship.Definition.Label} {controllerEvent}", occurredAtUtc)); } } internal static ControllerTaskRuntime CreateIdleTask(float threshold) => new() { Kind = ControllerTaskKind.Idle, Threshold = threshold, }; private static ControllerTaskKind ParseControllerTaskKind(string kind) => kind switch { "travel" => ControllerTaskKind.Travel, "extract" => ControllerTaskKind.Extract, "dock" => ControllerTaskKind.Dock, "load" => ControllerTaskKind.Load, "unload" => ControllerTaskKind.Unload, "deliver-construction" => ControllerTaskKind.DeliverConstruction, "build-construction-site" => ControllerTaskKind.BuildConstructionSite, "construct-module" => ControllerTaskKind.ConstructModule, "undock" => ControllerTaskKind.Undock, _ => ControllerTaskKind.Idle, }; private static void SyncCommanderTask(CommanderRuntime? commander, ControllerTaskRuntime task) { if (commander is null) { return; } commander.ActiveTask = new CommanderTaskRuntime { Kind = task.Kind.ToContractValue(), Status = task.Status, TargetEntityId = task.TargetEntityId, TargetNodeId = task.TargetNodeId, TargetPosition = task.TargetPosition, TargetSystemId = task.TargetSystemId, Threshold = task.Threshold, }; } }