feat: simplified local-space and celestial, removed bubbles

This commit is contained in:
2026-03-18 08:49:35 -04:00
parent 00a008bda5
commit 933c6afd08
19 changed files with 246 additions and 448 deletions

View File

@@ -5,8 +5,7 @@ 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 required string CelestialId { get; init; }
public string? CommanderId { get; set; }
public DateTimeOffset PlacedAtUtc { get; init; }
public DateTimeOffset ActivatesAtUtc { get; set; }
@@ -20,8 +19,7 @@ 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 CelestialId { get; init; }
public required string TargetKind { get; init; }
public required string TargetDefinitionId { get; init; }
public string? BlueprintId { get; set; }

View File

@@ -6,8 +6,6 @@ public enum SpatialNodeKind
Planet,
Moon,
LagrangePoint,
Station,
ResourceSite,
}
public enum WorkStatus
@@ -112,7 +110,7 @@ public static class ShipTaskKinds
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 DefendCelestial = "defend-celestial";
public const string Retreat = "retreat";
public const string HoldPosition = "hold-position";
}
@@ -167,8 +165,6 @@ public static class SimulationEnumMappings
SpatialNodeKind.Planet => "planet",
SpatialNodeKind.Moon => "moon",
SpatialNodeKind.LagrangePoint => "lagrange-point",
SpatialNodeKind.Station => "station",
SpatialNodeKind.ResourceSite => "resource-site",
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null),
};

View File

@@ -9,8 +9,7 @@ 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<CelestialRuntime> Celestials { get; init; }
public required List<StationRuntime> Stations { get; init; }
public required List<ShipRuntime> Ships { get; init; }
public required List<FactionRuntime> Factions { get; init; }

View File

