Implement roadmap phases 1 through 8
This commit is contained in:
@@ -7,8 +7,14 @@ public sealed record WorldSnapshot(
|
||||
int TickIntervalMs,
|
||||
DateTimeOffset GeneratedAtUtc,
|
||||
IReadOnlyList<SystemSnapshot> Systems,
|
||||
IReadOnlyList<SpatialNodeSnapshot> SpatialNodes,
|
||||
IReadOnlyList<LocalBubbleSnapshot> LocalBubbles,
|
||||
IReadOnlyList<ResourceNodeSnapshot> Nodes,
|
||||
IReadOnlyList<StationSnapshot> Stations,
|
||||
IReadOnlyList<ClaimSnapshot> Claims,
|
||||
IReadOnlyList<ConstructionSiteSnapshot> ConstructionSites,
|
||||
IReadOnlyList<MarketOrderSnapshot> MarketOrders,
|
||||
IReadOnlyList<PolicySetSnapshot> Policies,
|
||||
IReadOnlyList<ShipSnapshot> Ships,
|
||||
IReadOnlyList<FactionSnapshot> Factions);
|
||||
|
||||
@@ -18,17 +24,33 @@ public sealed record WorldDelta(
|
||||
DateTimeOffset GeneratedAtUtc,
|
||||
bool RequiresSnapshotRefresh,
|
||||
IReadOnlyList<SimulationEventRecord> Events,
|
||||
IReadOnlyList<SpatialNodeDelta> SpatialNodes,
|
||||
IReadOnlyList<LocalBubbleDelta> LocalBubbles,
|
||||
IReadOnlyList<ResourceNodeDelta> Nodes,
|
||||
IReadOnlyList<StationDelta> Stations,
|
||||
IReadOnlyList<ClaimDelta> Claims,
|
||||
IReadOnlyList<ConstructionSiteDelta> ConstructionSites,
|
||||
IReadOnlyList<MarketOrderDelta> MarketOrders,
|
||||
IReadOnlyList<PolicySetDelta> Policies,
|
||||
IReadOnlyList<ShipDelta> Ships,
|
||||
IReadOnlyList<FactionDelta> Factions);
|
||||
IReadOnlyList<FactionDelta> Factions,
|
||||
ObserverScope? Scope = null);
|
||||
|
||||
public sealed record SimulationEventRecord(
|
||||
string EntityKind,
|
||||
string EntityId,
|
||||
string Kind,
|
||||
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(
|
||||
string Id,
|
||||
@@ -65,6 +87,46 @@ public sealed record ResourceNodeSnapshot(
|
||||
float MaxOre,
|
||||
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(
|
||||
string Id,
|
||||
string SystemId,
|
||||
@@ -84,12 +146,23 @@ public sealed record StationSnapshot(
|
||||
string Category,
|
||||
string SystemId,
|
||||
Vector3Dto LocalPosition,
|
||||
string? NodeId,
|
||||
string? BubbleId,
|
||||
string? AnchorNodeId,
|
||||
string Color,
|
||||
int DockedShips,
|
||||
int DockingPads,
|
||||
float EnergyStored,
|
||||
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(
|
||||
string Id,
|
||||
@@ -97,12 +170,129 @@ public sealed record StationDelta(
|
||||
string Category,
|
||||
string SystemId,
|
||||
Vector3Dto LocalPosition,
|
||||
string? NodeId,
|
||||
string? BubbleId,
|
||||
string? AnchorNodeId,
|
||||
string Color,
|
||||
int DockedShips,
|
||||
int DockingPads,
|
||||
float EnergyStored,
|
||||
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(
|
||||
string Id,
|
||||
@@ -117,12 +307,19 @@ public sealed record ShipSnapshot(
|
||||
string? OrderKind,
|
||||
string DefaultBehaviorKind,
|
||||
string ControllerTaskKind,
|
||||
string? NodeId,
|
||||
string? BubbleId,
|
||||
string? DockedStationId,
|
||||
string? CommanderId,
|
||||
string? PolicySetId,
|
||||
float CargoCapacity,
|
||||
float WorkerPopulation,
|
||||
float EnergyStored,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
string FactionId,
|
||||
float Health,
|
||||
IReadOnlyList<string> History);
|
||||
IReadOnlyList<string> History,
|
||||
ShipSpatialStateSnapshot SpatialState);
|
||||
|
||||
public sealed record ShipDelta(
|
||||
string Id,
|
||||
@@ -137,31 +334,61 @@ public sealed record ShipDelta(
|
||||
string? OrderKind,
|
||||
string DefaultBehaviorKind,
|
||||
string ControllerTaskKind,
|
||||
string? NodeId,
|
||||
string? BubbleId,
|
||||
string? DockedStationId,
|
||||
string? CommanderId,
|
||||
string? PolicySetId,
|
||||
float CargoCapacity,
|
||||
float WorkerPopulation,
|
||||
float EnergyStored,
|
||||
IReadOnlyList<InventoryEntry> Inventory,
|
||||
string FactionId,
|
||||
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(
|
||||
string Id,
|
||||
string Label,
|
||||
string Color,
|
||||
float Credits,
|
||||
float PopulationTotal,
|
||||
float OreMined,
|
||||
float GoodsProduced,
|
||||
int ShipsBuilt,
|
||||
int ShipsLost);
|
||||
int ShipsLost,
|
||||
string? DefaultPolicySetId);
|
||||
|
||||
public sealed record FactionDelta(
|
||||
string Id,
|
||||
string Label,
|
||||
string Color,
|
||||
float Credits,
|
||||
float PopulationTotal,
|
||||
float OreMined,
|
||||
float GoodsProduced,
|
||||
int ShipsBuilt,
|
||||
int ShipsLost);
|
||||
int ShipsLost,
|
||||
string? DefaultPolicySetId);
|
||||
|
||||
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 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 required string Label { get; set; }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using SpaceGame.Simulation.Api.Contracts;
|
||||
using SpaceGame.Simulation.Api.Simulation;
|
||||
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();
|
||||
_ = 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.Body.FlushAsync(cancellationToken);
|
||||
|
||||
@@ -9,12 +9,20 @@ public sealed class SimulationWorld
|
||||
public required BalanceDefinition Balance { get; init; }
|
||||
public required List<SystemRuntime> Systems { 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<ShipRuntime> Ships { 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, ItemDefinition> ItemDefinitions { 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 DateTimeOffset GeneratedAtUtc { get; set; }
|
||||
}
|
||||
@@ -37,18 +45,55 @@ public sealed class ResourceNodeRuntime
|
||||
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 required string Id { get; init; }
|
||||
public required string SystemId { 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 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 Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<int, string> DockingPadAssignments { get; } = new();
|
||||
public HashSet<string> MarketOrderIds { get; } = new(StringComparer.Ordinal);
|
||||
public float EnergyStored { 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 ModuleConstructionRuntime? ActiveConstruction { get; set; }
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
@@ -70,6 +115,7 @@ public sealed class ShipRuntime
|
||||
public required string FactionId { get; init; }
|
||||
public required Vector3 Position { get; set; }
|
||||
public required Vector3 TargetPosition { get; set; }
|
||||
public required ShipSpatialStateRuntime SpatialState { get; set; }
|
||||
public Vector3 Velocity { get; set; } = Vector3.Zero;
|
||||
public string State { get; set; } = "idle";
|
||||
public ShipOrderRuntime? Order { get; set; }
|
||||
@@ -77,9 +123,12 @@ public sealed class ShipRuntime
|
||||
public required ControllerTaskRuntime ControllerTask { get; set; }
|
||||
public float ActionTimer { get; set; }
|
||||
public Dictionary<string, float> Inventory { get; } = new(StringComparer.Ordinal);
|
||||
public float WorkerPopulation { get; set; }
|
||||
public float EnergyStored { get; set; }
|
||||
public string? DockedStationId { get; set; }
|
||||
public int? AssignedDockingPadIndex { get; set; }
|
||||
public string? CommanderId { get; set; }
|
||||
public string? PolicySetId { get; set; }
|
||||
public float Health { get; set; }
|
||||
public List<string> History { get; } = [];
|
||||
public string LastSignature { get; set; } = string.Empty;
|
||||
@@ -92,10 +141,128 @@ public sealed class FactionRuntime
|
||||
public required string Label { get; init; }
|
||||
public required string Color { get; init; }
|
||||
public float Credits { get; set; }
|
||||
public float PopulationTotal { get; set; }
|
||||
public float OreMined { get; set; }
|
||||
public float GoodsProduced { get; set; }
|
||||
public int ShipsBuilt { get; set; }
|
||||
public int ShipsLost { get; set; }
|
||||
public HashSet<string> CommanderIds { get; } = new(StringComparer.Ordinal);
|
||||
public string? DefaultPolicySetId { get; set; }
|
||||
public 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;
|
||||
}
|
||||
|
||||
@@ -123,12 +290,131 @@ public sealed class DefaultBehaviorRuntime
|
||||
public sealed class ControllerTaskRuntime
|
||||
{
|
||||
public required string Kind { get; set; }
|
||||
public string Status { get; set; } = "pending";
|
||||
public string? CommanderId { get; set; }
|
||||
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 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 static Vector3 Zero => new(0f, 0f, 0f);
|
||||
|
||||
@@ -13,6 +13,11 @@ public sealed class ScenarioLoader
|
||||
private const float MinimumRefineryStock = 0f;
|
||||
private const float MinimumShipyardStock = 0f;
|
||||
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 =
|
||||
[
|
||||
"Aquila Verge",
|
||||
@@ -88,12 +93,14 @@ public sealed class ScenarioLoader
|
||||
var ships = Read<List<ShipDefinition>>("ships.json");
|
||||
var constructibles = Read<List<ConstructibleDefinition>>("constructibles.json");
|
||||
var items = Read<List<ItemDefinition>>("items.json");
|
||||
var recipes = Read<List<RecipeDefinition>>("recipes.json");
|
||||
var moduleRecipes = Read<List<ModuleRecipeDefinition>>("module-recipes.json");
|
||||
var balance = Read<BalanceDefinition>("balance.json");
|
||||
|
||||
var shipDefinitions = ships.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 recipeDefinitions = recipes.ToDictionary((definition) => definition.Id, StringComparer.Ordinal);
|
||||
var moduleRecipeDefinitions = moduleRecipes.ToDictionary((definition) => definition.ModuleId, StringComparer.Ordinal);
|
||||
var systemRuntimes = systems
|
||||
.Select((definition) => new SystemRuntime
|
||||
@@ -103,14 +110,26 @@ public sealed class ScenarioLoader
|
||||
})
|
||||
.ToList();
|
||||
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 spatialNodes = new List<NodeRuntime>();
|
||||
var localBubbles = new List<LocalBubbleRuntime>();
|
||||
var nodeIdCounter = 0;
|
||||
foreach (var graph in systemGraphs.Values)
|
||||
{
|
||||
spatialNodes.AddRange(graph.Nodes);
|
||||
localBubbles.AddRange(graph.Bubbles);
|
||||
}
|
||||
|
||||
foreach (var system in systemRuntimes)
|
||||
{
|
||||
foreach (var node in system.Definition.ResourceNodes)
|
||||
{
|
||||
nodes.Add(new ResourceNodeRuntime
|
||||
var resourceNode = new ResourceNodeRuntime
|
||||
{
|
||||
Id = $"node-{++nodeIdCounter}",
|
||||
SystemId = system.Definition.Id,
|
||||
@@ -122,6 +141,24 @@ public sealed class ScenarioLoader
|
||||
ItemId = node.ItemId,
|
||||
OreRemaining = 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;
|
||||
}
|
||||
|
||||
stations.Add(new StationRuntime
|
||||
var placement = ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], spatialNodes);
|
||||
var station = new StationRuntime
|
||||
{
|
||||
Id = $"station-{++stationIdCounter}",
|
||||
SystemId = system.Definition.Id,
|
||||
Definition = definition,
|
||||
Position = ResolveStationPosition(system, plan, balance),
|
||||
Position = placement.Position,
|
||||
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)
|
||||
{
|
||||
@@ -152,8 +216,13 @@ public sealed class ScenarioLoader
|
||||
|
||||
foreach (var station in stations)
|
||||
{
|
||||
InitializeStationPopulation(station);
|
||||
station.Inventory["fuel"] = 240f;
|
||||
station.Inventory["refined-metals"] = 120f;
|
||||
if (station.Population > 0f)
|
||||
{
|
||||
station.Inventory["water"] = MathF.Max(80f, station.Population * 1.5f);
|
||||
}
|
||||
}
|
||||
|
||||
var refinery = stations.FirstOrDefault((station) =>
|
||||
@@ -187,8 +256,9 @@ public sealed class ScenarioLoader
|
||||
FactionId = formation.FactionId ?? DefaultFactionId,
|
||||
Position = position,
|
||||
TargetPosition = position,
|
||||
SpatialState = CreateInitialShipSpatialState(formation.SystemId, position, spatialNodes),
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -209,6 +279,11 @@ public sealed class ScenarioLoader
|
||||
|
||||
var factions = CreateFactions(stations, shipsRuntime);
|
||||
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
|
||||
{
|
||||
@@ -217,12 +292,20 @@ public sealed class ScenarioLoader
|
||||
Balance = balance,
|
||||
Systems = systemRuntimes,
|
||||
Nodes = nodes,
|
||||
SpatialNodes = spatialNodes,
|
||||
LocalBubbles = localBubbles,
|
||||
Stations = stations,
|
||||
Ships = shipsRuntime,
|
||||
Factions = factions,
|
||||
Commanders = commanders,
|
||||
Claims = claims,
|
||||
ConstructionSites = constructionSites,
|
||||
MarketOrders = marketOrders,
|
||||
Policies = policies,
|
||||
ShipDefinitions = shipDefinitions,
|
||||
ItemDefinitions = itemDefinitions,
|
||||
ModuleRecipes = moduleRecipeDefinitions,
|
||||
Recipes = recipeDefinitions,
|
||||
GeneratedAtUtc = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
@@ -810,6 +893,442 @@ public sealed class ScenarioLoader
|
||||
private static float GetInventoryAmount(IReadOnlyDictionary<string, float> inventory, string itemId) =>
|
||||
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)
|
||||
{
|
||||
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 NormalizeScenarioPoint(SystemRuntime system, float[] values)
|
||||
@@ -925,4 +1424,73 @@ public sealed class ScenarioLoader
|
||||
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 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 ScenarioLoader _loader = new(environment.ContentRootPath);
|
||||
private readonly SimulationEngine _engine = new();
|
||||
private readonly Dictionary<Guid, Channel<WorldDelta>> _subscribers = [];
|
||||
private readonly Dictionary<Guid, SubscriptionState> _subscribers = [];
|
||||
private readonly Queue<WorldDelta> _history = [];
|
||||
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath).Load();
|
||||
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
|
||||
{
|
||||
@@ -43,11 +43,15 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
||||
lock (_sync)
|
||||
{
|
||||
subscriberId = Guid.NewGuid();
|
||||
_subscribers.Add(subscriberId, channel);
|
||||
_subscribers.Add(subscriberId, new SubscriptionState(scope, channel));
|
||||
|
||||
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())
|
||||
{
|
||||
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,
|
||||
DateTimeOffset.UtcNow,
|
||||
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);
|
||||
foreach (var subscriber in _subscribers.Values.ToList())
|
||||
{
|
||||
subscriber.Writer.TryWrite(resetDelta);
|
||||
subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(resetDelta, subscriber.Scope));
|
||||
}
|
||||
|
||||
return _engine.BuildSnapshot(_world, _sequence);
|
||||
@@ -111,8 +125,14 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
||||
private static bool HasMeaningfulDelta(WorldDelta delta) =>
|
||||
delta.RequiresSnapshotRefresh
|
||||
|| delta.Events.Count > 0
|
||||
|| delta.SpatialNodes.Count > 0
|
||||
|| delta.LocalBubbles.Count > 0
|
||||
|| delta.Nodes.Count > 0
|
||||
|| delta.Stations.Count > 0
|
||||
|| delta.Claims.Count > 0
|
||||
|| delta.ConstructionSites.Count > 0
|
||||
|| delta.MarketOrders.Count > 0
|
||||
|| delta.Policies.Count > 0
|
||||
|| delta.Ships.Count > 0
|
||||
|| delta.Factions.Count > 0;
|
||||
|
||||
@@ -120,12 +140,134 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_subscribers.Remove(subscriberId, out var channel))
|
||||
if (!_subscribers.Remove(subscriberId, out var subscription))
|
||||
{
|
||||
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";
|
||||
|
||||
export interface WorldStreamScope {
|
||||
scopeKind?: string;
|
||||
systemId?: string | null;
|
||||
bubbleId?: string | null;
|
||||
}
|
||||
|
||||
export async function fetchWorldSnapshot(signal?: AbortSignal) {
|
||||
const response = await fetch("/api/world", { signal });
|
||||
if (!response.ok) {
|
||||
@@ -15,8 +21,22 @@ export function openWorldStream(
|
||||
onOpen?: () => 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("error", () => handlers.onError?.());
|
||||
stream.addEventListener("world-delta", (event) => {
|
||||
|
||||
@@ -5,8 +5,14 @@ export interface WorldSnapshot {
|
||||
tickIntervalMs: number;
|
||||
generatedAtUtc: string;
|
||||
systems: SystemSnapshot[];
|
||||
spatialNodes: SpatialNodeSnapshot[];
|
||||
localBubbles: LocalBubbleSnapshot[];
|
||||
nodes: ResourceNodeSnapshot[];
|
||||
stations: StationSnapshot[];
|
||||
claims: ClaimSnapshot[];
|
||||
constructionSites: ConstructionSiteSnapshot[];
|
||||
marketOrders: MarketOrderSnapshot[];
|
||||
policies: PolicySetSnapshot[];
|
||||
ships: ShipSnapshot[];
|
||||
factions: FactionSnapshot[];
|
||||
}
|
||||
@@ -17,10 +23,17 @@ export interface WorldDelta {
|
||||
generatedAtUtc: string;
|
||||
requiresSnapshotRefresh: boolean;
|
||||
events: SimulationEventRecord[];
|
||||
spatialNodes: SpatialNodeDelta[];
|
||||
localBubbles: LocalBubbleDelta[];
|
||||
nodes: ResourceNodeDelta[];
|
||||
stations: StationDelta[];
|
||||
claims: ClaimDelta[];
|
||||
constructionSites: ConstructionSiteDelta[];
|
||||
marketOrders: MarketOrderDelta[];
|
||||
policies: PolicySetDelta[];
|
||||
ships: ShipDelta[];
|
||||
factions: FactionDelta[];
|
||||
scope?: ObserverScope | null;
|
||||
}
|
||||
|
||||
export interface SimulationEventRecord {
|
||||
@@ -29,6 +42,16 @@ export interface SimulationEventRecord {
|
||||
kind: string;
|
||||
message: 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 {
|
||||
@@ -77,6 +100,32 @@ export interface 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 {
|
||||
itemId: string;
|
||||
amount: number;
|
||||
@@ -88,16 +137,92 @@ export interface StationSnapshot {
|
||||
category: string;
|
||||
systemId: string;
|
||||
localPosition: Vector3Dto;
|
||||
nodeId?: string | null;
|
||||
bubbleId?: string | null;
|
||||
anchorNodeId?: string | null;
|
||||
color: string;
|
||||
dockedShips: number;
|
||||
dockingPads: number;
|
||||
energyStored: number;
|
||||
inventory: InventoryEntry[];
|
||||
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 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 {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -111,25 +236,55 @@ export interface ShipSnapshot {
|
||||
orderKind: string | null;
|
||||
defaultBehaviorKind: string;
|
||||
controllerTaskKind: string;
|
||||
nodeId?: string | null;
|
||||
bubbleId?: string | null;
|
||||
dockedStationId?: string | null;
|
||||
commanderId?: string | null;
|
||||
policySetId?: string | null;
|
||||
cargoCapacity: number;
|
||||
workerPopulation: number;
|
||||
energyStored: number;
|
||||
inventory: InventoryEntry[];
|
||||
factionId: string;
|
||||
health: number;
|
||||
history: string[];
|
||||
spatialState: ShipSpatialStateSnapshot;
|
||||
}
|
||||
|
||||
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 {
|
||||
id: string;
|
||||
label: string;
|
||||
color: string;
|
||||
credits: number;
|
||||
populationTotal: number;
|
||||
oreMined: number;
|
||||
goodsProduced: number;
|
||||
shipsBuilt: number;
|
||||
shipsLost: number;
|
||||
defaultPolicySetId?: string | null;
|
||||
}
|
||||
|
||||
export interface FactionDelta extends FactionSnapshot {}
|
||||
|
||||
Reference in New Issue
Block a user