Implement roadmap phases 1 through 8

This commit is contained in:
2026-03-14 02:30:15 -04:00
parent 86b8f4a73f
commit ddca4a16d5
10 changed files with 3862 additions and 198 deletions

View File

@@ -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);

View File

@@ -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; }

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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 {}