Compare commits

...

2 Commits

Author SHA1 Message Date
766fef1c8f chore: add .editorconfig and consistent formatting for backend projects
Adds an `.editorconfig` file with C# and project-specific conventions. Applies consistent indentation and formatting across backend handlers, runtime models, and AI services.
2026-03-24 02:55:15 -04:00
cfee1306de chore: add project-specific IDE config and ignored files 2026-03-24 02:36:15 -04:00
66 changed files with 17821 additions and 17733 deletions

15
.idea/.idea.SpaceGame/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,15 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/contentModel.xml
/modules.xml
/projectSettingsUpdater.xml
/.idea.SpaceGame.iml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

1
.idea/.idea.SpaceGame/.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
SpaceGame

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

6
.idea/.idea.SpaceGame/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,54 @@
root = true
[*.{cs,csx}]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
# .NET/C# conventions
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_collection_expression = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
csharp_prefer_braces = true:suggestion
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = false:suggestion
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_prefer_switch_expression = true:suggestion
csharp_style_prefer_primary_constructors = false:silent
csharp_new_line_before_open_brace = all
[*.{csproj,props,targets,sln,slnx}]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.{json,jsonc}]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

View File

@@ -4,323 +4,323 @@ namespace SpaceGame.Api.Definitions;
public sealed class ConstructionDefinition public sealed class ConstructionDefinition
{ {
public string? RecipeId { get; set; } public string? RecipeId { get; set; }
public string FacilityCategory { get; set; } = "station"; public string FacilityCategory { get; set; } = "station";
public List<string> RequiredModules { get; set; } = []; public List<string> RequiredModules { get; set; } = [];
public List<RecipeInputDefinition> Requirements { get; set; } = []; public List<RecipeInputDefinition> Requirements { get; set; } = [];
public float CycleTime { get; set; } public float CycleTime { get; set; }
public float BatchSize { get; set; } = 1f; public float BatchSize { get; set; } = 1f;
public float ProductsPerHour { get; set; } public float ProductsPerHour { get; set; }
public float MaxEfficiency { get; set; } = 1f; public float MaxEfficiency { get; set; } = 1f;
public int Priority { get; set; } public int Priority { get; set; }
} }
public sealed class ItemPriceDefinition public sealed class ItemPriceDefinition
{ {
public float Min { get; set; } public float Min { get; set; }
public float Max { get; set; } public float Max { get; set; }
public float Avg { get; set; } public float Avg { get; set; }
} }
public sealed class ItemEffectDefinition public sealed class ItemEffectDefinition
{ {
public required string Type { get; set; } public required string Type { get; set; }
public float Product { get; set; } public float Product { get; set; }
} }
public sealed class ItemProductionDefinition public sealed class ItemProductionDefinition
{ {
public float Time { get; set; } public float Time { get; set; }
public float Amount { get; set; } public float Amount { get; set; }
public string Method { get; set; } = "default"; public string Method { get; set; } = "default";
public string Name { get; set; } = "Universal"; public string Name { get; set; } = "Universal";
public List<RecipeInputDefinition> Wares { get; set; } = []; public List<RecipeInputDefinition> Wares { get; set; } = [];
public List<ItemEffectDefinition> Effects { get; set; } = []; public List<ItemEffectDefinition> Effects { get; set; } = [];
} }
public sealed class BalanceDefinition public sealed class BalanceDefinition
{ {
public float SimulationSpeedMultiplier { get; set; } = 1f; public float SimulationSpeedMultiplier { get; set; } = 1f;
public float YPlane { get; set; } public float YPlane { get; set; }
public float ArrivalThreshold { get; set; } public float ArrivalThreshold { get; set; }
public float MiningRate { get; set; } public float MiningRate { get; set; }
public float MiningCycleSeconds { get; set; } public float MiningCycleSeconds { get; set; }
public float TransferRate { get; set; } public float TransferRate { get; set; }
public float DockingDuration { get; set; } public float DockingDuration { get; set; }
public float UndockingDuration { get; set; } public float UndockingDuration { get; set; }
public float UndockDistance { get; set; } public float UndockDistance { get; set; }
} }
public sealed class StarDefinition public sealed class StarDefinition
{ {
public string Kind { get; set; } = "main-sequence"; public string Kind { get; set; } = "main-sequence";
public required string Color { get; set; } public required string Color { get; set; }
public required string Glow { get; set; } public required string Glow { get; set; }
public float Size { get; set; } public float Size { get; set; }
public float OrbitRadius { get; set; } public float OrbitRadius { get; set; }
public float OrbitSpeed { get; set; } public float OrbitSpeed { get; set; }
public float OrbitPhaseAtEpoch { get; set; } public float OrbitPhaseAtEpoch { get; set; }
} }
public sealed class MoonDefinition public sealed class MoonDefinition
{ {
public required string Label { get; set; } public required string Label { get; set; }
public float Size { get; set; } public float Size { get; set; }
public required string Color { get; set; } public required string Color { get; set; }
public float OrbitRadius { get; set; } public float OrbitRadius { get; set; }
public float OrbitSpeed { get; set; } public float OrbitSpeed { get; set; }
public float OrbitPhaseAtEpoch { get; set; } public float OrbitPhaseAtEpoch { get; set; }
public float OrbitInclination { get; set; } public float OrbitInclination { get; set; }
public float OrbitLongitudeOfAscendingNode { get; set; } public float OrbitLongitudeOfAscendingNode { get; set; }
} }
public sealed class SolarSystemDefinition public sealed class SolarSystemDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Label { get; set; } public required string Label { get; set; }
public required float[] Position { get; set; } public required float[] Position { get; set; }
public required List<StarDefinition> Stars { get; set; } public required List<StarDefinition> Stars { get; set; }
public required AsteroidFieldDefinition AsteroidField { get; set; } public required AsteroidFieldDefinition AsteroidField { get; set; }
public required List<ResourceNodeDefinition> ResourceNodes { get; set; } public required List<ResourceNodeDefinition> ResourceNodes { get; set; }
public required List<PlanetDefinition> Planets { get; set; } public required List<PlanetDefinition> Planets { get; set; }
} }
public sealed class AsteroidFieldDefinition public sealed class AsteroidFieldDefinition
{ {
public int DecorationCount { get; set; } public int DecorationCount { get; set; }
public float RadiusOffset { get; set; } public float RadiusOffset { get; set; }
public float RadiusVariance { get; set; } public float RadiusVariance { get; set; }
public float HeightVariance { get; set; } public float HeightVariance { get; set; }
} }
public sealed class ResourceNodeDefinition public sealed class ResourceNodeDefinition
{ {
public string SourceKind { get; set; } = "local-space"; public string SourceKind { get; set; } = "local-space";
public string? AnchorReference { get; set; } public string? AnchorReference { get; set; }
public float Angle { get; set; } public float Angle { get; set; }
public float RadiusOffset { get; set; } public float RadiusOffset { get; set; }
public float InclinationDegrees { get; set; } public float InclinationDegrees { get; set; }
public int? AnchorPlanetIndex { get; set; } public int? AnchorPlanetIndex { get; set; }
public int? AnchorMoonIndex { get; set; } public int? AnchorMoonIndex { get; set; }
public float OreAmount { get; set; } public float OreAmount { get; set; }
public required string ItemId { get; set; } public required string ItemId { get; set; }
public int ShardCount { get; set; } public int ShardCount { get; set; }
} }
public sealed class ItemDefinition public sealed class ItemDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public string Type { get; set; } = "material"; public string Type { get; set; } = "material";
public string CargoKind { get; set; } = string.Empty; public string CargoKind { get; set; } = string.Empty;
public float Volume { get; set; } = 1f; public float Volume { get; set; } = 1f;
public int Version { get; set; } public int Version { get; set; }
public string FactoryName { get; set; } = string.Empty; public string FactoryName { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty; public string Icon { get; set; } = string.Empty;
public string Group { get; set; } = string.Empty; public string Group { get; set; } = string.Empty;
public ItemPriceDefinition? Price { get; set; } public ItemPriceDefinition? Price { get; set; }
public List<string> Illegal { get; set; } = []; public List<string> Illegal { get; set; } = [];
public List<ItemProductionDefinition> Production { get; set; } = []; public List<ItemProductionDefinition> Production { get; set; } = [];
public ConstructionDefinition? Construction { get; set; } public ConstructionDefinition? Construction { get; set; }
[JsonPropertyName("transport")] [JsonPropertyName("transport")]
public string Transport public string Transport
{ {
set => CargoKind = value; set => CargoKind = value;
} }
} }
public sealed class RecipeOutputDefinition public sealed class RecipeOutputDefinition
{ {
public required string ItemId { get; set; } public required string ItemId { get; set; }
public float Amount { get; set; } public float Amount { get; set; }
} }
public sealed class RecipeInputDefinition public sealed class RecipeInputDefinition
{ {
public string ItemId { get; set; } = string.Empty; public string ItemId { get; set; } = string.Empty;
public float Amount { get; set; } public float Amount { get; set; }
[JsonPropertyName("ware")] [JsonPropertyName("ware")]
public string Ware public string Ware
{ {
set => ItemId = value; set => ItemId = value;
} }
} }
public sealed class ModuleConstructionDefinition public sealed class ModuleConstructionDefinition
{ {
public required List<RecipeInputDefinition> Requirements { get; set; } public required List<RecipeInputDefinition> Requirements { get; set; }
public float ProductionTime { get; set; } public float ProductionTime { get; set; }
} }
public sealed class ModuleDockDefinition public sealed class ModuleDockDefinition
{ {
public int Capacity { get; set; } public int Capacity { get; set; }
public required string Size { get; set; } public required string Size { get; set; }
} }
public sealed class ModuleCargoDefinition public sealed class ModuleCargoDefinition
{ {
public float Max { get; set; } public float Max { get; set; }
public required string Type { get; set; } public required string Type { get; set; }
} }
public sealed class ModuleWorkForceDefinition public sealed class ModuleWorkForceDefinition
{ {
public float Capacity { get; set; } public float Capacity { get; set; }
public float Max { get; set; } public float Max { get; set; }
public string Race { get; set; } = string.Empty; public string Race { get; set; } = string.Empty;
} }
public sealed class ModuleMountDefinition public sealed class ModuleMountDefinition
{ {
public required string Group { get; set; } public required string Group { get; set; }
public required string Size { get; set; } public required string Size { get; set; }
public bool Hittable { get; set; } public bool Hittable { get; set; }
public List<string> Types { get; set; } = []; public List<string> Types { get; set; } = [];
} }
public sealed class ModuleProductionDefinition public sealed class ModuleProductionDefinition
{ {
public float Time { get; set; } public float Time { get; set; }
public float Amount { get; set; } public float Amount { get; set; }
public string Method { get; set; } = "default"; public string Method { get; set; } = "default";
public string Name { get; set; } = "Universal"; public string Name { get; set; } = "Universal";
public List<RecipeInputDefinition> Wares { get; set; } = []; public List<RecipeInputDefinition> Wares { get; set; } = [];
} }
public sealed class ModuleDefinition public sealed class ModuleDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public required string Type { get; set; } public required string Type { get; set; }
[JsonIgnore] [JsonIgnore]
public string? Product { get; set; } public string? Product { get; set; }
public List<string> Products { get; set; } = []; public List<string> Products { get; set; } = [];
public string ProductionMode { get; set; } = "passive"; public string ProductionMode { get; set; } = "passive";
public float Radius { get; set; } = 12f; public float Radius { get; set; } = 12f;
public float Hull { get; set; } = 100f; public float Hull { get; set; } = 100f;
public float WorkforceNeeded { get; set; } public float WorkforceNeeded { get; set; }
public int Version { get; set; } public int Version { get; set; }
public string Macro { get; set; } = string.Empty; public string Macro { get; set; } = string.Empty;
public string MakerRace { get; set; } = string.Empty; public string MakerRace { get; set; } = string.Empty;
public int ExplosionDamage { get; set; } public int ExplosionDamage { get; set; }
public ItemPriceDefinition? Price { get; set; } public ItemPriceDefinition? Price { get; set; }
public List<string> Owners { get; set; } = []; public List<string> Owners { get; set; } = [];
public ModuleCargoDefinition? Cargo { get; set; } public ModuleCargoDefinition? Cargo { get; set; }
public ModuleWorkForceDefinition? WorkForce { get; set; } public ModuleWorkForceDefinition? WorkForce { get; set; }
public List<ModuleDockDefinition> Docks { get; set; } = []; public List<ModuleDockDefinition> Docks { get; set; } = [];
public List<ModuleMountDefinition> Shields { get; set; } = []; public List<ModuleMountDefinition> Shields { get; set; } = [];
public List<ModuleMountDefinition> Turrets { get; set; } = []; public List<ModuleMountDefinition> Turrets { get; set; } = [];
public List<ModuleProductionDefinition> Production { get; set; } = []; public List<ModuleProductionDefinition> Production { get; set; } = [];
public ModuleConstructionDefinition? Construction { get; set; } public ModuleConstructionDefinition? Construction { get; set; }
[JsonPropertyName("product")] [JsonPropertyName("product")]
public List<string> ProductIds public List<string> ProductIds
{ {
get => Products; get => Products;
set => Products = value ?? []; set => Products = value ?? [];
} }
} }
public sealed class ModuleRecipeDefinition public sealed class ModuleRecipeDefinition
{ {
public required string ModuleId { get; set; } public required string ModuleId { get; set; }
public float Duration { get; set; } public float Duration { get; set; }
public required List<RecipeInputDefinition> Inputs { get; set; } public required List<RecipeInputDefinition> Inputs { get; set; }
} }
public sealed class RecipeDefinition public sealed class RecipeDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Label { get; set; } public required string Label { get; set; }
public required string FacilityCategory { get; set; } public required string FacilityCategory { get; set; }
public float Duration { get; set; } public float Duration { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public List<string> RequiredModules { get; set; } = []; public List<string> RequiredModules { get; set; } = [];
public List<RecipeInputDefinition> Inputs { get; set; } = []; public List<RecipeInputDefinition> Inputs { get; set; } = [];
public List<RecipeOutputDefinition> Outputs { get; set; } = []; public List<RecipeOutputDefinition> Outputs { get; set; } = [];
public string? ShipOutputId { get; set; } public string? ShipOutputId { get; set; }
} }
public sealed class PlanetDefinition public sealed class PlanetDefinition
{ {
public required string Label { get; set; } public required string Label { get; set; }
public string PlanetType { get; set; } = "terrestrial"; public string PlanetType { get; set; } = "terrestrial";
public string Shape { get; set; } = "sphere"; public string Shape { get; set; } = "sphere";
public List<MoonDefinition> Moons { get; set; } = []; public List<MoonDefinition> Moons { get; set; } = [];
public float OrbitRadius { get; set; } public float OrbitRadius { get; set; }
public float OrbitSpeed { get; set; } public float OrbitSpeed { get; set; }
public float OrbitEccentricity { get; set; } public float OrbitEccentricity { get; set; }
public float OrbitInclination { get; set; } public float OrbitInclination { get; set; }
public float OrbitLongitudeOfAscendingNode { get; set; } public float OrbitLongitudeOfAscendingNode { get; set; }
public float OrbitArgumentOfPeriapsis { get; set; } public float OrbitArgumentOfPeriapsis { get; set; }
public float OrbitPhaseAtEpoch { get; set; } public float OrbitPhaseAtEpoch { get; set; }
public float Size { get; set; } public float Size { get; set; }
public required string Color { get; set; } public required string Color { get; set; }
public float Tilt { get; set; } public float Tilt { get; set; }
public bool HasRing { get; set; } public bool HasRing { get; set; }
} }
public sealed class ShipDefinition public sealed class ShipDefinition
{ {
public required string Id { get; set; } public required string Id { get; set; }
public required string Label { get; set; } public required string Label { get; set; }
public required string Kind { get; set; } public required string Kind { get; set; }
public required string Class { get; set; } public required string Class { get; set; }
public float Speed { get; set; } public float Speed { get; set; }
public float WarpSpeed { get; set; } public float WarpSpeed { get; set; }
public float FtlSpeed { get; set; } public float FtlSpeed { get; set; }
public float SpoolTime { get; set; } public float SpoolTime { get; set; }
public float CargoCapacity { get; set; } public float CargoCapacity { get; set; }
public string? CargoKind { get; set; } public string? CargoKind { get; set; }
public required string Color { get; set; } public required string Color { get; set; }
public required string HullColor { get; set; } public required string HullColor { get; set; }
public float Size { get; set; } public float Size { get; set; }
public float MaxHealth { get; set; } public float MaxHealth { get; set; }
public List<string> Capabilities { get; set; } = []; public List<string> Capabilities { get; set; } = [];
public ConstructionDefinition? Construction { get; set; } public ConstructionDefinition? Construction { get; set; }
} }
public sealed class ScenarioDefinition public sealed class ScenarioDefinition
{ {
public required List<InitialStationDefinition> InitialStations { get; set; } public required List<InitialStationDefinition> InitialStations { get; set; }
public required List<ShipFormationDefinition> ShipFormations { get; set; } public required List<ShipFormationDefinition> ShipFormations { get; set; }
public required List<PatrolRouteDefinition> PatrolRoutes { get; set; } public required List<PatrolRouteDefinition> PatrolRoutes { get; set; }
public required MiningDefaultsDefinition MiningDefaults { get; set; } public required MiningDefaultsDefinition MiningDefaults { get; set; }
} }
public sealed class InitialStationDefinition public sealed class InitialStationDefinition
{ {
public required string SystemId { get; set; } public required string SystemId { get; set; }
public string Label { get; set; } = "Orbital Station"; public string Label { get; set; } = "Orbital Station";
public string Color { get; set; } = "#8df0d2"; public string Color { get; set; } = "#8df0d2";
public string Objective { get; set; } = "general"; public string Objective { get; set; } = "general";
public List<string> StartingModules { get; set; } = []; public List<string> StartingModules { get; set; } = [];
public string? FactionId { get; set; } public string? FactionId { get; set; }
public int? PlanetIndex { get; set; } public int? PlanetIndex { get; set; }
public int? LagrangeSide { get; set; } public int? LagrangeSide { get; set; }
public float[]? Position { get; set; } public float[]? Position { get; set; }
} }
public sealed class ShipFormationDefinition public sealed class ShipFormationDefinition
{ {
public required string ShipId { get; set; } public required string ShipId { get; set; }
public int Count { get; set; } public int Count { get; set; }
public required float[] Center { get; set; } public required float[] Center { get; set; }
public required string SystemId { get; set; } public required string SystemId { get; set; }
public string? FactionId { get; set; } public string? FactionId { get; set; }
public Dictionary<string, float> StartingInventory { get; set; } = new(StringComparer.Ordinal); public Dictionary<string, float> StartingInventory { get; set; } = new(StringComparer.Ordinal);
} }
public sealed class PatrolRouteDefinition public sealed class PatrolRouteDefinition
{ {
public required string SystemId { get; set; } public required string SystemId { get; set; }
public required List<float[]> Points { get; set; } public required List<float[]> Points { get; set; }
} }
public sealed class MiningDefaultsDefinition public sealed class MiningDefaultsDefinition
{ {
public required string NodeSystemId { get; set; } public required string NodeSystemId { get; set; }
public required string RefinerySystemId { get; set; } public required string RefinerySystemId { get; set; }
} }

View File

@@ -2,33 +2,33 @@ namespace SpaceGame.Api.Economy.Runtime;
public sealed class MarketOrderRuntime public sealed class MarketOrderRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string FactionId { get; init; } public required string FactionId { get; init; }
public string? StationId { get; init; } public string? StationId { get; init; }
public string? ConstructionSiteId { get; init; } public string? ConstructionSiteId { get; init; }
public required string Kind { get; init; } public required string Kind { get; init; }
public required string ItemId { get; init; } public required string ItemId { get; init; }
public float Amount { get; init; } public float Amount { get; init; }
public float RemainingAmount { get; set; } public float RemainingAmount { get; set; }
public float Valuation { get; set; } public float Valuation { get; set; }
public float? ReserveThreshold { get; set; } public float? ReserveThreshold { get; set; }
public string? PolicySetId { get; set; } public string? PolicySetId { get; set; }
public string State { get; set; } = MarketOrderStateKinds.Open; public string State { get; set; } = MarketOrderStateKinds.Open;
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class PolicySetRuntime public sealed class PolicySetRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string OwnerKind { get; init; } public required string OwnerKind { get; init; }
public required string OwnerId { get; init; } public required string OwnerId { get; init; }
public string TradeAccessPolicy { get; set; } = "owner-and-allies"; public string TradeAccessPolicy { get; set; } = "owner-and-allies";
public string DockingAccessPolicy { get; set; } = "owner-and-allies"; public string DockingAccessPolicy { get; set; } = "owner-and-allies";
public string ConstructionAccessPolicy { get; set; } = "owner-only"; public string ConstructionAccessPolicy { get; set; } = "owner-only";
public string OperationalRangePolicy { get; set; } = "unrestricted"; public string OperationalRangePolicy { get; set; } = "unrestricted";
public string CombatEngagementPolicy { get; set; } = "defensive"; public string CombatEngagementPolicy { get; set; } = "defensive";
public bool AvoidHostileSystems { get; set; } = true; public bool AvoidHostileSystems { get; set; } = true;
public float FleeHullRatio { get; set; } = 0.35f; public float FleeHullRatio { get; set; } = 0.35f;
public HashSet<string> BlacklistedSystemIds { get; } = new(StringComparer.Ordinal); public HashSet<string> BlacklistedSystemIds { get; } = new(StringComparer.Ordinal);
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,347 +2,347 @@ namespace SpaceGame.Api.Factions.Runtime;
public sealed class FactionRuntime public sealed class FactionRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; init; } public required string Label { get; init; }
public required string Color { get; init; } public required string Color { get; init; }
public float Credits { get; set; } public float Credits { get; set; }
public float PopulationTotal { get; set; } public float PopulationTotal { get; set; }
public float OreMined { get; set; } public float OreMined { get; set; }
public float GoodsProduced { get; set; } public float GoodsProduced { get; set; }
public int ShipsBuilt { get; set; } public int ShipsBuilt { get; set; }
public int ShipsLost { get; set; } public int ShipsLost { get; set; }
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal); public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
public string? DefaultPolicySetId { get; set; } public string? DefaultPolicySetId { get; set; }
public FactionDoctrineRuntime Doctrine { get; set; } = new(); public FactionDoctrineRuntime Doctrine { get; set; } = new();
public FactionMemoryRuntime Memory { get; set; } = new(); public FactionMemoryRuntime Memory { get; set; } = new();
public FactionStrategicStateRuntime StrategicState { get; set; } = new(); public FactionStrategicStateRuntime StrategicState { get; set; } = new();
public List<FactionDecisionLogEntryRuntime> DecisionLog { get; } = []; public List<FactionDecisionLogEntryRuntime> DecisionLog { get; } = [];
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class CommanderRuntime public sealed class CommanderRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public required string FactionId { get; init; } public required string FactionId { get; init; }
public string? ParentCommanderId { get; set; } public string? ParentCommanderId { get; set; }
public string? ControlledEntityId { get; set; } public string? ControlledEntityId { get; set; }
public string? PolicySetId { get; set; } public string? PolicySetId { get; set; }
public string? Doctrine { get; set; } public string? Doctrine { get; set; }
public float ReplanTimer { get; set; } public float ReplanTimer { get; set; }
public bool NeedsReplan { get; set; } = true; public bool NeedsReplan { get; set; } = true;
public CommanderAssignmentRuntime? Assignment { get; set; } public CommanderAssignmentRuntime? Assignment { get; set; }
public CommanderSkillProfileRuntime Skills { get; set; } = new(); public CommanderSkillProfileRuntime Skills { get; set; } = new();
public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal); public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ActiveObjectiveIds { get; } = new(StringComparer.Ordinal); public HashSet<string> ActiveObjectiveIds { get; } = new(StringComparer.Ordinal);
public bool IsAlive { get; set; } = true; public bool IsAlive { get; set; } = true;
public int PlanningCycle { get; set; } public int PlanningCycle { get; set; }
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class CommanderAssignmentRuntime public sealed class CommanderAssignmentRuntime
{ {
public required string ObjectiveId { get; set; } public required string ObjectiveId { get; set; }
public string? CampaignId { get; set; } public string? CampaignId { get; set; }
public string? TheaterId { get; set; } public string? TheaterId { get; set; }
public required string Kind { get; set; } public required string Kind { get; set; }
public required string BehaviorKind { get; set; } public required string BehaviorKind { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public float Priority { get; set; } public float Priority { get; set; }
public string? HomeSystemId { get; set; } public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; } public string? HomeStationId { get; set; }
public string? TargetSystemId { get; set; } public string? TargetSystemId { get; set; }
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public Vector3? TargetPosition { get; set; } public Vector3? TargetPosition { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? Notes { get; set; } public string? Notes { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class CommanderSkillProfileRuntime public sealed class CommanderSkillProfileRuntime
{ {
public int Leadership { get; set; } = 3; public int Leadership { get; set; } = 3;
public int Coordination { get; set; } = 3; public int Coordination { get; set; } = 3;
public int Strategy { get; set; } = 3; public int Strategy { get; set; } = 3;
} }
public sealed class FactionDoctrineRuntime public sealed class FactionDoctrineRuntime
{ {
public string StrategicPosture { get; set; } = "balanced"; public string StrategicPosture { get; set; } = "balanced";
public string ExpansionPosture { get; set; } = "measured"; public string ExpansionPosture { get; set; } = "measured";
public string MilitaryPosture { get; set; } = "defensive"; public string MilitaryPosture { get; set; } = "defensive";
public string EconomicPosture { get; set; } = "self-sufficient"; public string EconomicPosture { get; set; } = "self-sufficient";
public int DesiredControlledSystems { get; set; } = 3; public int DesiredControlledSystems { get; set; } = 3;
public int DesiredMilitaryPerFront { get; set; } = 2; public int DesiredMilitaryPerFront { get; set; } = 2;
public int DesiredMinersPerSystem { get; set; } = 1; public int DesiredMinersPerSystem { get; set; } = 1;
public int DesiredTransportsPerSystem { get; set; } = 1; public int DesiredTransportsPerSystem { get; set; } = 1;
public int DesiredConstructors { get; set; } = 1; public int DesiredConstructors { get; set; } = 1;
public float ReserveCreditsRatio { get; set; } = 0.2f; public float ReserveCreditsRatio { get; set; } = 0.2f;
public float ExpansionBudgetRatio { get; set; } = 0.25f; public float ExpansionBudgetRatio { get; set; } = 0.25f;
public float WarBudgetRatio { get; set; } = 0.35f; public float WarBudgetRatio { get; set; } = 0.35f;
public float ReserveMilitaryRatio { get; set; } = 0.2f; public float ReserveMilitaryRatio { get; set; } = 0.2f;
public float OffensiveReadinessThreshold { get; set; } = 0.62f; public float OffensiveReadinessThreshold { get; set; } = 0.62f;
public float SupplySecurityBias { get; set; } = 0.55f; public float SupplySecurityBias { get; set; } = 0.55f;
public float FailureAversion { get; set; } = 0.45f; public float FailureAversion { get; set; } = 0.45f;
public int ReinforcementLeadPerFront { get; set; } = 1; public int ReinforcementLeadPerFront { get; set; } = 1;
} }
public sealed class FactionMemoryRuntime public sealed class FactionMemoryRuntime
{ {
public int LastPlanCycle { get; set; } public int LastPlanCycle { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } public DateTimeOffset UpdatedAtUtc { get; set; }
public int LastObservedShipsBuilt { get; set; } public int LastObservedShipsBuilt { get; set; }
public int LastObservedShipsLost { get; set; } public int LastObservedShipsLost { get; set; }
public float LastObservedCredits { get; set; } public float LastObservedCredits { get; set; }
public HashSet<string> KnownSystemIds { get; } = new(StringComparer.Ordinal); public HashSet<string> KnownSystemIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> KnownEnemyFactionIds { get; } = new(StringComparer.Ordinal); public HashSet<string> KnownEnemyFactionIds { get; } = new(StringComparer.Ordinal);
public List<FactionSystemMemoryRuntime> SystemMemories { get; } = []; public List<FactionSystemMemoryRuntime> SystemMemories { get; } = [];
public List<FactionCommodityMemoryRuntime> CommodityMemories { get; } = []; public List<FactionCommodityMemoryRuntime> CommodityMemories { get; } = [];
public List<FactionOutcomeRecordRuntime> RecentOutcomes { get; } = []; public List<FactionOutcomeRecordRuntime> RecentOutcomes { get; } = [];
} }
public sealed class FactionSystemMemoryRuntime public sealed class FactionSystemMemoryRuntime
{ {
public required string SystemId { get; init; } public required string SystemId { get; init; }
public DateTimeOffset LastSeenAtUtc { get; set; } public DateTimeOffset LastSeenAtUtc { get; set; }
public int LastEnemyShipCount { get; set; } public int LastEnemyShipCount { get; set; }
public int LastEnemyStationCount { get; set; } public int LastEnemyStationCount { get; set; }
public bool ControlledByFaction { get; set; } public bool ControlledByFaction { get; set; }
public string? LastRole { get; set; } public string? LastRole { get; set; }
public float FrontierPressure { get; set; } public float FrontierPressure { get; set; }
public float RouteRisk { get; set; } public float RouteRisk { get; set; }
public float HistoricalShortagePressure { get; set; } public float HistoricalShortagePressure { get; set; }
public int OffensiveFailures { get; set; } public int OffensiveFailures { get; set; }
public int DefensiveFailures { get; set; } public int DefensiveFailures { get; set; }
public int OffensiveSuccesses { get; set; } public int OffensiveSuccesses { get; set; }
public int DefensiveSuccesses { get; set; } public int DefensiveSuccesses { get; set; }
public DateTimeOffset? LastContestedAtUtc { get; set; } public DateTimeOffset? LastContestedAtUtc { get; set; }
public DateTimeOffset? LastShortageAtUtc { get; set; } public DateTimeOffset? LastShortageAtUtc { get; set; }
} }
public sealed class FactionCommodityMemoryRuntime public sealed class FactionCommodityMemoryRuntime
{ {
public required string ItemId { get; init; } public required string ItemId { get; init; }
public float HistoricalShortageScore { get; set; } public float HistoricalShortageScore { get; set; }
public float HistoricalSurplusScore { get; set; } public float HistoricalSurplusScore { get; set; }
public float LastObservedBacklog { get; set; } public float LastObservedBacklog { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } public DateTimeOffset UpdatedAtUtc { get; set; }
public DateTimeOffset? LastCriticalAtUtc { get; set; } public DateTimeOffset? LastCriticalAtUtc { get; set; }
} }
public sealed class FactionOutcomeRecordRuntime public sealed class FactionOutcomeRecordRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public required string Summary { get; set; } public required string Summary { get; set; }
public string? RelatedCampaignId { get; set; } public string? RelatedCampaignId { get; set; }
public string? RelatedObjectiveId { get; set; } public string? RelatedObjectiveId { get; set; }
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class FactionStrategicStateRuntime public sealed class FactionStrategicStateRuntime
{ {
public int PlanCycle { get; set; } public int PlanCycle { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } public DateTimeOffset UpdatedAtUtc { get; set; }
public string Status { get; set; } = "stable"; public string Status { get; set; } = "stable";
public FactionBudgetRuntime Budget { get; set; } = new(); public FactionBudgetRuntime Budget { get; set; } = new();
public FactionEconomicAssessmentRuntime EconomicAssessment { get; set; } = new(); public FactionEconomicAssessmentRuntime EconomicAssessment { get; set; } = new();
public FactionThreatAssessmentRuntime ThreatAssessment { get; set; } = new(); public FactionThreatAssessmentRuntime ThreatAssessment { get; set; } = new();
public List<FactionTheaterRuntime> Theaters { get; } = []; public List<FactionTheaterRuntime> Theaters { get; } = [];
public List<FactionCampaignRuntime> Campaigns { get; } = []; public List<FactionCampaignRuntime> Campaigns { get; } = [];
public List<FactionOperationalObjectiveRuntime> Objectives { get; } = []; public List<FactionOperationalObjectiveRuntime> Objectives { get; } = [];
public List<FactionAssetReservationRuntime> Reservations { get; } = []; public List<FactionAssetReservationRuntime> Reservations { get; } = [];
public List<FactionProductionProgramRuntime> ProductionPrograms { get; } = []; public List<FactionProductionProgramRuntime> ProductionPrograms { get; } = [];
} }
public sealed class FactionBudgetRuntime public sealed class FactionBudgetRuntime
{ {
public float ReservedCredits { get; set; } public float ReservedCredits { get; set; }
public float ExpansionCredits { get; set; } public float ExpansionCredits { get; set; }
public float WarCredits { get; set; } public float WarCredits { get; set; }
public int ReservedMilitaryAssets { get; set; } public int ReservedMilitaryAssets { get; set; }
public int ReservedLogisticsAssets { get; set; } public int ReservedLogisticsAssets { get; set; }
public int ReservedConstructionAssets { get; set; } public int ReservedConstructionAssets { get; set; }
} }
public sealed class FactionEconomicAssessmentRuntime public sealed class FactionEconomicAssessmentRuntime
{ {
public int PlanCycle { get; set; } public int PlanCycle { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } public DateTimeOffset UpdatedAtUtc { get; set; }
public int MilitaryShipCount { get; set; } public int MilitaryShipCount { get; set; }
public int MinerShipCount { get; set; } public int MinerShipCount { get; set; }
public int TransportShipCount { get; set; } public int TransportShipCount { get; set; }
public int ConstructorShipCount { get; set; } public int ConstructorShipCount { get; set; }
public int ControlledSystemCount { get; set; } public int ControlledSystemCount { get; set; }
public int TargetMilitaryShipCount { get; set; } public int TargetMilitaryShipCount { get; set; }
public int TargetMinerShipCount { get; set; } public int TargetMinerShipCount { get; set; }
public int TargetTransportShipCount { get; set; } public int TargetTransportShipCount { get; set; }
public int TargetConstructorShipCount { get; set; } public int TargetConstructorShipCount { get; set; }
public bool HasShipyard { get; set; } public bool HasShipyard { get; set; }
public bool HasWarIndustrySupplyChain { get; set; } public bool HasWarIndustrySupplyChain { get; set; }
public string? PrimaryExpansionSiteId { get; set; } public string? PrimaryExpansionSiteId { get; set; }
public string? PrimaryExpansionSystemId { get; set; } public string? PrimaryExpansionSystemId { get; set; }
public float ReplacementPressure { get; set; } public float ReplacementPressure { get; set; }
public float SustainmentScore { get; set; } public float SustainmentScore { get; set; }
public float LogisticsSecurityScore { get; set; } public float LogisticsSecurityScore { get; set; }
public int CriticalShortageCount { get; set; } public int CriticalShortageCount { get; set; }
public string? IndustrialBottleneckItemId { get; set; } public string? IndustrialBottleneckItemId { get; set; }
public List<FactionCommoditySignalRuntime> CommoditySignals { get; } = []; public List<FactionCommoditySignalRuntime> CommoditySignals { get; } = [];
} }
public sealed class FactionThreatAssessmentRuntime public sealed class FactionThreatAssessmentRuntime
{ {
public int PlanCycle { get; set; } public int PlanCycle { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } public DateTimeOffset UpdatedAtUtc { get; set; }
public int EnemyFactionCount { get; set; } public int EnemyFactionCount { get; set; }
public int EnemyShipCount { get; set; } public int EnemyShipCount { get; set; }
public int EnemyStationCount { get; set; } public int EnemyStationCount { get; set; }
public string? PrimaryThreatFactionId { get; set; } public string? PrimaryThreatFactionId { get; set; }
public string? PrimaryThreatSystemId { get; set; } public string? PrimaryThreatSystemId { get; set; }
public List<FactionThreatSignalRuntime> ThreatSignals { get; } = []; public List<FactionThreatSignalRuntime> ThreatSignals { get; } = [];
} }
public sealed class FactionTheaterRuntime public sealed class FactionTheaterRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public required string SystemId { get; set; } public required string SystemId { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public float Priority { get; set; } public float Priority { get; set; }
public float SupplyRisk { get; set; } public float SupplyRisk { get; set; }
public float FriendlyAssetValue { get; set; } public float FriendlyAssetValue { get; set; }
public string? TargetFactionId { get; set; } public string? TargetFactionId { get; set; }
public string? AnchorEntityId { get; set; } public string? AnchorEntityId { get; set; }
public Vector3? AnchorPosition { get; set; } public Vector3? AnchorPosition { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> CampaignIds { get; } = []; public List<string> CampaignIds { get; } = [];
} }
public sealed class FactionCampaignRuntime public sealed class FactionCampaignRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public string Status { get; set; } = "planned"; public string Status { get; set; } = "planned";
public float Priority { get; set; } public float Priority { get; set; }
public string? TheaterId { get; set; } public string? TheaterId { get; set; }
public string? TargetFactionId { get; set; } public string? TargetFactionId { get; set; }
public string? TargetSystemId { get; set; } public string? TargetSystemId { get; set; }
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public string? CommodityId { get; set; } public string? CommodityId { get; set; }
public string? SupportStationId { get; set; } public string? SupportStationId { get; set; }
public int CurrentStepIndex { get; set; } public int CurrentStepIndex { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public string? Summary { get; set; } public string? Summary { get; set; }
public string? PauseReason { get; set; } public string? PauseReason { get; set; }
public float ContinuationScore { get; set; } public float ContinuationScore { get; set; }
public float SupplyAdequacy { get; set; } public float SupplyAdequacy { get; set; }
public float ReplacementPressure { get; set; } public float ReplacementPressure { get; set; }
public int FailureCount { get; set; } public int FailureCount { get; set; }
public int SuccessCount { get; set; } public int SuccessCount { get; set; }
public string? FleetCommanderId { get; set; } public string? FleetCommanderId { get; set; }
public bool RequiresReinforcement { get; set; } public bool RequiresReinforcement { get; set; }
public List<FactionPlanStepRuntime> Steps { get; } = []; public List<FactionPlanStepRuntime> Steps { get; } = [];
public List<string> ObjectiveIds { get; } = []; public List<string> ObjectiveIds { get; } = [];
} }
public sealed class FactionOperationalObjectiveRuntime public sealed class FactionOperationalObjectiveRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string CampaignId { get; set; } public required string CampaignId { get; set; }
public string? TheaterId { get; set; } public string? TheaterId { get; set; }
public required string Kind { get; set; } public required string Kind { get; set; }
public required string DelegationKind { get; set; } public required string DelegationKind { get; set; }
public required string BehaviorKind { get; set; } public required string BehaviorKind { get; set; }
public string Status { get; set; } = "planned"; public string Status { get; set; } = "planned";
public float Priority { get; set; } public float Priority { get; set; }
public string? CommanderId { get; set; } public string? CommanderId { get; set; }
public string? HomeSystemId { get; set; } public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; } public string? HomeStationId { get; set; }
public string? TargetSystemId { get; set; } public string? TargetSystemId { get; set; }
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public Vector3? TargetPosition { get; set; } public Vector3? TargetPosition { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? Notes { get; set; } public string? Notes { get; set; }
public int CurrentStepIndex { get; set; } public int CurrentStepIndex { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public bool UseOrders { get; set; } public bool UseOrders { get; set; }
public string? StagingOrderKind { get; set; } public string? StagingOrderKind { get; set; }
public int ReinforcementLevel { get; set; } public int ReinforcementLevel { get; set; }
public List<FactionPlanStepRuntime> Steps { get; } = []; public List<FactionPlanStepRuntime> Steps { get; } = [];
public List<string> ReservedAssetIds { get; } = []; public List<string> ReservedAssetIds { get; } = [];
} }
public sealed class FactionPlanStepRuntime public sealed class FactionPlanStepRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public string Status { get; set; } = "planned"; public string Status { get; set; } = "planned";
public string? Summary { get; set; } public string? Summary { get; set; }
public string? BlockingReason { get; set; } public string? BlockingReason { get; set; }
} }
public sealed class FactionAssetReservationRuntime public sealed class FactionAssetReservationRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string ObjectiveId { get; set; } public required string ObjectiveId { get; set; }
public string? CampaignId { get; set; } public string? CampaignId { get; set; }
public required string AssetKind { get; set; } public required string AssetKind { get; set; }
public required string AssetId { get; set; } public required string AssetId { get; set; }
public float Priority { get; set; } public float Priority { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class FactionProductionProgramRuntime public sealed class FactionProductionProgramRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public string Status { get; set; } = "planned"; public string Status { get; set; } = "planned";
public float Priority { get; set; } public float Priority { get; set; }
public string? CampaignId { get; set; } public string? CampaignId { get; set; }
public string? CommodityId { get; set; } public string? CommodityId { get; set; }
public string? ModuleId { get; set; } public string? ModuleId { get; set; }
public string? ShipKind { get; set; } public string? ShipKind { get; set; }
public string? TargetSystemId { get; set; } public string? TargetSystemId { get; set; }
public int TargetCount { get; set; } public int TargetCount { get; set; }
public int CurrentCount { get; set; } public int CurrentCount { get; set; }
public string? Notes { get; set; } public string? Notes { get; set; }
} }
public sealed class FactionDecisionLogEntryRuntime public sealed class FactionDecisionLogEntryRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public required string Summary { get; set; } public required string Summary { get; set; }
public string? RelatedEntityId { get; set; } public string? RelatedEntityId { get; set; }
public int PlanCycle { get; set; } public int PlanCycle { get; set; }
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class FactionCommoditySignalRuntime public sealed class FactionCommoditySignalRuntime
{ {
public required string ItemId { get; init; } public required string ItemId { get; init; }
public float AvailableStock { get; set; } public float AvailableStock { get; set; }
public float OnHand { get; set; } public float OnHand { get; set; }
public float ProductionRatePerSecond { get; set; } public float ProductionRatePerSecond { get; set; }
public float CommittedProductionRatePerSecond { get; set; } public float CommittedProductionRatePerSecond { get; set; }
public float UsageRatePerSecond { get; set; } public float UsageRatePerSecond { get; set; }
public float NetRatePerSecond { get; set; } public float NetRatePerSecond { get; set; }
public float ProjectedNetRatePerSecond { get; set; } public float ProjectedNetRatePerSecond { get; set; }
public float LevelSeconds { get; set; } public float LevelSeconds { get; set; }
public string Level { get; set; } = "unknown"; public string Level { get; set; } = "unknown";
public float ProjectedProductionRatePerSecond { get; set; } public float ProjectedProductionRatePerSecond { get; set; }
public float BuyBacklog { get; set; } public float BuyBacklog { get; set; }
public float ReservedForConstruction { get; set; } public float ReservedForConstruction { get; set; }
} }
public sealed class FactionThreatSignalRuntime public sealed class FactionThreatSignalRuntime
{ {
public required string ScopeId { get; init; } public required string ScopeId { get; init; }
public required string ScopeKind { get; init; } public required string ScopeKind { get; init; }
public int EnemyShipCount { get; set; } public int EnemyShipCount { get; set; }
public int EnemyStationCount { get; set; } public int EnemyStationCount { get; set; }
public string? EnemyFactionId { get; set; } public string? EnemyFactionId { get; set; }
} }

