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

View File

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

View File

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

View File

@@ -2,53 +2,53 @@ namespace SpaceGame.Api.Industry.Planning;
internal static class CommodityOperationalSignal
{
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
internal static float ComputeNeedScore(FactionCommoditySnapshot commodity, float targetLevelSeconds)
{
CommodityLevelKind.Critical => 140f,
CommodityLevelKind.Low => 80f,
CommodityLevelKind.Stable => 20f,
_ => 0f,
};
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);
return levelWeight
+ (productionDeficit * 140f)
+ (levelDeficit * 120f)
+ backlogPressure;
}
var levelWeight = commodity.Level switch
{
CommodityLevelKind.Critical => 140f,
CommodityLevelKind.Low => 80f,
CommodityLevelKind.Stable => 20f,
_ => 0f,
};
internal static bool IsOperational(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
commodity.ProjectedProductionRatePerSecond > 0.01f
&& commodity.ProjectedNetRatePerSecond >= -0.01f
&& commodity.LevelSeconds >= targetLevelSeconds
&& 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;
return levelWeight
+ (productionDeficit * 140f)
+ (levelDeficit * 120f)
+ backlogPressure;
}
if (commodity.Level is CommodityLevelKind.Critical)
{
return 0.72f;
}
internal static bool IsOperational(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
commodity.ProjectedProductionRatePerSecond > 0.01f
&& commodity.ProjectedNetRatePerSecond >= -0.01f
&& commodity.LevelSeconds >= targetLevelSeconds
&& commodity.Level is CommodityLevelKind.Stable or CommodityLevelKind.Surplus;
if (commodity.Level is CommodityLevelKind.Low || commodity.LevelSeconds < targetLevelSeconds)
{
return 0.84f;
}
internal static bool IsStrained(FactionCommoditySnapshot commodity, float targetLevelSeconds) =>
!IsOperational(commodity, targetLevelSeconds)
|| 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
{
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)
{
if (!commodities.TryGetValue(itemId, out var commodity))
internal FactionCommoditySnapshot GetCommodity(string itemId)
{
commodity = new FactionCommoditySnapshot(itemId);
commodities[itemId] = commodity;
}
if (!commodities.TryGetValue(itemId, out var commodity))
{
commodity = new FactionCommoditySnapshot(itemId);
commodities[itemId] = commodity;
}
return commodity;
}
return commodity;
}
}
internal sealed class FactionCommoditySnapshot
{
internal FactionCommoditySnapshot(string itemId)
{
ItemId = itemId;
}
internal FactionCommoditySnapshot(string itemId)
{
ItemId = itemId;
}
internal string ItemId { get; }
internal float OnHand { get; set; }
internal float ReservedForConstruction { get; set; }
internal float BuyBacklog { get; set; }
internal float SellBacklog { get; set; }
internal float Inbound { get; set; }
internal float ProductionRatePerSecond { get; set; }
internal float CommittedProductionRatePerSecond { get; set; }
internal float ConsumptionRatePerSecond { get; set; }
internal string ItemId { get; }
internal float OnHand { get; set; }
internal float ReservedForConstruction { get; set; }
internal float BuyBacklog { get; set; }
internal float SellBacklog { get; set; }
internal float Inbound { get; set; }
internal float ProductionRatePerSecond { get; set; }
internal float CommittedProductionRatePerSecond { get; set; }
internal float ConsumptionRatePerSecond { get; set; }
internal float AvailableStock => MathF.Max(0f, OnHand + Inbound - ReservedForConstruction);
internal float NetRatePerSecond => ProductionRatePerSecond - ConsumptionRatePerSecond;
internal float ProjectedProductionRatePerSecond => ProductionRatePerSecond + CommittedProductionRatePerSecond;
internal float ProjectedNetRatePerSecond => ProjectedProductionRatePerSecond - ConsumptionRatePerSecond;
internal float OperationalUsageRatePerSecond => MathF.Max(ConsumptionRatePerSecond, BuyBacklog / 180f);
internal float LevelSeconds => AvailableStock <= 0.01f
? 0f
: AvailableStock / MathF.Max(OperationalUsageRatePerSecond, 0.01f);
internal float AvailableStock => MathF.Max(0f, OnHand + Inbound - ReservedForConstruction);
internal float NetRatePerSecond => ProductionRatePerSecond - ConsumptionRatePerSecond;
internal float ProjectedProductionRatePerSecond => ProductionRatePerSecond + CommittedProductionRatePerSecond;
internal float ProjectedNetRatePerSecond => ProjectedProductionRatePerSecond - ConsumptionRatePerSecond;
internal float OperationalUsageRatePerSecond => MathF.Max(ConsumptionRatePerSecond, BuyBacklog / 180f);
internal float LevelSeconds => AvailableStock <= 0.01f
? 0f
: AvailableStock / MathF.Max(OperationalUsageRatePerSecond, 0.01f);
internal CommodityLevelKind Level =>
LevelSeconds switch
{
<= 60f => CommodityLevelKind.Critical,
<= 180f => CommodityLevelKind.Low,
<= 480f => CommodityLevelKind.Stable,
_ => CommodityLevelKind.Surplus,
};
internal CommodityLevelKind Level =>
LevelSeconds switch
{
<= 60f => CommodityLevelKind.Critical,
<= 180f => CommodityLevelKind.Low,
<= 480f => CommodityLevelKind.Stable,
_ => CommodityLevelKind.Surplus,
};
}
internal enum CommodityLevelKind
{
Critical,
Low,
Stable,
Surplus,
Critical,
Low,
Stable,
Surplus,
}
internal static class FactionEconomyAnalyzer
{
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)))
internal static FactionEconomySnapshot Build(SimulationWorld world, string factionId)
{
foreach (var (itemId, amount) in station.Inventory)
{
snapshot.GetCommodity(itemId).OnHand += amount;
}
var snapshot = new FactionEconomySnapshot();
foreach (var laneKey in StationSimulationService.GetStationProductionLanes(world, station))
{
var recipe = StationSimulationService.SelectProductionRecipe(world, station, laneKey);
if (recipe is null)
foreach (var station in world.Stations.Where(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal)))
{
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);
var cyclesPerSecond = (station.WorkforceEffectiveRatio * throughput) / MathF.Max(recipe.Duration, 0.01f);
if (cyclesPerSecond <= 0.0001f)
foreach (var order in world.MarketOrders.Where(order =>
string.Equals(order.FactionId, factionId, StringComparison.Ordinal)
&& 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 =>
string.Equals(order.FactionId, factionId, StringComparison.Ordinal)
&& order.State != MarketOrderStateKinds.Cancelled
&& order.RemainingAmount > 0.01f))
{
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 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)
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)
{
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 required IReadOnlyDictionary<string, ProductionCommodityNode> Commodities { 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>> ProcessesByInputId { get; init; }
public required IReadOnlyDictionary<string, IReadOnlyList<string>> OutputsByModuleId { get; init; }
public required IReadOnlyDictionary<string, ProductionCommodityNode> Commodities { 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>> ProcessesByInputId { get; init; }
public required IReadOnlyDictionary<string, IReadOnlyList<string>> OutputsByModuleId { get; init; }
public IReadOnlyList<ProductionProcessNode> GetProcessesForOutput(string itemId) =>
ProcessesByOutputId.TryGetValue(itemId, out var processes) ? processes : [];
public IReadOnlyList<ProductionProcessNode> GetProcessesForOutput(string itemId) =>
ProcessesByOutputId.TryGetValue(itemId, out var processes) ? processes : [];
public IReadOnlyList<ProductionProcessNode> GetProcessesForInput(string itemId) =>
ProcessesByInputId.TryGetValue(itemId, out var processes) ? processes : [];
public IReadOnlyList<ProductionProcessNode> GetProcessesForInput(string itemId) =>
ProcessesByInputId.TryGetValue(itemId, out var processes) ? processes : [];
public string? GetPrimaryProducerModule(string itemId) =>
GetProcessesForOutput(itemId)
.SelectMany(process => process.RequiredModuleIds)
.FirstOrDefault();
public string? GetPrimaryProducerModule(string itemId) =>
GetProcessesForOutput(itemId)
.SelectMany(process => process.RequiredModuleIds)
.FirstOrDefault();
public string? GetPrimaryOutputForModule(string moduleId) =>
OutputsByModuleId.TryGetValue(moduleId, out var outputs)
? outputs.FirstOrDefault()
: null;
public string? GetPrimaryOutputForModule(string moduleId) =>
OutputsByModuleId.TryGetValue(moduleId, out var outputs)
? outputs.FirstOrDefault()
: null;
public IReadOnlyList<string> GetImmediateInputs(string itemId) =>
GetProcessesForOutput(itemId)
.SelectMany(process => process.Inputs.Keys)
.Distinct(StringComparer.Ordinal)
.ToList();
public IReadOnlyList<string> GetImmediateInputs(string itemId) =>
GetProcessesForOutput(itemId)
.SelectMany(process => process.Inputs.Keys)
.Distinct(StringComparer.Ordinal)
.ToList();
}
public sealed class ProductionCommodityNode
{
public required string ItemId { get; init; }
public required string Name { get; init; }
public required string Group { get; init; }
public required string CargoKind { get; init; }
public List<string> ProducerProcessIds { get; } = [];
public List<string> ConsumerProcessIds { get; } = [];
public required string ItemId { get; init; }
public required string Name { get; init; }
public required string Group { get; init; }
public required string CargoKind { get; init; }
public List<string> ProducerProcessIds { get; } = [];
public List<string> ConsumerProcessIds { get; } = [];
}
public sealed class ProductionProcessNode
{
public required string Id { get; init; }
public required string Label { get; init; }
public required string FacilityCategory { get; init; }
public required IReadOnlyList<string> RequiredModuleIds { get; init; }
public required IReadOnlyDictionary<string, float> Inputs { get; init; }
public required IReadOnlyDictionary<string, float> Outputs { get; init; }
public required bool ProducesShip { get; init; }
public required string Id { get; init; }
public required string Label { get; init; }
public required string FacilityCategory { get; init; }
public required IReadOnlyList<string> RequiredModuleIds { get; init; }
public required IReadOnlyDictionary<string, float> Inputs { get; init; }
public required IReadOnlyDictionary<string, float> Outputs { 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 ProductionGraph Build(
IReadOnlyCollection<ItemDefinition> items,
IReadOnlyCollection<RecipeDefinition> recipes,
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)
internal static ProductionGraph Build(
IReadOnlyCollection<ItemDefinition> items,
IReadOnlyCollection<RecipeDefinition> recipes,
IReadOnlyCollection<ModuleDefinition> modules)
{
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,
};
var commodities = items.ToDictionary(
item => item.Id,
item => new ProductionCommodityNode
{
ItemId = item.Id,
Name = item.Name,
Group = item.Group,
CargoKind = item.CargoKind,
},
StringComparer.Ordinal);
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)
{
if (!commodities.ContainsKey(output))
foreach (var recipe in recipes)
{
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);
if (!processesByOutputId.TryGetValue(output, out var outputProcesses))
foreach (var module in modules)
{
outputProcesses = [];
processesByOutputId[output] = outputProcesses;
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);
}
}
outputProcesses.Add(process);
}
foreach (var input in inputs.Keys)
{
if (!commodities.ContainsKey(input))
return new ProductionGraph
{
continue;
}
commodities[input].ConsumerProcessIds.Add(process.Id);
if (!processesByInputId.TryGetValue(input, out var inputProcesses))
{
inputProcesses = [];
processesByInputId[input] = inputProcesses;
}
inputProcesses.Add(process);
}
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),
};
}
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 override void Configure()
{
Post("/api/player-faction/organizations");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerOrganizationCommandRequest request, CancellationToken cancellationToken)
{
try
public override void Configure()
{
var snapshot = worldService.CreatePlayerOrganization(request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
Post("/api/player-faction/organizations");
AllowAnonymous();
}
await SendOkAsync(snapshot, cancellationToken);
}
catch (InvalidOperationException ex)
public override async Task HandleAsync(PlayerOrganizationCommandRequest request, CancellationToken cancellationToken)
{
AddError(ex.Message);
await SendErrorsAsync(cancellation: cancellationToken);
try
{
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 string DirectiveId { get; set; } = string.Empty;
public string DirectiveId { get; set; } = string.Empty;
}
public sealed class DeletePlayerDirectiveHandler(WorldService worldService) : Endpoint<DeletePlayerDirectiveRequest, PlayerFactionSnapshot>
{
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)
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Delete("/api/player-faction/directives/{directiveId}");
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 string OrganizationId { get; set; } = string.Empty;
public string OrganizationId { get; set; } = string.Empty;
}
public sealed class DeletePlayerOrganizationHandler(WorldService worldService) : Endpoint<DeletePlayerOrganizationRequest, PlayerFactionSnapshot>
{
public override void Configure()
{
Delete("/api/player-faction/organizations/{organizationId}");
AllowAnonymous();
}
public override async Task HandleAsync(DeletePlayerOrganizationRequest request, CancellationToken cancellationToken)
{
try
public override void Configure()
{
var snapshot = worldService.DeletePlayerOrganization(request.OrganizationId);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
Delete("/api/player-faction/organizations/{organizationId}");
AllowAnonymous();
}
await SendOkAsync(snapshot, cancellationToken);
}
catch (InvalidOperationException ex)
public override async Task HandleAsync(DeletePlayerOrganizationRequest request, CancellationToken cancellationToken)
{
AddError(ex.Message);
await SendErrorsAsync(cancellation: cancellationToken);
try
{
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 override void Configure()
{
Get("/api/player-faction");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var snapshot = worldService.GetPlayerFaction();
if (snapshot is null)
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Get("/api/player-faction");
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 override void Configure()
{
Put("/api/player-faction/organizations/{organizationId}/membership");
AllowAnonymous();
}
public override async Task HandleAsync(PlayerOrganizationMembershipCommandRequest request, CancellationToken cancellationToken)
{
try
public override void Configure()
{
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);
Put("/api/player-faction/organizations/{organizationId}/membership");
AllowAnonymous();
}
catch (InvalidOperationException ex)
public override async Task HandleAsync(PlayerOrganizationMembershipCommandRequest request, CancellationToken cancellationToken)
{
AddError(ex.Message);
await SendErrorsAsync(cancellation: cancellationToken);
try
{
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 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)
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Put("/api/player-faction/strategic-intent");
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 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))
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Put("/api/player-faction/assets/{assetId}/assignment");
AllowAnonymous();
}
var snapshot = worldService.UpsertPlayerAssignment(assetId, request);
if (snapshot is null)
public override async Task HandleAsync(PlayerAssetAssignmentCommandRequest request, CancellationToken cancellationToken)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var assetId = Route<string>("assetId");
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 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)
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Post("/api/player-faction/automation-policies");
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 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)
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Post("/api/player-faction/directives");
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 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)
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Post("/api/player-faction/policies");
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 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)
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Post("/api/player-faction/production-programs");
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 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)
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Post("/api/player-faction/reinforcement-policies");
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 required string Id { get; init; }
public required string Label { get; set; }
public required string SovereignFactionId { get; set; }
public string Status { get; set; } = "active";
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public PlayerAssetRegistryRuntime AssetRegistry { get; set; } = new();
public PlayerStrategicIntentRuntime StrategicIntent { get; set; } = new();
public List<PlayerFleetRuntime> Fleets { get; } = [];
public List<PlayerTaskForceRuntime> TaskForces { get; } = [];
public List<PlayerStationGroupRuntime> StationGroups { get; } = [];
public List<PlayerEconomicRegionRuntime> EconomicRegions { get; } = [];
public List<PlayerFrontRuntime> Fronts { get; } = [];
public List<PlayerReserveGroupRuntime> Reserves { get; } = [];
public List<PlayerFactionPolicyRuntime> Policies { get; } = [];
public List<PlayerAutomationPolicyRuntime> AutomationPolicies { get; } = [];
public List<PlayerReinforcementPolicyRuntime> ReinforcementPolicies { get; } = [];
public List<PlayerProductionProgramRuntime> ProductionPrograms { get; } = [];
public List<PlayerDirectiveRuntime> Directives { get; } = [];
public List<PlayerAssignmentRuntime> Assignments { get; } = [];
public List<PlayerDecisionLogEntryRuntime> DecisionLog { get; } = [];
public List<PlayerAlertRuntime> Alerts { get; } = [];
public string LastDeltaSignature { get; set; } = string.Empty;
public required string Id { get; init; }
public required string Label { get; set; }
public required string SovereignFactionId { get; set; }
public string Status { get; set; } = "active";
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public PlayerAssetRegistryRuntime AssetRegistry { get; set; } = new();
public PlayerStrategicIntentRuntime StrategicIntent { get; set; } = new();
public List<PlayerFleetRuntime> Fleets { get; } = [];
public List<PlayerTaskForceRuntime> TaskForces { get; } = [];
public List<PlayerStationGroupRuntime> StationGroups { get; } = [];
public List<PlayerEconomicRegionRuntime> EconomicRegions { get; } = [];
public List<PlayerFrontRuntime> Fronts { get; } = [];
public List<PlayerReserveGroupRuntime> Reserves { get; } = [];
public List<PlayerFactionPolicyRuntime> Policies { get; } = [];
public List<PlayerAutomationPolicyRuntime> AutomationPolicies { get; } = [];
public List<PlayerReinforcementPolicyRuntime> ReinforcementPolicies { get; } = [];
public List<PlayerProductionProgramRuntime> ProductionPrograms { get; } = [];
public List<PlayerDirectiveRuntime> Directives { get; } = [];
public List<PlayerAssignmentRuntime> Assignments { get; } = [];
public List<PlayerDecisionLogEntryRuntime> DecisionLog { get; } = [];
public List<PlayerAlertRuntime> Alerts { get; } = [];
public string LastDeltaSignature { get; set; } = string.Empty;
}
public sealed class PlayerAssetRegistryRuntime
{
public HashSet<string> ShipIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> StationIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ClaimIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ConstructionSiteIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> PolicySetIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> FleetIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> TaskForceIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> StationGroupIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> EconomicRegionIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> FrontIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ReserveIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ShipIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> StationIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ClaimIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ConstructionSiteIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> PolicySetIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> FleetIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> TaskForceIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> StationGroupIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> EconomicRegionIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> FrontIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> ReserveIds { get; } = new(StringComparer.Ordinal);
}
public sealed class PlayerStrategicIntentRuntime
{
public string StrategicPosture { get; set; } = "balanced";
public string EconomicPosture { get; set; } = "delegated";
public string MilitaryPosture { get; set; } = "layered-defense";
public string LogisticsPosture { get; set; } = "stable";
public float DesiredReserveRatio { get; set; } = 0.2f;
public bool AllowDelegatedCombatAutomation { get; set; } = true;
public bool AllowDelegatedEconomicAutomation { get; set; } = true;
public string? Notes { get; set; }
public string StrategicPosture { get; set; } = "balanced";
public string EconomicPosture { get; set; } = "delegated";
public string MilitaryPosture { get; set; } = "layered-defense";
public string LogisticsPosture { get; set; } = "stable";
public float DesiredReserveRatio { get; set; } = 0.2f;
public bool AllowDelegatedCombatAutomation { get; set; } = true;
public bool AllowDelegatedEconomicAutomation { get; set; } = true;
public string? Notes { get; set; }
}
public sealed class PlayerFleetRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Role { get; set; } = "general-purpose";
public string? CommanderId { get; set; }
public string? FrontId { get; set; }
public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; }
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public string? ReinforcementPolicyId { get; set; }
public List<string> AssetIds { get; } = [];
public List<string> TaskForceIds { get; } = [];
public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Role { get; set; } = "general-purpose";
public string? CommanderId { get; set; }
public string? FrontId { get; set; }
public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; }
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public string? ReinforcementPolicyId { get; set; }
public List<string> AssetIds { get; } = [];
public List<string> TaskForceIds { get; } = [];
public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerTaskForceRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Role { get; set; } = "task-force";
public string? FleetId { get; set; }
public string? CommanderId { get; set; }
public string? FrontId { get; set; }
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public List<string> AssetIds { get; } = [];
public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Role { get; set; } = "task-force";
public string? FleetId { get; set; }
public string? CommanderId { get; set; }
public string? FrontId { get; set; }
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public List<string> AssetIds { get; } = [];
public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerStationGroupRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Role { get; set; } = "industrial-group";
public string? EconomicRegionId { get; set; }
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public List<string> StationIds { get; } = [];
public List<string> DirectiveIds { get; } = [];
public List<string> FocusItemIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Role { get; set; } = "industrial-group";
public string? EconomicRegionId { get; set; }
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public List<string> StationIds { get; } = [];
public List<string> DirectiveIds { get; } = [];
public List<string> FocusItemIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerEconomicRegionRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Role { get; set; } = "balanced-region";
public string? SharedEconomicRegionId { get; set; }
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public List<string> SystemIds { get; } = [];
public List<string> StationGroupIds { get; } = [];
public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Role { get; set; } = "balanced-region";
public string? SharedEconomicRegionId { get; set; }
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public List<string> SystemIds { get; } = [];
public List<string> StationGroupIds { get; } = [];
public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerFrontRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public float Priority { get; set; } = 50f;
public string Posture { get; set; } = "hold";
public string? SharedFrontLineId { get; set; }
public string? TargetFactionId { get; set; }
public List<string> SystemIds { get; } = [];
public List<string> FleetIds { get; } = [];
public List<string> ReserveIds { get; } = [];
public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public float Priority { get; set; } = 50f;
public string Posture { get; set; } = "hold";
public string? SharedFrontLineId { get; set; }
public string? TargetFactionId { get; set; }
public List<string> SystemIds { get; } = [];
public List<string> FleetIds { get; } = [];
public List<string> ReserveIds { get; } = [];
public List<string> DirectiveIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerReserveGroupRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "ready";
public string ReserveKind { get; set; } = "military";
public string? HomeSystemId { get; set; }
public string? PolicyId { get; set; }
public List<string> AssetIds { get; } = [];
public List<string> FrontIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "ready";
public string ReserveKind { get; set; } = "military";
public string? HomeSystemId { get; set; }
public string? PolicyId { get; set; }
public List<string> AssetIds { get; } = [];
public List<string> FrontIds { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerFactionPolicyRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string ScopeKind { get; set; } = "player-faction";
public string? ScopeId { get; set; }
public string? PolicySetId { get; set; }
public bool AllowDelegatedCombat { get; set; } = true;
public bool AllowDelegatedTrade { get; set; } = true;
public float ReserveCreditsRatio { get; set; } = 0.2f;
public float ReserveMilitaryRatio { get; set; } = 0.2f;
public string TradeAccessPolicy { get; set; } = "owner-and-allies";
public string DockingAccessPolicy { get; set; } = "owner-and-allies";
public string ConstructionAccessPolicy { get; set; } = "owner-only";
public string OperationalRangePolicy { get; set; } = "unrestricted";
public string CombatEngagementPolicy { get; set; } = "defensive";
public bool AvoidHostileSystems { get; set; } = true;
public float FleeHullRatio { get; set; } = 0.35f;
public HashSet<string> BlacklistedSystemIds { get; } = new(StringComparer.Ordinal);
public string? Notes { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string ScopeKind { get; set; } = "player-faction";
public string? ScopeId { get; set; }
public string? PolicySetId { get; set; }
public bool AllowDelegatedCombat { get; set; } = true;
public bool AllowDelegatedTrade { get; set; } = true;
public float ReserveCreditsRatio { get; set; } = 0.2f;
public float ReserveMilitaryRatio { get; set; } = 0.2f;
public string TradeAccessPolicy { get; set; } = "owner-and-allies";
public string DockingAccessPolicy { get; set; } = "owner-and-allies";
public string ConstructionAccessPolicy { get; set; } = "owner-only";
public string OperationalRangePolicy { get; set; } = "unrestricted";
public string CombatEngagementPolicy { get; set; } = "defensive";
public bool AvoidHostileSystems { get; set; } = true;
public float FleeHullRatio { get; set; } = 0.35f;
public HashSet<string> BlacklistedSystemIds { get; } = new(StringComparer.Ordinal);
public string? Notes { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerAutomationPolicyRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string ScopeKind { get; set; } = "player-faction";
public string? ScopeId { get; set; }
public bool Enabled { get; set; } = true;
public string BehaviorKind { get; set; } = "idle";
public bool UseOrders { get; set; }
public string? StagingOrderKind { get; set; }
public int MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public float Radius { get; set; } = 24f;
public float WaitSeconds { get; set; } = 3f;
public string? PreferredItemId { get; set; }
public string? Notes { get; set; }
public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string ScopeKind { get; set; } = "player-faction";
public string? ScopeId { get; set; }
public bool Enabled { get; set; } = true;
public string BehaviorKind { get; set; } = "idle";
public bool UseOrders { get; set; }
public string? StagingOrderKind { get; set; }
public int MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public float Radius { get; set; } = 24f;
public float WaitSeconds { get; set; } = 3f;
public string? PreferredItemId { get; set; }
public string? Notes { get; set; }
public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = [];
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerReinforcementPolicyRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string ScopeKind { get; set; } = "player-faction";
public string? ScopeId { get; set; }
public string ShipKind { get; set; } = "military";
public int DesiredAssetCount { get; set; }
public int MinimumReserveCount { get; set; }
public bool AutoTransferReserves { get; set; } = true;
public bool AutoQueueProduction { get; set; } = true;
public string? SourceReserveId { get; set; }
public string? TargetFrontId { get; set; }
public string? Notes { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string ScopeKind { get; set; } = "player-faction";
public string? ScopeId { get; set; }
public string ShipKind { get; set; } = "military";
public int DesiredAssetCount { get; set; }
public int MinimumReserveCount { get; set; }
public bool AutoTransferReserves { get; set; } = true;
public bool AutoQueueProduction { get; set; } = true;
public string? SourceReserveId { get; set; }
public string? TargetFrontId { get; set; }
public string? Notes { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerProductionProgramRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Kind { get; set; } = "ship-production";
public string? TargetShipKind { get; set; }
public string? TargetModuleId { get; set; }
public string? TargetItemId { get; set; }
public int TargetCount { get; set; }
public int CurrentCount { get; set; }
public string? StationGroupId { get; set; }
public string? ReinforcementPolicyId { get; set; }
public string? Notes { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Kind { get; set; } = "ship-production";
public string? TargetShipKind { get; set; }
public string? TargetModuleId { get; set; }
public string? TargetItemId { get; set; }
public int TargetCount { get; set; }
public int CurrentCount { get; set; }
public string? StationGroupId { get; set; }
public string? ReinforcementPolicyId { get; set; }
public string? Notes { get; set; }
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerDirectiveRuntime
{
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Kind { get; set; } = "hold";
public string ScopeKind { get; set; } = "asset";
public string ScopeId { get; set; } = string.Empty;
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; }
public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; }
public string BehaviorKind { get; set; } = "idle";
public bool UseOrders { get; set; }
public string? StagingOrderKind { get; set; }
public string? ItemId { get; set; }
public string? PreferredNodeId { get; set; }
public string? PreferredConstructionSiteId { get; set; }
public string? PreferredModuleId { get; set; }
public int Priority { get; set; } = 50;
public float Radius { get; set; } = 24f;
public float WaitSeconds { get; set; } = 3f;
public int MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public List<Vector3> PatrolPoints { get; } = [];
public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = [];
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public string? Notes { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Label { get; set; }
public string Status { get; set; } = "active";
public string Kind { get; set; } = "hold";
public string ScopeKind { get; set; } = "asset";
public string ScopeId { get; set; } = string.Empty;
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; }
public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; }
public string BehaviorKind { get; set; } = "idle";
public bool UseOrders { get; set; }
public string? StagingOrderKind { get; set; }
public string? ItemId { get; set; }
public string? PreferredNodeId { get; set; }
public string? PreferredConstructionSiteId { get; set; }
public string? PreferredModuleId { get; set; }
public int Priority { get; set; } = 50;
public float Radius { get; set; } = 24f;
public float WaitSeconds { get; set; } = 3f;
public int MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public List<Vector3> PatrolPoints { get; } = [];
public List<ShipOrderTemplateRuntime> RepeatOrders { get; } = [];
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public string? Notes { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerAssignmentRuntime
{
public required string Id { get; init; }
public required string AssetKind { get; set; }
public required string AssetId { get; set; }
public string? FleetId { get; set; }
public string? TaskForceId { get; set; }
public string? StationGroupId { get; set; }
public string? EconomicRegionId { get; set; }
public string? FrontId { get; set; }
public string? ReserveId { get; set; }
public string? DirectiveId { get; set; }
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public string Role { get; set; } = "line";
public string Status { get; set; } = "active";
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string AssetKind { get; set; }
public required string AssetId { get; set; }
public string? FleetId { get; set; }
public string? TaskForceId { get; set; }
public string? StationGroupId { get; set; }
public string? EconomicRegionId { get; set; }
public string? FrontId { get; set; }
public string? ReserveId { get; set; }
public string? DirectiveId { get; set; }
public string? PolicyId { get; set; }
public string? AutomationPolicyId { get; set; }
public string Role { get; set; } = "line";
public string Status { get; set; } = "active";
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerDecisionLogEntryRuntime
{
public required string Id { get; init; }
public required string Kind { get; set; }
public required string Summary { get; set; }
public string? RelatedEntityKind { get; set; }
public string? RelatedEntityId { get; set; }
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Kind { get; set; }
public required string Summary { get; set; }
public string? RelatedEntityKind { get; set; }
public string? RelatedEntityId { get; set; }
public DateTimeOffset OccurredAtUtc { get; set; } = DateTimeOffset.UtcNow;
}
public sealed class PlayerAlertRuntime
{
public required string Id { get; init; }
public required string Kind { get; set; }
public required string Severity { get; set; }
public required string Summary { get; set; }
public string? AssetKind { get; set; }
public string? AssetId { get; set; }
public string? RelatedDirectiveId { get; set; }
public string Status { get; set; } = "open";
public DateTimeOffset CreatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public required string Id { get; init; }
public required string Kind { get; set; }
public required string Severity { get; set; }
public required string Summary { get; set; }
public string? AssetKind { get; set; }
public string? AssetId { get; set; }
public string? RelatedDirectiveId { get; set; }
public string Status { get; set; } = "open";
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) =>
{
options.AddDefaultPolicy((policy) =>
{
policy
.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin();
});
options.AddDefaultPolicy((policy) =>
{
policy
.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin();
});
});
builder.Services.Configure<WorldGenerationOptions>(builder.Configuration.GetSection("WorldGeneration"));
builder.Services.Configure<OrbitalSimulationOptions>(builder.Configuration.GetSection("OrbitalSimulation"));

View File

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

View File

@@ -3,179 +3,179 @@ namespace SpaceGame.Api.Shared.Runtime;
internal static class SimulationRuntimeSupport
{
internal static bool HasShipCapabilities(ShipDefinition definition, params string[] capabilities) =>
capabilities.All(cap => definition.Capabilities.Contains(cap, StringComparer.Ordinal));
internal static bool HasShipCapabilities(ShipDefinition definition, params string[] capabilities) =>
capabilities.All(cap => definition.Capabilities.Contains(cap, StringComparer.Ordinal));
internal static int CountStationModules(StationRuntime station, string moduleId) =>
station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal));
internal static int CountStationModules(StationRuntime station, string moduleId) =>
station.Modules.Count(module => string.Equals(module.ModuleId, moduleId, StringComparison.Ordinal));
internal static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId)
{
if (!world.ModuleDefinitions.TryGetValue(moduleId, out var definition))
internal static void AddStationModule(SimulationWorld world, StationRuntime station, string moduleId)
{
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}",
ModuleId = moduleId,
Health = definition.Hull,
MaxHealth = definition.Hull,
});
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;
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)));
}
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 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;
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;
}
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) =>
modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal)));
internal static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
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)
{
if (workforceRequired <= 0.01f)
internal static void AddInventory(IDictionary<string, float> inventory, string itemId, float amount)
{
return 1f;
if (amount <= 0f)
{
return;
}
inventory[itemId] = GetInventoryAmount((IReadOnlyDictionary<string, float>)inventory, itemId) + amount;
}
var staffedRatio = MathF.Min(1f, population / workforceRequired);
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))
internal static float RemoveInventory(IDictionary<string, float> inventory, string itemId, float amount)
{
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;
var requiredModule = GetStorageRequirement(storageClass);
if (requiredModule is not null && !station.InstalledModules.Contains(requiredModule, StringComparer.Ordinal))
internal static bool HasStationModules(StationRuntime station, params string[] modules) =>
modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.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);
if (capacity <= 0.01f)
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)
{
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
.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)
internal static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) =>
recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount);
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)
{
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);
return accepted;
}
internal static bool IsConstructionSiteReady(SimulationWorld world, ConstructionSiteRuntime site) =>
site.RequiredItems.All(entry => GetConstructionDeliveredAmount(world, site, entry.Key) + 0.001f >= entry.Value);
internal static bool CanStartModuleConstruction(StationRuntime station, ModuleRecipeDefinition recipe) =>
recipe.Inputs.All(input => GetInventoryAmount(station.Inventory, input.ItemId) + 0.001f >= input.Amount);
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();
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 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))
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Post("/api/ships/{shipId}/orders");
AllowAnonymous();
}
try
public override async Task HandleAsync(ShipOrderCommandRequest request, CancellationToken cancellationToken)
{
var snapshot = worldService.EnqueueShipOrder(shipId, request);
if (snapshot is null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var shipId = Route<string>("shipId");
if (string.IsNullOrWhiteSpace(shipId))
{
await SendNotFoundAsync(cancellationToken);
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 string ShipId { get; set; } = string.Empty;
public string OrderId { get; set; } = string.Empty;
public string ShipId { get; set; } = string.Empty;
public string OrderId { get; set; } = string.Empty;
}
public sealed class RemoveShipOrderHandler(WorldService worldService) : Endpoint<RemoveShipOrderRequest, ShipSnapshot>
{
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)
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Delete("/api/ships/{shipId}/orders/{orderId}");
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 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))
public override void Configure()
{
await SendNotFoundAsync(cancellationToken);
return;
Put("/api/ships/{shipId}/default-behavior");
AllowAnonymous();
}
var snapshot = worldService.UpdateShipDefaultBehavior(shipId, request);
if (snapshot is null)
public override async Task HandleAsync(ShipDefaultBehaviorCommandRequest request, CancellationToken cancellationToken)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var shipId = Route<string>("shipId");
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 required string Id { get; init; }
public required string SystemId { get; set; }
public required ShipDefinition Definition { get; init; }
public required string FactionId { get; init; }
public required Vector3 Position { get; set; }
public required Vector3 TargetPosition { get; set; }
public required ShipSpatialStateRuntime SpatialState { get; set; }
public Vector3 Velocity { get; set; } = Vector3.Zero;
public ShipState State { get; set; } = ShipState.Idle;
public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
public List<ShipOrderRuntime> OrderQueue { get; } = [];
public ShipPlanRuntime? ActivePlan { get; set; }
public required ShipSkillProfileRuntime Skills { get; set; }
public bool NeedsReplan { get; set; } = true;
public float ReplanCooldownSeconds { get; set; }
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public string? DockedStationId { get; set; }
public int? AssignedDockingPadIndex { get; set; }
public string? CommanderId { get; set; }
public string? PolicySetId { get; set; }
public string ControlSourceKind { get; set; } = "unassigned";
public string? ControlSourceId { get; set; }
public string? ControlReason { get; set; }
public string? LastReplanReason { get; set; }
public string? LastAccessFailureReason { get; set; }
public float Health { get; set; }
public HashSet<string> KnownStationIds { get; } = new(StringComparer.Ordinal);
public List<string> History { get; } = [];
public string LastSignature { get; set; } = string.Empty;
public string LastDeltaSignature { get; set; } = string.Empty;
public required string Id { get; init; }
public required string SystemId { get; set; }
public required ShipDefinition Definition { get; init; }
public required string FactionId { get; init; }
public required Vector3 Position { get; set; }
public required Vector3 TargetPosition { get; set; }
public required ShipSpatialStateRuntime SpatialState { get; set; }
public Vector3 Velocity { get; set; } = Vector3.Zero;
public ShipState State { get; set; } = ShipState.Idle;
public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
public List<ShipOrderRuntime> OrderQueue { get; } = [];
public ShipPlanRuntime? ActivePlan { get; set; }
public required ShipSkillProfileRuntime Skills { get; set; }
public bool NeedsReplan { get; set; } = true;
public float ReplanCooldownSeconds { get; set; }
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public string? DockedStationId { get; set; }
public int? AssignedDockingPadIndex { get; set; }
public string? CommanderId { get; set; }
public string? PolicySetId { get; set; }
public string ControlSourceKind { get; set; } = "unassigned";
public string? ControlSourceId { get; set; }
public string? ControlReason { get; set; }
public string? LastReplanReason { get; set; }
public string? LastAccessFailureReason { get; set; }
public float Health { get; set; }
public HashSet<string> KnownStationIds { get; } = new(StringComparer.Ordinal);
public List<string> History { get; } = [];
public string LastSignature { get; set; } = string.Empty;
public string LastDeltaSignature { get; set; } = string.Empty;
}
public sealed class ShipSkillProfileRuntime
{
public int Navigation { get; set; }
public int Trade { get; set; }
public int Mining { get; set; }
public int Combat { get; set; }
public int Construction { get; set; }
public int Navigation { get; set; }
public int Trade { get; set; }
public int Mining { get; set; }
public int Combat { get; set; }
public int Construction { get; set; }
}
public sealed class ShipOrderRuntime
{
public required string Id { get; init; }
public required string Kind { get; init; }
public OrderStatus Status { get; set; } = OrderStatus.Queued;
public int Priority { get; set; }
public bool InterruptCurrentPlan { get; set; } = true;
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
public string? Label { get; set; }
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; }
public string? ItemId { get; set; }
public string? NodeId { get; set; }
public string? ConstructionSiteId { get; set; }
public string? ModuleId { get; set; }
public float WaitSeconds { get; set; }
public float Radius { get; set; }
public int? MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public string? FailureReason { get; set; }
public required string Id { get; init; }
public required string Kind { get; init; }
public OrderStatus Status { get; set; } = OrderStatus.Queued;
public int Priority { get; set; }
public bool InterruptCurrentPlan { get; set; } = true;
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
public string? Label { get; set; }
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; }
public string? ItemId { get; set; }
public string? NodeId { get; set; }
public string? ConstructionSiteId { get; set; }
public string? ModuleId { get; set; }
public float WaitSeconds { get; set; }
public float Radius { get; set; }
public int? MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public string? FailureReason { get; set; }
}
public sealed class DefaultBehaviorRuntime
{
public required string Kind { get; set; }
public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; }
public string? AreaSystemId { get; set; }
public string? TargetEntityId { get; set; }
public string? PreferredItemId { get; set; }
public string? PreferredNodeId { get; set; }
public string? PreferredConstructionSiteId { get; set; }
public string? PreferredModuleId { get; set; }
public Vector3? TargetPosition { get; set; }
public float WaitSeconds { get; set; } = 3f;
public float Radius { get; set; } = 24f;
public int MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public List<Vector3> PatrolPoints { get; set; } = [];
public int PatrolIndex { get; set; }
public List<ShipOrderTemplateRuntime> RepeatOrders { get; set; } = [];
public int RepeatIndex { get; set; }
public required string Kind { get; set; }
public string? HomeSystemId { get; set; }
public string? HomeStationId { get; set; }
public string? AreaSystemId { get; set; }
public string? TargetEntityId { get; set; }
public string? PreferredItemId { get; set; }
public string? PreferredNodeId { get; set; }
public string? PreferredConstructionSiteId { get; set; }
public string? PreferredModuleId { get; set; }
public Vector3? TargetPosition { get; set; }
public float WaitSeconds { get; set; } = 3f;
public float Radius { get; set; } = 24f;
public int MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public List<Vector3> PatrolPoints { get; set; } = [];
public int PatrolIndex { get; set; }
public List<ShipOrderTemplateRuntime> RepeatOrders { get; set; } = [];
public int RepeatIndex { get; set; }
}
public sealed class ShipOrderTemplateRuntime
{
public required string Kind { get; init; }
public string? Label { get; set; }
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; }
public string? ItemId { get; set; }
public string? NodeId { get; set; }
public string? ConstructionSiteId { get; set; }
public string? ModuleId { get; set; }
public float WaitSeconds { get; set; }
public float Radius { get; set; }
public int? MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
public required string Kind { get; init; }
public string? Label { get; set; }
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? SourceStationId { get; set; }
public string? DestinationStationId { get; set; }
public string? ItemId { get; set; }
public string? NodeId { get; set; }
public string? ConstructionSiteId { get; set; }
public string? ModuleId { get; set; }
public float WaitSeconds { get; set; }
public float Radius { get; set; }
public int? MaxSystemRange { get; set; }
public bool KnownStationsOnly { get; set; }
}
public sealed class ShipPlanRuntime
{
public required string Id { get; init; }
public required AiPlanSourceKind SourceKind { get; init; }
public required string SourceId { get; init; }
public required string Kind { get; init; }
public required string Summary { get; set; }
public AiPlanStatus Status { get; set; } = AiPlanStatus.Planned;
public int CurrentStepIndex { get; set; }
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public string? InterruptReason { get; set; }
public string? FailureReason { get; set; }
public List<ShipPlanStepRuntime> Steps { get; } = [];
public required string Id { get; init; }
public required AiPlanSourceKind SourceKind { get; init; }
public required string SourceId { get; init; }
public required string Kind { get; init; }
public required string Summary { get; set; }
public AiPlanStatus Status { get; set; } = AiPlanStatus.Planned;
public int CurrentStepIndex { get; set; }
public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAtUtc { get; set; } = DateTimeOffset.UtcNow;
public string? InterruptReason { get; set; }
public string? FailureReason { get; set; }
public List<ShipPlanStepRuntime> Steps { get; } = [];
}
public sealed class ShipPlanStepRuntime
{
public required string Id { get; init; }
public required string Kind { get; init; }
public required string Summary { get; set; }
public AiPlanStepStatus Status { get; set; } = AiPlanStepStatus.Planned;
public int CurrentSubTaskIndex { get; set; }
public string? BlockingReason { get; set; }
public List<ShipSubTaskRuntime> SubTasks { get; } = [];
public required string Id { get; init; }
public required string Kind { get; init; }
public required string Summary { get; set; }
public AiPlanStepStatus Status { get; set; } = AiPlanStepStatus.Planned;
public int CurrentSubTaskIndex { get; set; }
public string? BlockingReason { get; set; }
public List<ShipSubTaskRuntime> SubTasks { get; } = [];
}
public sealed class ShipSubTaskRuntime
{
public required string Id { get; init; }
public required string Kind { get; init; }
public required string Summary { get; set; }
public WorkStatus Status { get; set; } = WorkStatus.Pending;
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public string? TargetNodeId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? ItemId { get; set; }
public string? ModuleId { get; set; }
public float Threshold { get; set; }
public float Amount { get; set; }
public float ElapsedSeconds { get; set; }
public float TotalSeconds { get; set; }
public float Progress { get; set; }
public string? BlockingReason { get; set; }
public required string Id { get; init; }
public required string Kind { get; init; }
public required string Summary { get; set; }
public WorkStatus Status { get; set; } = WorkStatus.Pending;
public string? TargetEntityId { get; set; }
public string? TargetSystemId { get; set; }
public string? TargetNodeId { get; set; }
public Vector3? TargetPosition { get; set; }
public string? ItemId { get; set; }
public string? ModuleId { get; set; }
public float Threshold { get; set; }
public float Amount { get; set; }
public float ElapsedSeconds { get; set; }
public float TotalSeconds { get; set; }
public float Progress { 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
{
private readonly OrbitalSimulationOptions _orbitalSimulation;
private readonly OrbitalStateUpdater _orbitalStateUpdater;
private readonly InfrastructureSimulationService _infrastructureSimulation;
private readonly GeopoliticalSimulationService _geopolitics;
private readonly CommanderPlanningService _commanderPlanning;
private readonly PlayerFactionService _playerFaction;
private readonly StationSimulationService _stationSimulation;
private readonly StationLifecycleService _stationLifecycle;
private readonly ShipAiService _shipAi;
private readonly SimulationProjectionService _projection;
private readonly OrbitalSimulationOptions _orbitalSimulation;
private readonly OrbitalStateUpdater _orbitalStateUpdater;
private readonly InfrastructureSimulationService _infrastructureSimulation;
private readonly GeopoliticalSimulationService _geopolitics;
private readonly CommanderPlanningService _commanderPlanning;
private readonly PlayerFactionService _playerFaction;
private readonly StationSimulationService _stationSimulation;
private readonly StationLifecycleService _stationLifecycle;
private readonly ShipAiService _shipAi;
private readonly SimulationProjectionService _projection;
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())
public SimulationEngine(OrbitalSimulationOptions? orbitalSimulation = null)
{
if (ship.Health <= 0f)
{
continue;
}
var previousPosition = ship.Position;
_shipAi.UpdateShip(world, ship, simulationDeltaSeconds, events);
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(simulationDeltaSeconds);
_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);
}
_orbitalStateUpdater.SyncSpatialState(world);
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())
public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence)
{
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);
}
var nowUtc = DateTimeOffset.UtcNow;
var events = new List<SimulationEventRecord>();
var simulationDeltaSeconds = deltaSeconds * MathF.Max(world.Balance.SimulationSpeedMultiplier, 0.01f);
world.GeneratedAtUtc = nowUtc;
if (world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId) is { } faction)
{
faction.ShipsLost += 1;
}
world.OrbitalTimeSeconds += simulationDeltaSeconds * _orbitalSimulation.SimulatedSecondsPerRealSecond;
if (ship.CommanderId is not null && world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId) is { } commander)
{
commander.IsAlive = false;
}
_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);
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);
world.Stations.Remove(station);
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));
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)
{
celestial.OccupyingStructureId = null;
}
if (world.Factions.FirstOrDefault(candidate => candidate.Id == ship.FactionId) is { } faction)
{
faction.ShipsLost += 1;
}
foreach (var claim in world.Claims.Where(candidate => candidate.CelestialId == station.CelestialId))
{
claim.Health = 0f;
claim.State = ClaimStateKinds.Destroyed;
}
if (ship.CommanderId is not null && world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId) is { } commander)
{
commander.IsAlive = false;
}
foreach (var site in world.ConstructionSites.Where(candidate => candidate.StationId == station.Id))
{
site.State = ConstructionSiteStateKinds.Destroyed;
}
events.Add(new SimulationEventRecord("ship", ship.Id, "destroyed", $"{ship.Definition.Label} was destroyed.", DateTimeOffset.UtcNow));
}
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)
{
var itemId = world.ItemDefinitions.ContainsKey("scrapmetal")
? "scrapmetal"
: world.ItemDefinitions.ContainsKey("rawscrap")
? "rawscrap"
: world.ItemDefinitions.Keys.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault();
if (itemId is null || amount <= 0.01f)
{
return;
if (station.CelestialId is not null && world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId) is { } celestial)
{
celestial.OccupyingStructureId = null;
}
foreach (var claim in world.Claims.Where(candidate => candidate.CelestialId == station.CelestialId))
{
claim.Health = 0f;
claim.State = ClaimStateKinds.Destroyed;
}
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}",
SourceKind = sourceKind,
SourceEntityId = sourceEntityId,
SystemId = systemId,
Position = position,
ItemId = itemId,
RemainingAmount = amount,
MaxAmount = amount,
});
}
var itemId = world.ItemDefinitions.ContainsKey("scrapmetal")
? "scrapmetal"
: world.ItemDefinitions.ContainsKey("rawscrap")
? "rawscrap"
: world.ItemDefinitions.Keys.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault();
if (itemId is null || amount <= 0.01f)
{
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 required string Id { get; init; }
public required string FactionId { get; init; }
public required string SystemId { get; init; }
public required string CelestialId { get; init; }
public string? CommanderId { get; set; }
public DateTimeOffset PlacedAtUtc { get; init; }
public DateTimeOffset ActivatesAtUtc { get; set; }
public string State { get; set; } = ClaimStateKinds.Placed;
public float Health { get; set; }
public string LastDeltaSignature { get; set; } = string.Empty;
public required string Id { get; init; }
public required string FactionId { get; init; }
public required string SystemId { get; init; }
public required string CelestialId { get; init; }
public string? CommanderId { get; set; }
public DateTimeOffset PlacedAtUtc { get; init; }
public DateTimeOffset ActivatesAtUtc { get; set; }
public string State { get; set; } = ClaimStateKinds.Placed;
public float Health { get; set; }
public string LastDeltaSignature { get; set; } = string.Empty;
}
public sealed class ConstructionSiteRuntime
{
public required string Id { get; init; }
public required string FactionId { get; init; }
public required string SystemId { get; init; }
public required string CelestialId { get; init; }
public required string TargetKind { get; init; }
public required string TargetDefinitionId { get; init; }
public string? BlueprintId { get; set; }
public string? ClaimId { get; set; }
public string? StationId { get; set; }
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public Dictionary<string, float> RequiredItems { get; } = new(StringComparer.Ordinal);
public Dictionary<string, float> DeliveredItems { get; } = new(StringComparer.Ordinal);
public HashSet<string> AssignedConstructorShipIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
public float Progress { get; set; }
public string State { get; set; } = ConstructionSiteStateKinds.Planned;
public string LastDeltaSignature { get; set; } = string.Empty;
public required string Id { get; init; }
public required string FactionId { get; init; }
public required string SystemId { get; init; }
public required string CelestialId { get; init; }
public required string TargetKind { get; init; }
public required string TargetDefinitionId { get; init; }
public string? BlueprintId { get; set; }
public string? ClaimId { get; set; }
public string? StationId { get; set; }
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
public Dictionary<string, float> RequiredItems { get; } = new(StringComparer.Ordinal);
public Dictionary<string, float> DeliveredItems { get; } = new(StringComparer.Ordinal);
public HashSet<string> AssignedConstructorShipIds { get; } = new(StringComparer.Ordinal);
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
public float Progress { get; set; }
public string State { get; set; } = ConstructionSiteStateKinds.Planned;
public string LastDeltaSignature { get; set; } = string.Empty;
}

