2526 lines
116 KiB
C#
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.PreferredAnchorId = request.PreferredAnchorId;
|
|
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,
|
|
AnchorId = template.AnchorId,
|
|
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,
|
|
AnchorId = template.AnchorId,
|
|
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,
|
|
AnchorId = request.AnchorId,
|
|
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.PreferredAnchorId = request.PreferredAnchorId;
|
|
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,
|
|
AnchorId = template.AnchorId,
|
|
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,
|
|
PreferredAnchorId = directive?.PreferredAnchorId ?? ship.DefaultBehavior.PreferredAnchorId,
|
|
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,
|
|
AnchorId = directive.PreferredAnchorId,
|
|
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.PreferredAnchorId = source.PreferredAnchorId;
|
|
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.PreferredAnchorId, right.PreferredAnchorId, 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.AnchorId, right.AnchorId, 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.AnchorId, right.AnchorId, 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,
|
|
AnchorId = template.AnchorId,
|
|
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);
|
|
}
|
|
}
|
|
}
|