Implement roadmap phases 1 through 8
This commit is contained in:
@@ -7,8 +7,14 @@ public sealed record WorldSnapshot(
|
|||||||
int TickIntervalMs,
|
int TickIntervalMs,
|
||||||
DateTimeOffset GeneratedAtUtc,
|
DateTimeOffset GeneratedAtUtc,
|
||||||
IReadOnlyList<SystemSnapshot> Systems,
|
IReadOnlyList<SystemSnapshot> Systems,
|
||||||
|
IReadOnlyList<SpatialNodeSnapshot> SpatialNodes,
|
||||||
|
IReadOnlyList<LocalBubbleSnapshot> LocalBubbles,
|
||||||
IReadOnlyList<ResourceNodeSnapshot> Nodes,
|
IReadOnlyList<ResourceNodeSnapshot> Nodes,
|
||||||
IReadOnlyList<StationSnapshot> Stations,
|
IReadOnlyList<StationSnapshot> Stations,
|
||||||
|
IReadOnlyList<ClaimSnapshot> Claims,
|
||||||
|
IReadOnlyList<ConstructionSiteSnapshot> ConstructionSites,
|
||||||
|
IReadOnlyList<MarketOrderSnapshot> MarketOrders,
|
||||||
|
IReadOnlyList<PolicySetSnapshot> Policies,
|
||||||
IReadOnlyList<ShipSnapshot> Ships,
|
IReadOnlyList<ShipSnapshot> Ships,
|
||||||
IReadOnlyList<FactionSnapshot> Factions);
|
IReadOnlyList<FactionSnapshot> Factions);
|
||||||
|
|
||||||
@@ -18,17 +24,33 @@ public sealed record WorldDelta(
|
|||||||
DateTimeOffset GeneratedAtUtc,
|
DateTimeOffset GeneratedAtUtc,
|
||||||
bool RequiresSnapshotRefresh,
|
bool RequiresSnapshotRefresh,
|
||||||
IReadOnlyList<SimulationEventRecord> Events,
|
IReadOnlyList<SimulationEventRecord> Events,
|
||||||
|
IReadOnlyList<SpatialNodeDelta> SpatialNodes,
|
||||||
|
IReadOnlyList<LocalBubbleDelta> LocalBubbles,
|
||||||
IReadOnlyList<ResourceNodeDelta> Nodes,
|
IReadOnlyList<ResourceNodeDelta> Nodes,
|
||||||
IReadOnlyList<StationDelta> Stations,
|
IReadOnlyList<StationDelta> Stations,
|
||||||
|
IReadOnlyList<ClaimDelta> Claims,
|
||||||
|
IReadOnlyList<ConstructionSiteDelta> ConstructionSites,
|
||||||
|
IReadOnlyList<MarketOrderDelta> MarketOrders,
|
||||||
|
IReadOnlyList<PolicySetDelta> Policies,
|
||||||
IReadOnlyList<ShipDelta> Ships,
|
IReadOnlyList<ShipDelta> Ships,
|
||||||
IReadOnlyList<FactionDelta> Factions);
|
IReadOnlyList<FactionDelta> Factions,
|
||||||
|
ObserverScope? Scope = null);
|
||||||
|
|
||||||
public sealed record SimulationEventRecord(
|
public sealed record SimulationEventRecord(
|
||||||
string EntityKind,
|
string EntityKind,
|
||||||
string EntityId,
|
string EntityId,
|
||||||
string Kind,
|
string Kind,
|
||||||
string Message,
|
string Message,
|
||||||
DateTimeOffset OccurredAtUtc);
|
DateTimeOffset OccurredAtUtc,
|
||||||
|
string Family = "simulation",
|
||||||
|
string ScopeKind = "universe",
|
||||||
|
string? ScopeEntityId = null,
|
||||||
|
string Visibility = "public");
|
||||||
|
|
||||||
|
public sealed record ObserverScope(
|
||||||
|
string ScopeKind,
|
||||||
|
string? SystemId = null,
|
||||||
|
string? BubbleId = null);
|
||||||
|
|
||||||
public sealed record SystemSnapshot(
|
public sealed record SystemSnapshot(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -65,6 +87,46 @@ public sealed record ResourceNodeSnapshot(
|
|||||||
float MaxOre,
|
float MaxOre,
|
||||||
string ItemId);
|
string ItemId);
|
||||||
|
|
||||||
|
public sealed record SpatialNodeSnapshot(
|
||||||
|
string Id,
|
||||||
|
string SystemId,
|
||||||
|
string Kind,
|
||||||
|
Vector3Dto LocalPosition,
|
||||||
|
string BubbleId,
|
||||||
|
string? ParentNodeId,
|
||||||
|
string? OccupyingStructureId,
|
||||||
|
string? OrbitReferenceId);
|
||||||
|
|
||||||
|
public sealed record SpatialNodeDelta(
|
||||||
|
string Id,
|
||||||
|
string SystemId,
|
||||||
|
string Kind,
|
||||||
|
Vector3Dto LocalPosition,
|
||||||
|
string BubbleId,
|
||||||
|
string? ParentNodeId,
|
||||||
|
string? OccupyingStructureId,
|
||||||
|
string? OrbitReferenceId);
|
||||||
|
|
||||||
|
public sealed record LocalBubbleSnapshot(
|
||||||
|
string Id,
|
||||||
|
string NodeId,
|
||||||
|
string SystemId,
|
||||||
|
float Radius,
|
||||||
|
IReadOnlyList<string> OccupantShipIds,
|
||||||
|
IReadOnlyList<string> OccupantStationIds,
|
||||||
|
IReadOnlyList<string> OccupantClaimIds,
|
||||||
|
IReadOnlyList<string> OccupantConstructionSiteIds);
|
||||||
|
|
||||||
|
public sealed record LocalBubbleDelta(
|
||||||
|
string Id,
|
||||||
|
string NodeId,
|
||||||
|
string SystemId,
|
||||||
|
float Radius,
|
||||||
|
IReadOnlyList<string> OccupantShipIds,
|
||||||
|
IReadOnlyList<string> OccupantStationIds,
|
||||||
|
IReadOnlyList<string> OccupantClaimIds,
|
||||||
|
IReadOnlyList<string> OccupantConstructionSiteIds);
|
||||||
|
|
||||||
public sealed record ResourceNodeDelta(
|
public sealed record ResourceNodeDelta(
|
||||||
string Id,
|
string Id,
|
||||||
string SystemId,
|
string SystemId,
|
||||||
@@ -84,12 +146,23 @@ public sealed record StationSnapshot(
|
|||||||
string Category,
|
string Category,
|
||||||
string SystemId,
|
string SystemId,
|
||||||
Vector3Dto LocalPosition,
|
Vector3Dto LocalPosition,
|
||||||
|
string? NodeId,
|
||||||
|
string? BubbleId,
|
||||||
|
string? AnchorNodeId,
|
||||||
string Color,
|
string Color,
|
||||||
int DockedShips,
|
int DockedShips,
|
||||||
int DockingPads,
|
int DockingPads,
|
||||||
float EnergyStored,
|
float EnergyStored,
|
||||||
IReadOnlyList<InventoryEntry> Inventory,
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
string FactionId);
|
string FactionId,
|
||||||
|
string? CommanderId,
|
||||||
|
string? PolicySetId,
|
||||||
|
float Population,
|
||||||
|
float PopulationCapacity,
|
||||||
|
float WorkforceRequired,
|
||||||
|
float WorkforceEffectiveRatio,
|
||||||
|
IReadOnlyList<string> InstalledModules,
|
||||||
|
IReadOnlyList<string> MarketOrderIds);
|
||||||
|
|
||||||
public sealed record StationDelta(
|
public sealed record StationDelta(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -97,12 +170,129 @@ public sealed record StationDelta(
|
|||||||
string Category,
|
string Category,
|
||||||
string SystemId,
|
string SystemId,
|
||||||
Vector3Dto LocalPosition,
|
Vector3Dto LocalPosition,
|
||||||
|
string? NodeId,
|
||||||
|
string? BubbleId,
|
||||||
|
string? AnchorNodeId,
|
||||||
string Color,
|
string Color,
|
||||||
int DockedShips,
|
int DockedShips,
|
||||||
int DockingPads,
|
int DockingPads,
|
||||||
float EnergyStored,
|
float EnergyStored,
|
||||||
IReadOnlyList<InventoryEntry> Inventory,
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
string FactionId);
|
string FactionId,
|
||||||
|
string? CommanderId,
|
||||||
|
string? PolicySetId,
|
||||||
|
float Population,
|
||||||
|
float PopulationCapacity,
|
||||||
|
float WorkforceRequired,
|
||||||
|
float WorkforceEffectiveRatio,
|
||||||
|
IReadOnlyList<string> InstalledModules,
|
||||||
|
IReadOnlyList<string> MarketOrderIds);
|
||||||
|
|
||||||
|
public sealed record ClaimSnapshot(
|
||||||
|
string Id,
|
||||||
|
string FactionId,
|
||||||
|
string SystemId,
|
||||||
|
string NodeId,
|
||||||
|
string BubbleId,
|
||||||
|
string State,
|
||||||
|
float Health,
|
||||||
|
DateTimeOffset PlacedAtUtc,
|
||||||
|
DateTimeOffset ActivatesAtUtc);
|
||||||
|
|
||||||
|
public sealed record ClaimDelta(
|
||||||
|
string Id,
|
||||||
|
string FactionId,
|
||||||
|
string SystemId,
|
||||||
|
string NodeId,
|
||||||
|
string BubbleId,
|
||||||
|
string State,
|
||||||
|
float Health,
|
||||||
|
DateTimeOffset PlacedAtUtc,
|
||||||
|
DateTimeOffset ActivatesAtUtc);
|
||||||
|
|
||||||
|
public sealed record ConstructionSiteSnapshot(
|
||||||
|
string Id,
|
||||||
|
string FactionId,
|
||||||
|
string SystemId,
|
||||||
|
string NodeId,
|
||||||
|
string BubbleId,
|
||||||
|
string TargetKind,
|
||||||
|
string TargetDefinitionId,
|
||||||
|
string? BlueprintId,
|
||||||
|
string? ClaimId,
|
||||||
|
string? StationId,
|
||||||
|
string State,
|
||||||
|
float Progress,
|
||||||
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
|
IReadOnlyList<InventoryEntry> RequiredItems,
|
||||||
|
IReadOnlyList<InventoryEntry> DeliveredItems,
|
||||||
|
IReadOnlyList<string> AssignedConstructorShipIds,
|
||||||
|
IReadOnlyList<string> MarketOrderIds);
|
||||||
|
|
||||||
|
public sealed record ConstructionSiteDelta(
|
||||||
|
string Id,
|
||||||
|
string FactionId,
|
||||||
|
string SystemId,
|
||||||
|
string NodeId,
|
||||||
|
string BubbleId,
|
||||||
|
string TargetKind,
|
||||||
|
string TargetDefinitionId,
|
||||||
|
string? BlueprintId,
|
||||||
|
string? ClaimId,
|
||||||
|
string? StationId,
|
||||||
|
string State,
|
||||||
|
float Progress,
|
||||||
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
|
IReadOnlyList<InventoryEntry> RequiredItems,
|
||||||
|
IReadOnlyList<InventoryEntry> DeliveredItems,
|
||||||
|
IReadOnlyList<string> AssignedConstructorShipIds,
|
||||||
|
IReadOnlyList<string> MarketOrderIds);
|
||||||
|
|
||||||
|
public sealed record MarketOrderSnapshot(
|
||||||
|
string Id,
|
||||||
|
string FactionId,
|
||||||
|
string? StationId,
|
||||||
|
string? ConstructionSiteId,
|
||||||
|
string Kind,
|
||||||
|
string ItemId,
|
||||||
|
float Amount,
|
||||||
|
float RemainingAmount,
|
||||||
|
float Valuation,
|
||||||
|
float? ReserveThreshold,
|
||||||
|
string? PolicySetId,
|
||||||
|
string State);
|
||||||
|
|
||||||
|
public sealed record MarketOrderDelta(
|
||||||
|
string Id,
|
||||||
|
string FactionId,
|
||||||
|
string? StationId,
|
||||||
|
string? ConstructionSiteId,
|
||||||
|
string Kind,
|
||||||
|
string ItemId,
|
||||||
|
float Amount,
|
||||||
|
float RemainingAmount,
|
||||||
|
float Valuation,
|
||||||
|
float? ReserveThreshold,
|
||||||
|
string? PolicySetId,
|
||||||
|
string State);
|
||||||
|
|
||||||
|
public sealed record PolicySetSnapshot(
|
||||||
|
string Id,
|
||||||
|
string OwnerKind,
|
||||||
|
string OwnerId,
|
||||||
|
string TradeAccessPolicy,
|
||||||
|
string DockingAccessPolicy,
|
||||||
|
string ConstructionAccessPolicy,
|
||||||
|
string OperationalRangePolicy);
|
||||||
|
|
||||||
|
public sealed record PolicySetDelta(
|
||||||
|
string Id,
|
||||||
|
string OwnerKind,
|
||||||
|
string OwnerId,
|
||||||
|
string TradeAccessPolicy,
|
||||||
|
string DockingAccessPolicy,
|
||||||
|
string ConstructionAccessPolicy,
|
||||||
|
string OperationalRangePolicy);
|
||||||
|
|
||||||
public sealed record ShipSnapshot(
|
public sealed record ShipSnapshot(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -117,12 +307,19 @@ public sealed record ShipSnapshot(
|
|||||||
string? OrderKind,
|
string? OrderKind,
|
||||||
string DefaultBehaviorKind,
|
string DefaultBehaviorKind,
|
||||||
string ControllerTaskKind,
|
string ControllerTaskKind,
|
||||||
|
string? NodeId,
|
||||||
|
string? BubbleId,
|
||||||
|
string? DockedStationId,
|
||||||
|
string? CommanderId,
|
||||||
|
string? PolicySetId,
|
||||||
float CargoCapacity,
|
float CargoCapacity,
|
||||||
|
float WorkerPopulation,
|
||||||
float EnergyStored,
|
float EnergyStored,
|
||||||
IReadOnlyList<InventoryEntry> Inventory,
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
string FactionId,
|
string FactionId,
|
||||||
float Health,
|
float Health,
|
||||||
IReadOnlyList<string> History);
|
IReadOnlyList<string> History,
|
||||||
|
ShipSpatialStateSnapshot SpatialState);
|
||||||
|
|
||||||
public sealed record ShipDelta(
|
public sealed record ShipDelta(
|
||||||
string Id,
|
string Id,
|
||||||
@@ -137,31 +334,61 @@ public sealed record ShipDelta(
|
|||||||
string? OrderKind,
|
string? OrderKind,
|
||||||
string DefaultBehaviorKind,
|
string DefaultBehaviorKind,
|
||||||
string ControllerTaskKind,
|
string ControllerTaskKind,
|
||||||
|
string? NodeId,
|
||||||
|
string? BubbleId,
|
||||||
|
string? DockedStationId,
|
||||||
|
string? CommanderId,
|
||||||
|
string? PolicySetId,
|
||||||
float CargoCapacity,
|
float CargoCapacity,
|
||||||
|
float WorkerPopulation,
|
||||||
float EnergyStored,
|
float EnergyStored,
|
||||||
IReadOnlyList<InventoryEntry> Inventory,
|
IReadOnlyList<InventoryEntry> Inventory,
|
||||||
string FactionId,
|
string FactionId,
|
||||||
float Health,
|
float Health,
|
||||||
IReadOnlyList<string> History);
|
IReadOnlyList<string> History,
|
||||||
|
ShipSpatialStateSnapshot SpatialState);
|
||||||
|
|
||||||
|
public sealed record ShipSpatialStateSnapshot(
|
||||||
|
string SpaceLayer,
|
||||||
|
string CurrentSystemId,
|
||||||
|
string? CurrentNodeId,
|
||||||
|
string? CurrentBubbleId,
|
||||||
|
Vector3Dto? LocalPosition,
|
||||||
|
Vector3Dto? SystemPosition,
|
||||||
|
string MovementRegime,
|
||||||
|
string? DestinationNodeId,
|
||||||
|
ShipTransitSnapshot? Transit);
|
||||||
|
|
||||||
|
public sealed record ShipTransitSnapshot(
|
||||||
|
string Regime,
|
||||||
|
string? OriginNodeId,
|
||||||
|
string? DestinationNodeId,
|
||||||
|
DateTimeOffset? StartedAtUtc,
|
||||||
|
DateTimeOffset? ArrivalDueAtUtc,
|
||||||
|
float Progress);
|
||||||
|
|
||||||
public sealed record FactionSnapshot(
|
public sealed record FactionSnapshot(
|
||||||
string Id,
|
string Id,
|
||||||
string Label,
|
string Label,
|
||||||
string Color,
|
string Color,
|
||||||
float Credits,
|
float Credits,
|
||||||
|
float PopulationTotal,
|
||||||
float OreMined,
|
float OreMined,
|
||||||
float GoodsProduced,
|
float GoodsProduced,
|
||||||
int ShipsBuilt,
|
int ShipsBuilt,
|
||||||
int ShipsLost);
|
int ShipsLost,
|
||||||
|
string? DefaultPolicySetId);
|
||||||
|
|
||||||
public sealed record FactionDelta(
|
public sealed record FactionDelta(
|
||||||
string Id,
|
string Id,
|
||||||
string Label,
|
string Label,
|
||||||
string Color,
|
string Color,
|
||||||
float Credits,
|
float Credits,
|
||||||
|
float PopulationTotal,
|
||||||
float OreMined,
|
float OreMined,
|
||||||
float GoodsProduced,
|
float GoodsProduced,
|
||||||
int ShipsBuilt,
|
int ShipsBuilt,
|
||||||
int ShipsLost);
|
int ShipsLost,
|
||||||
|
string? DefaultPolicySetId);
|
||||||
|
|
||||||
public sealed record Vector3Dto(float X, float Y, float Z);
|
public sealed record Vector3Dto(float X, float Y, float Z);
|
||||||
|
|||||||
@@ -83,6 +83,24 @@ public sealed class ModuleRecipeDefinition
|
|||||||
public required List<RecipeInputDefinition> Inputs { get; set; }
|
public required List<RecipeInputDefinition> Inputs { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class RecipeOutputDefinition
|
||||||
|
{
|
||||||
|
public required string ItemId { get; set; }
|
||||||
|
public float Amount { 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 sealed class PlanetDefinition
|
public sealed class PlanetDefinition
|
||||||
{
|
{
|
||||||
public required string Label { get; set; }
|
public required string Label { get; set; }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using SpaceGame.Simulation.Api.Contracts;
|
||||||
using SpaceGame.Simulation.Api.Simulation;
|
using SpaceGame.Simulation.Api.Simulation;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
@@ -31,7 +32,24 @@ app.MapGet("/api/world/stream", async (HttpContext httpContext, WorldService wor
|
|||||||
|
|
||||||
var afterSequenceRaw = httpContext.Request.Query["afterSequence"].ToString();
|
var afterSequenceRaw = httpContext.Request.Query["afterSequence"].ToString();
|
||||||
_ = long.TryParse(afterSequenceRaw, out var afterSequence);
|
_ = long.TryParse(afterSequenceRaw, out var afterSequence);
|
||||||
var stream = worldService.Subscribe(afterSequence, cancellationToken);
|
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.WriteAsync(": connected\n\n", cancellationToken);
|
||||||
await httpContext.Response.Body.FlushAsync(cancellationToken);
|
await httpContext.Response.Body.FlushAsync(cancellationToken);
|
||||||
|
|||||||
@@ -9,12 +9,20 @@ public sealed class SimulationWorld
|
|||||||
public required BalanceDefinition Balance { get; init; }
|
public required BalanceDefinition Balance { get; init; }
|
||||||
public required List<SystemRuntime> Systems { get; init; }
|
public required List<SystemRuntime> Systems { get; init; }
|
||||||
public required List<ResourceNodeRuntime> Nodes { get; init; }
|
public required List<ResourceNodeRuntime> Nodes { get; init; }
|
||||||
|
public required List<NodeRuntime> SpatialNodes { get; init; }
|
||||||
|
public required List<LocalBubbleRuntime> LocalBubbles { get; init; }
|
||||||
public required List<StationRuntime> Stations { get; init; }
|
public required List<StationRuntime> Stations { get; init; }
|
||||||
public required List<ShipRuntime> Ships { get; init; }
|
public required List<ShipRuntime> Ships { get; init; }
|
||||||
public required List<FactionRuntime> Factions { get; init; }
|
public required List<FactionRuntime> Factions { get; init; }
|
||||||
|
public required List<CommanderRuntime> Commanders { get; init; }
|
||||||
|
public required List<ClaimRuntime> Claims { get; init; }
|
||||||
|
public required List<ConstructionSiteRuntime> ConstructionSites { get; init; }
|
||||||
|
public required List<MarketOrderRuntime> MarketOrders { get; init; }
|
||||||
|
public required List<PolicySetRuntime> Policies { get; init; }
|
||||||
public required Dictionary<string, ShipDefinition> ShipDefinitions { get; init; }
|
public required Dictionary<string, ShipDefinition> ShipDefinitions { get; init; }
|
||||||
public required Dictionary<string, ItemDefinition> ItemDefinitions { get; init; }
|
public required Dictionary<string, ItemDefinition> ItemDefinitions { get; init; }
|
||||||
public required Dictionary<string, ModuleRecipeDefinition> ModuleRecipes { get; init; }
|
public required Dictionary<string, ModuleRecipeDefinition> ModuleRecipes { get; init; }
|
||||||
|
public required Dictionary<string, RecipeDefinition> Recipes { get; init; }
|
||||||
public int TickIntervalMs { get; init; } = 200;
|
public int TickIntervalMs { get; init; } = 200;
|
||||||
public DateTimeOffset GeneratedAtUtc { get; set; }
|
public DateTimeOffset GeneratedAtUtc { get; set; }
|
||||||
}
|
}
|
||||||
@@ -37,18 +45,55 @@ public sealed class ResourceNodeRuntime
|
|||||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class NodeRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string SystemId { get; init; }
|
||||||
|
public required string Kind { get; init; }
|
||||||
|
public required Vector3 Position { get; set; }
|
||||||
|
public required string BubbleId { get; init; }
|
||||||
|
public string? ParentNodeId { get; set; }
|
||||||
|
public string? OccupyingStructureId { get; set; }
|
||||||
|
public string? OrbitReferenceId { get; set; }
|
||||||
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LocalBubbleRuntime
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string NodeId { get; init; }
|
||||||
|
public required string SystemId { get; init; }
|
||||||
|
public float Radius { get; init; }
|
||||||
|
public HashSet<string> OccupantShipIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> OccupantStationIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> OccupantClaimIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public HashSet<string> OccupantConstructionSiteIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class StationRuntime
|
public sealed class StationRuntime
|
||||||
{
|
{
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
public required string SystemId { get; init; }
|
public required string SystemId { get; init; }
|
||||||
public required ConstructibleDefinition Definition { get; init; }
|
public required ConstructibleDefinition Definition { get; init; }
|
||||||
public required Vector3 Position { get; init; }
|
public required Vector3 Position { get; set; }
|
||||||
public required string FactionId { get; init; }
|
public required string FactionId { get; init; }
|
||||||
|
public string? NodeId { get; set; }
|
||||||
|
public string? BubbleId { get; set; }
|
||||||
|
public string? AnchorNodeId { get; set; }
|
||||||
|
public string? CommanderId { get; set; }
|
||||||
|
public string? PolicySetId { get; set; }
|
||||||
public HashSet<string> InstalledModules { get; } = new(StringComparer.Ordinal);
|
public HashSet<string> InstalledModules { get; } = new(StringComparer.Ordinal);
|
||||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||||
public Dictionary<int, string> DockingPadAssignments { get; } = new();
|
public Dictionary<int, string> DockingPadAssignments { get; } = new();
|
||||||
|
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
|
||||||
public float EnergyStored { get; set; }
|
public float EnergyStored { get; set; }
|
||||||
public float ProcessTimer { get; set; }
|
public float ProcessTimer { get; set; }
|
||||||
|
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 HashSet<string> DockedShipIds { get; } = [];
|
public HashSet<string> DockedShipIds { get; } = [];
|
||||||
public ModuleConstructionRuntime? ActiveConstruction { get; set; }
|
public ModuleConstructionRuntime? ActiveConstruction { get; set; }
|
||||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
@@ -70,6 +115,7 @@ public sealed class ShipRuntime
|
|||||||
public required string FactionId { get; init; }
|
public required string FactionId { get; init; }
|
||||||
public required Vector3 Position { get; set; }
|
public required Vector3 Position { get; set; }
|
||||||
public required Vector3 TargetPosition { get; set; }
|
public required Vector3 TargetPosition { get; set; }
|
||||||
|
public required ShipSpatialStateRuntime SpatialState { get; set; }
|
||||||
public Vector3 Velocity { get; set; } = Vector3.Zero;
|
public Vector3 Velocity { get; set; } = Vector3.Zero;
|
||||||
public string State { get; set; } = "idle";
|
public string State { get; set; } = "idle";
|
||||||
public ShipOrderRuntime? Order { get; set; }
|
public ShipOrderRuntime? Order { get; set; }
|
||||||
@@ -77,9 +123,12 @@ public sealed class ShipRuntime
|
|||||||
public required ControllerTaskRuntime ControllerTask { get; set; }
|
public required ControllerTaskRuntime ControllerTask { get; set; }
|
||||||
public float ActionTimer { get; set; }
|
public float ActionTimer { get; set; }
|
||||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||||
|
public float WorkerPopulation { get; set; }
|
||||||
public float EnergyStored { get; set; }
|
public float EnergyStored { get; set; }
|
||||||
public string? DockedStationId { get; set; }
|
public string? DockedStationId { get; set; }
|
||||||
public int? AssignedDockingPadIndex { get; set; }
|
public int? AssignedDockingPadIndex { get; set; }
|
||||||
|
public string? CommanderId { get; set; }
|
||||||
|
public string? PolicySetId { get; set; }
|
||||||
public float Health { get; set; }
|
public float Health { get; set; }
|
||||||
public List<string> History { get; } = [];
|
public List<string> History { get; } = [];
|
||||||
public string LastSignature { get; set; } = string.Empty;
|
public string LastSignature { get; set; } = string.Empty;
|
||||||
@@ -92,10 +141,128 @@ public sealed class FactionRuntime
|
|||||||
public required string Label { get; init; }
|
public required string Label { get; init; }
|
||||||
public required string Color { get; init; }
|
public required string Color { get; init; }
|
||||||
public float Credits { get; set; }
|
public float Credits { get; set; }
|
||||||
|
public float PopulationTotal { get; set; }
|
||||||
public float OreMined { get; set; }
|
public float OreMined { get; set; }
|
||||||
public float GoodsProduced { get; set; }
|
public float GoodsProduced { get; set; }
|
||||||
public int ShipsBuilt { get; set; }
|
public int ShipsBuilt { get; set; }
|
||||||
public int ShipsLost { get; set; }
|
public int ShipsLost { get; set; }
|
||||||
|
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public string? DefaultPolicySetId { get; set; }
|
||||||
|
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 List<string> Goals { get; } = [];
|
||||||
|
public CommanderBehaviorRuntime? ActiveBehavior { get; set; }
|
||||||
|
public CommanderOrderRuntime? ActiveOrder { get; set; }
|
||||||
|
public CommanderTaskRuntime? ActiveTask { get; set; }
|
||||||
|
public HashSet<string> SubordinateCommanderIds { get; } = new(StringComparer.Ordinal);
|
||||||
|
public bool IsAlive { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CommanderBehaviorRuntime
|
||||||
|
{
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public string? Phase { get; set; }
|
||||||
|
public string? NodeId { get; set; }
|
||||||
|
public string? StationId { get; set; }
|
||||||
|
public string? ModuleId { get; set; }
|
||||||
|
public string? AreaSystemId { get; set; }
|
||||||
|
public int PatrolIndex { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CommanderOrderRuntime
|
||||||
|
{
|
||||||
|
public required string Kind { get; init; }
|
||||||
|
public string Status { get; set; } = "accepted";
|
||||||
|
public string? TargetEntityId { get; set; }
|
||||||
|
public string? DestinationNodeId { get; set; }
|
||||||
|
public required string DestinationSystemId { get; init; }
|
||||||
|
public required Vector3 DestinationPosition { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CommanderTaskRuntime
|
||||||
|
{
|
||||||
|
public required string Kind { get; set; }
|
||||||
|
public string Status { get; set; } = "pending";
|
||||||
|
public string? TargetEntityId { get; set; }
|
||||||
|
public string? TargetSystemId { get; set; }
|
||||||
|
public string? TargetNodeId { get; set; }
|
||||||
|
public Vector3? TargetPosition { get; set; }
|
||||||
|
public float Threshold { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 NodeId { get; init; }
|
||||||
|
public required string BubbleId { 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 NodeId { get; init; }
|
||||||
|
public required string BubbleId { 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 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 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 LastDeltaSignature { get; set; } = string.Empty;
|
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,12 +290,131 @@ public sealed class DefaultBehaviorRuntime
|
|||||||
public sealed class ControllerTaskRuntime
|
public sealed class ControllerTaskRuntime
|
||||||
{
|
{
|
||||||
public required string Kind { get; set; }
|
public required string Kind { get; set; }
|
||||||
|
public string Status { get; set; } = "pending";
|
||||||
|
public string? CommanderId { get; set; }
|
||||||
public string? TargetEntityId { get; set; }
|
public string? TargetEntityId { get; set; }
|
||||||
public string? TargetSystemId { get; set; }
|
public string? TargetSystemId { get; set; }
|
||||||
|
public string? TargetNodeId { get; set; }
|
||||||
public Vector3? TargetPosition { get; set; }
|
public Vector3? TargetPosition { get; set; }
|
||||||
public float Threshold { get; set; }
|
public float Threshold { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class ShipSpatialStateRuntime
|
||||||
|
{
|
||||||
|
public string SpaceLayer { get; set; } = SpaceLayerKinds.LocalSpace;
|
||||||
|
public required string CurrentSystemId { get; set; }
|
||||||
|
public string? CurrentNodeId { get; set; }
|
||||||
|
public string? CurrentBubbleId { get; set; }
|
||||||
|
public Vector3? LocalPosition { get; set; }
|
||||||
|
public Vector3? SystemPosition { get; set; }
|
||||||
|
public string MovementRegime { get; set; } = MovementRegimeKinds.LocalFlight;
|
||||||
|
public string? DestinationNodeId { get; set; }
|
||||||
|
public ShipTransitRuntime? Transit { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ShipTransitRuntime
|
||||||
|
{
|
||||||
|
public required string Regime { get; init; }
|
||||||
|
public string? OriginNodeId { get; init; }
|
||||||
|
public string? DestinationNodeId { get; init; }
|
||||||
|
public DateTimeOffset? StartedAtUtc { get; set; }
|
||||||
|
public DateTimeOffset? ArrivalDueAtUtc { get; set; }
|
||||||
|
public float Progress { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 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 static class ShipTaskKinds
|
||||||
|
{
|
||||||
|
public const string Idle = "idle";
|
||||||
|
public const string LocalMove = "local-move";
|
||||||
|
public const string WarpToNode = "warp-to-node";
|
||||||
|
public const string UseStargate = "use-stargate";
|
||||||
|
public const string UseFtl = "use-ftl";
|
||||||
|
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 LoadWorkers = "load-workers";
|
||||||
|
public const string UnloadWorkers = "unload-workers";
|
||||||
|
public const string MineNode = "mine-node";
|
||||||
|
public const string HarvestGas = "harvest-gas";
|
||||||
|
public const string DeliverToStation = "deliver-to-station";
|
||||||
|
public const string ClaimLagrangePoint = "claim-lagrange-point";
|
||||||
|
public const string BuildConstructionSite = "build-construction-site";
|
||||||
|
public const string EscortTarget = "escort-target";
|
||||||
|
public const string AttackTarget = "attack-target";
|
||||||
|
public const string DefendBubble = "defend-bubble";
|
||||||
|
public const string Retreat = "retreat";
|
||||||
|
public const string HoldPosition = "hold-position";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ShipOrderKinds
|
||||||
|
{
|
||||||
|
public const string DirectMove = "direct-move";
|
||||||
|
public const string TravelToNode = "travel-to-node";
|
||||||
|
public const string DockAtStation = "dock-at-station";
|
||||||
|
public const string DeliverCargo = "deliver-cargo";
|
||||||
|
public const string BuildAtSite = "build-at-site";
|
||||||
|
public const string AttackTarget = "attack-target";
|
||||||
|
public const string HoldPosition = "hold-position";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 static class MarketOrderKinds
|
||||||
|
{
|
||||||
|
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 readonly record struct Vector3(float X, float Y, float Z)
|
public readonly record struct Vector3(float X, float Y, float Z)
|
||||||
{
|
{
|
||||||
public static Vector3 Zero => new(0f, 0f, 0f);
|
public static Vector3 Zero => new(0f, 0f, 0f);
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ public sealed class ScenarioLoader
|
|||||||
private const float MinimumRefineryStock = 0f;
|
private const float MinimumRefineryStock = 0f;
|
||||||
private const float MinimumShipyardStock = 0f;
|
private const float MinimumShipyardStock = 0f;
|
||||||
private const float MinimumSystemSeparation = 3200f;
|
private const float MinimumSystemSeparation = 3200f;
|
||||||
|
private const float StarBubbleRadiusPadding = 40f;
|
||||||
|
private const float PlanetBubbleRadiusPadding = 80f;
|
||||||
|
private const float MoonBubbleRadiusPadding = 40f;
|
||||||
|
private const float LagrangeBubbleRadius = 150f;
|
||||||
|
private const float ResourceBubbleRadius = 120f;
|
||||||
private static readonly string[] GeneratedSystemNames =
|
private static readonly string[] GeneratedSystemNames =
|
||||||
[
|
[
|
||||||
"Aquila Verge",
|
"Aquila Verge",
|
||||||
@@ -88,12 +93,14 @@ public sealed class ScenarioLoader
|
|||||||
var ships = Read<List<ShipDefinition>>("ships.json");
|
var ships = Read<List<ShipDefinition>>("ships.json");
|
||||||
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.json");
|
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.json");
|
||||||
var items = Read<List<ItemDefinition>>("items.json");
|
var items = Read<List<ItemDefinition>>("items.json");
|
||||||
|
var recipes = Read<List<RecipeDefinition>>("recipes.json");
|
||||||
var moduleRecipes = Read<List<ModuleRecipeDefinition>>("module-recipes.json");
|
var moduleRecipes = Read<List<ModuleRecipeDefinition>>("module-recipes.json");
|
||||||
var balance = Read<BalanceDefinition>("balance.json");
|
var balance = Read<BalanceDefinition>("balance.json");
|
||||||
|
|
||||||
var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||||
var constructibleDefinitions = constructibles.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
var constructibleDefinitions = constructibles.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||||
var itemDefinitions = items.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
var itemDefinitions = items.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||||
|
var recipeDefinitions = recipes.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||||
var moduleRecipeDefinitions = moduleRecipes.ToDictionary((definition) => definition.ModuleId, StringComparer.Ordinal);
|
var moduleRecipeDefinitions = moduleRecipes.ToDictionary((definition) => definition.ModuleId, StringComparer.Ordinal);
|
||||||
var systemRuntimes = systems
|
var systemRuntimes = systems
|
||||||
.Select((definition) => new SystemRuntime
|
.Select((definition) => new SystemRuntime
|
||||||
@@ -103,14 +110,26 @@ public sealed class ScenarioLoader
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
var systemsById = systemRuntimes.ToDictionary((system) => system.Definition.Id, StringComparer.Ordinal);
|
var systemsById = systemRuntimes.ToDictionary((system) => system.Definition.Id, StringComparer.Ordinal);
|
||||||
|
var systemGraphs = systemRuntimes.ToDictionary(
|
||||||
|
(system) => system.Definition.Id,
|
||||||
|
(system) => BuildSystemSpatialGraph(system),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
var nodes = new List<ResourceNodeRuntime>();
|
var nodes = new List<ResourceNodeRuntime>();
|
||||||
|
var spatialNodes = new List<NodeRuntime>();
|
||||||
|
var localBubbles = new List<LocalBubbleRuntime>();
|
||||||
var nodeIdCounter = 0;
|
var nodeIdCounter = 0;
|
||||||
|
foreach (var graph in systemGraphs.Values)
|
||||||
|
{
|
||||||
|
spatialNodes.AddRange(graph.Nodes);
|
||||||
|
localBubbles.AddRange(graph.Bubbles);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var system in systemRuntimes)
|
foreach (var system in systemRuntimes)
|
||||||
{
|
{
|
||||||
foreach (var node in system.Definition.ResourceNodes)
|
foreach (var node in system.Definition.ResourceNodes)
|
||||||
{
|
{
|
||||||
nodes.Add(new ResourceNodeRuntime
|
var resourceNode = new ResourceNodeRuntime
|
||||||
{
|
{
|
||||||
Id = $"node-{++nodeIdCounter}",
|
Id = $"node-{++nodeIdCounter}",
|
||||||
SystemId = system.Definition.Id,
|
SystemId = system.Definition.Id,
|
||||||
@@ -122,6 +141,24 @@ public sealed class ScenarioLoader
|
|||||||
ItemId = node.ItemId,
|
ItemId = node.ItemId,
|
||||||
OreRemaining = node.OreAmount,
|
OreRemaining = node.OreAmount,
|
||||||
MaxOre = node.OreAmount,
|
MaxOre = node.OreAmount,
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.Add(resourceNode);
|
||||||
|
var bubbleId = $"bubble-{resourceNode.Id}";
|
||||||
|
spatialNodes.Add(new NodeRuntime
|
||||||
|
{
|
||||||
|
Id = resourceNode.Id,
|
||||||
|
SystemId = resourceNode.SystemId,
|
||||||
|
Kind = "resource-site",
|
||||||
|
Position = resourceNode.Position,
|
||||||
|
BubbleId = bubbleId,
|
||||||
|
});
|
||||||
|
localBubbles.Add(new LocalBubbleRuntime
|
||||||
|
{
|
||||||
|
Id = bubbleId,
|
||||||
|
NodeId = resourceNode.Id,
|
||||||
|
SystemId = resourceNode.SystemId,
|
||||||
|
Radius = ResourceBubbleRadius,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,14 +172,41 @@ public sealed class ScenarioLoader
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
stations.Add(new StationRuntime
|
var placement = ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], spatialNodes);
|
||||||
|
var station = new StationRuntime
|
||||||
{
|
{
|
||||||
Id = $"station-{++stationIdCounter}",
|
Id = $"station-{++stationIdCounter}",
|
||||||
SystemId = system.Definition.Id,
|
SystemId = system.Definition.Id,
|
||||||
Definition = definition,
|
Definition = definition,
|
||||||
Position = ResolveStationPosition(system, plan, balance),
|
Position = placement.Position,
|
||||||
FactionId = plan.FactionId ?? DefaultFactionId,
|
FactionId = plan.FactionId ?? DefaultFactionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
var stationNodeId = $"node-{station.Id}";
|
||||||
|
var stationBubbleId = $"bubble-{station.Id}";
|
||||||
|
station.NodeId = stationNodeId;
|
||||||
|
station.BubbleId = stationBubbleId;
|
||||||
|
station.AnchorNodeId = placement.AnchorNode.Id;
|
||||||
|
stations.Add(station);
|
||||||
|
spatialNodes.Add(new NodeRuntime
|
||||||
|
{
|
||||||
|
Id = stationNodeId,
|
||||||
|
SystemId = station.SystemId,
|
||||||
|
Kind = "station",
|
||||||
|
Position = station.Position,
|
||||||
|
BubbleId = stationBubbleId,
|
||||||
|
ParentNodeId = placement.AnchorNode.Id,
|
||||||
|
OccupyingStructureId = station.Id,
|
||||||
});
|
});
|
||||||
|
localBubbles.Add(new LocalBubbleRuntime
|
||||||
|
{
|
||||||
|
Id = stationBubbleId,
|
||||||
|
NodeId = stationNodeId,
|
||||||
|
SystemId = station.SystemId,
|
||||||
|
Radius = MathF.Max(160f, definition.Radius + 60f),
|
||||||
|
});
|
||||||
|
localBubbles[^1].OccupantStationIds.Add(station.Id);
|
||||||
|
placement.AnchorNode.OccupyingStructureId = station.Id;
|
||||||
|
|
||||||
foreach (var moduleId in definition.Modules)
|
foreach (var moduleId in definition.Modules)
|
||||||
{
|
{
|
||||||
@@ -152,8 +216,13 @@ public sealed class ScenarioLoader
|
|||||||
|
|
||||||
foreach (var station in stations)
|
foreach (var station in stations)
|
||||||
{
|
{
|
||||||
|
InitializeStationPopulation(station);
|
||||||
station.Inventory["fuel"] = 240f;
|
station.Inventory["fuel"] = 240f;
|
||||||
station.Inventory["refined-metals"] = 120f;
|
station.Inventory["refined-metals"] = 120f;
|
||||||
|
if (station.Population > 0f)
|
||||||
|
{
|
||||||
|
station.Inventory["water"] = MathF.Max(80f, station.Population * 1.5f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var refinery = stations.FirstOrDefault((station) =>
|
var refinery = stations.FirstOrDefault((station) =>
|
||||||
@@ -187,8 +256,9 @@ public sealed class ScenarioLoader
|
|||||||
FactionId = formation.FactionId ?? DefaultFactionId,
|
FactionId = formation.FactionId ?? DefaultFactionId,
|
||||||
Position = position,
|
Position = position,
|
||||||
TargetPosition = position,
|
TargetPosition = position,
|
||||||
|
SpatialState = CreateInitialShipSpatialState(formation.SystemId, position, spatialNodes),
|
||||||
DefaultBehavior = CreateBehavior(definition, formation.SystemId, scenario, patrolRoutes, refinery),
|
DefaultBehavior = CreateBehavior(definition, formation.SystemId, scenario, patrolRoutes, refinery),
|
||||||
ControllerTask = new ControllerTaskRuntime { Kind = "idle", Threshold = balance.ArrivalThreshold },
|
ControllerTask = new ControllerTaskRuntime { Kind = "idle", Threshold = balance.ArrivalThreshold, Status = "pending" },
|
||||||
Health = definition.MaxHealth,
|
Health = definition.MaxHealth,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,6 +279,11 @@ public sealed class ScenarioLoader
|
|||||||
|
|
||||||
var factions = CreateFactions(stations, shipsRuntime);
|
var factions = CreateFactions(stations, shipsRuntime);
|
||||||
BootstrapFactionEconomy(factions, stations);
|
BootstrapFactionEconomy(factions, stations);
|
||||||
|
var policies = CreatePolicies(factions);
|
||||||
|
var commanders = CreateCommanders(factions, stations, shipsRuntime);
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
var claims = CreateClaims(stations, spatialNodes, now);
|
||||||
|
var (constructionSites, marketOrders) = CreateConstructionSites(stations, claims, spatialNodes, moduleRecipeDefinitions);
|
||||||
|
|
||||||
return new SimulationWorld
|
return new SimulationWorld
|
||||||
{
|
{
|
||||||
@@ -217,12 +292,20 @@ public sealed class ScenarioLoader
|
|||||||
Balance = balance,
|
Balance = balance,
|
||||||
Systems = systemRuntimes,
|
Systems = systemRuntimes,
|
||||||
Nodes = nodes,
|
Nodes = nodes,
|
||||||
|
SpatialNodes = spatialNodes,
|
||||||
|
LocalBubbles = localBubbles,
|
||||||
Stations = stations,
|
Stations = stations,
|
||||||
Ships = shipsRuntime,
|
Ships = shipsRuntime,
|
||||||
Factions = factions,
|
Factions = factions,
|
||||||
|
Commanders = commanders,
|
||||||
|
Claims = claims,
|
||||||
|
ConstructionSites = constructionSites,
|
||||||
|
MarketOrders = marketOrders,
|
||||||
|
Policies = policies,
|
||||||
ShipDefinitions = shipDefinitions,
|
ShipDefinitions = shipDefinitions,
|
||||||
ItemDefinitions = itemDefinitions,
|
ItemDefinitions = itemDefinitions,
|
||||||
ModuleRecipes = moduleRecipeDefinitions,
|
ModuleRecipes = moduleRecipeDefinitions,
|
||||||
|
Recipes = recipeDefinitions,
|
||||||
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -810,6 +893,442 @@ public sealed class ScenarioLoader
|
|||||||
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
|
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
|
||||||
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
|
inventory.TryGetValue(itemId, out var amount) ? amount : 0f;
|
||||||
|
|
||||||
|
private static SystemSpatialGraph BuildSystemSpatialGraph(SystemRuntime system)
|
||||||
|
{
|
||||||
|
var nodes = new List<NodeRuntime>();
|
||||||
|
var bubbles = new List<LocalBubbleRuntime>();
|
||||||
|
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, NodeRuntime>>();
|
||||||
|
|
||||||
|
var starNode = AddSpatialNode(
|
||||||
|
nodes,
|
||||||
|
bubbles,
|
||||||
|
id: $"node-{system.Definition.Id}-star",
|
||||||
|
systemId: system.Definition.Id,
|
||||||
|
kind: "star",
|
||||||
|
position: Vector3.Zero,
|
||||||
|
radius: MathF.Max(system.Definition.GravityWellRadius + StarBubbleRadiusPadding, 180f));
|
||||||
|
|
||||||
|
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 planetNode = AddSpatialNode(
|
||||||
|
nodes,
|
||||||
|
bubbles,
|
||||||
|
id: planetNodeId,
|
||||||
|
systemId: system.Definition.Id,
|
||||||
|
kind: "planet",
|
||||||
|
position: planetPosition,
|
||||||
|
radius: MathF.Max(planet.Size + PlanetBubbleRadiusPadding, 120f),
|
||||||
|
parentNodeId: starNode.Id);
|
||||||
|
|
||||||
|
var lagrangeNodes = new Dictionary<string, NodeRuntime>(StringComparer.Ordinal);
|
||||||
|
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet.OrbitRadius, planetIndex))
|
||||||
|
{
|
||||||
|
var lagrangeNode = AddSpatialNode(
|
||||||
|
nodes,
|
||||||
|
bubbles,
|
||||||
|
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{point.Designation.ToLowerInvariant()}",
|
||||||
|
systemId: system.Definition.Id,
|
||||||
|
kind: "lagrange-point",
|
||||||
|
position: point.Position,
|
||||||
|
radius: LagrangeBubbleRadius,
|
||||||
|
parentNodeId: planetNode.Id,
|
||||||
|
orbitReferenceId: point.Designation);
|
||||||
|
lagrangeNodes[point.Designation] = lagrangeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;
|
||||||
|
|
||||||
|
if (planet.MoonCount <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var moonOrbitRadius = MathF.Max(planet.Size + 48f, 42f);
|
||||||
|
for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1)
|
||||||
|
{
|
||||||
|
var moonPosition = ComputeMoonPosition(planetPosition, moonOrbitRadius, moonIndex, planetIndex);
|
||||||
|
AddSpatialNode(
|
||||||
|
nodes,
|
||||||
|
bubbles,
|
||||||
|
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
|
||||||
|
systemId: system.Definition.Id,
|
||||||
|
kind: "moon",
|
||||||
|
position: moonPosition,
|
||||||
|
radius: MoonBubbleRadiusPadding + 24f,
|
||||||
|
parentNodeId: planetNode.Id);
|
||||||
|
moonOrbitRadius += 30f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SystemSpatialGraph(system.Definition.Id, nodes, bubbles, lagrangeNodesByPlanetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NodeRuntime AddSpatialNode(
|
||||||
|
ICollection<NodeRuntime> nodes,
|
||||||
|
ICollection<LocalBubbleRuntime> bubbles,
|
||||||
|
string id,
|
||||||
|
string systemId,
|
||||||
|
string kind,
|
||||||
|
Vector3 position,
|
||||||
|
float radius,
|
||||||
|
string? parentNodeId = null,
|
||||||
|
string? orbitReferenceId = null)
|
||||||
|
{
|
||||||
|
var bubbleId = $"bubble-{id}";
|
||||||
|
var node = new NodeRuntime
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
SystemId = systemId,
|
||||||
|
Kind = kind,
|
||||||
|
Position = position,
|
||||||
|
BubbleId = bubbleId,
|
||||||
|
ParentNodeId = parentNodeId,
|
||||||
|
OrbitReferenceId = orbitReferenceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.Add(node);
|
||||||
|
bubbles.Add(new LocalBubbleRuntime
|
||||||
|
{
|
||||||
|
Id = bubbleId,
|
||||||
|
NodeId = id,
|
||||||
|
SystemId = systemId,
|
||||||
|
Radius = radius,
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(Vector3 planetPosition, float orbitRadius, int planetIndex)
|
||||||
|
{
|
||||||
|
var radial = NormalizeOrFallback(planetPosition, new Vector3(1f, 0f, 0f));
|
||||||
|
var tangential = new Vector3(-radial.Z, 0f, radial.X);
|
||||||
|
var offset = MathF.Max(orbitRadius * 0.18f, 72f + (planetIndex * 6f));
|
||||||
|
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, -orbitRadius));
|
||||||
|
yield return new LagrangePointPlacement(
|
||||||
|
"L4",
|
||||||
|
Add(Scale(radial, orbitRadius * MathF.Cos(triangularAngle)), Scale(tangential, orbitRadius * MathF.Sin(triangularAngle))));
|
||||||
|
yield return new LagrangePointPlacement(
|
||||||
|
"L5",
|
||||||
|
Add(Scale(radial, orbitRadius * MathF.Cos(triangularAngle)), Scale(tangential, -orbitRadius * MathF.Sin(triangularAngle))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StationPlacement ResolveStationPlacement(
|
||||||
|
InitialStationDefinition plan,
|
||||||
|
SystemRuntime system,
|
||||||
|
SystemSpatialGraph graph,
|
||||||
|
IReadOnlyCollection<NodeRuntime> existingNodes)
|
||||||
|
{
|
||||||
|
if (plan.PlanetIndex is int planetIndex &&
|
||||||
|
graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes))
|
||||||
|
{
|
||||||
|
var designation = ResolveLagrangeDesignation(plan.LagrangeSide);
|
||||||
|
if (lagrangeNodes.TryGetValue(designation, out var lagrangeNode))
|
||||||
|
{
|
||||||
|
return new StationPlacement(lagrangeNode, lagrangeNode.Position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.Position is { Length: 3 })
|
||||||
|
{
|
||||||
|
var targetPosition = NormalizeScenarioPoint(system, plan.Position);
|
||||||
|
var preferredNode = existingNodes
|
||||||
|
.Where((node) => node.SystemId == system.Definition.Id && node.Kind == "lagrange-point")
|
||||||
|
.OrderBy((node) => node.Position.DistanceTo(targetPosition))
|
||||||
|
.FirstOrDefault()
|
||||||
|
?? existingNodes
|
||||||
|
.Where((node) => node.SystemId == system.Definition.Id)
|
||||||
|
.OrderBy((node) => node.Position.DistanceTo(targetPosition))
|
||||||
|
.First();
|
||||||
|
return new StationPlacement(preferredNode, preferredNode.Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fallbackNode = graph.Nodes
|
||||||
|
.FirstOrDefault((node) => node.Kind == "lagrange-point" && string.IsNullOrEmpty(node.OccupyingStructureId))
|
||||||
|
?? graph.Nodes.First((node) => node.Kind == "planet");
|
||||||
|
return new StationPlacement(fallbackNode, fallbackNode.Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch
|
||||||
|
{
|
||||||
|
< 0 => "L4",
|
||||||
|
> 0 => "L5",
|
||||||
|
_ => "L1",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
|
||||||
|
{
|
||||||
|
var angle = DegreesToRadians(planet.OrbitPhaseAtEpoch);
|
||||||
|
var x = MathF.Cos(angle) * planet.OrbitRadius;
|
||||||
|
var z = MathF.Sin(angle) * planet.OrbitRadius;
|
||||||
|
return new Vector3(x, 0f, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3 ComputeMoonPosition(Vector3 planetPosition, float orbitRadius, int moonIndex, int planetIndex)
|
||||||
|
{
|
||||||
|
var angle = ((MathF.PI * 2f) / MathF.Max(1, moonIndex + 3)) * (moonIndex + 1) + (planetIndex * 0.37f);
|
||||||
|
return Add(planetPosition, new Vector3(MathF.Cos(angle) * orbitRadius, 0f, MathF.Sin(angle) * orbitRadius));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<NodeRuntime> nodes)
|
||||||
|
{
|
||||||
|
var nearestNode = nodes
|
||||||
|
.Where((node) => node.SystemId == systemId)
|
||||||
|
.OrderBy((node) => node.Position.DistanceTo(position))
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
return new ShipSpatialStateRuntime
|
||||||
|
{
|
||||||
|
CurrentSystemId = systemId,
|
||||||
|
SpaceLayer = SpaceLayerKinds.LocalSpace,
|
||||||
|
CurrentNodeId = nearestNode?.Id,
|
||||||
|
CurrentBubbleId = nearestNode?.BubbleId,
|
||||||
|
LocalPosition = position,
|
||||||
|
SystemPosition = position,
|
||||||
|
MovementRegime = MovementRegimeKinds.LocalFlight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ClaimRuntime> CreateClaims(
|
||||||
|
IReadOnlyCollection<StationRuntime> stations,
|
||||||
|
IReadOnlyCollection<NodeRuntime> nodes,
|
||||||
|
DateTimeOffset nowUtc)
|
||||||
|
{
|
||||||
|
var claims = new List<ClaimRuntime>(stations.Count);
|
||||||
|
foreach (var station in stations)
|
||||||
|
{
|
||||||
|
if (station.AnchorNodeId is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var anchorNode = nodes.FirstOrDefault((node) => node.Id == station.AnchorNodeId);
|
||||||
|
if (anchorNode is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
claims.Add(new ClaimRuntime
|
||||||
|
{
|
||||||
|
Id = $"claim-{station.Id}",
|
||||||
|
FactionId = station.FactionId,
|
||||||
|
SystemId = station.SystemId,
|
||||||
|
NodeId = anchorNode.Id,
|
||||||
|
BubbleId = anchorNode.BubbleId,
|
||||||
|
PlacedAtUtc = nowUtc,
|
||||||
|
ActivatesAtUtc = nowUtc.AddSeconds(8),
|
||||||
|
State = ClaimStateKinds.Activating,
|
||||||
|
Health = 100f,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (List<ConstructionSiteRuntime> ConstructionSites, List<MarketOrderRuntime> MarketOrders) CreateConstructionSites(
|
||||||
|
IReadOnlyCollection<StationRuntime> stations,
|
||||||
|
IReadOnlyCollection<ClaimRuntime> claims,
|
||||||
|
IReadOnlyCollection<NodeRuntime> nodes,
|
||||||
|
IReadOnlyDictionary<string, ModuleRecipeDefinition> moduleRecipes)
|
||||||
|
{
|
||||||
|
var sites = new List<ConstructionSiteRuntime>();
|
||||||
|
var orders = new List<MarketOrderRuntime>();
|
||||||
|
|
||||||
|
foreach (var station in stations)
|
||||||
|
{
|
||||||
|
var moduleId = GetNextConstructionSiteModule(station, moduleRecipes);
|
||||||
|
if (moduleId is null || station.AnchorNodeId is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var anchorNode = nodes.FirstOrDefault((node) => node.Id == station.AnchorNodeId);
|
||||||
|
var claim = claims.FirstOrDefault((candidate) => candidate.Id == $"claim-{station.Id}");
|
||||||
|
if (anchorNode is null || claim is null || !moduleRecipes.TryGetValue(moduleId, out var recipe))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var site = new ConstructionSiteRuntime
|
||||||
|
{
|
||||||
|
Id = $"site-{station.Id}",
|
||||||
|
FactionId = station.FactionId,
|
||||||
|
SystemId = station.SystemId,
|
||||||
|
NodeId = anchorNode.Id,
|
||||||
|
BubbleId = anchorNode.BubbleId,
|
||||||
|
TargetKind = "station-module",
|
||||||
|
TargetDefinitionId = station.Definition.Id,
|
||||||
|
BlueprintId = moduleId,
|
||||||
|
ClaimId = claim.Id,
|
||||||
|
StationId = station.Id,
|
||||||
|
State = claim.State == ClaimStateKinds.Active ? ConstructionSiteStateKinds.Active : ConstructionSiteStateKinds.Planned,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var input in recipe.Inputs)
|
||||||
|
{
|
||||||
|
site.RequiredItems[input.ItemId] = input.Amount;
|
||||||
|
site.DeliveredItems[input.ItemId] = 0f;
|
||||||
|
|
||||||
|
var orderId = $"market-order-{station.Id}-{moduleId}-{input.ItemId}";
|
||||||
|
site.MarketOrderIds.Add(orderId);
|
||||||
|
station.MarketOrderIds.Add(orderId);
|
||||||
|
orders.Add(new MarketOrderRuntime
|
||||||
|
{
|
||||||
|
Id = orderId,
|
||||||
|
FactionId = station.FactionId,
|
||||||
|
StationId = station.Id,
|
||||||
|
ConstructionSiteId = site.Id,
|
||||||
|
Kind = MarketOrderKinds.Buy,
|
||||||
|
ItemId = input.ItemId,
|
||||||
|
Amount = input.Amount,
|
||||||
|
RemainingAmount = input.Amount,
|
||||||
|
Valuation = 1f,
|
||||||
|
State = MarketOrderStateKinds.Open,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sites.Add(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (sites, orders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetNextConstructionSiteModule(
|
||||||
|
StationRuntime station,
|
||||||
|
IReadOnlyDictionary<string, ModuleRecipeDefinition> moduleRecipes)
|
||||||
|
{
|
||||||
|
foreach (var moduleId in new[] { "gas-tank", "fuel-processor", "refinery-stack", "dock-bay-small" })
|
||||||
|
{
|
||||||
|
if (!station.InstalledModules.Contains(moduleId, StringComparer.Ordinal)
|
||||||
|
&& moduleRecipes.ContainsKey(moduleId))
|
||||||
|
{
|
||||||
|
return moduleId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InitializeStationPopulation(StationRuntime station)
|
||||||
|
{
|
||||||
|
var habitatModules = CountModules(station.InstalledModules, "habitat-ring");
|
||||||
|
station.PopulationCapacity = 40f + (habitatModules * 220f);
|
||||||
|
station.WorkforceRequired = MathF.Max(12f, station.InstalledModules.Count * 14f);
|
||||||
|
station.Population = habitatModules > 0
|
||||||
|
? MathF.Min(station.PopulationCapacity * 0.65f, station.WorkforceRequired * 1.05f)
|
||||||
|
: MathF.Min(28f, station.PopulationCapacity);
|
||||||
|
station.WorkforceEffectiveRatio = ComputeWorkforceRatio(station.Population, station.WorkforceRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PolicySetRuntime> CreatePolicies(IReadOnlyCollection<FactionRuntime> factions)
|
||||||
|
{
|
||||||
|
var policies = new List<PolicySetRuntime>(factions.Count);
|
||||||
|
foreach (var faction in factions)
|
||||||
|
{
|
||||||
|
var policyId = $"policy-{faction.Id}";
|
||||||
|
faction.DefaultPolicySetId = policyId;
|
||||||
|
policies.Add(new PolicySetRuntime
|
||||||
|
{
|
||||||
|
Id = policyId,
|
||||||
|
OwnerKind = "faction",
|
||||||
|
OwnerId = faction.Id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return policies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<CommanderRuntime> CreateCommanders(
|
||||||
|
IReadOnlyCollection<FactionRuntime> factions,
|
||||||
|
IReadOnlyCollection<StationRuntime> stations,
|
||||||
|
IReadOnlyCollection<ShipRuntime> ships)
|
||||||
|
{
|
||||||
|
var commanders = new List<CommanderRuntime>();
|
||||||
|
var factionCommanders = new Dictionary<string, CommanderRuntime>(StringComparer.Ordinal);
|
||||||
|
var factionsById = factions.ToDictionary((faction) => faction.Id, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var faction in factions)
|
||||||
|
{
|
||||||
|
var commander = new CommanderRuntime
|
||||||
|
{
|
||||||
|
Id = $"commander-faction-{faction.Id}",
|
||||||
|
Kind = CommanderKind.Faction,
|
||||||
|
FactionId = faction.Id,
|
||||||
|
ControlledEntityId = faction.Id,
|
||||||
|
PolicySetId = faction.DefaultPolicySetId,
|
||||||
|
Doctrine = "strategic-default",
|
||||||
|
};
|
||||||
|
|
||||||
|
commanders.Add(commander);
|
||||||
|
factionCommanders[faction.Id] = commander;
|
||||||
|
faction.CommanderIds.Add(commander.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var station in stations)
|
||||||
|
{
|
||||||
|
if (!factionCommanders.TryGetValue(station.FactionId, out var parentCommander))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var commander = new CommanderRuntime
|
||||||
|
{
|
||||||
|
Id = $"commander-station-{station.Id}",
|
||||||
|
Kind = CommanderKind.Station,
|
||||||
|
FactionId = station.FactionId,
|
||||||
|
ParentCommanderId = parentCommander.Id,
|
||||||
|
ControlledEntityId = station.Id,
|
||||||
|
PolicySetId = parentCommander.PolicySetId,
|
||||||
|
Doctrine = "station-default",
|
||||||
|
};
|
||||||
|
|
||||||
|
station.CommanderId = commander.Id;
|
||||||
|
station.PolicySetId = parentCommander.PolicySetId;
|
||||||
|
parentCommander.SubordinateCommanderIds.Add(commander.Id);
|
||||||
|
factionsById[station.FactionId].CommanderIds.Add(commander.Id);
|
||||||
|
commanders.Add(commander);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var ship in ships)
|
||||||
|
{
|
||||||
|
if (!factionCommanders.TryGetValue(ship.FactionId, out var parentCommander))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var commander = new CommanderRuntime
|
||||||
|
{
|
||||||
|
Id = $"commander-ship-{ship.Id}",
|
||||||
|
Kind = CommanderKind.Ship,
|
||||||
|
FactionId = ship.FactionId,
|
||||||
|
ParentCommanderId = parentCommander.Id,
|
||||||
|
ControlledEntityId = ship.Id,
|
||||||
|
PolicySetId = parentCommander.PolicySetId,
|
||||||
|
Doctrine = "ship-default",
|
||||||
|
ActiveBehavior = CopyBehavior(ship.DefaultBehavior),
|
||||||
|
ActiveTask = CopyTask(ship.ControllerTask, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ship.Order is not null)
|
||||||
|
{
|
||||||
|
commander.ActiveOrder = CopyOrder(ship.Order);
|
||||||
|
}
|
||||||
|
|
||||||
|
ship.CommanderId = commander.Id;
|
||||||
|
ship.PolicySetId = parentCommander.PolicySetId;
|
||||||
|
parentCommander.SubordinateCommanderIds.Add(commander.Id);
|
||||||
|
factionsById[ship.FactionId].CommanderIds.Add(commander.Id);
|
||||||
|
commanders.Add(commander);
|
||||||
|
}
|
||||||
|
|
||||||
|
return commanders;
|
||||||
|
}
|
||||||
|
|
||||||
private static string ToFactionLabel(string factionId)
|
private static string ToFactionLabel(string factionId)
|
||||||
{
|
{
|
||||||
return string.Join(" ",
|
return string.Join(" ",
|
||||||
@@ -880,26 +1399,6 @@ public sealed class ScenarioLoader
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector3 ResolveStationPosition(SystemRuntime system, InitialStationDefinition plan, BalanceDefinition balance)
|
|
||||||
{
|
|
||||||
if (plan.Position is { Length: 3 })
|
|
||||||
{
|
|
||||||
return NormalizeScenarioPoint(system, plan.Position);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plan.PlanetIndex is int planetIndex && planetIndex >= 0 && planetIndex < system.Definition.Planets.Count)
|
|
||||||
{
|
|
||||||
var planet = system.Definition.Planets[planetIndex];
|
|
||||||
var side = plan.LagrangeSide ?? 1;
|
|
||||||
return new Vector3(
|
|
||||||
planet.OrbitRadius + (side * 72f),
|
|
||||||
balance.YPlane,
|
|
||||||
(planetIndex + 1) * 42f * side);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Vector3(180f, balance.YPlane, 0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector3 ToVector(float[] values) => new(values[0], values[1], values[2]);
|
private static Vector3 ToVector(float[] values) => new(values[0], values[1], values[2]);
|
||||||
|
|
||||||
private static Vector3 NormalizeScenarioPoint(SystemRuntime system, float[] values)
|
private static Vector3 NormalizeScenarioPoint(SystemRuntime system, float[] values)
|
||||||
@@ -925,4 +1424,73 @@ public sealed class ScenarioLoader
|
|||||||
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
modules.All((moduleId) => definition.Modules.Contains(moduleId, StringComparer.Ordinal));
|
||||||
|
|
||||||
private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
|
private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
|
||||||
|
|
||||||
|
private static int CountModules(IEnumerable<string> modules, string moduleId) =>
|
||||||
|
modules.Count((candidate) => string.Equals(candidate, moduleId, StringComparison.Ordinal));
|
||||||
|
|
||||||
|
private 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CommanderBehaviorRuntime CopyBehavior(DefaultBehaviorRuntime behavior) => new()
|
||||||
|
{
|
||||||
|
Kind = behavior.Kind,
|
||||||
|
AreaSystemId = behavior.AreaSystemId,
|
||||||
|
ModuleId = behavior.ModuleId,
|
||||||
|
NodeId = behavior.NodeId,
|
||||||
|
Phase = behavior.Phase,
|
||||||
|
PatrolIndex = behavior.PatrolIndex,
|
||||||
|
StationId = behavior.StationId,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static CommanderOrderRuntime CopyOrder(ShipOrderRuntime order) => new()
|
||||||
|
{
|
||||||
|
Kind = order.Kind,
|
||||||
|
Status = order.Status,
|
||||||
|
DestinationSystemId = order.DestinationSystemId,
|
||||||
|
DestinationPosition = order.DestinationPosition,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static CommanderTaskRuntime CopyTask(ControllerTaskRuntime task, string? targetNodeId) => new()
|
||||||
|
{
|
||||||
|
Kind = task.Kind,
|
||||||
|
Status = task.Status,
|
||||||
|
TargetEntityId = task.TargetEntityId,
|
||||||
|
TargetNodeId = targetNodeId ?? task.TargetNodeId,
|
||||||
|
TargetPosition = task.TargetPosition,
|
||||||
|
TargetSystemId = task.TargetSystemId,
|
||||||
|
Threshold = task.Threshold,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Vector3 Scale(Vector3 vector, float scale) => new(vector.X * scale, vector.Y * scale, vector.Z * scale);
|
||||||
|
|
||||||
|
private static float DegreesToRadians(float degrees) => degrees * (MathF.PI / 180f);
|
||||||
|
|
||||||
|
private static Vector3 NormalizeOrFallback(Vector3 vector, Vector3 fallback)
|
||||||
|
{
|
||||||
|
var length = MathF.Sqrt(vector.LengthSquared());
|
||||||
|
if (length <= 0.0001f)
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vector.Divide(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record SystemSpatialGraph(
|
||||||
|
string SystemId,
|
||||||
|
List<NodeRuntime> Nodes,
|
||||||
|
List<LocalBubbleRuntime> Bubbles,
|
||||||
|
Dictionary<int, Dictionary<string, NodeRuntime>> LagrangeNodesByPlanetIndex);
|
||||||
|
|
||||||
|
private sealed record LagrangePointPlacement(string Designation, Vector3 Position);
|
||||||
|
|
||||||
|
private sealed record StationPlacement(NodeRuntime AnchorNode, Vector3 Position);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
|||||||
private readonly object _sync = new();
|
private readonly object _sync = new();
|
||||||
private readonly ScenarioLoader _loader = new(environment.ContentRootPath);
|
private readonly ScenarioLoader _loader = new(environment.ContentRootPath);
|
||||||
private readonly SimulationEngine _engine = new();
|
private readonly SimulationEngine _engine = new();
|
||||||
private readonly Dictionary<Guid, Channel<WorldDelta>> _subscribers = [];
|
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
|
||||||
private readonly Queue<WorldDelta> _history = [];
|
private readonly Queue<WorldDelta> _history = [];
|
||||||
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath).Load();
|
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath).Load();
|
||||||
private long _sequence;
|
private long _sequence;
|
||||||
@@ -31,7 +31,7 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChannelReader<WorldDelta> Subscribe(long afterSequence, CancellationToken cancellationToken)
|
public ChannelReader<WorldDelta> Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
|
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
|
||||||
{
|
{
|
||||||
@@ -43,11 +43,15 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
|||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
subscriberId = Guid.NewGuid();
|
subscriberId = Guid.NewGuid();
|
||||||
_subscribers.Add(subscriberId, channel);
|
_subscribers.Add(subscriberId, new SubscriptionState(scope, channel));
|
||||||
|
|
||||||
foreach (var delta in _history.Where((candidate) => candidate.Sequence > afterSequence))
|
foreach (var delta in _history.Where((candidate) => candidate.Sequence > afterSequence))
|
||||||
{
|
{
|
||||||
channel.Writer.TryWrite(delta);
|
var filtered = FilterDeltaForScope(delta, scope);
|
||||||
|
if (HasMeaningfulDelta(filtered))
|
||||||
|
{
|
||||||
|
channel.Writer.TryWrite(filtered);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +78,11 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
|||||||
|
|
||||||
foreach (var subscriber in _subscribers.Values.ToList())
|
foreach (var subscriber in _subscribers.Values.ToList())
|
||||||
{
|
{
|
||||||
subscriber.Writer.TryWrite(delta);
|
var filtered = FilterDeltaForScope(delta, subscriber.Scope);
|
||||||
|
if (HasMeaningfulDelta(filtered))
|
||||||
|
{
|
||||||
|
subscriber.Channel.Writer.TryWrite(filtered);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +100,13 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
|||||||
_world.TickIntervalMs,
|
_world.TickIntervalMs,
|
||||||
DateTimeOffset.UtcNow,
|
DateTimeOffset.UtcNow,
|
||||||
true,
|
true,
|
||||||
[new SimulationEventRecord("world", "world", "reset", "World reset requested", DateTimeOffset.UtcNow)],
|
[new SimulationEventRecord("world", "world", "reset", "World reset requested", DateTimeOffset.UtcNow, "world", "universe", "world")],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
@@ -101,7 +115,7 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
|||||||
_history.Enqueue(resetDelta);
|
_history.Enqueue(resetDelta);
|
||||||
foreach (var subscriber in _subscribers.Values.ToList())
|
foreach (var subscriber in _subscribers.Values.ToList())
|
||||||
{
|
{
|
||||||
subscriber.Writer.TryWrite(resetDelta);
|
subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(resetDelta, subscriber.Scope));
|
||||||
}
|
}
|
||||||
|
|
||||||
return _engine.BuildSnapshot(_world, _sequence);
|
return _engine.BuildSnapshot(_world, _sequence);
|
||||||
@@ -111,8 +125,14 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
|||||||
private static bool HasMeaningfulDelta(WorldDelta delta) =>
|
private static bool HasMeaningfulDelta(WorldDelta delta) =>
|
||||||
delta.RequiresSnapshotRefresh
|
delta.RequiresSnapshotRefresh
|
||||||
|| delta.Events.Count > 0
|
|| delta.Events.Count > 0
|
||||||
|
|| delta.SpatialNodes.Count > 0
|
||||||
|
|| delta.LocalBubbles.Count > 0
|
||||||
|| delta.Nodes.Count > 0
|
|| delta.Nodes.Count > 0
|
||||||
|| delta.Stations.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.Ships.Count > 0
|
||||||
|| delta.Factions.Count > 0;
|
|| delta.Factions.Count > 0;
|
||||||
|
|
||||||
@@ -120,12 +140,134 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
|||||||
{
|
{
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
if (!_subscribers.Remove(subscriberId, out var channel))
|
if (!_subscribers.Remove(subscriberId, out var subscription))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.Writer.TryComplete();
|
subscription.Channel.Writer.TryComplete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WorldDelta FilterDeltaForScope(WorldDelta delta, ObserverScope scope)
|
||||||
|
{
|
||||||
|
if (string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return delta with
|
||||||
|
{
|
||||||
|
Events = delta.Events.Select((evt) => EnrichEventScope(evt)).ToList(),
|
||||||
|
Scope = scope,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemFilter = scope.SystemId;
|
||||||
|
if (string.Equals(scope.ScopeKind, "local-bubble", StringComparison.OrdinalIgnoreCase) && systemFilter is null && scope.BubbleId is not null)
|
||||||
|
{
|
||||||
|
systemFilter = ResolveBubbleSystemId(scope.BubbleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return delta with
|
||||||
|
{
|
||||||
|
Events = delta.Events
|
||||||
|
.Select((evt) => EnrichEventScope(evt))
|
||||||
|
.Where((evt) => IsEventVisibleToScope(evt, scope, systemFilter))
|
||||||
|
.ToList(),
|
||||||
|
SpatialNodes = delta.SpatialNodes.Where((node) => systemFilter is null || node.SystemId == systemFilter).ToList(),
|
||||||
|
LocalBubbles = delta.LocalBubbles.Where((bubble) => systemFilter is null || bubble.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 : [],
|
||||||
|
Scope = scope,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimulationEventRecord EnrichEventScope(SimulationEventRecord evt)
|
||||||
|
{
|
||||||
|
if (!string.Equals(evt.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) || evt.ScopeEntityId is not null)
|
||||||
|
{
|
||||||
|
return evt;
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
"spatial-node" => WithEntityScope(evt, "system", _world.SpatialNodes.FirstOrDefault((node) => node.Id == evt.EntityId)?.SystemId),
|
||||||
|
"local-bubble" => WithEntityScope(evt, "local-bubble", _world.LocalBubbles.FirstOrDefault((bubble) => bubble.Id == evt.EntityId)?.Id),
|
||||||
|
"claim" => WithEntityScope(evt, "system", _world.Claims.FirstOrDefault((claim) => claim.Id == evt.EntityId)?.SystemId),
|
||||||
|
"construction-site" => WithEntityScope(evt, "system", _world.ConstructionSites.FirstOrDefault((site) => site.Id == evt.EntityId)?.SystemId),
|
||||||
|
"market-order" => WithEntityScope(evt, "system", ResolveMarketOrderSystemId(evt.EntityId)),
|
||||||
|
_ => evt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SimulationEventRecord WithEntityScope(SimulationEventRecord evt, string scopeKind, string? scopeEntityId) =>
|
||||||
|
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? ResolveBubbleSystemId(string bubbleId) =>
|
||||||
|
_world.LocalBubbles.FirstOrDefault((bubble) => bubble.Id == bubbleId)?.SystemId;
|
||||||
|
|
||||||
|
private string? ResolveMarketOrderSystemId(string orderId)
|
||||||
|
{
|
||||||
|
var order = _world.MarketOrders.FirstOrDefault((candidate) => candidate.Id == orderId);
|
||||||
|
if (order?.StationId is not null)
|
||||||
|
{
|
||||||
|
return _world.Stations.FirstOrDefault((station) => station.Id == order.StationId)?.SystemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order?.ConstructionSiteId is not null)
|
||||||
|
{
|
||||||
|
return _world.ConstructionSites.FirstOrDefault((site) => site.Id == order.ConstructionSiteId)?.SystemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsOrderVisibleToScope(MarketOrderDelta order, string? systemFilter)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEventVisibleToScope(SimulationEventRecord evt, ObserverScope scope, string? systemFilter)
|
||||||
|
{
|
||||||
|
return scope.ScopeKind switch
|
||||||
|
{
|
||||||
|
"universe" => true,
|
||||||
|
"system" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
|
||||||
|
"local-bubble" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record SubscriptionState(ObserverScope Scope, Channel<WorldDelta> Channel);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,11 @@
|
|||||||
import type { WorldDelta, WorldSnapshot } from "./contracts";
|
import type { WorldDelta, WorldSnapshot } from "./contracts";
|
||||||
|
|
||||||
|
export interface WorldStreamScope {
|
||||||
|
scopeKind?: string;
|
||||||
|
systemId?: string | null;
|
||||||
|
bubbleId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchWorldSnapshot(signal?: AbortSignal) {
|
export async function fetchWorldSnapshot(signal?: AbortSignal) {
|
||||||
const response = await fetch("/api/world", { signal });
|
const response = await fetch("/api/world", { signal });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -15,8 +21,22 @@ export function openWorldStream(
|
|||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
onError?: () => void;
|
onError?: () => void;
|
||||||
},
|
},
|
||||||
|
scope?: WorldStreamScope,
|
||||||
) {
|
) {
|
||||||
const stream = new EventSource(`/api/world/stream?afterSequence=${afterSequence}`);
|
const query = new URLSearchParams({
|
||||||
|
afterSequence: String(afterSequence),
|
||||||
|
});
|
||||||
|
if (scope?.scopeKind) {
|
||||||
|
query.set("scopeKind", scope.scopeKind);
|
||||||
|
}
|
||||||
|
if (scope?.systemId) {
|
||||||
|
query.set("systemId", scope.systemId);
|
||||||
|
}
|
||||||
|
if (scope?.bubbleId) {
|
||||||
|
query.set("bubbleId", scope.bubbleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = new EventSource(`/api/world/stream?${query.toString()}`);
|
||||||
stream.addEventListener("open", () => handlers.onOpen?.());
|
stream.addEventListener("open", () => handlers.onOpen?.());
|
||||||
stream.addEventListener("error", () => handlers.onError?.());
|
stream.addEventListener("error", () => handlers.onError?.());
|
||||||
stream.addEventListener("world-delta", (event) => {
|
stream.addEventListener("world-delta", (event) => {
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ export interface WorldSnapshot {
|
|||||||
tickIntervalMs: number;
|
tickIntervalMs: number;
|
||||||
generatedAtUtc: string;
|
generatedAtUtc: string;
|
||||||
systems: SystemSnapshot[];
|
systems: SystemSnapshot[];
|
||||||
|
spatialNodes: SpatialNodeSnapshot[];
|
||||||
|
localBubbles: LocalBubbleSnapshot[];
|
||||||
nodes: ResourceNodeSnapshot[];
|
nodes: ResourceNodeSnapshot[];
|
||||||
stations: StationSnapshot[];
|
stations: StationSnapshot[];
|
||||||
|
claims: ClaimSnapshot[];
|
||||||
|
constructionSites: ConstructionSiteSnapshot[];
|
||||||
|
marketOrders: MarketOrderSnapshot[];
|
||||||
|
policies: PolicySetSnapshot[];
|
||||||
ships: ShipSnapshot[];
|
ships: ShipSnapshot[];
|
||||||
factions: FactionSnapshot[];
|
factions: FactionSnapshot[];
|
||||||
}
|
}
|
||||||
@@ -17,10 +23,17 @@ export interface WorldDelta {
|
|||||||
generatedAtUtc: string;
|
generatedAtUtc: string;
|
||||||
requiresSnapshotRefresh: boolean;
|
requiresSnapshotRefresh: boolean;
|
||||||
events: SimulationEventRecord[];
|
events: SimulationEventRecord[];
|
||||||
|
spatialNodes: SpatialNodeDelta[];
|
||||||
|
localBubbles: LocalBubbleDelta[];
|
||||||
nodes: ResourceNodeDelta[];
|
nodes: ResourceNodeDelta[];
|
||||||
stations: StationDelta[];
|
stations: StationDelta[];
|
||||||
|
claims: ClaimDelta[];
|
||||||
|
constructionSites: ConstructionSiteDelta[];
|
||||||
|
marketOrders: MarketOrderDelta[];
|
||||||
|
policies: PolicySetDelta[];
|
||||||
ships: ShipDelta[];
|
ships: ShipDelta[];
|
||||||
factions: FactionDelta[];
|
factions: FactionDelta[];
|
||||||
|
scope?: ObserverScope | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SimulationEventRecord {
|
export interface SimulationEventRecord {
|
||||||
@@ -29,6 +42,16 @@ export interface SimulationEventRecord {
|
|||||||
kind: string;
|
kind: string;
|
||||||
message: string;
|
message: string;
|
||||||
occurredAtUtc: string;
|
occurredAtUtc: string;
|
||||||
|
family?: string;
|
||||||
|
scopeKind?: string;
|
||||||
|
scopeEntityId?: string | null;
|
||||||
|
visibility?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObserverScope {
|
||||||
|
scopeKind: string;
|
||||||
|
systemId?: string | null;
|
||||||
|
bubbleId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Vector3Dto {
|
export interface Vector3Dto {
|
||||||
@@ -77,6 +100,32 @@ export interface ResourceNodeSnapshot {
|
|||||||
|
|
||||||
export interface ResourceNodeDelta extends ResourceNodeSnapshot {}
|
export interface ResourceNodeDelta extends ResourceNodeSnapshot {}
|
||||||
|
|
||||||
|
export interface SpatialNodeSnapshot {
|
||||||
|
id: string;
|
||||||
|
systemId: string;
|
||||||
|
kind: string;
|
||||||
|
localPosition: Vector3Dto;
|
||||||
|
bubbleId: string;
|
||||||
|
parentNodeId?: string | null;
|
||||||
|
occupyingStructureId?: string | null;
|
||||||
|
orbitReferenceId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpatialNodeDelta extends SpatialNodeSnapshot {}
|
||||||
|
|
||||||
|
export interface LocalBubbleSnapshot {
|
||||||
|
id: string;
|
||||||
|
nodeId: string;
|
||||||
|
systemId: string;
|
||||||
|
radius: number;
|
||||||
|
occupantShipIds: string[];
|
||||||
|
occupantStationIds: string[];
|
||||||
|
occupantClaimIds: string[];
|
||||||
|
occupantConstructionSiteIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalBubbleDelta extends LocalBubbleSnapshot {}
|
||||||
|
|
||||||
export interface InventoryEntry {
|
export interface InventoryEntry {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -88,16 +137,92 @@ export interface StationSnapshot {
|
|||||||
category: string;
|
category: string;
|
||||||
systemId: string;
|
systemId: string;
|
||||||
localPosition: Vector3Dto;
|
localPosition: Vector3Dto;
|
||||||
|
nodeId?: string | null;
|
||||||
|
bubbleId?: string | null;
|
||||||
|
anchorNodeId?: string | null;
|
||||||
color: string;
|
color: string;
|
||||||
dockedShips: number;
|
dockedShips: number;
|
||||||
dockingPads: number;
|
dockingPads: number;
|
||||||
energyStored: number;
|
energyStored: number;
|
||||||
inventory: InventoryEntry[];
|
inventory: InventoryEntry[];
|
||||||
factionId: string;
|
factionId: string;
|
||||||
|
commanderId?: string | null;
|
||||||
|
policySetId?: string | null;
|
||||||
|
population: number;
|
||||||
|
populationCapacity: number;
|
||||||
|
workforceRequired: number;
|
||||||
|
workforceEffectiveRatio: number;
|
||||||
|
installedModules: string[];
|
||||||
|
marketOrderIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StationDelta extends StationSnapshot {}
|
export interface StationDelta extends StationSnapshot {}
|
||||||
|
|
||||||
|
export interface ClaimSnapshot {
|
||||||
|
id: string;
|
||||||
|
factionId: string;
|
||||||
|
systemId: string;
|
||||||
|
nodeId: string;
|
||||||
|
bubbleId: string;
|
||||||
|
state: string;
|
||||||
|
health: number;
|
||||||
|
placedAtUtc: string;
|
||||||
|
activatesAtUtc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaimDelta extends ClaimSnapshot {}
|
||||||
|
|
||||||
|
export interface ConstructionSiteSnapshot {
|
||||||
|
id: string;
|
||||||
|
factionId: string;
|
||||||
|
systemId: string;
|
||||||
|
nodeId: string;
|
||||||
|
bubbleId: string;
|
||||||
|
targetKind: string;
|
||||||
|
targetDefinitionId: string;
|
||||||
|
blueprintId?: string | null;
|
||||||
|
claimId?: string | null;
|
||||||
|
stationId?: string | null;
|
||||||
|
state: string;
|
||||||
|
progress: number;
|
||||||
|
inventory: InventoryEntry[];
|
||||||
|
requiredItems: InventoryEntry[];
|
||||||
|
deliveredItems: InventoryEntry[];
|
||||||
|
assignedConstructorShipIds: string[];
|
||||||
|
marketOrderIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConstructionSiteDelta extends ConstructionSiteSnapshot {}
|
||||||
|
|
||||||
|
export interface MarketOrderSnapshot {
|
||||||
|
id: string;
|
||||||
|
factionId: string;
|
||||||
|
stationId?: string | null;
|
||||||
|
constructionSiteId?: string | null;
|
||||||
|
kind: string;
|
||||||
|
itemId: string;
|
||||||
|
amount: number;
|
||||||
|
remainingAmount: number;
|
||||||
|
valuation: number;
|
||||||
|
reserveThreshold?: number | null;
|
||||||
|
policySetId?: string | null;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketOrderDelta extends MarketOrderSnapshot {}
|
||||||
|
|
||||||
|
export interface PolicySetSnapshot {
|
||||||
|
id: string;
|
||||||
|
ownerKind: string;
|
||||||
|
ownerId: string;
|
||||||
|
tradeAccessPolicy: string;
|
||||||
|
dockingAccessPolicy: string;
|
||||||
|
constructionAccessPolicy: string;
|
||||||
|
operationalRangePolicy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PolicySetDelta extends PolicySetSnapshot {}
|
||||||
|
|
||||||
export interface ShipSnapshot {
|
export interface ShipSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -111,25 +236,55 @@ export interface ShipSnapshot {
|
|||||||
orderKind: string | null;
|
orderKind: string | null;
|
||||||
defaultBehaviorKind: string;
|
defaultBehaviorKind: string;
|
||||||
controllerTaskKind: string;
|
controllerTaskKind: string;
|
||||||
|
nodeId?: string | null;
|
||||||
|
bubbleId?: string | null;
|
||||||
|
dockedStationId?: string | null;
|
||||||
|
commanderId?: string | null;
|
||||||
|
policySetId?: string | null;
|
||||||
cargoCapacity: number;
|
cargoCapacity: number;
|
||||||
|
workerPopulation: number;
|
||||||
energyStored: number;
|
energyStored: number;
|
||||||
inventory: InventoryEntry[];
|
inventory: InventoryEntry[];
|
||||||
factionId: string;
|
factionId: string;
|
||||||
health: number;
|
health: number;
|
||||||
history: string[];
|
history: string[];
|
||||||
|
spatialState: ShipSpatialStateSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShipDelta extends ShipSnapshot {}
|
export interface ShipDelta extends ShipSnapshot {}
|
||||||
|
|
||||||
|
export interface ShipSpatialStateSnapshot {
|
||||||
|
spaceLayer: string;
|
||||||
|
currentSystemId: string;
|
||||||
|
currentNodeId?: string | null;
|
||||||
|
currentBubbleId?: string | null;
|
||||||
|
localPosition?: Vector3Dto | null;
|
||||||
|
systemPosition?: Vector3Dto | null;
|
||||||
|
movementRegime: string;
|
||||||
|
destinationNodeId?: string | null;
|
||||||
|
transit?: ShipTransitSnapshot | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShipTransitSnapshot {
|
||||||
|
regime: string;
|
||||||
|
originNodeId?: string | null;
|
||||||
|
destinationNodeId?: string | null;
|
||||||
|
startedAtUtc?: string | null;
|
||||||
|
arrivalDueAtUtc?: string | null;
|
||||||
|
progress: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FactionSnapshot {
|
export interface FactionSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
color: string;
|
color: string;
|
||||||
credits: number;
|
credits: number;
|
||||||
|
populationTotal: number;
|
||||||
oreMined: number;
|
oreMined: number;
|
||||||
goodsProduced: number;
|
goodsProduced: number;
|
||||||
shipsBuilt: number;
|
shipsBuilt: number;
|
||||||
shipsLost: number;
|
shipsLost: number;
|
||||||
|
defaultPolicySetId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FactionDelta extends FactionSnapshot {}
|
export interface FactionDelta extends FactionSnapshot {}
|
||||||
|
|||||||
Reference in New Issue
Block a user