View File

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

View File

@@ -4,208 +4,208 @@ namespace SpaceGame.Api.Stations.Simulation;
internal sealed class StationLifecycleService
{
private const float WaterConsumptionPerWorkerPerSecond = 0.004f;
private const float PopulationGrowthPerSecond = 0.012f;
private const float PopulationAttritionPerSecond = 0.018f;
private readonly StationSimulationService _stationSimulation;
private const float WaterConsumptionPerWorkerPerSecond = 0.004f;
private const float PopulationGrowthPerSecond = 0.012f;
private const float PopulationAttritionPerSecond = 0.018f;
private readonly 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)
internal StationLifecycleService(StationSimulationService stationSimulation)
{
UpdateStationPopulation(station, deltaSeconds, events);
_stationSimulation.ReviewStationMarketOrders(world, station);
_stationSimulation.RunStationProduction(world, station, deltaSeconds, events);
factionPopulation[station.FactionId] = GetInventoryAmount(factionPopulation, station.FactionId) + station.Population;
_stationSimulation = stationSimulation;
}
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)
{
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));
}
foreach (var faction in world.Factions)
{
faction.PopulationTotal = GetInventoryAmount(factionPopulation, faction.Id);
}
}
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
}
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))
private void UpdateStationPopulation(StationRuntime station, float deltaSeconds, ICollection<SimulationEventRecord> events)
{
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);
var ship = new ShipRuntime
internal static float CompleteShipRecipe(SimulationWorld world, StationRuntime station, RecipeDefinition recipe, ICollection<SimulationEventRecord> events)
{
Id = $"ship-{world.Ships.Count + 1}",
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,
if (recipe.ShipOutputId is null || !world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var definition))
{
return 0f;
}
var spawnPosition = new Vector3(station.Position.X + GetStationRadius(world, station) + 32f, station.Position.Y, station.Position.Z);
var ship = new ShipRuntime
{
Id = $"ship-{world.Ships.Count + 1}",
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);
EnsureSpawnedShipCommander(world, station, ship);
if (world.Factions.FirstOrDefault(candidate => candidate.Id == station.FactionId) is { } faction)
private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station)
{
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));
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,
};
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),
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 - patrolRadius, station.Position.Y, station.Position.Z),
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 =>
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)
internal static void EnsureStationCommander(SimulationWorld world, StationRuntime station)
{
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}",
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,
},
};
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;
}
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
{
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),
},
};
private static void EnsureSpawnedShipCommander(SimulationWorld world, StationRuntime station, ShipRuntime ship)
{
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;
ship.CommanderId = commander.Id;
ship.PolicySetId = factionCommander.PolicySetId;
factionCommander.SubordinateCommanderIds.Add(commander.Id);
faction.CommanderIds.Add(commander.Id);
world.Commanders.Add(commander);
}
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 override void Configure()
{
Get("/api/balance");
AllowAnonymous();
}
public override void Configure()
{
Get("/api/balance");
AllowAnonymous();
}
public override Task HandleAsync(CancellationToken cancellationToken) =>
SendOkAsync(worldService.GetBalance(), cancellationToken);
public override Task HandleAsync(CancellationToken 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 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
public override void Configure()
{
process = new
{
uptimeSeconds = uptime.TotalSeconds,
cpuPercent = Math.Round(telemetry.CpuPercent, 1),
workingSetMb = Math.Round(telemetry.WorkingSetBytes / 1_048_576.0, 1),
gcMemoryMb = Math.Round(telemetry.GcMemoryBytes / 1_048_576.0, 1),
threadCount = telemetry.ThreadCount,
processorCount = Environment.ProcessorCount,
},
simulation = new
{
sequence = status.Sequence,
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);
}
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
{
uptimeSeconds = uptime.TotalSeconds,
cpuPercent = Math.Round(telemetry.CpuPercent, 1),
workingSetMb = Math.Round(telemetry.WorkingSetBytes / 1_048_576.0, 1),
gcMemoryMb = Math.Round(telemetry.GcMemoryBytes / 1_048_576.0, 1),
threadCount = telemetry.ThreadCount,
processorCount = Environment.ProcessorCount,
},
simulation = new
{
sequence = status.Sequence,
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 override void Configure()
{
Get("/api/world");
AllowAnonymous();
}
public override void Configure()
{
Get("/api/world");
AllowAnonymous();
}
public override Task HandleAsync(CancellationToken cancellationToken) =>
SendOkAsync(worldService.GetSnapshot(), cancellationToken);
public override Task HandleAsync(CancellationToken cancellationToken) =>
SendOkAsync(worldService.GetSnapshot(), cancellationToken);
}

View File

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

View File

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

View File

@@ -5,49 +5,49 @@ namespace SpaceGame.Api.Universe.Api;
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()
{
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))
public override void Configure()
{
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 override void Configure()
{
Put("/api/balance");
AllowAnonymous();
}
public override void Configure()
{
Put("/api/balance");
AllowAnonymous();
}
public override Task HandleAsync(BalanceDefinition req, CancellationToken cancellationToken)
{
var applied = worldService.UpdateBalance(req);
return SendOkAsync(applied, cancellationToken);
}
public override Task HandleAsync(BalanceDefinition req, CancellationToken cancellationToken)
{
var applied = worldService.UpdateBalance(req);
return SendOkAsync(applied, cancellationToken);
}
}

