namespace SpaceGame.Api.PlayerFaction.Simulation; internal sealed class PlayerFactionService { private const int MaxDecisionEntries = 64; private const int MaxAlerts = 32; private const string PlayerFactionDomainId = "player-faction"; internal static bool IsPlayerFaction(SimulationWorld world, string factionId) => world.PlayerFaction is not null && string.Equals(world.PlayerFaction.SovereignFactionId, factionId, StringComparison.Ordinal); internal PlayerFactionRuntime EnsureDomain(SimulationWorld world) { if (world.PlayerFaction is not null) { return world.PlayerFaction; } var sovereignFaction = world.Factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).FirstOrDefault() ?? throw new InvalidOperationException("Cannot create a player faction domain without any factions in the world."); world.PlayerFaction = new PlayerFactionRuntime { Id = PlayerFactionDomainId, Label = $"{sovereignFaction.Label} Command", SovereignFactionId = sovereignFaction.Id, CreatedAtUtc = world.GeneratedAtUtc, UpdatedAtUtc = world.GeneratedAtUtc, }; EnsureBaseStructures(world, world.PlayerFaction); SyncRegistry(world, world.PlayerFaction); return world.PlayerFaction; } internal void Update(SimulationWorld world, float _deltaSeconds, ICollection events) { if (world.PlayerFaction is null && world.Factions.Count == 0) { return; } var player = EnsureDomain(world); EnsureBaseStructures(world, player); SyncRegistry(world, player); PrunePlayerState(world, player); RefreshGeopoliticalOrganizationContext(world, player); ReconcileOrganizationAssignments(world, player); ReconcileDirectiveScopes(player); RefreshProductionPrograms(world, player); ApplyStrategicIntegration(world, player); ApplyPolicies(world, player); ApplyAssignmentsAndDirectives(world, player, events); RefreshAlerts(world, player); player.UpdatedAtUtc = DateTimeOffset.UtcNow; } internal PlayerFactionRuntime CreateOrganization(SimulationWorld world, PlayerOrganizationCommandRequest request) { var player = EnsureDomain(world); var id = CreateDomainId(request.Kind, request.Label, ExistingOrganizationIds(player)); var nowUtc = DateTimeOffset.UtcNow; switch (NormalizeKind(request.Kind)) { case "fleet": player.Fleets.Add(new PlayerFleetRuntime { Id = id, Label = request.Label, Role = request.Role ?? "general-purpose", FrontId = request.FrontId, HomeSystemId = request.HomeSystemId, HomeStationId = request.HomeStationId, PolicyId = request.PolicyId, AutomationPolicyId = request.AutomationPolicyId, ReinforcementPolicyId = request.ReinforcementPolicyId, UpdatedAtUtc = nowUtc, }); player.AssetRegistry.FleetIds.Add(id); break; case "task-force": player.TaskForces.Add(new PlayerTaskForceRuntime { Id = id, Label = request.Label, Role = request.Role ?? "task-force", FleetId = request.ParentOrganizationId, FrontId = request.FrontId, PolicyId = request.PolicyId, AutomationPolicyId = request.AutomationPolicyId, UpdatedAtUtc = nowUtc, }); player.AssetRegistry.TaskForceIds.Add(id); break; case "station-group": var stationGroup = new PlayerStationGroupRuntime { Id = id, Label = request.Label, Role = request.Role ?? "industrial-group", EconomicRegionId = request.ParentOrganizationId, PolicyId = request.PolicyId, AutomationPolicyId = request.AutomationPolicyId, UpdatedAtUtc = nowUtc, }; foreach (var itemId in request.FocusItemIds ?? []) { stationGroup.FocusItemIds.Add(itemId); } player.StationGroups.Add(stationGroup); player.AssetRegistry.StationGroupIds.Add(id); break; case "economic-region": var region = new PlayerEconomicRegionRuntime { Id = id, Label = request.Label, Role = request.Role ?? "balanced-region", PolicyId = request.PolicyId, AutomationPolicyId = request.AutomationPolicyId, UpdatedAtUtc = nowUtc, }; foreach (var systemId in request.SystemIds ?? []) { if (world.Systems.Any(system => string.Equals(system.Definition.Id, systemId, StringComparison.Ordinal))) { region.SystemIds.Add(systemId); } } player.EconomicRegions.Add(region); player.AssetRegistry.EconomicRegionIds.Add(id); break; case "front": var front = new PlayerFrontRuntime { Id = id, Label = request.Label, Priority = request.Priority ?? 50f, Posture = request.Role ?? "hold", TargetFactionId = request.TargetFactionId, UpdatedAtUtc = nowUtc, }; foreach (var systemId in request.SystemIds ?? []) { if (world.Systems.Any(system => string.Equals(system.Definition.Id, systemId, StringComparison.Ordinal))) { front.SystemIds.Add(systemId); } } player.Fronts.Add(front); player.AssetRegistry.FrontIds.Add(id); break; case "reserve": player.Reserves.Add(new PlayerReserveGroupRuntime { Id = id, Label = request.Label, ReserveKind = request.ReserveKind ?? "military", HomeSystemId = request.HomeSystemId, PolicyId = request.PolicyId, UpdatedAtUtc = nowUtc, }); player.AssetRegistry.ReserveIds.Add(id); break; default: throw new InvalidOperationException($"Unsupported organization kind '{request.Kind}'."); } AddDecision(player, "organization-created", $"Created {request.Kind} {request.Label}.", request.Kind, id); player.UpdatedAtUtc = nowUtc; return player; } internal PlayerFactionRuntime DeleteOrganization(SimulationWorld world, string organizationId) { var player = EnsureDomain(world); RemoveOrganization(player, organizationId); player.Assignments.RemoveAll(assignment => assignment.FleetId == organizationId || assignment.TaskForceId == organizationId || assignment.StationGroupId == organizationId || assignment.EconomicRegionId == organizationId || assignment.FrontId == organizationId || assignment.ReserveId == organizationId); ReconcileOrganizationAssignments(world, player); ReconcileDirectiveScopes(player); AddDecision(player, "organization-deleted", $"Removed organization {organizationId}.", "organization", organizationId); player.UpdatedAtUtc = DateTimeOffset.UtcNow; return player; } internal PlayerFactionRuntime UpdateOrganizationMembership(SimulationWorld world, string organizationId, PlayerOrganizationMembershipCommandRequest request) { var player = EnsureDomain(world); var kind = ResolveOrganizationKind(player, organizationId); switch (kind) { case "fleet": var fleet = player.Fleets.First(entity => entity.Id == organizationId); UpdateStringList(fleet.AssetIds, request.AssetIds, request.Replace, player.AssetRegistry.ShipIds); UpdateStringList(fleet.TaskForceIds, request.ChildOrganizationIds, request.Replace, player.AssetRegistry.TaskForceIds); fleet.UpdatedAtUtc = DateTimeOffset.UtcNow; break; case "task-force": var taskForce = player.TaskForces.First(entity => entity.Id == organizationId); UpdateStringList(taskForce.AssetIds, request.AssetIds, request.Replace, player.AssetRegistry.ShipIds); taskForce.UpdatedAtUtc = DateTimeOffset.UtcNow; break; case "station-group": var stationGroup = player.StationGroups.First(entity => entity.Id == organizationId); UpdateStringList(stationGroup.StationIds, request.AssetIds, request.Replace, player.AssetRegistry.StationIds); stationGroup.UpdatedAtUtc = DateTimeOffset.UtcNow; break; case "economic-region": var region = player.EconomicRegions.First(entity => entity.Id == organizationId); UpdateStringList(region.SystemIds, request.SystemIds, request.Replace, world.Systems.Select(system => system.Definition.Id)); UpdateStringList(region.StationGroupIds, request.ChildOrganizationIds, request.Replace, player.AssetRegistry.StationGroupIds); region.UpdatedAtUtc = DateTimeOffset.UtcNow; break; case "front": var front = player.Fronts.First(entity => entity.Id == organizationId); UpdateStringList(front.SystemIds, request.SystemIds, request.Replace, world.Systems.Select(system => system.Definition.Id)); UpdateStringList(front.FleetIds, request.ChildOrganizationIds, request.Replace, player.AssetRegistry.FleetIds); front.UpdatedAtUtc = DateTimeOffset.UtcNow; break; case "reserve": var reserve = player.Reserves.First(entity => entity.Id == organizationId); UpdateStringList(reserve.AssetIds, request.AssetIds, request.Replace, player.AssetRegistry.ShipIds); UpdateStringList(reserve.FrontIds, request.FrontIds, request.Replace, player.AssetRegistry.FrontIds); reserve.UpdatedAtUtc = DateTimeOffset.UtcNow; break; default: throw new InvalidOperationException($"Unknown organization '{organizationId}'."); } ReconcileOrganizationAssignments(world, player); ReconcileDirectiveScopes(player); AddDecision(player, "membership-updated", $"Updated membership for {organizationId}.", "organization", organizationId); player.UpdatedAtUtc = DateTimeOffset.UtcNow; return player; } internal PlayerFactionRuntime UpsertDirective(SimulationWorld world, string? directiveId, PlayerDirectiveCommandRequest request) { var player = EnsureDomain(world); var directive = directiveId is null ? null : player.Directives.FirstOrDefault(candidate => string.Equals(candidate.Id, directiveId, StringComparison.Ordinal)); if (directive is null) { directive = new PlayerDirectiveRuntime { Id = directiveId ?? CreateDomainId("directive", request.Label, player.Directives.Select(candidate => candidate.Id)), Label = request.Label, CreatedAtUtc = DateTimeOffset.UtcNow, }; player.Directives.Add(directive); } directive.Label = request.Label; directive.Kind = request.Kind; directive.ScopeKind = request.ScopeKind; directive.ScopeId = request.ScopeId; directive.BehaviorKind = request.BehaviorKind; directive.UseOrders = request.UseOrders; directive.StagingOrderKind = request.StagingOrderKind; directive.TargetEntityId = request.TargetEntityId; directive.TargetSystemId = request.TargetSystemId; directive.TargetPosition = request.TargetPosition is null ? null : new Vector3(request.TargetPosition.X, request.TargetPosition.Y, request.TargetPosition.Z); directive.HomeSystemId = request.HomeSystemId; directive.HomeStationId = request.HomeStationId; directive.SourceStationId = request.SourceStationId; directive.DestinationStationId = request.DestinationStationId; directive.ItemId = request.ItemId; directive.PreferredNodeId = request.PreferredNodeId; directive.PreferredConstructionSiteId = request.PreferredConstructionSiteId; directive.PreferredModuleId = request.PreferredModuleId; directive.Priority = request.Priority; directive.Radius = MathF.Max(0f, request.Radius ?? directive.Radius); directive.WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? directive.WaitSeconds); directive.MaxSystemRange = Math.Max(0, request.MaxSystemRange ?? directive.MaxSystemRange); directive.KnownStationsOnly = request.KnownStationsOnly ?? directive.KnownStationsOnly; directive.PatrolPoints.Clear(); foreach (var point in request.PatrolPoints ?? []) { directive.PatrolPoints.Add(new Vector3(point.X, point.Y, point.Z)); } directive.RepeatOrders.Clear(); foreach (var template in request.RepeatOrders ?? []) { directive.RepeatOrders.Add(new ShipOrderTemplateRuntime { Kind = template.Kind, Label = template.Label, TargetEntityId = template.TargetEntityId, TargetSystemId = template.TargetSystemId, TargetPosition = template.TargetPosition is null ? null : new Vector3(template.TargetPosition.X, template.TargetPosition.Y, template.TargetPosition.Z), SourceStationId = template.SourceStationId, DestinationStationId = template.DestinationStationId, ItemId = template.ItemId, NodeId = template.NodeId, ConstructionSiteId = template.ConstructionSiteId, ModuleId = template.ModuleId, WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f), Radius = MathF.Max(0f, template.Radius ?? 0f), MaxSystemRange = template.MaxSystemRange, KnownStationsOnly = template.KnownStationsOnly ?? false, }); } directive.PolicyId = request.PolicyId; directive.AutomationPolicyId = request.AutomationPolicyId; directive.Notes = request.Notes; directive.UpdatedAtUtc = DateTimeOffset.UtcNow; AddDecision(player, "directive-upserted", $"Updated directive {directive.Label}.", "directive", directive.Id); player.UpdatedAtUtc = directive.UpdatedAtUtc; return player; } internal PlayerFactionRuntime DeleteDirective(SimulationWorld world, string directiveId) { var player = EnsureDomain(world); player.Directives.RemoveAll(directive => directive.Id == directiveId); foreach (var assignment in player.Assignments.Where(assignment => assignment.DirectiveId == directiveId)) { assignment.DirectiveId = null; } ReconcileDirectiveScopes(player); AddDecision(player, "directive-deleted", $"Removed directive {directiveId}.", "directive", directiveId); player.UpdatedAtUtc = DateTimeOffset.UtcNow; return player; } internal PlayerFactionRuntime UpsertPolicy(SimulationWorld world, string? policyId, PlayerPolicyCommandRequest request) { var player = EnsureDomain(world); var policy = policyId is null ? null : player.Policies.FirstOrDefault(candidate => string.Equals(candidate.Id, policyId, StringComparison.Ordinal)); if (policy is null) { policy = new PlayerFactionPolicyRuntime { Id = policyId ?? CreateDomainId("policy", request.Label, player.Policies.Select(candidate => candidate.Id)), Label = request.Label, }; player.Policies.Add(policy); } policy.Label = request.Label; policy.ScopeKind = request.ScopeKind; policy.ScopeId = request.ScopeId; policy.AllowDelegatedCombat = request.AllowDelegatedCombat; policy.AllowDelegatedTrade = request.AllowDelegatedTrade; policy.ReserveCreditsRatio = Math.Clamp(request.ReserveCreditsRatio, 0f, 1f); policy.ReserveMilitaryRatio = Math.Clamp(request.ReserveMilitaryRatio, 0f, 1f); if (request.TradeAccessPolicy is not null) { policy.TradeAccessPolicy = request.TradeAccessPolicy; } if (request.DockingAccessPolicy is not null) { policy.DockingAccessPolicy = request.DockingAccessPolicy; } if (request.ConstructionAccessPolicy is not null) { policy.ConstructionAccessPolicy = request.ConstructionAccessPolicy; } if (request.OperationalRangePolicy is not null) { policy.OperationalRangePolicy = request.OperationalRangePolicy; } if (request.CombatEngagementPolicy is not null) { policy.CombatEngagementPolicy = request.CombatEngagementPolicy; } if (request.AvoidHostileSystems.HasValue) { policy.AvoidHostileSystems = request.AvoidHostileSystems.Value; } if (request.FleeHullRatio.HasValue) { policy.FleeHullRatio = Math.Clamp(request.FleeHullRatio.Value, 0f, 1f); } if (request.BlacklistedSystemIds is not null) { policy.BlacklistedSystemIds.Clear(); foreach (var systemId in request.BlacklistedSystemIds) { policy.BlacklistedSystemIds.Add(systemId); } } policy.Notes = request.Notes; policy.UpdatedAtUtc = DateTimeOffset.UtcNow; var policySet = EnsurePolicySet(world, player, policy, request.PolicySetId); ApplyPolicySetRequest(policySet, request); policy.PolicySetId = policySet.Id; AddDecision(player, "policy-upserted", $"Updated policy {policy.Label}.", "policy", policy.Id); player.UpdatedAtUtc = policy.UpdatedAtUtc; return player; } internal PlayerFactionRuntime UpsertAutomationPolicy(SimulationWorld world, string? automationPolicyId, PlayerAutomationPolicyCommandRequest request) { var player = EnsureDomain(world); var policy = automationPolicyId is null ? null : player.AutomationPolicies.FirstOrDefault(candidate => string.Equals(candidate.Id, automationPolicyId, StringComparison.Ordinal)); if (policy is null) { policy = new PlayerAutomationPolicyRuntime { Id = automationPolicyId ?? CreateDomainId("automation", request.Label, player.AutomationPolicies.Select(candidate => candidate.Id)), Label = request.Label, }; player.AutomationPolicies.Add(policy); } policy.Label = request.Label; policy.ScopeKind = request.ScopeKind; policy.ScopeId = request.ScopeId; policy.Enabled = request.Enabled; policy.BehaviorKind = request.BehaviorKind; policy.UseOrders = request.UseOrders; policy.StagingOrderKind = request.StagingOrderKind; policy.MaxSystemRange = Math.Max(0, request.MaxSystemRange); policy.KnownStationsOnly = request.KnownStationsOnly; policy.Radius = MathF.Max(0f, request.Radius); policy.WaitSeconds = MathF.Max(0f, request.WaitSeconds); policy.PreferredItemId = request.PreferredItemId; policy.Notes = request.Notes; policy.RepeatOrders.Clear(); foreach (var template in request.RepeatOrders ?? []) { policy.RepeatOrders.Add(new ShipOrderTemplateRuntime { Kind = template.Kind, Label = template.Label, TargetEntityId = template.TargetEntityId, TargetSystemId = template.TargetSystemId, TargetPosition = template.TargetPosition is null ? null : new Vector3(template.TargetPosition.X, template.TargetPosition.Y, template.TargetPosition.Z), SourceStationId = template.SourceStationId, DestinationStationId = template.DestinationStationId, ItemId = template.ItemId, NodeId = template.NodeId, ConstructionSiteId = template.ConstructionSiteId, ModuleId = template.ModuleId, WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f), Radius = MathF.Max(0f, template.Radius ?? 0f), MaxSystemRange = template.MaxSystemRange, KnownStationsOnly = template.KnownStationsOnly ?? false, }); } policy.UpdatedAtUtc = DateTimeOffset.UtcNow; AddDecision(player, "automation-upserted", $"Updated automation policy {policy.Label}.", "automation-policy", policy.Id); player.UpdatedAtUtc = policy.UpdatedAtUtc; return player; } internal PlayerFactionRuntime UpsertReinforcementPolicy(SimulationWorld world, string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request) { var player = EnsureDomain(world); var policy = reinforcementPolicyId is null ? null : player.ReinforcementPolicies.FirstOrDefault(candidate => string.Equals(candidate.Id, reinforcementPolicyId, StringComparison.Ordinal)); if (policy is null) { policy = new PlayerReinforcementPolicyRuntime { Id = reinforcementPolicyId ?? CreateDomainId("reinforcement", request.Label, player.ReinforcementPolicies.Select(candidate => candidate.Id)), Label = request.Label, }; player.ReinforcementPolicies.Add(policy); } policy.Label = request.Label; policy.ScopeKind = request.ScopeKind; policy.ScopeId = request.ScopeId; policy.ShipKind = request.ShipKind; policy.DesiredAssetCount = Math.Max(0, request.DesiredAssetCount); policy.MinimumReserveCount = Math.Max(0, request.MinimumReserveCount); policy.AutoTransferReserves = request.AutoTransferReserves; policy.AutoQueueProduction = request.AutoQueueProduction; policy.SourceReserveId = request.SourceReserveId; policy.TargetFrontId = request.TargetFrontId; policy.Notes = request.Notes; policy.UpdatedAtUtc = DateTimeOffset.UtcNow; AddDecision(player, "reinforcement-upserted", $"Updated reinforcement policy {policy.Label}.", "reinforcement-policy", policy.Id); player.UpdatedAtUtc = policy.UpdatedAtUtc; return player; } internal PlayerFactionRuntime UpsertProductionProgram(SimulationWorld world, string? productionProgramId, PlayerProductionProgramCommandRequest request) { var player = EnsureDomain(world); var program = productionProgramId is null ? null : player.ProductionPrograms.FirstOrDefault(candidate => string.Equals(candidate.Id, productionProgramId, StringComparison.Ordinal)); if (program is null) { program = new PlayerProductionProgramRuntime { Id = productionProgramId ?? CreateDomainId("production", request.Label, player.ProductionPrograms.Select(candidate => candidate.Id)), Label = request.Label, }; player.ProductionPrograms.Add(program); } program.Label = request.Label; program.Kind = request.Kind; program.TargetShipKind = request.TargetShipKind; program.TargetModuleId = request.TargetModuleId; program.TargetItemId = request.TargetItemId; program.TargetCount = Math.Max(0, request.TargetCount); program.StationGroupId = request.StationGroupId; program.ReinforcementPolicyId = request.ReinforcementPolicyId; program.Notes = request.Notes; program.UpdatedAtUtc = DateTimeOffset.UtcNow; AddDecision(player, "production-upserted", $"Updated production program {program.Label}.", "production-program", program.Id); player.UpdatedAtUtc = program.UpdatedAtUtc; return player; } internal PlayerFactionRuntime UpsertAssignment(SimulationWorld world, string assetId, PlayerAssetAssignmentCommandRequest request) { var player = EnsureDomain(world); var assignment = player.Assignments.FirstOrDefault(candidate => string.Equals(candidate.AssetId, assetId, StringComparison.Ordinal) && string.Equals(candidate.AssetKind, request.AssetKind, StringComparison.Ordinal)); if (assignment is null) { assignment = new PlayerAssignmentRuntime { Id = $"assignment-{request.AssetKind}-{assetId}", AssetKind = request.AssetKind, AssetId = assetId, }; player.Assignments.Add(assignment); } if (request.ClearConflicts) { RemoveAssetFromOrganizations(player, request.AssetKind, assetId); } if (request.FleetId is not null) { AddAssetToFleet(player, request.FleetId, assetId); } if (request.TaskForceId is not null) { AddAssetToTaskForce(player, request.TaskForceId, assetId); } if (request.StationGroupId is not null) { AddAssetToStationGroup(player, request.StationGroupId, assetId); } if (request.ReserveId is not null) { AddAssetToReserve(player, request.ReserveId, assetId); } assignment.FleetId = request.FleetId; assignment.TaskForceId = request.TaskForceId; assignment.StationGroupId = request.StationGroupId; assignment.EconomicRegionId = request.EconomicRegionId ?? assignment.EconomicRegionId; assignment.FrontId = request.FrontId ?? assignment.FrontId; assignment.ReserveId = request.ReserveId; assignment.DirectiveId = request.DirectiveId; assignment.PolicyId = request.PolicyId; assignment.AutomationPolicyId = request.AutomationPolicyId; assignment.Role = request.Role; assignment.Status = "active"; assignment.UpdatedAtUtc = DateTimeOffset.UtcNow; ReconcileOrganizationAssignments(world, player); ReconcileDirectiveScopes(player); AddDecision(player, "assignment-upserted", $"Assigned {request.AssetKind} {assetId}.", request.AssetKind, assetId); player.UpdatedAtUtc = assignment.UpdatedAtUtc; return player; } internal PlayerFactionRuntime UpdateStrategicIntent(SimulationWorld world, PlayerStrategicIntentCommandRequest request) { var player = EnsureDomain(world); player.StrategicIntent.StrategicPosture = request.StrategicPosture; player.StrategicIntent.EconomicPosture = request.EconomicPosture; player.StrategicIntent.MilitaryPosture = request.MilitaryPosture; player.StrategicIntent.LogisticsPosture = request.LogisticsPosture; player.StrategicIntent.DesiredReserveRatio = Math.Clamp(request.DesiredReserveRatio, 0f, 1f); player.StrategicIntent.AllowDelegatedCombatAutomation = request.AllowDelegatedCombatAutomation; player.StrategicIntent.AllowDelegatedEconomicAutomation = request.AllowDelegatedEconomicAutomation; player.StrategicIntent.Notes = request.Notes; player.UpdatedAtUtc = DateTimeOffset.UtcNow; AddDecision(player, "strategic-intent-updated", "Updated player strategic intent.", "player-faction", player.Id); return player; } internal ShipRuntime? EnqueueDirectShipOrder(SimulationWorld world, string shipId, ShipOrderCommandRequest request) { var player = EnsureDomain(world); if (!player.AssetRegistry.ShipIds.Contains(shipId)) { return null; } var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == shipId); if (ship is null) { return null; } if (ship.OrderQueue.Count >= 8) { throw new InvalidOperationException("Order queue is full."); } ship.OrderQueue.Add(new ShipOrderRuntime { Id = $"order-{ship.Id}-{Guid.NewGuid():N}", Kind = request.Kind, Priority = request.Priority, InterruptCurrentPlan = request.InterruptCurrentPlan, Label = request.Label, TargetEntityId = request.TargetEntityId, TargetSystemId = request.TargetSystemId, TargetPosition = request.TargetPosition is null ? null : new Vector3(request.TargetPosition.X, request.TargetPosition.Y, request.TargetPosition.Z), SourceStationId = request.SourceStationId, DestinationStationId = request.DestinationStationId, ItemId = request.ItemId, NodeId = request.NodeId, ConstructionSiteId = request.ConstructionSiteId, ModuleId = request.ModuleId, WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? 0f), Radius = MathF.Max(0f, request.Radius ?? 0f), MaxSystemRange = request.MaxSystemRange, KnownStationsOnly = request.KnownStationsOnly ?? false, }); AddDecision(player, "ship-order-enqueued", $"Queued {request.Kind} for {ship.Definition.Label}.", "ship", shipId); player.UpdatedAtUtc = DateTimeOffset.UtcNow; ship.ControlSourceKind = "player-order"; ship.ControlSourceId = ship.OrderQueue .Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) .OrderByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => order.Id) .FirstOrDefault(); ship.ControlReason = request.Label ?? request.Kind; ship.NeedsReplan = true; ship.LastReplanReason = "player-order-enqueued"; ship.LastDeltaSignature = string.Empty; return ship; } internal ShipRuntime? RemoveDirectShipOrder(SimulationWorld world, string shipId, string orderId) { var player = EnsureDomain(world); if (!player.AssetRegistry.ShipIds.Contains(shipId)) { return null; } var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == shipId); if (ship is null) { return null; } var removed = ship.OrderQueue.RemoveAll(order => order.Id == orderId); if (removed > 0) { AddDecision(player, "ship-order-removed", $"Removed order {orderId} from {ship.Definition.Label}.", "ship", shipId); player.UpdatedAtUtc = DateTimeOffset.UtcNow; } ship.ControlSourceKind = ship.OrderQueue.Any(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) ? "player-order" : "player-manual"; ship.ControlSourceId = ship.OrderQueue .Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) .OrderByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => order.Id) .FirstOrDefault(); ship.ControlReason = ship.OrderQueue .Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) .OrderByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => order.Label ?? order.Kind) .FirstOrDefault() ?? "manual-player-control"; ship.NeedsReplan = true; ship.LastReplanReason = "player-order-removed"; ship.LastDeltaSignature = string.Empty; return ship; } internal ShipRuntime? ConfigureDirectShipBehavior(SimulationWorld world, string shipId, ShipDefaultBehaviorCommandRequest request) { var player = EnsureDomain(world); if (!player.AssetRegistry.ShipIds.Contains(shipId)) { return null; } var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == shipId); if (ship is null) { return null; } var directiveId = $"player-directive-ship-{shipId}"; var directive = player.Directives.FirstOrDefault(candidate => candidate.Id == directiveId); if (directive is null) { directive = new PlayerDirectiveRuntime { Id = directiveId, Label = $"Direct control {ship.Definition.Label}", ScopeKind = "ship", ScopeId = shipId, Kind = "direct-control", CreatedAtUtc = DateTimeOffset.UtcNow, }; player.Directives.Add(directive); } directive.Label = $"Direct control {ship.Definition.Label}"; directive.Kind = "direct-control"; directive.ScopeKind = "ship"; directive.ScopeId = shipId; directive.BehaviorKind = request.Kind; directive.UseOrders = false; directive.StagingOrderKind = null; directive.TargetEntityId = request.TargetEntityId; directive.TargetSystemId = request.AreaSystemId; directive.TargetPosition = request.TargetPosition is null ? null : new Vector3(request.TargetPosition.X, request.TargetPosition.Y, request.TargetPosition.Z); directive.HomeSystemId = request.HomeSystemId ?? ship.SystemId; directive.HomeStationId = request.HomeStationId; directive.SourceStationId = request.HomeStationId; directive.DestinationStationId = null; directive.ItemId = request.PreferredItemId; directive.PreferredNodeId = request.PreferredNodeId; directive.PreferredConstructionSiteId = request.PreferredConstructionSiteId; directive.PreferredModuleId = request.PreferredModuleId; directive.Priority = 100; directive.Radius = MathF.Max(0f, request.Radius ?? ship.DefaultBehavior.Radius); directive.WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? ship.DefaultBehavior.WaitSeconds); directive.MaxSystemRange = Math.Max(0, request.MaxSystemRange ?? ship.DefaultBehavior.MaxSystemRange); directive.KnownStationsOnly = request.KnownStationsOnly ?? ship.DefaultBehavior.KnownStationsOnly; directive.PatrolPoints.Clear(); foreach (var point in request.PatrolPoints ?? []) { directive.PatrolPoints.Add(new Vector3(point.X, point.Y, point.Z)); } directive.RepeatOrders.Clear(); foreach (var template in request.RepeatOrders ?? []) { directive.RepeatOrders.Add(new ShipOrderTemplateRuntime { Kind = template.Kind, Label = template.Label, TargetEntityId = template.TargetEntityId, TargetSystemId = template.TargetSystemId, TargetPosition = template.TargetPosition is null ? null : new Vector3(template.TargetPosition.X, template.TargetPosition.Y, template.TargetPosition.Z), SourceStationId = template.SourceStationId, DestinationStationId = template.DestinationStationId, ItemId = template.ItemId, NodeId = template.NodeId, ConstructionSiteId = template.ConstructionSiteId, ModuleId = template.ModuleId, WaitSeconds = MathF.Max(0f, template.WaitSeconds ?? 0f), Radius = MathF.Max(0f, template.Radius ?? 0f), MaxSystemRange = template.MaxSystemRange, KnownStationsOnly = template.KnownStationsOnly ?? false, }); } directive.UpdatedAtUtc = DateTimeOffset.UtcNow; var assignment = GetOrCreateAssignment(player, "ship", shipId); assignment.DirectiveId = directive.Id; assignment.Status = "active"; assignment.UpdatedAtUtc = directive.UpdatedAtUtc; ApplyBehavior(ship.DefaultBehavior, BuildDirectiveBehavior(ship, directive, null)); ship.ControlSourceKind = "player-directive"; ship.ControlSourceId = directive.Id; ship.ControlReason = directive.Label; AddDecision(player, "ship-behavior-configured", $"Configured {request.Kind} for {ship.Definition.Label}.", "ship", shipId); player.UpdatedAtUtc = directive.UpdatedAtUtc; ship.NeedsReplan = true; ship.LastReplanReason = "player-behavior-configured"; ship.LastDeltaSignature = string.Empty; return ship; } private static void EnsureBaseStructures(SimulationWorld world, PlayerFactionRuntime player) { if (player.Policies.Count == 0) { var sovereign = world.Factions.FirstOrDefault(faction => string.Equals(faction.Id, player.SovereignFactionId, StringComparison.Ordinal)); player.Policies.Add(new PlayerFactionPolicyRuntime { Id = "player-core-policy", Label = "Core Empire Policy", PolicySetId = sovereign?.DefaultPolicySetId, }); if (sovereign?.DefaultPolicySetId is { } defaultPolicySetId && world.Policies.FirstOrDefault(policy => policy.Id == defaultPolicySetId) is { } defaultPolicySet) { CopyPolicySetToPlayerPolicy(defaultPolicySet, player.Policies[0]); } } if (player.AutomationPolicies.Count == 0) { player.AutomationPolicies.Add(new PlayerAutomationPolicyRuntime { Id = "player-core-automation", Label = "Core Automation", BehaviorKind = "idle", }); } if (player.Reserves.Count == 0) { player.Reserves.Add(new PlayerReserveGroupRuntime { Id = "player-core-reserve", Label = "Strategic Reserve", ReserveKind = "military", }); player.AssetRegistry.ReserveIds.Add("player-core-reserve"); } } private static void SyncRegistry(SimulationWorld world, PlayerFactionRuntime player) { SyncSet(player.AssetRegistry.ShipIds, world.Ships.Where(ship => ship.FactionId == player.SovereignFactionId).Select(ship => ship.Id)); SyncSet(player.AssetRegistry.StationIds, world.Stations.Where(station => station.FactionId == player.SovereignFactionId).Select(station => station.Id)); SyncSet(player.AssetRegistry.CommanderIds, world.Commanders.Where(commander => commander.FactionId == player.SovereignFactionId).Select(commander => commander.Id)); SyncSet(player.AssetRegistry.ClaimIds, world.Claims.Where(claim => claim.FactionId == player.SovereignFactionId).Select(claim => claim.Id)); SyncSet(player.AssetRegistry.ConstructionSiteIds, world.ConstructionSites.Where(site => site.FactionId == player.SovereignFactionId).Select(site => site.Id)); SyncSet(player.AssetRegistry.PolicySetIds, world.Policies.Where(policy => policy.OwnerId == player.SovereignFactionId || player.Policies.Any(entry => entry.PolicySetId == policy.Id)).Select(policy => policy.Id)); SyncSet(player.AssetRegistry.MarketOrderIds, world.MarketOrders.Where(order => order.FactionId == player.SovereignFactionId).Select(order => order.Id)); SyncSet(player.AssetRegistry.FleetIds, player.Fleets.Select(fleet => fleet.Id)); SyncSet(player.AssetRegistry.TaskForceIds, player.TaskForces.Select(taskForce => taskForce.Id)); SyncSet(player.AssetRegistry.StationGroupIds, player.StationGroups.Select(group => group.Id)); SyncSet(player.AssetRegistry.EconomicRegionIds, player.EconomicRegions.Select(region => region.Id)); SyncSet(player.AssetRegistry.FrontIds, player.Fronts.Select(front => front.Id)); SyncSet(player.AssetRegistry.ReserveIds, player.Reserves.Select(reserve => reserve.Id)); } private static void PrunePlayerState(SimulationWorld world, PlayerFactionRuntime player) { var shipIds = player.AssetRegistry.ShipIds; var stationIds = player.AssetRegistry.StationIds; var frontIds = player.AssetRegistry.FrontIds; var fleetIds = player.AssetRegistry.FleetIds; var reserveIds = player.AssetRegistry.ReserveIds; var taskForceIds = player.AssetRegistry.TaskForceIds; var stationGroupIds = player.AssetRegistry.StationGroupIds; var regionIds = player.AssetRegistry.EconomicRegionIds; var directiveIds = player.Directives.Select(directive => directive.Id).ToHashSet(StringComparer.Ordinal); var policyIds = player.Policies.Select(policy => policy.Id).ToHashSet(StringComparer.Ordinal); var automationIds = player.AutomationPolicies.Select(policy => policy.Id).ToHashSet(StringComparer.Ordinal); foreach (var fleet in player.Fleets) { fleet.AssetIds.RemoveAll(assetId => !shipIds.Contains(assetId)); fleet.TaskForceIds.RemoveAll(taskForceId => !taskForceIds.Contains(taskForceId)); fleet.DirectiveIds.RemoveAll(directiveId => !directiveIds.Contains(directiveId)); } foreach (var taskForce in player.TaskForces) { taskForce.AssetIds.RemoveAll(assetId => !shipIds.Contains(assetId)); taskForce.DirectiveIds.RemoveAll(directiveId => !directiveIds.Contains(directiveId)); if (taskForce.FleetId is not null && !fleetIds.Contains(taskForce.FleetId)) { taskForce.FleetId = null; } } foreach (var group in player.StationGroups) { group.StationIds.RemoveAll(stationId => !stationIds.Contains(stationId)); group.DirectiveIds.RemoveAll(directiveId => !directiveIds.Contains(directiveId)); if (group.EconomicRegionId is not null && !regionIds.Contains(group.EconomicRegionId)) { group.EconomicRegionId = null; } } foreach (var region in player.EconomicRegions) { region.SystemIds.RemoveAll(systemId => !world.Systems.Any(system => string.Equals(system.Definition.Id, systemId, StringComparison.Ordinal))); region.StationGroupIds.RemoveAll(groupId => !stationGroupIds.Contains(groupId)); region.DirectiveIds.RemoveAll(directiveId => !directiveIds.Contains(directiveId)); } foreach (var front in player.Fronts) { front.SystemIds.RemoveAll(systemId => !world.Systems.Any(system => string.Equals(system.Definition.Id, systemId, StringComparison.Ordinal))); front.FleetIds.RemoveAll(fleetId => !fleetIds.Contains(fleetId)); front.ReserveIds.RemoveAll(reserveId => !reserveIds.Contains(reserveId)); front.DirectiveIds.RemoveAll(directiveId => !directiveIds.Contains(directiveId)); } foreach (var reserve in player.Reserves) { reserve.AssetIds.RemoveAll(assetId => !shipIds.Contains(assetId)); reserve.FrontIds.RemoveAll(frontId => !frontIds.Contains(frontId)); } player.Assignments.RemoveAll(assignment => (assignment.AssetKind == "ship" && !shipIds.Contains(assignment.AssetId)) || (assignment.AssetKind == "station" && !stationIds.Contains(assignment.AssetId))); foreach (var assignment in player.Assignments) { if (assignment.FleetId is not null && !fleetIds.Contains(assignment.FleetId)) { assignment.FleetId = null; } if (assignment.TaskForceId is not null && !taskForceIds.Contains(assignment.TaskForceId)) { assignment.TaskForceId = null; } if (assignment.StationGroupId is not null && !stationGroupIds.Contains(assignment.StationGroupId)) { assignment.StationGroupId = null; } if (assignment.EconomicRegionId is not null && !regionIds.Contains(assignment.EconomicRegionId)) { assignment.EconomicRegionId = null; } if (assignment.FrontId is not null && !frontIds.Contains(assignment.FrontId)) { assignment.FrontId = null; } if (assignment.ReserveId is not null && !reserveIds.Contains(assignment.ReserveId)) { assignment.ReserveId = null; } if (assignment.DirectiveId is not null && !directiveIds.Contains(assignment.DirectiveId)) { assignment.DirectiveId = null; } if (assignment.PolicyId is not null && !policyIds.Contains(assignment.PolicyId)) { assignment.PolicyId = null; } if (assignment.AutomationPolicyId is not null && !automationIds.Contains(assignment.AutomationPolicyId)) { assignment.AutomationPolicyId = null; } } } private static void ApplyPolicies(SimulationWorld world, PlayerFactionRuntime player) { foreach (var policy in player.Policies) { if (policy.PolicySetId is null) { continue; } if (world.Policies.FirstOrDefault(candidate => candidate.Id == policy.PolicySetId) is { } policySet) { policySet.TradeAccessPolicy = policy.TradeAccessPolicy; policySet.DockingAccessPolicy = policy.DockingAccessPolicy; policySet.ConstructionAccessPolicy = policy.ConstructionAccessPolicy; policySet.OperationalRangePolicy = policy.OperationalRangePolicy; policySet.CombatEngagementPolicy = policy.CombatEngagementPolicy; policySet.FleeHullRatio = Math.Clamp(policy.FleeHullRatio, 0.05f, 0.95f); policySet.AvoidHostileSystems = policy.AvoidHostileSystems; policySet.BlacklistedSystemIds.Clear(); foreach (var systemId in policy.BlacklistedSystemIds) { policySet.BlacklistedSystemIds.Add(systemId); } } } } private static void ApplyAssignmentsAndDirectives(SimulationWorld world, PlayerFactionRuntime player, ICollection events) { var factionCommander = world.Commanders.FirstOrDefault(commander => commander.Kind == CommanderKind.Faction && string.Equals(commander.FactionId, player.SovereignFactionId, StringComparison.Ordinal)); if (factionCommander is null) { return; } var fleetCommanders = EnsureFleetCommanders(world, player, factionCommander); var taskForceCommanders = EnsureTaskForceCommanders(world, player, factionCommander, fleetCommanders); var assignmentsByAsset = player.Assignments .Where(assignment => assignment.Status == "active") .GroupBy(assignment => $"{assignment.AssetKind}:{assignment.AssetId}", StringComparer.Ordinal) .ToDictionary(group => group.Key, group => group.OrderByDescending(item => item.UpdatedAtUtc).First(), StringComparer.Ordinal); foreach (var ship in world.Ships.Where(candidate => candidate.FactionId == player.SovereignFactionId)) { if (ship.CommanderId is null) { continue; } var commander = world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId); if (commander is null) { continue; } var assignment = ResolveAssignment(assignmentsByAsset, "ship", ship.Id); var directive = ResolveDirective(player, assignment, "ship", ship.Id); var automation = ResolveAutomation(player, assignment, directive, "ship", ship.Id); var policy = ResolvePolicy(player, assignment, directive, "ship", ship.Id); commander.ParentCommanderId = ResolveParentCommanderId(factionCommander, assignment, fleetCommanders, taskForceCommanders); commander.PolicySetId = policy?.PolicySetId ?? factionCommander.PolicySetId; ship.PolicySetId = commander.PolicySetId; var changed = ApplyDirectiveToShip(commander, ship, directive, automation, assignment); if (changed && directive is not null) { events.Add(new SimulationEventRecord("ship", ship.Id, "player-directive", $"{ship.Definition.Label} aligned to player directive {directive.Label}.", DateTimeOffset.UtcNow, "player", "universe", ship.Id)); } } foreach (var station in world.Stations.Where(candidate => candidate.FactionId == player.SovereignFactionId)) { if (station.CommanderId is null) { continue; } var commander = world.Commanders.FirstOrDefault(candidate => candidate.Id == station.CommanderId); if (commander is null) { continue; } var assignment = ResolveAssignment(assignmentsByAsset, "station", station.Id); var directive = ResolveDirective(player, assignment, "station", station.Id); var policy = ResolvePolicy(player, assignment, directive, "station", station.Id); commander.PolicySetId = policy?.PolicySetId ?? factionCommander.PolicySetId; station.PolicySetId = commander.PolicySetId; commander.Assignment = directive is null && assignment is null ? null : new CommanderAssignmentRuntime { ObjectiveId = directive?.Id ?? assignment?.StationGroupId ?? $"player-station-{station.Id}", Kind = directive?.Kind ?? "player-station-control", BehaviorKind = directive?.BehaviorKind ?? assignment?.Role ?? "station-control", Status = directive?.Status ?? assignment?.Status ?? "active", Priority = directive?.Priority ?? 40f, HomeSystemId = directive?.HomeSystemId ?? station.SystemId, HomeStationId = directive?.HomeStationId ?? station.Id, TargetSystemId = directive?.TargetSystemId, TargetEntityId = directive?.TargetEntityId, TargetPosition = directive?.TargetPosition, ItemId = directive?.ItemId, Notes = directive?.Notes ?? assignment?.Role, UpdatedAtUtc = directive?.UpdatedAtUtc ?? assignment?.UpdatedAtUtc ?? DateTimeOffset.UtcNow, }; } } private static Dictionary EnsureFleetCommanders(SimulationWorld world, PlayerFactionRuntime player, CommanderRuntime factionCommander) { var map = new Dictionary(StringComparer.Ordinal); foreach (var fleet in player.Fleets) { var commander = world.Commanders.FirstOrDefault(candidate => candidate.Kind == CommanderKind.Fleet && candidate.FactionId == player.SovereignFactionId && string.Equals(candidate.ControlledEntityId, fleet.Id, StringComparison.Ordinal)); if (commander is null) { commander = new CommanderRuntime { Id = $"commander-player-fleet-{fleet.Id}", Kind = CommanderKind.Fleet, FactionId = player.SovereignFactionId, ControlledEntityId = fleet.Id, Doctrine = "player-fleet-control", Skills = new CommanderSkillProfileRuntime { Leadership = 5, Coordination = 4, Strategy = 4 }, }; world.Commanders.Add(commander); } commander.ParentCommanderId = factionCommander.Id; commander.PolicySetId = ResolvePolicySetId(world, player, fleet.PolicyId) ?? factionCommander.PolicySetId; commander.Assignment = new CommanderAssignmentRuntime { ObjectiveId = fleet.Id, Kind = "player-fleet", BehaviorKind = fleet.Role, Status = fleet.Status, Priority = 80f, HomeSystemId = fleet.HomeSystemId, HomeStationId = fleet.HomeStationId, Notes = fleet.Label, UpdatedAtUtc = fleet.UpdatedAtUtc, }; fleet.CommanderId = commander.Id; map[fleet.Id] = commander; } return map; } private static Dictionary EnsureTaskForceCommanders( SimulationWorld world, PlayerFactionRuntime player, CommanderRuntime factionCommander, IReadOnlyDictionary fleetCommanders) { var map = new Dictionary(StringComparer.Ordinal); foreach (var taskForce in player.TaskForces) { var commander = world.Commanders.FirstOrDefault(candidate => candidate.Kind == CommanderKind.TaskGroup && candidate.FactionId == player.SovereignFactionId && string.Equals(candidate.ControlledEntityId, taskForce.Id, StringComparison.Ordinal)); if (commander is null) { commander = new CommanderRuntime { Id = $"commander-player-task-force-{taskForce.Id}", Kind = CommanderKind.TaskGroup, FactionId = player.SovereignFactionId, ControlledEntityId = taskForce.Id, Doctrine = "player-task-force-control", Skills = new CommanderSkillProfileRuntime { Leadership = 4, Coordination = 4, Strategy = 4 }, }; world.Commanders.Add(commander); } commander.ParentCommanderId = taskForce.FleetId is not null && fleetCommanders.TryGetValue(taskForce.FleetId, out var fleetCommander) ? fleetCommander.Id : factionCommander.Id; commander.PolicySetId = ResolvePolicySetId(world, player, taskForce.PolicyId) ?? factionCommander.PolicySetId; commander.Assignment = new CommanderAssignmentRuntime { ObjectiveId = taskForce.Id, Kind = "player-task-force", BehaviorKind = taskForce.Role, Status = taskForce.Status, Priority = 75f, Notes = taskForce.Label, UpdatedAtUtc = taskForce.UpdatedAtUtc, }; taskForce.CommanderId = commander.Id; map[taskForce.Id] = commander; } return map; } private static string ResolveParentCommanderId( CommanderRuntime factionCommander, PlayerAssignmentRuntime? assignment, IReadOnlyDictionary fleetCommanders, IReadOnlyDictionary taskForceCommanders) { if (assignment?.TaskForceId is not null && taskForceCommanders.TryGetValue(assignment.TaskForceId, out var taskForceCommander)) { return taskForceCommander.Id; } if (assignment?.FleetId is not null && fleetCommanders.TryGetValue(assignment.FleetId, out var fleetCommander)) { return fleetCommander.Id; } return factionCommander.Id; } private static PlayerAssignmentRuntime? ResolveAssignment( IReadOnlyDictionary assignmentsByAsset, string assetKind, string assetId) => assignmentsByAsset.GetValueOrDefault($"{assetKind}:{assetId}"); private static PlayerDirectiveRuntime? ResolveDirective(PlayerFactionRuntime player, PlayerAssignmentRuntime? assignment, string assetKind, string assetId) { if (assignment?.DirectiveId is not null) { return player.Directives.FirstOrDefault(directive => directive.Id == assignment.DirectiveId); } return SelectScopedDirective( player.Directives.Where(directive => directive.Status == "active"), player, assignment, assetKind, assetId); } private static PlayerAutomationPolicyRuntime? ResolveAutomation(PlayerFactionRuntime player, PlayerAssignmentRuntime? assignment, PlayerDirectiveRuntime? directive, string assetKind, string assetId) { var automationId = assignment?.AutomationPolicyId ?? directive?.AutomationPolicyId; if (automationId is not null) { return player.AutomationPolicies.FirstOrDefault(policy => policy.Id == automationId); } return SelectScopedAutomationPolicy(player, assignment, assetKind, assetId) ?? player.AutomationPolicies.FirstOrDefault(policy => policy.Id == "player-core-automation"); } private static PlayerFactionPolicyRuntime? ResolvePolicy(PlayerFactionRuntime player, PlayerAssignmentRuntime? assignment, PlayerDirectiveRuntime? directive, string assetKind, string assetId) { var policyId = assignment?.PolicyId ?? directive?.PolicyId; if (policyId is not null) { return player.Policies.FirstOrDefault(policy => policy.Id == policyId); } return SelectScopedFactionPolicy(player, assignment, assetKind, assetId) ?? player.Policies.FirstOrDefault(policy => policy.Id == "player-core-policy"); } private static bool ApplyDirectiveToShip( CommanderRuntime commander, ShipRuntime ship, PlayerDirectiveRuntime? directive, PlayerAutomationPolicyRuntime? automation, PlayerAssignmentRuntime? assignment) { var desiredAssignment = BuildDirectiveAssignment(ship, directive, automation, assignment); var desiredBehavior = BuildDirectiveBehavior(ship, directive, automation); var hasBehaviorSource = directive is not null || automation is not null; var desiredControlSourceKind = directive is not null ? "player-directive" : automation is not null ? "player-automation" : ship.OrderQueue.Any(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) ? "player-order" : "player-manual"; var desiredControlSourceId = directive?.Id ?? automation?.Id ?? ship.OrderQueue .Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) .OrderByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => order.Id) .FirstOrDefault(); var desiredControlReason = directive?.Label ?? automation?.Label ?? ship.OrderQueue .Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) .OrderByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => order.Label ?? order.Kind) .FirstOrDefault() ?? (hasBehaviorSource ? "delegated-player-control" : "manual-player-control"); var assignmentChanged = !AssignmentsEqual(commander.Assignment, desiredAssignment); var behaviorChanged = hasBehaviorSource && !DefaultBehaviorsEqual(ship.DefaultBehavior, desiredBehavior!); var ordersChanged = ReconcileDirectiveOrders(ship, directive, automation); var controlChanged = !string.Equals(ship.ControlSourceKind, desiredControlSourceKind, StringComparison.Ordinal) || !string.Equals(ship.ControlSourceId, desiredControlSourceId, StringComparison.Ordinal) || !string.Equals(ship.ControlReason, desiredControlReason, StringComparison.Ordinal); if (assignmentChanged) { commander.Assignment = desiredAssignment; } if (behaviorChanged && desiredBehavior is not null) { ApplyBehavior(ship.DefaultBehavior, desiredBehavior); } if (directive is null && automation is null) { ship.ControlSourceKind = desiredControlSourceKind; ship.ControlSourceId = desiredControlSourceId; ship.ControlReason = desiredControlReason; var surfaceChanged = assignmentChanged || ordersChanged || controlChanged; if (surfaceChanged) { ship.LastDeltaSignature = string.Empty; } if (assignmentChanged || ordersChanged) { ship.NeedsReplan = true; ship.LastReplanReason = assignmentChanged ? "player-assignment-updated" : ordersChanged ? "player-order-updated" : "player-control-updated"; } return surfaceChanged; } ship.ControlSourceKind = desiredControlSourceKind; ship.ControlSourceId = desiredControlSourceId; ship.ControlReason = desiredControlReason; var changed = assignmentChanged || behaviorChanged || ordersChanged || controlChanged; if (changed) { ship.LastDeltaSignature = string.Empty; } if (assignmentChanged || behaviorChanged || ordersChanged) { ship.NeedsReplan = true; ship.LastReplanReason = assignmentChanged ? "player-assignment-updated" : behaviorChanged ? "player-behavior-updated" : ordersChanged ? "player-order-updated" : "player-control-updated"; } return changed; } private static DefaultBehaviorRuntime BuildDirectiveBehavior(ShipRuntime ship, PlayerDirectiveRuntime? directive, PlayerAutomationPolicyRuntime? automation) { return new DefaultBehaviorRuntime { Kind = directive?.BehaviorKind ?? automation?.BehaviorKind ?? ship.DefaultBehavior.Kind, HomeSystemId = directive?.HomeSystemId ?? ship.DefaultBehavior.HomeSystemId ?? ship.SystemId, HomeStationId = directive?.HomeStationId ?? ship.DefaultBehavior.HomeStationId, AreaSystemId = directive?.TargetSystemId ?? directive?.HomeSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId, TargetEntityId = directive?.TargetEntityId, PreferredItemId = directive?.ItemId ?? automation?.PreferredItemId ?? ship.DefaultBehavior.PreferredItemId, PreferredNodeId = directive?.PreferredNodeId ?? ship.DefaultBehavior.PreferredNodeId, PreferredConstructionSiteId = directive?.PreferredConstructionSiteId ?? ship.DefaultBehavior.PreferredConstructionSiteId, PreferredModuleId = directive?.PreferredModuleId ?? ship.DefaultBehavior.PreferredModuleId, TargetPosition = directive?.TargetPosition, WaitSeconds = directive?.WaitSeconds ?? automation?.WaitSeconds ?? ship.DefaultBehavior.WaitSeconds, Radius = directive?.Radius ?? automation?.Radius ?? ship.DefaultBehavior.Radius, MaxSystemRange = directive?.MaxSystemRange ?? automation?.MaxSystemRange ?? ship.DefaultBehavior.MaxSystemRange, KnownStationsOnly = directive?.KnownStationsOnly ?? automation?.KnownStationsOnly ?? ship.DefaultBehavior.KnownStationsOnly, PatrolPoints = directive?.PatrolPoints.Select(point => point).ToList() ?? ship.DefaultBehavior.PatrolPoints.Select(point => point).ToList(), PatrolIndex = ship.DefaultBehavior.PatrolIndex, RepeatOrders = directive?.RepeatOrders.Select(CloneTemplate).ToList() ?? automation?.RepeatOrders.Select(CloneTemplate).ToList() ?? ship.DefaultBehavior.RepeatOrders.Select(CloneTemplate).ToList(), RepeatIndex = ship.DefaultBehavior.RepeatIndex, }; } private static bool ReconcileDirectiveOrders(ShipRuntime ship, PlayerDirectiveRuntime? directive, PlayerAutomationPolicyRuntime? automation) { var aiOrderId = directive is null ? null : $"player-order-{directive.Id}"; var changed = ship.OrderQueue.RemoveAll(order => order.Id.StartsWith("player-order-", StringComparison.Ordinal) && order.Id != aiOrderId) > 0; var useOrders = directive?.UseOrders ?? automation?.UseOrders ?? false; if (!useOrders || directive is null || string.IsNullOrWhiteSpace(directive.StagingOrderKind)) { return changed; } var desiredOrder = new ShipOrderRuntime { Id = aiOrderId!, Kind = directive.StagingOrderKind!, Priority = Math.Max(0, directive.Priority), InterruptCurrentPlan = true, Label = directive.Label, TargetEntityId = directive.TargetEntityId, TargetSystemId = directive.TargetSystemId, TargetPosition = directive.TargetPosition, SourceStationId = directive.SourceStationId ?? directive.HomeStationId, DestinationStationId = directive.DestinationStationId, ItemId = directive.ItemId, NodeId = directive.PreferredNodeId, ConstructionSiteId = directive.PreferredConstructionSiteId, ModuleId = directive.PreferredModuleId, WaitSeconds = directive.WaitSeconds, Radius = directive.Radius, MaxSystemRange = directive.MaxSystemRange, KnownStationsOnly = directive.KnownStationsOnly, }; var existing = ship.OrderQueue.FirstOrDefault(order => order.Id == aiOrderId); if (existing is null) { ship.OrderQueue.Add(desiredOrder); return true; } if (!ShipOrdersEqual(existing, desiredOrder)) { ship.OrderQueue.Remove(existing); ship.OrderQueue.Add(desiredOrder); return true; } return changed; } private static CommanderAssignmentRuntime? BuildDirectiveAssignment( ShipRuntime ship, PlayerDirectiveRuntime? directive, PlayerAutomationPolicyRuntime? automation, PlayerAssignmentRuntime? assignment) { if (directive is null && automation is null) { return null; } var behavior = directive?.BehaviorKind ?? automation?.BehaviorKind ?? ship.DefaultBehavior.Kind; return new CommanderAssignmentRuntime { ObjectiveId = directive?.Id ?? assignment?.DirectiveId ?? $"automation-{ship.Id}", Kind = directive?.Kind ?? "player-automation", BehaviorKind = behavior, Status = directive?.Status ?? "active", Priority = directive?.Priority ?? 50f, HomeSystemId = directive?.HomeSystemId ?? ship.DefaultBehavior.HomeSystemId, HomeStationId = directive?.HomeStationId ?? ship.DefaultBehavior.HomeStationId, TargetSystemId = directive?.TargetSystemId, TargetEntityId = directive?.TargetEntityId, TargetPosition = directive?.TargetPosition, ItemId = directive?.ItemId, Notes = directive?.Notes ?? automation?.Notes, UpdatedAtUtc = directive?.UpdatedAtUtc ?? automation?.UpdatedAtUtc ?? DateTimeOffset.UtcNow, }; } private static void ApplyBehavior(DefaultBehaviorRuntime target, DefaultBehaviorRuntime source) { target.Kind = source.Kind; target.HomeSystemId = source.HomeSystemId; target.HomeStationId = source.HomeStationId; target.AreaSystemId = source.AreaSystemId; target.TargetEntityId = source.TargetEntityId; target.PreferredItemId = source.PreferredItemId; target.PreferredNodeId = source.PreferredNodeId; target.PreferredConstructionSiteId = source.PreferredConstructionSiteId; target.PreferredModuleId = source.PreferredModuleId; target.TargetPosition = source.TargetPosition; target.WaitSeconds = source.WaitSeconds; target.Radius = source.Radius; target.MaxSystemRange = source.MaxSystemRange; target.KnownStationsOnly = source.KnownStationsOnly; target.PatrolPoints = source.PatrolPoints.Select(point => point).ToList(); target.PatrolIndex = source.PatrolIndex; target.RepeatOrders = source.RepeatOrders.Select(CloneTemplate).ToList(); target.RepeatIndex = source.RepeatIndex; } private static bool DefaultBehaviorsEqual(DefaultBehaviorRuntime left, DefaultBehaviorRuntime right) => string.Equals(left.Kind, right.Kind, StringComparison.Ordinal) && string.Equals(left.HomeSystemId, right.HomeSystemId, StringComparison.Ordinal) && string.Equals(left.HomeStationId, right.HomeStationId, StringComparison.Ordinal) && string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) && string.Equals(left.PreferredItemId, right.PreferredItemId, StringComparison.Ordinal) && string.Equals(left.PreferredNodeId, right.PreferredNodeId, StringComparison.Ordinal) && string.Equals(left.PreferredConstructionSiteId, right.PreferredConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal) && Nullable.Equals(left.TargetPosition, right.TargetPosition) && left.WaitSeconds.Equals(right.WaitSeconds) && left.Radius.Equals(right.Radius) && left.MaxSystemRange == right.MaxSystemRange && left.KnownStationsOnly == right.KnownStationsOnly && left.PatrolPoints.SequenceEqual(right.PatrolPoints) && left.RepeatOrders.Count == right.RepeatOrders.Count && left.RepeatOrders.Zip(right.RepeatOrders, ShipOrderTemplatesEqual).All(equal => equal); private static bool ShipOrderTemplatesEqual(ShipOrderTemplateRuntime left, ShipOrderTemplateRuntime right) => string.Equals(left.Kind, right.Kind, StringComparison.Ordinal) && string.Equals(left.Label, right.Label, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) && string.Equals(left.TargetSystemId, right.TargetSystemId, StringComparison.Ordinal) && Nullable.Equals(left.TargetPosition, right.TargetPosition) && string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal) && string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal) && string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal) && left.WaitSeconds.Equals(right.WaitSeconds) && left.Radius.Equals(right.Radius) && left.MaxSystemRange == right.MaxSystemRange && left.KnownStationsOnly == right.KnownStationsOnly; private static bool ShipOrdersEqual(ShipOrderRuntime left, ShipOrderRuntime right) => string.Equals(left.Id, right.Id, StringComparison.Ordinal) && string.Equals(left.Kind, right.Kind, 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) && Nullable.Equals(left.TargetPosition, right.TargetPosition) && string.Equals(left.SourceStationId, right.SourceStationId, StringComparison.Ordinal) && string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal) && string.Equals(left.ConstructionSiteId, right.ConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.ModuleId, right.ModuleId, StringComparison.Ordinal) && left.WaitSeconds.Equals(right.WaitSeconds) && left.Radius.Equals(right.Radius) && left.MaxSystemRange == right.MaxSystemRange && left.KnownStationsOnly == right.KnownStationsOnly; private static bool AssignmentsEqual(CommanderAssignmentRuntime? left, CommanderAssignmentRuntime? right) { if (ReferenceEquals(left, right)) { return true; } if (left is null || right is null) { return false; } return string.Equals(left.ObjectiveId, right.ObjectiveId, StringComparison.Ordinal) && string.Equals(left.CampaignId, right.CampaignId, StringComparison.Ordinal) && string.Equals(left.TheaterId, right.TheaterId, StringComparison.Ordinal) && string.Equals(left.Kind, right.Kind, StringComparison.Ordinal) && string.Equals(left.BehaviorKind, right.BehaviorKind, StringComparison.Ordinal) && string.Equals(left.Status, right.Status, StringComparison.Ordinal) && left.Priority.Equals(right.Priority) && string.Equals(left.HomeSystemId, right.HomeSystemId, StringComparison.Ordinal) && string.Equals(left.HomeStationId, right.HomeStationId, StringComparison.Ordinal) && string.Equals(left.TargetSystemId, right.TargetSystemId, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) && Nullable.Equals(left.TargetPosition, right.TargetPosition) && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal); } private static ShipOrderTemplateRuntime CloneTemplate(ShipOrderTemplateRuntime template) => new() { Kind = template.Kind, Label = template.Label, TargetEntityId = template.TargetEntityId, TargetSystemId = template.TargetSystemId, TargetPosition = template.TargetPosition, SourceStationId = template.SourceStationId, DestinationStationId = template.DestinationStationId, ItemId = template.ItemId, NodeId = template.NodeId, ConstructionSiteId = template.ConstructionSiteId, ModuleId = template.ModuleId, WaitSeconds = template.WaitSeconds, Radius = template.Radius, MaxSystemRange = template.MaxSystemRange, KnownStationsOnly = template.KnownStationsOnly, }; private static void ReconcileOrganizationAssignments(SimulationWorld world, PlayerFactionRuntime player) { var fleetMemberships = new Dictionary>(StringComparer.Ordinal); var taskForceMemberships = new Dictionary>(StringComparer.Ordinal); var stationGroupMemberships = new Dictionary>(StringComparer.Ordinal); var reserveMemberships = new Dictionary>(StringComparer.Ordinal); foreach (var fleet in player.Fleets) { foreach (var assetId in fleet.AssetIds.Where(player.AssetRegistry.ShipIds.Contains)) { AddMembership(fleetMemberships, assetId, fleet.Id); GetOrCreateAssignment(player, "ship", assetId); } } foreach (var taskForce in player.TaskForces) { foreach (var assetId in taskForce.AssetIds.Where(player.AssetRegistry.ShipIds.Contains)) { AddMembership(taskForceMemberships, assetId, taskForce.Id); GetOrCreateAssignment(player, "ship", assetId); } } foreach (var group in player.StationGroups) { foreach (var stationId in group.StationIds.Where(player.AssetRegistry.StationIds.Contains)) { AddMembership(stationGroupMemberships, stationId, group.Id); GetOrCreateAssignment(player, "station", stationId); } } foreach (var reserve in player.Reserves) { foreach (var assetId in reserve.AssetIds.Where(player.AssetRegistry.ShipIds.Contains)) { AddMembership(reserveMemberships, assetId, reserve.Id); GetOrCreateAssignment(player, "ship", assetId); } } foreach (var assignment in player.Assignments) { if (assignment.AssetKind == "ship") { assignment.FleetId = SelectSingleMembership(fleetMemberships, assignment.AssetId); assignment.TaskForceId = SelectSingleMembership(taskForceMemberships, assignment.AssetId); assignment.ReserveId = SelectSingleMembership(reserveMemberships, assignment.AssetId); if (assignment.TaskForceId is not null && player.TaskForces.FirstOrDefault(taskForce => taskForce.Id == assignment.TaskForceId) is { FleetId: not null } taskForce) { assignment.FleetId ??= taskForce.FleetId; } if (assignment.FleetId is not null) { assignment.FrontId = player.Fronts .Where(front => front.FleetIds.Contains(assignment.FleetId, StringComparer.Ordinal)) .OrderByDescending(front => front.Priority) .ThenBy(front => front.Id, StringComparer.Ordinal) .Select(front => front.Id) .FirstOrDefault() ?? assignment.FrontId; } else if (assignment.ReserveId is not null) { assignment.FrontId = player.Fronts .Where(front => front.ReserveIds.Contains(assignment.ReserveId, StringComparer.Ordinal)) .OrderByDescending(front => front.Priority) .ThenBy(front => front.Id, StringComparer.Ordinal) .Select(front => front.Id) .FirstOrDefault() ?? player.Reserves.FirstOrDefault(reserve => reserve.Id == assignment.ReserveId)?.FrontIds .OrderBy(id => id, StringComparer.Ordinal) .FirstOrDefault() ?? assignment.FrontId; } } else if (assignment.AssetKind == "station") { assignment.StationGroupId = SelectSingleMembership(stationGroupMemberships, assignment.AssetId); if (assignment.StationGroupId is not null && player.StationGroups.FirstOrDefault(group => group.Id == assignment.StationGroupId) is { EconomicRegionId: not null } stationGroup) { assignment.EconomicRegionId = stationGroup.EconomicRegionId; } } } foreach (var assignment in player.Assignments) { assignment.UpdatedAtUtc = DateTimeOffset.UtcNow; } } private static void ReconcileDirectiveScopes(PlayerFactionRuntime player) { foreach (var fleet in player.Fleets) { fleet.DirectiveIds.Clear(); } foreach (var taskForce in player.TaskForces) { taskForce.DirectiveIds.Clear(); } foreach (var group in player.StationGroups) { group.DirectiveIds.Clear(); } foreach (var region in player.EconomicRegions) { region.DirectiveIds.Clear(); } foreach (var front in player.Fronts) { front.DirectiveIds.Clear(); } foreach (var directive in player.Directives.Where(directive => directive.Status == "active")) { switch (directive.ScopeKind) { case "fleet": player.Fleets.FirstOrDefault(entity => entity.Id == directive.ScopeId)?.DirectiveIds.Add(directive.Id); break; case "task-force": player.TaskForces.FirstOrDefault(entity => entity.Id == directive.ScopeId)?.DirectiveIds.Add(directive.Id); break; case "station-group": player.StationGroups.FirstOrDefault(entity => entity.Id == directive.ScopeId)?.DirectiveIds.Add(directive.Id); break; case "economic-region": player.EconomicRegions.FirstOrDefault(entity => entity.Id == directive.ScopeId)?.DirectiveIds.Add(directive.Id); break; case "front": player.Fronts.FirstOrDefault(entity => entity.Id == directive.ScopeId)?.DirectiveIds.Add(directive.Id); break; } } } private static void RefreshProductionPrograms(SimulationWorld world, PlayerFactionRuntime player) { foreach (var program in player.ProductionPrograms) { if (!string.IsNullOrWhiteSpace(program.TargetShipKind)) { program.CurrentCount = world.Ships.Count(ship => ship.FactionId == player.SovereignFactionId && string.Equals(ship.Definition.Kind, program.TargetShipKind, StringComparison.Ordinal)); } else { program.CurrentCount = 0; } } } private static void ApplyStrategicIntegration(SimulationWorld world, PlayerFactionRuntime player) { var faction = world.Factions.FirstOrDefault(candidate => candidate.Id == player.SovereignFactionId); if (faction is null) { return; } var corePolicy = player.Policies.FirstOrDefault(policy => policy.Id == "player-core-policy"); faction.Doctrine.StrategicPosture = player.StrategicIntent.StrategicPosture; faction.Doctrine.EconomicPosture = player.StrategicIntent.EconomicPosture; faction.Doctrine.MilitaryPosture = player.StrategicIntent.MilitaryPosture; faction.Doctrine.ReserveCreditsRatio = corePolicy?.ReserveCreditsRatio ?? faction.Doctrine.ReserveCreditsRatio; faction.Doctrine.ReserveMilitaryRatio = corePolicy?.ReserveMilitaryRatio ?? faction.Doctrine.ReserveMilitaryRatio; } private static void RefreshGeopoliticalOrganizationContext(SimulationWorld world, PlayerFactionRuntime player) { var regions = world.Geopolitics?.EconomyRegions.Regions .Where(region => string.Equals(region.FactionId, player.SovereignFactionId, StringComparison.Ordinal)) .ToList() ?? []; var fronts = world.Geopolitics?.Territory.FrontLines .Where(front => front.FactionIds.Contains(player.SovereignFactionId, StringComparer.Ordinal)) .ToList() ?? []; foreach (var region in player.EconomicRegions) { if (region.SystemIds.Count == 0) { region.SystemIds.AddRange( region.StationGroupIds .SelectMany(groupId => player.StationGroups.FirstOrDefault(group => group.Id == groupId)?.StationIds ?? []) .Select(stationId => world.Stations.FirstOrDefault(station => station.Id == stationId)?.SystemId) .Where(systemId => !string.IsNullOrWhiteSpace(systemId)) .Cast() .Distinct(StringComparer.Ordinal) .OrderBy(systemId => systemId, StringComparer.Ordinal)); } var matchedRegion = regions .Select(candidate => new { Region = candidate, Overlap = candidate.SystemIds.Intersect(region.SystemIds, StringComparer.Ordinal).Count(), }) .OrderByDescending(entry => entry.Overlap) .ThenBy(entry => entry.Region.Id, StringComparer.Ordinal) .Select(entry => entry.Region) .FirstOrDefault(); region.SharedEconomicRegionId = matchedRegion?.Id; if (matchedRegion is null) { continue; } if (region.SystemIds.Count == 0) { region.SystemIds.AddRange(matchedRegion.SystemIds.OrderBy(systemId => systemId, StringComparer.Ordinal)); } if (string.Equals(region.Role, "balanced-region", StringComparison.Ordinal)) { region.Role = matchedRegion.Kind; } } foreach (var front in player.Fronts) { if (front.SystemIds.Count == 0) { var fleetSystems = front.FleetIds .SelectMany(fleetId => player.Fleets.FirstOrDefault(fleet => fleet.Id == fleetId)?.AssetIds ?? []) .Select(assetId => world.Ships.FirstOrDefault(ship => ship.Id == assetId)?.SystemId) .Where(systemId => !string.IsNullOrWhiteSpace(systemId)) .Cast() .Distinct(StringComparer.Ordinal) .OrderBy(systemId => systemId, StringComparer.Ordinal) .ToList(); front.SystemIds.AddRange(fleetSystems); } var matchedFront = fronts .Select(candidate => new { Front = candidate, Overlap = candidate.SystemIds.Intersect(front.SystemIds, StringComparer.Ordinal).Count(), TargetBias = front.TargetFactionId is not null && candidate.FactionIds.Contains(front.TargetFactionId, StringComparer.Ordinal) ? 1 : 0, }) .OrderByDescending(entry => entry.Overlap + entry.TargetBias) .ThenBy(entry => entry.Front.Id, StringComparer.Ordinal) .Select(entry => entry.Front) .FirstOrDefault(); front.SharedFrontLineId = matchedFront?.Id; if (matchedFront is null) { continue; } if (front.SystemIds.Count == 0) { front.SystemIds.AddRange(matchedFront.SystemIds.OrderBy(systemId => systemId, StringComparer.Ordinal)); } front.TargetFactionId ??= matchedFront.FactionIds.FirstOrDefault(id => !string.Equals(id, player.SovereignFactionId, StringComparison.Ordinal)); } } private static PlayerDirectiveRuntime? SelectScopedDirective( IEnumerable directives, PlayerFactionRuntime player, PlayerAssignmentRuntime? assignment, string assetKind, string assetId) => directives .Where(directive => ScopeMatches(player, directive.ScopeKind, directive.ScopeId, assignment, assetKind, assetId)) .OrderByDescending(directive => ScopePriority(directive.ScopeKind)) .ThenByDescending(directive => directive.Priority) .ThenByDescending(directive => directive.UpdatedAtUtc) .ThenBy(directive => directive.Id, StringComparer.Ordinal) .FirstOrDefault(); private static PlayerAutomationPolicyRuntime? SelectScopedAutomationPolicy( PlayerFactionRuntime player, PlayerAssignmentRuntime? assignment, string assetKind, string assetId) => player.AutomationPolicies .Where(policy => policy.Enabled && ScopeMatches(player, policy.ScopeKind, policy.ScopeId, assignment, assetKind, assetId)) .OrderByDescending(policy => ScopePriority(policy.ScopeKind)) .ThenByDescending(policy => policy.UpdatedAtUtc) .ThenBy(policy => policy.Id, StringComparer.Ordinal) .FirstOrDefault(); private static PlayerFactionPolicyRuntime? SelectScopedFactionPolicy( PlayerFactionRuntime player, PlayerAssignmentRuntime? assignment, string assetKind, string assetId) => player.Policies .Where(policy => ScopeMatches(player, policy.ScopeKind, policy.ScopeId, assignment, assetKind, assetId)) .OrderByDescending(policy => ScopePriority(policy.ScopeKind)) .ThenByDescending(policy => policy.UpdatedAtUtc) .ThenBy(policy => policy.Id, StringComparer.Ordinal) .FirstOrDefault(); private static bool ScopeMatches( PlayerFactionRuntime player, string scopeKind, string? scopeId, PlayerAssignmentRuntime? assignment, string assetKind, string assetId) { return scopeKind switch { "player-faction" => string.IsNullOrWhiteSpace(scopeId) || string.Equals(scopeId, player.Id, StringComparison.Ordinal) || string.Equals(scopeId, player.SovereignFactionId, StringComparison.Ordinal), "asset" => string.Equals(scopeId, assetId, StringComparison.Ordinal), "ship" => assetKind == "ship" && string.Equals(scopeId, assetId, StringComparison.Ordinal), "station" => assetKind == "station" && string.Equals(scopeId, assetId, StringComparison.Ordinal), "fleet" => string.Equals(scopeId, assignment?.FleetId, StringComparison.Ordinal), "task-force" => string.Equals(scopeId, assignment?.TaskForceId, StringComparison.Ordinal), "station-group" => string.Equals(scopeId, assignment?.StationGroupId, StringComparison.Ordinal), "economic-region" => string.Equals(scopeId, assignment?.EconomicRegionId, StringComparison.Ordinal), "front" => string.Equals(scopeId, assignment?.FrontId, StringComparison.Ordinal), "reserve" => string.Equals(scopeId, assignment?.ReserveId, StringComparison.Ordinal), _ => false, }; } private static int ScopePriority(string scopeKind) => scopeKind switch { "ship" or "station" or "asset" => 100, "task-force" => 90, "fleet" or "station-group" or "reserve" => 80, "economic-region" or "front" => 70, "player-faction" => 10, _ => 0, }; private static PlayerAssignmentRuntime GetOrCreateAssignment(PlayerFactionRuntime player, string assetKind, string assetId) { var assignment = player.Assignments.FirstOrDefault(candidate => string.Equals(candidate.AssetKind, assetKind, StringComparison.Ordinal) && string.Equals(candidate.AssetId, assetId, StringComparison.Ordinal)); if (assignment is not null) { return assignment; } assignment = new PlayerAssignmentRuntime { Id = $"assignment-{assetKind}-{assetId}", AssetKind = assetKind, AssetId = assetId, }; player.Assignments.Add(assignment); return assignment; } private static void AddMembership(Dictionary> memberships, string assetId, string organizationId) { if (!memberships.TryGetValue(assetId, out var values)) { values = []; memberships[assetId] = values; } if (!values.Contains(organizationId, StringComparer.Ordinal)) { values.Add(organizationId); } } private static string? SelectSingleMembership(Dictionary> memberships, string assetId) => memberships.TryGetValue(assetId, out var values) ? values.OrderBy(value => value, StringComparer.Ordinal).FirstOrDefault() : null; private static void RemoveAssetFromOrganizations(PlayerFactionRuntime player, string assetKind, string assetId) { foreach (var fleet in player.Fleets) { fleet.AssetIds.RemoveAll(id => string.Equals(id, assetId, StringComparison.Ordinal)); } foreach (var taskForce in player.TaskForces) { taskForce.AssetIds.RemoveAll(id => string.Equals(id, assetId, StringComparison.Ordinal)); } foreach (var reserve in player.Reserves) { reserve.AssetIds.RemoveAll(id => string.Equals(id, assetId, StringComparison.Ordinal)); } if (assetKind == "station") { foreach (var group in player.StationGroups) { group.StationIds.RemoveAll(id => string.Equals(id, assetId, StringComparison.Ordinal)); } } } private static void AddAssetToFleet(PlayerFactionRuntime player, string fleetId, string assetId) { var fleet = player.Fleets.FirstOrDefault(entity => entity.Id == fleetId) ?? throw new InvalidOperationException($"Unknown fleet '{fleetId}'."); if (!fleet.AssetIds.Contains(assetId, StringComparer.Ordinal)) { fleet.AssetIds.Add(assetId); } } private static void AddAssetToTaskForce(PlayerFactionRuntime player, string taskForceId, string assetId) { var taskForce = player.TaskForces.FirstOrDefault(entity => entity.Id == taskForceId) ?? throw new InvalidOperationException($"Unknown task force '{taskForceId}'."); if (!taskForce.AssetIds.Contains(assetId, StringComparer.Ordinal)) { taskForce.AssetIds.Add(assetId); } } private static void AddAssetToStationGroup(PlayerFactionRuntime player, string groupId, string assetId) { var group = player.StationGroups.FirstOrDefault(entity => entity.Id == groupId) ?? throw new InvalidOperationException($"Unknown station group '{groupId}'."); if (!group.StationIds.Contains(assetId, StringComparer.Ordinal)) { group.StationIds.Add(assetId); } } private static void AddAssetToReserve(PlayerFactionRuntime player, string reserveId, string assetId) { var reserve = player.Reserves.FirstOrDefault(entity => entity.Id == reserveId) ?? throw new InvalidOperationException($"Unknown reserve '{reserveId}'."); if (!reserve.AssetIds.Contains(assetId, StringComparer.Ordinal)) { reserve.AssetIds.Add(assetId); } } private static void RefreshAlerts(SimulationWorld world, PlayerFactionRuntime player) { player.Alerts.Clear(); foreach (var shipId in player.AssetRegistry.ShipIds .Where(shipId => player.Fleets.Count(fleet => fleet.AssetIds.Contains(shipId, StringComparer.Ordinal)) > 1) .OrderBy(id => id, StringComparer.Ordinal) .Take(4)) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-conflict-fleet-{shipId}", Kind = "conflicting-fleet-membership", Severity = "warning", Summary = $"Ship {shipId} belongs to multiple fleets.", AssetKind = "ship", AssetId = shipId, }); } foreach (var shipId in player.AssetRegistry.ShipIds .Where(shipId => player.TaskForces.Count(taskForce => taskForce.AssetIds.Contains(shipId, StringComparer.Ordinal)) > 1) .OrderBy(id => id, StringComparer.Ordinal) .Take(4)) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-conflict-task-force-{shipId}", Kind = "conflicting-task-force-membership", Severity = "warning", Summary = $"Ship {shipId} belongs to multiple task forces.", AssetKind = "ship", AssetId = shipId, }); } foreach (var stationId in player.AssetRegistry.StationIds .Where(stationId => player.StationGroups.Count(group => group.StationIds.Contains(stationId, StringComparer.Ordinal)) > 1) .OrderBy(id => id, StringComparer.Ordinal) .Take(4)) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-conflict-station-group-{stationId}", Kind = "conflicting-station-group-membership", Severity = "warning", Summary = $"Station {stationId} belongs to multiple station groups.", AssetKind = "station", AssetId = stationId, }); } foreach (var shipId in player.AssetRegistry.ShipIds .Where(shipId => !player.Assignments.Any(assignment => assignment.AssetKind == "ship" && assignment.AssetId == shipId)) .OrderBy(id => id, StringComparer.Ordinal) .Take(10)) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-unassigned-ship-{shipId}", Kind = "unassigned-ship", Severity = "warning", Summary = $"Ship {shipId} has no player assignment.", AssetKind = "ship", AssetId = shipId, }); } foreach (var stationId in player.AssetRegistry.StationIds .Where(stationId => !player.Assignments.Any(assignment => assignment.AssetKind == "station" && assignment.AssetId == stationId)) .OrderBy(id => id, StringComparer.Ordinal) .Take(6)) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-unassigned-station-{stationId}", Kind = "unassigned-station", Severity = "info", Summary = $"Station {stationId} is not part of a player station group.", AssetKind = "station", AssetId = stationId, }); } foreach (var directive in player.Directives.Where(directive => directive.Status == "active" && !player.Assignments.Any(assignment => assignment.DirectiveId == directive.Id)).Take(6)) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-orphan-directive-{directive.Id}", Kind = "orphan-directive", Severity = "warning", Summary = $"Directive {directive.Label} is not assigned to any asset or group.", RelatedDirectiveId = directive.Id, }); } foreach (var policy in player.ReinforcementPolicies .Where(policy => policy.DesiredAssetCount > 0) .OrderBy(policy => policy.Id, StringComparer.Ordinal) .Take(6)) { var available = world.Ships.Count(ship => ship.FactionId == player.SovereignFactionId && string.Equals(ship.Definition.Kind, policy.ShipKind, StringComparison.Ordinal)); if (available < policy.DesiredAssetCount) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-reinforcement-{policy.Id}", Kind = "reinforcement-deficit", Severity = "warning", Summary = $"Reinforcement policy {policy.Label} is short {policy.DesiredAssetCount - available} {policy.ShipKind} assets.", }); } } foreach (var program in player.ProductionPrograms .Where(program => program.TargetCount > 0 && program.CurrentCount < program.TargetCount) .OrderBy(program => program.Id, StringComparer.Ordinal) .Take(6)) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-production-{program.Id}", Kind = "production-program-deficit", Severity = "info", Summary = $"Production program {program.Label} is at {program.CurrentCount}/{program.TargetCount}.", }); } foreach (var systemId in world.Geopolitics?.Territory.ControlStates .Where(state => state.IsContested && (string.Equals(state.ControllerFactionId, player.SovereignFactionId, StringComparison.Ordinal) || string.Equals(state.PrimaryClaimantFactionId, player.SovereignFactionId, StringComparison.Ordinal) || state.ClaimantFactionIds.Contains(player.SovereignFactionId, StringComparer.Ordinal))) .Select(state => state.SystemId) .Where(systemId => player.Fronts.All(front => !front.SystemIds.Contains(systemId, StringComparer.Ordinal))) .OrderBy(systemId => systemId, StringComparer.Ordinal) .Take(4) ?? []) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-contested-system-{systemId}", Kind = "uncovered-contested-system", Severity = "warning", Summary = $"Contested player system {systemId} is not covered by a player front.", }); } foreach (var region in player.EconomicRegions.Take(6)) { var sharedRegion = world.Geopolitics?.EconomyRegions.Regions.FirstOrDefault(candidate => string.Equals(candidate.FactionId, player.SovereignFactionId, StringComparison.Ordinal) && candidate.SystemIds.Intersect(region.SystemIds, StringComparer.Ordinal).Any()); if (sharedRegion is null) { continue; } var bottleneck = world.Geopolitics?.EconomyRegions.Bottlenecks .Where(candidate => string.Equals(candidate.RegionId, sharedRegion.Id, StringComparison.Ordinal)) .OrderByDescending(candidate => candidate.Severity) .ThenBy(candidate => candidate.ItemId, StringComparer.Ordinal) .FirstOrDefault(); var security = world.Geopolitics?.EconomyRegions.SecurityAssessments.FirstOrDefault(candidate => string.Equals(candidate.RegionId, sharedRegion.Id, StringComparison.Ordinal)); if (bottleneck is not null && bottleneck.Severity >= 2.5f) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-region-bottleneck-{region.Id}-{bottleneck.ItemId}", Kind = "economic-region-bottleneck", Severity = "warning", Summary = $"Region {region.Label} is bottlenecked on {bottleneck.ItemId}.", }); } if ((security?.SupplyRisk ?? 0f) >= 0.55f) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-region-risk-{region.Id}", Kind = "economic-region-risk", Severity = "warning", Summary = $"Region {region.Label} has elevated logistics risk.", }); } } foreach (var front in player.Fronts .Where(front => !string.IsNullOrWhiteSpace(front.TargetFactionId)) .Take(6)) { var relation = GeopoliticalSimulationService.FindRelation(world, player.SovereignFactionId, front.TargetFactionId!); if (relation is not null && relation.Posture is not "hostile" and not "war" && front.Priority >= 60f && !string.Equals(front.Posture, "hold", StringComparison.Ordinal)) { player.Alerts.Add(new PlayerAlertRuntime { Id = $"alert-front-posture-{front.Id}", Kind = "front-diplomatic-misalignment", Severity = "info", Summary = $"Front {front.Label} targets {front.TargetFactionId} while diplomatic posture is {relation.Posture}.", }); } } while (player.Alerts.Count > MaxAlerts) { player.Alerts.RemoveAt(player.Alerts.Count - 1); } } private static void AddDecision(PlayerFactionRuntime player, string kind, string summary, string? relatedKind, string? relatedId) { player.DecisionLog.Insert(0, new PlayerDecisionLogEntryRuntime { Id = $"player-decision-{Guid.NewGuid():N}", Kind = kind, Summary = summary, RelatedEntityKind = relatedKind, RelatedEntityId = relatedId, OccurredAtUtc = DateTimeOffset.UtcNow, }); while (player.DecisionLog.Count > MaxDecisionEntries) { player.DecisionLog.RemoveAt(player.DecisionLog.Count - 1); } } private static PolicySetRuntime EnsurePolicySet(SimulationWorld world, PlayerFactionRuntime player, PlayerFactionPolicyRuntime policy, string? requestedPolicySetId) { if (requestedPolicySetId is not null && world.Policies.FirstOrDefault(candidate => candidate.Id == requestedPolicySetId) is { } existing) { return existing; } if (policy.PolicySetId is not null && world.Policies.FirstOrDefault(candidate => candidate.Id == policy.PolicySetId) is { } current) { return current; } var created = new PolicySetRuntime { Id = $"policy-player-{policy.Id}", OwnerKind = "player-faction-policy", OwnerId = policy.Id, }; world.Policies.Add(created); player.AssetRegistry.PolicySetIds.Add(created.Id); return created; } private static void CopyPolicySetToPlayerPolicy(PolicySetRuntime policySet, PlayerFactionPolicyRuntime policy) { policy.TradeAccessPolicy = policySet.TradeAccessPolicy; policy.DockingAccessPolicy = policySet.DockingAccessPolicy; policy.ConstructionAccessPolicy = policySet.ConstructionAccessPolicy; policy.OperationalRangePolicy = policySet.OperationalRangePolicy; policy.CombatEngagementPolicy = policySet.CombatEngagementPolicy; policy.AvoidHostileSystems = policySet.AvoidHostileSystems; policy.FleeHullRatio = policySet.FleeHullRatio; policy.BlacklistedSystemIds.Clear(); foreach (var systemId in policySet.BlacklistedSystemIds) { policy.BlacklistedSystemIds.Add(systemId); } } private static void ApplyPolicySetRequest(PolicySetRuntime policySet, PlayerPolicyCommandRequest request) { if (request.TradeAccessPolicy is not null) { policySet.TradeAccessPolicy = request.TradeAccessPolicy; } if (request.DockingAccessPolicy is not null) { policySet.DockingAccessPolicy = request.DockingAccessPolicy; } if (request.ConstructionAccessPolicy is not null) { policySet.ConstructionAccessPolicy = request.ConstructionAccessPolicy; } if (request.OperationalRangePolicy is not null) { policySet.OperationalRangePolicy = request.OperationalRangePolicy; } if (request.CombatEngagementPolicy is not null) { policySet.CombatEngagementPolicy = request.CombatEngagementPolicy; } if (request.AvoidHostileSystems.HasValue) { policySet.AvoidHostileSystems = request.AvoidHostileSystems.Value; } if (request.FleeHullRatio.HasValue) { policySet.FleeHullRatio = Math.Clamp(request.FleeHullRatio.Value, 0f, 1f); } policySet.BlacklistedSystemIds.Clear(); foreach (var systemId in request.BlacklistedSystemIds ?? []) { policySet.BlacklistedSystemIds.Add(systemId); } } private static string? ResolvePolicySetId(SimulationWorld world, PlayerFactionRuntime player, string? policyId) { if (policyId is null) { return player.Policies.FirstOrDefault(policy => policy.Id == "player-core-policy")?.PolicySetId; } return player.Policies.FirstOrDefault(policy => policy.Id == policyId)?.PolicySetId ?? world.Policies.FirstOrDefault(policy => policy.Id == policyId)?.Id; } private static void RemoveOrganization(PlayerFactionRuntime player, string organizationId) { if (player.Fleets.RemoveAll(entity => entity.Id == organizationId) > 0) { player.AssetRegistry.FleetIds.Remove(organizationId); return; } if (player.TaskForces.RemoveAll(entity => entity.Id == organizationId) > 0) { player.AssetRegistry.TaskForceIds.Remove(organizationId); return; } if (player.StationGroups.RemoveAll(entity => entity.Id == organizationId) > 0) { player.AssetRegistry.StationGroupIds.Remove(organizationId); return; } if (player.EconomicRegions.RemoveAll(entity => entity.Id == organizationId) > 0) { player.AssetRegistry.EconomicRegionIds.Remove(organizationId); return; } if (player.Fronts.RemoveAll(entity => entity.Id == organizationId) > 0) { player.AssetRegistry.FrontIds.Remove(organizationId); return; } if (player.Reserves.RemoveAll(entity => entity.Id == organizationId) > 0) { player.AssetRegistry.ReserveIds.Remove(organizationId); return; } throw new InvalidOperationException($"Unknown organization '{organizationId}'."); } private static string ResolveOrganizationKind(PlayerFactionRuntime player, string organizationId) { if (player.Fleets.Any(entity => entity.Id == organizationId)) return "fleet"; if (player.TaskForces.Any(entity => entity.Id == organizationId)) return "task-force"; if (player.StationGroups.Any(entity => entity.Id == organizationId)) return "station-group"; if (player.EconomicRegions.Any(entity => entity.Id == organizationId)) return "economic-region"; if (player.Fronts.Any(entity => entity.Id == organizationId)) return "front"; if (player.Reserves.Any(entity => entity.Id == organizationId)) return "reserve"; throw new InvalidOperationException($"Unknown organization '{organizationId}'."); } private static IEnumerable ExistingOrganizationIds(PlayerFactionRuntime player) => player.Fleets.Select(entity => entity.Id) .Concat(player.TaskForces.Select(entity => entity.Id)) .Concat(player.StationGroups.Select(entity => entity.Id)) .Concat(player.EconomicRegions.Select(entity => entity.Id)) .Concat(player.Fronts.Select(entity => entity.Id)) .Concat(player.Reserves.Select(entity => entity.Id)); private static string NormalizeKind(string value) => value.Trim().ToLowerInvariant(); private static string CreateDomainId(string prefix, string label, IEnumerable existingIds) { var slug = new string(label .Trim() .ToLowerInvariant() .Select(character => char.IsLetterOrDigit(character) ? character : '-') .ToArray()) .Trim('-'); if (string.IsNullOrWhiteSpace(slug)) { slug = prefix; } var candidate = $"{prefix}-{slug}"; var known = existingIds.ToHashSet(StringComparer.Ordinal); if (!known.Contains(candidate)) { return candidate; } var suffix = 2; while (known.Contains($"{candidate}-{suffix}")) { suffix += 1; } return $"{candidate}-{suffix}"; } private static void UpdateStringList(List target, IEnumerable? requested, bool replace, IEnumerable allowedValues) { if (replace) { target.Clear(); } if (requested is null) { return; } var allowed = allowedValues.ToHashSet(StringComparer.Ordinal); foreach (var value in requested.Where(value => !string.IsNullOrWhiteSpace(value))) { if (allowed.Contains(value) && !target.Contains(value, StringComparer.Ordinal)) { target.Add(value); } } } private static void SyncSet(HashSet target, IEnumerable source) { target.Clear(); foreach (var value in source.Where(value => !string.IsNullOrWhiteSpace(value))) { target.Add(value); } } }