Files
space-game/apps/backend/PlayerFaction/Simulation/PlayerFactionService.cs

2526 lines
116 KiB
C#

using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds;
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(IPlayerStateStore playerStateStore, string factionId) =>
playerStateStore.GetPlayerFactions().Any(player =>
string.Equals(player.SovereignFactionId, factionId, StringComparison.Ordinal));
internal PlayerFactionRuntime? TryGetDomain(IPlayerStateStore playerStateStore, string playerId)
{
return playerStateStore.TryGetPlayerFaction(playerId, out var player) ? player : null;
}
internal PlayerFactionRuntime EnsureDomain(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId)
{
var player = playerStateStore.GetOrAddPlayerFaction(playerId, () => new PlayerFactionRuntime
{
Id = PlayerFactionDomainId,
Label = "Pending Pilot",
SovereignFactionId = string.Empty,
RequiresOnboarding = true,
CreatedAtUtc = world.GeneratedAtUtc,
UpdatedAtUtc = world.GeneratedAtUtc,
});
EnsureBaseStructures(world, player);
SyncRegistry(world, player);
return player;
}
internal PlayerFactionRuntime CompleteOnboarding(
SimulationWorld world,
IPlayerStateStore playerStateStore,
string playerId,
CompletePlayerOnboardingRequest request)
{
var player = EnsureDomain(world, playerStateStore, playerId);
if (!player.RequiresOnboarding)
{
throw new InvalidOperationException("Player onboarding has already been completed.");
}
var personaName = request.Name.Trim();
if (personaName.Length < 2)
{
throw new InvalidOperationException("Player name must contain at least 2 characters.");
}
if (personaName.Length > 48)
{
throw new InvalidOperationException("Player name must contain at most 48 characters.");
}
var ownedFactionId = BuildOwnedFactionId(playerId);
if (world.Factions.Any(faction => string.Equals(faction.Id, ownedFactionId, StringComparison.Ordinal)))
{
throw new InvalidOperationException($"Player faction '{ownedFactionId}' already exists in the current world.");
}
player.Label = personaName;
player.PersonaName = personaName;
player.RaceId = request.RaceId.Trim();
player.SovereignFactionId = ownedFactionId;
player.RequiresOnboarding = false;
player.UpdatedAtUtc = DateTimeOffset.UtcNow;
return player;
}
internal PlayerFactionRuntime EnsureInitializedDomain(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId)
{
var player = EnsureDomain(world, playerStateStore, playerId);
if (player.RequiresOnboarding || string.IsNullOrWhiteSpace(player.SovereignFactionId))
{
throw new InvalidOperationException("Player onboarding must be completed before issuing gameplay commands.");
}
return player;
}
internal static string BuildOwnedFactionId(string playerId) =>
$"player-{playerId.Replace("-", string.Empty, StringComparison.Ordinal).ToLowerInvariant()}";
internal void Update(SimulationWorld world, IPlayerStateStore playerStateStore, float _deltaSeconds, ICollection<SimulationEventRecord> events)
{
if (playerStateStore.GetPlayerFactions().Count == 0)
{
return;
}
foreach (var player in playerStateStore.GetPlayerFactions())
{
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, IPlayerStateStore playerStateStore, string playerId, PlayerOrganizationCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, string organizationId)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, string organizationId, PlayerOrganizationMembershipCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, string? directiveId, PlayerDirectiveCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, string directiveId)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, string? policyId, PlayerPolicyCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, string? automationPolicyId, PlayerAutomationPolicyCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, string? productionProgramId, PlayerProductionProgramCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, string assetId, PlayerAssetAssignmentCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, PlayerStrategicIntentCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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, IPlayerStateStore playerStateStore, string playerId, string shipId, ShipOrderCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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,
SourceKind = ShipOrderSourceKind.Player,
SourceId = playerId,
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.Name}.", "ship", shipId);
player.UpdatedAtUtc = DateTimeOffset.UtcNow;
ship.ControlSourceKind = "player-order";
ship.ControlSourceId = ship.OrderQueue
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
.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, IPlayerStateStore playerStateStore, string playerId, string shipId, string orderId)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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.Name}.", "ship", shipId);
player.UpdatedAtUtc = DateTimeOffset.UtcNow;
}
ship.ControlSourceKind = ship.OrderQueue.Any(order => order.SourceKind == ShipOrderSourceKind.Player)
? "player-order"
: "player-manual";
ship.ControlSourceId = ship.OrderQueue
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
.OrderByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.Select(order => order.Id)
.FirstOrDefault();
ship.ControlReason = ship.OrderQueue
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
.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, IPlayerStateStore playerStateStore, string playerId, string shipId, ShipDefaultBehaviorCommandRequest request)
{
var player = EnsureInitializedDomain(world, playerStateStore, playerId);
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.Name}",
ScopeKind = "ship",
ScopeId = shipId,
Kind = "direct-control",
CreatedAtUtc = DateTimeOffset.UtcNow,
};
player.Directives.Add(directive);
}
directive.Label = $"Direct control {ship.Definition.Name}";
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.ItemId;
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.Name}.", "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)
{
if (string.IsNullOrWhiteSpace(player.SovereignFactionId))
{
SyncSet(player.AssetRegistry.ShipIds, []);
SyncSet(player.AssetRegistry.StationIds, []);
SyncSet(player.AssetRegistry.CommanderIds, []);
SyncSet(player.AssetRegistry.ClaimIds, []);
SyncSet(player.AssetRegistry.ConstructionSiteIds, []);
SyncSet(player.AssetRegistry.PolicySetIds, player.Policies.Where(entry => entry.PolicySetId is not null).Select(entry => entry.PolicySetId!));
SyncSet(player.AssetRegistry.MarketOrderIds, []);
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));
return;
}
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<SimulationEventRecord> 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.Name} 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<string, CommanderRuntime> EnsureFleetCommanders(SimulationWorld world, PlayerFactionRuntime player, CommanderRuntime factionCommander)
{
var map = new Dictionary<string, CommanderRuntime>(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<string, CommanderRuntime> EnsureTaskForceCommanders(
SimulationWorld world,
PlayerFactionRuntime player,
CommanderRuntime factionCommander,
IReadOnlyDictionary<string, CommanderRuntime> fleetCommanders)
{
var map = new Dictionary<string, CommanderRuntime>(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<string, CommanderRuntime> fleetCommanders,
IReadOnlyDictionary<string, CommanderRuntime> 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<string, PlayerAssignmentRuntime> 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);
}
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.SourceKind == ShipOrderSourceKind.Player)
? "player-order"
: "player-manual";
var desiredControlSourceId = directive?.Id
?? automation?.Id
?? ship.OrderQueue
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
.OrderByDescending(order => order.Priority)
.ThenBy(order => order.CreatedAtUtc)
.Select(order => order.Id)
.FirstOrDefault();
var desiredControlReason = directive?.Label
?? automation?.Label
?? ship.OrderQueue
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
.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,
ItemId = directive?.ItemId ?? automation?.PreferredItemId ?? ship.DefaultBehavior.ItemId,
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!,
SourceKind = ShipOrderSourceKind.Player,
SourceId = directive.Id,
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.ItemId = source.ItemId;
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.ItemId, right.ItemId, 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.SourceKind == right.SourceKind
&& string.Equals(left.SourceId, right.SourceId, StringComparison.Ordinal)
&& left.Priority == right.Priority
&& left.InterruptCurrentPlan == right.InterruptCurrentPlan
&& string.Equals(left.Label, right.Label, StringComparison.Ordinal)
&& string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal)
&& string.Equals(left.TargetSystemId, right.TargetSystemId, StringComparison.Ordinal)
&& 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<string, List<string>>(StringComparer.Ordinal);
var taskForceMemberships = new Dictionary<string, List<string>>(StringComparer.Ordinal);
var stationGroupMemberships = new Dictionary<string, List<string>>(StringComparer.Ordinal);
var reserveMemberships = new Dictionary<string, List<string>>(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(GetShipCategory(ship.Definition), 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<string>()
.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<string>()
.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<PlayerDirectiveRuntime> 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<string, List<string>> 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<string, List<string>> 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(GetShipCategory(ship.Definition), 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<string> 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<string> 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<string> target, IEnumerable<string>? requested, bool replace, IEnumerable<string> 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<string> target, IEnumerable<string> source)
{
target.Clear();
foreach (var value in source.Where(value => !string.IsNullOrWhiteSpace(value)))
{
target.Add(value);
}
}
}