View File

@@ -5,292 +5,292 @@ namespace SpaceGame.Api.Universe.Scenario;
internal sealed class DataCatalogLoader(string dataRoot)
{
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)
private readonly JsonSerializerOptions _jsonOptions = new()
{
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)
? "sol"
: availableSystemIds[0];
string ResolveSystemId(string systemId) =>
availableSystemIds.Contains(systemId, StringComparer.Ordinal) ? systemId : fallbackSystemId;
return new ScenarioDefinition
internal ScenarioDefinition NormalizeScenarioToAvailableSystems(
ScenarioDefinition scenario,
IReadOnlyList<string> availableSystemIds)
{
InitialStations = scenario.InitialStations
.Select(station => new InitialStationDefinition
if (availableSystemIds.Count == 0)
{
SystemId = ResolveSystemId(station.SystemId),
Label = station.Label,
Color = station.Color,
Objective = station.Objective,
StartingModules = station.StartingModules.ToList(),
FactionId = station.FactionId,
PlanetIndex = station.PlanetIndex,
LagrangeSide = station.LagrangeSide,
Position = station.Position?.ToArray(),
})
.ToList(),
ShipFormations = scenario.ShipFormations
.Select(formation => new ShipFormationDefinition
return scenario;
}
var fallbackSystemId = availableSystemIds.Contains("sol", StringComparer.Ordinal)
? "sol"
: availableSystemIds[0];
string ResolveSystemId(string systemId) =>
availableSystemIds.Contains(systemId, StringComparer.Ordinal) ? systemId : fallbackSystemId;
return new ScenarioDefinition
{
ShipId = formation.ShipId,
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),
},
};
}
InitialStations = scenario.InitialStations
.Select(station => new InitialStationDefinition
{
SystemId = ResolveSystemId(station.SystemId),
Label = station.Label,
Color = station.Color,
Objective = station.Objective,
StartingModules = station.StartingModules.ToList(),
FactionId = station.FactionId,
PlanetIndex = station.PlanetIndex,
LagrangeSide = station.LagrangeSide,
Position = station.Position?.ToArray(),
})
.ToList(),
ShipFormations = scenario.ShipFormations
.Select(formation => new ShipFormationDefinition
{
ShipId = formation.ShipId,
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)
{
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)
private T Read<T>(string fileName)
{
if (item.Production.Count > 0)
{
foreach (var production in item.Production)
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
{
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
{
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(),
Outputs =
[
new RecipeOutputDefinition
})
.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)
{
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,
Amount = production.Amount,
},
],
});
}
});
}
continue;
}
continue;
}
if (item.Construction is null)
{
continue;
}
if (item.Construction is null)
{
continue;
}
recipes.Add(new RecipeDefinition
{
Id = item.Construction.RecipeId ?? $"{item.Id}-production",
Label = item.Name,
FacilityCategory = item.Construction.FacilityCategory,
Duration = item.Construction.CycleTime,
Priority = item.Construction.Priority,
RequiredModules = item.Construction.RequiredModules.ToList(),
Inputs = item.Construction.Requirements
.Select(input => new RecipeInputDefinition
{
ItemId = input.ItemId,
Amount = input.Amount,
})
.ToList(),
Outputs =
[
new RecipeOutputDefinition
recipes.Add(new RecipeDefinition
{
Id = item.Construction.RecipeId ?? $"{item.Id}-production",
Label = item.Name,
FacilityCategory = item.Construction.FacilityCategory,
Duration = item.Construction.CycleTime,
Priority = item.Construction.Priority,
RequiredModules = item.Construction.RequiredModules.ToList(),
Inputs = item.Construction.Requirements
.Select(input => new RecipeInputDefinition
{
ItemId = input.ItemId,
Amount = input.Amount,
})
.ToList(),
Outputs =
[
new RecipeOutputDefinition
{
ItemId = item.Id,
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)
{
if (ship.Construction is null)
private static string InferFacilityCategory(ItemDefinition item) =>
item.Group switch
{
continue;
}
"agricultural" or "food" or "pharmaceutical" or "water" => "farm",
_ => "station",
};
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,
});
private static List<string> InferRequiredModules(ItemDefinition item, IReadOnlyDictionary<string, string> preferredProducerByItemId)
{
if (preferredProducerByItemId.TryGetValue(item.Id, out var moduleId))
{
return [moduleId];
}
return [];
}
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) =>
item.Group switch
private static List<ItemDefinition> NormalizeItems(List<ItemDefinition> items)
{
"agricultural" or "food" or "pharmaceutical" or "water" => "farm",
_ => "station",
};
foreach (var item in items)
{
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)
{
if (preferredProducerByItemId.TryGetValue(item.Id, out var moduleId))
{
return [moduleId];
return items;
}
return [];
}
private static int InferRecipePriority(ItemDefinition item) =>
item.Group switch
private static List<ModuleDefinition> NormalizeModules(List<ModuleDefinition> modules)
{
"energy" => 140,
"water" => 130,
"food" => 120,
"agricultural" => 110,
"refined" => 100,
"hightech" => 90,
"shiptech" => 80,
"pharmaceutical" => 70,
_ => 60,
};
foreach (var module in modules)
{
if (module.Products.Count == 0 && !string.IsNullOrWhiteSpace(module.Product))
{
module.Products = [module.Product];
}
private static List<ItemDefinition> NormalizeItems(List<ItemDefinition> items)
{
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item.Type))
{
item.Type = string.IsNullOrWhiteSpace(item.Group) ? "material" : item.Group;
}
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;
}
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(

View File

@@ -3,18 +3,18 @@ namespace SpaceGame.Api.Universe.Scenario;
internal static class LoaderSupport
{
internal const string DefaultFactionId = "sol-dominion";
internal const int WorldSeed = 1;
internal const float MinimumFactionCredits = 0f;
internal const float MinimumRefineryOre = 0f;
internal const float MinimumRefineryStock = 0f;
internal const float MinimumShipyardStock = 0f;
internal const float MinimumSystemSeparation = 3.2f;
internal const float LocalSpaceRadius = 10_000f;
internal const string DefaultFactionId = "sol-dominion";
internal const int WorldSeed = 1;
internal const float MinimumFactionCredits = 0f;
internal const float MinimumRefineryOre = 0f;
internal const float MinimumRefineryStock = 0f;
internal const float MinimumShipyardStock = 0f;
internal const float MinimumSystemSeparation = 3.2f;
internal const float LocalSpaceRadius = 10_000f;
internal static readonly string[] GeneratedSystemNames =
[
"Aquila Verge",
internal static readonly string[] GeneratedSystemNames =
[
"Aquila Verge",
"Orion Fold",
"Draco Span",
"Lyra Shoal",
@@ -48,9 +48,9 @@ internal static class LoaderSupport
"Telescopium Strand",
];
internal static readonly StarProfile[] StarProfiles =
[
new("main-sequence", "#ffd27a", "#ffb14a", 696340f),
internal static readonly StarProfile[] StarProfiles =
[
new("main-sequence", "#ffd27a", "#ffb14a", 696340f),
new("blue-white", "#9dc6ff", "#66a0ff", 930000f),
new("white-dwarf", "#f1f5ff", "#b8caff", 12000f),
new("brown-dwarf", "#b97d56", "#8a5438", 70000f),
@@ -59,9 +59,9 @@ internal static class LoaderSupport
new("binary-white-dwarf", "#edf3ff", "#c8d6ff", 14000f),
];
internal static readonly PlanetProfile[] PlanetProfiles =
[
new("barren", "sphere", "#bca48f", 2800f, 0.22f, 0, false),
internal static readonly PlanetProfile[] PlanetProfiles =
[
new("barren", "sphere", "#bca48f", 2800f, 0.22f, 0, false),
new("terrestrial", "sphere", "#58a36c", 6400f, 0.28f, 1, false),
new("oceanic", "sphere", "#4f84c4", 7000f, 0.30f, 2, 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),
];
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)
{
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))
internal static Vector3 NormalizeScenarioPoint(SystemRuntime system, float[] values)
{
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}",
ModuleId = moduleId,
Health = definition.Hull,
MaxHealth = definition.Hull,
});
station.Radius = GetStationRadius(moduleDefinitions, station);
}
if (!moduleDefinitions.TryGetValue(moduleId, out var definition))
{
return;
}
internal static float GetStationRadius(IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, StationRuntime station)
{
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)));
}
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;
station.Modules.Add(new StationModuleRuntime
{
Id = $"{station.Id}-module-{station.Modules.Count + 1}",
ModuleId = moduleId,
Health = definition.Hull,
MaxHealth = definition.Hull,
});
station.Radius = GetStationRadius(moduleDefinitions, station);
}
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)
internal static float GetStationRadius(IReadOnlyDictionary<string, ModuleDefinition> moduleDefinitions, StationRuntime station)
{
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(
@@ -167,5 +167,5 @@ internal sealed record PlanetProfile(
int BaseMoonCount,
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
{
private readonly WorldBuilder _worldBuilder;
private readonly WorldBuilder _worldBuilder;
public ScenarioLoader(string contentRootPath, WorldGenerationOptions? worldGeneration = null)
{
var generationOptions = worldGeneration ?? new WorldGenerationOptions();
var dataRoot = Path.GetFullPath(Path.Combine(contentRootPath, "..", "..", "shared", "data"));
var dataLoader = new DataCatalogLoader(dataRoot);
var generationService = new SystemGenerationService();
var spatialBuilder = new SpatialBuilder();
var seedingService = new WorldSeedingService();
public ScenarioLoader(string contentRootPath, WorldGenerationOptions? worldGeneration = null)
{
var generationOptions = worldGeneration ?? new WorldGenerationOptions();
var dataRoot = Path.GetFullPath(Path.Combine(contentRootPath, "..", "..", "shared", "data"));
var dataLoader = new DataCatalogLoader(dataRoot);
var generationService = new SystemGenerationService();
var spatialBuilder = new SpatialBuilder();
var seedingService = new WorldSeedingService();
_worldBuilder = new WorldBuilder(
generationOptions,
dataLoader,
generationService,
spatialBuilder,
seedingService);
}
_worldBuilder = new WorldBuilder(
generationOptions,
dataLoader,
generationService,
spatialBuilder,
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 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)
internal ScenarioSpatialLayout BuildLayout(IReadOnlyList<SystemRuntime> systems, BalanceDefinition balance)
{
var systemGraph = systemGraphs[system.Definition.Id];
foreach (var node in system.Definition.ResourceNodes)
{
var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node);
nodes.Add(new ResourceNodeRuntime
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)
{
Id = $"node-{++nodeIdCounter}",
SystemId = system.Definition.Id,
Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane),
SourceKind = node.SourceKind,
ItemId = node.ItemId,
CelestialId = anchorCelestial?.Id,
OrbitRadius = node.RadiusOffset,
OrbitPhase = node.Angle,
OrbitInclination = DegreesToRadians(node.InclinationDegrees),
OreRemaining = node.OreAmount,
MaxOre = node.OreAmount,
});
}
var systemGraph = systemGraphs[system.Definition.Id];
foreach (var node in system.Definition.ResourceNodes)
{
var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node);
nodes.Add(new ResourceNodeRuntime
{
Id = $"node-{++nodeIdCounter}",
SystemId = system.Definition.Id,
Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane),
SourceKind = node.SourceKind,
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)
{
var celestials = new List<CelestialRuntime>();
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>();
for (var starIndex = 0; starIndex < system.Definition.Stars.Count; starIndex += 1)
private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system)
{
AddCelestial(
celestials,
id: $"node-{system.Definition.Id}-star-{starIndex + 1}",
systemId: system.Definition.Id,
kind: SpatialNodeKind.Star,
position: Vector3.Zero,
localSpaceRadius: LocalSpaceRadius);
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(
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";
for (var planetIndex = 0; planetIndex < system.Definition.Planets.Count; planetIndex += 1)
private static CelestialRuntime AddCelestial(
ICollection<CelestialRuntime> celestials,
string id,
string systemId,
SpatialNodeKind kind,
Vector3 position,
float localSpaceRadius,
string? parentNodeId = null,
string? orbitReferenceId = null)
{
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 celestial = new CelestialRuntime
{
Id = id,
SystemId = systemId,
Kind = kind,
Position = position,
LocalSpaceRadius = localSpaceRadius,
ParentNodeId = parentNodeId,
OrbitReferenceId = orbitReferenceId,
};
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);
}
celestials.Add(celestial);
return celestial;
}
return new SystemSpatialGraph(system.Definition.Id, celestials, lagrangeNodesByPlanetIndex);
}
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
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(Vector3 planetPosition, PlanetDefinition planet)
{
Id = id,
SystemId = systemId,
Kind = kind,
Position = position,
LocalSpaceRadius = localSpaceRadius,
ParentNodeId = parentNodeId,
OrbitReferenceId = orbitReferenceId,
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,
"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);
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
private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
{
"gas-giant" => 0.24f,
"ice-giant" => 0.18f,
"oceanic" => 0.95f,
"ice" => 0.7f,
_ => 1f,
};
if (!string.IsNullOrWhiteSpace(definition.AnchorReference))
{
var anchorId = definition.AnchorReference.ToLowerInvariant() switch
{
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;
return earthMasses / 332_946f;
}
if (anchorId is not null)
{
return graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, anchorId, StringComparison.Ordinal));
}
}
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 (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0)
{
return null;
}
if (definition.AnchorMoonIndex is int moonIndex && moonIndex >= 0)
{
var moonNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
return graph.Celestials.FirstOrDefault(c => c.Id == moonNodeId);
}
var planetNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}";
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 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 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);
}
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",
};
private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
{
if (!string.IsNullOrWhiteSpace(definition.AnchorReference))
private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
{
var anchorId = definition.AnchorReference.ToLowerInvariant() switch
{
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,
};
if (anchorId is not null)
{
return graph.Celestials.FirstOrDefault(c => string.Equals(c.Id, anchorId, StringComparison.Ordinal));
}
var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
var orbitRadiusKm = SimulationUnits.AuToKilometers(planet.OrbitRadius);
return new Vector3(MathF.Cos(angle) * orbitRadiusKm, 0f, MathF.Sin(angle) * orbitRadiusKm);
}
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}";
return graph.Celestials.FirstOrDefault(c => c.Id == moonNodeId);
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,
};
}
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(

File diff suppressed because it is too large Load Diff

View File

@@ -9,335 +9,335 @@ internal sealed class WorldBuilder(
SpatialBuilder spatialBuilder,
WorldSeedingService seedingService)
{
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)
internal SimulationWorld Build()
{
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 catalog = dataLoader.LoadCatalog();
var systems = generationService.ExpandSystems(
generationService.InjectSpecialSystems(catalog.AuthoredSystems),
worldGeneration.TargetSystemCount);
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);
Console.WriteLine("TEST");
Console.WriteLine(string.Join(',', systems.Select(s => s.Id)));
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 scenario = dataLoader.NormalizeScenarioToAvailableSystems(
catalog.Scenario,
systems.Select(system => system.Id).ToList());
var geopolitics = new GeopoliticalSimulationService();
geopolitics.Update(world, 0f, []);
return world;
}
Console.WriteLine(string.Join(',', systems.Select(s => s.Id)));
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)
var systemRuntimes = systems
.Select(definition => new SystemRuntime
{
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 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
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(200));
try
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
{
worldService.Tick(0.2f);
}
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(200));
try
{
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
{
private readonly Process _process = Process.GetCurrentProcess();
private readonly Timer _timer;
private double _cpuPercent;
private DateTime _lastSampleTime;
private TimeSpan _lastCpuTime;
private readonly Process _process = Process.GetCurrentProcess();
private readonly Timer _timer;
private double _cpuPercent;
private DateTime _lastSampleTime;
private TimeSpan _lastCpuTime;
public TelemetryService()
{
_process.Refresh();
_lastSampleTime = DateTime.UtcNow;
_lastCpuTime = _process.TotalProcessorTime;
_timer = new Timer(Sample, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
}
public TelemetryService()
{
_process.Refresh();
_lastSampleTime = DateTime.UtcNow;
_lastCpuTime = _process.TotalProcessorTime;
_timer = new Timer(Sample, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
}
private void Sample(object? _)
{
_process.Refresh();
var now = DateTime.UtcNow;
var cpu = _process.TotalProcessorTime;
var elapsed = (now - _lastSampleTime).TotalSeconds;
var cpuUsed = (cpu - _lastCpuTime).TotalSeconds;
Volatile.Write(ref _cpuPercent, elapsed > 0 ? cpuUsed / elapsed / Environment.ProcessorCount * 100.0 : 0);
_lastSampleTime = now;
_lastCpuTime = cpu;
}
private void Sample(object? _)
{
_process.Refresh();
var now = DateTime.UtcNow;
var cpu = _process.TotalProcessorTime;
var elapsed = (now - _lastSampleTime).TotalSeconds;
var cpuUsed = (cpu - _lastCpuTime).TotalSeconds;
Volatile.Write(ref _cpuPercent, elapsed > 0 ? cpuUsed / elapsed / Environment.ProcessorCount * 100.0 : 0);
_lastSampleTime = now;
_lastCpuTime = cpu;
}
public double CpuPercent => Volatile.Read(ref _cpuPercent);
public long WorkingSetBytes => _process.WorkingSet64;
public long GcMemoryBytes => GC.GetTotalMemory(false);
public int ThreadCount => _process.Threads.Count;
public TimeSpan Uptime => DateTime.UtcNow - _process.StartTime.ToUniversalTime();
public double CpuPercent => Volatile.Read(ref _cpuPercent);
public long WorkingSetBytes => _process.WorkingSet64;
public long GcMemoryBytes => GC.GetTotalMemory(false);
public int ThreadCount => _process.Threads.Count;
public TimeSpan Uptime => DateTime.UtcNow - _process.StartTime.ToUniversalTime();
public void Dispose()
{
_timer.Dispose();
_process.Dispose();
}
public void Dispose()
{
_timer.Dispose();
_process.Dispose();
}
}

View File

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

View File

@@ -8,507 +8,507 @@ public sealed class WorldService(
IOptions<WorldGenerationOptions> worldGenerationOptions,
IOptions<OrbitalSimulationOptions> orbitalSimulationOptions)
{
private const int DeltaHistoryLimit = 256;
private const int DeltaHistoryLimit = 256;
private readonly Lock _sync = new();
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
private readonly ScenarioLoader _loader = new(environment.ContentRootPath, worldGenerationOptions.Value);
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
private readonly PlayerFactionService _playerFaction = new();
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
private readonly Queue<WorldDelta> _history = [];
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
private long _sequence;
private BalanceDefinition? _balanceOverride;
private readonly Lock _sync = new();
private readonly OrbitalSimulationSnapshot _orbitalSimulation = new(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond);
private readonly ScenarioLoader _loader = new(environment.ContentRootPath, worldGenerationOptions.Value);
private readonly SimulationEngine _engine = new(orbitalSimulationOptions.Value);
private readonly PlayerFactionService _playerFaction = new();
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
private readonly Queue<WorldDelta> _history = [];
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath, worldGenerationOptions.Value).Load();
private long _sequence;
private BalanceDefinition? _balanceOverride;
public WorldSnapshot GetSnapshot()
{
lock (_sync)
public WorldSnapshot GetSnapshot()
{
return _engine.BuildSnapshot(_world, _sequence);
}
}
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))
lock (_sync)
{
channel.Writer.TryWrite(filtered);
return _engine.BuildSnapshot(_world, _sequence);
}
}
}
cancellationToken.Register(() => Unsubscribe(subscriberId));
return channel.Reader;
}
public void Tick(float deltaSeconds)
{
WorldDelta? delta = null;
lock (_sync)
public (long Sequence, DateTimeOffset GeneratedAtUtc) GetStatus()
{
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))
lock (_sync)
{
subscriber.Channel.Writer.TryWrite(filtered);
return (_sequence, _world.GeneratedAtUtc);
}
}
}
}
public WorldSnapshot Reset()
{
lock (_sync)
public (int ConnectedClients, int DeltaHistoryCount) GetConnectionStats()
{
_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);
lock (_sync)
{
return (_subscribers.Count, _history.Count);
}
}
}
private static void ApplyBalance(SimulationWorld world, BalanceDefinition balance) =>
world.Balance = new BalanceDefinition
public BalanceDefinition GetBalance()
{
SimulationSpeedMultiplier = balance.SimulationSpeedMultiplier,
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)),
};
}
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();
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,
};
}
}
}
private WorldDelta FilterDeltaForScope(WorldDelta delta, ObserverScope scope)
{
if (string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase))
public BalanceDefinition UpdateBalance(BalanceDefinition balance)
{
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(),
Scope = scope,
SimulationSpeedMultiplier = balance.SimulationSpeedMultiplier,
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;
if (string.Equals(scope.ScopeKind, "local-celestial", StringComparison.OrdinalIgnoreCase) && systemFilter is null && scope.CelestialId is not null)
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)
{
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
.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,
};
}
if (string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase))
{
return delta with
{
Events = delta.Events.Select((evt) => EnrichEventScope(evt)).ToList(),
Scope = scope,
};
}
private SimulationEventRecord EnrichEventScope(SimulationEventRecord evt)
{
if (!string.Equals(evt.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) || evt.ScopeEntityId is not null)
{
return evt;
var systemFilter = scope.SystemId;
if (string.Equals(scope.ScopeKind, "local-celestial", StringComparison.OrdinalIgnoreCase) && systemFilter is null && scope.CelestialId is not null)
{
systemFilter = ResolveCelestialSystemId(scope.CelestialId);
}
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),
"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),
"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,
};
}
if (!string.Equals(evt.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) || evt.ScopeEntityId is not null)
{
return evt;
}
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)
{
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;
return evt.EntityKind switch
{
"ship" => WithEntityScope(evt, "system", _world.Ships.FirstOrDefault((ship) => ship.Id == evt.EntityId)?.SystemId),
"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),
"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,
};
}
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)
{
if (systemFilter is null)
private bool IsOrderVisibleToScope(MarketOrderDelta order, string? systemFilter)
{
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)
{
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);
private sealed record SubscriptionState(ObserverScope Scope, Channel<WorldDelta> Channel);
}