View File

@@ -2,335 +2,335 @@ namespace SpaceGame.Api.Geopolitics.Runtime;
public sealed class GeopoliticalStateRuntime public sealed class GeopoliticalStateRuntime
{ {
public int Cycle { get; set; } public int Cycle { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<SystemRouteLinkRuntime> Routes { get; } = []; public List<SystemRouteLinkRuntime> Routes { get; } = [];
public DiplomaticStateRuntime Diplomacy { get; set; } = new(); public DiplomaticStateRuntime Diplomacy { get; set; } = new();
public TerritoryStateRuntime Territory { get; set; } = new(); public TerritoryStateRuntime Territory { get; set; } = new();
public EconomyRegionStateRuntime EconomyRegions { get; set; } = new(); public EconomyRegionStateRuntime EconomyRegions { get; set; } = new();
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class SystemRouteLinkRuntime public sealed class SystemRouteLinkRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string SourceSystemId { get; set; } public required string SourceSystemId { get; set; }
public required string DestinationSystemId { get; set; } public required string DestinationSystemId { get; set; }
public float Distance { get; set; } public float Distance { get; set; }
public bool IsPrimaryLane { get; set; } = true; public bool IsPrimaryLane { get; set; } = true;
} }
public sealed class DiplomaticStateRuntime public sealed class DiplomaticStateRuntime
{ {
public List<DiplomaticRelationRuntime> Relations { get; } = []; public List<DiplomaticRelationRuntime> Relations { get; } = [];
public List<TreatyRuntime> Treaties { get; } = []; public List<TreatyRuntime> Treaties { get; } = [];
public List<DiplomaticIncidentRuntime> Incidents { get; } = []; public List<DiplomaticIncidentRuntime> Incidents { get; } = [];
public List<BorderTensionRuntime> BorderTensions { get; } = []; public List<BorderTensionRuntime> BorderTensions { get; } = [];
public List<WarStateRuntime> Wars { get; } = []; public List<WarStateRuntime> Wars { get; } = [];
} }
public sealed class DiplomaticRelationRuntime public sealed class DiplomaticRelationRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string FactionAId { get; set; } public required string FactionAId { get; set; }
public required string FactionBId { get; set; } public required string FactionBId { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string Posture { get; set; } = "neutral"; public string Posture { get; set; } = "neutral";
public float TrustScore { get; set; } public float TrustScore { get; set; }
public float TensionScore { get; set; } public float TensionScore { get; set; }
public float GrievanceScore { get; set; } public float GrievanceScore { get; set; }
public string TradeAccessPolicy { get; set; } = "restricted"; public string TradeAccessPolicy { get; set; } = "restricted";
public string MilitaryAccessPolicy { get; set; } = "restricted"; public string MilitaryAccessPolicy { get; set; } = "restricted";
public string? WarStateId { get; set; } public string? WarStateId { get; set; }
public DateTimeOffset? CeasefireUntilUtc { get; set; } public DateTimeOffset? CeasefireUntilUtc { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> ActiveTreatyIds { get; } = []; public List<string> ActiveTreatyIds { get; } = [];
public List<string> ActiveIncidentIds { get; } = []; public List<string> ActiveIncidentIds { get; } = [];
} }
public sealed class TreatyRuntime public sealed class TreatyRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string TradeAccessPolicy { get; set; } = "restricted"; public string TradeAccessPolicy { get; set; } = "restricted";
public string MilitaryAccessPolicy { get; set; } = "restricted"; public string MilitaryAccessPolicy { get; set; } = "restricted";
public string? Summary { get; set; } public string? Summary { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> FactionIds { get; } = []; public List<string> FactionIds { get; } = [];
} }
public sealed class DiplomaticIncidentRuntime public sealed class DiplomaticIncidentRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public required string SourceFactionId { get; set; } public required string SourceFactionId { get; set; }
public required string TargetFactionId { get; set; } public required string TargetFactionId { get; set; }
public string? SystemId { get; set; } public string? SystemId { get; set; }
public string? BorderEdgeId { get; set; } public string? BorderEdgeId { get; set; }
public required string Summary { get; set; } public required string Summary { get; set; }
public float Severity { get; set; } public float Severity { get; set; }
public float EscalationScore { get; set; } public float EscalationScore { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset LastObservedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset LastObservedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class BorderTensionRuntime public sealed class BorderTensionRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string RelationId { get; set; } public required string RelationId { get; set; }
public required string BorderEdgeId { get; set; } public required string BorderEdgeId { get; set; }
public required string FactionAId { get; set; } public required string FactionAId { get; set; }
public required string FactionBId { get; set; } public required string FactionBId { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public float TensionScore { get; set; } public float TensionScore { get; set; }
public float IncidentScore { get; set; } public float IncidentScore { get; set; }
public float MilitaryPressure { get; set; } public float MilitaryPressure { get; set; }
public float AccessFriction { get; set; } public float AccessFriction { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> SystemIds { get; } = []; public List<string> SystemIds { get; } = [];
} }
public sealed class WarStateRuntime public sealed class WarStateRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string RelationId { get; set; } public required string RelationId { get; set; }
public required string FactionAId { get; set; } public required string FactionAId { get; set; }
public required string FactionBId { get; set; } public required string FactionBId { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string WarGoal { get; set; } = "territorial-pressure"; public string WarGoal { get; set; } = "territorial-pressure";
public float EscalationScore { get; set; } public float EscalationScore { get; set; }
public DateTimeOffset StartedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset StartedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? CeasefireUntilUtc { get; set; } public DateTimeOffset? CeasefireUntilUtc { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> ActiveFrontLineIds { get; } = []; public List<string> ActiveFrontLineIds { get; } = [];
} }
public sealed class TerritoryStateRuntime public sealed class TerritoryStateRuntime
{ {
public List<TerritoryClaimRuntime> Claims { get; } = []; public List<TerritoryClaimRuntime> Claims { get; } = [];
public List<TerritoryInfluenceRuntime> Influences { get; } = []; public List<TerritoryInfluenceRuntime> Influences { get; } = [];
public List<TerritoryControlStateRuntime> ControlStates { get; } = []; public List<TerritoryControlStateRuntime> ControlStates { get; } = [];
public List<SectorStrategicProfileRuntime> StrategicProfiles { get; } = []; public List<SectorStrategicProfileRuntime> StrategicProfiles { get; } = [];
public List<BorderEdgeRuntime> BorderEdges { get; } = []; public List<BorderEdgeRuntime> BorderEdges { get; } = [];
public List<FrontLineRuntime> FrontLines { get; } = []; public List<FrontLineRuntime> FrontLines { get; } = [];
public List<TerritoryZoneRuntime> Zones { get; } = []; public List<TerritoryZoneRuntime> Zones { get; } = [];
public List<TerritoryPressureRuntime> Pressures { get; } = []; public List<TerritoryPressureRuntime> Pressures { get; } = [];
} }
public sealed class TerritoryClaimRuntime public sealed class TerritoryClaimRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public string? SourceClaimId { get; set; } public string? SourceClaimId { get; set; }
public required string FactionId { get; set; } public required string FactionId { get; set; }
public required string SystemId { get; set; } public required string SystemId { get; set; }
public required string CelestialId { get; set; } public required string CelestialId { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string ClaimKind { get; set; } = "infrastructure"; public string ClaimKind { get; set; } = "infrastructure";
public float ClaimStrength { get; set; } public float ClaimStrength { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class TerritoryInfluenceRuntime public sealed class TerritoryInfluenceRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string SystemId { get; set; } public required string SystemId { get; set; }
public required string FactionId { get; set; } public required string FactionId { get; set; }
public float ClaimStrength { get; set; } public float ClaimStrength { get; set; }
public float AssetStrength { get; set; } public float AssetStrength { get; set; }
public float LogisticsStrength { get; set; } public float LogisticsStrength { get; set; }
public float TotalInfluence { get; set; } public float TotalInfluence { get; set; }
public bool IsContesting { get; set; } public bool IsContesting { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class TerritoryControlStateRuntime public sealed class TerritoryControlStateRuntime
{ {
public required string SystemId { get; init; } public required string SystemId { get; init; }
public string? ControllerFactionId { get; set; } public string? ControllerFactionId { get; set; }
public string? PrimaryClaimantFactionId { get; set; } public string? PrimaryClaimantFactionId { get; set; }
public string ControlKind { get; set; } = "unclaimed"; public string ControlKind { get; set; } = "unclaimed";
public bool IsContested { get; set; } public bool IsContested { get; set; }
public float ControlScore { get; set; } public float ControlScore { get; set; }
public float StrategicValue { get; set; } public float StrategicValue { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> ClaimantFactionIds { get; } = []; public List<string> ClaimantFactionIds { get; } = [];
public List<string> InfluencingFactionIds { get; } = []; public List<string> InfluencingFactionIds { get; } = [];
} }
public sealed class SectorStrategicProfileRuntime public sealed class SectorStrategicProfileRuntime
{ {
public required string SystemId { get; init; } public required string SystemId { get; init; }
public string? ControllerFactionId { get; set; } public string? ControllerFactionId { get; set; }
public string ZoneKind { get; set; } = "unclaimed"; public string ZoneKind { get; set; } = "unclaimed";
public bool IsContested { get; set; } public bool IsContested { get; set; }
public float StrategicValue { get; set; } public float StrategicValue { get; set; }
public float SecurityRating { get; set; } public float SecurityRating { get; set; }
public float TerritorialPressure { get; set; } public float TerritorialPressure { get; set; }
public float LogisticsValue { get; set; } public float LogisticsValue { get; set; }
public string? EconomicRegionId { get; set; } public string? EconomicRegionId { get; set; }
public string? FrontLineId { get; set; } public string? FrontLineId { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class BorderEdgeRuntime public sealed class BorderEdgeRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string SourceSystemId { get; set; } public required string SourceSystemId { get; set; }
public required string DestinationSystemId { get; set; } public required string DestinationSystemId { get; set; }
public string? SourceFactionId { get; set; } public string? SourceFactionId { get; set; }
public string? DestinationFactionId { get; set; } public string? DestinationFactionId { get; set; }
public bool IsContested { get; set; } public bool IsContested { get; set; }
public string? RelationId { get; set; } public string? RelationId { get; set; }
public float TensionScore { get; set; } public float TensionScore { get; set; }
public float CorridorImportance { get; set; } public float CorridorImportance { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class FrontLineRuntime public sealed class FrontLineRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public string Kind { get; set; } = "border-front"; public string Kind { get; set; } = "border-front";
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string? AnchorSystemId { get; set; } public string? AnchorSystemId { get; set; }
public float PressureScore { get; set; } public float PressureScore { get; set; }
public float SupplyRisk { get; set; } public float SupplyRisk { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> FactionIds { get; } = []; public List<string> FactionIds { get; } = [];
public List<string> SystemIds { get; } = []; public List<string> SystemIds { get; } = [];
public List<string> BorderEdgeIds { get; } = []; public List<string> BorderEdgeIds { get; } = [];
} }
public sealed class TerritoryZoneRuntime public sealed class TerritoryZoneRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string SystemId { get; set; } public required string SystemId { get; set; }
public string? FactionId { get; set; } public string? FactionId { get; set; }
public string Kind { get; set; } = "unclaimed"; public string Kind { get; set; } = "unclaimed";
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string? Reason { get; set; } public string? Reason { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class TerritoryPressureRuntime public sealed class TerritoryPressureRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string SystemId { get; set; } public required string SystemId { get; set; }
public string? FactionId { get; set; } public string? FactionId { get; set; }
public string Kind { get; set; } = "border-pressure"; public string Kind { get; set; } = "border-pressure";
public float PressureScore { get; set; } public float PressureScore { get; set; }
public float SecurityScore { get; set; } public float SecurityScore { get; set; }
public float HostileInfluence { get; set; } public float HostileInfluence { get; set; }
public float CorridorRisk { get; set; } public float CorridorRisk { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class EconomyRegionStateRuntime public sealed class EconomyRegionStateRuntime
{ {
public List<EconomicRegionRuntime> Regions { get; } = []; public List<EconomicRegionRuntime> Regions { get; } = [];
public List<SupplyNetworkRuntime> SupplyNetworks { get; } = []; public List<SupplyNetworkRuntime> SupplyNetworks { get; } = [];
public List<LogisticsCorridorRuntime> Corridors { get; } = []; public List<LogisticsCorridorRuntime> Corridors { get; } = [];
public List<RegionalProductionProfileRuntime> ProductionProfiles { get; } = []; public List<RegionalProductionProfileRuntime> ProductionProfiles { get; } = [];
public List<RegionalTradeBalanceRuntime> TradeBalances { get; } = []; public List<RegionalTradeBalanceRuntime> TradeBalances { get; } = [];
public List<RegionalBottleneckRuntime> Bottlenecks { get; } = []; public List<RegionalBottleneckRuntime> Bottlenecks { get; } = [];
public List<RegionalSecurityAssessmentRuntime> SecurityAssessments { get; } = []; public List<RegionalSecurityAssessmentRuntime> SecurityAssessments { get; } = [];
public List<RegionalEconomicAssessmentRuntime> EconomicAssessments { get; } = []; public List<RegionalEconomicAssessmentRuntime> EconomicAssessments { get; } = [];
} }
public sealed class EconomicRegionRuntime public sealed class EconomicRegionRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public string? FactionId { get; set; } public string? FactionId { get; set; }
public required string Label { get; set; } public required string Label { get; set; }
public string Kind { get; set; } = "balanced-region"; public string Kind { get; set; } = "balanced-region";
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public required string CoreSystemId { get; set; } public required string CoreSystemId { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> SystemIds { get; } = []; public List<string> SystemIds { get; } = [];
public List<string> StationIds { get; } = []; public List<string> StationIds { get; } = [];
public List<string> FrontLineIds { get; } = []; public List<string> FrontLineIds { get; } = [];
public List<string> CorridorIds { get; } = []; public List<string> CorridorIds { get; } = [];
} }
public sealed class SupplyNetworkRuntime public sealed class SupplyNetworkRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string RegionId { get; set; } public required string RegionId { get; set; }
public float ThroughputScore { get; set; } public float ThroughputScore { get; set; }
public float RiskScore { get; set; } public float RiskScore { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> StationIds { get; } = []; public List<string> StationIds { get; } = [];
public List<string> ProducerItemIds { get; } = []; public List<string> ProducerItemIds { get; } = [];
public List<string> ConsumerItemIds { get; } = []; public List<string> ConsumerItemIds { get; } = [];
public List<string> ConstructionItemIds { get; } = []; public List<string> ConstructionItemIds { get; } = [];
} }
public sealed class LogisticsCorridorRuntime public sealed class LogisticsCorridorRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public string? FactionId { get; set; } public string? FactionId { get; set; }
public string Kind { get; set; } = "supply-corridor"; public string Kind { get; set; } = "supply-corridor";
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public float RiskScore { get; set; } public float RiskScore { get; set; }
public float ThroughputScore { get; set; } public float ThroughputScore { get; set; }
public string AccessState { get; set; } = "restricted"; public string AccessState { get; set; } = "restricted";
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> SystemPathIds { get; } = []; public List<string> SystemPathIds { get; } = [];
public List<string> RegionIds { get; } = []; public List<string> RegionIds { get; } = [];
public List<string> BorderEdgeIds { get; } = []; public List<string> BorderEdgeIds { get; } = [];
} }
public sealed class RegionalProductionProfileRuntime public sealed class RegionalProductionProfileRuntime
{ {
public required string RegionId { get; set; } public required string RegionId { get; set; }
public string PrimaryIndustry { get; set; } = "mixed"; public string PrimaryIndustry { get; set; } = "mixed";
public int ShipyardCount { get; set; } public int ShipyardCount { get; set; }
public int StationCount { get; set; } public int StationCount { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public List<string> ProducedItemIds { get; } = []; public List<string> ProducedItemIds { get; } = [];
public List<string> ScarceItemIds { get; } = []; public List<string> ScarceItemIds { get; } = [];
} }
public sealed class RegionalTradeBalanceRuntime public sealed class RegionalTradeBalanceRuntime
{ {
public required string RegionId { get; set; } public required string RegionId { get; set; }
public int ImportsRequiredCount { get; set; } public int ImportsRequiredCount { get; set; }
public int ExportsSurplusCount { get; set; } public int ExportsSurplusCount { get; set; }
public int CriticalShortageCount { get; set; } public int CriticalShortageCount { get; set; }
public float NetTradeScore { get; set; } public float NetTradeScore { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class RegionalBottleneckRuntime public sealed class RegionalBottleneckRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string RegionId { get; set; } public required string RegionId { get; set; }
public required string ItemId { get; set; } public required string ItemId { get; set; }
public string Cause { get; set; } = "regional-shortage"; public string Cause { get; set; } = "regional-shortage";
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public float Severity { get; set; } public float Severity { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class RegionalSecurityAssessmentRuntime public sealed class RegionalSecurityAssessmentRuntime
{ {
public required string RegionId { get; set; } public required string RegionId { get; set; }
public float SupplyRisk { get; set; } public float SupplyRisk { get; set; }
public float BorderPressure { get; set; } public float BorderPressure { get; set; }
public int ActiveWarCount { get; set; } public int ActiveWarCount { get; set; }
public int HostileRelationCount { get; set; } public int HostileRelationCount { get; set; }
public float AccessFriction { get; set; } public float AccessFriction { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class RegionalEconomicAssessmentRuntime public sealed class RegionalEconomicAssessmentRuntime
{ {
public required string RegionId { get; set; } public required string RegionId { get; set; }
public float SustainmentScore { get; set; } public float SustainmentScore { get; set; }
public float ProductionDepth { get; set; } public float ProductionDepth { get; set; }
public float ConstructionPressure { get; set; } public float ConstructionPressure { get; set; }
public float CorridorDependency { get; set; } public float CorridorDependency { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }

View File

@@ -2,53 +2,53 @@ namespace SpaceGame.Api.Industry.Planning;
internal static class CommodityOperationalSignal internal static class CommodityOperationalSignal
{ {
internal static float ComputeNeedScore(FactionCommoditySnapshot commodity, float targetLevelSeconds) internal static float ComputeNeedScore(FactionCommoditySnapshot commodity, float targetLevelSeconds)
{
var productionDeficit = MathF.Max(0f, commodity.ConsumptionRatePerSecond - commodity.ProjectedProductionRatePerSecond);
var levelDeficit = MathF.Max(0f, targetLevelSeconds - commodity.LevelSeconds) / MathF.Max(targetLevelSeconds, 1f);
var backlogPressure = MathF.Max(0f, commodity.BuyBacklog + commodity.ReservedForConstruction - commodity.AvailableStock);
var levelWeight = commodity.Level switch
{ {
CommodityLevelKind.Critical => 140f, var productionDeficit = MathF.Max(0f, commodity.ConsumptionRatePerSecond - commodity.ProjectedProductionRatePerSecond);
CommodityLevelKind.Low => 80f, var levelDeficit = MathF.Max(0f, targetLevelSeconds - commodity.LevelSeconds) / MathF.Max(targetLevelSeconds, 1f);
CommodityLevelKind.Stable => 20f, var backlogPressure = MathF.Max(0f, commodity.BuyBacklog + commodity.ReservedForConstruction - commodity.AvailableStock);
_ => 0f,
};
return levelWeight var levelWeight = commodity.Level switch
+ (productionDeficit * 140f) {
+ (levelDeficit * 120f) CommodityLevelKind.Critical => 140f,
+ backlogPressure; CommodityLevelKind.Low => 80f,
} CommodityLevelKind.Stable => 20f,
_ => 0f,
};
internal static bool IsOperational(FactionCommoditySnapshot commodity, float targetLevelSeconds) => return levelWeight
commodity.ProjectedProductionRatePerSecond > 0.01f + (productionDeficit * 140f)
&& commodity.ProjectedNetRatePerSecond >= -0.01f + (levelDeficit * 120f)
&& commodity.LevelSeconds >= targetLevelSeconds + backlogPressure;
&& commodity.Level is CommodityLevelKind.Stable or CommodityLevelKind.Surplus;
internal static bool IsStrained(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
!IsOperational(commodity, targetLevelSeconds)
|| commodity.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low;
internal static float ComputeFeasibilityFactor(FactionCommoditySnapshot commodity, float targetLevelSeconds)
{
if (commodity.AvailableStock <= 0.01f && commodity.ProjectedProductionRatePerSecond <= 0.01f)
{
return 0.65f;
} }
if (commodity.Level is CommodityLevelKind.Critical) internal static bool IsOperational(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
{ commodity.ProjectedProductionRatePerSecond > 0.01f
return 0.72f; && commodity.ProjectedNetRatePerSecond >= -0.01f
} && commodity.LevelSeconds >= targetLevelSeconds
&& commodity.Level is CommodityLevelKind.Stable or CommodityLevelKind.Surplus;
if (commodity.Level is CommodityLevelKind.Low || commodity.LevelSeconds < targetLevelSeconds) internal static bool IsStrained(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
{ !IsOperational(commodity, targetLevelSeconds)
return 0.84f; || commodity.Level is CommodityLevelKind.Critical or CommodityLevelKind.Low;
}
return 1f; internal static float ComputeFeasibilityFactor(FactionCommoditySnapshot commodity, float targetLevelSeconds)
} {
if (commodity.AvailableStock <= 0.01f && commodity.ProjectedProductionRatePerSecond <= 0.01f)
{
return 0.65f;
}
if (commodity.Level is CommodityLevelKind.Critical)
{
return 0.72f;
}
if (commodity.Level is CommodityLevelKind.Low || commodity.LevelSeconds < targetLevelSeconds)
{
return 0.84f;
}
return 1f;
}
} }

View File

@@ -4,202 +4,202 @@ using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport;
internal sealed class FactionEconomySnapshot internal sealed class FactionEconomySnapshot
{ {
private readonly Dictionary<string, FactionCommoditySnapshot> commodities = new(StringComparer.Ordinal); private readonly Dictionary<string, FactionCommoditySnapshot> commodities = new(StringComparer.Ordinal);
internal IReadOnlyDictionary<string, FactionCommoditySnapshot> Commodities => commodities; internal IReadOnlyDictionary<string, FactionCommoditySnapshot> Commodities => commodities;
internal FactionCommoditySnapshot GetCommodity(string itemId) internal FactionCommoditySnapshot GetCommodity(string itemId)
{
if (!commodities.TryGetValue(itemId, out var commodity))
{ {
commodity = new FactionCommoditySnapshot(itemId); if (!commodities.TryGetValue(itemId, out var commodity))
commodities[itemId] = commodity; {
} commodity = new FactionCommoditySnapshot(itemId);
commodities[itemId] = commodity;
}
return commodity; return commodity;
} }
} }
internal sealed class FactionCommoditySnapshot internal sealed class FactionCommoditySnapshot
{ {
internal FactionCommoditySnapshot(string itemId) internal FactionCommoditySnapshot(string itemId)
{ {
ItemId = itemId; ItemId = itemId;
} }
internal string ItemId { get; } internal string ItemId { get; }
internal float OnHand { get; set; } internal float OnHand { get; set; }
internal float ReservedForConstruction { get; set; } internal float ReservedForConstruction { get; set; }
internal float BuyBacklog { get; set; } internal float BuyBacklog { get; set; }
internal float SellBacklog { get; set; } internal float SellBacklog { get; set; }
internal float Inbound { get; set; } internal float Inbound { get; set; }
internal float ProductionRatePerSecond { get; set; } internal float ProductionRatePerSecond { get; set; }
internal float CommittedProductionRatePerSecond { get; set; } internal float CommittedProductionRatePerSecond { get; set; }
internal float ConsumptionRatePerSecond { get; set; } internal float ConsumptionRatePerSecond { get; set; }
internal float AvailableStock => MathF.Max(0f, OnHand + Inbound - ReservedForConstruction); internal float AvailableStock => MathF.Max(0f, OnHand + Inbound - ReservedForConstruction);
internal float NetRatePerSecond => ProductionRatePerSecond - ConsumptionRatePerSecond; internal float NetRatePerSecond => ProductionRatePerSecond - ConsumptionRatePerSecond;
internal float ProjectedProductionRatePerSecond => ProductionRatePerSecond + CommittedProductionRatePerSecond; internal float ProjectedProductionRatePerSecond => ProductionRatePerSecond + CommittedProductionRatePerSecond;
internal float ProjectedNetRatePerSecond => ProjectedProductionRatePerSecond - ConsumptionRatePerSecond; internal float ProjectedNetRatePerSecond => ProjectedProductionRatePerSecond - ConsumptionRatePerSecond;
internal float OperationalUsageRatePerSecond => MathF.Max(ConsumptionRatePerSecond, BuyBacklog / 180f); internal float OperationalUsageRatePerSecond => MathF.Max(ConsumptionRatePerSecond, BuyBacklog / 180f);
internal float LevelSeconds => AvailableStock <= 0.01f internal float LevelSeconds => AvailableStock <= 0.01f
? 0f ? 0f
: AvailableStock / MathF.Max(OperationalUsageRatePerSecond, 0.01f); : AvailableStock / MathF.Max(OperationalUsageRatePerSecond, 0.01f);
internal CommodityLevelKind Level => internal CommodityLevelKind Level =>
LevelSeconds switch LevelSeconds switch
{ {
<= 60f => CommodityLevelKind.Critical, <= 60f => CommodityLevelKind.Critical,
<= 180f => CommodityLevelKind.Low, <= 180f => CommodityLevelKind.Low,
<= 480f => CommodityLevelKind.Stable, <= 480f => CommodityLevelKind.Stable,
_ => CommodityLevelKind.Surplus, _ => CommodityLevelKind.Surplus,
}; };
} }
internal enum CommodityLevelKind internal enum CommodityLevelKind
{ {
Critical, Critical,
Low, Low,
Stable, Stable,
Surplus, Surplus,
} }
internal static class FactionEconomyAnalyzer internal static class FactionEconomyAnalyzer
{ {
internal static FactionEconomySnapshot Build(SimulationWorld world, string factionId) internal static FactionEconomySnapshot Build(SimulationWorld world, string factionId)
{
var snapshot = new FactionEconomySnapshot();
foreach (var station in world.Stations.Where(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal)))
{ {
foreach (var (itemId, amount) in station.Inventory) var snapshot = new FactionEconomySnapshot();
{
snapshot.GetCommodity(itemId).OnHand += amount;
}
foreach (var laneKey in StationSimulationService.GetStationProductionLanes(world, station)) foreach (var station in world.Stations.Where(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal)))
{
var recipe = StationSimulationService.SelectProductionRecipe(world, station, laneKey);
if (recipe is null)
{ {
continue; foreach (var (itemId, amount) in station.Inventory)
{
snapshot.GetCommodity(itemId).OnHand += amount;
}
foreach (var laneKey in StationSimulationService.GetStationProductionLanes(world, station))
{
var recipe = StationSimulationService.SelectProductionRecipe(world, station, laneKey);
if (recipe is null)
{
continue;
}
var throughput = StationSimulationService.GetStationProductionThroughput(world, station, recipe);
var cyclesPerSecond = (station.WorkforceEffectiveRatio * throughput) / MathF.Max(recipe.Duration, 0.01f);
if (cyclesPerSecond <= 0.0001f)
{
continue;
}
foreach (var input in recipe.Inputs)
{
snapshot.GetCommodity(input.ItemId).ConsumptionRatePerSecond += input.Amount * cyclesPerSecond;
}
foreach (var output in recipe.Outputs)
{
snapshot.GetCommodity(output.ItemId).ProductionRatePerSecond += output.Amount * cyclesPerSecond;
}
}
} }
var throughput = StationSimulationService.GetStationProductionThroughput(world, station, recipe); foreach (var order in world.MarketOrders.Where(order =>
var cyclesPerSecond = (station.WorkforceEffectiveRatio * throughput) / MathF.Max(recipe.Duration, 0.01f); string.Equals(order.FactionId, factionId, StringComparison.Ordinal)
if (cyclesPerSecond <= 0.0001f) && order.State != MarketOrderStateKinds.Cancelled
&& order.RemainingAmount > 0.01f))
{ {
continue; var commodity = snapshot.GetCommodity(order.ItemId);
if (string.Equals(order.Kind, MarketOrderKinds.Buy, StringComparison.Ordinal))
{
commodity.BuyBacklog += order.RemainingAmount;
}
else if (string.Equals(order.Kind, MarketOrderKinds.Sell, StringComparison.Ordinal))
{
commodity.SellBacklog += order.RemainingAmount;
}
} }
foreach (var input in recipe.Inputs) foreach (var site in world.ConstructionSites.Where(site =>
string.Equals(site.FactionId, factionId, StringComparison.Ordinal)
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed))
{ {
snapshot.GetCommodity(input.ItemId).ConsumptionRatePerSecond += input.Amount * cyclesPerSecond; ApplyCommittedProduction(world, snapshot, site);
foreach (var required in site.RequiredItems)
{
var remaining = MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key));
if (remaining > 0.01f)
{
snapshot.GetCommodity(required.Key).ReservedForConstruction += remaining;
}
}
} }
foreach (var output in recipe.Outputs) return snapshot;
}
private static void ApplyCommittedProduction(
SimulationWorld world,
FactionEconomySnapshot snapshot,
ConstructionSiteRuntime site)
{
if (string.IsNullOrWhiteSpace(site.BlueprintId)
|| !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe))
{ {
snapshot.GetCommodity(output.ItemId).ProductionRatePerSecond += output.Amount * cyclesPerSecond; return;
} }
}
}
foreach (var order in world.MarketOrders.Where(order => var recipeOutputs = world.Recipes.Values
string.Equals(order.FactionId, factionId, StringComparison.Ordinal) .Where(candidate => string.Equals(StationSimulationService.GetStationProductionLaneKey(world, candidate), site.BlueprintId, StringComparison.Ordinal))
&& order.State != MarketOrderStateKinds.Cancelled .SelectMany(candidate => candidate.Outputs)
&& order.RemainingAmount > 0.01f)) .GroupBy(output => output.ItemId, StringComparer.Ordinal)
{ .ToDictionary(group => group.Key, group => group.Sum(output => output.Amount), StringComparer.Ordinal);
var commodity = snapshot.GetCommodity(order.ItemId); if (recipeOutputs.Count == 0)
if (string.Equals(order.Kind, MarketOrderKinds.Buy, StringComparison.Ordinal))
{
commodity.BuyBacklog += order.RemainingAmount;
}
else if (string.Equals(order.Kind, MarketOrderKinds.Sell, StringComparison.Ordinal))
{
commodity.SellBacklog += order.RemainingAmount;
}
}
foreach (var site in world.ConstructionSites.Where(site =>
string.Equals(site.FactionId, factionId, StringComparison.Ordinal)
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed))
{
ApplyCommittedProduction(world, snapshot, site);
foreach (var required in site.RequiredItems)
{
var remaining = MathF.Max(0f, required.Value - GetConstructionDeliveredAmount(world, site, required.Key));
if (remaining > 0.01f)
{ {
snapshot.GetCommodity(required.Key).ReservedForConstruction += remaining; return;
}
var materialFraction = 0f;
var materialTerms = 0;
foreach (var required in site.RequiredItems)
{
materialTerms += 1;
materialFraction += required.Value <= 0.01f
? 1f
: Math.Clamp(GetConstructionDeliveredAmount(world, site, required.Key) / required.Value, 0f, 1f);
}
materialFraction = materialTerms == 0 ? 1f : materialFraction / materialTerms;
var buildFraction = recipe.Duration <= 0.01f
? 0f
: Math.Clamp(site.Progress / recipe.Duration, 0f, 1f);
var readiness = site.State switch
{
ConstructionSiteStateKinds.Active => 0.3f,
ConstructionSiteStateKinds.Planned => 0.15f,
_ => 0f,
};
readiness += materialFraction * 0.45f;
readiness += buildFraction * 0.25f;
if (site.AssignedConstructorShipIds.Count > 0)
{
readiness += 0.1f;
}
readiness = Math.Clamp(readiness, 0f, 1f);
if (readiness <= 0.01f)
{
return;
}
var cyclesPerSecond = readiness / MathF.Max(recipe.Duration, 0.01f);
foreach (var (productItemId, amount) in recipeOutputs)
{
snapshot.GetCommodity(productItemId).CommittedProductionRatePerSecond += amount * cyclesPerSecond;
} }
}
} }
return snapshot;
}
private static void ApplyCommittedProduction(
SimulationWorld world,
FactionEconomySnapshot snapshot,
ConstructionSiteRuntime site)
{
if (string.IsNullOrWhiteSpace(site.BlueprintId)
|| !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe))
{
return;
}
var recipeOutputs = world.Recipes.Values
.Where(candidate => string.Equals(StationSimulationService.GetStationProductionLaneKey(world, candidate), site.BlueprintId, StringComparison.Ordinal))
.SelectMany(candidate => candidate.Outputs)
.GroupBy(output => output.ItemId, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.Sum(output => output.Amount), StringComparer.Ordinal);
if (recipeOutputs.Count == 0)
{
return;
}
var materialFraction = 0f;
var materialTerms = 0;
foreach (var required in site.RequiredItems)
{
materialTerms += 1;
materialFraction += required.Value <= 0.01f
? 1f
: Math.Clamp(GetConstructionDeliveredAmount(world, site, required.Key) / required.Value, 0f, 1f);
}
materialFraction = materialTerms == 0 ? 1f : materialFraction / materialTerms;
var buildFraction = recipe.Duration <= 0.01f
? 0f
: Math.Clamp(site.Progress / recipe.Duration, 0f, 1f);
var readiness = site.State switch
{
ConstructionSiteStateKinds.Active => 0.3f,
ConstructionSiteStateKinds.Planned => 0.15f,
_ => 0f,
};
readiness += materialFraction * 0.45f;
readiness += buildFraction * 0.25f;
if (site.AssignedConstructorShipIds.Count > 0)
{
readiness += 0.1f;
}
readiness = Math.Clamp(readiness, 0f, 1f);
if (readiness <= 0.01f)
{
return;
}
var cyclesPerSecond = readiness / MathF.Max(recipe.Duration, 0.01f);
foreach (var (productItemId, amount) in recipeOutputs)
{
snapshot.GetCommodity(productItemId).CommittedProductionRatePerSecond += amount * cyclesPerSecond;
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,52 +2,52 @@ namespace SpaceGame.Api.Industry.Planning;
public sealed class ProductionGraph public sealed class ProductionGraph
{ {
public required IReadOnlyDictionary<string, ProductionCommodityNode> Commodities { get; init; } public required IReadOnlyDictionary<string, ProductionCommodityNode> Commodities { get; init; }
public required IReadOnlyDictionary<string, ProductionProcessNode> Processes { get; init; } public required IReadOnlyDictionary<string, ProductionProcessNode> Processes { get; init; }
public required IReadOnlyDictionary<string, IReadOnlyList<ProductionProcessNode>> ProcessesByOutputId { get; init; } public required IReadOnlyDictionary<string, IReadOnlyList<ProductionProcessNode>> ProcessesByOutputId { get; init; }
public required IReadOnlyDictionary<string, IReadOnlyList<ProductionProcessNode>> ProcessesByInputId { get; init; } public required IReadOnlyDictionary<string, IReadOnlyList<ProductionProcessNode>> ProcessesByInputId { get; init; }
public required IReadOnlyDictionary<string, IReadOnlyList<string>> OutputsByModuleId { get; init; } public required IReadOnlyDictionary<string, IReadOnlyList<string>> OutputsByModuleId { get; init; }
public IReadOnlyList<ProductionProcessNode> GetProcessesForOutput(string itemId) => public IReadOnlyList<ProductionProcessNode> GetProcessesForOutput(string itemId) =>
ProcessesByOutputId.TryGetValue(itemId, out var processes) ? processes : []; ProcessesByOutputId.TryGetValue(itemId, out var processes) ? processes : [];
public IReadOnlyList<ProductionProcessNode> GetProcessesForInput(string itemId) => public IReadOnlyList<ProductionProcessNode> GetProcessesForInput(string itemId) =>
ProcessesByInputId.TryGetValue(itemId, out var processes) ? processes : []; ProcessesByInputId.TryGetValue(itemId, out var processes) ? processes : [];
public string? GetPrimaryProducerModule(string itemId) => public string? GetPrimaryProducerModule(string itemId) =>
GetProcessesForOutput(itemId) GetProcessesForOutput(itemId)
.SelectMany(process => process.RequiredModuleIds) .SelectMany(process => process.RequiredModuleIds)
.FirstOrDefault(); .FirstOrDefault();
public string? GetPrimaryOutputForModule(string moduleId) => public string? GetPrimaryOutputForModule(string moduleId) =>
OutputsByModuleId.TryGetValue(moduleId, out var outputs) OutputsByModuleId.TryGetValue(moduleId, out var outputs)
? outputs.FirstOrDefault() ? outputs.FirstOrDefault()
: null; : null;
public IReadOnlyList<string> GetImmediateInputs(string itemId) => public IReadOnlyList<string> GetImmediateInputs(string itemId) =>
GetProcessesForOutput(itemId) GetProcessesForOutput(itemId)
.SelectMany(process => process.Inputs.Keys) .SelectMany(process => process.Inputs.Keys)
.Distinct(StringComparer.Ordinal) .Distinct(StringComparer.Ordinal)
.ToList(); .ToList();
} }
public sealed class ProductionCommodityNode public sealed class ProductionCommodityNode
{ {
public required string ItemId { get; init; } public required string ItemId { get; init; }
public required string Name { get; init; } public required string Name { get; init; }
public required string Group { get; init; } public required string Group { get; init; }
public required string CargoKind { get; init; } public required string CargoKind { get; init; }
public List<string> ProducerProcessIds { get; } = []; public List<string> ProducerProcessIds { get; } = [];
public List<string> ConsumerProcessIds { get; } = []; public List<string> ConsumerProcessIds { get; } = [];
} }
public sealed class ProductionProcessNode public sealed class ProductionProcessNode
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; init; } public required string Label { get; init; }
public required string FacilityCategory { get; init; } public required string FacilityCategory { get; init; }
public required IReadOnlyList<string> RequiredModuleIds { get; init; } public required IReadOnlyList<string> RequiredModuleIds { get; init; }
public required IReadOnlyDictionary<string, float> Inputs { get; init; } public required IReadOnlyDictionary<string, float> Inputs { get; init; }
public required IReadOnlyDictionary<string, float> Outputs { get; init; } public required IReadOnlyDictionary<string, float> Outputs { get; init; }
public required bool ProducesShip { get; init; } public required bool ProducesShip { get; init; }
} }

View File

@@ -2,104 +2,104 @@ namespace SpaceGame.Api.Industry.Planning;
internal static class ProductionGraphBuilder internal static class ProductionGraphBuilder
{ {
internal static ProductionGraph Build( internal static ProductionGraph Build(
IReadOnlyCollection<ItemDefinition> items, IReadOnlyCollection<ItemDefinition> items,
IReadOnlyCollection<RecipeDefinition> recipes, IReadOnlyCollection<RecipeDefinition> recipes,
IReadOnlyCollection<ModuleDefinition> modules) IReadOnlyCollection<ModuleDefinition> modules)
{
var commodities = items.ToDictionary(
item => item.Id,
item => new ProductionCommodityNode
{
ItemId = item.Id,
Name = item.Name,
Group = item.Group,
CargoKind = item.CargoKind,
},
StringComparer.Ordinal);
var processes = new Dictionary<string, ProductionProcessNode>(StringComparer.Ordinal);
var processesByOutputId = new Dictionary<string, List<ProductionProcessNode>>(StringComparer.Ordinal);
var processesByInputId = new Dictionary<string, List<ProductionProcessNode>>(StringComparer.Ordinal);
var outputsByModuleId = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
foreach (var recipe in recipes)
{ {
var outputs = recipe.Outputs var commodities = items.ToDictionary(
.GroupBy(output => output.ItemId, StringComparer.Ordinal) item => item.Id,
.ToDictionary(group => group.Key, group => group.Sum(output => output.Amount), StringComparer.Ordinal); item => new ProductionCommodityNode
var inputs = recipe.Inputs {
.GroupBy(input => input.ItemId, StringComparer.Ordinal) ItemId = item.Id,
.ToDictionary(group => group.Key, group => group.Sum(input => input.Amount), StringComparer.Ordinal); Name = item.Name,
var process = new ProductionProcessNode Group = item.Group,
{ CargoKind = item.CargoKind,
Id = recipe.Id, },
Label = recipe.Label, StringComparer.Ordinal);
FacilityCategory = recipe.FacilityCategory,
RequiredModuleIds = recipe.RequiredModules.ToList(),
Inputs = inputs,
Outputs = outputs,
ProducesShip = recipe.ShipOutputId is not null,
};
processes[process.Id] = process; var processes = new Dictionary<string, ProductionProcessNode>(StringComparer.Ordinal);
var processesByOutputId = new Dictionary<string, List<ProductionProcessNode>>(StringComparer.Ordinal);
var processesByInputId = new Dictionary<string, List<ProductionProcessNode>>(StringComparer.Ordinal);
var outputsByModuleId = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
foreach (var output in outputs.Keys) foreach (var recipe in recipes)
{
if (!commodities.ContainsKey(output))
{ {
continue; var outputs = recipe.Outputs
.GroupBy(output => output.ItemId, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.Sum(output => output.Amount), StringComparer.Ordinal);
var inputs = recipe.Inputs
.GroupBy(input => input.ItemId, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.Sum(input => input.Amount), StringComparer.Ordinal);
var process = new ProductionProcessNode
{
Id = recipe.Id,
Label = recipe.Label,
FacilityCategory = recipe.FacilityCategory,
RequiredModuleIds = recipe.RequiredModules.ToList(),
Inputs = inputs,
Outputs = outputs,
ProducesShip = recipe.ShipOutputId is not null,
};
processes[process.Id] = process;
foreach (var output in outputs.Keys)
{
if (!commodities.ContainsKey(output))
{
continue;
}
commodities[output].ProducerProcessIds.Add(process.Id);
if (!processesByOutputId.TryGetValue(output, out var outputProcesses))
{
outputProcesses = [];
processesByOutputId[output] = outputProcesses;
}
outputProcesses.Add(process);
}
foreach (var input in inputs.Keys)
{
if (!commodities.ContainsKey(input))
{
continue;
}
commodities[input].ConsumerProcessIds.Add(process.Id);
if (!processesByInputId.TryGetValue(input, out var inputProcesses))
{
inputProcesses = [];
processesByInputId[input] = inputProcesses;
}
inputProcesses.Add(process);
}
} }
commodities[output].ProducerProcessIds.Add(process.Id); foreach (var module in modules)
if (!processesByOutputId.TryGetValue(output, out var outputProcesses))
{ {
outputProcesses = []; if (!outputsByModuleId.TryGetValue(module.Id, out var outputs))
processesByOutputId[output] = outputProcesses; {
outputs = new HashSet<string>(StringComparer.Ordinal);
outputsByModuleId[module.Id] = outputs;
}
foreach (var product in module.Products)
{
outputs.Add(product);
}
} }
outputProcesses.Add(process); return new ProductionGraph
}
foreach (var input in inputs.Keys)
{
if (!commodities.ContainsKey(input))
{ {
continue; Commodities = commodities,
} Processes = processes,
ProcessesByOutputId = processesByOutputId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<ProductionProcessNode>)entry.Value, StringComparer.Ordinal),
commodities[input].ConsumerProcessIds.Add(process.Id); ProcessesByInputId = processesByInputId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<ProductionProcessNode>)entry.Value, StringComparer.Ordinal),
if (!processesByInputId.TryGetValue(input, out var inputProcesses)) OutputsByModuleId = outputsByModuleId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<string>)entry.Value.OrderBy(value => value, StringComparer.Ordinal).ToList(), StringComparer.Ordinal),
{ };
inputProcesses = [];
processesByInputId[input] = inputProcesses;
}
inputProcesses.Add(process);
}
} }
foreach (var module in modules)
{
if (!outputsByModuleId.TryGetValue(module.Id, out var outputs))
{
outputs = new HashSet<string>(StringComparer.Ordinal);
outputsByModuleId[module.Id] = outputs;
}
foreach (var product in module.Products)
{
outputs.Add(product);
}
}
return new ProductionGraph
{
Commodities = commodities,
Processes = processes,
ProcessesByOutputId = processesByOutputId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<ProductionProcessNode>)entry.Value, StringComparer.Ordinal),
ProcessesByInputId = processesByInputId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<ProductionProcessNode>)entry.Value, StringComparer.Ordinal),
OutputsByModuleId = outputsByModuleId.ToDictionary(entry => entry.Key, entry => (IReadOnlyList<string>)entry.Value.OrderBy(value => value, StringComparer.Ordinal).ToList(), StringComparer.Ordinal),
};
}
} }

View File

@@ -4,29 +4,29 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class CreatePlayerOrganizationHandler(WorldService worldService) : Endpoint<PlayerOrganizationCommandRequest, PlayerFactionSnapshot> public sealed class CreatePlayerOrganizationHandler(WorldService worldService) : Endpoint<PlayerOrganizationCommandRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Post("/api/player-faction/organizations");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerOrganizationCommandRequest request, CancellationToken cancellationToken)
{
try
{ {
var snapshot = worldService.CreatePlayerOrganization(request); Post("/api/player-faction/organizations");
if (snapshot is null) AllowAnonymous();
{ }
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(PlayerOrganizationCommandRequest request, CancellationToken cancellationToken)
}
catch (InvalidOperationException ex)
{ {
AddError(ex.Message); try
await SendErrorsAsync(cancellation: cancellationToken); {
var snapshot = worldService.CreatePlayerOrganization(request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
catch (InvalidOperationException ex)
{
AddError(ex.Message);
await SendErrorsAsync(cancellation: cancellationToken);
}
} }
}
} }

View File

@@ -4,26 +4,26 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class DeletePlayerDirectiveRequest public sealed class DeletePlayerDirectiveRequest
{ {
public string DirectiveId { get; set; } = string.Empty; public string DirectiveId { get; set; } = string.Empty;
} }
public sealed class DeletePlayerDirectiveHandler(WorldService worldService) : Endpoint<DeletePlayerDirectiveRequest, PlayerFactionSnapshot> public sealed class DeletePlayerDirectiveHandler(WorldService worldService) : Endpoint<DeletePlayerDirectiveRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Delete("/api/player-faction/directives/{directiveId}");
AllowAnonymous();
}
public override async Task HandleAsync(DeletePlayerDirectiveRequest request, CancellationToken cancellationToken)
{
var snapshot = worldService.DeletePlayerDirective(request.DirectiveId);
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); Delete("/api/player-faction/directives/{directiveId}");
return; AllowAnonymous();
} }
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(DeletePlayerDirectiveRequest request, CancellationToken cancellationToken)
} {
var snapshot = worldService.DeletePlayerDirective(request.DirectiveId);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -4,34 +4,34 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class DeletePlayerOrganizationRequest public sealed class DeletePlayerOrganizationRequest
{ {
public string OrganizationId { get; set; } = string.Empty; public string OrganizationId { get; set; } = string.Empty;
} }
public sealed class DeletePlayerOrganizationHandler(WorldService worldService) : Endpoint<DeletePlayerOrganizationRequest, PlayerFactionSnapshot> public sealed class DeletePlayerOrganizationHandler(WorldService worldService) : Endpoint<DeletePlayerOrganizationRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Delete("/api/player-faction/organizations/{organizationId}");
AllowAnonymous();
}
public override async Task HandleAsync(DeletePlayerOrganizationRequest request, CancellationToken cancellationToken)
{
try
{ {
var snapshot = worldService.DeletePlayerOrganization(request.OrganizationId); Delete("/api/player-faction/organizations/{organizationId}");
if (snapshot is null) AllowAnonymous();
{ }
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(DeletePlayerOrganizationRequest request, CancellationToken cancellationToken)
}
catch (InvalidOperationException ex)
{ {
AddError(ex.Message); try
await SendErrorsAsync(cancellation: cancellationToken); {
var snapshot = worldService.DeletePlayerOrganization(request.OrganizationId);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
catch (InvalidOperationException ex)
{
AddError(ex.Message);
await SendErrorsAsync(cancellation: cancellationToken);
}
} }
}
} }