@@ -15,7 +15,7 @@ public sealed class ResourceNodeRuntime
public required Vector3 Position { get; set; }
public required string SourceKind { get; init; }
public required string ItemId { get; init; }
public string? AnchorNodeId { get; set; }
public string? CelestialId { get; set; }
public float OrbitRadius { get; init; }
public float OrbitPhase { get; init; }
public float OrbitInclination { get; init; }
@@ -24,38 +24,24 @@ public sealed class ResourceNodeRuntime
public string LastDeltaSignature { get; set; } = string.Empty;
}
public sealed class NodeRuntime
public sealed class CelestialRuntime
{
public required string Id { get; init; }
public required string SystemId { get; init; }
public required SpatialNodeKind Kind { get; init; }
public required Vector3 Position { get; set; }
public required string BubbleId { get; init; }
public float LocalSpaceRadius { 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 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 string? CurrentCelestialId { get; set; }
public Vector3? LocalPosition { get; set; }
public Vector3? SystemPosition { get; set; }
public string MovementRegime { get; set; } = MovementRegimeKinds.LocalFlight;

View File

@@ -10,9 +10,7 @@ public sealed class StationRuntime
public required Vector3 Position { get; set; }
public float Radius { get; set; } = 24f;
public required string FactionId { get; init; }
public string? NodeId { get; set; }
public string? BubbleId { get; set; }
public string? AnchorNodeId { get; set; }
public string? CelestialId { get; set; }
public string? CommanderId { get; set; }
public string? PolicySetId { get; set; }
public List<StationModuleRuntime> Modules { get; } = [];

View File

@@ -88,27 +88,26 @@ public sealed partial class ScenarioLoader
private static List<ClaimRuntime> CreateClaims(
IReadOnlyCollection<StationRuntime> stations,
IReadOnlyCollection<NodeRuntime> nodes,
IReadOnlyCollection<CelestialRuntime> celestials,
DateTimeOffset nowUtc)
{
var stationsByAnchorNodeId = stations
.Where((station) => station.AnchorNodeId is not null)
.ToDictionary((station) => station.AnchorNodeId!, StringComparer.Ordinal);
var stationsByCelestialId = stations
.Where((station) => station.CelestialId is not null)
.ToDictionary((station) => station.CelestialId!, StringComparer.Ordinal);
var claims = new List<ClaimRuntime>();
foreach (var node in nodes.Where((candidate) => candidate.Kind == SpatialNodeKind.LagrangePoint))
foreach (var celestial in celestials.Where((c) => c.Kind == SpatialNodeKind.LagrangePoint))
{
if (!stationsByAnchorNodeId.TryGetValue(node.Id, out var station))
if (!stationsByCelestialId.TryGetValue(celestial.Id, out var station))
{
continue;
}
claims.Add(new ClaimRuntime
{
Id = $"claim-{node.Id}",
Id = $"claim-{celestial.Id}",
FactionId = station.FactionId,
SystemId = node.SystemId,
NodeId = node.Id,
BubbleId = node.BubbleId,
SystemId = celestial.SystemId,
CelestialId = celestial.Id,
PlacedAtUtc = nowUtc,
ActivatesAtUtc = nowUtc.AddSeconds(8),
State = ClaimStateKinds.Activating,
@@ -122,7 +121,6 @@ public sealed partial class ScenarioLoader
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>();
@@ -131,18 +129,12 @@ public sealed partial class ScenarioLoader
foreach (var station in stations)
{
var moduleId = GetNextConstructionSiteModule(station, moduleRecipes);
if (moduleId is null || station.AnchorNodeId is null)
if (moduleId is null || station.CelestialId is null)
{
continue;
}
var anchorNode = nodes.FirstOrDefault((node) => node.Id == station.AnchorNodeId);
if (anchorNode is null)
{
continue;
}
var claim = claims.FirstOrDefault((candidate) => candidate.NodeId == anchorNode.Id);
var claim = claims.FirstOrDefault((candidate) => candidate.CelestialId == station.CelestialId);
if (claim is null || !moduleRecipes.TryGetValue(moduleId, out var recipe))
{
continue;
@@ -153,8 +145,7 @@ public sealed partial class ScenarioLoader
Id = $"site-{station.Id}",
FactionId = station.FactionId,
SystemId = station.SystemId,
NodeId = anchorNode.Id,
BubbleId = anchorNode.BubbleId,
CelestialId = station.CelestialId,
TargetKind = "station-module",
TargetDefinitionId = "station",
BlueprintId = moduleId,

View File

@@ -6,48 +6,44 @@ public sealed partial class ScenarioLoader
{
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 celestials = new List<CelestialRuntime>();
var lagrangeNodesByPlanetIndex = new Dictionary<int, Dictionary<string, CelestialRuntime>>();
var starNode = AddSpatialNode(
nodes,
bubbles,
AddCelestial(
celestials,
id: $"node-{system.Definition.Id}-star",
systemId: system.Definition.Id,
kind: SpatialNodeKind.Star,
position: Vector3.Zero,
radius: MathF.Max(system.Definition.GravityWellRadius + StarBubbleRadiusPadding, 180f));
localSpaceRadius: MathF.Max(system.Definition.GravityWellRadius + StarBubbleRadiusPadding, LocalSpaceRadius));
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,
var planetCelestial = AddCelestial(
celestials,
id: planetNodeId,
systemId: system.Definition.Id,
kind: SpatialNodeKind.Planet,
position: planetPosition,
radius: MathF.Max(planet.Size + PlanetBubbleRadiusPadding, 120f),
parentNodeId: starNode.Id);
localSpaceRadius: LocalSpaceRadius,
parentNodeId: $"node-{system.Definition.Id}-star");
var lagrangeNodes = new Dictionary<string, NodeRuntime>(StringComparer.Ordinal);
var lagrangeNodes = new Dictionary<string, CelestialRuntime>(StringComparer.Ordinal);
foreach (var point in EnumeratePlanetLagrangePoints(planetPosition, planet))
{
var lagrangeNode = AddSpatialNode(
nodes,
bubbles,
var lagrangeCelestial = AddCelestial(
celestials,
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{point.Designation.ToLowerInvariant()}",
systemId: system.Definition.Id,
kind: SpatialNodeKind.LagrangePoint,
position: point.Position,
radius: LagrangeBubbleRadius,
parentNodeId: planetNode.Id,
localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id,
orbitReferenceId: point.Designation);
lagrangeNodes[point.Designation] = lagrangeNode;
lagrangeNodes[point.Designation] = lagrangeCelestial;
}
lagrangeNodesByPlanetIndex[planetIndex] = lagrangeNodes;
@@ -61,54 +57,44 @@ public sealed partial class ScenarioLoader
for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1)
{
var moonPosition = ComputeMoonPosition(planetPosition, moonOrbitRadius, moonIndex, planetIndex);
AddSpatialNode(
nodes,
bubbles,
AddCelestial(
celestials,
id: $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}",
systemId: system.Definition.Id,
kind: SpatialNodeKind.Moon,
position: moonPosition,
radius: MoonBubbleRadiusPadding + 24f,
parentNodeId: planetNode.Id);
localSpaceRadius: LocalSpaceRadius,
parentNodeId: planetCelestial.Id);
moonOrbitRadius += 30f;
}
}
return new SystemSpatialGraph(system.Definition.Id, nodes, bubbles, lagrangeNodesByPlanetIndex);
return new SystemSpatialGraph(system.Definition.Id, celestials, lagrangeNodesByPlanetIndex);
}
private static NodeRuntime AddSpatialNode(
ICollection<NodeRuntime> nodes,
ICollection<LocalBubbleRuntime> bubbles,
private static CelestialRuntime AddCelestial(
ICollection<CelestialRuntime> celestials,
string id,
string systemId,
SpatialNodeKind kind,
Vector3 position,
float radius,
float localSpaceRadius,
string? parentNodeId = null,
string? orbitReferenceId = null)
{
var bubbleId = $"bubble-{id}";
var node = new NodeRuntime
var celestial = new CelestialRuntime
{
Id = id,
SystemId = systemId,
Kind = kind,
Position = position,
BubbleId = bubbleId,
LocalSpaceRadius = localSpaceRadius,
ParentNodeId = parentNodeId,
OrbitReferenceId = orbitReferenceId,
};
nodes.Add(node);
bubbles.Add(new LocalBubbleRuntime
{
Id = bubbleId,
NodeId = id,
SystemId = systemId,
Radius = radius,
});
return node;
celestials.Add(celestial);
return celestial;
}
private static IEnumerable<LagrangePointPlacement> EnumeratePlanetLagrangePoints(
@@ -165,36 +151,36 @@ public sealed partial class ScenarioLoader
InitialStationDefinition plan,
SystemRuntime system,
SystemSpatialGraph graph,
IReadOnlyCollection<NodeRuntime> existingNodes)
IReadOnlyCollection<CelestialRuntime> existingCelestials)
{
if (plan.PlanetIndex is int planetIndex &&
graph.LagrangeNodesByPlanetIndex.TryGetValue(planetIndex, out var lagrangeNodes))
{
var designation = ResolveLagrangeDesignation(plan.LagrangeSide);
if (lagrangeNodes.TryGetValue(designation, out var lagrangeNode))
if (lagrangeNodes.TryGetValue(designation, out var lagrangeCelestial))
{
return new StationPlacement(lagrangeNode, lagrangeNode.Position);
return new StationPlacement(lagrangeCelestial, lagrangeCelestial.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 == SpatialNodeKind.LagrangePoint)
.OrderBy((node) => node.Position.DistanceTo(targetPosition))
var preferredCelestial = existingCelestials
.Where((c) => c.SystemId == system.Definition.Id && c.Kind == SpatialNodeKind.LagrangePoint)
.OrderBy((c) => c.Position.DistanceTo(targetPosition))
.FirstOrDefault()
?? existingNodes
.Where((node) => node.SystemId == system.Definition.Id)
.OrderBy((node) => node.Position.DistanceTo(targetPosition))
?? existingCelestials
.Where((c) => c.SystemId == system.Definition.Id)
.OrderBy((c) => c.Position.DistanceTo(targetPosition))
.First();
return new StationPlacement(preferredNode, preferredNode.Position);
return new StationPlacement(preferredCelestial, preferredCelestial.Position);
}
var fallbackNode = graph.Nodes
.FirstOrDefault((node) => node.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(node.OccupyingStructureId))
?? graph.Nodes.First((node) => node.Kind == SpatialNodeKind.Planet);
return new StationPlacement(fallbackNode, fallbackNode.Position);
var fallbackCelestial = graph.Celestials
.FirstOrDefault((c) => c.Kind == SpatialNodeKind.LagrangePoint && string.IsNullOrEmpty(c.OccupyingStructureId))
?? graph.Celestials.First((c) => c.Kind == SpatialNodeKind.Planet);
return new StationPlacement(fallbackCelestial, fallbackCelestial.Position);
}
private static string ResolveLagrangeDesignation(int? lagrangeSide) => lagrangeSide switch
@@ -204,7 +190,7 @@ public sealed partial class ScenarioLoader
_ => "L1",
};
private static NodeRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
private static CelestialRuntime? ResolveResourceNodeAnchor(SystemSpatialGraph graph, ResourceNodeDefinition definition)
{
if (definition.AnchorPlanetIndex is not int planetIndex || planetIndex < 0)
{
@@ -214,14 +200,14 @@ public sealed partial class ScenarioLoader
if (definition.AnchorMoonIndex is int moonIndex && moonIndex >= 0)
{
var moonNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
return graph.Nodes.FirstOrDefault((node) => node.Id == moonNodeId);
return graph.Celestials.FirstOrDefault((c) => c.Id == moonNodeId);
}
var planetNodeId = $"node-{graph.SystemId}-planet-{planetIndex + 1}";
return graph.Nodes.FirstOrDefault((node) => node.Id == planetNodeId);
return graph.Celestials.FirstOrDefault((c) => c.Id == planetNodeId);
}
private static Vector3 ComputeResourceNodePosition(NodeRuntime? anchorNode, ResourceNodeDefinition definition, float yPlane)
private static Vector3 ComputeResourceNodePosition(CelestialRuntime? anchorCelestial, ResourceNodeDefinition definition, float yPlane)
{
var verticalOffset = MathF.Sin(DegreesToRadians(definition.InclinationDegrees)) * MathF.Min(definition.RadiusOffset * 0.04f, 25000f);
var offset = new Vector3(
@@ -229,12 +215,12 @@ public sealed partial class ScenarioLoader
verticalOffset,
MathF.Sin(definition.Angle) * definition.RadiusOffset);
if (anchorNode is null)
if (anchorCelestial is null)
{
return new Vector3(offset.X, yPlane + offset.Y, offset.Z);
}
return Add(anchorNode.Position, offset);
return Add(anchorCelestial.Position, offset);
}
private static Vector3 ComputePlanetPosition(PlanetDefinition planet)
@@ -252,19 +238,18 @@ public sealed partial class ScenarioLoader
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)
private static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<CelestialRuntime> celestials)
{
var nearestNode = nodes
.Where((node) => node.SystemId == systemId)
.OrderBy((node) => node.Position.DistanceTo(position))
var nearestCelestial = celestials
.Where((c) => c.SystemId == systemId)
.OrderBy((c) => c.Position.DistanceTo(position))
.FirstOrDefault();
return new ShipSpatialStateRuntime
{
CurrentSystemId = systemId,
SpaceLayer = SpaceLayerKinds.LocalSpace,
CurrentNodeId = nearestNode?.Id,
CurrentBubbleId = nearestNode?.BubbleId,
CurrentCelestialId = nearestCelestial?.Id,
LocalPosition = position,
SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight,
@@ -273,11 +258,10 @@ public sealed partial class ScenarioLoader
private sealed record SystemSpatialGraph(
string SystemId,
List<NodeRuntime> Nodes,
List<LocalBubbleRuntime> Bubbles,
Dictionary<int, Dictionary<string, NodeRuntime>> LagrangeNodesByPlanetIndex);
List<CelestialRuntime> Celestials,
Dictionary<int, Dictionary<string, CelestialRuntime>> LagrangeNodesByPlanetIndex);
private sealed record LagrangePointPlacement(string Designation, Vector3 Position);
private sealed record StationPlacement(NodeRuntime AnchorNode, Vector3 Position);
private sealed record StationPlacement(CelestialRuntime AnchorCelestial, Vector3 Position);
}

View File

@@ -13,10 +13,7 @@ public sealed partial class ScenarioLoader
private const float MinimumShipyardStock = 0f;
private const float MinimumSystemSeparation = 3.2f;
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 const float LocalSpaceRadius = 10_000f;
private static readonly string[] GeneratedSystemNames =
[
"Aquila Verge",
@@ -121,14 +118,12 @@ public sealed partial class ScenarioLoader
(system) => BuildSystemSpatialGraph(system),
StringComparer.Ordinal);
var celestials = new List<CelestialRuntime>();
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);
celestials.AddRange(graph.Celestials);
}
foreach (var system in systemRuntimes)
@@ -136,15 +131,15 @@ public sealed partial class ScenarioLoader
var systemGraph = systemGraphs[system.Definition.Id];
foreach (var node in system.Definition.ResourceNodes)
{
var anchorNode = ResolveResourceNodeAnchor(systemGraph, node);
var anchorCelestial = ResolveResourceNodeAnchor(systemGraph, node);
var resourceNode = new ResourceNodeRuntime
{
Id = $"node-{++nodeIdCounter}",
SystemId = system.Definition.Id,
Position = ComputeResourceNodePosition(anchorNode, node, balance.YPlane),
Position = ComputeResourceNodePosition(anchorCelestial, node, balance.YPlane),
SourceKind = node.SourceKind,
ItemId = node.ItemId,
AnchorNodeId = anchorNode?.Id,
CelestialId = anchorCelestial?.Id,
OrbitRadius = node.RadiusOffset,
OrbitPhase = node.Angle,
OrbitInclination = DegreesToRadians(node.InclinationDegrees),
@@ -153,23 +148,6 @@ public sealed partial class ScenarioLoader
};
nodes.Add(resourceNode);
var bubbleId = $"bubble-{resourceNode.Id}";
spatialNodes.Add(new NodeRuntime
{
Id = resourceNode.Id,
SystemId = resourceNode.SystemId,
Kind = SpatialNodeKind.ResourceSite,
Position = resourceNode.Position,
BubbleId = bubbleId,
ParentNodeId = anchorNode?.Id,
});
localBubbles.Add(new LocalBubbleRuntime
{
Id = bubbleId,
NodeId = resourceNode.Id,
SystemId = resourceNode.SystemId,
Radius = ResourceBubbleRadius,
});
}
}
@@ -182,7 +160,7 @@ public sealed partial class ScenarioLoader
continue;
}
var placement = ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], spatialNodes);
var placement = ResolveStationPlacement(plan, system, systemGraphs[system.Definition.Id], celestials);
var station = new StationRuntime
{
Id = $"station-{++stationIdCounter}",
@@ -193,31 +171,9 @@ public sealed partial class ScenarioLoader
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;
station.CelestialId = placement.AnchorCelestial.Id;
stations.Add(station);
spatialNodes.Add(new NodeRuntime
{
Id = stationNodeId,
SystemId = station.SystemId,
Kind = SpatialNodeKind.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, GetStationRadius(moduleDefinitions, station) + 60f),
});
localBubbles[^1].OccupantStationIds.Add(station.Id);
placement.AnchorNode.OccupyingStructureId = station.Id;
placement.AnchorCelestial.OccupyingStructureId = station.Id;
var startingModules = plan.StartingModules.Count > 0
? plan.StartingModules
@@ -274,7 +230,7 @@ public sealed partial class ScenarioLoader
FactionId = formation.FactionId ?? DefaultFactionId,
Position = position,
TargetPosition = position,
SpatialState = CreateInitialShipSpatialState(formation.SystemId, position, spatialNodes),
SpatialState = CreateInitialShipSpatialState(formation.SystemId, position, celestials),
DefaultBehavior = CreateBehavior(definition, formation.SystemId, scenario, patrolRoutes, refinery),
ControllerTask = new ControllerTaskRuntime { Kind = ControllerTaskKind.Idle, Threshold = balance.ArrivalThreshold, Status = WorkStatus.Pending },
Health = definition.MaxHealth,
@@ -287,8 +243,8 @@ public sealed partial class ScenarioLoader
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);
var claims = CreateClaims(stations, celestials, now);
var (constructionSites, marketOrders) = CreateConstructionSites(stations, claims, moduleRecipeDefinitions);
return new SimulationWorld
{
@@ -296,9 +252,8 @@ public sealed partial class ScenarioLoader
Seed = WorldSeed,
Balance = balance,
Systems = systemRuntimes,
Celestials = celestials,
Nodes = nodes,
SpatialNodes = spatialNodes,
LocalBubbles = localBubbles,
Stations = stations,
Ships = shipsRuntime,
Factions = factions,

View File

@@ -51,8 +51,9 @@ public sealed partial class SimulationEngine
return "none";
}
var targetPosition = task.TargetPosition.Value;
var targetNode = ResolveTravelTargetNode(world, task, targetPosition);
// Resolve live position each frame — entities like stations orbit celestials and move every tick
var targetPosition = ResolveCurrentTargetPosition(world, task);
var targetCelestial = ResolveTravelTargetCelestial(world, task, targetPosition);
ship.TargetPosition = targetPosition;
if (ship.SystemId != task.TargetSystemId)
@@ -63,63 +64,83 @@ public sealed partial class SimulationEngine
return "none";
}
var destinationEntryNode = ResolveSystemEntryNode(world, task.TargetSystemId);
var destinationEntryPosition = destinationEntryNode?.Position ?? Vector3.Zero;
return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryNode);
var destinationEntryCelestial = ResolveSystemEntryCelestial(world, task.TargetSystemId);
var destinationEntryPosition = destinationEntryCelestial?.Position ?? Vector3.Zero;
return UpdateFtlTransit(ship, world, deltaSeconds, task.TargetSystemId, destinationEntryPosition, destinationEntryCelestial);
}
var currentNode = ResolveCurrentNode(world, ship);
if (targetNode is not null && currentNode is not null && !string.Equals(currentNode.Id, targetNode.Id, StringComparison.Ordinal))
var currentCelestial = ResolveCurrentCelestial(world, ship);
if (targetCelestial is not null && currentCelestial is not null && !string.Equals(currentCelestial.Id, targetCelestial.Id, StringComparison.Ordinal))
{
if (!HasShipCapabilities(ship.Definition, "warp"))
{
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetNode, task.Threshold);
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetCelestial, task.Threshold);
}
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetNode);
return UpdateWarpTransit(ship, world, deltaSeconds, targetPosition, targetCelestial);
}
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetNode, task.Threshold);
return UpdateLocalTravel(ship, world, deltaSeconds, task.TargetSystemId, targetPosition, targetCelestial, task.Threshold);
}
private static NodeRuntime? ResolveTravelTargetNode(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition)
private static Vector3 ResolveCurrentTargetPosition(SimulationWorld world, ControllerTaskRuntime task)
{
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
{
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (station?.NodeId is not null)
if (station is not null)
{
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == station.NodeId);
return station.Position;
}
var node = world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (node is not null)
var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (celestial is not null)
{
return node;
return celestial.Position;
}
}
return world.SpatialNodes
return task.TargetPosition!.Value;
}
private static CelestialRuntime? ResolveTravelTargetCelestial(SimulationWorld world, ControllerTaskRuntime task, Vector3 targetPosition)
{
if (!string.IsNullOrWhiteSpace(task.TargetEntityId))
{
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (station?.CelestialId is not null)
{
return world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId);
}
var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == task.TargetEntityId);
if (celestial is not null)
{
return celestial;
}
}
return world.Celestials
.Where(candidate => task.TargetSystemId is null || candidate.SystemId == task.TargetSystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(targetPosition))
.FirstOrDefault();
}
private static NodeRuntime? ResolveCurrentNode(SimulationWorld world, ShipRuntime ship)
private static CelestialRuntime? ResolveCurrentCelestial(SimulationWorld world, ShipRuntime ship)
{
if (ship.SpatialState.CurrentNodeId is not null)
if (ship.SpatialState.CurrentCelestialId is not null)
{
return world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentNodeId);
return world.Celestials.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentCelestialId);
}
return world.SpatialNodes
return world.Celestials
.Where(candidate => candidate.SystemId == ship.SystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
.FirstOrDefault();
}
private static NodeRuntime? ResolveSystemEntryNode(SimulationWorld world, string systemId) =>
world.SpatialNodes.FirstOrDefault(candidate =>
private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) =>
world.Celestials.FirstOrDefault(candidate =>
candidate.SystemId == systemId &&
candidate.Kind == SpatialNodeKind.Star);
@@ -129,14 +150,14 @@ public sealed partial class SimulationEngine
float deltaSeconds,
string targetSystemId,
Vector3 targetPosition,
NodeRuntime? targetNode,
CelestialRuntime? targetCelestial,
float threshold)
{
var distance = ship.Position.DistanceTo(targetPosition);
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
ship.SpatialState.Transit = null;
ship.SpatialState.DestinationNodeId = targetNode?.Id;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
if (distance <= threshold)
{
@@ -144,8 +165,7 @@ public sealed partial class SimulationEngine
ship.Position = targetPosition;
ship.TargetPosition = ship.Position;
ship.SystemId = targetSystemId;
ship.SpatialState.CurrentNodeId = targetNode?.Id;
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
ship.State = ShipState.Arriving;
return "arrived";
}
@@ -156,16 +176,16 @@ public sealed partial class SimulationEngine
return "none";
}
private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, NodeRuntime targetNode)
private string UpdateWarpTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, Vector3 targetPosition, CelestialRuntime targetCelestial)
{
var transit = ship.SpatialState.Transit;
if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetNode.Id)
if (transit is null || transit.Regime != MovementRegimeKinds.Warp || transit.DestinationNodeId != targetCelestial.Id)
{
transit = new ShipTransitRuntime
{
Regime = MovementRegimeKinds.Warp,
OriginNodeId = ship.SpatialState.CurrentNodeId,
DestinationNodeId = targetNode.Id,
OriginNodeId = ship.SpatialState.CurrentCelestialId,
DestinationNodeId = targetCelestial.Id,
StartedAtUtc = world.GeneratedAtUtc,
};
ship.SpatialState.Transit = transit;
@@ -173,9 +193,8 @@ public sealed partial class SimulationEngine
ship.SpatialState.SpaceLayer = SpaceLayerKinds.SystemSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.Warp;
ship.SpatialState.CurrentNodeId = null;
ship.SpatialState.CurrentBubbleId = null;
ship.SpatialState.DestinationNodeId = targetNode.Id;
ship.SpatialState.CurrentCelestialId = null;
ship.SpatialState.DestinationNodeId = targetCelestial.Id;
var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f);
if (ship.State != ShipState.Warping)
@@ -196,24 +215,24 @@ public sealed partial class SimulationEngine
var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null
? ship.Position.DistanceTo(targetPosition)
: (world.SpatialNodes.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
: (world.Celestials.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition)));
ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds);
transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance));
return ship.Position.DistanceTo(targetPosition) <= 18f
? CompleteTransitArrival(ship, targetNode.SystemId, targetPosition, targetNode)
? CompleteTransitArrival(ship, targetCelestial.SystemId, targetPosition, targetCelestial)
: "none";
}
private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
private string UpdateFtlTransit(ShipRuntime ship, SimulationWorld world, float deltaSeconds, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
{
var destinationNodeId = targetNode?.Id;
var destinationNodeId = targetCelestial?.Id;
var transit = ship.SpatialState.Transit;
if (transit is null || transit.Regime != MovementRegimeKinds.FtlTransit || transit.DestinationNodeId != destinationNodeId)
{
transit = new ShipTransitRuntime
{
Regime = MovementRegimeKinds.FtlTransit,
OriginNodeId = ship.SpatialState.CurrentNodeId,
OriginNodeId = ship.SpatialState.CurrentCelestialId,
DestinationNodeId = destinationNodeId,
StartedAtUtc = world.GeneratedAtUtc,
};
@@ -222,8 +241,7 @@ public sealed partial class SimulationEngine
ship.SpatialState.SpaceLayer = SpaceLayerKinds.GalaxySpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.FtlTransit;
ship.SpatialState.CurrentNodeId = null;
ship.SpatialState.CurrentBubbleId = null;
ship.SpatialState.CurrentCelestialId = null;
ship.SpatialState.DestinationNodeId = destinationNodeId;
if (ship.State != ShipState.Ftl)
@@ -247,11 +265,11 @@ public sealed partial class SimulationEngine
var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition));
transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * deltaSeconds) / totalDistance));
return transit.Progress >= 0.999f
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetNode)
? CompleteSystemEntryArrival(ship, targetSystemId, targetPosition, targetCelestial)
: "none";
}
private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
private static string CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
{
ship.ActionTimer = 0f;
ship.Position = targetPosition;
@@ -260,14 +278,13 @@ public sealed partial class SimulationEngine
ship.SpatialState.Transit = null;
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
ship.SpatialState.CurrentNodeId = targetNode?.Id;
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
ship.SpatialState.DestinationNodeId = targetNode?.Id;
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
ship.State = ShipState.Arriving;
return "arrived";
}
private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, NodeRuntime? targetNode)
private static string CompleteSystemEntryArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial)
{
ship.ActionTimer = 0f;
ship.Position = targetPosition;
@@ -276,9 +293,8 @@ public sealed partial class SimulationEngine
ship.SpatialState.Transit = null;
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
ship.SpatialState.CurrentNodeId = targetNode?.Id;
ship.SpatialState.CurrentBubbleId = targetNode?.BubbleId;
ship.SpatialState.DestinationNodeId = targetNode?.Id;
ship.SpatialState.CurrentCelestialId = targetCelestial?.Id;
ship.SpatialState.DestinationNodeId = targetCelestial?.Id;
ship.State = ShipState.Arriving;
return "none";
}

