using SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Simulation; public sealed partial class SimulationEngine { 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 = 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 }; commander.ActiveTask.Kind = ship.ControllerTask.Kind; 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; } private 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); } } private void PlanControllerTask(ShipRuntime ship, SimulationWorld world) { var commander = GetShipCommander(world, ship); if (ship.Order is not null) { ship.ControllerTask = new ControllerTaskRuntime { Kind = "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(this, 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) .OrderByDescending(candidate => candidate.OreRemaining) .FirstOrDefault() : world.Nodes.FirstOrDefault(candidate => candidate.Id == behavior.NodeId); if (refinery is null || node is null || !HasShipModules(ship.Definition, "reactor-core", "capacitor-bank", requiredModule)) { behavior.Kind = "idle"; ship.ControllerTask = CreateIdleTask(world.Balance.ArrivalThreshold); return; } behavior.NodeId ??= node.Id; if (ship.DockedStationId == refinery.Id) { if (GetShipCargoAmount(ship) > 0.01f) { behavior.Phase = "unload"; } else if (NeedsRefuel(ship)) { behavior.Phase = "refuel"; } else if (behavior.Phase is "dock" or "unload" or "refuel") { behavior.Phase = "undock"; } } else if (NeedsRefuel(ship) && behavior.Phase is not "travel-to-station" and not "dock") { behavior.Phase = "travel-to-station"; } switch (behavior.Phase) { case "extract": ship.ControllerTask = new ControllerTaskRuntime { Kind = "extract", TargetEntityId = node.Id, TargetSystemId = node.SystemId, TargetPosition = node.Position, Threshold = 14f, }; break; case "travel-to-station": ship.ControllerTask = new ControllerTaskRuntime { Kind = "travel", TargetEntityId = refinery.Id, TargetSystemId = refinery.SystemId, TargetPosition = refinery.Position, Threshold = refinery.Definition.Radius + 8f, }; break; case "dock": ship.ControllerTask = new ControllerTaskRuntime { Kind = "dock", TargetEntityId = refinery.Id, TargetSystemId = refinery.SystemId, TargetPosition = refinery.Position, Threshold = refinery.Definition.Radius + 4f, }; break; case "unload": ship.ControllerTask = new ControllerTaskRuntime { Kind = "unload", TargetEntityId = refinery.Id, TargetSystemId = refinery.SystemId, TargetPosition = refinery.Position, Threshold = 0f, }; break; case "refuel": ship.ControllerTask = new ControllerTaskRuntime { Kind = "refuel", TargetEntityId = refinery.Id, TargetSystemId = refinery.SystemId, TargetPosition = refinery.Position, Threshold = 0f, }; break; case "undock": ship.ControllerTask = new ControllerTaskRuntime { Kind = "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 = "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; } 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 == station.Id) { if (NeedsRefuel(ship)) { behavior.Phase = "refuel"; } else if (site is not null && site.State == ConstructionSiteStateKinds.Active && !IsConstructionSiteReady(site)) { behavior.Phase = "deliver-to-site"; } else if (site is not null && site.State == ConstructionSiteStateKinds.Active && IsConstructionSiteReady(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 is not "travel-to-station" and not "dock") { behavior.Phase = "travel-to-station"; } switch (behavior.Phase) { case "dock": ship.ControllerTask = new ControllerTaskRuntime { Kind = "dock", TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = station.Position, Threshold = station.Definition.Radius + 4f, }; break; case "refuel": ship.ControllerTask = new ControllerTaskRuntime { Kind = "refuel", TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = station.Position, Threshold = 0f, }; break; case "construct-module": ship.ControllerTask = new ControllerTaskRuntime { Kind = "construct-module", TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = station.Position, Threshold = 0f, }; break; case "deliver-to-site": ship.ControllerTask = new ControllerTaskRuntime { Kind = "deliver-construction", TargetEntityId = site?.Id, TargetSystemId = station.SystemId, TargetPosition = station.Position, Threshold = 0f, }; break; case "build-site": ship.ControllerTask = new ControllerTaskRuntime { Kind = "build-construction-site", TargetEntityId = site?.Id, TargetSystemId = station.SystemId, TargetPosition = station.Position, Threshold = 0f, }; break; case "wait-for-materials": ship.ControllerTask = new ControllerTaskRuntime { Kind = "idle", TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = station.Position, Threshold = 0f, }; break; default: ship.ControllerTask = new ControllerTaskRuntime { Kind = "travel", TargetEntityId = station.Id, TargetSystemId = station.SystemId, TargetPosition = station.Position, Threshold = station.Definition.Radius + 8f, }; behavior.Phase = "travel-to-station"; break; } } private void AdvanceControlState(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 = "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(this, 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; } } } private static void TrackHistory(ShipRuntime ship) { var signature = $"{ship.State}|{ship.DefaultBehavior.Kind}|{ship.ControllerTask.Kind}|{GetShipCargoAmount(ship):0.0}"; if (signature == ship.LastSignature) { return; } ship.LastSignature = signature; ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State} behavior={ship.DefaultBehavior.Kind} task={ship.ControllerTask.Kind} cargo={GetShipCargoAmount(ship):0.#}"); if (ship.History.Count > 18) { ship.History.RemoveAt(0); } } private static ControllerTaskRuntime CreateIdleTask(float threshold) => new() { Kind = "idle", Threshold = threshold, }; private static void SyncCommanderTask(CommanderRuntime? commander, ControllerTaskRuntime task) { if (commander is null) { return; } commander.ActiveTask = new CommanderTaskRuntime { Kind = task.Kind, Status = task.Status, TargetEntityId = task.TargetEntityId, TargetNodeId = task.TargetNodeId, TargetPosition = task.TargetPosition, TargetSystemId = task.TargetSystemId, Threshold = task.Threshold, }; } }