using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; using static SpaceGame.Api.Stations.Simulation.StationSimulationService; namespace SpaceGame.Api.Ships.AI; public sealed partial class ShipAiService { private static Vector3 ResolveCurrentTargetPosition(SimulationWorld world, ShipSubTaskRuntime subTask) { if (!string.IsNullOrWhiteSpace(subTask.TargetAnchorId) && ResolveAnchor(world, subTask.TargetAnchorId) is not null) { return subTask.TargetPosition ?? Vector3.Zero; } if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId)) { var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); if (ship is not null) { return ship.Position; } var station = ResolveStation(world, subTask.TargetEntityId); if (station is not null) { return station.Position; } var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); if (celestial is not null) { return celestial.Position; } var wreck = world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); if (wreck is not null) { return wreck.Position; } } return subTask.TargetPosition ?? Vector3.Zero; } private static CelestialRuntime? ResolveTravelTargetCelestial(SimulationWorld world, ShipSubTaskRuntime subTask, Vector3 targetPosition) { if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId)) { var station = ResolveStation(world, subTask.TargetEntityId); if (station?.AnchorId is not null) { return ResolveAnchorBackedCelestial(world, station.AnchorId); } var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); if (site?.AnchorId is not null) { return ResolveAnchorBackedCelestial(world, site.AnchorId); } if (ResolveAnchor(world, subTask.TargetEntityId) is { } anchorBackedCelestialTarget) { return ResolveAnchorBackedCelestial(world, anchorBackedCelestialTarget.Id); } var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); if (celestial is not null) { return celestial; } if (world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) is { } wreck) { return world.Celestials .Where(candidate => candidate.SystemId == wreck.SystemId) .OrderBy(candidate => candidate.Position.DistanceTo(wreck.Position)) .FirstOrDefault(); } } return world.Celestials .Where(candidate => subTask.TargetSystemId is null || candidate.SystemId == subTask.TargetSystemId) .OrderBy(candidate => candidate.Position.DistanceTo(targetPosition)) .FirstOrDefault(); } private static AnchorRuntime? ResolveTravelTargetAnchor(SimulationWorld world, ShipSubTaskRuntime subTask, Vector3 targetPosition) { if (!string.IsNullOrWhiteSpace(subTask.TargetAnchorId) && ResolveAnchor(world, subTask.TargetAnchorId) is { } explicitTargetAnchor) { return explicitTargetAnchor; } if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId)) { var station = ResolveStation(world, subTask.TargetEntityId); if (station?.AnchorId is not null) { return ResolveAnchor(world, station.AnchorId); } var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); if (site?.AnchorId is not null) { return ResolveAnchor(world, site.AnchorId); } var node = ResolveNode(world, subTask.TargetEntityId); if (node is not null) { return ResolveAnchor(world, node.AnchorId); } if (ResolveAnchor(world, subTask.TargetEntityId) is { } directAnchor) { return directAnchor; } if (world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) is { } celestial) { return ResolveAnchor(world, celestial.Id); } if (world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) is { } wreck) { return world.Anchors .Where(candidate => candidate.SystemId == wreck.SystemId) .OrderBy(candidate => candidate.Position.DistanceTo(wreck.Position)) .FirstOrDefault(); } } return world.Anchors .Where(candidate => subTask.TargetSystemId is null || candidate.SystemId == subTask.TargetSystemId) .OrderBy(candidate => candidate.Position.DistanceTo(targetPosition)) .FirstOrDefault(); } private static AnchorRuntime? ResolveCurrentAnchor(SimulationWorld world, ShipRuntime ship) { if (ship.SpatialState.CurrentAnchorId is not null && ResolveAnchor(world, ship.SpatialState.CurrentAnchorId) is { } explicitAnchor) { return explicitAnchor; } if (ship.DockedStationId is not null && ResolveStation(world, ship.DockedStationId)?.AnchorId is { } dockAnchorId) { return ResolveAnchor(world, dockAnchorId); } return world.Anchors .Where(candidate => candidate.SystemId == ship.SystemId) .OrderBy(candidate => candidate.Position.DistanceTo(ResolveShipSystemPosition(world, ship))) .FirstOrDefault(); } private static CelestialRuntime? ResolveCurrentCelestial(SimulationWorld world, ShipRuntime ship) { if (ship.SpatialState.CurrentAnchorId is not null && ResolveAnchorBackedCelestial(world, ship.SpatialState.CurrentAnchorId) is { } currentAnchorCelestial) { return currentAnchorCelestial; } return world.Celestials .Where(candidate => candidate.SystemId == ship.SystemId) .OrderBy(candidate => candidate.Position.DistanceTo(ResolveShipSystemPosition(world, ship))) .FirstOrDefault(); } private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) => world.Celestials.FirstOrDefault(candidate => candidate.SystemId == systemId && candidate.Kind == SpatialNodeKind.Star); private static AnchorRuntime? ResolveSystemEntryAnchor(SimulationWorld world, string systemId) => world.Anchors.FirstOrDefault(candidate => candidate.SystemId == systemId && candidate.Kind == SpatialNodeKind.Star); private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) => world.Systems.FirstOrDefault(candidate => candidate.Definition.Id == systemId)?.Position ?? Vector3.Zero; private static Vector3 ResolveAnchorPosition(SimulationWorld world, string? anchorId, Vector3 fallbackPosition) => ResolveAnchor(world, anchorId)?.Position ?? fallbackPosition; private static Vector3 ResolveStationSystemPosition(SimulationWorld world, StationRuntime station) { if (station.AnchorId is not null && ResolveAnchor(world, station.AnchorId) is { } anchor) { var localOffset = SimulationUnits.MetersToKilometers(station.Position); return new Vector3( anchor.Position.X + localOffset.X, anchor.Position.Y + localOffset.Y, anchor.Position.Z + localOffset.Z); } return SimulationUnits.MetersToKilometers(station.Position); } private static Vector3 ResolveNodeSystemPosition(SimulationWorld world, ResourceNodeRuntime node) { if (ResolveAnchor(world, node.AnchorId) is { } anchor) { return new Vector3( anchor.Position.X + node.Position.X, anchor.Position.Y + node.Position.Y, anchor.Position.Z + node.Position.Z); } return node.Position; } private static Vector3 ResolveShipSystemPosition(SimulationWorld world, ShipRuntime ship) { if (ship.SpatialState.SystemPosition is { } systemPosition) { return systemPosition; } if (ResolveCurrentAnchor(world, ship) is { } anchor) { var localOffset = SimulationUnits.MetersToKilometers(ship.Position); return new Vector3( anchor.Position.X + localOffset.X, anchor.Position.Y + localOffset.Y, anchor.Position.Z + localOffset.Z); } return SimulationUnits.MetersToKilometers(ship.Position); } private static float GetLocalTravelSpeed(ShipRuntime ship) => ship.Definition.Speed * GetSkillFactor(ship.Skills.Navigation); private static float GetWarpTravelSpeed(ShipRuntime ship) => SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed) * GetSkillFactor(ship.Skills.Navigation); private static float GetSkillFactor(int skillLevel) => Math.Clamp(1f + ((skillLevel - 3) * 0.08f), 0.75f, 1.4f); private static int GetEffectiveSkillLevel( SimulationWorld world, ShipRuntime ship, Func captainSelector, Func managerSelector) { var captainLevel = captainSelector(ship.Skills); if (ship.CommanderId is null) { return captainLevel; } var shipCommander = world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId); var manager = shipCommander?.ParentCommanderId is null ? shipCommander : world.Commanders.FirstOrDefault(candidate => candidate.Id == shipCommander.ParentCommanderId) ?? shipCommander; return Math.Clamp((captainLevel + (manager is null ? 3 : managerSelector(manager.Skills)) + 1) / 2, 1, 5); } private static int ResolveBehaviorSystemRange(SimulationWorld world, ShipRuntime ship, string behaviorKind, int explicitRange) { if (explicitRange > 0) { return explicitRange; } var tradeSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Trade, skills => skills.Coordination); var miningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination); var combatSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Combat, skills => skills.Strategy); return behaviorKind switch { LocalAutoMine or LocalAutoTrade => 0, AdvancedAutoMine => Math.Clamp(1 + ((miningSkill - 1) / 2), 1, 3), AdvancedAutoTrade => Math.Clamp(1 + ((tradeSkill - 1) / 2), 1, 3), ExpertAutoMine => Math.Clamp(2 + ((miningSkill - 1) / 2), 2, Math.Max(world.Systems.Count - 1, 2)), FillShortages or FindBuildTasks or RevisitKnownStations or SupplyFleet => Math.Clamp(1 + ((tradeSkill + 1) / 2), 1, Math.Max(world.Systems.Count - 1, 1)), Patrol or Police or ProtectPosition or ProtectShip or ProtectStation => Math.Clamp(1 + ((combatSkill - 1) / 2), 1, Math.Max(world.Systems.Count - 1, 1)), _ => Math.Max(world.Systems.Count - 1, 0), }; } private static int GetSystemDistanceTier(SimulationWorld world, string originSystemId, string targetSystemId) { if (string.Equals(originSystemId, targetSystemId, StringComparison.Ordinal)) { return 0; } var originPosition = ResolveSystemGalaxyPosition(world, originSystemId); return world.Systems .OrderBy(system => system.Position.DistanceTo(originPosition)) .ThenBy(system => system.Definition.Id, StringComparer.Ordinal) .Select(system => system.Definition.Id) .TakeWhile(systemId => !string.Equals(systemId, targetSystemId, StringComparison.Ordinal)) .Count(); } private static bool IsWithinSystemRange(SimulationWorld world, string originSystemId, string targetSystemId, int maxRange) => maxRange < 0 || GetSystemDistanceTier(world, originSystemId, targetSystemId) <= maxRange; private static float GetShipDamagePerSecond(ShipRuntime ship) => ship.Definition.Type switch { ShipType.Frigate => FrigateDps, ShipType.Destroyer => DestroyerDps, ShipType.Battleship => CruiserDps, ShipType.Carrier => CapitalDps, _ => 4f, }; private static MiningOpportunity? SelectMiningOpportunity( SimulationWorld world, ShipRuntime ship, StationRuntime homeStation, CommanderAssignmentRuntime? assignment, string behaviorKind) { var policy = ResolvePolicy(world, ship.PolicySetId); var preferredItemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId; var preferredAnchorId = ship.DefaultBehavior.PreferredAnchorId; var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange); var effectiveMiningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination); string? deniedReason = null; var opportunity = world.Nodes .Where(node => { if (node.OreRemaining <= 0.01f || !CanExtractNode(ship, node, world) || (preferredItemId is not null && !string.Equals(node.ItemId, preferredItemId, StringComparison.Ordinal))) { return false; } if (preferredAnchorId is not null && !string.Equals(node.AnchorId, preferredAnchorId, StringComparison.Ordinal)) { return false; } if (!TryCheckSystemAllowed(world, policy, ship.FactionId, node.SystemId, "military", out var reason)) { deniedReason ??= reason; return false; } return IsWithinSystemRange(world, homeStation.SystemId, node.SystemId, rangeBudget); }) .Select(node => { var buyer = SelectBestDeliveryStation(world, ship, node.ItemId, homeStation, behaviorKind); var demandScore = GetFactionDemandScore(world, ship.FactionId, node.ItemId); var distancePenalty = GetSystemDistanceTier(world, homeStation.SystemId, node.SystemId) * 18f; var routeRiskPenalty = GeopoliticalSimulationService.GetSystemRouteRisk(world, node.SystemId, ship.FactionId) * 30f; var score = (node.SystemId == homeStation.SystemId ? 55f : 0f) + (node.OreRemaining * 0.025f) + (demandScore * (string.Equals(behaviorKind, ExpertAutoMine, StringComparison.Ordinal) ? 22f : 12f)) + (effectiveMiningSkill * 10f) - distancePenalty - routeRiskPenalty - ResolveNodeSystemPosition(world, node).DistanceTo(ResolveShipSystemPosition(world, ship)); return new MiningOpportunity(node, buyer, score, $"Mine {node.ItemId} for {buyer.Label}"); }) .OrderByDescending(candidate => candidate.Score) .ThenBy(candidate => candidate.Node.Id, StringComparer.Ordinal) .FirstOrDefault(); if (opportunity is null && deniedReason is not null) { ship.LastAccessFailureReason = deniedReason; } return opportunity; } private static TradeRoutePlan? SelectTradeRoute( SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation, string behaviorKind, bool knownStationsOnly) { var policy = ResolvePolicy(world, ship.PolicySetId); var stationsById = world.Stations .Where(station => station.FactionId == ship.FactionId) .ToDictionary(station => station.Id, StringComparer.Ordinal); var originSystemId = homeStation?.SystemId ?? ship.SystemId; var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange); var effectiveTradeSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Trade, skills => skills.Coordination); var requireKnownStations = knownStationsOnly || string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal); string? deniedReason = null; var route = world.MarketOrders .Where(order => order.FactionId == ship.FactionId && order.Kind == MarketOrderKinds.Buy && order.RemainingAmount > 0.01f) .Select(order => { StationRuntime? destination = null; ConstructionSiteRuntime? destinationSite = null; if (order.StationId is not null && stationsById.TryGetValue(order.StationId, out var destinationStation)) { destination = destinationStation; } else if (order.ConstructionSiteId is not null) { destinationSite = world.ConstructionSites.FirstOrDefault(site => site.Id == order.ConstructionSiteId); if (destinationSite is not null) { destination = ResolveSupportStation(world, ship, destinationSite); } } if (destination is null) { return null; } if (!TryCheckSystemAllowed(world, policy, ship.FactionId, destination.SystemId, "trade", out var destinationDeniedReason)) { deniedReason ??= destinationDeniedReason; return null; } if (!IsWithinSystemRange(world, originSystemId, destination.SystemId, rangeBudget)) { return null; } if (requireKnownStations && ship.KnownStationIds.Count > 0 && !ship.KnownStationIds.Contains(destination.Id) && (homeStation is null || !string.Equals(destination.Id, homeStation.Id, StringComparison.Ordinal))) { return null; } if (string.Equals(behaviorKind, FindBuildTasks, StringComparison.Ordinal) && destinationSite is null) { return null; } if (!string.Equals(behaviorKind, FindBuildTasks, StringComparison.Ordinal) && destinationSite is not null) { return null; } var source = stationsById.Values .Where(station => { if (station.Id == destination.Id || GetInventoryAmount(station.Inventory, order.ItemId) <= GetStationReserveFloor(world, station, order.ItemId) + 1f) { return false; } if (!TryCheckSystemAllowed(world, policy, ship.FactionId, station.SystemId, "trade", out var sourceDeniedReason)) { deniedReason ??= sourceDeniedReason; return false; } if (!IsWithinSystemRange(world, originSystemId, station.SystemId, rangeBudget)) { return false; } return !requireKnownStations || ship.KnownStationIds.Count == 0 || ship.KnownStationIds.Contains(station.Id) || (homeStation is not null && string.Equals(station.Id, homeStation.Id, StringComparison.Ordinal)); }) .OrderByDescending(station => GetInventoryAmount(station.Inventory, order.ItemId) - GetStationReserveFloor(world, station, order.ItemId)) .ThenByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0) .ThenBy(station => station.Id, StringComparer.Ordinal) .FirstOrDefault(); if (source is null) { return null; } var shortageBias = string.Equals(behaviorKind, FillShortages, StringComparison.Ordinal) ? GetFactionDemandScore(world, ship.FactionId, order.ItemId) * 35f : 0f; var buildBias = destinationSite is null ? 0f : 65f; var revisitBias = string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal) && ship.KnownStationIds.Contains(source.Id) && ship.KnownStationIds.Contains(destination.Id) ? 28f : 0f; var regionalNeedBias = GetRegionalCommodityPressure(world, ship.FactionId, destination.SystemId, order.ItemId) * 18f; var systemRangePenalty = (GetSystemDistanceTier(world, originSystemId, source.SystemId) + GetSystemDistanceTier(world, originSystemId, destination.SystemId)) * 16f; var riskPenalty = (GeopoliticalSimulationService.GetSystemRouteRisk(world, source.SystemId, ship.FactionId) + GeopoliticalSimulationService.GetSystemRouteRisk(world, destination.SystemId, ship.FactionId)) * 22f; var distanceScore = source.Position.DistanceTo(ship.Position) + source.Position.DistanceTo(destination.Position); var score = (order.Valuation * 50f) + shortageBias + buildBias + revisitBias + regionalNeedBias + (effectiveTradeSkill * 12f) - systemRangePenalty - riskPenalty - distanceScore; var summary = destinationSite is null ? $"{order.ItemId}: {source.Label} -> {destination.Label}" : $"{order.ItemId}: {source.Label} -> build support {destination.Label}"; return new TradeRoutePlan(source, destination, order.ItemId, score, summary); }) .Where(route => route is not null) .Cast() .OrderByDescending(route => route.Score) .ThenBy(route => route.ItemId, StringComparer.Ordinal) .ThenBy(route => route.SourceStation.Id, StringComparer.Ordinal) .FirstOrDefault(); if (route is null && deniedReason is not null) { ship.LastAccessFailureReason = deniedReason; } return route; } private static FleetSupplyPlan? SelectFleetSupplyPlan(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation) { var assignment = ResolveAssignment(world, ship); var targetCandidates = world.Ships .Where(candidate => candidate.Id != ship.Id && candidate.FactionId == ship.FactionId && candidate.Definition.GetTotalCargoCapacity() > 0.01f && (assignment?.TargetEntityId is null || string.Equals(candidate.Id, assignment.TargetEntityId, StringComparison.Ordinal))) .OrderByDescending(candidate => IsMilitaryShip(candidate.Definition) ? 1 : 0) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) .ToList(); if (targetCandidates.Count == 0) { return null; } var sourceStations = world.Stations .Where(station => station.FactionId == ship.FactionId) .OrderByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0) .ThenBy(station => station.Id, StringComparer.Ordinal) .ToList(); foreach (var target in targetCandidates) { var itemId = assignment?.ItemId ?? sourceStations .SelectMany(station => station.Inventory) .Where(entry => entry.Value > 2f) .OrderByDescending(entry => entry.Value) .ThenBy(entry => entry.Key, StringComparer.Ordinal) .Select(entry => entry.Key) .FirstOrDefault(); if (itemId is null) { continue; } var source = sourceStations.FirstOrDefault(station => GetInventoryAmount(station.Inventory, itemId) > 2f); if (source is null) { continue; } var amount = MathF.Min(MathF.Max(10f, ship.Definition.GetTotalCargoCapacity() * 0.5f), GetInventoryAmount(source.Inventory, itemId)); return new FleetSupplyPlan(source, target, itemId, amount, MathF.Max(16f, ship.DefaultBehavior.Radius), $"Supply {target.Definition.Name} with {itemId}"); } return null; } private static StationRuntime? SelectKnownStationVisit(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation) { var candidateIds = ship.KnownStationIds.Count == 0 && homeStation is not null ? [homeStation.Id] : ship.KnownStationIds.OrderBy(id => id, StringComparer.Ordinal).ToArray(); return candidateIds .Select(id => ResolveStation(world, id)) .Where(station => station is not null && station.FactionId == ship.FactionId) .Cast() .OrderByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0) .ThenBy(station => station.SystemId == ship.SystemId ? 0 : 1) .ThenBy(station => station.Position.DistanceTo(ship.Position)) .FirstOrDefault(); } private static StationRuntime SelectBestDeliveryStation(SimulationWorld world, ShipRuntime ship, string itemId, StationRuntime homeStation, string behaviorKind) { if (!string.Equals(behaviorKind, ExpertAutoMine, StringComparison.Ordinal)) { return homeStation; } return world.Stations .Where(station => station.FactionId == ship.FactionId) .OrderByDescending(station => GetFactionDemandScore(world, ship.FactionId, itemId) + GetRegionalCommodityPressure(world, ship.FactionId, station.SystemId, itemId) + (station.Id == homeStation.Id ? 5f : 0f)) .ThenBy(station => station.SystemId == homeStation.SystemId ? 0 : 1) .ThenBy(station => station.Id, StringComparer.Ordinal) .FirstOrDefault() ?? homeStation; } private static ResourceNodeRuntime? SelectLocalMiningNode(SimulationWorld world, ShipRuntime ship, string systemId, string itemId, string? anchorId = null) { var policy = ResolvePolicy(world, ship.PolicySetId); string? deniedReason = null; var node = world.Nodes .Where(candidate => { if (!string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal) || !string.Equals(candidate.ItemId, itemId, StringComparison.Ordinal) || candidate.OreRemaining <= 0.01f || !CanExtractNode(ship, candidate, world)) { return false; } if (anchorId is not null && !string.Equals(candidate.AnchorId, anchorId, StringComparison.Ordinal)) { return false; } if (!TryCheckSystemAllowed(world, policy, ship.FactionId, candidate.SystemId, "military", out var reason)) { deniedReason ??= reason; return false; } return true; }) .OrderByDescending(candidate => candidate.OreRemaining) .ThenBy(candidate => candidate.Position.DistanceTo(ship.Position)) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) .FirstOrDefault(); if (node is null && deniedReason is not null) { ship.LastAccessFailureReason = deniedReason; } return node; } private static ResourceDepositRuntime? ResolveResourceDeposit(SimulationWorld world, string? depositId) { if (string.IsNullOrWhiteSpace(depositId)) { return null; } foreach (var node in world.Nodes) { var deposit = node.Deposits.FirstOrDefault(candidate => string.Equals(candidate.Id, depositId, StringComparison.Ordinal)); if (deposit is not null) { return deposit; } } return null; } private static ResourceDepositRuntime? SelectMiningDeposit(ResourceNodeRuntime node, string shipId) { return node.Deposits .Where(candidate => candidate.OreRemaining > 0.01f) .OrderByDescending(candidate => candidate.OreRemaining) .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) .FirstOrDefault(); } private static void SyncNodeOreTotals(ResourceNodeRuntime node) { node.OreRemaining = node.Deposits.Sum(candidate => candidate.OreRemaining); } private static AnchorRuntime? ResolveMiningAnchor(SimulationWorld world, string? anchorId, string? nodeId) { if (anchorId is not null) { return ResolveAnchor(world, anchorId); } if (nodeId is not null && ResolveNode(world, nodeId) is { } node) { return ResolveAnchor(world, node.AnchorId); } return null; } private static StationRuntime? SelectLocalAutoMineBuyer(SimulationWorld world, ShipRuntime ship, string systemId, string itemId) { var policy = ResolvePolicy(world, ship.PolicySetId); var stationsById = world.Stations.ToDictionary(station => station.Id, StringComparer.Ordinal); string? deniedReason = null; var buyer = world.MarketOrders .Where(order => order.Kind == MarketOrderKinds.Buy && string.Equals(order.ItemId, itemId, StringComparison.Ordinal) && order.RemainingAmount > 0.01f) .Select(order => { StationRuntime? destination = null; if (order.StationId is not null && stationsById.TryGetValue(order.StationId, out var station)) { destination = station; } else if (order.ConstructionSiteId is not null) { var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == order.ConstructionSiteId); if (site is not null) { destination = ResolveSupportStation(world, ship, site); } } if (destination is null || !string.Equals(destination.SystemId, systemId, StringComparison.Ordinal)) { return null; } if (!TryCheckSystemAllowed(world, policy, ship.FactionId, destination.SystemId, "trade", out var reason)) { deniedReason ??= reason; return null; } var score = (order.Valuation * 20f) + MathF.Min(order.RemainingAmount, ship.Definition.GetTotalCargoCapacity()) - destination.Position.DistanceTo(ship.Position); return new LocalMiningBuyerCandidate(destination, score); }) .Where(candidate => candidate is not null) .Cast() .OrderByDescending(candidate => candidate.Score) .ThenBy(candidate => candidate.Station.Id, StringComparer.Ordinal) .Select(candidate => candidate.Station) .FirstOrDefault(); if (buyer is null && deniedReason is not null) { ship.LastAccessFailureReason = deniedReason; } return buyer; } private static float GetFactionDemandScore(SimulationWorld world, string factionId, string itemId) { var signal = CommanderPlanningService.FindFactionEconomicAssessment(world, factionId)? .CommoditySignals .FirstOrDefault(candidate => candidate.ItemId == itemId); var regionalBottleneckScore = world.Geopolitics?.EconomyRegions.Bottlenecks .Where(bottleneck => string.Equals(bottleneck.ItemId, itemId, StringComparison.Ordinal)) .Join( world.Geopolitics.EconomyRegions.Regions.Where(region => string.Equals(region.FactionId, factionId, StringComparison.Ordinal)), bottleneck => bottleneck.RegionId, region => region.Id, (bottleneck, _) => bottleneck.Severity) .DefaultIfEmpty() .Max() ?? 0f; if (signal is null) { return regionalBottleneckScore * 8f; } return MathF.Max(0f, signal.BuyBacklog + signal.ReservedForConstruction + MathF.Max(0f, -signal.ProjectedNetRatePerSecond * 50f) + (regionalBottleneckScore * 8f)); } private static float GetRegionalCommodityPressure(SimulationWorld world, string factionId, string systemId, string itemId) { var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, systemId); if (region is null) { return 0f; } var bottleneck = world.Geopolitics?.EconomyRegions.Bottlenecks .FirstOrDefault(candidate => string.Equals(candidate.RegionId, region.Id, StringComparison.Ordinal) && string.Equals(candidate.ItemId, itemId, StringComparison.Ordinal)); var assessment = world.Geopolitics?.EconomyRegions.EconomicAssessments .FirstOrDefault(candidate => string.Equals(candidate.RegionId, region.Id, StringComparison.Ordinal)); return (bottleneck?.Severity ?? 0f) + ((assessment?.ConstructionPressure ?? 0f) * 2f); } private static ThreatTargetCandidate? SelectThreatTarget( SimulationWorld world, ShipRuntime ship, string targetSystemId, Vector3 anchorPosition, float radius, string? excludeEntityId = null) { var policy = ResolvePolicy(world, ship.PolicySetId); return world.Ships .Where(candidate => candidate.Id != excludeEntityId && candidate.Health > 0f && candidate.FactionId != ship.FactionId && string.Equals(candidate.SystemId, targetSystemId, StringComparison.Ordinal) && candidate.Position.DistanceTo(anchorPosition) <= radius * 1.75f) .Select(candidate => new ThreatTargetCandidate( candidate.Id, candidate.SystemId, candidate.Position, 100f + (IsMilitaryShip(candidate.Definition) ? 30f : 0f) - candidate.Position.DistanceTo(anchorPosition) - candidate.Position.DistanceTo(ship.Position) + (string.Equals(policy?.CombatEngagementPolicy, "aggressive", StringComparison.OrdinalIgnoreCase) ? 12f : 0f))) .Concat(world.Stations .Where(candidate => candidate.Id != excludeEntityId && candidate.FactionId != ship.FactionId && string.Equals(candidate.SystemId, targetSystemId, StringComparison.Ordinal) && candidate.Position.DistanceTo(anchorPosition) <= radius * 2f) .Select(candidate => new ThreatTargetCandidate(candidate.Id, candidate.SystemId, candidate.Position, 45f - candidate.Position.DistanceTo(anchorPosition) * 0.2f))) .OrderByDescending(candidate => candidate.Score) .ThenBy(candidate => candidate.EntityId, StringComparer.Ordinal) .FirstOrDefault(); } private static PoliceContactCandidate? SelectPoliceContact(SimulationWorld world, ShipRuntime ship, string systemId, Vector3 anchorPosition, float radius) { var policy = ResolvePolicy(world, ship.PolicySetId); return world.Ships .Where(candidate => candidate.Id != ship.Id && candidate.Health > 0f && candidate.FactionId != ship.FactionId && string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal) && candidate.Position.DistanceTo(anchorPosition) <= radius * 1.5f) .Select(candidate => { var engage = IsMilitaryShip(candidate.Definition) || string.Equals(policy?.CombatEngagementPolicy, "aggressive", StringComparison.OrdinalIgnoreCase); var score = (engage ? 80f : 40f) - candidate.Position.DistanceTo(anchorPosition) - candidate.Position.DistanceTo(ship.Position) + (IsTransportShip(candidate.Definition) ? 8f : 0f); return new PoliceContactCandidate(candidate.Id, candidate.SystemId, candidate.Position, engage, score); }) .OrderByDescending(candidate => candidate.Score) .ThenBy(candidate => candidate.EntityId, StringComparer.Ordinal) .FirstOrDefault(); } private static SalvageOpportunity? SelectSalvageOpportunity(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation) { if (homeStation is null) { return null; } var rangeBudget = ResolveBehaviorSystemRange(world, ship, AutoSalvage, ship.DefaultBehavior.MaxSystemRange > 0 ? ship.DefaultBehavior.MaxSystemRange : 1); return world.Wrecks .Where(wreck => wreck.RemainingAmount > 0.01f && IsWithinSystemRange(world, homeStation.SystemId, wreck.SystemId, rangeBudget)) .Select(wreck => new SalvageOpportunity( wreck, (wreck.RemainingAmount * 3f) - wreck.Position.DistanceTo(ship.Position) - (GetSystemDistanceTier(world, homeStation.SystemId, wreck.SystemId) * 25f), $"Salvage {wreck.ItemId} from {wreck.SourceEntityId}")) .OrderByDescending(candidate => candidate.Score) .ThenBy(candidate => candidate.Wreck.Id, StringComparer.Ordinal) .FirstOrDefault(); } private static (string SystemId, Vector3 Position)? ResolveObjectTarget(SimulationWorld world, string? entityId) { if (entityId is null) { return null; } if (world.Ships.FirstOrDefault(candidate => candidate.Id == entityId) is { } ship) { return (ship.SystemId, ship.Position); } if (ResolveStation(world, entityId) is { } station) { return (station.SystemId, station.Position); } if (world.Celestials.FirstOrDefault(candidate => candidate.Id == entityId) is { } celestial) { return (celestial.SystemId, celestial.Position); } if (ResolveAnchor(world, entityId) is { } anchor) { return (anchor.SystemId, anchor.Position); } if (world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == entityId) is { } site) { var position = ResolveAnchor(world, site.AnchorId)?.Position ?? Vector3.Zero; return (site.SystemId, position); } if (world.Wrecks.FirstOrDefault(candidate => candidate.Id == entityId) is { } wreck) { return (wreck.SystemId, wreck.Position); } return null; } private static Vector3 GetFormationPosition(Vector3 anchorPosition, string seed, float radius) { var hash = Math.Abs(seed.Aggregate(17, (acc, c) => (acc * 31) + c)); var angle = (hash % 360) * (MathF.PI / 180f); return new Vector3( anchorPosition.X + (MathF.Cos(angle) * radius), anchorPosition.Y, anchorPosition.Z + (MathF.Sin(angle) * radius)); } private static TradeRoutePlan? ResolveTradeRoute(SimulationWorld world, string itemId, string sourceStationId, string destinationStationId) { var source = ResolveStation(world, sourceStationId); var destination = ResolveStation(world, destinationStationId); return source is null || destination is null ? null : new TradeRoutePlan(source, destination, itemId, 0f, $"{itemId}: {source.Label} -> {destination.Label}"); } private static StationRuntime? ResolveStation(SimulationWorld world, string? stationId) => stationId is null ? null : world.Stations.FirstOrDefault(candidate => candidate.Id == stationId); private static AnchorRuntime? ResolveAnchor(SimulationWorld world, string? anchorId) => anchorId is null ? null : world.Anchors.FirstOrDefault(candidate => candidate.Id == anchorId); private static CelestialRuntime? ResolveAnchorBackedCelestial(SimulationWorld world, string? anchorId) { var anchor = ResolveAnchor(world, anchorId); var celestialId = SpatialBuilder.ResolveCompatibleCelestialId(anchor); return celestialId is null ? null : world.Celestials.FirstOrDefault(candidate => candidate.Id == celestialId); } private static ResourceNodeRuntime? ResolveNode(SimulationWorld world, string? nodeId) => nodeId is null ? null : world.Nodes.FirstOrDefault(candidate => candidate.Id == nodeId); private static PolicySetRuntime? ResolvePolicy(SimulationWorld world, string? policySetId) => policySetId is null ? null : world.Policies.FirstOrDefault(policy => policy.Id == policySetId); private static bool IsSystemAllowed( SimulationWorld world, PolicySetRuntime? policy, string factionId, string systemId, string accessKind) => TryCheckSystemAllowed(world, policy, factionId, systemId, accessKind, out _); private static bool TryCheckSystemAllowed( SimulationWorld world, PolicySetRuntime? policy, string factionId, string systemId, string accessKind, out string? denialReason) { denialReason = null; if (policy?.BlacklistedSystemIds.Contains(systemId) == true) { denialReason = $"blacklisted:{systemId}"; return false; } var controlState = GeopoliticalSimulationService.GetSystemControlState(world, systemId); var authorityFactionId = controlState?.ControllerFactionId ?? controlState?.PrimaryClaimantFactionId; if (authorityFactionId is null || string.Equals(authorityFactionId, factionId, StringComparison.Ordinal)) { return true; } var hasAccess = string.Equals(accessKind, "trade", StringComparison.Ordinal) ? GeopoliticalSimulationService.HasTradeAccess(world, factionId, authorityFactionId) : GeopoliticalSimulationService.HasMilitaryAccess(world, factionId, authorityFactionId); if (!hasAccess) { denialReason = $"{accessKind}-access-denied:{authorityFactionId}"; return false; } if (policy?.AvoidHostileSystems != true) { return true; } if (GeopoliticalSimulationService.HasHostileRelation(world, factionId, authorityFactionId)) { denialReason = $"hostile-authority:{authorityFactionId}"; return false; } var hostileInfluencer = controlState?.InfluencingFactionIds.FirstOrDefault(candidate => !string.Equals(candidate, factionId, StringComparison.Ordinal) && GeopoliticalSimulationService.HasHostileRelation(world, factionId, candidate)); if (hostileInfluencer is not null) { denialReason = $"hostile-influence:{hostileInfluencer}"; return false; } return true; } private static CommanderAssignmentRuntime? ResolveAssignment(SimulationWorld world, ShipRuntime ship) => ship.CommanderId is null ? null : world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId)?.Assignment; private static StationRuntime? ResolveSupportStation(SimulationWorld world, ShipRuntime ship, ConstructionSiteRuntime site) { return ResolveStation(world, ResolveAssignment(world, ship)?.HomeStationId ?? ship.DefaultBehavior.HomeStationId) ?? world.Stations .Where(station => station.FactionId == ship.FactionId) .OrderByDescending(station => station.SystemId == site.SystemId ? 1 : 0) .ThenBy(station => station.Id, StringComparer.Ordinal) .FirstOrDefault(); } private static Vector3 ResolveSupportPosition(ShipRuntime ship, StationRuntime station, ConstructionSiteRuntime? site, SimulationWorld world) { if (ship.DockedStationId is not null) { return GetShipDockedPosition(ship, station); } if (site?.StationId is null && site is not null) { var anchorPosition = ResolveAnchor(world, site.AnchorId)?.Position ?? station.Position; return GetResourceHoldPosition(anchorPosition, ship.Id, 78f); } return GetConstructionHoldPosition(station, ship.Id); } private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) => ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f); private static void TrackHistory(ShipRuntime ship) { var orderId = ship.ActiveOrderId ?? "none"; var subTask = ship.ActiveSubTaskIndex >= ship.ActiveSubTasks.Count ? null : ship.ActiveSubTasks[ship.ActiveSubTaskIndex]; var signature = $"{ship.State.ToContractValue()}|{orderId}|{subTask?.Kind ?? "none"}|{GetShipCargoAmount(ship):0.0}"; if (ship.LastSignature == signature) { return; } ship.LastSignature = signature; ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} order={orderId} task={subTask?.Kind ?? "none"} cargo={GetShipCargoAmount(ship):0.#}"); if (ship.History.Count > 24) { ship.History.RemoveAt(0); } } private static void EmitStateEvents(ShipRuntime ship, ShipState previousState, string? previousOrderId, string? previousTaskId, ICollection events) { var currentOrderId = ship.ActiveOrderId; var currentTaskId = ship.ActiveSubTaskIndex >= ship.ActiveSubTasks.Count ? null : ship.ActiveSubTasks[ship.ActiveSubTaskIndex].Id; var occurredAtUtc = DateTimeOffset.UtcNow; if (previousState != ship.State) { events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Name} state {previousState.ToContractValue()} -> {ship.State.ToContractValue()}.", occurredAtUtc)); } if (!string.Equals(previousOrderId, currentOrderId, StringComparison.Ordinal)) { events.Add(new SimulationEventRecord("ship", ship.Id, "order-changed", $"{ship.Definition.Name} switched active order.", occurredAtUtc)); } if (!string.Equals(previousTaskId, currentTaskId, StringComparison.Ordinal)) { events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Name} advanced active task.", occurredAtUtc)); } } private static void CompleteStationFoundation(SimulationWorld world, StationRuntime supportStation, ConstructionSiteRuntime site) { var anchor = ResolveAnchor(world, site.AnchorId); if (anchor is null || site.BlueprintId is null) { site.State = ConstructionSiteStateKinds.Destroyed; return; } var station = new StationRuntime { Id = $"station-{world.Stations.Count + 1}", SystemId = site.SystemId, AnchorId = site.AnchorId, Label = BuildFoundedStationLabel(site.TargetDefinitionId), Category = "station", Objective = DetermineFoundationObjective(site.TargetDefinitionId), Color = world.Factions.FirstOrDefault(candidate => candidate.Id == site.FactionId)?.Color ?? supportStation.Color, Position = Vector3.Zero, FactionId = site.FactionId, Health = 600f, MaxHealth = 600f, }; foreach (var moduleId in GetFoundationModules(world, site.BlueprintId)) { AddStationModule(world, station, moduleId); } world.Stations.Add(station); StationLifecycleService.EnsureStationCommander(world, station); anchor.OccupyingStructureId = station.Id; site.StationId = station.Id; PrepareNextConstructionSiteStep(world, station, site); } private static IReadOnlyList GetFoundationModules(SimulationWorld world, string primaryModuleId) { var modules = new List { "module_arg_dock_m_01_lowtech" }; foreach (var itemId in world.ProductionGraph.OutputsByModuleId.GetValueOrDefault(primaryModuleId, [])) { if (world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) { var storageModule = GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind); if (storageModule is not null && !modules.Contains(storageModule, StringComparer.Ordinal)) { modules.Add(storageModule); } } } if (!modules.Contains("module_arg_stor_container_m_01", StringComparer.Ordinal)) { modules.Add("module_arg_stor_container_m_01"); } if (!string.Equals(primaryModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal)) { modules.Add("module_gen_prod_energycells_01"); } modules.Add(primaryModuleId); return modules.Distinct(StringComparer.Ordinal).ToList(); } private static string DetermineFoundationObjective(string commodityId) => commodityId switch { "energycells" => "power", "water" => "water", "refinedmetals" => "refinery", "hullparts" => "hullparts", "claytronics" => "claytronics", "shipyard" => "shipyard", _ => "general", }; private static string BuildFoundedStationLabel(string commodityId) => $"{char.ToUpperInvariant(commodityId[0])}{commodityId[1..]} Foundry"; }