View File

@@ -4,21 +4,21 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class GetPlayerFactionHandler(WorldService worldService) : EndpointWithoutRequest<PlayerFactionSnapshot> public sealed class GetPlayerFactionHandler(WorldService worldService) : EndpointWithoutRequest<PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Get("/api/player-faction");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var snapshot = worldService.GetPlayerFaction();
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); Get("/api/player-faction");
return; AllowAnonymous();
} }
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(CancellationToken cancellationToken)
} {
var snapshot = worldService.GetPlayerFaction();
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -4,37 +4,37 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class UpdatePlayerOrganizationMembershipHandler(WorldService worldService) : Endpoint<PlayerOrganizationMembershipCommandRequest, PlayerFactionSnapshot> public sealed class UpdatePlayerOrganizationMembershipHandler(WorldService worldService) : Endpoint<PlayerOrganizationMembershipCommandRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Put("/api/player-faction/organizations/{organizationId}/membership");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerOrganizationMembershipCommandRequest request, CancellationToken cancellationToken)
{
try
{ {
var organizationId = Route<string>("organizationId"); Put("/api/player-faction/organizations/{organizationId}/membership");
if (string.IsNullOrWhiteSpace(organizationId)) AllowAnonymous();
{
AddError("organizationId route parameter is required.");
await SendErrorsAsync(cancellation: cancellationToken);
return;
}
var snapshot = worldService.UpdatePlayerOrganizationMembership(organizationId, request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
} }
catch (InvalidOperationException ex)
public override async Task HandleAsync(PlayerOrganizationMembershipCommandRequest request, CancellationToken cancellationToken)
{ {
AddError(ex.Message); try
await SendErrorsAsync(cancellation: cancellationToken); {
var organizationId = Route<string>("organizationId");
if (string.IsNullOrWhiteSpace(organizationId))
{
AddError("organizationId route parameter is required.");
await SendErrorsAsync(cancellation: cancellationToken);
return;
}
var snapshot = worldService.UpdatePlayerOrganizationMembership(organizationId, request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
catch (InvalidOperationException ex)
{
AddError(ex.Message);
await SendErrorsAsync(cancellation: cancellationToken);
}
} }
}
} }

View File

@@ -4,21 +4,21 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class UpdatePlayerStrategicIntentHandler(WorldService worldService) : Endpoint<PlayerStrategicIntentCommandRequest, PlayerFactionSnapshot> public sealed class UpdatePlayerStrategicIntentHandler(WorldService worldService) : Endpoint<PlayerStrategicIntentCommandRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Put("/api/player-faction/strategic-intent");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerStrategicIntentCommandRequest request, CancellationToken cancellationToken)
{
var snapshot = worldService.UpdatePlayerStrategicIntent(request);
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); Put("/api/player-faction/strategic-intent");
return; AllowAnonymous();
} }
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(PlayerStrategicIntentCommandRequest request, CancellationToken cancellationToken)
} {
var snapshot = worldService.UpdatePlayerStrategicIntent(request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -4,28 +4,28 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class UpsertPlayerAssignmentHandler(WorldService worldService) : Endpoint<PlayerAssetAssignmentCommandRequest, PlayerFactionSnapshot> public sealed class UpsertPlayerAssignmentHandler(WorldService worldService) : Endpoint<PlayerAssetAssignmentCommandRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Put("/api/player-faction/assets/{assetId}/assignment");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerAssetAssignmentCommandRequest request, CancellationToken cancellationToken)
{
var assetId = Route<string>("assetId");
if (string.IsNullOrWhiteSpace(assetId))
{ {
await SendNotFoundAsync(cancellationToken); Put("/api/player-faction/assets/{assetId}/assignment");
return; AllowAnonymous();
} }
var snapshot = worldService.UpsertPlayerAssignment(assetId, request); public override async Task HandleAsync(PlayerAssetAssignmentCommandRequest request, CancellationToken cancellationToken)
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); var assetId = Route<string>("assetId");
return; if (string.IsNullOrWhiteSpace(assetId))
} {
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken); var snapshot = worldService.UpsertPlayerAssignment(assetId, request);
} if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -4,23 +4,23 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class UpsertPlayerAutomationPolicyHandler(WorldService worldService) : Endpoint<PlayerAutomationPolicyCommandRequest, PlayerFactionSnapshot> public sealed class UpsertPlayerAutomationPolicyHandler(WorldService worldService) : Endpoint<PlayerAutomationPolicyCommandRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Post("/api/player-faction/automation-policies");
Put("/api/player-faction/automation-policies/{automationPolicyId}");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerAutomationPolicyCommandRequest request, CancellationToken cancellationToken)
{
var automationPolicyId = Route<string?>("automationPolicyId");
var snapshot = worldService.UpsertPlayerAutomationPolicy(string.IsNullOrWhiteSpace(automationPolicyId) ? null : automationPolicyId, request);
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); Post("/api/player-faction/automation-policies");
return; Put("/api/player-faction/automation-policies/{automationPolicyId}");
AllowAnonymous();
} }
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(PlayerAutomationPolicyCommandRequest request, CancellationToken cancellationToken)
} {
var automationPolicyId = Route<string?>("automationPolicyId");
var snapshot = worldService.UpsertPlayerAutomationPolicy(string.IsNullOrWhiteSpace(automationPolicyId) ? null : automationPolicyId, request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -4,23 +4,23 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class UpsertPlayerDirectiveHandler(WorldService worldService) : Endpoint<PlayerDirectiveCommandRequest, PlayerFactionSnapshot> public sealed class UpsertPlayerDirectiveHandler(WorldService worldService) : Endpoint<PlayerDirectiveCommandRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Post("/api/player-faction/directives");
Put("/api/player-faction/directives/{directiveId}");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerDirectiveCommandRequest request, CancellationToken cancellationToken)
{
var directiveId = Route<string?>("directiveId");
var snapshot = worldService.UpsertPlayerDirective(string.IsNullOrWhiteSpace(directiveId) ? null : directiveId, request);
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); Post("/api/player-faction/directives");
return; Put("/api/player-faction/directives/{directiveId}");
AllowAnonymous();
} }
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(PlayerDirectiveCommandRequest request, CancellationToken cancellationToken)
} {
var directiveId = Route<string?>("directiveId");
var snapshot = worldService.UpsertPlayerDirective(string.IsNullOrWhiteSpace(directiveId) ? null : directiveId, request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -4,23 +4,23 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class UpsertPlayerPolicyHandler(WorldService worldService) : Endpoint<PlayerPolicyCommandRequest, PlayerFactionSnapshot> public sealed class UpsertPlayerPolicyHandler(WorldService worldService) : Endpoint<PlayerPolicyCommandRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Post("/api/player-faction/policies");
Put("/api/player-faction/policies/{policyId}");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerPolicyCommandRequest request, CancellationToken cancellationToken)
{
var policyId = Route<string?>("policyId");
var snapshot = worldService.UpsertPlayerPolicy(string.IsNullOrWhiteSpace(policyId) ? null : policyId, request);
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); Post("/api/player-faction/policies");
return; Put("/api/player-faction/policies/{policyId}");
AllowAnonymous();
} }
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(PlayerPolicyCommandRequest request, CancellationToken cancellationToken)
} {
var policyId = Route<string?>("policyId");
var snapshot = worldService.UpsertPlayerPolicy(string.IsNullOrWhiteSpace(policyId) ? null : policyId, request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -4,23 +4,23 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class UpsertPlayerProductionProgramHandler(WorldService worldService) : Endpoint<PlayerProductionProgramCommandRequest, PlayerFactionSnapshot> public sealed class UpsertPlayerProductionProgramHandler(WorldService worldService) : Endpoint<PlayerProductionProgramCommandRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Post("/api/player-faction/production-programs");
Put("/api/player-faction/production-programs/{productionProgramId}");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerProductionProgramCommandRequest request, CancellationToken cancellationToken)
{
var productionProgramId = Route<string?>("productionProgramId");
var snapshot = worldService.UpsertPlayerProductionProgram(string.IsNullOrWhiteSpace(productionProgramId) ? null : productionProgramId, request);
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); Post("/api/player-faction/production-programs");
return; Put("/api/player-faction/production-programs/{productionProgramId}");
AllowAnonymous();
} }
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(PlayerProductionProgramCommandRequest request, CancellationToken cancellationToken)
} {
var productionProgramId = Route<string?>("productionProgramId");
var snapshot = worldService.UpsertPlayerProductionProgram(string.IsNullOrWhiteSpace(productionProgramId) ? null : productionProgramId, request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -4,23 +4,23 @@ namespace SpaceGame.Api.PlayerFaction.Api;
public sealed class UpsertPlayerReinforcementPolicyHandler(WorldService worldService) : Endpoint<PlayerReinforcementPolicyCommandRequest, PlayerFactionSnapshot> public sealed class UpsertPlayerReinforcementPolicyHandler(WorldService worldService) : Endpoint<PlayerReinforcementPolicyCommandRequest, PlayerFactionSnapshot>
{ {
public override void Configure() public override void Configure()
{
Post("/api/player-faction/reinforcement-policies");
Put("/api/player-faction/reinforcement-policies/{reinforcementPolicyId}");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerReinforcementPolicyCommandRequest request, CancellationToken cancellationToken)
{
var reinforcementPolicyId = Route<string?>("reinforcementPolicyId");
var snapshot = worldService.UpsertPlayerReinforcementPolicy(string.IsNullOrWhiteSpace(reinforcementPolicyId) ? null : reinforcementPolicyId, request);
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); Post("/api/player-faction/reinforcement-policies");
return; Put("/api/player-faction/reinforcement-policies/{reinforcementPolicyId}");
AllowAnonymous();
} }
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(PlayerReinforcementPolicyCommandRequest request, CancellationToken cancellationToken)
} {
var reinforcementPolicyId = Route<string?>("reinforcementPolicyId");
var snapshot = worldService.UpsertPlayerReinforcementPolicy(string.IsNullOrWhiteSpace(reinforcementPolicyId) ? null : reinforcementPolicyId, request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -2,305 +2,305 @@ namespace SpaceGame.Api.PlayerFaction.Runtime;
public sealed class PlayerFactionRuntime public sealed class PlayerFactionRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public required string SovereignFactionId { get; set; } public required string SovereignFactionId { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public PlayerAssetRegistryRuntime AssetRegistry { get; set; } = new(); public PlayerAssetRegistryRuntime AssetRegistry { get; set; } = new();
public PlayerStrategicIntentRuntime StrategicIntent { get; set; } = new(); public PlayerStrategicIntentRuntime StrategicIntent { get; set; } = new();
public List<PlayerFleetRuntime> Fleets { get; } = []; public List<PlayerFleetRuntime> Fleets { get; } = [];
public List<PlayerTaskForceRuntime> TaskForces { get; } = []; public List<PlayerTaskForceRuntime> TaskForces { get; } = [];
public List<PlayerStationGroupRuntime> StationGroups { get; } = []; public List<PlayerStationGroupRuntime> StationGroups { get; } = [];
public List<PlayerEconomicRegionRuntime> EconomicRegions { get; } = []; public List<PlayerEconomicRegionRuntime> EconomicRegions { get; } = [];
public List<PlayerFrontRuntime> Fronts { get; } = []; public List<PlayerFrontRuntime> Fronts { get; } = [];
public List<PlayerReserveGroupRuntime> Reserves { get; } = []; public List<PlayerReserveGroupRuntime> Reserves { get; } = [];
public List<PlayerFactionPolicyRuntime> Policies { get; } = []; public List<PlayerFactionPolicyRuntime> Policies { get; } = [];
public List<PlayerAutomationPolicyRuntime> AutomationPolicies { get; } = []; public List<PlayerAutomationPolicyRuntime> AutomationPolicies { get; } = [];
public List<PlayerReinforcementPolicyRuntime> ReinforcementPolicies { get; } = []; public List<PlayerReinforcementPolicyRuntime> ReinforcementPolicies { get; } = [];
public List<PlayerProductionProgramRuntime> ProductionPrograms { get; } = []; public List<PlayerProductionProgramRuntime> ProductionPrograms { get; } = [];
public List<PlayerDirectiveRuntime> Directives { get; } = []; public List<PlayerDirectiveRuntime> Directives { get; } = [];
public List<PlayerAssignmentRuntime> Assignments { get; } = []; public List<PlayerAssignmentRuntime> Assignments { get; } = [];
public List<PlayerDecisionLogEntryRuntime> DecisionLog { get; } = []; public List<PlayerDecisionLogEntryRuntime> DecisionLog { get; } = [];
public List<PlayerAlertRuntime> Alerts { get; } = []; public List<PlayerAlertRuntime> Alerts { get; } = [];
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class PlayerAssetRegistryRuntime public sealed class PlayerAssetRegistryRuntime
{ {
public HashSet<string> ShipIds { get; } = new(StringComparer.Ordinal); public HashSet<string> ShipIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> StationIds { get; } = new(StringComparer.Ordinal); public HashSet<string> StationIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal); public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ClaimIds { get; } = new(StringComparer.Ordinal); public HashSet<string> ClaimIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ConstructionSiteIds { get; } = new(StringComparer.Ordinal); public HashSet<string> ConstructionSiteIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> PolicySetIds { get; } = new(StringComparer.Ordinal); public HashSet<string> PolicySetIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal); public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> FleetIds { get; } = new(StringComparer.Ordinal); public HashSet<string> FleetIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> TaskForceIds { get; } = new(StringComparer.Ordinal); public HashSet<string> TaskForceIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> StationGroupIds { get; } = new(StringComparer.Ordinal); public HashSet<string> StationGroupIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> EconomicRegionIds { get; } = new(StringComparer.Ordinal); public HashSet<string> EconomicRegionIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> FrontIds { get; } = new(StringComparer.Ordinal); public HashSet<string> FrontIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ReserveIds { get; } = new(StringComparer.Ordinal); public HashSet<string> ReserveIds { get; } = new(StringComparer.Ordinal);
} }
public sealed class PlayerStrategicIntentRuntime public sealed class PlayerStrategicIntentRuntime
{ {
public string StrategicPosture { get; set; } = "balanced"; public string StrategicPosture { get; set; } = "balanced";
public string EconomicPosture { get; set; } = "delegated"; public string EconomicPosture { get; set; } = "delegated";
public string MilitaryPosture { get; set; } = "layered-defense"; public string MilitaryPosture { get; set; } = "layered-defense";
public string LogisticsPosture { get; set; } = "stable"; public string LogisticsPosture { get; set; } = "stable";
public float DesiredReserveRatio { get; set; } = 0.2f; public float DesiredReserveRatio { get; set; } = 0.2f;
public bool AllowDelegatedCombatAutomation { get; set; } = true; public bool AllowDelegatedCombatAutomation { get; set; } = true;
public bool AllowDelegatedEconomicAutomation { get; set; } = true; public bool AllowDelegatedEconomicAutomation { get; set; } = true;
public string? Notes { get; set; } public string? Notes { get; set; }
} }
public sealed class PlayerFleetRuntime public sealed class PlayerFleetRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string Role { get; set; } = "general-purpose"; public string Role { get; set; } = "general-purpose";
public string? CommanderId { get; set; } public string? CommanderId { get; set; }
public string? FrontId { get; set; } public string? FrontId { get; set; }
public string? HomeSystemId { get; set; } public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; } public string? HomeStationId { get; set; }
public string? PolicyId { get; set; } public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; } public string? AutomationPolicyId { get; set; }
public string? ReinforcementPolicyId { get; set; } public string? ReinforcementPolicyId { get; set; }
public List<string> AssetIds { get; } = []; public List<string> AssetIds { get; } = [];
public List<string> TaskForceIds { get; } = []; public List<string> TaskForceIds { get; } = [];
public List<string> DirectiveIds { get; } = []; public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerTaskForceRuntime public sealed class PlayerTaskForceRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string Role { get; set; } = "task-force"; public string Role { get; set; } = "task-force";
public string? FleetId { get; set; } public string? FleetId { get; set; }
public string? CommanderId { get; set; } public string? CommanderId { get; set; }
public string? FrontId { get; set; } public string? FrontId { get; set; }
public string? PolicyId { get; set; } public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; } public string? AutomationPolicyId { get; set; }
public List<string> AssetIds { get; } = []; public List<string> AssetIds { get; } = [];
public List<string> DirectiveIds { get; } = []; public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerStationGroupRuntime public sealed class PlayerStationGroupRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string Role { get; set; } = "industrial-group"; public string Role { get; set; } = "industrial-group";
public string? EconomicRegionId { get; set; } public string? EconomicRegionId { get; set; }
public string? PolicyId { get; set; } public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; } public string? AutomationPolicyId { get; set; }
public List<string> StationIds { get; } = []; public List<string> StationIds { get; } = [];
public List<string> DirectiveIds { get; } = []; public List<string> DirectiveIds { get; } = [];
public List<string> FocusItemIds { get; } = []; public List<string> FocusItemIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerEconomicRegionRuntime public sealed class PlayerEconomicRegionRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string Role { get; set; } = "balanced-region"; public string Role { get; set; } = "balanced-region";
public string? SharedEconomicRegionId { get; set; } public string? SharedEconomicRegionId { get; set; }
public string? PolicyId { get; set; } public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; } public string? AutomationPolicyId { get; set; }
public List<string> SystemIds { get; } = []; public List<string> SystemIds { get; } = [];
public List<string> StationGroupIds { get; } = []; public List<string> StationGroupIds { get; } = [];
public List<string> DirectiveIds { get; } = []; public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerFrontRuntime public sealed class PlayerFrontRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public float Priority { get; set; } = 50f; public float Priority { get; set; } = 50f;
public string Posture { get; set; } = "hold"; public string Posture { get; set; } = "hold";
public string? SharedFrontLineId { get; set; } public string? SharedFrontLineId { get; set; }
public string? TargetFactionId { get; set; } public string? TargetFactionId { get; set; }
public List<string> SystemIds { get; } = []; public List<string> SystemIds { get; } = [];
public List<string> FleetIds { get; } = []; public List<string> FleetIds { get; } = [];
public List<string> ReserveIds { get; } = []; public List<string> ReserveIds { get; } = [];
public List<string> DirectiveIds { get; } = []; public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerReserveGroupRuntime public sealed class PlayerReserveGroupRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string Status { get; set; } = "ready"; public string Status { get; set; } = "ready";
public string ReserveKind { get; set; } = "military"; public string ReserveKind { get; set; } = "military";
public string? HomeSystemId { get; set; } public string? HomeSystemId { get; set; }
public string? PolicyId { get; set; } public string? PolicyId { get; set; }
public List<string> AssetIds { get; } = []; public List<string> AssetIds { get; } = [];
public List<string> FrontIds { get; } = []; public List<string> FrontIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerFactionPolicyRuntime public sealed class PlayerFactionPolicyRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string ScopeKind { get; set; } = "player-faction"; public string ScopeKind { get; set; } = "player-faction";
public string? ScopeId { get; set; } public string? ScopeId { get; set; }
public string? PolicySetId { get; set; } public string? PolicySetId { get; set; }
public bool AllowDelegatedCombat { get; set; } = true; public bool AllowDelegatedCombat { get; set; } = true;
public bool AllowDelegatedTrade { get; set; } = true; public bool AllowDelegatedTrade { get; set; } = true;
public float ReserveCreditsRatio { get; set; } = 0.2f; public float ReserveCreditsRatio { get; set; } = 0.2f;
public float ReserveMilitaryRatio { get; set; } = 0.2f; public float ReserveMilitaryRatio { get; set; } = 0.2f;
public string TradeAccessPolicy { get; set; } = "owner-and-allies"; public string TradeAccessPolicy { get; set; } = "owner-and-allies";
public string DockingAccessPolicy { get; set; } = "owner-and-allies"; public string DockingAccessPolicy { get; set; } = "owner-and-allies";
public string ConstructionAccessPolicy { get; set; } = "owner-only"; public string ConstructionAccessPolicy { get; set; } = "owner-only";
public string OperationalRangePolicy { get; set; } = "unrestricted"; public string OperationalRangePolicy { get; set; } = "unrestricted";
public string CombatEngagementPolicy { get; set; } = "defensive"; public string CombatEngagementPolicy { get; set; } = "defensive";
public bool AvoidHostileSystems { get; set; } = true; public bool AvoidHostileSystems { get; set; } = true;
public float FleeHullRatio { get; set; } = 0.35f; public float FleeHullRatio { get; set; } = 0.35f;
public HashSet<string> BlacklistedSystemIds { get; } = new(StringComparer.Ordinal); public HashSet<string> BlacklistedSystemIds { get; } = new(StringComparer.Ordinal);
public string? Notes { get; set; } public string? Notes { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerAutomationPolicyRuntime public sealed class PlayerAutomationPolicyRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string ScopeKind { get; set; } = "player-faction"; public string ScopeKind { get; set; } = "player-faction";
public string? ScopeId { get; set; } public string? ScopeId { get; set; }
public bool Enabled { get; set; } = true; public bool Enabled { get; set; } = true;
public string BehaviorKind { get; set; } = "idle"; public string BehaviorKind { get; set; } = "idle";
public bool UseOrders { get; set; } public bool UseOrders { get; set; }
public string? StagingOrderKind { get; set; } public string? StagingOrderKind { get; set; }
public int MaxSystemRange { get; set; } public int MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; } public bool KnownStationsOnly { get; set; }
public float Radius { get; set; } = 24f; public float Radius { get; set; } = 24f;
public float WaitSeconds { get; set; } = 3f; public float WaitSeconds { get; set; } = 3f;
public string? PreferredItemId { get; set; } public string? PreferredItemId { get; set; }
public string? Notes { get; set; } public string? Notes { get; set; }
public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = []; public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerReinforcementPolicyRuntime public sealed class PlayerReinforcementPolicyRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string ScopeKind { get; set; } = "player-faction"; public string ScopeKind { get; set; } = "player-faction";
public string? ScopeId { get; set; } public string? ScopeId { get; set; }
public string ShipKind { get; set; } = "military"; public string ShipKind { get; set; } = "military";
public int DesiredAssetCount { get; set; } public int DesiredAssetCount { get; set; }
public int MinimumReserveCount { get; set; } public int MinimumReserveCount { get; set; }
public bool AutoTransferReserves { get; set; } = true; public bool AutoTransferReserves { get; set; } = true;
public bool AutoQueueProduction { get; set; } = true; public bool AutoQueueProduction { get; set; } = true;
public string? SourceReserveId { get; set; } public string? SourceReserveId { get; set; }
public string? TargetFrontId { get; set; } public string? TargetFrontId { get; set; }
public string? Notes { get; set; } public string? Notes { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerProductionProgramRuntime public sealed class PlayerProductionProgramRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string Kind { get; set; } = "ship-production"; public string Kind { get; set; } = "ship-production";
public string? TargetShipKind { get; set; } public string? TargetShipKind { get; set; }
public string? TargetModuleId { get; set; } public string? TargetModuleId { get; set; }
public string? TargetItemId { get; set; } public string? TargetItemId { get; set; }
public int TargetCount { get; set; } public int TargetCount { get; set; }
public int CurrentCount { get; set; } public int CurrentCount { get; set; }
public string? StationGroupId { get; set; } public string? StationGroupId { get; set; }
public string? ReinforcementPolicyId { get; set; } public string? ReinforcementPolicyId { get; set; }
public string? Notes { get; set; } public string? Notes { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerDirectiveRuntime public sealed class PlayerDirectiveRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public string Kind { get; set; } = "hold"; public string Kind { get; set; } = "hold";
public string ScopeKind { get; set; } = "asset"; public string ScopeKind { get; set; } = "asset";
public string ScopeId { get; set; } = string.Empty; public string ScopeId { get; set; } = string.Empty;
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; } public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; } public Vector3? TargetPosition { get; set; }
public string? HomeSystemId { get; set; } public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; } public string? HomeStationId { get; set; }
public string? SourceStationId { get; set; } public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; } public string? DestinationStationId { get; set; }
public string BehaviorKind { get; set; } = "idle"; public string BehaviorKind { get; set; } = "idle";
public bool UseOrders { get; set; } public bool UseOrders { get; set; }
public string? StagingOrderKind { get; set; } public string? StagingOrderKind { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? PreferredNodeId { get; set; } public string? PreferredNodeId { get; set; }
public string? PreferredConstructionSiteId { get; set; } public string? PreferredConstructionSiteId { get; set; }
public string? PreferredModuleId { get; set; } public string? PreferredModuleId { get; set; }
public int Priority { get; set; } = 50; public int Priority { get; set; } = 50;
public float Radius { get; set; } = 24f; public float Radius { get; set; } = 24f;
public float WaitSeconds { get; set; } = 3f; public float WaitSeconds { get; set; } = 3f;
public int MaxSystemRange { get; set; } public int MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; } public bool KnownStationsOnly { get; set; }
public List<Vector3> PatrolPoints { get; } = []; public List<Vector3> PatrolPoints { get; } = [];
public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = []; public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = [];
public string? PolicyId { get; set; } public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; } public string? AutomationPolicyId { get; set; }
public string? Notes { get; set; } public string? Notes { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerAssignmentRuntime public sealed class PlayerAssignmentRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string AssetKind { get; set; } public required string AssetKind { get; set; }
public required string AssetId { get; set; } public required string AssetId { get; set; }
public string? FleetId { get; set; } public string? FleetId { get; set; }
public string? TaskForceId { get; set; } public string? TaskForceId { get; set; }
public string? StationGroupId { get; set; } public string? StationGroupId { get; set; }
public string? EconomicRegionId { get; set; } public string? EconomicRegionId { get; set; }
public string? FrontId { get; set; } public string? FrontId { get; set; }
public string? ReserveId { get; set; } public string? ReserveId { get; set; }
public string? DirectiveId { get; set; } public string? DirectiveId { get; set; }
public string? PolicyId { get; set; } public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; } public string? AutomationPolicyId { get; set; }
public string Role { get; set; } = "line"; public string Role { get; set; } = "line";
public string Status { get; set; } = "active"; public string Status { get; set; } = "active";
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerDecisionLogEntryRuntime public sealed class PlayerDecisionLogEntryRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public required string Summary { get; set; } public required string Summary { get; set; }
public string? RelatedEntityKind { get; set; } public string? RelatedEntityKind { get; set; }
public string? RelatedEntityId { get; set; } public string? RelatedEntityId { get; set; }
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }
public sealed class PlayerAlertRuntime public sealed class PlayerAlertRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; set; } public required string Kind { get; set; }
public required string Severity { get; set; } public required string Severity { get; set; }
public required string Summary { get; set; } public required string Summary { get; set; }
public string? AssetKind { get; set; } public string? AssetKind { get; set; }
public string? AssetId { get; set; } public string? AssetId { get; set; }
public string? RelatedDirectiveId { get; set; } public string? RelatedDirectiveId { get; set; }
public string Status { get; set; } = "open"; public string Status { get; set; } = "open";
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -6,13 +6,13 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors((options) => builder.Services.AddCors((options) =>
{ {
options.AddDefaultPolicy((policy) => options.AddDefaultPolicy((policy) =>
{ {
policy policy
.AllowAnyHeader() .AllowAnyHeader()
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyOrigin(); .AllowAnyOrigin();
}); });
}); });
builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSection("WorldGeneration")); builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSection("WorldGeneration"));
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation")); builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));