View File

@@ -175,12 +175,12 @@ public sealed partial class SimulationEngine
private void UpdateOrbitalState(SimulationWorld world)
{
var worldTimeSeconds = (float)world.OrbitalTimeSeconds;
var spatialNodesById = world.SpatialNodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
var celestialsById = world.Celestials.ToDictionary(c => c.Id, StringComparer.Ordinal);
foreach (var system in world.Systems)
{
var starNodeId = $"node-{system.Definition.Id}-star";
if (spatialNodesById.TryGetValue(starNodeId, out var starNode))
if (celestialsById.TryGetValue(starNodeId, out var starNode))
{
starNode.Position = Vector3.Zero;
}
@@ -189,7 +189,7 @@ public sealed partial class SimulationEngine
{
var planet = system.Definition.Planets[planetIndex];
var planetNodeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}";
if (!spatialNodesById.TryGetValue(planetNodeId, out var planetNode))
if (!celestialsById.TryGetValue(planetNodeId, out var planetNode))
{
continue;
}
@@ -200,7 +200,7 @@ public sealed partial class SimulationEngine
foreach (var lagrange in EnumeratePlanetLagrangePoints(planetPosition, planet))
{
var lagrangeId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-{lagrange.Designation.ToLowerInvariant()}";
if (spatialNodesById.TryGetValue(lagrangeId, out var lagrangeNode))
if (celestialsById.TryGetValue(lagrangeId, out var lagrangeNode))
{
lagrangeNode.Position = lagrange.Position;
}
@@ -209,7 +209,7 @@ public sealed partial class SimulationEngine
for (var moonIndex = 0; moonIndex < planet.MoonCount; moonIndex += 1)
{
var moonId = $"node-{system.Definition.Id}-planet-{planetIndex + 1}-moon-{moonIndex + 1}";
if (!spatialNodesById.TryGetValue(moonId, out var moonNode))
if (!celestialsById.TryGetValue(moonId, out var moonNode))
{
continue;
}
@@ -221,30 +221,22 @@ public sealed partial class SimulationEngine
foreach (var station in world.Stations)
{
if (station.AnchorNodeId is null || !spatialNodesById.TryGetValue(station.AnchorNodeId, out var anchorNode))
if (station.CelestialId is null || !celestialsById.TryGetValue(station.CelestialId, out var anchorCelestial))
{
continue;
}
station.Position = anchorNode.Position;
if (station.NodeId is not null && spatialNodesById.TryGetValue(station.NodeId, out var stationNode))
{
stationNode.Position = station.Position;
}
station.Position = anchorCelestial.Position;
}
foreach (var node in world.Nodes)
{
if (node.AnchorNodeId is null || !spatialNodesById.TryGetValue(node.AnchorNodeId, out var anchorNode))
if (node.CelestialId is null || !celestialsById.TryGetValue(node.CelestialId, out var anchorCelestial))
{
continue;
}
node.Position = Add(anchorNode.Position, ComputeResourceNodeOffset(node, worldTimeSeconds));
if (spatialNodesById.TryGetValue(node.Id, out var resourceNode))
{
resourceNode.Position = node.Position;
}
node.Position = Add(anchorCelestial.Position, ComputeResourceNodeOffset(node, worldTimeSeconds));
}
foreach (var ship in world.Ships.Where(ship => ship.DockedStationId is not null))
@@ -263,11 +255,6 @@ public sealed partial class SimulationEngine
private static void SyncSpatialState(SimulationWorld world)
{
foreach (var bubble in world.LocalBubbles)
{
bubble.OccupantShipIds.Clear();
}
foreach (var ship in world.Ships)
{
ship.SpatialState.CurrentSystemId = ship.SystemId;
@@ -275,25 +262,17 @@ public sealed partial class SimulationEngine
ship.SpatialState.SystemPosition = ship.Position;
if (ship.SpatialState.Transit is not null)
{
ship.SpatialState.CurrentNodeId = null;
ship.SpatialState.CurrentBubbleId = null;
ship.SpatialState.CurrentCelestialId = null;
continue;
}
ship.SpatialState.SpaceLayer = SpaceLayerKinds.LocalSpace;
ship.SpatialState.MovementRegime = MovementRegimeKinds.LocalFlight;
var nearestNode = world.SpatialNodes
var nearestCelestial = world.Celestials
.Where(candidate => candidate.SystemId == ship.SystemId)
.OrderBy(candidate => candidate.Position.DistanceTo(ship.Position))
.FirstOrDefault();
ship.SpatialState.CurrentNodeId = nearestNode?.Id;
ship.SpatialState.CurrentBubbleId = nearestNode?.BubbleId;
if (nearestNode is not null)
{
var nearestBubble = world.LocalBubbles.FirstOrDefault(candidate => candidate.Id == nearestNode.BubbleId);
nearestBubble?.OccupantShipIds.Add(ship.Id);
}
ship.SpatialState.CurrentCelestialId = nearestCelestial?.Id;
if (ship.DockedStationId is null)
{
@@ -301,15 +280,10 @@ public sealed partial class SimulationEngine
}
var station = world.Stations.FirstOrDefault(candidate => candidate.Id == ship.DockedStationId);
if (station?.BubbleId is null)
if (station?.CelestialId is not null)
{
continue;
ship.SpatialState.CurrentCelestialId = station.CelestialId;
}
ship.SpatialState.CurrentNodeId = station.NodeId;
ship.SpatialState.CurrentBubbleId = station.BubbleId;
var bubble = world.LocalBubbles.FirstOrDefault(candidate => candidate.Id == station.BubbleId);
bubble?.OccupantShipIds.Add(ship.Id);
}
}

View File

@@ -39,29 +39,20 @@ public sealed partial class SimulationEngine
planet.Size,
planet.Color,
planet.HasRing)).ToList())).ToList(),
world.SpatialNodes.Select(ToSpatialNodeDelta).Select(node => new SpatialNodeSnapshot(
node.Id,
node.SystemId,
node.Kind,
node.LocalPosition,
node.BubbleId,
node.ParentNodeId,
node.OccupyingStructureId,
node.OrbitReferenceId)).ToList(),
world.LocalBubbles.Select(ToLocalBubbleDelta).Select(bubble => new LocalBubbleSnapshot(
bubble.Id,
bubble.NodeId,
bubble.SystemId,
bubble.Radius,
bubble.OccupantShipIds,
bubble.OccupantStationIds,
bubble.OccupantClaimIds,
bubble.OccupantConstructionSiteIds)).ToList(),
world.Celestials.Select(ToCelestialDelta).Select(c => new CelestialSnapshot(
c.Id,
c.SystemId,
c.Kind,
c.OrbitalAnchor,
c.LocalSpaceRadius,
c.ParentNodeId,
c.OccupyingStructureId,
c.OrbitReferenceId)).ToList(),
world.Nodes.Select(ToNodeDelta).Select(node => new ResourceNodeSnapshot(
node.Id,
node.SystemId,
node.LocalPosition,
node.AnchorNodeId,
node.CelestialId,
node.SourceKind,
node.OreRemaining,
node.MaxOre,
@@ -72,9 +63,7 @@ public sealed partial class SimulationEngine
station.Category,
station.SystemId,
station.LocalPosition,
station.NodeId,
station.BubbleId,
station.AnchorNodeId,
station.CelestialId,
station.Color,
station.DockedShips,
station.DockedShipIds,
@@ -95,8 +84,7 @@ public sealed partial class SimulationEngine
claim.Id,
claim.FactionId,
claim.SystemId,
claim.NodeId,
claim.BubbleId,
claim.CelestialId,
claim.State,
claim.Health,
claim.PlacedAtUtc,
@@ -105,8 +93,7 @@ public sealed partial class SimulationEngine
site.Id,
site.FactionId,
site.SystemId,
site.NodeId,
site.BubbleId,
site.CelestialId,
site.TargetKind,
site.TargetDefinitionId,
site.BlueprintId,
@@ -155,8 +142,7 @@ public sealed partial class SimulationEngine
ship.BehaviorPhase,
ship.ControllerTaskKind,
ship.CommanderObjective,
ship.NodeId,
ship.BubbleId,
ship.CelestialId,
ship.DockedStationId,
ship.CommanderId,
ship.PolicySetId,
@@ -191,14 +177,9 @@ public sealed partial class SimulationEngine
node.LastDeltaSignature = BuildNodeSignature(node);
}
foreach (var node in world.SpatialNodes)
foreach (var celestial in world.Celestials)
{
node.LastDeltaSignature = BuildSpatialNodeSignature(node);
}
foreach (var bubble in world.LocalBubbles)
{
bubble.LastDeltaSignature = BuildLocalBubbleSignature(bubble);
celestial.LastDeltaSignature = BuildCelestialSignature(celestial);
}
foreach (var station in world.Stations)
@@ -255,37 +236,19 @@ public sealed partial class SimulationEngine
return deltas;
}
private static IReadOnlyList<SpatialNodeDelta> BuildSpatialNodeDeltas(SimulationWorld world)
private static IReadOnlyList<CelestialDelta> BuildCelestialDeltas(SimulationWorld world)
{
var deltas = new List<SpatialNodeDelta>();
foreach (var node in world.SpatialNodes)
var deltas = new List<CelestialDelta>();
foreach (var celestial in world.Celestials)
{
var signature = BuildSpatialNodeSignature(node);
if (signature == node.LastDeltaSignature)
var signature = BuildCelestialSignature(celestial);
if (signature == celestial.LastDeltaSignature)
{
continue;
}
node.LastDeltaSignature = signature;
deltas.Add(ToSpatialNodeDelta(node));
}
return deltas;
}
private static IReadOnlyList<LocalBubbleDelta> BuildLocalBubbleDeltas(SimulationWorld world)
{
var deltas = new List<LocalBubbleDelta>();
foreach (var bubble in world.LocalBubbles)
{
var signature = BuildLocalBubbleSignature(bubble);
if (signature == bubble.LastDeltaSignature)
{
continue;
}
bubble.LastDeltaSignature = signature;
deltas.Add(ToLocalBubbleDelta(bubble));
celestial.LastDeltaSignature = signature;
deltas.Add(ToCelestialDelta(celestial));
}
return deltas;
@@ -424,22 +387,17 @@ public sealed partial class SimulationEngine
string.Equals(c.Kind, CommanderKind.Faction, StringComparison.Ordinal));
private static string BuildNodeSignature(ResourceNodeRuntime node) =>
$"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.AnchorNodeId}|{node.OreRemaining:0.###}";
$"{node.SystemId}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.CelestialId}|{node.OreRemaining:0.###}";
private static string BuildSpatialNodeSignature(NodeRuntime node) =>
$"{node.SystemId}|{node.Kind.ToContractValue()}|{node.Position.X:0.###}|{node.Position.Y:0.###}|{node.Position.Z:0.###}|{node.BubbleId}|{node.ParentNodeId}|{node.OccupyingStructureId}|{node.OrbitReferenceId}";
private static string BuildLocalBubbleSignature(LocalBubbleRuntime bubble) =>
$"{bubble.SystemId}|{bubble.NodeId}|{bubble.Radius:0.###}|{string.Join(",", bubble.OccupantShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantStationIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantClaimIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", bubble.OccupantConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal))}";
private static string BuildCelestialSignature(CelestialRuntime celestial) =>
$"{celestial.SystemId}|{celestial.Kind.ToContractValue()}|{celestial.Position.X:0.###}|{celestial.Position.Y:0.###}|{celestial.Position.Z:0.###}|{celestial.LocalSpaceRadius:0.###}|{celestial.ParentNodeId}|{celestial.OccupyingStructureId}|{celestial.OrbitReferenceId}";
private static string BuildStationSignature(SimulationWorld world, StationRuntime station)
{
var processes = ToStationActionProgressSnapshots(world, station);
return string.Join("|",
station.SystemId,
station.NodeId ?? "none",
station.BubbleId ?? "none",
station.AnchorNodeId ?? "none",
station.CelestialId ?? "none",
station.CommanderId ?? "none",
station.PolicySetId ?? "none",
BuildInventorySignature(station.Inventory),
@@ -458,10 +416,10 @@ public sealed partial class SimulationEngine
}
private static string BuildClaimSignature(ClaimRuntime claim) =>
$"{claim.FactionId}|{claim.SystemId}|{claim.NodeId}|{claim.BubbleId}|{claim.State}|{claim.Health:0.###}|{claim.ActivatesAtUtc:O}";
$"{claim.FactionId}|{claim.SystemId}|{claim.CelestialId}|{claim.State}|{claim.Health:0.###}|{claim.ActivatesAtUtc:O}";
private static string BuildConstructionSiteSignature(ConstructionSiteRuntime site) =>
$"{site.FactionId}|{site.SystemId}|{site.NodeId}|{site.BubbleId}|{site.TargetKind}|{site.TargetDefinitionId}|{site.BlueprintId}|{site.ClaimId}|{site.StationId}|{site.State}|{site.Progress:0.###}|{BuildInventorySignature(site.Inventory)}|{BuildInventorySignature(site.RequiredItems)}|{BuildInventorySignature(site.DeliveredItems)}|{string.Join(",", site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal))}";
$"{site.FactionId}|{site.SystemId}|{site.CelestialId}|{site.TargetKind}|{site.TargetDefinitionId}|{site.BlueprintId}|{site.ClaimId}|{site.StationId}|{site.State}|{site.Progress:0.###}|{BuildInventorySignature(site.Inventory)}|{BuildInventorySignature(site.RequiredItems)}|{BuildInventorySignature(site.DeliveredItems)}|{string.Join(",", site.AssignedConstructorShipIds.OrderBy(id => id, StringComparer.Ordinal))}|{string.Join(",", site.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal))}";
private static string BuildMarketOrderSignature(MarketOrderRuntime order) =>
$"{order.FactionId}|{order.StationId}|{order.ConstructionSiteId}|{order.Kind}|{order.ItemId}|{order.Amount:0.###}|{order.RemainingAmount:0.###}|{order.Valuation:0.###}|{order.ReserveThreshold?.ToString("0.###") ?? "none"}|{order.PolicySetId}|{order.State}";
@@ -486,14 +444,12 @@ public sealed partial class SimulationEngine
ship.DefaultBehavior.Kind,
ship.DefaultBehavior.Phase ?? "none",
ship.ControllerTask.Kind.ToContractValue(),
ship.SpatialState.CurrentNodeId ?? "none",
ship.SpatialState.CurrentBubbleId ?? "none",
ship.SpatialState.CurrentCelestialId ?? "none",
ship.DockedStationId ?? "none",
ship.CommanderId ?? "none",
ship.PolicySetId ?? "none",
ship.SpatialState.SpaceLayer,
ship.SpatialState.CurrentNodeId ?? "none",
ship.SpatialState.CurrentBubbleId ?? "none",
ship.SpatialState.CurrentCelestialId ?? "none",
ship.SpatialState.MovementRegime,
ship.SpatialState.DestinationNodeId ?? "none",
ship.SpatialState.Transit?.Regime ?? "none",
@@ -528,31 +484,21 @@ public sealed partial class SimulationEngine
node.Id,
node.SystemId,
ToDto(node.Position),
node.AnchorNodeId,
node.CelestialId,
node.SourceKind,
node.OreRemaining,
node.MaxOre,
node.ItemId);
private static SpatialNodeDelta ToSpatialNodeDelta(NodeRuntime node) => new(
node.Id,
node.SystemId,
node.Kind.ToContractValue(),
ToDto(node.Position),
node.BubbleId,
node.ParentNodeId,
node.OccupyingStructureId,
node.OrbitReferenceId);
private static LocalBubbleDelta ToLocalBubbleDelta(LocalBubbleRuntime bubble) => new(
bubble.Id,
bubble.NodeId,
bubble.SystemId,
bubble.Radius,
bubble.OccupantShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
bubble.OccupantStationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
bubble.OccupantClaimIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
bubble.OccupantConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal).ToList());
private static CelestialDelta ToCelestialDelta(CelestialRuntime celestial) => new(
celestial.Id,
celestial.SystemId,
celestial.Kind.ToContractValue(),
ToDto(celestial.Position),
celestial.LocalSpaceRadius,
celestial.ParentNodeId,
celestial.OccupyingStructureId,
celestial.OrbitReferenceId);
private static StationDelta ToStationDelta(SimulationWorld world, StationRuntime station) => new(
station.Id,
@@ -560,9 +506,7 @@ public sealed partial class SimulationEngine
station.Category,
station.SystemId,
ToDto(station.Position),
station.NodeId,
station.BubbleId,
station.AnchorNodeId,
station.CelestialId,
station.Color,
station.DockedShipIds.Count,
station.DockedShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(),
@@ -615,8 +559,7 @@ public sealed partial class SimulationEngine
claim.Id,
claim.FactionId,
claim.SystemId,
claim.NodeId,
claim.BubbleId,
claim.CelestialId,
claim.State,
claim.Health,
claim.PlacedAtUtc,
@@ -626,8 +569,7 @@ public sealed partial class SimulationEngine
site.Id,
site.FactionId,
site.SystemId,
site.NodeId,
site.BubbleId,
site.CelestialId,
site.TargetKind,
site.TargetDefinitionId,
site.BlueprintId,
@@ -696,8 +638,7 @@ public sealed partial class SimulationEngine
ship.DefaultBehavior.Phase,
ship.ControllerTask.Kind.ToContractValue(),
commander?.ActiveActionName,
ship.SpatialState.CurrentNodeId,
ship.SpatialState.CurrentBubbleId,
ship.SpatialState.CurrentCelestialId,
ship.DockedStationId,
ship.CommanderId,
ship.PolicySetId,
@@ -810,8 +751,7 @@ public sealed partial class SimulationEngine
private static ShipSpatialStateSnapshot ToShipSpatialStateSnapshot(ShipSpatialStateRuntime state) => new(
state.SpaceLayer,
state.CurrentSystemId,
state.CurrentNodeId,
state.CurrentBubbleId,
state.CurrentCelestialId,
state.LocalPosition is null ? null : ToDto(state.LocalPosition.Value),
state.SystemPosition is null ? null : ToDto(state.SystemPosition.Value),
state.MovementRegime,

View File

@@ -305,7 +305,7 @@ public sealed partial class SimulationEngine
private static bool FactionControlsSystem(SimulationWorld world, string factionId, string systemId)
{
var totalLagrangePoints = world.SpatialNodes.Count(node =>
var totalLagrangePoints = world.Celestials.Count(node =>
node.SystemId == systemId &&
node.Kind == SpatialNodeKind.LagrangePoint);
if (totalLagrangePoints == 0)

View File

@@ -90,8 +90,7 @@ public sealed partial class SimulationEngine
{
CurrentSystemId = station.SystemId,
SpaceLayer = SpaceLayerKinds.LocalSpace,
CurrentNodeId = station.NodeId,
CurrentBubbleId = station.BubbleId,
CurrentCelestialId = station.CelestialId,
LocalPosition = position,
SystemPosition = position,
MovementRegime = MovementRegimeKinds.LocalFlight,

View File

@@ -70,8 +70,7 @@ public sealed partial class SimulationEngine
world.GeneratedAtUtc,
false,
events,
BuildSpatialNodeDeltas(world),
BuildLocalBubbleDeltas(world),
BuildCelestialDeltas(world),
BuildNodeDeltas(world),
BuildStationDeltas(world),
BuildClaimDeltas(world),

View File

@@ -116,7 +116,6 @@ public sealed class WorldService(
[],
[],
[],
[],
[]);
_history.Enqueue(resetDelta);
@@ -132,8 +131,7 @@ public sealed class WorldService(
private static bool HasMeaningfulDelta(WorldDelta delta) =>
delta.RequiresSnapshotRefresh
|| delta.Events.Count > 0
|| delta.SpatialNodes.Count > 0
|| delta.LocalBubbles.Count > 0
|| delta.Celestials.Count > 0
|| delta.Nodes.Count > 0
|| delta.Stations.Count > 0
|| delta.Claims.Count > 0
@@ -168,9 +166,9 @@ public sealed class WorldService(
}
var systemFilter = scope.SystemId;
if (string.Equals(scope.ScopeKind, "local-bubble", StringComparison.OrdinalIgnoreCase) && systemFilter is null && scope.BubbleId is not null)
if (string.Equals(scope.ScopeKind, "local-celestial", StringComparison.OrdinalIgnoreCase) && systemFilter is null && scope.CelestialId is not null)
{
systemFilter = ResolveBubbleSystemId(scope.BubbleId);
systemFilter = ResolveCelestialSystemId(scope.CelestialId);
}
return delta with
@@ -179,8 +177,7 @@ public sealed class WorldService(
.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(),
Celestials = delta.Celestials.Where((celestial) => systemFilter is null || celestial.SystemId == systemFilter).ToList(),
Nodes = delta.Nodes.Where((node) => systemFilter is null || node.SystemId == systemFilter).ToList(),
Stations = delta.Stations.Where((station) => systemFilter is null || station.SystemId == systemFilter).ToList(),
Claims = delta.Claims.Where((claim) => systemFilter is null || claim.SystemId == systemFilter).ToList(),
@@ -205,8 +202,7 @@ public sealed class WorldService(
"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),
"celestial" => WithEntityScope(evt, "system", _world.Celestials.FirstOrDefault((c) => c.Id == evt.EntityId)?.SystemId),
"claim" => WithEntityScope(evt, "system", _world.Claims.FirstOrDefault((claim) => claim.Id == evt.EntityId)?.SystemId),
"construction-site" => WithEntityScope(evt, "system", _world.ConstructionSites.FirstOrDefault((site) => site.Id == evt.EntityId)?.SystemId),
"market-order" => WithEntityScope(evt, "system", ResolveMarketOrderSystemId(evt.EntityId)),
@@ -226,8 +222,8 @@ public sealed class WorldService(
ScopeEntityId = scopeEntityId,
};
private string? ResolveBubbleSystemId(string bubbleId) =>
_world.LocalBubbles.FirstOrDefault((bubble) => bubble.Id == bubbleId)?.SystemId;
private string? ResolveCelestialSystemId(string celestialId) =>
_world.Celestials.FirstOrDefault((c) => c.Id == celestialId)?.SystemId;
private string? ResolveMarketOrderSystemId(string orderId)
{
@@ -271,7 +267,7 @@ public sealed class WorldService(
{
"universe" => true,
"system" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
"local-bubble" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
"local-celestial" => evt.ScopeKind == "universe" || evt.ScopeEntityId == systemFilter,
_ => true,
};
}