View File

@@ -2,276 +2,276 @@ namespace SpaceGame.Api.Shared.Runtime;
public enum SpatialNodeKind public enum SpatialNodeKind
{ {
Star, Star,
Planet, Planet,
Moon, Moon,
LagrangePoint, LagrangePoint,
} }
public enum WorkStatus public enum WorkStatus
{ {
Pending, Pending,
Active, Active,
Blocked, Blocked,
Completed, Completed,
Failed, Failed,
Interrupted, Interrupted,
} }
public enum OrderStatus public enum OrderStatus
{ {
Queued, Queued,
Active, Active,
Completed, Completed,
Cancelled, Cancelled,
Failed, Failed,
Interrupted, Interrupted,
} }
public enum AiPlanStatus public enum AiPlanStatus
{ {
Planned, Planned,
Running, Running,
Blocked, Blocked,
Completed, Completed,
Failed, Failed,
Interrupted, Interrupted,
} }
public enum AiPlanStepStatus public enum AiPlanStepStatus
{ {
Planned, Planned,
Running, Running,
Blocked, Blocked,
Completed, Completed,
Failed, Failed,
Interrupted, Interrupted,
} }
public enum AiPlanSourceKind public enum AiPlanSourceKind
{ {
Rule, Rule,
Order, Order,
DefaultBehavior, DefaultBehavior,
} }
public enum ShipState public enum ShipState
{ {
Idle, Idle,
Arriving, Arriving,
LocalFlight, LocalFlight,
SpoolingWarp, SpoolingWarp,
Warping, Warping,
SpoolingFtl, SpoolingFtl,
Ftl, Ftl,
CargoFull, CargoFull,
MiningApproach, MiningApproach,
Mining, Mining,
NodeDepleted, NodeDepleted,
AwaitingDock, AwaitingDock,
DockingApproach, DockingApproach,
Docking, Docking,
Docked, Docked,
Transferring, Transferring,
Loading, Loading,
Unloading, Unloading,
WaitingMaterials, WaitingMaterials,
ConstructionBlocked, ConstructionBlocked,
Constructing, Constructing,
DeliveringConstruction, DeliveringConstruction,
Blocked, Blocked,
Undocking, Undocking,
EngagingTarget, EngagingTarget,
HoldingPosition, HoldingPosition,
Fleeing, Fleeing,
} }
public static class SpaceLayerKinds public static class SpaceLayerKinds
{ {
public const string UniverseSpace = "universe-space"; public const string UniverseSpace = "universe-space";
public const string GalaxySpace = "galaxy-space"; public const string GalaxySpace = "galaxy-space";
public const string SystemSpace = "system-space"; public const string SystemSpace = "system-space";
public const string LocalSpace = "local-space"; public const string LocalSpace = "local-space";
} }
public static class MovementRegimeKinds public static class MovementRegimeKinds
{ {
public const string LocalFlight = "local-flight"; public const string LocalFlight = "local-flight";
public const string Warp = "warp"; public const string Warp = "warp";
public const string StargateTransit = "stargate-transit"; public const string StargateTransit = "stargate-transit";
public const string FtlTransit = "ftl-transit"; public const string FtlTransit = "ftl-transit";
} }
public static class CommanderKind public static class CommanderKind
{ {
public const string Faction = "faction"; public const string Faction = "faction";
public const string Station = "station"; public const string Station = "station";
public const string Ship = "ship"; public const string Ship = "ship";
public const string Fleet = "fleet"; public const string Fleet = "fleet";
public const string Sector = "sector"; public const string Sector = "sector";
public const string TaskGroup = "task-group"; public const string TaskGroup = "task-group";
} }
public static class ShipTaskKinds public static class ShipTaskKinds
{ {
public const string HoldPosition = "hold-position"; public const string HoldPosition = "hold-position";
public const string Travel = "travel"; public const string Travel = "travel";
public const string FollowTarget = "follow-target"; public const string FollowTarget = "follow-target";
public const string MineNode = "mine-node"; public const string MineNode = "mine-node";
public const string Dock = "dock"; public const string Dock = "dock";
public const string Undock = "undock"; public const string Undock = "undock";
public const string LoadCargo = "load-cargo"; public const string LoadCargo = "load-cargo";
public const string UnloadCargo = "unload-cargo"; public const string UnloadCargo = "unload-cargo";
public const string TransferCargoToShip = "transfer-cargo-to-ship"; public const string TransferCargoToShip = "transfer-cargo-to-ship";
public const string SalvageWreck = "salvage-wreck"; public const string SalvageWreck = "salvage-wreck";
public const string DeliverConstruction = "deliver-construction"; public const string DeliverConstruction = "deliver-construction";
public const string ConstructModule = "construct-module"; public const string ConstructModule = "construct-module";
public const string BuildConstructionSite = "build-construction-site"; public const string BuildConstructionSite = "build-construction-site";
public const string AttackTarget = "attack-target"; public const string AttackTarget = "attack-target";
public const string Flee = "flee"; public const string Flee = "flee";
public const string Wait = "wait"; public const string Wait = "wait";
} }
public static class ShipOrderKinds public static class ShipOrderKinds
{ {
public const string Move = "move"; public const string Move = "move";
public const string DockAtStation = "dock-at-station"; public const string DockAtStation = "dock-at-station";
public const string DockAndWait = "dock-and-wait"; public const string DockAndWait = "dock-and-wait";
public const string FlyAndWait = "fly-and-wait"; public const string FlyAndWait = "fly-and-wait";
public const string FlyToObject = "fly-to-object"; public const string FlyToObject = "fly-to-object";
public const string FollowShip = "follow-ship"; public const string FollowShip = "follow-ship";
public const string TradeRoute = "trade-route"; public const string TradeRoute = "trade-route";
public const string MineAndDeliver = "mine-and-deliver"; public const string MineAndDeliver = "mine-and-deliver";
public const string BuildAtSite = "build-at-site"; public const string BuildAtSite = "build-at-site";
public const string AttackTarget = "attack-target"; public const string AttackTarget = "attack-target";
public const string HoldPosition = "hold-position"; public const string HoldPosition = "hold-position";
public const string RepeatOrders = "repeat-orders"; public const string RepeatOrders = "repeat-orders";
public const string Flee = "flee"; public const string Flee = "flee";
} }
public static class ClaimStateKinds public static class ClaimStateKinds
{ {
public const string Placed = "placed"; public const string Placed = "placed";
public const string Activating = "activating"; public const string Activating = "activating";
public const string Active = "active"; public const string Active = "active";
public const string Destroyed = "destroyed"; public const string Destroyed = "destroyed";
} }
public static class ConstructionSiteStateKinds public static class ConstructionSiteStateKinds
{ {
public const string Planned = "planned"; public const string Planned = "planned";
public const string Active = "active"; public const string Active = "active";
public const string Paused = "paused"; public const string Paused = "paused";
public const string Completed = "completed"; public const string Completed = "completed";
public const string Destroyed = "destroyed"; public const string Destroyed = "destroyed";
} }
public static class MarketOrderKinds public static class MarketOrderKinds
{ {
public const string Buy = "buy"; public const string Buy = "buy";
public const string Sell = "sell"; public const string Sell = "sell";
} }
public static class MarketOrderStateKinds public static class MarketOrderStateKinds
{ {
public const string Open = "open"; public const string Open = "open";
public const string PartiallyFilled = "partially-filled"; public const string PartiallyFilled = "partially-filled";
public const string Filled = "filled"; public const string Filled = "filled";
public const string Cancelled = "cancelled"; public const string Cancelled = "cancelled";
} }
public static class SimulationEnumMappings public static class SimulationEnumMappings
{ {
public static string ToContractValue(this SpatialNodeKind kind) => kind switch public static string ToContractValue(this SpatialNodeKind kind) => kind switch
{ {
SpatialNodeKind.Star => "star", SpatialNodeKind.Star => "star",
SpatialNodeKind.Planet => "planet", SpatialNodeKind.Planet => "planet",
SpatialNodeKind.Moon => "moon", SpatialNodeKind.Moon => "moon",
SpatialNodeKind.LagrangePoint => "lagrange-point", SpatialNodeKind.LagrangePoint => "lagrange-point",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
}; };
public static string ToContractValue(this WorkStatus status) => status switch public static string ToContractValue(this WorkStatus status) => status switch
{ {
WorkStatus.Pending => "pending", WorkStatus.Pending => "pending",
WorkStatus.Active => "active", WorkStatus.Active => "active",
WorkStatus.Blocked => "blocked", WorkStatus.Blocked => "blocked",
WorkStatus.Completed => "completed", WorkStatus.Completed => "completed",
WorkStatus.Failed => "failed", WorkStatus.Failed => "failed",
WorkStatus.Interrupted => "interrupted", WorkStatus.Interrupted => "interrupted",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null), _ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
}; };
public static string ToContractValue(this OrderStatus status) => status switch public static string ToContractValue(this OrderStatus status) => status switch
{ {
OrderStatus.Queued => "queued", OrderStatus.Queued => "queued",
OrderStatus.Active => "active", OrderStatus.Active => "active",
OrderStatus.Completed => "completed", OrderStatus.Completed => "completed",
OrderStatus.Cancelled => "cancelled", OrderStatus.Cancelled => "cancelled",
OrderStatus.Failed => "failed", OrderStatus.Failed => "failed",
OrderStatus.Interrupted => "interrupted", OrderStatus.Interrupted => "interrupted",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null), _ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
}; };
public static string ToContractValue(this AiPlanStatus status) => status switch public static string ToContractValue(this AiPlanStatus status) => status switch
{ {
AiPlanStatus.Planned => "planned", AiPlanStatus.Planned => "planned",
AiPlanStatus.Running => "running", AiPlanStatus.Running => "running",
AiPlanStatus.Blocked => "blocked", AiPlanStatus.Blocked => "blocked",
AiPlanStatus.Completed => "completed", AiPlanStatus.Completed => "completed",
AiPlanStatus.Failed => "failed", AiPlanStatus.Failed => "failed",
AiPlanStatus.Interrupted => "interrupted", AiPlanStatus.Interrupted => "interrupted",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null), _ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
}; };
public static string ToContractValue(this AiPlanStepStatus status) => status switch public static string ToContractValue(this AiPlanStepStatus status) => status switch
{ {
AiPlanStepStatus.Planned => "planned", AiPlanStepStatus.Planned => "planned",
AiPlanStepStatus.Running => "running", AiPlanStepStatus.Running => "running",
AiPlanStepStatus.Blocked => "blocked", AiPlanStepStatus.Blocked => "blocked",
AiPlanStepStatus.Completed => "completed", AiPlanStepStatus.Completed => "completed",
AiPlanStepStatus.Failed => "failed", AiPlanStepStatus.Failed => "failed",
AiPlanStepStatus.Interrupted => "interrupted", AiPlanStepStatus.Interrupted => "interrupted",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null), _ => throw new ArgumentOutOfRangeException(nameof(status), status, null),
}; };
public static string ToContractValue(this AiPlanSourceKind kind) => kind switch public static string ToContractValue(this AiPlanSourceKind kind) => kind switch
{ {
AiPlanSourceKind.Rule => "rule", AiPlanSourceKind.Rule => "rule",
AiPlanSourceKind.Order => "order", AiPlanSourceKind.Order => "order",
AiPlanSourceKind.DefaultBehavior => "default-behavior", AiPlanSourceKind.DefaultBehavior => "default-behavior",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
}; };
public static string ToContractValue(this ShipState state) => state switch public static string ToContractValue(this ShipState state) => state switch
{ {
ShipState.Idle => "idle", ShipState.Idle => "idle",
ShipState.Arriving => "arriving", ShipState.Arriving => "arriving",
ShipState.LocalFlight => "local-flight", ShipState.LocalFlight => "local-flight",
ShipState.SpoolingWarp => "spooling-warp", ShipState.SpoolingWarp => "spooling-warp",
ShipState.Warping => "warping", ShipState.Warping => "warping",
ShipState.SpoolingFtl => "spooling-ftl", ShipState.SpoolingFtl => "spooling-ftl",
ShipState.Ftl => "ftl", ShipState.Ftl => "ftl",
ShipState.CargoFull => "cargo-full", ShipState.CargoFull => "cargo-full",
ShipState.MiningApproach => "mining-approach", ShipState.MiningApproach => "mining-approach",
ShipState.Mining => "mining", ShipState.Mining => "mining",
ShipState.NodeDepleted => "node-depleted", ShipState.NodeDepleted => "node-depleted",
ShipState.AwaitingDock => "awaiting-dock", ShipState.AwaitingDock => "awaiting-dock",
ShipState.DockingApproach => "docking-approach", ShipState.DockingApproach => "docking-approach",
ShipState.Docking => "docking", ShipState.Docking => "docking",
ShipState.Docked => "docked", ShipState.Docked => "docked",
ShipState.Transferring => "transferring", ShipState.Transferring => "transferring",
ShipState.Loading => "loading", ShipState.Loading => "loading",
ShipState.Unloading => "unloading", ShipState.Unloading => "unloading",
ShipState.WaitingMaterials => "waiting-materials", ShipState.WaitingMaterials => "waiting-materials",
ShipState.ConstructionBlocked => "construction-blocked", ShipState.ConstructionBlocked => "construction-blocked",
ShipState.Constructing => "constructing", ShipState.Constructing => "constructing",
ShipState.DeliveringConstruction => "delivering-construction", ShipState.DeliveringConstruction => "delivering-construction",
ShipState.Blocked => "blocked", ShipState.Blocked => "blocked",
ShipState.Undocking => "undocking", ShipState.Undocking => "undocking",
ShipState.EngagingTarget => "engaging-target", ShipState.EngagingTarget => "engaging-target",
ShipState.HoldingPosition => "holding-position", ShipState.HoldingPosition => "holding-position",
ShipState.Fleeing => "fleeing", ShipState.Fleeing => "fleeing",
_ => throw new ArgumentOutOfRangeException(nameof(state), state, null), _ => throw new ArgumentOutOfRangeException(nameof(state), state, null),
}; };
} }

View File

@@ -3,179 +3,179 @@ namespace SpaceGame.Api.Shared.Runtime;
internal static class SimulationRuntimeSupport internal static class SimulationRuntimeSupport
{ {
internal static bool HasShipCapabilities(ShipDefinition definition, params string[] capabilities) => internal static bool HasShipCapabilities(ShipDefinition definition, params string[] capabilities) =>
capabilities.All(cap => definition.Capabilities.Contains(cap, StringComparer.Ordinal)); capabilities.All(cap => definition.Capabilities.Contains(cap, StringComparer.Ordinal));
internal static int CountStationModules(StationRuntime station, string moduleId) => internal static int CountStationModules(StationRuntime station, string moduleId) =>
station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal)); station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal));
internal static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId) internal static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId)
{
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition))
{ {
return; if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition))
{
return;
}
station.Modules.Add(new StationModuleRuntime
{
Id = $"{station.Id}-module-{station.Modules.Count + 1}",
ModuleId = moduleId,
Health = definition.Hull,
MaxHealth = definition.Hull,
});
station.Radius = GetStationRadius(world, station);
} }
station.Modules.Add(new StationModuleRuntime internal static float GetStationRadius(SimulationWorld world, StationRuntime station)
{ {
Id = $"{station.Id}-module-{station.Modules.Count + 1}", var totalArea = station.Modules
ModuleId = moduleId, .Select(module => world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f)
Health = definition.Hull, .Sum();
MaxHealth = definition.Hull, return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f)));
});
station.Radius = GetStationRadius(world, station);
}
internal static float GetStationRadius(SimulationWorld world, StationRuntime station)
{
var totalArea = station.Modules
.Select(module => world.ModuleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f)
.Sum();
return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f)));
}
internal static float GetStationStorageCapacity(StationRuntime station, string storageClass)
{
var baseCapacity = storageClass switch
{
"manufactured" => 400f,
_ => 0f,
};
var bulkBays = CountStationModules(station, "module_arg_stor_solid_m_01");
var liquidTanks = CountStationModules(station, "module_arg_stor_liquid_m_01");
var containerBays = CountStationModules(station, "module_arg_stor_container_m_01");
var moduleCapacity = storageClass switch
{
"solid" => bulkBays * 1000f,
"liquid" => liquidTanks * 500f,
"container" => containerBays * 800f,
"manufactured" => containerBays * 200f,
_ => 0f,
};
return baseCapacity + moduleCapacity;
}
internal static int CountModules(IEnumerable<string> modules, string moduleId) =>
modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal));
internal static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
internal static void AddInventory(IDictionary<string, float> inventory, string itemId, float amount)
{
if (amount <= 0f)
{
return;
} }
inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId) + amount; internal static float GetStationStorageCapacity(StationRuntime station, string storageClass)
} {
var baseCapacity = storageClass switch
{
"manufactured" => 400f,
_ => 0f,
};
internal static float RemoveInventory(IDictionary<string, float> inventory, string itemId, float amount) var bulkBays = CountStationModules(station, "module_arg_stor_solid_m_01");
{ var liquidTanks = CountStationModules(station, "module_arg_stor_liquid_m_01");
var current = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId); var containerBays = CountStationModules(station, "module_arg_stor_container_m_01");
var removed = MathF.Min(current, amount);
var remaining = current - removed; var moduleCapacity = storageClass switch
if (remaining <= 0.001f) {
{ "solid" => bulkBays * 1000f,
inventory.Remove(itemId); "liquid" => liquidTanks * 500f,
} "container" => containerBays * 800f,
else "manufactured" => containerBays * 200f,
{ _ => 0f,
inventory[itemId] = remaining; };
return baseCapacity + moduleCapacity;
} }
return removed; internal static int CountModules(IEnumerable<string> modules, string moduleId) =>
} modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal));
internal static bool HasStationModules(StationRuntime station, params string[] modules) => internal static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal))); inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
internal static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node, SimulationWorld world) => internal static void AddInventory(IDictionary<string, float> inventory, string itemId, float amount)
HasShipCapabilities(ship.Definition, "mining")
&& world.ItemDefinitions.TryGetValue(node.ItemId, out var item)
&& string.Equals(item.CargoKind, ship.Definition.CargoKind, StringComparison.Ordinal);
internal static bool CanBuildClaimBeacon(ShipRuntime ship) =>
string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal);
internal static float ComputeWorkforceRatio(float population, float workforceRequired)
{
if (workforceRequired <= 0.01f)
{ {
return 1f; if (amount <= 0f)
{
return;
}
inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId) + amount;
} }
var staffedRatio = MathF.Min(1f, population / workforceRequired); internal static float RemoveInventory(IDictionary<string, float> inventory, string itemId, float amount)
return 0.1f + (0.9f * staffedRatio);
}
internal static string? GetStorageRequirement(string storageClass) =>
storageClass switch
{
"solid" => "module_arg_stor_solid_m_01",
"liquid" => "module_arg_stor_liquid_m_01",
_ => null,
};
internal static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
{
if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
{ {
return 0f; var current = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId);
var removed = MathF.Min(current, amount);
var remaining = current - removed;
if (remaining <= 0.001f)
{
inventory.Remove(itemId);
}
else
{
inventory[itemId] = remaining;
}
return removed;
} }
var storageClass = itemDefinition.CargoKind; internal static bool HasStationModules(StationRuntime station, params string[] modules) =>
var requiredModule = GetStorageRequirement(storageClass); modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal)));
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
internal static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node, SimulationWorld world) =>
HasShipCapabilities(ship.Definition, "mining")
&& world.ItemDefinitions.TryGetValue(node.ItemId, out var item)
&& string.Equals(item.CargoKind, ship.Definition.CargoKind, StringComparison.Ordinal);
internal static bool CanBuildClaimBeacon(ShipRuntime ship) =>
string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal);
internal static float ComputeWorkforceRatio(float population, float workforceRequired)
{ {
return 0f; if (workforceRequired <= 0.01f)
{
return 1f;
}
var staffedRatio = MathF.Min(1f, population / workforceRequired);
return 0.1f + (0.9f * staffedRatio);
} }
var capacity = GetStationStorageCapacity(station, storageClass); internal static string? GetStorageRequirement(string storageClass) =>
if (capacity <= 0.01f) storageClass switch
{
"solid" => "module_arg_stor_solid_m_01",
"liquid" => "module_arg_stor_liquid_m_01",
_ => null,
};
internal static float TryAddStationInventory(SimulationWorld world, StationRuntime station, string itemId, float amount)
{ {
return 0f; if (amount <= 0f || !world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition))
{
return 0f;
}
var storageClass = itemDefinition.CargoKind;
var requiredModule = GetStorageRequirement(storageClass);
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
{
return 0f;
}
var capacity = GetStationStorageCapacity(station, storageClass);
if (capacity <= 0.01f)
{
return 0f;
}
var used = station.Inventory
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass)
.Sum(entry => entry.Value);
var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used));
if (accepted <= 0.01f)
{
return 0f;
}
AddInventory(station.Inventory, itemId, accepted);
return accepted;
} }
var used = station.Inventory internal static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) =>
.Where(entry => world.ItemDefinitions.TryGetValue(entry.Key, out var definition) && definition.CargoKind == storageClass) recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount);
.Sum(entry => entry.Value);
var accepted = MathF.Min(amount, MathF.Max(0f, capacity - used)); internal static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) =>
if (accepted <= 0.01f) world.ConstructionSites.FirstOrDefault(site =>
string.Equals(site.StationId, stationId, StringComparison.Ordinal)
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
internal static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId)
{ {
return 0f; if (site.StationId is not null
&& world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station)
{
return GetInventoryAmount(station.Inventory, itemId);
}
return GetInventoryAmount(site.DeliveredItems, itemId);
} }
AddInventory(station.Inventory, itemId, accepted); internal static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) =>
return accepted; site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value);
}
internal static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) => internal static float GetShipCargoAmount(ShipRuntime ship) =>
recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount); ship.Inventory.Values.Sum();
internal static ConstructionSiteRuntime? GetConstructionSiteForStation(SimulationWorld world, string stationId) =>
world.ConstructionSites.FirstOrDefault(site =>
string.Equals(site.StationId, stationId, StringComparison.Ordinal)
&& site.State is not ConstructionSiteStateKinds.Completed and not ConstructionSiteStateKinds.Destroyed);
internal static float GetConstructionDeliveredAmount(SimulationWorld world, ConstructionSiteRuntime site, string itemId)
{
if (site.StationId is not null
&& world.Stations.FirstOrDefault(candidate => candidate.Id == site.StationId) is { } station)
{
return GetInventoryAmount(station.Inventory, itemId);
}
return GetInventoryAmount(site.DeliveredItems, itemId);
}
internal static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) =>
site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value);
internal static float GetShipCargoAmount(ShipRuntime ship) =>
ship.Inventory.Values.Sum();
} }

View File

@@ -4,36 +4,36 @@ namespace SpaceGame.Api.Ships.Api;
public sealed class EnqueueShipOrderHandler(WorldService worldService) : Endpoint<ShipOrderCommandRequest, ShipSnapshot> public sealed class EnqueueShipOrderHandler(WorldService worldService) : Endpoint<ShipOrderCommandRequest, ShipSnapshot>
{ {
public override void Configure() public override void Configure()
{
Post("/api/ships/{shipId}/orders");
AllowAnonymous();
}
public override async Task HandleAsync(ShipOrderCommandRequest request, CancellationToken cancellationToken)
{
var shipId = Route<string>("shipId");
if (string.IsNullOrWhiteSpace(shipId))
{ {
await SendNotFoundAsync(cancellationToken); Post("/api/ships/{shipId}/orders");
return; AllowAnonymous();
} }
try public override async Task HandleAsync(ShipOrderCommandRequest request, CancellationToken cancellationToken)
{ {
var snapshot = worldService.EnqueueShipOrder(shipId, request); var shipId = Route<string>("shipId");
if (snapshot is null) if (string.IsNullOrWhiteSpace(shipId))
{ {
await SendNotFoundAsync(cancellationToken); await SendNotFoundAsync(cancellationToken);
return; return;
} }
await SendOkAsync(snapshot, cancellationToken); try
{
var snapshot = worldService.EnqueueShipOrder(shipId, request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
catch (InvalidOperationException ex)
{
AddError(ex.Message);
await SendErrorsAsync(cancellation: cancellationToken);
}
} }
catch (InvalidOperationException ex)
{
AddError(ex.Message);
await SendErrorsAsync(cancellation: cancellationToken);
}
}
} }

View File

@@ -4,27 +4,27 @@ namespace SpaceGame.Api.Ships.Api;
public sealed class RemoveShipOrderRequest public sealed class RemoveShipOrderRequest
{ {
public string ShipId { get; set; } = string.Empty; public string ShipId { get; set; } = string.Empty;
public string OrderId { get; set; } = string.Empty; public string OrderId { get; set; } = string.Empty;
} }
public sealed class RemoveShipOrderHandler(WorldService worldService) : Endpoint<RemoveShipOrderRequest, ShipSnapshot> public sealed class RemoveShipOrderHandler(WorldService worldService) : Endpoint<RemoveShipOrderRequest, ShipSnapshot>
{ {
public override void Configure() public override void Configure()
{
Delete("/api/ships/{shipId}/orders/{orderId}");
AllowAnonymous();
}
public override async Task HandleAsync(RemoveShipOrderRequest request, CancellationToken cancellationToken)
{
var snapshot = worldService.RemoveShipOrder(request.ShipId, request.OrderId);
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); Delete("/api/ships/{shipId}/orders/{orderId}");
return; AllowAnonymous();
} }
await SendOkAsync(snapshot, cancellationToken); public override async Task HandleAsync(RemoveShipOrderRequest request, CancellationToken cancellationToken)
} {
var snapshot = worldService.RemoveShipOrder(request.ShipId, request.OrderId);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -4,28 +4,28 @@ namespace SpaceGame.Api.Ships.Api;
public sealed class UpdateShipDefaultBehaviorHandler(WorldService worldService) : Endpoint<ShipDefaultBehaviorCommandRequest, ShipSnapshot> public sealed class UpdateShipDefaultBehaviorHandler(WorldService worldService) : Endpoint<ShipDefaultBehaviorCommandRequest, ShipSnapshot>
{ {
public override void Configure() public override void Configure()
{
Put("/api/ships/{shipId}/default-behavior");
AllowAnonymous();
}
public override async Task HandleAsync(ShipDefaultBehaviorCommandRequest request, CancellationToken cancellationToken)
{
var shipId = Route<string>("shipId");
if (string.IsNullOrWhiteSpace(shipId))
{ {
await SendNotFoundAsync(cancellationToken); Put("/api/ships/{shipId}/default-behavior");
return; AllowAnonymous();
} }
var snapshot = worldService.UpdateShipDefaultBehavior(shipId, request); public override async Task HandleAsync(ShipDefaultBehaviorCommandRequest request, CancellationToken cancellationToken)
if (snapshot is null)
{ {
await SendNotFoundAsync(cancellationToken); var shipId = Route<string>("shipId");
return; if (string.IsNullOrWhiteSpace(shipId))
} {
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken); var snapshot = worldService.UpdateShipDefaultBehavior(shipId, request);
} if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(snapshot, cancellationToken);
}
} }

View File

@@ -2,156 +2,156 @@ namespace SpaceGame.Api.Ships.Runtime;
public sealed class ShipRuntime public sealed class ShipRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string SystemId { get; set; } public required string SystemId { get; set; }
public required ShipDefinition Definition { get; init; } public required ShipDefinition Definition { get; init; }
public required string FactionId { get; init; } public required string FactionId { get; init; }
public required Vector3 Position { get; set; } public required Vector3 Position { get; set; }
public required Vector3 TargetPosition { get; set; } public required Vector3 TargetPosition { get; set; }
public required ShipSpatialStateRuntime SpatialState { get; set; } public required ShipSpatialStateRuntime SpatialState { get; set; }
public Vector3 Velocity { get; set; } = Vector3.Zero; public Vector3 Velocity { get; set; } = Vector3.Zero;
public ShipState State { get; set; } = ShipState.Idle; public ShipState State { get; set; } = ShipState.Idle;
public required DefaultBehaviorRuntime DefaultBehavior { get; set; } public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
public List<ShipOrderRuntime> OrderQueue { get; } = []; public List<ShipOrderRuntime> OrderQueue { get; } = [];
public ShipPlanRuntime? ActivePlan { get; set; } public ShipPlanRuntime? ActivePlan { get; set; }
public required ShipSkillProfileRuntime Skills { get; set; } public required ShipSkillProfileRuntime Skills { get; set; }
public bool NeedsReplan { get; set; } = true; public bool NeedsReplan { get; set; } = true;
public float ReplanCooldownSeconds { get; set; } public float ReplanCooldownSeconds { get; set; }
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal); public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public string? DockedStationId { get; set; } public string? DockedStationId { get; set; }
public int? AssignedDockingPadIndex { get; set; } public int? AssignedDockingPadIndex { get; set; }
public string? CommanderId { get; set; } public string? CommanderId { get; set; }
public string? PolicySetId { get; set; } public string? PolicySetId { get; set; }
public string ControlSourceKind { get; set; } = "unassigned"; public string ControlSourceKind { get; set; } = "unassigned";
public string? ControlSourceId { get; set; } public string? ControlSourceId { get; set; }
public string? ControlReason { get; set; } public string? ControlReason { get; set; }
public string? LastReplanReason { get; set; } public string? LastReplanReason { get; set; }
public string? LastAccessFailureReason { get; set; } public string? LastAccessFailureReason { get; set; }
public float Health { get; set; } public float Health { get; set; }
public HashSet<string> KnownStationIds { get; } = new(StringComparer.Ordinal); public HashSet<string> KnownStationIds { get; } = new(StringComparer.Ordinal);
public List<string> History { get; } = []; public List<string> History { get; } = [];
public string LastSignature { get; set; } = string.Empty; public string LastSignature { get; set; } = string.Empty;
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class ShipSkillProfileRuntime public sealed class ShipSkillProfileRuntime
{ {
public int Navigation { get; set; } public int Navigation { get; set; }
public int Trade { get; set; } public int Trade { get; set; }
public int Mining { get; set; } public int Mining { get; set; }
public int Combat { get; set; } public int Combat { get; set; }
public int Construction { get; set; } public int Construction { get; set; }
} }
public sealed class ShipOrderRuntime public sealed class ShipOrderRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; init; } public required string Kind { get; init; }
public OrderStatus Status { get; set; } = OrderStatus.Queued; public OrderStatus Status { get; set; } = OrderStatus.Queued;
public int Priority { get; set; } public int Priority { get; set; }
public bool InterruptCurrentPlan { get; set; } = true; public bool InterruptCurrentPlan { get; set; } = true;
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
public string? Label { get; set; } public string? Label { get; set; }
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; } public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; } public Vector3? TargetPosition { get; set; }
public string? SourceStationId { get; set; } public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; } public string? DestinationStationId { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? NodeId { get; set; } public string? NodeId { get; set; }
public string? ConstructionSiteId { get; set; } public string? ConstructionSiteId { get; set; }
public string? ModuleId { get; set; } public string? ModuleId { get; set; }
public float WaitSeconds { get; set; } public float WaitSeconds { get; set; }
public float Radius { get; set; } public float Radius { get; set; }
public int? MaxSystemRange { get; set; } public int? MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; } public bool KnownStationsOnly { get; set; }
public string? FailureReason { get; set; } public string? FailureReason { get; set; }
} }
public sealed class DefaultBehaviorRuntime public sealed class DefaultBehaviorRuntime
{ {
public required string Kind { get; set; } public required string Kind { get; set; }
public string? HomeSystemId { get; set; } public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; } public string? HomeStationId { get; set; }
public string? AreaSystemId { get; set; } public string? AreaSystemId { get; set; }
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public string? PreferredItemId { get; set; } public string? PreferredItemId { get; set; }
public string? PreferredNodeId { get; set; } public string? PreferredNodeId { get; set; }
public string? PreferredConstructionSiteId { get; set; } public string? PreferredConstructionSiteId { get; set; }
public string? PreferredModuleId { get; set; } public string? PreferredModuleId { get; set; }
public Vector3? TargetPosition { get; set; } public Vector3? TargetPosition { get; set; }
public float WaitSeconds { get; set; } = 3f; public float WaitSeconds { get; set; } = 3f;
public float Radius { get; set; } = 24f; public float Radius { get; set; } = 24f;
public int MaxSystemRange { get; set; } public int MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; } public bool KnownStationsOnly { get; set; }
public List<Vector3> PatrolPoints { get; set; } = []; public List<Vector3> PatrolPoints { get; set; } = [];
public int PatrolIndex { get; set; } public int PatrolIndex { get; set; }
public List<ShipOrderTemplateRuntime> RepeatOrders { get; set; } = []; public List<ShipOrderTemplateRuntime> RepeatOrders { get; set; } = [];
public int RepeatIndex { get; set; } public int RepeatIndex { get; set; }
} }
public sealed class ShipOrderTemplateRuntime public sealed class ShipOrderTemplateRuntime
{ {
public required string Kind { get; init; } public required string Kind { get; init; }
public string? Label { get; set; } public string? Label { get; set; }
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; } public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; } public Vector3? TargetPosition { get; set; }
public string? SourceStationId { get; set; } public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; } public string? DestinationStationId { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? NodeId { get; set; } public string? NodeId { get; set; }
public string? ConstructionSiteId { get; set; } public string? ConstructionSiteId { get; set; }
public string? ModuleId { get; set; } public string? ModuleId { get; set; }
public float WaitSeconds { get; set; } public float WaitSeconds { get; set; }
public float Radius { get; set; } public float Radius { get; set; }
public int? MaxSystemRange { get; set; } public int? MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; } public bool KnownStationsOnly { get; set; }
} }
public sealed class ShipPlanRuntime public sealed class ShipPlanRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required AiPlanSourceKind SourceKind { get; init; } public required AiPlanSourceKind SourceKind { get; init; }
public required string SourceId { get; init; } public required string SourceId { get; init; }
public required string Kind { get; init; } public required string Kind { get; init; }
public required string Summary { get; set; } public required string Summary { get; set; }
public AiPlanStatus Status { get; set; } = AiPlanStatus.Planned; public AiPlanStatus Status { get; set; } = AiPlanStatus.Planned;
public int CurrentStepIndex { get; set; } public int CurrentStepIndex { get; set; }
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow; public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public string? InterruptReason { get; set; } public string? InterruptReason { get; set; }
public string? FailureReason { get; set; } public string? FailureReason { get; set; }
public List<ShipPlanStepRuntime> Steps { get; } = []; public List<ShipPlanStepRuntime> Steps { get; } = [];
} }
public sealed class ShipPlanStepRuntime public sealed class ShipPlanStepRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; init; } public required string Kind { get; init; }
public required string Summary { get; set; } public required string Summary { get; set; }
public AiPlanStepStatus Status { get; set; } = AiPlanStepStatus.Planned; public AiPlanStepStatus Status { get; set; } = AiPlanStepStatus.Planned;
public int CurrentSubTaskIndex { get; set; } public int CurrentSubTaskIndex { get; set; }
public string? BlockingReason { get; set; } public string? BlockingReason { get; set; }
public List<ShipSubTaskRuntime> SubTasks { get; } = []; public List<ShipSubTaskRuntime> SubTasks { get; } = [];
} }
public sealed class ShipSubTaskRuntime public sealed class ShipSubTaskRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string Kind { get; init; } public required string Kind { get; init; }
public required string Summary { get; set; } public required string Summary { get; set; }
public WorkStatus Status { get; set; } = WorkStatus.Pending; public WorkStatus Status { get; set; } = WorkStatus.Pending;
public string? TargetEntityId { get; set; } public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; } public string? TargetSystemId { get; set; }
public string? TargetNodeId { get; set; } public string? TargetNodeId { get; set; }
public Vector3? TargetPosition { get; set; } public Vector3? TargetPosition { get; set; }
public string? ItemId { get; set; } public string? ItemId { get; set; }
public string? ModuleId { get; set; } public string? ModuleId { get; set; }
public float Threshold { get; set; } public float Threshold { get; set; }
public float Amount { get; set; } public float Amount { get; set; }
public float ElapsedSeconds { get; set; } public float ElapsedSeconds { get; set; }
public float TotalSeconds { get; set; } public float TotalSeconds { get; set; }
public float Progress { get; set; } public float Progress { get; set; }
public string? BlockingReason { get; set; } public string? BlockingReason { get; set; }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3,146 +3,146 @@ namespace SpaceGame.Api.Simulation.Core;
public sealed class SimulationEngine public sealed class SimulationEngine
{ {
private readonly OrbitalSimulationOptions _orbitalSimulation; private readonly OrbitalSimulationOptions _orbitalSimulation;
private readonly OrbitalStateUpdater _orbitalStateUpdater; private readonly OrbitalStateUpdater _orbitalStateUpdater;
private readonly InfrastructureSimulationService _infrastructureSimulation; private readonly InfrastructureSimulationService _infrastructureSimulation;
private readonly GeopoliticalSimulationService _geopolitics; private readonly GeopoliticalSimulationService _geopolitics;
private readonly CommanderPlanningService _commanderPlanning; private readonly CommanderPlanningService _commanderPlanning;
private readonly PlayerFactionService _playerFaction; private readonly PlayerFactionService _playerFaction;
private readonly StationSimulationService _stationSimulation; private readonly StationSimulationService _stationSimulation;
private readonly StationLifecycleService _stationLifecycle; private readonly StationLifecycleService _stationLifecycle;
private readonly ShipAiService _shipAi; private readonly ShipAiService _shipAi;
private readonly SimulationProjectionService _projection; private readonly SimulationProjectionService _projection;
public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null) public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null)
{
_orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions();
_orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation);
_infrastructureSimulation = new InfrastructureSimulationService();
_geopolitics = new GeopoliticalSimulationService();
_commanderPlanning = new CommanderPlanningService();
_playerFaction = new PlayerFactionService();
_stationSimulation = new StationSimulationService();
_stationLifecycle = new StationLifecycleService(_stationSimulation);
_shipAi = new ShipAiService();
_projection = new SimulationProjectionService(_orbitalSimulation);
}
public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence)
{
var nowUtc = DateTimeOffset.UtcNow;
var events = new List<SimulationEventRecord>();
var simulationDeltaSeconds = deltaSeconds * MathF.Max(world.Balance.SimulationSpeedMultiplier, 0.01f);
world.GeneratedAtUtc = nowUtc;
world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
_orbitalStateUpdater.Update(world);
_infrastructureSimulation.UpdateClaims(world, events);
_infrastructureSimulation.UpdateConstructionSites(world, events);
_geopolitics.Update(world, simulationDeltaSeconds, events);
_commanderPlanning.UpdateCommanders(world, simulationDeltaSeconds, events);
_playerFaction.Update(world, simulationDeltaSeconds, events);
_stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events);
foreach (var ship in world.Ships.ToList())
{ {
if (ship.Health <= 0f) _orbitalSimulation = orbitalSimulation ?? new OrbitalSimulationOptions();
{ _orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation);
continue; _infrastructureSimulation = new InfrastructureSimulationService();
} _geopolitics = new GeopoliticalSimulationService();
_commanderPlanning = new CommanderPlanningService();
var previousPosition = ship.Position; _playerFaction = new PlayerFactionService();
_shipAi.UpdateShip(world, ship, simulationDeltaSeconds, events); _stationSimulation = new StationSimulationService();
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(simulationDeltaSeconds); _stationLifecycle = new StationLifecycleService(_stationSimulation);
_shipAi = new ShipAiService();
_projection = new SimulationProjectionService(_orbitalSimulation);
} }
_orbitalStateUpdater.SyncSpatialState(world); public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence)
CleanupDestroyedEntities(world, events);
return _projection.BuildDelta(world, sequence, events);
}
public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence) =>
_projection.BuildSnapshot(world, sequence);
public void PrimeDeltaBaseline(SimulationWorld world) =>
_projection.PrimeDeltaBaseline(world);
internal static float GetShipCargoAmount(ShipRuntime ship) =>
SimulationRuntimeSupport.GetShipCargoAmount(ship);
private static void CleanupDestroyedEntities(SimulationWorld world, ICollection<SimulationEventRecord> events)
{
foreach (var ship in world.Ships.Where(candidate => candidate.Health <= 0f).ToList())
{ {
CreateWreck(world, "ship", ship.Id, ship.SystemId, ship.Position, ship.Definition.CargoCapacity + (ship.Definition.MaxHealth * 0.08f)); var nowUtc = DateTimeOffset.UtcNow;
world.Ships.Remove(ship); var events = new List<SimulationEventRecord>();
if (ship.DockedStationId is not null && world.Stations.FirstOrDefault(station => station.Id == ship.DockedStationId) is { } dockedStation) var simulationDeltaSeconds = deltaSeconds * MathF.Max(world.Balance.SimulationSpeedMultiplier, 0.01f);
{ world.GeneratedAtUtc = nowUtc;
dockedStation.DockedShipIds.Remove(ship.Id);
dockedStation.DockingPadAssignments.Remove(ship.AssignedDockingPadIndex ?? -1);
}
if (world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId) is { } faction) world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
{
faction.ShipsLost += 1;
}
if (ship.CommanderId is not null && world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId) is { } commander) _orbitalStateUpdater.Update(world);
{ _infrastructureSimulation.UpdateClaims(world, events);
commander.IsAlive = false; _infrastructureSimulation.UpdateConstructionSites(world, events);
} _geopolitics.Update(world, simulationDeltaSeconds, events);
_commanderPlanning.UpdateCommanders(world, simulationDeltaSeconds, events);
_playerFaction.Update(world, simulationDeltaSeconds, events);
_stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events);
events.Add(new SimulationEventRecord("ship", ship.Id, "destroyed", $"{ship.Definition.Label} was destroyed.", DateTimeOffset.UtcNow)); foreach (var ship in world.Ships.ToList())
{
if (ship.Health <= 0f)
{
continue;
}
var previousPosition = ship.Position;
_shipAi.UpdateShip(world, ship, simulationDeltaSeconds, events);
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(simulationDeltaSeconds);
}
_orbitalStateUpdater.SyncSpatialState(world);
CleanupDestroyedEntities(world, events);
return _projection.BuildDelta(world, sequence, events);
} }
foreach (var station in world.Stations.Where(candidate => candidate.Health <= 0f).ToList()) public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence) =>
_projection.BuildSnapshot(world, sequence);
public void PrimeDeltaBaseline(SimulationWorld world) =>
_projection.PrimeDeltaBaseline(world);
internal static float GetShipCargoAmount(ShipRuntime ship) =>
SimulationRuntimeSupport.GetShipCargoAmount(ship);
private static void CleanupDestroyedEntities(SimulationWorld world, ICollection<SimulationEventRecord> events)
{ {
CreateWreck(world, "station", station.Id, station.SystemId, station.Position, station.MaxHealth * 0.12f); foreach (var ship in world.Ships.Where(candidate => candidate.Health <= 0f).ToList())
world.Stations.Remove(station); {
CreateWreck(world, "ship", ship.Id, ship.SystemId, ship.Position, ship.Definition.CargoCapacity + (ship.Definition.MaxHealth * 0.08f));
world.Ships.Remove(ship);
if (ship.DockedStationId is not null && world.Stations.FirstOrDefault(station => station.Id == ship.DockedStationId) is { } dockedStation)
{
dockedStation.DockedShipIds.Remove(ship.Id);
dockedStation.DockingPadAssignments.Remove(ship.AssignedDockingPadIndex ?? -1);
}
if (station.CelestialId is not null && world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId) is { } celestial) if (world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId) is { } faction)
{ {
celestial.OccupyingStructureId = null; faction.ShipsLost += 1;
} }
foreach (var claim in world.Claims.Where(candidate => candidate.CelestialId == station.CelestialId)) if (ship.CommanderId is not null && world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId) is { } commander)
{ {
claim.Health = 0f; commander.IsAlive = false;
claim.State = ClaimStateKinds.Destroyed; }
}
foreach (var site in world.ConstructionSites.Where(candidate => candidate.StationId == station.Id)) events.Add(new SimulationEventRecord("ship", ship.Id, "destroyed", $"{ship.Definition.Label} was destroyed.", DateTimeOffset.UtcNow));
{ }
site.State = ConstructionSiteStateKinds.Destroyed;
}
events.Add(new SimulationEventRecord("station", station.Id, "destroyed", $"{station.Label} was destroyed.", DateTimeOffset.UtcNow)); foreach (var station in world.Stations.Where(candidate => candidate.Health <= 0f).ToList())
} {
} CreateWreck(world, "station", station.Id, station.SystemId, station.Position, station.MaxHealth * 0.12f);
world.Stations.Remove(station);
private static void CreateWreck(SimulationWorld world, string sourceKind, string sourceEntityId, string systemId, Vector3 position, float amount) if (station.CelestialId is not null && world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId) is { } celestial)
{ {
var itemId = world.ItemDefinitions.ContainsKey("scrapmetal") celestial.OccupyingStructureId = null;
? "scrapmetal" }
: world.ItemDefinitions.ContainsKey("rawscrap")
? "rawscrap" foreach (var claim in world.Claims.Where(candidate => candidate.CelestialId == station.CelestialId))
: world.ItemDefinitions.Keys.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault(); {
if (itemId is null || amount <= 0.01f) claim.Health = 0f;
{ claim.State = ClaimStateKinds.Destroyed;
return; }
foreach (var site in world.ConstructionSites.Where(candidate => candidate.StationId == station.Id))
{
site.State = ConstructionSiteStateKinds.Destroyed;
}
events.Add(new SimulationEventRecord("station", station.Id, "destroyed", $"{station.Label} was destroyed.", DateTimeOffset.UtcNow));
}
} }
world.Wrecks.Add(new WreckRuntime private static void CreateWreck(SimulationWorld world, string sourceKind, string sourceEntityId, string systemId, Vector3 position, float amount)
{ {
Id = $"wreck-{sourceKind}-{sourceEntityId}", var itemId = world.ItemDefinitions.ContainsKey("scrapmetal")
SourceKind = sourceKind, ? "scrapmetal"
SourceEntityId = sourceEntityId, : world.ItemDefinitions.ContainsKey("rawscrap")
SystemId = systemId, ? "rawscrap"
Position = position, : world.ItemDefinitions.Keys.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault();
ItemId = itemId, if (itemId is null || amount <= 0.01f)
RemainingAmount = amount, {
MaxAmount = amount, return;
}); }
}
world.Wrecks.Add(new WreckRuntime
{
Id = $"wreck-{sourceKind}-{sourceEntityId}",
SourceKind = sourceKind,
SourceEntityId = sourceEntityId,
SystemId = systemId,
Position = position,
ItemId = itemId,
RemainingAmount = amount,
MaxAmount = amount,
});
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,35 +2,35 @@ namespace SpaceGame.Api.Stations.Runtime;
public sealed class ClaimRuntime public sealed class ClaimRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string FactionId { get; init; } public required string FactionId { get; init; }
public required string SystemId { get; init; } public required string SystemId { get; init; }
public required string CelestialId { get; init; } public required string CelestialId { get; init; }
public string? CommanderId { get; set; } public string? CommanderId { get; set; }
public DateTimeOffset PlacedAtUtc { get; init; } public DateTimeOffset PlacedAtUtc { get; init; }
public DateTimeOffset ActivatesAtUtc { get; set; } public DateTimeOffset ActivatesAtUtc { get; set; }
public string State { get; set; } = ClaimStateKinds.Placed; public string State { get; set; } = ClaimStateKinds.Placed;
public float Health { get; set; } public float Health { get; set; }
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class ConstructionSiteRuntime public sealed class ConstructionSiteRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string FactionId { get; init; } public required string FactionId { get; init; }
public required string SystemId { get; init; } public required string SystemId { get; init; }
public required string CelestialId { get; init; } public required string CelestialId { get; init; }
public required string TargetKind { get; init; } public required string TargetKind { get; init; }
public required string TargetDefinitionId { get; init; } public required string TargetDefinitionId { get; init; }
public string? BlueprintId { get; set; } public string? BlueprintId { get; set; }
public string? ClaimId { get; set; } public string? ClaimId { get; set; }
public string? StationId { get; set; } public string? StationId { get; set; }
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal); public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public Dictionary<string, float> RequiredItems { get; } = new(StringComparer.Ordinal); public Dictionary<string, float> RequiredItems { get; } = new(StringComparer.Ordinal);
public Dictionary<string, float> DeliveredItems { get; } = new(StringComparer.Ordinal); public Dictionary<string, float> DeliveredItems { get; } = new(StringComparer.Ordinal);
public HashSet<string> AssignedConstructorShipIds { get; } = new(StringComparer.Ordinal); public HashSet<string> AssignedConstructorShipIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal); public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
public float Progress { get; set; } public float Progress { get; set; }
public string State { get; set; } = ConstructionSiteStateKinds.Planned; public string State { get; set; } = ConstructionSiteStateKinds.Planned;
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }

View File

@@ -2,49 +2,49 @@ namespace SpaceGame.Api.Stations.Runtime;
public sealed class StationRuntime public sealed class StationRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string SystemId { get; init; } public required string SystemId { get; init; }
public required string Label { get; set; } public required string Label { get; set; }
public string Category { get; set; } = "station"; public string Category { get; set; } = "station";
public string Objective { get; set; } = "general"; public string Objective { get; set; } = "general";
public string Color { get; set; } = "#8df0d2"; public string Color { get; set; } = "#8df0d2";
public required Vector3 Position { get; set; } public required Vector3 Position { get; set; }
public float Radius { get; set; } = 24f; public float Radius { get; set; } = 24f;
public required string FactionId { get; init; } public required string FactionId { get; init; }
public string? CelestialId { get; set; } public string? CelestialId { get; set; }
public string? CommanderId { get; set; } public string? CommanderId { get; set; }
public string? PolicySetId { get; set; } public string? PolicySetId { get; set; }
public List<StationModuleRuntime> Modules { get; } = []; public List<StationModuleRuntime> Modules { get; } = [];
public float Health { get; set; } = 600f; public float Health { get; set; } = 600f;
public float MaxHealth { get; set; } = 600f; public float MaxHealth { get; set; } = 600f;
public IEnumerable<string> InstalledModules => Modules.Select((module) => module.ModuleId); public IEnumerable<string> InstalledModules => Modules.Select((module) => module.ModuleId);
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal); public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public Dictionary<string, float> ProductionLaneTimers { get; } = new(StringComparer.Ordinal); public Dictionary<string, float> ProductionLaneTimers { get; } = new(StringComparer.Ordinal);
public Dictionary<int, string> DockingPadAssignments { get; } = new(); public Dictionary<int, string> DockingPadAssignments { get; } = new();
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal); public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
public float Population { get; set; } public float Population { get; set; }
public float PopulationCapacity { get; set; } public float PopulationCapacity { get; set; }
public float WorkforceRequired { get; set; } public float WorkforceRequired { get; set; }
public float WorkforceEffectiveRatio { get; set; } = 0.1f; public float WorkforceEffectiveRatio { get; set; } = 0.1f;
public float PopulationGrowthProgress { get; set; } public float PopulationGrowthProgress { get; set; }
public float ShipProductionProgressSeconds { get; set; } public float ShipProductionProgressSeconds { get; set; }
public HashSet<string> DockedShipIds { get; } = []; public HashSet<string> DockedShipIds { get; } = [];
public ModuleConstructionRuntime? ActiveConstruction { get; set; } public ModuleConstructionRuntime? ActiveConstruction { get; set; }
public string LastDeltaSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty;
} }
public sealed class StationModuleRuntime public sealed class StationModuleRuntime
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string ModuleId { get; init; } public required string ModuleId { get; init; }
public float Health { get; set; } public float Health { get; set; }
public float MaxHealth { get; set; } public float MaxHealth { get; set; }
} }
public sealed class ModuleConstructionRuntime public sealed class ModuleConstructionRuntime
{ {
public required string ModuleId { get; init; } public required string ModuleId { get; init; }
public float ProgressSeconds { get; set; } public float ProgressSeconds { get; set; }
public float RequiredSeconds { get; init; } public float RequiredSeconds { get; init; }
public string AssignedConstructorShipId { get; set; } = string.Empty; public string AssignedConstructorShipId { get; set; } = string.Empty;
} }

View File

@@ -4,208 +4,208 @@ namespace SpaceGame.Api.Stations.Simulation;
internal sealed class StationLifecycleService internal sealed class StationLifecycleService
{ {
private const float WaterConsumptionPerWorkerPerSecond = 0.004f; private const float WaterConsumptionPerWorkerPerSecond = 0.004f;
private const float PopulationGrowthPerSecond = 0.012f; private const float PopulationGrowthPerSecond = 0.012f;
private const float PopulationAttritionPerSecond = 0.018f; private const float PopulationAttritionPerSecond = 0.018f;
private readonly StationSimulationService _stationSimulation; private readonly StationSimulationService _stationSimulation;
internal StationLifecycleService(StationSimulationService stationSimulation) internal StationLifecycleService(StationSimulationService stationSimulation)
{
_stationSimulation = stationSimulation;
}
internal void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
{
var factionPopulation = new Dictionary<string, float>(StringComparer.Ordinal);
foreach (var station in world.Stations)
{ {
UpdateStationPopulation(station, deltaSeconds, events); _stationSimulation = stationSimulation;
_stationSimulation.ReviewStationMarketOrders(world, station);
_stationSimulation.RunStationProduction(world, station, deltaSeconds, events);
factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population;
} }
foreach (var faction in world.Factions) internal void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
{ {
faction.PopulationTotal = GetInventoryAmount(factionPopulation, faction.Id); var factionPopulation = new Dictionary<string, float>(StringComparer.Ordinal);
} foreach (var station in world.Stations)
} {
UpdateStationPopulation(station, deltaSeconds, events);
_stationSimulation.ReviewStationMarketOrders(world, station);
_stationSimulation.RunStationProduction(world, station, deltaSeconds, events);
factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population;
}
private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events) foreach (var faction in world.Factions)
{ {
station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f); faction.PopulationTotal = GetInventoryAmount(factionPopulation, faction.Id);
}
var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater;
var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01");
station.PopulationCapacity = 40f + (habitatModules * 220f);
if (waterSatisfied)
{
if (habitatModules > 0 && station.Population < station.PopulationCapacity)
{
station.Population = MathF.Min(station.PopulationCapacity, station.Population + (PopulationGrowthPerSecond * deltaSeconds));
}
}
else if (station.Population > 0f)
{
var previous = station.Population;
station.Population = MathF.Max(0f, station.Population - (PopulationAttritionPerSecond * deltaSeconds));
if (MathF.Floor(previous) > MathF.Floor(station.Population))
{
events.Add(new SimulationEventRecord("station", station.Id, "population-loss", $"{station.Label} lost population due to support shortages.", DateTimeOffset.UtcNow));
}
} }
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired); private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
}
internal static float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection<SimulationEventRecord> events)
{
if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition))
{ {
return 0f; station.WorkforceRequired = MathF.Max(12f, station.Modules.Count * 14f);
var requiredWater = station.Population * WaterConsumptionPerWorkerPerSecond * deltaSeconds;
var consumedWater = RemoveInventory(station.Inventory, "water", requiredWater);
var waterSatisfied = requiredWater <= 0.01f || consumedWater + 0.001f >= requiredWater;
var habitatModules = CountModules(station.InstalledModules, "module_arg_hab_m_01");
station.PopulationCapacity = 40f + (habitatModules * 220f);
if (waterSatisfied)
{
if (habitatModules > 0 && station.Population < station.PopulationCapacity)
{
station.Population = MathF.Min(station.PopulationCapacity, station.Population + (PopulationGrowthPerSecond * deltaSeconds));
}
}
else if (station.Population > 0f)
{
var previous = station.Population;
station.Population = MathF.Max(0f, station.Population - (PopulationAttritionPerSecond * deltaSeconds));
if (MathF.Floor(previous) > MathF.Floor(station.Population))
{
events.Add(new SimulationEventRecord("station", station.Id, "population-loss", $"{station.Label} lost population due to support shortages.", DateTimeOffset.UtcNow));
}
}
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
} }
var spawnPosition = new Vector3(station.Position.X + GetStationRadius(world, station) + 32f, station.Position.Y, station.Position.Z); internal static float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection<SimulationEventRecord> events)
var ship = new ShipRuntime
{ {
Id = $"ship-{world.Ships.Count + 1}", if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition))
SystemId = station.SystemId, {
Definition = definition, return 0f;
FactionId = station.FactionId, }
Position = spawnPosition,
TargetPosition = spawnPosition, var spawnPosition = new Vector3(station.Position.X + GetStationRadius(world, station) + 32f, station.Position.Y, station.Position.Z);
SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition), var ship = new ShipRuntime
DefaultBehavior = CreateSpawnedShipBehavior(definition, station), {
Skills = WorldSeedingService.CreateSkills(definition), Id = $"ship-{world.Ships.Count + 1}",
Health = definition.MaxHealth, SystemId = station.SystemId,
Definition = definition,
FactionId = station.FactionId,
Position = spawnPosition,
TargetPosition = spawnPosition,
SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition),
DefaultBehavior = CreateSpawnedShipBehavior(definition, station),
Skills = WorldSeedingService.CreateSkills(definition),
Health = definition.MaxHealth,
};
world.Ships.Add(ship);
EnsureSpawnedShipCommander(world, station, ship);
if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction)
{
faction.ShipsBuilt += 1;
}
events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Label} launched {definition.Label}.", DateTimeOffset.UtcNow));
return 1f;
}
private static ShipSpatialStateRuntime CreateSpawnedShipSpatialState(StationRuntime station, Vector3 position) => new()
{
CurrentSystemId = station.SystemId,
SpaceLayer = SpaceLayerKinds.LocalSpace,
CurrentCelestialId = station.CelestialId,
LocalPosition = position,
SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight,
}; };
world.Ships.Add(ship); private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station)
EnsureSpawnedShipCommander(world, station, ship);
if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction)
{ {
faction.ShipsBuilt += 1; if (!string.Equals(definition.Kind, "military", StringComparison.Ordinal))
} {
return new DefaultBehaviorRuntime
{
Kind = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? "advanced-auto-trade" : "idle",
HomeSystemId = station.SystemId,
HomeStationId = station.Id,
MaxSystemRange = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? 2 : 0,
};
}
events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Label} launched {definition.Label}.", DateTimeOffset.UtcNow)); var patrolRadius = station.Radius + 90f;
return 1f; return new DefaultBehaviorRuntime
} {
Kind = "patrol",
private static ShipSpatialStateRuntime CreateSpawnedShipSpatialState(StationRuntime station, Vector3 position) => new() HomeSystemId = station.SystemId,
{ HomeStationId = station.Id,
CurrentSystemId = station.SystemId, AreaSystemId = station.SystemId,
SpaceLayer = SpaceLayerKinds.LocalSpace, PatrolPoints =
CurrentCelestialId = station.CelestialId, [
LocalPosition = position, new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z),
SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight,
};
private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station)
{
if (!string.Equals(definition.Kind, "military", StringComparison.Ordinal))
{
return new DefaultBehaviorRuntime
{
Kind = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? "advanced-auto-trade" : "idle",
HomeSystemId = station.SystemId,
HomeStationId = station.Id,
MaxSystemRange = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? 2 : 0,
};
}
var patrolRadius = station.Radius + 90f;
return new DefaultBehaviorRuntime
{
Kind = "patrol",
HomeSystemId = station.SystemId,
HomeStationId = station.Id,
AreaSystemId = station.SystemId,
PatrolPoints =
[
new Vector3(station.Position.X + patrolRadius, station.Position.Y, station.Position.Z),
new Vector3(station.Position.X, station.Position.Y, station.Position.Z + patrolRadius), new Vector3(station.Position.X, station.Position.Y, station.Position.Z + patrolRadius),
new Vector3(station.Position.X - patrolRadius, station.Position.Y, station.Position.Z), new Vector3(station.Position.X - patrolRadius, station.Position.Y, station.Position.Z),
new Vector3(station.Position.X, station.Position.Y, station.Position.Z - patrolRadius), new Vector3(station.Position.X, station.Position.Y, station.Position.Z - patrolRadius),
], ],
}; };
}
internal static void EnsureStationCommander(SimulationWorld world, StationRuntime station)
{
if (!string.IsNullOrWhiteSpace(station.CommanderId))
{
return;
} }
var factionCommander = world.Commanders.FirstOrDefault(candidate => internal static void EnsureStationCommander(SimulationWorld world, StationRuntime station)
string.Equals(candidate.Kind, CommanderKind.Faction, StringComparison.Ordinal)
&& string.Equals(candidate.FactionId, station.FactionId, StringComparison.Ordinal));
var faction = world.Factions.FirstOrDefault(candidate => string.Equals(candidate.Id, station.FactionId, StringComparison.Ordinal));
if (factionCommander is null || faction is null)
{ {
return; if (!string.IsNullOrWhiteSpace(station.CommanderId))
{
return;
}
var factionCommander = world.Commanders.FirstOrDefault(candidate =>
string.Equals(candidate.Kind, CommanderKind.Faction, StringComparison.Ordinal)
&& string.Equals(candidate.FactionId, station.FactionId, StringComparison.Ordinal));
var faction = world.Factions.FirstOrDefault(candidate => string.Equals(candidate.Id, station.FactionId, StringComparison.Ordinal));
if (factionCommander is null || faction is null)
{
return;
}
var commander = new CommanderRuntime
{
Id = $"commander-station-{station.Id}",
Kind = CommanderKind.Station,
FactionId = station.FactionId,
ParentCommanderId = factionCommander.Id,
ControlledEntityId = station.Id,
PolicySetId = factionCommander.PolicySetId,
Doctrine = "station-control",
Skills = new CommanderSkillProfileRuntime
{
Leadership = 3,
Coordination = Math.Clamp(3 + (station.Modules.Count / 8), 3, 5),
Strategy = 3,
},
};
station.CommanderId = commander.Id;
station.PolicySetId = factionCommander.PolicySetId;
factionCommander.SubordinateCommanderIds.Add(commander.Id);
faction.CommanderIds.Add(commander.Id);
world.Commanders.Add(commander);
} }
var commander = new CommanderRuntime private static void EnsureSpawnedShipCommander(SimulationWorld world, StationRuntime station, ShipRuntime ship)
{ {
Id = $"commander-station-{station.Id}", var factionCommander = world.Commanders.FirstOrDefault(candidate =>
Kind = CommanderKind.Station, string.Equals(candidate.Kind, CommanderKind.Faction, StringComparison.Ordinal)
FactionId = station.FactionId, && string.Equals(candidate.FactionId, station.FactionId, StringComparison.Ordinal));
ParentCommanderId = factionCommander.Id, var faction = world.Factions.FirstOrDefault(candidate => string.Equals(candidate.Id, station.FactionId, StringComparison.Ordinal));
ControlledEntityId = station.Id, if (factionCommander is null || faction is null)
PolicySetId = factionCommander.PolicySetId, {
Doctrine = "station-control", return;
Skills = new CommanderSkillProfileRuntime }
{
Leadership = 3,
Coordination = Math.Clamp(3 + (station.Modules.Count / 8), 3, 5),
Strategy = 3,
},
};
station.CommanderId = commander.Id; var commander = new CommanderRuntime
station.PolicySetId = factionCommander.PolicySetId; {
factionCommander.SubordinateCommanderIds.Add(commander.Id); Id = $"commander-ship-{ship.Id}",
faction.CommanderIds.Add(commander.Id); Kind = CommanderKind.Ship,
world.Commanders.Add(commander); FactionId = ship.FactionId,
} ParentCommanderId = factionCommander.Id,
ControlledEntityId = ship.Id,
PolicySetId = factionCommander.PolicySetId,
Doctrine = "ship-control",
Skills = new CommanderSkillProfileRuntime
{
Leadership = Math.Clamp((ship.Skills.Navigation + ship.Skills.Combat + 1) / 2, 2, 5),
Coordination = Math.Clamp((ship.Skills.Trade + ship.Skills.Mining + 1) / 2, 2, 5),
Strategy = Math.Clamp((ship.Skills.Combat + ship.Skills.Construction + 1) / 2, 2, 5),
},
};
private static void EnsureSpawnedShipCommander(SimulationWorld world, StationRuntime station, ShipRuntime ship) ship.CommanderId = commander.Id;
{ ship.PolicySetId = factionCommander.PolicySetId;
var factionCommander = world.Commanders.FirstOrDefault(candidate => factionCommander.SubordinateCommanderIds.Add(commander.Id);
string.Equals(candidate.Kind, CommanderKind.Faction, StringComparison.Ordinal) faction.CommanderIds.Add(commander.Id);
&& string.Equals(candidate.FactionId, station.FactionId, StringComparison.Ordinal)); world.Commanders.Add(commander);
var faction = world.Factions.FirstOrDefault(candidate => string.Equals(candidate.Id, station.FactionId, StringComparison.Ordinal));
if (factionCommander is null || faction is null)
{
return;
} }
var commander = new CommanderRuntime
{
Id = $"commander-ship-{ship.Id}",
Kind = CommanderKind.Ship,
FactionId = ship.FactionId,
ParentCommanderId = factionCommander.Id,
ControlledEntityId = ship.Id,
PolicySetId = factionCommander.PolicySetId,
Doctrine = "ship-control",
Skills = new CommanderSkillProfileRuntime
{
Leadership = Math.Clamp((ship.Skills.Navigation + ship.Skills.Combat + 1) / 2, 2, 5),
Coordination = Math.Clamp((ship.Skills.Trade + ship.Skills.Mining + 1) / 2, 2, 5),
Strategy = Math.Clamp((ship.Skills.Combat + ship.Skills.Construction + 1) / 2, 2, 5),
},
};
ship.CommanderId = commander.Id;
ship.PolicySetId = factionCommander.PolicySetId;
factionCommander.SubordinateCommanderIds.Add(commander.Id);
faction.CommanderIds.Add(commander.Id);
world.Commanders.Add(commander);
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,12 @@ namespace SpaceGame.Api.Universe.Api;
public sealed class GetBalanceHandler(WorldService worldService) : EndpointWithoutRequest public sealed class GetBalanceHandler(WorldService worldService) : EndpointWithoutRequest
{ {
public override void Configure() public override void Configure()
{ {
Get("/api/balance"); Get("/api/balance");
AllowAnonymous(); AllowAnonymous();
} }
public override Task HandleAsync(CancellationToken cancellationToken) => public override Task HandleAsync(CancellationToken cancellationToken) =>
SendOkAsync(worldService.GetBalance(), cancellationToken); SendOkAsync(worldService.GetBalance(), cancellationToken);
} }

View File

@@ -6,44 +6,44 @@ namespace SpaceGame.Api.Universe.Api;
public sealed class GetTelemetryHandler(TelemetryService telemetry, WorldService worldService) : EndpointWithoutRequest public sealed class GetTelemetryHandler(TelemetryService telemetry, WorldService worldService) : EndpointWithoutRequest
{ {
public override void Configure() public override void Configure()
{
Get("/api/telemetry");
AllowAnonymous();
}
public override Task HandleAsync(CancellationToken cancellationToken)
{
var status = worldService.GetStatus();
var connections = worldService.GetConnectionStats();
var uptime = telemetry.Uptime;
return SendOkAsync(new
{ {
process = new Get("/api/telemetry");
{ AllowAnonymous();
uptimeSeconds = uptime.TotalSeconds, }
cpuPercent = Math.Round(telemetry.CpuPercent, 1),
workingSetMb = Math.Round(telemetry.WorkingSetBytes / 1_048_576.0, 1), public override Task HandleAsync(CancellationToken cancellationToken)
gcMemoryMb = Math.Round(telemetry.GcMemoryBytes / 1_048_576.0, 1), {
threadCount = telemetry.ThreadCount, var status = worldService.GetStatus();
processorCount = Environment.ProcessorCount, var connections = worldService.GetConnectionStats();
}, var uptime = telemetry.Uptime;
simulation = new
{ return SendOkAsync(new
sequence = status.Sequence, {
connectedClients = connections.ConnectedClients, process = new
deltaHistoryCount = connections.DeltaHistoryCount, {
tickIntervalMs = 200, uptimeSeconds = uptime.TotalSeconds,
}, cpuPercent = Math.Round(telemetry.CpuPercent, 1),
runtime = new workingSetMb = Math.Round(telemetry.WorkingSetBytes / 1_048_576.0, 1),
{ gcMemoryMb = Math.Round(telemetry.GcMemoryBytes / 1_048_576.0, 1),
frameworkDescription = RuntimeInformation.FrameworkDescription, threadCount = telemetry.ThreadCount,
osDescription = RuntimeInformation.OSDescription, processorCount = Environment.ProcessorCount,
gcGen0 = GC.CollectionCount(0), },
gcGen1 = GC.CollectionCount(1), simulation = new
gcGen2 = GC.CollectionCount(2), {
}, sequence = status.Sequence,
}, cancellationToken); connectedClients = connections.ConnectedClients,
} deltaHistoryCount = connections.DeltaHistoryCount,
tickIntervalMs = 200,
},
runtime = new
{
frameworkDescription = RuntimeInformation.FrameworkDescription,
osDescription = RuntimeInformation.OSDescription,
gcGen0 = GC.CollectionCount(0),
gcGen1 = GC.CollectionCount(1),
gcGen2 = GC.CollectionCount(2),
},
}, cancellationToken);
}
} }

View File

@@ -4,12 +4,12 @@ namespace SpaceGame.Api.Universe.Api;
public sealed class GetWorldHandler(WorldService worldService) : EndpointWithoutRequest public sealed class GetWorldHandler(WorldService worldService) : EndpointWithoutRequest
{ {
public override void Configure() public override void Configure()
{ {
Get("/api/world"); Get("/api/world");
AllowAnonymous(); AllowAnonymous();
} }
public override Task HandleAsync(CancellationToken cancellationToken) => public override Task HandleAsync(CancellationToken cancellationToken) =>
SendOkAsync(worldService.GetSnapshot(), cancellationToken); SendOkAsync(worldService.GetSnapshot(), cancellationToken);
} }

View File

@@ -4,20 +4,20 @@ namespace SpaceGame.Api.Universe.Api;
public sealed class GetWorldHealthHandler(WorldService worldService) : EndpointWithoutRequest public sealed class GetWorldHealthHandler(WorldService worldService) : EndpointWithoutRequest
{ {
public override void Configure() public override void Configure()
{
Get("/api/world/health");
AllowAnonymous();
}
public override Task HandleAsync(CancellationToken cancellationToken)
{
var status = worldService.GetStatus();
return SendOkAsync(new
{ {
ok = true, Get("/api/world/health");
sequence = status.Sequence, AllowAnonymous();
generatedAtUtc = status.GeneratedAtUtc, }
}, cancellationToken);
} public override Task HandleAsync(CancellationToken cancellationToken)
{
var status = worldService.GetStatus();
return SendOkAsync(new
{
ok = true,
sequence = status.Sequence,
generatedAtUtc = status.GeneratedAtUtc,
}, cancellationToken);
}
} }

View File

@@ -4,12 +4,12 @@ namespace SpaceGame.Api.Universe.Api;
public sealed class ResetWorldHandler(WorldService worldService) : EndpointWithoutRequest public sealed class ResetWorldHandler(WorldService worldService) : EndpointWithoutRequest
{ {
public override void Configure() public override void Configure()
{ {
Post("/api/world/reset"); Post("/api/world/reset");
AllowAnonymous(); AllowAnonymous();
} }
public override Task HandleAsync(CancellationToken cancellationToken) => public override Task HandleAsync(CancellationToken cancellationToken) =>
SendOkAsync(worldService.Reset(), cancellationToken); SendOkAsync(worldService.Reset(), cancellationToken);
} }

View File

@@ -4,15 +4,15 @@ namespace SpaceGame.Api.Universe.Api;
public sealed class RootRedirectHandler : EndpointWithoutRequest public sealed class RootRedirectHandler : EndpointWithoutRequest
{ {
public override void Configure() public override void Configure()
{ {
Get("/"); Get("/");
AllowAnonymous(); AllowAnonymous();
} }
public override Task HandleAsync(CancellationToken cancellationToken) public override Task HandleAsync(CancellationToken cancellationToken)
{ {
HttpContext.Response.Redirect("/api/world"); HttpContext.Response.Redirect("/api/world");
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@@ -5,49 +5,49 @@ namespace SpaceGame.Api.Universe.Api;
public sealed class StreamWorldHandler(WorldService worldService) : EndpointWithoutRequest public sealed class StreamWorldHandler(WorldService worldService) : EndpointWithoutRequest
{ {
private static readonly JsonSerializerOptions SseJsonOptions = new(JsonSerializerDefaults.Web); private static readonly JsonSerializerOptions SseJsonOptions = new(JsonSerializerDefaults.Web);
public override void Configure() public override void Configure()
{
Get("/api/world/stream");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken cancellationToken)
{
HttpContext.Response.Headers.Append("Cache-Control", "no-cache");
HttpContext.Response.Headers.Append("Content-Type", "text/event-stream");
var afterSequenceRaw = HttpContext.Request.Query["afterSequence"].ToString();
_ = long.TryParse(afterSequenceRaw, out var afterSequence);
var scopeKind = HttpContext.Request.Query["scopeKind"].ToString();
if (string.IsNullOrWhiteSpace(scopeKind))
{ {
scopeKind = HttpContext.Request.Query["scope"].ToString(); Get("/api/world/stream");
AllowAnonymous();
} }
if (string.IsNullOrWhiteSpace(scopeKind)) public override async Task HandleAsync(CancellationToken cancellationToken)
{ {
scopeKind = "universe"; HttpContext.Response.Headers.Append("Cache-Control", "no-cache");
HttpContext.Response.Headers.Append("Content-Type", "text/event-stream");
var afterSequenceRaw = HttpContext.Request.Query["afterSequence"].ToString();
_ = long.TryParse(afterSequenceRaw, out var afterSequence);
var scopeKind = HttpContext.Request.Query["scopeKind"].ToString();
if (string.IsNullOrWhiteSpace(scopeKind))
{
scopeKind = HttpContext.Request.Query["scope"].ToString();
}
if (string.IsNullOrWhiteSpace(scopeKind))
{
scopeKind = "universe";
}
var systemId = HttpContext.Request.Query["systemId"].ToString();
var bubbleId = HttpContext.Request.Query["bubbleId"].ToString();
var scope = new ObserverScope(
scopeKind,
string.IsNullOrWhiteSpace(systemId) ? null : systemId,
string.IsNullOrWhiteSpace(bubbleId) ? null : bubbleId);
var stream = worldService.Subscribe(scope, afterSequence, cancellationToken);
await HttpContext.Response.WriteAsync(": connected\n\n", cancellationToken);
await HttpContext.Response.Body.FlushAsync(cancellationToken);
await foreach (var delta in stream.ReadAllAsync(cancellationToken))
{
var payload = JsonSerializer.Serialize(delta, SseJsonOptions);
await HttpContext.Response.WriteAsync($"event: world-delta\ndata: {payload}\n\n", cancellationToken);
await HttpContext.Response.Body.FlushAsync(cancellationToken);
}
} }
var systemId = HttpContext.Request.Query["systemId"].ToString();
var bubbleId = HttpContext.Request.Query["bubbleId"].ToString();
var scope = new ObserverScope(
scopeKind,
string.IsNullOrWhiteSpace(systemId) ? null : systemId,
string.IsNullOrWhiteSpace(bubbleId) ? null : bubbleId);
var stream = worldService.Subscribe(scope, afterSequence, cancellationToken);
await HttpContext.Response.WriteAsync(": connected\n\n", cancellationToken);
await HttpContext.Response.Body.FlushAsync(cancellationToken);
await foreach (var delta in stream.ReadAllAsync(cancellationToken))
{
var payload = JsonSerializer.Serialize(delta, SseJsonOptions);
await HttpContext.Response.WriteAsync($"event: world-delta\ndata: {payload}\n\n", cancellationToken);
await HttpContext.Response.Body.FlushAsync(cancellationToken);
}
}
} }

View File

@@ -6,15 +6,15 @@ namespace SpaceGame.Api.Universe.Api;
public sealed class UpdateBalanceHandler(WorldService worldService) : Endpoint<BalanceDefinition> public sealed class UpdateBalanceHandler(WorldService worldService) : Endpoint<BalanceDefinition>
{ {
public override void Configure() public override void Configure()
{ {
Put("/api/balance"); Put("/api/balance");
AllowAnonymous(); AllowAnonymous();
} }
public override Task HandleAsync(BalanceDefinition req, CancellationToken cancellationToken) public override Task HandleAsync(BalanceDefinition req, CancellationToken cancellationToken)
{ {
var applied = worldService.UpdateBalance(req); var applied = worldService.UpdateBalance(req);
return SendOkAsync(applied, cancellationToken); return SendOkAsync(applied, cancellationToken);
} }
} }

View File

@@ -5,292 +5,292 @@ namespace SpaceGame.Api.Universe.Scenario;
internal sealed class DataCatalogLoader(string dataRoot) internal sealed class DataCatalogLoader(string dataRoot)
{ {
private readonly JsonSerializerOptions _jsonOptions = new() private readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNameCaseInsensitive = true,
};
internal ScenarioCatalog LoadCatalog()
{
var authoredSystems = Read<List<SolarSystemDefinition>>("systems.json");
var scenario = Read<ScenarioDefinition>("scenario.json");
var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
var ships = Read<List<ShipDefinition>>("ships.json");
var items = NormalizeItems(Read<List<ItemDefinition>>("items.json"));
var balance = Read<BalanceDefinition>("balance.json");
var recipes = BuildRecipes(items, ships, modules);
var moduleRecipes = BuildModuleRecipes(modules);
var productionGraph = ProductionGraphBuilder.Build(items, recipes, modules);
return new ScenarioCatalog(
authoredSystems,
scenario,
balance,
modules.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
ships.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
items.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
recipes.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
moduleRecipes.ToDictionary(definition => definition.ModuleId, StringComparer.Ordinal),
productionGraph);
}
internal ScenarioDefinition NormalizeScenarioToAvailableSystems(
ScenarioDefinition scenario,
IReadOnlyList<string> availableSystemIds)
{
if (availableSystemIds.Count == 0)
{ {
return scenario; PropertyNameCaseInsensitive = true,
};
internal ScenarioCatalog LoadCatalog()
{
var authoredSystems = Read<List<SolarSystemDefinition>>("systems.json");
var scenario = Read<ScenarioDefinition>("scenario.json");
var modules = NormalizeModules(Read<List<ModuleDefinition>>("modules.json"));
var ships = Read<List<ShipDefinition>>("ships.json");
var items = NormalizeItems(Read<List<ItemDefinition>>("items.json"));
var balance = Read<BalanceDefinition>("balance.json");
var recipes = BuildRecipes(items, ships, modules);
var moduleRecipes = BuildModuleRecipes(modules);
var productionGraph = ProductionGraphBuilder.Build(items, recipes, modules);
return new ScenarioCatalog(
authoredSystems,
scenario,
balance,
modules.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
ships.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
items.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
recipes.ToDictionary(definition => definition.Id, StringComparer.Ordinal),
moduleRecipes.ToDictionary(definition => definition.ModuleId, StringComparer.Ordinal),
productionGraph);
} }
var fallbackSystemId = availableSystemIds.Contains("sol", StringComparer.Ordinal) internal ScenarioDefinition NormalizeScenarioToAvailableSystems(
? "sol" ScenarioDefinition scenario,
: availableSystemIds[0]; IReadOnlyList<string> availableSystemIds)
string ResolveSystemId(string systemId) =>
availableSystemIds.Contains(systemId, StringComparer.Ordinal) ? systemId : fallbackSystemId;
return new ScenarioDefinition
{ {
InitialStations = scenario.InitialStations if (availableSystemIds.Count == 0)
.Select(station => new InitialStationDefinition
{ {
SystemId = ResolveSystemId(station.SystemId), return scenario;
Label = station.Label, }
Color = station.Color,
Objective = station.Objective, var fallbackSystemId = availableSystemIds.Contains("sol", StringComparer.Ordinal)
StartingModules = station.StartingModules.ToList(), ? "sol"
FactionId = station.FactionId, : availableSystemIds[0];
PlanetIndex = station.PlanetIndex,
LagrangeSide = station.LagrangeSide, string ResolveSystemId(string systemId) =>
Position = station.Position?.ToArray(), availableSystemIds.Contains(systemId, StringComparer.Ordinal) ? systemId : fallbackSystemId;
})
.ToList(), return new ScenarioDefinition
ShipFormations = scenario.ShipFormations
.Select(formation => new ShipFormationDefinition
{ {
ShipId = formation.ShipId, InitialStations = scenario.InitialStations
Count = formation.Count, .Select(station => new InitialStationDefinition
Center = formation.Center.ToArray(), {
SystemId = ResolveSystemId(formation.SystemId), SystemId = ResolveSystemId(station.SystemId),
FactionId = formation.FactionId, Label = station.Label,
StartingInventory = new Dictionary<string, float>(formation.StartingInventory, StringComparer.Ordinal), Color = station.Color,
}) Objective = station.Objective,
.ToList(), StartingModules = station.StartingModules.ToList(),
PatrolRoutes = scenario.PatrolRoutes FactionId = station.FactionId,
.Select(route => new PatrolRouteDefinition PlanetIndex = station.PlanetIndex,
{ LagrangeSide = station.LagrangeSide,
SystemId = ResolveSystemId(route.SystemId), Position = station.Position?.ToArray(),
Points = route.Points.Select(point => point.ToArray()).ToList(), })
}) .ToList(),
.ToList(), ShipFormations = scenario.ShipFormations
MiningDefaults = new MiningDefaultsDefinition .Select(formation => new ShipFormationDefinition
{ {
NodeSystemId = ResolveSystemId(scenario.MiningDefaults.NodeSystemId), ShipId = formation.ShipId,
RefinerySystemId = ResolveSystemId(scenario.MiningDefaults.RefinerySystemId), Count = formation.Count,
}, Center = formation.Center.ToArray(),
}; SystemId = ResolveSystemId(formation.SystemId),
} FactionId = formation.FactionId,
StartingInventory = new Dictionary<string, float>(formation.StartingInventory, StringComparer.Ordinal),
})
.ToList(),
PatrolRoutes = scenario.PatrolRoutes
.Select(route => new PatrolRouteDefinition
{
SystemId = ResolveSystemId(route.SystemId),
Points = route.Points.Select(point => point.ToArray()).ToList(),
})
.ToList(),
MiningDefaults = new MiningDefaultsDefinition
{
NodeSystemId = ResolveSystemId(scenario.MiningDefaults.NodeSystemId),
RefinerySystemId = ResolveSystemId(scenario.MiningDefaults.RefinerySystemId),
},
};
}
private T Read<T>(string fileName) private T Read<T>(string fileName)
{
var path = Path.Combine(dataRoot, fileName);
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<T>(json, _jsonOptions)
?? throw new InvalidOperationException($"Unable to read {fileName}.");
}
private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) =>
modules
.Where(module => module.Construction is not null || module.Production.Count > 0)
.Select(module => new ModuleRecipeDefinition
{
ModuleId = module.Id,
Duration = module.Construction?.ProductionTime ?? module.Production[0].Time,
Inputs = (module.Construction?.Requirements ?? module.Production[0].Wares)
.Select(input => new RecipeInputDefinition
{
ItemId = input.ItemId,
Amount = input.Amount,
})
.ToList(),
})
.ToList();
private static List<RecipeDefinition> BuildRecipes(IEnumerable<ItemDefinition> items, IEnumerable<ShipDefinition> ships, IReadOnlyCollection<ModuleDefinition> modules)
{
var recipes = new List<RecipeDefinition>();
var preferredProducerByItemId = modules
.Where(module => module.Products.Count > 0)
.GroupBy(module => module.Products[0], StringComparer.Ordinal)
.ToDictionary(
group => group.Key,
group => group.OrderBy(module => module.Id, StringComparer.Ordinal).First().Id,
StringComparer.Ordinal);
foreach (var item in items)
{ {
if (item.Production.Count > 0) var path = Path.Combine(dataRoot, fileName);
{ var json = File.ReadAllText(path);
foreach (var production in item.Production) return JsonSerializer.Deserialize<T>(json, _jsonOptions)
?? throw new InvalidOperationException($"Unable to read {fileName}.");
}
private static List<ModuleRecipeDefinition> BuildModuleRecipes(IEnumerable<ModuleDefinition> modules) =>
modules
.Where(module => module.Construction is not null || module.Production.Count > 0)
.Select(module => new ModuleRecipeDefinition
{ {
recipes.Add(new RecipeDefinition ModuleId = module.Id,
{ Duration = module.Construction?.ProductionTime ?? module.Production[0].Time,
Id = $"{item.Id}-{production.Method}-production", Inputs = (module.Construction?.Requirements ?? module.Production[0].Wares)
Label = production.Name == "Universal" ? item.Name : $"{item.Name} ({production.Name})", .Select(input => new RecipeInputDefinition
FacilityCategory = InferFacilityCategory(item), {
Duration = production.Time,
Priority = InferRecipePriority(item),
RequiredModules = InferRequiredModules(item, preferredProducerByItemId),
Inputs = production.Wares
.Select(input => new RecipeInputDefinition
{
ItemId = input.ItemId, ItemId = input.ItemId,
Amount = input.Amount, Amount = input.Amount,
}) })
.ToList(), .ToList(),
Outputs = })
[ .ToList();
new RecipeOutputDefinition
private static List<RecipeDefinition> BuildRecipes(IEnumerable<ItemDefinition> items, IEnumerable<ShipDefinition> ships, IReadOnlyCollection<ModuleDefinition> modules)
{
var recipes = new List<RecipeDefinition>();
var preferredProducerByItemId = modules
.Where(module => module.Products.Count > 0)
.GroupBy(module => module.Products[0], StringComparer.Ordinal)
.ToDictionary(
group => group.Key,
group => group.OrderBy(module => module.Id, StringComparer.Ordinal).First().Id,
StringComparer.Ordinal);
foreach (var item in items)
{
if (item.Production.Count > 0)
{
foreach (var production in item.Production)
{
recipes.Add(new RecipeDefinition
{
Id = $"{item.Id}-{production.Method}-production",
Label = production.Name == "Universal" ? item.Name : $"{item.Name} ({production.Name})",
FacilityCategory = InferFacilityCategory(item),
Duration = production.Time,
Priority = InferRecipePriority(item),
RequiredModules = InferRequiredModules(item, preferredProducerByItemId),
Inputs = production.Wares
.Select(input => new RecipeInputDefinition
{
ItemId = input.ItemId,
Amount = input.Amount,
})
.ToList(),
Outputs =
[
new RecipeOutputDefinition
{ {
ItemId = item.Id, ItemId = item.Id,
Amount = production.Amount, Amount = production.Amount,
}, },
], ],
}); });
} }
continue; continue;
} }
if (item.Construction is null) if (item.Construction is null)
{ {
continue; continue;
} }
recipes.Add(new RecipeDefinition recipes.Add(new RecipeDefinition
{ {
Id = item.Construction.RecipeId ?? $"{item.Id}-production", Id = item.Construction.RecipeId ?? $"{item.Id}-production",
Label = item.Name, Label = item.Name,
FacilityCategory = item.Construction.FacilityCategory, FacilityCategory = item.Construction.FacilityCategory,
Duration = item.Construction.CycleTime, Duration = item.Construction.CycleTime,
Priority = item.Construction.Priority, Priority = item.Construction.Priority,
RequiredModules = item.Construction.RequiredModules.ToList(), RequiredModules = item.Construction.RequiredModules.ToList(),
Inputs = item.Construction.Requirements Inputs = item.Construction.Requirements
.Select(input => new RecipeInputDefinition .Select(input => new RecipeInputDefinition
{ {
ItemId = input.ItemId, ItemId = input.ItemId,
Amount = input.Amount, Amount = input.Amount,
}) })
.ToList(), .ToList(),
Outputs = Outputs =
[ [
new RecipeOutputDefinition new RecipeOutputDefinition
{ {
ItemId = item.Id, ItemId = item.Id,
Amount = item.Construction.BatchSize, Amount = item.Construction.BatchSize,
}, },
], ],
}); });
}
foreach (var ship in ships)
{
if (ship.Construction is null)
{
continue;
}
recipes.Add(new RecipeDefinition
{
Id = ship.Construction.RecipeId ?? $"{ship.Id}-construction",
Label = $"{ship.Label} Construction",
FacilityCategory = ship.Construction.FacilityCategory,
Duration = ship.Construction.CycleTime,
Priority = ship.Construction.Priority,
RequiredModules = ship.Construction.RequiredModules.ToList(),
Inputs = ship.Construction.Requirements
.Select(input => new RecipeInputDefinition
{
ItemId = input.ItemId,
Amount = input.Amount,
})
.ToList(),
ShipOutputId = ship.Id,
});
}
return recipes;
} }
foreach (var ship in ships) private static string InferFacilityCategory(ItemDefinition item) =>
{ item.Group switch
if (ship.Construction is null)
{ {
continue; "agricultural" or "food" or "pharmaceutical" or "water" => "farm",
} _ => "station",
};
recipes.Add(new RecipeDefinition private static List<string> InferRequiredModules(ItemDefinition item, IReadOnlyDictionary<string, string> preferredProducerByItemId)
{ {
Id = ship.Construction.RecipeId ?? $"{ship.Id}-construction", if (preferredProducerByItemId.TryGetValue(item.Id, out var moduleId))
Label = $"{ship.Label} Construction", {
FacilityCategory = ship.Construction.FacilityCategory, return [moduleId];
Duration = ship.Construction.CycleTime, }
Priority = ship.Construction.Priority,
RequiredModules = ship.Construction.RequiredModules.ToList(), return [];
Inputs = ship.Construction.Requirements
.Select(input => new RecipeInputDefinition
{
ItemId = input.ItemId,
Amount = input.Amount,
})
.ToList(),
ShipOutputId = ship.Id,
});
} }
return recipes; private static int InferRecipePriority(ItemDefinition item) =>
} item.Group switch
{
"energy" => 140,
"water" => 130,
"food" => 120,
"agricultural" => 110,
"refined" => 100,
"hightech" => 90,
"shiptech" => 80,
"pharmaceutical" => 70,
_ => 60,
};
private static string InferFacilityCategory(ItemDefinition item) => private static List<ItemDefinition> NormalizeItems(List<ItemDefinition> items)
item.Group switch
{ {
"agricultural" or "food" or "pharmaceutical" or "water" => "farm", foreach (var item in items)
_ => "station", {
}; if (string.IsNullOrWhiteSpace(item.Type))
{
item.Type = string.IsNullOrWhiteSpace(item.Group) ? "material" : item.Group;
}
}
private static List<string> InferRequiredModules(ItemDefinition item, IReadOnlyDictionary<string, string> preferredProducerByItemId) return items;
{
if (preferredProducerByItemId.TryGetValue(item.Id, out var moduleId))
{
return [moduleId];
} }
return []; private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules)
}
private static int InferRecipePriority(ItemDefinition item) =>
item.Group switch
{ {
"energy" => 140, foreach (var module in modules)
"water" => 130, {
"food" => 120, if (module.Products.Count == 0 && !string.IsNullOrWhiteSpace(module.Product))
"agricultural" => 110, {
"refined" => 100, module.Products = [module.Product];
"hightech" => 90, }
"shiptech" => 80,
"pharmaceutical" => 70,
_ => 60,
};
private static List<ItemDefinition> NormalizeItems(List<ItemDefinition> items) if (string.IsNullOrWhiteSpace(module.ProductionMode))
{ {
foreach (var item in items) module.ProductionMode = string.Equals(module.Type, "buildmodule", StringComparison.Ordinal)
{ ? "commanded"
if (string.IsNullOrWhiteSpace(item.Type)) : "passive";
{ }
item.Type = string.IsNullOrWhiteSpace(item.Group) ? "material" : item.Group;
} if (module.WorkforceNeeded <= 0f)
{
module.WorkforceNeeded = module.WorkForce?.Max ?? 0f;
}
}
return modules;
} }
return items;
}
private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules)
{
foreach (var module in modules)
{
if (module.Products.Count == 0 && !string.IsNullOrWhiteSpace(module.Product))
{
module.Products = [module.Product];
}
if (string.IsNullOrWhiteSpace(module.ProductionMode))
{
module.ProductionMode = string.Equals(module.Type, "buildmodule", StringComparison.Ordinal)
? "commanded"
: "passive";
}
if (module.WorkforceNeeded <= 0f)
{
module.WorkforceNeeded = module.WorkForce?.Max ?? 0f;
}
}
return modules;
}
} }
internal sealed record ScenarioCatalog( internal sealed record ScenarioCatalog(

View File

@@ -3,18 +3,18 @@ namespace SpaceGame.Api.Universe.Scenario;
internal static class LoaderSupport internal static class LoaderSupport
{ {
internal const string DefaultFactionId = "sol-dominion"; internal const string DefaultFactionId = "sol-dominion";
internal const int WorldSeed = 1; internal const int WorldSeed = 1;
internal const float MinimumFactionCredits = 0f; internal const float MinimumFactionCredits = 0f;
internal const float MinimumRefineryOre = 0f; internal const float MinimumRefineryOre = 0f;
internal const float MinimumRefineryStock = 0f; internal const float MinimumRefineryStock = 0f;
internal const float MinimumShipyardStock = 0f; internal const float MinimumShipyardStock = 0f;
internal const float MinimumSystemSeparation = 3.2f; internal const float MinimumSystemSeparation = 3.2f;
internal const float LocalSpaceRadius = 10_000f; internal const float LocalSpaceRadius = 10_000f;
internal static readonly string[] GeneratedSystemNames = internal static readonly string[] GeneratedSystemNames =
[ [
"Aquila Verge", "Aquila Verge",
"Orion Fold", "Orion Fold",
"Draco Span", "Draco Span",
"Lyra Shoal", "Lyra Shoal",
@@ -48,9 +48,9 @@ internal static class LoaderSupport
"Telescopium Strand", "Telescopium Strand",
]; ];
internal static readonly StarProfile[] StarProfiles = internal static readonly StarProfile[] StarProfiles =
[ [
new("main-sequence", "#ffd27a", "#ffb14a", 696340f), new("main-sequence", "#ffd27a", "#ffb14a", 696340f),
new("blue-white", "#9dc6ff", "#66a0ff", 930000f), new("blue-white", "#9dc6ff", "#66a0ff", 930000f),
new("white-dwarf", "#f1f5ff", "#b8caff", 12000f), new("white-dwarf", "#f1f5ff", "#b8caff", 12000f),
new("brown-dwarf", "#b97d56", "#8a5438", 70000f), new("brown-dwarf", "#b97d56", "#8a5438", 70000f),
@@ -59,9 +59,9 @@ internal static class LoaderSupport
new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f), new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f),
]; ];
internal static readonly PlanetProfile[] PlanetProfiles = internal static readonly PlanetProfile[] PlanetProfiles =
[ [
new("barren", "sphere", "#bca48f", 2800f, 0.22f, 0, false), new("barren", "sphere", "#bca48f", 2800f, 0.22f, 0, false),
new("terrestrial", "sphere", "#58a36c", 6400f, 0.28f, 1, false), new("terrestrial", "sphere", "#58a36c", 6400f, 0.28f, 1, false),
new("oceanic", "sphere", "#4f84c4", 7000f, 0.30f, 2, false), new("oceanic", "sphere", "#4f84c4", 7000f, 0.30f, 2, false),
new("desert", "sphere", "#d4a373", 5200f, 0.26f, 0, false), new("desert", "sphere", "#d4a373", 5200f, 0.26f, 0, false),
@@ -71,85 +71,85 @@ internal static class LoaderSupport
new("lava", "sphere", "#db6846", 3200f, 0.20f, 0, false), new("lava", "sphere", "#db6846", 3200f, 0.20f, 0, false),
]; ];
internal static Vector3 ToVector(float[] values) => new(values[0], values[1], values[2]); internal static Vector3 ToVector(float[] values) => new(values[0], values[1], values[2]);
internal static Vector3 NormalizeScenarioPoint(SystemRuntime system, float[] values) internal static Vector3 NormalizeScenarioPoint(SystemRuntime system, float[] values)
{
var raw = ToVector(values);
var relativeToSystem = new Vector3(
raw.X - system.Position.X,
raw.Y - system.Position.Y,
raw.Z - system.Position.Z);
return relativeToSystem.LengthSquared() < raw.LengthSquared()
? relativeToSystem
: raw;
}
internal static bool HasInstalledModules(StationRuntime station, params string[] modules) =>
modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal)));
internal static bool HasCapabilities(ShipDefinition definition, params string[] capabilities) =>
capabilities.All(capability => definition.Capabilities.Contains(capability, StringComparer.Ordinal));
internal static void AddStationModule(StationRuntime station, IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, string moduleId)
{
if (!moduleDefinitions.TryGetValue(moduleId, out var definition))
{ {
return; var raw = ToVector(values);
var relativeToSystem = new Vector3(
raw.X - system.Position.X,
raw.Y - system.Position.Y,
raw.Z - system.Position.Z);
return relativeToSystem.LengthSquared() < raw.LengthSquared()
? relativeToSystem
: raw;
} }
station.Modules.Add(new StationModuleRuntime internal static bool HasInstalledModules(StationRuntime station, params string[] modules) =>
modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal)));
internal static bool HasCapabilities(ShipDefinition definition, params string[] capabilities) =>
capabilities.All(capability => definition.Capabilities.Contains(capability, StringComparer.Ordinal));
internal static void AddStationModule(StationRuntime station, IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, string moduleId)
{ {
Id = $"{station.Id}-module-{station.Modules.Count + 1}", if (!moduleDefinitions.TryGetValue(moduleId, out var definition))
ModuleId = moduleId, {
Health = definition.Hull, return;
MaxHealth = definition.Hull, }
});
station.Radius = GetStationRadius(moduleDefinitions, station);
}
internal static float GetStationRadius(IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, StationRuntime station) station.Modules.Add(new StationModuleRuntime
{ {
var totalArea = station.Modules Id = $"{station.Id}-module-{station.Modules.Count + 1}",
.Select(module => moduleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f) ModuleId = moduleId,
.Sum(); Health = definition.Hull,
return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f))); MaxHealth = definition.Hull,
} });
station.Radius = GetStationRadius(moduleDefinitions, station);
internal static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
internal static Vector3 Scale(Vector3 vector, float scale) => new(vector.X * scale, vector.Y * scale, vector.Z * scale);
internal static int CountModules(IEnumerable<string> modules, string moduleId) =>
modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal));
internal static float ComputeWorkforceRatio(float population, float workforceRequired)
{
if (workforceRequired <= 0.01f)
{
return 1f;
} }
var staffedRatio = MathF.Min(1f, population / workforceRequired); internal static float GetStationRadius(IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, StationRuntime station)
return 0.1f + (0.9f * staffedRatio);
}
internal static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
internal static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f);
internal static Vector3 NormalizeOrFallback(Vector3 vector, Vector3 fallback)
{
var length = MathF.Sqrt(vector.LengthSquared());
if (length <= 0.0001f)
{ {
return fallback; var totalArea = station.Modules
.Select(module => moduleDefinitions.TryGetValue(module.ModuleId, out var definition) ? definition.Radius * definition.Radius : 0f)
.Sum();
return MathF.Max(24f, MathF.Sqrt(MathF.Max(totalArea, 1f)));
} }
return vector.Divide(length); internal static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
}
internal static Vector3 Scale(Vector3 vector, float scale) => new(vector.X * scale, vector.Y * scale, vector.Z * scale);
internal static int CountModules(IEnumerable<string> modules, string moduleId) =>
modules.Count(candidate => string.Equals(candidate, moduleId, StringComparison.Ordinal));
internal static float ComputeWorkforceRatio(float population, float workforceRequired)
{
if (workforceRequired <= 0.01f)
{
return 1f;
}
var staffedRatio = MathF.Min(1f, population / workforceRequired);
return 0.1f + (0.9f * staffedRatio);
}
internal static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
internal static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f);
internal static Vector3 NormalizeOrFallback(Vector3 vector, Vector3 fallback)
{
var length = MathF.Sqrt(vector.LengthSquared());
if (length <= 0.0001f)
{
return fallback;
}
return vector.Divide(length);
}
} }
internal sealed record StarProfile( internal sealed record StarProfile(
@@ -167,5 +167,5 @@ internal sealed record PlanetProfile(
int BaseMoonCount, int BaseMoonCount,
bool CanHaveRing) bool CanHaveRing)
{ {
public float OrbitGapMax => OrbitGapMin + MathF.Max(0.12f, OrbitGapMin * 0.45f); public float OrbitGapMax => OrbitGapMin + MathF.Max(0.12f, OrbitGapMin * 0.45f);
} }

View File

@@ -3,24 +3,24 @@ namespace SpaceGame.Api.Universe.Scenario;
public sealed class ScenarioLoader public sealed class ScenarioLoader
{ {
private readonly WorldBuilder _worldBuilder; private readonly WorldBuilder _worldBuilder;
public ScenarioLoader(string contentRootPath, WorldGenerationOptions? worldGeneration = null) public ScenarioLoader(string contentRootPath, WorldGenerationOptions? worldGeneration = null)
{ {
var generationOptions = worldGeneration ?? new WorldGenerationOptions(); var generationOptions = worldGeneration ?? new WorldGenerationOptions();
var dataRoot = Path.GetFullPath(Path.Combine(contentRootPath, "..", "..", "shared", "data")); var dataRoot = Path.GetFullPath(Path.Combine(contentRootPath, "..", "..", "shared", "data"));
var dataLoader = new DataCatalogLoader(dataRoot); var dataLoader = new DataCatalogLoader(dataRoot);
var generationService = new SystemGenerationService(); var generationService = new SystemGenerationService();
var spatialBuilder = new SpatialBuilder(); var spatialBuilder = new SpatialBuilder();
var seedingService = new WorldSeedingService(); var seedingService = new WorldSeedingService();
_worldBuilder = new WorldBuilder( _worldBuilder = new WorldBuilder(
generationOptions, generationOptions,
dataLoader, dataLoader,
generationService, generationService,
spatialBuilder, spatialBuilder,
seedingService); seedingService);
} }
public SimulationWorld Load() => _worldBuilder.Build(); public SimulationWorld Load() => _worldBuilder.Build();
} }

View File

@@ -4,305 +4,305 @@ namespace SpaceGame.Api.Universe.Scenario;
internal sealed class SpatialBuilder internal sealed class SpatialBuilder
{ {
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems, BalanceDefinition balance) internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems, BalanceDefinition balance)
{
var systemGraphs = systems.ToDictionary(
system => system.Definition.Id,
BuildSystemSpatialGraph,
StringComparer.Ordinal);
var celestials = systemGraphs.Values.SelectMany(graph => graph.Celestials).ToList();
var nodes = new List<ResourceNodeRuntime>();
var nodeIdCounter = 0;
foreach (var system in systems)
{ {
var systemGraph = systemGraphs[system.Definition.Id]; var systemGraphs = systems.ToDictionary(
foreach (var node in system.Definition.ResourceNodes) system => system.Definition.Id,
{ BuildSystemSpatialGraph,
var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node); StringComparer.Ordinal);
nodes.Add(new ResourceNodeRuntime var celestials = systemGraphs.Values.SelectMany(graph => graph.Celestials).ToList();
var nodes = new List<ResourceNodeRuntime>();
var nodeIdCounter = 0;
foreach (var system in systems)
{ {
Id = $"node-{++nodeIdCounter}", var systemGraph = systemGraphs[system.Definition.Id];
SystemId = system.Definition.Id, foreach (var node in system.Definition.ResourceNodes)
Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane), {
SourceKind = node.SourceKind, var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node);
ItemId = node.ItemId, nodes.Add(new ResourceNodeRuntime
CelestialId = anchorCelestial?.Id, {
OrbitRadius = node.RadiusOffset, Id = $"node-{++nodeIdCounter}",
OrbitPhase = node.Angle, SystemId = system.Definition.Id,
OrbitInclination = DegreesToRadians(node.InclinationDegrees), Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane),
OreRemaining = node.OreAmount, SourceKind = node.SourceKind,
MaxOre = node.OreAmount, ItemId = node.ItemId,
}); CelestialId = anchorCelestial?.Id,
} OrbitRadius = node.RadiusOffset,
OrbitPhase = node.Angle,
OrbitInclination = DegreesToRadians(node.InclinationDegrees),
OreRemaining = node.OreAmount,
MaxOre = node.OreAmount,
});
}
}
return new ScenarioSpatialLayout(systemGraphs, celestials, nodes);
} }
return new ScenarioSpatialLayout(systemGraphs, celestials, nodes); private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system)
}
private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system)
{
var celestials = new List<CelestialRuntime>();
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>();
for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
{ {
AddCelestial( var celestials = new List<CelestialRuntime>();
celestials, var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>();
id: $"node-{system.Definition.Id}-star-{starIndex + 1}",
systemId: system.Definition.Id, for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
kind: SpatialNodeKind.Star, {
position: Vector3.Zero, AddCelestial(
localSpaceRadius: LocalSpaceRadius); celestials,
id: $"node-{system.Definition.Id}-star-{starIndex + 1}",
systemId: system.Definition.Id,
kind: SpatialNodeKind.Star,
position: Vector3.Zero,
localSpaceRadius: LocalSpaceRadius);
}
var primaryStarNodeId = $"node-{system.Definition.Id}-star-1";
for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1)
{
var planet = system.Definition.Planets[planetIndex];
var planetNodeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}";
var planetPosition = ComputePlanetPosition(planet);
var planetCelestial = AddCelestial(
celestials,
id: planetNodeId,
systemId: system.Definition.Id,
kind: SpatialNodeKind.Planet,
position: planetPosition,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: primaryStarNodeId);
var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal);
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
{
var lagrangeCelestial = AddCelestial(
celestials,
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{point.Designation.ToLowerInvariant()}",
systemId: system.Definition.Id,
kind: SpatialNodeKind.LagrangePoint,
position: point.Position,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id,
orbitReferenceId: point.Designation);
lagrangeNodes[point.Designation] = lagrangeCelestial;
}
lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;
for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
{
var moon = planet.Moons[moonIndex];
var moonPosition = ComputeMoonPosition(planetPosition, moon);
AddCelestial(
celestials,
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
systemId: system.Definition.Id,
kind: SpatialNodeKind.Moon,
position: moonPosition,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id);
}
}
return new SystemSpatialGraph(system.Definition.Id, celestials, lagrangeNodesByPlanetIndex);
} }
var primaryStarNodeId = $"node-{system.Definition.Id}-star-1"; private static CelestialRuntime AddCelestial(
ICollection<CelestialRuntime> celestials,
for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1) string id,
string systemId,
SpatialNodeKind kind,
Vector3 position,
float localSpaceRadius,
string? parentNodeId = null,
string? orbitReferenceId = null)
{ {
var planet = system.Definition.Planets[planetIndex]; var celestial = new CelestialRuntime
var planetNodeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}"; {
var planetPosition = ComputePlanetPosition(planet); Id = id,
var planetCelestial = AddCelestial( SystemId = systemId,
celestials, Kind = kind,
id: planetNodeId, Position = position,
systemId: system.Definition.Id, LocalSpaceRadius = localSpaceRadius,
kind: SpatialNodeKind.Planet, ParentNodeId = parentNodeId,
position: planetPosition, OrbitReferenceId = orbitReferenceId,
localSpaceRadius: LocalSpaceRadius, };
parentNodeId: primaryStarNodeId);
var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal); celestials.Add(celestial);
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet)) return celestial;
{
var lagrangeCelestial = AddCelestial(
celestials,
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{point.Designation.ToLowerInvariant()}",
systemId: system.Definition.Id,
kind: SpatialNodeKind.LagrangePoint,
position: point.Position,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id,
orbitReferenceId: point.Designation);
lagrangeNodes[point.Designation] = lagrangeCelestial;
}
lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;
for (var moonIndex = 0; moonIndex < planet.Moons.Count; moonIndex += 1)
{
var moon = planet.Moons[moonIndex];
var moonPosition = ComputeMoonPosition(planetPosition, moon);
AddCelestial(
celestials,
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
systemId: system.Definition.Id,
kind: SpatialNodeKind.Moon,
position: moonPosition,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id);
}
} }
return new SystemSpatialGraph(system.Definition.Id, celestials, lagrangeNodesByPlanetIndex); private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(Vector3 planetPosition, PlanetDefinition planet)
}
private static CelestialRuntime AddCelestial(
ICollection<CelestialRuntime> celestials,
string id,
string systemId,
SpatialNodeKind kind,
Vector3 position,
float localSpaceRadius,
string? parentNodeId = null,
string? orbitReferenceId = null)
{
var celestial = new CelestialRuntime
{ {
Id = id, var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
SystemId = systemId, var tangential = new Vector3(-radial.Z, 0f, radial.X);
Kind = kind, var orbitRadiusKm = MathF.Sqrt(planetPosition.X * planetPosition.X + planetPosition.Z * planetPosition.Z);
Position = position, var offset = ComputePlanetLocalLagrangeOffset(orbitRadiusKm, planet);
LocalSpaceRadius = localSpaceRadius, var triangularAngle = MathF.PI / 3f;
ParentNodeId = parentNodeId,
OrbitReferenceId = orbitReferenceId, yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset)));
yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset)));
yield return new LagrangePointPlacement("L3", Scale(radial, -orbitRadiusKm));
yield return new LagrangePointPlacement(
"L4",
Add(
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle))));
yield return new LagrangePointPlacement(
"L5",
Add(
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
Scale(tangential, -orbitRadiusKm * MathF.Sin(triangularAngle))));
}
private static float ComputePlanetLocalLagrangeOffset(float orbitRadiusKm, PlanetDefinition planet)
{
var planetMassProxy = EstimatePlanetMassRatio(planet);
var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f));
var minimumOffset = MathF.Max(planet.Size * 4f, 25000f);
return MathF.Max(minimumOffset, hillLikeOffset);
}
private static float EstimatePlanetMassRatio(PlanetDefinition planet)
{
var earthRadiusRatio = MathF.Max(planet.Size / 6371f, 0.05f);
var densityFactor = planet.PlanetType switch
{
"gas-giant" => 0.24f,
"ice-giant" => 0.18f,
"oceanic" => 0.95f,
"ice" => 0.7f,
_ => 1f,
};
var earthMasses = MathF.Pow(earthRadiusRatio, 3f) * densityFactor;
return earthMasses / 332_946f;
}
internal static StationPlacement ResolveStationPlacement(
InitialStationDefinition plan,
SystemRuntime system,
SystemSpatialGraph graph,
IReadOnlyCollection<CelestialRuntime> existingCelestials)
{
if (plan.PlanetIndex is int planetIndex &&
graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes))
{
var designation = ResolveLagrangeDesignation(plan.LagrangeSide);
if (lagrangeNodes.TryGetValue(designation, out var lagrangeCelestial))
{
return new StationPlacement(lagrangeCelestial, lagrangeCelestial.Position);
}
}
if (plan.Position is { Length: 3 })
{
var targetPosition = NormalizeScenarioPoint(system, plan.Position);
var preferredCelestial = existingCelestials
.Where(c => c.SystemId == system.Definition.Id && c.Kind == SpatialNodeKind.LagrangePoint)
.OrderBy(c => c.Position.DistanceTo(targetPosition))
.FirstOrDefault()
?? existingCelestials
.Where(c => c.SystemId == system.Definition.Id)
.OrderBy(c => c.Position.DistanceTo(targetPosition))
.First();
return new StationPlacement(preferredCelestial, preferredCelestial.Position);
}
var fallbackCelestial = graph.Celestials
.FirstOrDefault(c => c.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(c.OccupyingStructureId))
?? graph.Celestials.First(c => c.Kind == SpatialNodeKind.Planet);
return new StationPlacement(fallbackCelestial, fallbackCelestial.Position);
}
private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch
{
< 0 => "L4",
> 0 => "L5",
_ => "L1",
}; };
celestials.Add(celestial); private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
return celestial;
}
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(Vector3 planetPosition, PlanetDefinition planet)
{
var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
var tangential = new Vector3(-radial.Z, 0f, radial.X);
var orbitRadiusKm = MathF.Sqrt(planetPosition.X * planetPosition.X + planetPosition.Z * planetPosition.Z);
var offset = ComputePlanetLocalLagrangeOffset(orbitRadiusKm, planet);
var triangularAngle = MathF.PI / 3f;
yield return new LagrangePointPlacement("L1", Add(planetPosition, Scale(radial, -offset)));
yield return new LagrangePointPlacement("L2", Add(planetPosition, Scale(radial, offset)));
yield return new LagrangePointPlacement("L3", Scale(radial, -orbitRadiusKm));
yield return new LagrangePointPlacement(
"L4",
Add(
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
Scale(tangential, orbitRadiusKm * MathF.Sin(triangularAngle))));
yield return new LagrangePointPlacement(
"L5",
Add(
Scale(radial, orbitRadiusKm * MathF.Cos(triangularAngle)),
Scale(tangential, -orbitRadiusKm * MathF.Sin(triangularAngle))));
}
private static float ComputePlanetLocalLagrangeOffset(float orbitRadiusKm, PlanetDefinition planet)
{
var planetMassProxy = EstimatePlanetMassRatio(planet);
var hillLikeOffset = orbitRadiusKm * MathF.Cbrt(MathF.Max(planetMassProxy / 3f, 1e-9f));
var minimumOffset = MathF.Max(planet.Size * 4f, 25000f);
return MathF.Max(minimumOffset, hillLikeOffset);
}
private static float EstimatePlanetMassRatio(PlanetDefinition planet)
{
var earthRadiusRatio = MathF.Max(planet.Size / 6371f, 0.05f);
var densityFactor = planet.PlanetType switch
{ {
"gas-giant" => 0.24f, if (!string.IsNullOrWhiteSpace(definition.AnchorReference))
"ice-giant" => 0.18f, {
"oceanic" => 0.95f, var anchorId = definition.AnchorReference.ToLowerInvariant() switch
"ice" => 0.7f, {
_ => 1f, var reference when reference.StartsWith("star-", StringComparison.Ordinal)
}; => $"node-{graph.SystemId}-{reference}",
var reference when reference.StartsWith("planet-", StringComparison.Ordinal)
=> $"node-{graph.SystemId}-{reference}",
_ => null,
};
var earthMasses = MathF.Pow(earthRadiusRatio, 3f) * densityFactor; if (anchorId is not null)
return earthMasses / 332_946f; {
} return graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, anchorId, StringComparison.Ordinal));
}
}
internal static StationPlacement ResolveStationPlacement( if (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0)
InitialStationDefinition plan, {
SystemRuntime system, return null;
SystemSpatialGraph graph, }
IReadOnlyCollection<CelestialRuntime> existingCelestials)
{ if (definition.AnchorMoonIndex is int moonIndex && moonIndex >= 0)
if (plan.PlanetIndex is int planetIndex && {
graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes)) var moonNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
{ return graph.Celestials.FirstOrDefault(c => c.Id == moonNodeId);
var designation = ResolveLagrangeDesignation(plan.LagrangeSide); }
if (lagrangeNodes.TryGetValue(designation, out var lagrangeCelestial))
{ var planetNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}";
return new StationPlacement(lagrangeCelestial, lagrangeCelestial.Position); return graph.Celestials.FirstOrDefault(c => c.Id == planetNodeId);
}
} }
if (plan.Position is { Length: 3 }) private static Vector3 ComputeResourceNodePosition(CelestialRuntime? anchorCelestial, ResourceNodeDefinition definition, float yPlane)
{ {
var targetPosition = NormalizeScenarioPoint(system, plan.Position); var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.04f, 25000f);
var preferredCelestial = existingCelestials var offset = new Vector3(
.Where(c => c.SystemId == system.Definition.Id && c.Kind == SpatialNodeKind.LagrangePoint) MathF.Cos(definition.Angle) * definition.RadiusOffset,
.OrderBy(c => c.Position.DistanceTo(targetPosition)) verticalOffset,
.FirstOrDefault() MathF.Sin(definition.Angle) * definition.RadiusOffset);
?? existingCelestials
.Where(c => c.SystemId == system.Definition.Id) if (anchorCelestial is null)
.OrderBy(c => c.Position.DistanceTo(targetPosition)) {
.First(); return new Vector3(offset.X, yPlane + offset.Y, offset.Z);
return new StationPlacement(preferredCelestial, preferredCelestial.Position); }
return Add(anchorCelestial.Position, offset);
} }
var fallbackCelestial = graph.Celestials private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
.FirstOrDefault(c => c.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(c.OccupyingStructureId))
?? graph.Celestials.First(c => c.Kind == SpatialNodeKind.Planet);
return new StationPlacement(fallbackCelestial, fallbackCelestial.Position);
}
private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch
{
< 0 => "L4",
> 0 => "L5",
_ => "L1",
};
private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
{
if (!string.IsNullOrWhiteSpace(definition.AnchorReference))
{ {
var anchorId = definition.AnchorReference.ToLowerInvariant() switch var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
{ var orbitRadiusKm = SimulationUnits.AuToKilometers(planet.OrbitRadius);
var reference when reference.StartsWith("star-", StringComparison.Ordinal) return new Vector3(MathF.Cos(angle) * orbitRadiusKm, 0f, MathF.Sin(angle) * orbitRadiusKm);
=> $"node-{graph.SystemId}-{reference}",
var reference when reference.StartsWith("planet-", StringComparison.Ordinal)
=> $"node-{graph.SystemId}-{reference}",
_ => null,
};
if (anchorId is not null)
{
return graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, anchorId, StringComparison.Ordinal));
}
} }
if (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0) private static Vector3 ComputeMoonPosition(Vector3 planetPosition, MoonDefinition moon)
{ {
return null; var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch);
var local = new Vector3(MathF.Cos(angle) * moon.OrbitRadius, 0f, MathF.Sin(angle) * moon.OrbitRadius);
return Add(planetPosition, local);
} }
if (definition.AnchorMoonIndex is int moonIndex && moonIndex >= 0) internal static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<CelestialRuntime> celestials)
{ {
var moonNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}-moon-{moonIndex + 1}"; var nearestCelestial = celestials
return graph.Celestials.FirstOrDefault(c => c.Id == moonNodeId); .Where(c => c.SystemId == systemId)
.OrderBy(c => c.Position.DistanceTo(position))
.FirstOrDefault();
return new ShipSpatialStateRuntime
{
CurrentSystemId = systemId,
SpaceLayer = SpaceLayerKinds.LocalSpace,
CurrentCelestialId = nearestCelestial?.Id,
LocalPosition = position,
SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight,
};
} }
var planetNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}";
return graph.Celestials.FirstOrDefault(c => c.Id == planetNodeId);
}
private static Vector3 ComputeResourceNodePosition(CelestialRuntime? anchorCelestial, ResourceNodeDefinition definition, float yPlane)
{
var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.04f, 25000f);
var offset = new Vector3(
MathF.Cos(definition.Angle) * definition.RadiusOffset,
verticalOffset,
MathF.Sin(definition.Angle) * definition.RadiusOffset);
if (anchorCelestial is null)
{
return new Vector3(offset.X, yPlane + offset.Y, offset.Z);
}
return Add(anchorCelestial.Position, offset);
}
private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
{
var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
var orbitRadiusKm = SimulationUnits.AuToKilometers(planet.OrbitRadius);
return new Vector3(MathF.Cos(angle) * orbitRadiusKm, 0f, MathF.Sin(angle) * orbitRadiusKm);
}
private static Vector3 ComputeMoonPosition(Vector3 planetPosition, MoonDefinition moon)
{
var angle = DegreesToRadians(moon.OrbitPhaseAtEpoch);
var local = new Vector3(MathF.Cos(angle) * moon.OrbitRadius, 0f, MathF.Sin(angle) * moon.OrbitRadius);
return Add(planetPosition, local);
}
internal static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<CelestialRuntime> celestials)
{
var nearestCelestial = celestials
.Where(c => c.SystemId == systemId)
.OrderBy(c => c.Position.DistanceTo(position))
.FirstOrDefault();
return new ShipSpatialStateRuntime
{
CurrentSystemId = systemId,
SpaceLayer = SpaceLayerKinds.LocalSpace,
CurrentCelestialId = nearestCelestial?.Id,
LocalPosition = position,
SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight,
};
}
} }
internal sealed record ScenarioSpatialLayout( internal sealed record ScenarioSpatialLayout(

File diff suppressed because it is too large Load Diff

View File

@@ -9,335 +9,335 @@ internal sealed class WorldBuilder(
SpatialBuilder spatialBuilder, SpatialBuilder spatialBuilder,
WorldSeedingService seedingService) WorldSeedingService seedingService)
{ {
internal SimulationWorld Build() internal SimulationWorld Build()
{
var catalog = dataLoader.LoadCatalog();
var systems = generationService.ExpandSystems(
generationService.InjectSpecialSystems(catalog.AuthoredSystems),
worldGeneration.TargetSystemCount);
Console.WriteLine("TEST");
Console.WriteLine(string.Join(',', systems.Select(s => s.Id)));
var scenario = dataLoader.NormalizeScenarioToAvailableSystems(
catalog.Scenario,
systems.Select(system => system.Id).ToList());
Console.WriteLine(string.Join(',', systems.Select(s => s.Id)));
var systemRuntimes = systems
.Select(definition => new SystemRuntime
{
Definition = definition,
Position = ToVector(definition.Position),
})
.ToList();
var systemsById = systemRuntimes.ToDictionary(system => system.Definition.Id, StringComparer.Ordinal);
var spatialLayout = spatialBuilder.BuildLayout(systemRuntimes, catalog.Balance);
var stations = CreateStations(
scenario,
systemsById,
spatialLayout.SystemGraphs,
spatialLayout.Celestials,
catalog.ModuleDefinitions,
catalog.ItemDefinitions);
seedingService.InitializeStationStockpiles(stations);
var refinery = seedingService.SelectRefineryStation(stations, scenario);
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery);
if (worldGeneration.AiControllerFactionCount < int.MaxValue)
{ {
var aiFactionIds = stations var catalog = dataLoader.LoadCatalog();
.Select(s => s.FactionId) var systems = generationService.ExpandSystems(
.Concat(ships.Select(s => s.FactionId)) generationService.InjectSpecialSystems(catalog.AuthoredSystems),
.Where(id => !string.IsNullOrWhiteSpace(id) && !string.Equals(id, DefaultFactionId, StringComparison.Ordinal)) worldGeneration.TargetSystemCount);
.Distinct(StringComparer.Ordinal)
.OrderBy(id => id, StringComparer.Ordinal)
.Take(worldGeneration.AiControllerFactionCount)
.ToHashSet(StringComparer.Ordinal);
aiFactionIds.Add(DefaultFactionId);
stations = stations.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
ships = ships.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
}
var factions = seedingService.CreateFactions(stations, ships); Console.WriteLine("TEST");
seedingService.BootstrapFactionEconomy(factions, stations); Console.WriteLine(string.Join(',', systems.Select(s => s.Id)));
var policies = seedingService.CreatePolicies(factions);
var commanders = seedingService.CreateCommanders(factions, stations, ships);
var nowUtc = DateTimeOffset.UtcNow;
var playerFaction = worldGeneration.GeneratePlayerFaction
? seedingService.CreatePlayerFaction(factions, stations, ships, commanders, policies, nowUtc)
: null;
var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc);
var bootstrapWorld = new SimulationWorld
{
Label = "Split Viewer / Bootstrap World",
Seed = WorldSeed,
Balance = catalog.Balance,
Systems = systemRuntimes,
Celestials = spatialLayout.Celestials,
Nodes = spatialLayout.Nodes,
Wrecks = [],
Stations = stations,
Ships = ships,
Factions = factions,
PlayerFaction = playerFaction,
Commanders = commanders,
Claims = claims,
ConstructionSites = [],
MarketOrders = [],
Policies = policies,
ShipDefinitions = new Dictionary<string, ShipDefinition>(catalog.ShipDefinitions, StringComparer.Ordinal),
ItemDefinitions = new Dictionary<string, ItemDefinition>(catalog.ItemDefinitions, StringComparer.Ordinal),
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(catalog.ModuleDefinitions, StringComparer.Ordinal),
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(catalog.ModuleRecipes, StringComparer.Ordinal),
Recipes = new Dictionary<string, RecipeDefinition>(catalog.Recipes, StringComparer.Ordinal),
ProductionGraph = catalog.ProductionGraph,
OrbitalTimeSeconds = WorldSeed * 97d,
GeneratedAtUtc = nowUtc,
};
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(bootstrapWorld);
var world = new SimulationWorld var scenario = dataLoader.NormalizeScenarioToAvailableSystems(
{ catalog.Scenario,
Label = "Split Viewer / Simulation World", systems.Select(system => system.Id).ToList());
Seed = WorldSeed,
Balance = catalog.Balance,
Systems = systemRuntimes,
Celestials = spatialLayout.Celestials,
Nodes = spatialLayout.Nodes,
Wrecks = [],
Stations = stations,
Ships = ships,
Factions = factions,
PlayerFaction = playerFaction,
Geopolitics = null,
Commanders = commanders,
Claims = claims,
ConstructionSites = constructionSites,
MarketOrders = marketOrders,
Policies = policies,
ShipDefinitions = new Dictionary<string, ShipDefinition>(catalog.ShipDefinitions, StringComparer.Ordinal),
ItemDefinitions = new Dictionary<string, ItemDefinition>(catalog.ItemDefinitions, StringComparer.Ordinal),
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(catalog.ModuleDefinitions, StringComparer.Ordinal),
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(catalog.ModuleRecipes, StringComparer.Ordinal),
Recipes = new Dictionary<string, RecipeDefinition>(catalog.Recipes, StringComparer.Ordinal),
ProductionGraph = catalog.ProductionGraph,
OrbitalTimeSeconds = WorldSeed * 97d,
GeneratedAtUtc = DateTimeOffset.UtcNow,
};
var geopolitics = new GeopoliticalSimulationService(); Console.WriteLine(string.Join(',', systems.Select(s => s.Id)));
geopolitics.Update(world, 0f, []);
return world;
}
private static List<StationRuntime> CreateStations( var systemRuntimes = systems
ScenarioDefinition scenario, .Select(definition => new SystemRuntime
IReadOnlyDictionary<string, SystemRuntime> systemsById,
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
IReadOnlyCollection<CelestialRuntime> celestials,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
{
var stations = new List<StationRuntime>();
var stationIdCounter = 0;
foreach (var plan in scenario.InitialStations)
{
if (!systemsById.TryGetValue(plan.SystemId, out var system))
{
continue;
}
var placement = SpatialBuilder.ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], celestials);
var station = new StationRuntime
{
Id = $"station-{++stationIdCounter}",
SystemId = system.Definition.Id,
Label = plan.Label,
Color = plan.Color,
Objective = StationSimulationService.NormalizeStationObjective(plan.Objective),
Position = placement.Position,
FactionId = plan.FactionId ?? DefaultFactionId,
CelestialId = placement.AnchorCelestial.Id,
Health = 600f,
MaxHealth = 600f,
};
stations.Add(station);
placement.AnchorCelestial.OccupyingStructureId = station.Id;
var startingModules = BuildStartingModules(plan, moduleDefinitions, itemDefinitions);
foreach (var moduleId in startingModules)
{
AddStationModule(station, moduleDefinitions, moduleId);
}
}
return stations;
}
private static IReadOnlyList<string> BuildStartingModules(
InitialStationDefinition plan,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
{
var startingModules = new List<string>(plan.StartingModules.Count > 0
? plan.StartingModules
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_container_m_01"]);
EnsureStartingModule(startingModules, "module_arg_dock_m_01_lowtech");
var objectiveModuleId = GetObjectiveStartingModuleId(plan.Objective);
if (!string.IsNullOrWhiteSpace(objectiveModuleId))
{
EnsureStartingModule(startingModules, objectiveModuleId);
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal))
{
EnsureStartingModule(startingModules, "module_gen_prod_energycells_01");
}
foreach (var storageModuleId in GetRequiredStartingStorageModules(objectiveModuleId, moduleDefinitions, itemDefinitions))
{
EnsureStartingModule(startingModules, storageModuleId);
}
}
return startingModules;
}
private static string? GetObjectiveStartingModuleId(string? objective) =>
StationSimulationService.NormalizeStationObjective(objective) switch
{
"power" => "module_gen_prod_energycells_01",
"refinery" => "module_gen_ref_ore_01",
"graphene" => "module_gen_prod_graphene_01",
"siliconwafers" => "module_gen_prod_siliconwafers_01",
"hullparts" => "module_gen_prod_hullparts_01",
"claytronics" => "module_gen_prod_claytronics_01",
"quantumtubes" => "module_gen_prod_quantumtubes_01",
"antimattercells" => "module_gen_prod_antimattercells_01",
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
"water" => "module_gen_prod_water_01",
_ => null,
};
private static IEnumerable<string> GetRequiredStartingStorageModules(
string moduleId,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
{
if (!moduleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
{
yield break;
}
foreach (var wareId in moduleDefinition.Production
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
.Concat(moduleDefinition.Products)
.Distinct(StringComparer.Ordinal))
{
if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition))
{
continue;
}
var storageModuleId = itemDefinition.CargoKind switch
{
"solid" => "module_arg_stor_solid_m_01",
"liquid" => "module_arg_stor_liquid_m_01",
_ => "module_arg_stor_container_m_01",
};
yield return storageModuleId;
}
}
private static void EnsureStartingModule(List<string> modules, string moduleId)
{
if (!modules.Contains(moduleId, StringComparer.Ordinal))
{
modules.Add(moduleId);
}
}
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById)
{
return scenario.PatrolRoutes
.GroupBy(route => route.SystemId, StringComparer.Ordinal)
.ToDictionary(
group => group.Key,
group => group
.SelectMany(route => route.Points)
.Select(point => NormalizeScenarioPoint(systemsById[group.Key], point))
.ToList(),
StringComparer.Ordinal);
}
private static List<ShipRuntime> CreateShips(
ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById,
IReadOnlyCollection<CelestialRuntime> celestials,
BalanceDefinition balance,
IReadOnlyDictionary<string, ShipDefinition> shipDefinitions,
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
IReadOnlyCollection<StationRuntime> stations,
StationRuntime? refinery)
{
var ships = new List<ShipRuntime>();
var shipIdCounter = 0;
foreach (var formation in scenario.ShipFormations)
{
if (!shipDefinitions.TryGetValue(formation.ShipId, out var definition))
{
continue;
}
for (var index = 0; index < formation.Count; index += 1)
{
var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f);
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
ships.Add(new ShipRuntime
{
Id = $"ship-{++shipIdCounter}",
SystemId = formation.SystemId,
Definition = definition,
FactionId = formation.FactionId ?? DefaultFactionId,
Position = position,
TargetPosition = position,
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials),
DefaultBehavior = WorldSeedingService.CreateBehavior(
definition,
formation.SystemId,
formation.FactionId ?? DefaultFactionId,
scenario,
patrolRoutes,
stations,
refinery),
Skills = WorldSeedingService.CreateSkills(definition),
Health = definition.MaxHealth,
});
foreach (var (itemId, amount) in formation.StartingInventory)
{
if (amount > 0f)
{ {
ships[^1].Inventory[itemId] = amount; Definition = definition,
} Position = ToVector(definition.Position),
})
.ToList();
var systemsById = systemRuntimes.ToDictionary(system => system.Definition.Id, StringComparer.Ordinal);
var spatialLayout = spatialBuilder.BuildLayout(systemRuntimes, catalog.Balance);
var stations = CreateStations(
scenario,
systemsById,
spatialLayout.SystemGraphs,
spatialLayout.Celestials,
catalog.ModuleDefinitions,
catalog.ItemDefinitions);
seedingService.InitializeStationStockpiles(stations);
var refinery = seedingService.SelectRefineryStation(stations, scenario);
var patrolRoutes = BuildPatrolRoutes(scenario, systemsById);
var ships = CreateShips(scenario, systemsById, spatialLayout.Celestials, catalog.Balance, catalog.ShipDefinitions, patrolRoutes, stations, refinery);
if (worldGeneration.AiControllerFactionCount < int.MaxValue)
{
var aiFactionIds = stations
.Select(s => s.FactionId)
.Concat(ships.Select(s => s.FactionId))
.Where(id => !string.IsNullOrWhiteSpace(id) && !string.Equals(id, DefaultFactionId, StringComparison.Ordinal))
.Distinct(StringComparer.Ordinal)
.OrderBy(id => id, StringComparer.Ordinal)
.Take(worldGeneration.AiControllerFactionCount)
.ToHashSet(StringComparer.Ordinal);
aiFactionIds.Add(DefaultFactionId);
stations = stations.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
ships = ships.Where(s => aiFactionIds.Contains(s.FactionId)).ToList();
} }
}
var factions = seedingService.CreateFactions(stations, ships);
seedingService.BootstrapFactionEconomy(factions, stations);
var policies = seedingService.CreatePolicies(factions);
var commanders = seedingService.CreateCommanders(factions, stations, ships);
var nowUtc = DateTimeOffset.UtcNow;
var playerFaction = worldGeneration.GeneratePlayerFaction
? seedingService.CreatePlayerFaction(factions, stations, ships, commanders, policies, nowUtc)
: null;
var claims = seedingService.CreateClaims(stations, spatialLayout.Celestials, nowUtc);
var bootstrapWorld = new SimulationWorld
{
Label = "Split Viewer / Bootstrap World",
Seed = WorldSeed,
Balance = catalog.Balance,
Systems = systemRuntimes,
Celestials = spatialLayout.Celestials,
Nodes = spatialLayout.Nodes,
Wrecks = [],
Stations = stations,
Ships = ships,
Factions = factions,
PlayerFaction = playerFaction,
Commanders = commanders,
Claims = claims,
ConstructionSites = [],
MarketOrders = [],
Policies = policies,
ShipDefinitions = new Dictionary<string, ShipDefinition>(catalog.ShipDefinitions, StringComparer.Ordinal),
ItemDefinitions = new Dictionary<string, ItemDefinition>(catalog.ItemDefinitions, StringComparer.Ordinal),
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(catalog.ModuleDefinitions, StringComparer.Ordinal),
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(catalog.ModuleRecipes, StringComparer.Ordinal),
Recipes = new Dictionary<string, RecipeDefinition>(catalog.Recipes, StringComparer.Ordinal),
ProductionGraph = catalog.ProductionGraph,
OrbitalTimeSeconds = WorldSeed * 97d,
GeneratedAtUtc = nowUtc,
};
var (constructionSites, marketOrders) = seedingService.CreateConstructionSites(bootstrapWorld);
var world = new SimulationWorld
{
Label = "Split Viewer / Simulation World",
Seed = WorldSeed,
Balance = catalog.Balance,
Systems = systemRuntimes,
Celestials = spatialLayout.Celestials,
Nodes = spatialLayout.Nodes,
Wrecks = [],
Stations = stations,
Ships = ships,
Factions = factions,
PlayerFaction = playerFaction,
Geopolitics = null,
Commanders = commanders,
Claims = claims,
ConstructionSites = constructionSites,
MarketOrders = marketOrders,
Policies = policies,
ShipDefinitions = new Dictionary<string, ShipDefinition>(catalog.ShipDefinitions, StringComparer.Ordinal),
ItemDefinitions = new Dictionary<string, ItemDefinition>(catalog.ItemDefinitions, StringComparer.Ordinal),
ModuleDefinitions = new Dictionary<string, ModuleDefinition>(catalog.ModuleDefinitions, StringComparer.Ordinal),
ModuleRecipes = new Dictionary<string, ModuleRecipeDefinition>(catalog.ModuleRecipes, StringComparer.Ordinal),
Recipes = new Dictionary<string, RecipeDefinition>(catalog.Recipes, StringComparer.Ordinal),
ProductionGraph = catalog.ProductionGraph,
OrbitalTimeSeconds = WorldSeed * 97d,
GeneratedAtUtc = DateTimeOffset.UtcNow,
};
var geopolitics = new GeopoliticalSimulationService();
geopolitics.Update(world, 0f, []);
return world;
} }
return ships; private static List<StationRuntime> CreateStations(
} ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById,
IReadOnlyDictionary<string, SystemSpatialGraph> systemGraphs,
IReadOnlyCollection<CelestialRuntime> celestials,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
{
var stations = new List<StationRuntime>();
var stationIdCounter = 0;
foreach (var plan in scenario.InitialStations)
{
if (!systemsById.TryGetValue(plan.SystemId, out var system))
{
continue;
}
var placement = SpatialBuilder.ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], celestials);
var station = new StationRuntime
{
Id = $"station-{++stationIdCounter}",
SystemId = system.Definition.Id,
Label = plan.Label,
Color = plan.Color,
Objective = StationSimulationService.NormalizeStationObjective(plan.Objective),
Position = placement.Position,
FactionId = plan.FactionId ?? DefaultFactionId,
CelestialId = placement.AnchorCelestial.Id,
Health = 600f,
MaxHealth = 600f,
};
stations.Add(station);
placement.AnchorCelestial.OccupyingStructureId = station.Id;
var startingModules = BuildStartingModules(plan, moduleDefinitions, itemDefinitions);
foreach (var moduleId in startingModules)
{
AddStationModule(station, moduleDefinitions, moduleId);
}
}
return stations;
}
private static IReadOnlyList<string> BuildStartingModules(
InitialStationDefinition plan,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
{
var startingModules = new List<string>(plan.StartingModules.Count > 0
? plan.StartingModules
: ["module_arg_dock_m_01_lowtech", "module_gen_prod_energycells_01", "module_arg_stor_container_m_01"]);
EnsureStartingModule(startingModules, "module_arg_dock_m_01_lowtech");
var objectiveModuleId = GetObjectiveStartingModuleId(plan.Objective);
if (!string.IsNullOrWhiteSpace(objectiveModuleId))
{
EnsureStartingModule(startingModules, objectiveModuleId);
if (!string.Equals(objectiveModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal))
{
EnsureStartingModule(startingModules, "module_gen_prod_energycells_01");
}
foreach (var storageModuleId in GetRequiredStartingStorageModules(objectiveModuleId, moduleDefinitions, itemDefinitions))
{
EnsureStartingModule(startingModules, storageModuleId);
}
}
return startingModules;
}
private static string? GetObjectiveStartingModuleId(string? objective) =>
StationSimulationService.NormalizeStationObjective(objective) switch
{
"power" => "module_gen_prod_energycells_01",
"refinery" => "module_gen_ref_ore_01",
"graphene" => "module_gen_prod_graphene_01",
"siliconwafers" => "module_gen_prod_siliconwafers_01",
"hullparts" => "module_gen_prod_hullparts_01",
"claytronics" => "module_gen_prod_claytronics_01",
"quantumtubes" => "module_gen_prod_quantumtubes_01",
"antimattercells" => "module_gen_prod_antimattercells_01",
"superfluidcoolant" => "module_gen_prod_superfluidcoolant_01",
"water" => "module_gen_prod_water_01",
_ => null,
};
private static IEnumerable<string> GetRequiredStartingStorageModules(
string moduleId,
IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions,
IReadOnlyDictionary<string, ItemDefinition> itemDefinitions)
{
if (!moduleDefinitions.TryGetValue(moduleId, out var moduleDefinition))
{
yield break;
}
foreach (var wareId in moduleDefinition.Production
.SelectMany(production => production.Wares.Select(ware => ware.ItemId))
.Concat(moduleDefinition.Products)
.Distinct(StringComparer.Ordinal))
{
if (!itemDefinitions.TryGetValue(wareId, out var itemDefinition))
{
continue;
}
var storageModuleId = itemDefinition.CargoKind switch
{
"solid" => "module_arg_stor_solid_m_01",
"liquid" => "module_arg_stor_liquid_m_01",
_ => "module_arg_stor_container_m_01",
};
yield return storageModuleId;
}
}
private static void EnsureStartingModule(List<string> modules, string moduleId)
{
if (!modules.Contains(moduleId, StringComparer.Ordinal))
{
modules.Add(moduleId);
}
}
private static Dictionary<string, List<Vector3>> BuildPatrolRoutes(
ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById)
{
return scenario.PatrolRoutes
.GroupBy(route => route.SystemId, StringComparer.Ordinal)
.ToDictionary(
group => group.Key,
group => group
.SelectMany(route => route.Points)
.Select(point => NormalizeScenarioPoint(systemsById[group.Key], point))
.ToList(),
StringComparer.Ordinal);
}
private static List<ShipRuntime> CreateShips(
ScenarioDefinition scenario,
IReadOnlyDictionary<string, SystemRuntime> systemsById,
IReadOnlyCollection<CelestialRuntime> celestials,
BalanceDefinition balance,
IReadOnlyDictionary<string, ShipDefinition> shipDefinitions,
IReadOnlyDictionary<string, List<Vector3>> patrolRoutes,
IReadOnlyCollection<StationRuntime> stations,
StationRuntime? refinery)
{
var ships = new List<ShipRuntime>();
var shipIdCounter = 0;
foreach (var formation in scenario.ShipFormations)
{
if (!shipDefinitions.TryGetValue(formation.ShipId, out var definition))
{
continue;
}
for (var index = 0; index < formation.Count; index += 1)
{
var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f);
var position = Add(NormalizeScenarioPoint(systemsById[formation.SystemId], formation.Center), offset);
ships.Add(new ShipRuntime
{
Id = $"ship-{++shipIdCounter}",
SystemId = formation.SystemId,
Definition = definition,
FactionId = formation.FactionId ?? DefaultFactionId,
Position = position,
TargetPosition = position,
SpatialState = SpatialBuilder.CreateInitialShipSpatialState(formation.SystemId, position, celestials),
DefaultBehavior = WorldSeedingService.CreateBehavior(
definition,
formation.SystemId,
formation.FactionId ?? DefaultFactionId,
scenario,
patrolRoutes,
stations,
refinery),
Skills = WorldSeedingService.CreateSkills(definition),
Health = definition.MaxHealth,
});
foreach (var (itemId, amount) in formation.StartingInventory)
{
if (amount > 0f)
{
ships[^1].Inventory[itemId] = amount;
}
}
}
}
return ships;
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,5 +2,5 @@ namespace SpaceGame.Api.Universe.Simulation;
public sealed class OrbitalSimulationOptions public sealed class OrbitalSimulationOptions
{ {
public double SimulatedSecondsPerRealSecond { get; init; } = 0d; public double SimulatedSecondsPerRealSecond { get; init; } = 0d;
} }

View File

@@ -2,18 +2,18 @@ namespace SpaceGame.Api.Universe.Simulation;
public sealed class SimulationHostedService(WorldService worldService) : BackgroundService public sealed class SimulationHostedService(WorldService worldService) : BackgroundService
{ {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(200));
try
{ {
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken)) using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(200));
{ try
worldService.Tick(0.2f); {
} while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
{
worldService.Tick(0.2f);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
}
} }
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
}
}
} }

View File

@@ -4,41 +4,41 @@ namespace SpaceGame.Api.Universe.Simulation;
public sealed class TelemetryService : IDisposable public sealed class TelemetryService : IDisposable
{ {
private readonly Process _process = Process.GetCurrentProcess(); private readonly Process _process = Process.GetCurrentProcess();
private readonly Timer _timer; private readonly Timer _timer;
private double _cpuPercent; private double _cpuPercent;
private DateTime _lastSampleTime; private DateTime _lastSampleTime;
private TimeSpan _lastCpuTime; private TimeSpan _lastCpuTime;
public TelemetryService() public TelemetryService()
{ {
_process.Refresh(); _process.Refresh();
_lastSampleTime = DateTime.UtcNow; _lastSampleTime = DateTime.UtcNow;
_lastCpuTime = _process.TotalProcessorTime; _lastCpuTime = _process.TotalProcessorTime;
_timer = new Timer(Sample, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); _timer = new Timer(Sample, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
} }
private void Sample(object? _) private void Sample(object? _)
{ {
_process.Refresh(); _process.Refresh();
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var cpu = _process.TotalProcessorTime; var cpu = _process.TotalProcessorTime;
var elapsed = (now - _lastSampleTime).TotalSeconds; var elapsed = (now - _lastSampleTime).TotalSeconds;
var cpuUsed = (cpu - _lastCpuTime).TotalSeconds; var cpuUsed = (cpu - _lastCpuTime).TotalSeconds;
Volatile.Write(ref _cpuPercent, elapsed > 0 ? cpuUsed / elapsed / Environment.ProcessorCount * 100.0 : 0); Volatile.Write(ref _cpuPercent, elapsed > 0 ? cpuUsed / elapsed / Environment.ProcessorCount * 100.0 : 0);
_lastSampleTime = now; _lastSampleTime = now;
_lastCpuTime = cpu; _lastCpuTime = cpu;
} }
public double CpuPercent => Volatile.Read(ref _cpuPercent); public double CpuPercent => Volatile.Read(ref _cpuPercent);
public long WorkingSetBytes => _process.WorkingSet64; public long WorkingSetBytes => _process.WorkingSet64;
public long GcMemoryBytes => GC.GetTotalMemory(false); public long GcMemoryBytes => GC.GetTotalMemory(false);
public int ThreadCount => _process.Threads.Count; public int ThreadCount => _process.Threads.Count;
public TimeSpan Uptime => DateTime.UtcNow - _process.StartTime.ToUniversalTime(); public TimeSpan Uptime => DateTime.UtcNow - _process.StartTime.ToUniversalTime();
public void Dispose() public void Dispose()
{ {
_timer.Dispose(); _timer.Dispose();
_process.Dispose(); _process.Dispose();
} }
} }

View File

@@ -2,7 +2,7 @@ namespace SpaceGame.Api.Universe.Simulation;
public sealed class WorldGenerationOptions public sealed class WorldGenerationOptions
{ {
public int TargetSystemCount { get; init; } public int TargetSystemCount { get; init; }
public int AiControllerFactionCount { get; init; } public int AiControllerFactionCount { get; init; }
public bool GeneratePlayerFaction { get; init; } public bool GeneratePlayerFaction { get; init; }
} }

View File

@@ -8,507 +8,507 @@ public sealed class WorldService(
IOptions<WorldGenerationOptions> worldGenerationOptions, IOptions<WorldGenerationOptions> worldGenerationOptions,
IOptions<OrbitalSimulationOptions> orbitalSimulationOptions) IOptions<OrbitalSimulationOptions> orbitalSimulationOptions)
{ {
private const int DeltaHistoryLimit = 256; private const int DeltaHistoryLimit = 256;
private readonly Lock _sync = new(); private readonly Lock _sync = new();
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond); private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
private readonly ScenarioLoader _loader = new(environment.ContentRootPath, worldGenerationOptions.Value); private readonly ScenarioLoader _loader = new(environment.ContentRootPath, worldGenerationOptions.Value);
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value); private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
private readonly PlayerFactionService _playerFaction = new(); private readonly PlayerFactionService _playerFaction = new();
private readonly Dictionary<Guid, SubscriptionState> _subscribers = []; private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
private readonly Queue<WorldDelta> _history = []; private readonly Queue<WorldDelta> _history = [];
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load(); private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
private long _sequence; private long _sequence;
private BalanceDefinition? _balanceOverride; private BalanceDefinition? _balanceOverride;
public WorldSnapshot GetSnapshot() public WorldSnapshot GetSnapshot()
{
lock (_sync)
{ {
return _engine.BuildSnapshot(_world, _sequence); lock (_sync)
}
}
public (long Sequence, DateTimeOffset GeneratedAtUtc) GetStatus()
{
lock (_sync)
{
return (_sequence, _world.GeneratedAtUtc);
}
}
public (int ConnectedClients, int DeltaHistoryCount) GetConnectionStats()
{
lock (_sync)
{
return (_subscribers.Count, _history.Count);
}
}
public BalanceDefinition GetBalance()
{
lock (_sync)
{
var b = _world.Balance;
return new BalanceDefinition
{
SimulationSpeedMultiplier = b.SimulationSpeedMultiplier,
YPlane = b.YPlane,
ArrivalThreshold = b.ArrivalThreshold,
MiningRate = b.MiningRate,
MiningCycleSeconds = b.MiningCycleSeconds,
TransferRate = b.TransferRate,
DockingDuration = b.DockingDuration,
UndockingDuration = b.UndockingDuration,
UndockDistance = b.UndockDistance,
};
}
}
public BalanceDefinition UpdateBalance(BalanceDefinition balance)
{
lock (_sync)
{
_balanceOverride = SanitizeBalance(balance);
ApplyBalance(_world, _balanceOverride);
return GetBalance();
}
}
public ShipSnapshot? EnqueueShipOrder(string shipId, ShipOrderCommandRequest request)
{
lock (_sync)
{
var ship = _playerFaction.EnqueueDirectShipOrder(_world, shipId, request);
if (ship is null)
{
return null;
}
return GetShipSnapshotUnsafe(ship.Id);
}
}
public ShipSnapshot? RemoveShipOrder(string shipId, string orderId)
{
lock (_sync)
{
var ship = _playerFaction.RemoveDirectShipOrder(_world, shipId, orderId);
if (ship is null)
{
return null;
}
return GetShipSnapshotUnsafe(ship.Id);
}
}
public ShipSnapshot? UpdateShipDefaultBehavior(string shipId, ShipDefaultBehaviorCommandRequest request)
{
lock (_sync)
{
var ship = _playerFaction.ConfigureDirectShipBehavior(_world, shipId, request);
if (ship is null)
{
return null;
}
return GetShipSnapshotUnsafe(ship.Id);
}
}
public PlayerFactionSnapshot? GetPlayerFaction()
{
lock (_sync)
{
_playerFaction.EnsureDomain(_world);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? CreatePlayerOrganization(PlayerOrganizationCommandRequest request)
{
lock (_sync)
{
_playerFaction.CreateOrganization(_world, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? DeletePlayerOrganization(string organizationId)
{
lock (_sync)
{
_playerFaction.DeleteOrganization(_world, organizationId);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpdatePlayerOrganizationMembership(string organizationId, PlayerOrganizationMembershipCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpdateOrganizationMembership(_world, organizationId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerDirective(string? directiveId, PlayerDirectiveCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertDirective(_world, directiveId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? DeletePlayerDirective(string directiveId)
{
lock (_sync)
{
_playerFaction.DeleteDirective(_world, directiveId);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerPolicy(string? policyId, PlayerPolicyCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertPolicy(_world, policyId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerAutomationPolicy(string? automationPolicyId, PlayerAutomationPolicyCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertAutomationPolicy(_world, automationPolicyId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerReinforcementPolicy(string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertReinforcementPolicy(_world, reinforcementPolicyId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerProductionProgram(string? productionProgramId, PlayerProductionProgramCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertProductionProgram(_world, productionProgramId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerAssignment(string assetId, PlayerAssetAssignmentCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertAssignment(_world, assetId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpdatePlayerStrategicIntent(PlayerStrategicIntentCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpdateStrategicIntent(_world, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false,
});
Guid subscriberId;
lock (_sync)
{
subscriberId = Guid.NewGuid();
_subscribers.Add(subscriberId, new SubscriptionState(scope, channel));
foreach (var delta in _history.Where((candidate) => candidate.Sequence > afterSequence))
{
var filtered = FilterDeltaForScope(delta, scope);
if (HasMeaningfulDelta(filtered))
{ {
channel.Writer.TryWrite(filtered); return _engine.BuildSnapshot(_world, _sequence);
} }
}
} }
cancellationToken.Register(() => Unsubscribe(subscriberId)); public (long Sequence, DateTimeOffset GeneratedAtUtc) GetStatus()
return channel.Reader;
}
public void Tick(float deltaSeconds)
{
WorldDelta? delta = null;
lock (_sync)
{ {
delta = _engine.Tick(_world, deltaSeconds, ++_sequence); lock (_sync)
if (!HasMeaningfulDelta(delta))
{
return;
}
_history.Enqueue(delta);
while (_history.Count > DeltaHistoryLimit)
{
_history.Dequeue();
}
foreach (var subscriber in _subscribers.Values.ToList())
{
var filtered = FilterDeltaForScope(delta, subscriber.Scope);
if (HasMeaningfulDelta(filtered))
{ {
subscriber.Channel.Writer.TryWrite(filtered); return (_sequence, _world.GeneratedAtUtc);
} }
}
} }
}
public WorldSnapshot Reset() public (int ConnectedClients, int DeltaHistoryCount) GetConnectionStats()
{
lock (_sync)
{ {
_world = _loader.Load(); lock (_sync)
if (_balanceOverride is not null) {
{ return (_subscribers.Count, _history.Count);
ApplyBalance(_world, _balanceOverride); }
}
_sequence += 1;
_history.Clear();
var resetDelta = new WorldDelta(
_sequence,
_world.TickIntervalMs,
_world.OrbitalTimeSeconds,
_orbitalSimulation,
DateTimeOffset.UtcNow,
true,
[new SimulationEventRecord("world", "world", "reset", "World reset requested", DateTimeOffset.UtcNow, "world", "universe", "world")],
[],
[],
[],
[],
[],
[],
[],
[],
[],
null,
null);
_history.Enqueue(resetDelta);
foreach (var subscriber in _subscribers.Values.ToList())
{
subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(resetDelta, subscriber.Scope));
}
return _engine.BuildSnapshot(_world, _sequence);
} }
}
private static void ApplyBalance(SimulationWorld world, BalanceDefinition balance) => public BalanceDefinition GetBalance()
world.Balance = new BalanceDefinition
{ {
SimulationSpeedMultiplier = balance.SimulationSpeedMultiplier, lock (_sync)
YPlane = balance.YPlane, {
ArrivalThreshold = balance.ArrivalThreshold, var b = _world.Balance;
MiningRate = balance.MiningRate, return new BalanceDefinition
MiningCycleSeconds = balance.MiningCycleSeconds, {
TransferRate = balance.TransferRate, SimulationSpeedMultiplier = b.SimulationSpeedMultiplier,
DockingDuration = balance.DockingDuration, YPlane = b.YPlane,
UndockingDuration = balance.UndockingDuration, ArrivalThreshold = b.ArrivalThreshold,
UndockDistance = balance.UndockDistance, MiningRate = b.MiningRate,
}; MiningCycleSeconds = b.MiningCycleSeconds,
TransferRate = b.TransferRate,
private static BalanceDefinition SanitizeBalance(BalanceDefinition candidate) DockingDuration = b.DockingDuration,
{ UndockingDuration = b.UndockingDuration,
static float finiteOr(float value, float fallback) => UndockDistance = b.UndockDistance,
float.IsFinite(value) ? value : fallback; };
}
return new BalanceDefinition
{
SimulationSpeedMultiplier = MathF.Max(0.01f, finiteOr(candidate.SimulationSpeedMultiplier, 1f)),
YPlane = MathF.Max(0f, finiteOr(candidate.YPlane, 0f)),
ArrivalThreshold = MathF.Max(0.1f, finiteOr(candidate.ArrivalThreshold, 16f)),
MiningRate = MathF.Max(0f, finiteOr(candidate.MiningRate, 10f)),
MiningCycleSeconds = MathF.Max(0.1f, finiteOr(candidate.MiningCycleSeconds, 10f)),
TransferRate = MathF.Max(0f, finiteOr(candidate.TransferRate, 56f)),
DockingDuration = MathF.Max(0.1f, finiteOr(candidate.DockingDuration, 1.2f)),
UndockingDuration = MathF.Max(0.1f, finiteOr(candidate.UndockingDuration, 1.2f)),
UndockDistance = MathF.Max(0f, finiteOr(candidate.UndockDistance, 42f)),
};
}
private ShipSnapshot? GetShipSnapshotUnsafe(string shipId) =>
_engine.BuildSnapshot(_world, _sequence).Ships.FirstOrDefault(ship => ship.Id == shipId);
private PlayerFactionSnapshot? GetPlayerFactionSnapshotUnsafe() =>
_engine.BuildSnapshot(_world, _sequence).PlayerFaction;
private static bool HasMeaningfulDelta(WorldDelta delta) =>
delta.RequiresSnapshotRefresh
|| delta.Events.Count > 0
|| delta.Celestials.Count > 0
|| delta.Nodes.Count > 0
|| delta.Stations.Count > 0
|| delta.Claims.Count > 0
|| delta.ConstructionSites.Count > 0
|| delta.MarketOrders.Count > 0
|| delta.Policies.Count > 0
|| delta.Ships.Count > 0
|| delta.Factions.Count > 0
|| delta.PlayerFaction is not null
|| delta.Geopolitics is not null;
private void Unsubscribe(Guid subscriberId)
{
lock (_sync)
{
if (!_subscribers.Remove(subscriberId, out var subscription))
{
return;
}
subscription.Channel.Writer.TryComplete();
} }
}
private WorldDelta FilterDeltaForScope(WorldDelta delta, ObserverScope scope) public BalanceDefinition UpdateBalance(BalanceDefinition balance)
{
if (string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase))
{ {
return delta with lock (_sync)
{
_balanceOverride = SanitizeBalance(balance);
ApplyBalance(_world, _balanceOverride);
return GetBalance();
}
}
public ShipSnapshot? EnqueueShipOrder(string shipId, ShipOrderCommandRequest request)
{
lock (_sync)
{
var ship = _playerFaction.EnqueueDirectShipOrder(_world, shipId, request);
if (ship is null)
{
return null;
}
return GetShipSnapshotUnsafe(ship.Id);
}
}
public ShipSnapshot? RemoveShipOrder(string shipId, string orderId)
{
lock (_sync)
{
var ship = _playerFaction.RemoveDirectShipOrder(_world, shipId, orderId);
if (ship is null)
{
return null;
}
return GetShipSnapshotUnsafe(ship.Id);
}
}
public ShipSnapshot? UpdateShipDefaultBehavior(string shipId, ShipDefaultBehaviorCommandRequest request)
{
lock (_sync)
{
var ship = _playerFaction.ConfigureDirectShipBehavior(_world, shipId, request);
if (ship is null)
{
return null;
}
return GetShipSnapshotUnsafe(ship.Id);
}
}
public PlayerFactionSnapshot? GetPlayerFaction()
{
lock (_sync)
{
_playerFaction.EnsureDomain(_world);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? CreatePlayerOrganization(PlayerOrganizationCommandRequest request)
{
lock (_sync)
{
_playerFaction.CreateOrganization(_world, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? DeletePlayerOrganization(string organizationId)
{
lock (_sync)
{
_playerFaction.DeleteOrganization(_world, organizationId);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpdatePlayerOrganizationMembership(string organizationId, PlayerOrganizationMembershipCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpdateOrganizationMembership(_world, organizationId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerDirective(string? directiveId, PlayerDirectiveCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertDirective(_world, directiveId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? DeletePlayerDirective(string directiveId)
{
lock (_sync)
{
_playerFaction.DeleteDirective(_world, directiveId);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerPolicy(string? policyId, PlayerPolicyCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertPolicy(_world, policyId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerAutomationPolicy(string? automationPolicyId, PlayerAutomationPolicyCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertAutomationPolicy(_world, automationPolicyId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerReinforcementPolicy(string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertReinforcementPolicy(_world, reinforcementPolicyId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerProductionProgram(string? productionProgramId, PlayerProductionProgramCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertProductionProgram(_world, productionProgramId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpsertPlayerAssignment(string assetId, PlayerAssetAssignmentCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpsertAssignment(_world, assetId, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public PlayerFactionSnapshot? UpdatePlayerStrategicIntent(PlayerStrategicIntentCommandRequest request)
{
lock (_sync)
{
_playerFaction.UpdateStrategicIntent(_world, request);
return GetPlayerFactionSnapshotUnsafe();
}
}
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false,
});
Guid subscriberId;
lock (_sync)
{
subscriberId = Guid.NewGuid();
_subscribers.Add(subscriberId, new SubscriptionState(scope, channel));
foreach (var delta in _history.Where((candidate) => candidate.Sequence > afterSequence))
{
var filtered = FilterDeltaForScope(delta, scope);
if (HasMeaningfulDelta(filtered))
{
channel.Writer.TryWrite(filtered);
}
}
}
cancellationToken.Register(() => Unsubscribe(subscriberId));
return channel.Reader;
}
public void Tick(float deltaSeconds)
{
WorldDelta? delta = null;
lock (_sync)
{
delta = _engine.Tick(_world, deltaSeconds, ++_sequence);
if (!HasMeaningfulDelta(delta))
{
return;
}
_history.Enqueue(delta);
while (_history.Count > DeltaHistoryLimit)
{
_history.Dequeue();
}
foreach (var subscriber in _subscribers.Values.ToList())
{
var filtered = FilterDeltaForScope(delta, subscriber.Scope);
if (HasMeaningfulDelta(filtered))
{
subscriber.Channel.Writer.TryWrite(filtered);
}
}
}
}
public WorldSnapshot Reset()
{
lock (_sync)
{
_world = _loader.Load();
if (_balanceOverride is not null)
{
ApplyBalance(_world, _balanceOverride);
}
_sequence += 1;
_history.Clear();
var resetDelta = new WorldDelta(
_sequence,
_world.TickIntervalMs,
_world.OrbitalTimeSeconds,
_orbitalSimulation,
DateTimeOffset.UtcNow,
true,
[new SimulationEventRecord("world", "world", "reset", "World reset requested", DateTimeOffset.UtcNow, "world", "universe", "world")],
[],
[],
[],
[],
[],
[],
[],
[],
[],
null,
null);
_history.Enqueue(resetDelta);
foreach (var subscriber in _subscribers.Values.ToList())
{
subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(resetDelta, subscriber.Scope));
}
return _engine.BuildSnapshot(_world, _sequence);
}
}
private static void ApplyBalance(SimulationWorld world, BalanceDefinition balance) =>
world.Balance = new BalanceDefinition
{ {
Events = delta.Events.Select((evt) => EnrichEventScope(evt)).ToList(), SimulationSpeedMultiplier = balance.SimulationSpeedMultiplier,
Scope = scope, YPlane = balance.YPlane,
ArrivalThreshold = balance.ArrivalThreshold,
MiningRate = balance.MiningRate,
MiningCycleSeconds = balance.MiningCycleSeconds,
TransferRate = balance.TransferRate,
DockingDuration = balance.DockingDuration,
UndockingDuration = balance.UndockingDuration,
UndockDistance = balance.UndockDistance,
}; };
private static BalanceDefinition SanitizeBalance(BalanceDefinition candidate)
{
static float finiteOr(float value, float fallback) =>
float.IsFinite(value) ? value : fallback;
return new BalanceDefinition
{
SimulationSpeedMultiplier = MathF.Max(0.01f, finiteOr(candidate.SimulationSpeedMultiplier, 1f)),
YPlane = MathF.Max(0f, finiteOr(candidate.YPlane, 0f)),
ArrivalThreshold = MathF.Max(0.1f, finiteOr(candidate.ArrivalThreshold, 16f)),
MiningRate = MathF.Max(0f, finiteOr(candidate.MiningRate, 10f)),
MiningCycleSeconds = MathF.Max(0.1f, finiteOr(candidate.MiningCycleSeconds, 10f)),
TransferRate = MathF.Max(0f, finiteOr(candidate.TransferRate, 56f)),
DockingDuration = MathF.Max(0.1f, finiteOr(candidate.DockingDuration, 1.2f)),
UndockingDuration = MathF.Max(0.1f, finiteOr(candidate.UndockingDuration, 1.2f)),
UndockDistance = MathF.Max(0f, finiteOr(candidate.UndockDistance, 42f)),
};
} }
var systemFilter = scope.SystemId; private ShipSnapshot? GetShipSnapshotUnsafe(string shipId) =>
if (string.Equals(scope.ScopeKind, "local-celestial", StringComparison.OrdinalIgnoreCase) && systemFilter is null && scope.CelestialId is not null) _engine.BuildSnapshot(_world, _sequence).Ships.FirstOrDefault(ship => ship.Id == shipId);
private PlayerFactionSnapshot? GetPlayerFactionSnapshotUnsafe() =>
_engine.BuildSnapshot(_world, _sequence).PlayerFaction;
private static bool HasMeaningfulDelta(WorldDelta delta) =>
delta.RequiresSnapshotRefresh
|| delta.Events.Count > 0
|| delta.Celestials.Count > 0
|| delta.Nodes.Count > 0
|| delta.Stations.Count > 0
|| delta.Claims.Count > 0
|| delta.ConstructionSites.Count > 0
|| delta.MarketOrders.Count > 0
|| delta.Policies.Count > 0
|| delta.Ships.Count > 0
|| delta.Factions.Count > 0
|| delta.PlayerFaction is not null
|| delta.Geopolitics is not null;
private void Unsubscribe(Guid subscriberId)
{ {
systemFilter = ResolveCelestialSystemId(scope.CelestialId); lock (_sync)
{
if (!_subscribers.Remove(subscriberId, out var subscription))
{
return;
}
subscription.Channel.Writer.TryComplete();
}
} }
return delta with private WorldDelta FilterDeltaForScope(WorldDelta delta, ObserverScope scope)
{ {
Events = delta.Events if (string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase))
.Select((evt) => EnrichEventScope(evt)) {
.Where((evt) => IsEventVisibleToScope(evt, scope, systemFilter)) return delta with
.ToList(), {
Celestials = delta.Celestials.Where((celestial) => systemFilter is null || celestial.SystemId == systemFilter).ToList(), Events = delta.Events.Select((evt) => EnrichEventScope(evt)).ToList(),
Nodes = delta.Nodes.Where((node) => systemFilter is null || node.SystemId == systemFilter).ToList(), Scope = scope,
Stations = delta.Stations.Where((station) => systemFilter is null || station.SystemId == systemFilter).ToList(), };
Claims = delta.Claims.Where((claim) => systemFilter is null || claim.SystemId == systemFilter).ToList(), }
ConstructionSites = delta.ConstructionSites.Where((site) => systemFilter is null || site.SystemId == systemFilter).ToList(),
MarketOrders = delta.MarketOrders.Where((order) => IsOrderVisibleToScope(order, systemFilter)).ToList(),
Policies = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Policies : [],
Ships = delta.Ships.Where((ship) => systemFilter is null || ship.SystemId == systemFilter).ToList(),
Factions = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Factions : [],
PlayerFaction = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.PlayerFaction : null,
Geopolitics = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Geopolitics : null,
Scope = scope,
};
}
private SimulationEventRecord EnrichEventScope(SimulationEventRecord evt) var systemFilter = scope.SystemId;
{ if (string.Equals(scope.ScopeKind, "local-celestial", StringComparison.OrdinalIgnoreCase) && systemFilter is null && scope.CelestialId is not null)
if (!string.Equals(evt.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) || evt.ScopeEntityId is not null) {
{ systemFilter = ResolveCelestialSystemId(scope.CelestialId);
return evt; }
return delta with
{
Events = delta.Events
.Select((evt) => EnrichEventScope(evt))
.Where((evt) => IsEventVisibleToScope(evt, scope, systemFilter))
.ToList(),
Celestials = delta.Celestials.Where((celestial) => systemFilter is null || celestial.SystemId == systemFilter).ToList(),
Nodes = delta.Nodes.Where((node) => systemFilter is null || node.SystemId == systemFilter).ToList(),
Stations = delta.Stations.Where((station) => systemFilter is null || station.SystemId == systemFilter).ToList(),
Claims = delta.Claims.Where((claim) => systemFilter is null || claim.SystemId == systemFilter).ToList(),
ConstructionSites = delta.ConstructionSites.Where((site) => systemFilter is null || site.SystemId == systemFilter).ToList(),
MarketOrders = delta.MarketOrders.Where((order) => IsOrderVisibleToScope(order, systemFilter)).ToList(),
Policies = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Policies : [],
Ships = delta.Ships.Where((ship) => systemFilter is null || ship.SystemId == systemFilter).ToList(),
Factions = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Factions : [],
PlayerFaction = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.PlayerFaction : null,
Geopolitics = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Geopolitics : null,
Scope = scope,
};
} }
return evt.EntityKind switch private SimulationEventRecord EnrichEventScope(SimulationEventRecord evt)
{ {
"ship" => WithEntityScope(evt, "system", _world.Ships.FirstOrDefault((ship) => ship.Id == evt.EntityId)?.SystemId), if (!string.Equals(evt.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) || evt.ScopeEntityId is not null)
"station" => WithEntityScope(evt, "system", _world.Stations.FirstOrDefault((station) => station.Id == evt.EntityId)?.SystemId), {
"node" => WithEntityScope(evt, "system", _world.Nodes.FirstOrDefault((node) => node.Id == evt.EntityId)?.SystemId), return evt;
"celestial" => WithEntityScope(evt, "system", _world.Celestials.FirstOrDefault((c) => c.Id == evt.EntityId)?.SystemId), }
"claim" => WithEntityScope(evt, "system", _world.Claims.FirstOrDefault((claim) => claim.Id == evt.EntityId)?.SystemId),
"construction-site" => WithEntityScope(evt, "system", _world.ConstructionSites.FirstOrDefault((site) => site.Id == evt.EntityId)?.SystemId),
"market-order" => WithEntityScope(evt, "system", ResolveMarketOrderSystemId(evt.EntityId)),
_ => evt,
};
}
private static SimulationEventRecord WithEntityScope(SimulationEventRecord evt, string scopeKind, string? scopeEntityId) => return evt.EntityKind switch
evt with {
{ "ship" => WithEntityScope(evt, "system", _world.Ships.FirstOrDefault((ship) => ship.Id == evt.EntityId)?.SystemId),
Family = evt.Kind.Contains("power", StringComparison.Ordinal) ? "power" : "station" => WithEntityScope(evt, "system", _world.Stations.FirstOrDefault((station) => station.Id == evt.EntityId)?.SystemId),
evt.Kind.Contains("construction", StringComparison.Ordinal) ? "construction" : "node" => WithEntityScope(evt, "system", _world.Nodes.FirstOrDefault((node) => node.Id == evt.EntityId)?.SystemId),
evt.Kind.Contains("population", StringComparison.Ordinal) ? "population" : "celestial" => WithEntityScope(evt, "system", _world.Celestials.FirstOrDefault((c) => c.Id == evt.EntityId)?.SystemId),
evt.Kind.Contains("claim", StringComparison.Ordinal) ? "claim" : "claim" => WithEntityScope(evt, "system", _world.Claims.FirstOrDefault((claim) => claim.Id == evt.EntityId)?.SystemId),
"simulation", "construction-site" => WithEntityScope(evt, "system", _world.ConstructionSites.FirstOrDefault((site) => site.Id == evt.EntityId)?.SystemId),
ScopeKind = scopeKind, "market-order" => WithEntityScope(evt, "system", ResolveMarketOrderSystemId(evt.EntityId)),
ScopeEntityId = scopeEntityId, _ => evt,
}; };
private string? ResolveCelestialSystemId(string celestialId) =>
_world.Celestials.FirstOrDefault((c) => c.Id == celestialId)?.SystemId;
private string? ResolveMarketOrderSystemId(string orderId)
{
var order = _world.MarketOrders.FirstOrDefault((candidate) => candidate.Id == orderId);
if (order?.StationId is not null)
{
return _world.Stations.FirstOrDefault((station) => station.Id == order.StationId)?.SystemId;
} }
if (order?.ConstructionSiteId is not null) private static SimulationEventRecord WithEntityScope(SimulationEventRecord evt, string scopeKind, string? scopeEntityId) =>
evt with
{
Family = evt.Kind.Contains("power", StringComparison.Ordinal) ? "power" :
evt.Kind.Contains("construction", StringComparison.Ordinal) ? "construction" :
evt.Kind.Contains("population", StringComparison.Ordinal) ? "population" :
evt.Kind.Contains("claim", StringComparison.Ordinal) ? "claim" :
"simulation",
ScopeKind = scopeKind,
ScopeEntityId = scopeEntityId,
};
private string? ResolveCelestialSystemId(string celestialId) =>
_world.Celestials.FirstOrDefault((c) => c.Id == celestialId)?.SystemId;
private string? ResolveMarketOrderSystemId(string orderId)
{ {
return _world.ConstructionSites.FirstOrDefault((site) => site.Id == order.ConstructionSiteId)?.SystemId; var order = _world.MarketOrders.FirstOrDefault((candidate) => candidate.Id == orderId);
if (order?.StationId is not null)
{
return _world.Stations.FirstOrDefault((station) => station.Id == order.StationId)?.SystemId;
}
if (order?.ConstructionSiteId is not null)
{
return _world.ConstructionSites.FirstOrDefault((site) => site.Id == order.ConstructionSiteId)?.SystemId;
}
return null;
} }
return null; private bool IsOrderVisibleToScope(MarketOrderDelta order, string? systemFilter)
}
private bool IsOrderVisibleToScope(MarketOrderDelta order, string? systemFilter)
{
if (systemFilter is null)
{ {
return true; if (systemFilter is null)
{
return true;
}
if (order.StationId is not null)
{
return _world.Stations.Any((station) => station.Id == order.StationId && station.SystemId == systemFilter);
}
if (order.ConstructionSiteId is not null)
{
return _world.ConstructionSites.Any((site) => site.Id == order.ConstructionSiteId && site.SystemId == systemFilter);
}
return false;
} }
if (order.StationId is not null) private static bool IsEventVisibleToScope(SimulationEventRecord evt, ObserverScope scope, string? systemFilter)
{ {
return _world.Stations.Any((station) => station.Id == order.StationId && station.SystemId == systemFilter); return scope.ScopeKind switch
{
"universe" => true,
"system" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
"local-celestial" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
_ => true,
};
} }
if (order.ConstructionSiteId is not null) private sealed record SubscriptionState(ObserverScope Scope, Channel<WorldDelta> Channel);
{
return _world.ConstructionSites.Any((site) => site.Id == order.ConstructionSiteId && site.SystemId == systemFilter);
}
return false;
}
private static bool IsEventVisibleToScope(SimulationEventRecord evt, ObserverScope scope, string? systemFilter)
{
return scope.ScopeKind switch
{
"universe" => true,
"system" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
"local-celestial" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
_ => true,
};
}
private sealed record SubscriptionState(ObserverScope Scope, Channel<WorldDelta> Channel);
} }