Add world delta streaming and viewer smoothing
This commit is contained in:
@@ -13,6 +13,7 @@ public sealed class SimulationWorld
|
||||
public required List<ShipRuntime> Ships { get; init; }
|
||||
public required List<FactionRuntime> Factions { get; init; }
|
||||
public required Dictionary<string, ShipDefinition> ShipDefinitions { get; init; }
|
||||
public int TickIntervalMs { get; init; } = 200;
|
||||
public DateTimeOffset GeneratedAtUtc { get; set; }
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ public sealed class ResourceNodeRuntime
|
||||
public required string ItemId { get; init; }
|
||||
public float OreRemaining { get; set; }
|
||||
public float MaxOre { get; init; }
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class StationRuntime
|
||||
@@ -43,6 +45,7 @@ public sealed class StationRuntime
|
||||
public float RefinedStock { get; set; }
|
||||
public float ProcessTimer { get; set; }
|
||||
public HashSet<string> DockedShipIds { get; } = [];
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ShipRuntime
|
||||
@@ -53,6 +56,7 @@ public sealed class ShipRuntime
|
||||
public required string FactionId { get; init; }
|
||||
public required Vector3 Position { get; set; }
|
||||
public required Vector3 TargetPosition { get; set; }
|
||||
public Vector3 Velocity { get; set; } = Vector3.Zero;
|
||||
public string State { get; set; } = "idle";
|
||||
public ShipOrderRuntime? Order { get; set; }
|
||||
public required DefaultBehaviorRuntime DefaultBehavior { get; set; }
|
||||
@@ -63,6 +67,7 @@ public sealed class ShipRuntime
|
||||
public float Health { get; set; }
|
||||
public List<string> History { get; } = [];
|
||||
public string LastSignature { get; set; } = string.Empty;
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class FactionRuntime
|
||||
@@ -75,6 +80,7 @@ public sealed class FactionRuntime
|
||||
public float GoodsProduced { get; set; }
|
||||
public int ShipsBuilt { get; set; }
|
||||
public int ShipsLost { get; set; }
|
||||
public string LastDeltaSignature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ShipOrderRuntime
|
||||
@@ -131,4 +137,16 @@ public readonly record struct Vector3(float X, float Y, float Z)
|
||||
Y + ((target.Y - Y) * t),
|
||||
Z + ((target.Z - Z) * t));
|
||||
}
|
||||
|
||||
public Vector3 Subtract(Vector3 other) => new(X - other.X, Y - other.Y, Z - other.Z);
|
||||
|
||||
public Vector3 Divide(float value)
|
||||
{
|
||||
if (value == 0f)
|
||||
{
|
||||
return Zero;
|
||||
}
|
||||
|
||||
return new(X / value, Y / value, Z / value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,27 +4,52 @@ namespace SpaceGame.Simulation.Api.Simulation;
|
||||
|
||||
public sealed class SimulationEngine
|
||||
{
|
||||
public void Tick(SimulationWorld world, float deltaSeconds)
|
||||
public WorldDelta Tick(SimulationWorld world, float deltaSeconds, long sequence)
|
||||
{
|
||||
UpdateStations(world, deltaSeconds);
|
||||
var events = new List<SimulationEventRecord>();
|
||||
|
||||
UpdateStations(world, deltaSeconds, events);
|
||||
|
||||
foreach (var ship in world.Ships)
|
||||
{
|
||||
var previousPosition = ship.Position;
|
||||
var previousState = ship.State;
|
||||
var previousBehavior = ship.DefaultBehavior.Kind;
|
||||
var previousTask = ship.ControllerTask.Kind;
|
||||
|
||||
RefreshControlLayers(ship);
|
||||
PlanControllerTask(ship, world);
|
||||
var controllerEvent = UpdateControllerTask(ship, world, deltaSeconds);
|
||||
AdvanceControlState(ship, controllerEvent);
|
||||
ship.Velocity = ship.Position.Subtract(previousPosition).Divide(deltaSeconds);
|
||||
TrackHistory(ship);
|
||||
|
||||
EmitShipStateEvents(ship, previousState, previousBehavior, previousTask, controllerEvent, events);
|
||||
}
|
||||
|
||||
world.GeneratedAtUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
return new WorldDelta(
|
||||
sequence,
|
||||
world.TickIntervalMs,
|
||||
world.GeneratedAtUtc,
|
||||
false,
|
||||
events,
|
||||
BuildNodeDeltas(world),
|
||||
BuildStationDeltas(world),
|
||||
BuildShipDeltas(world),
|
||||
BuildFactionDeltas(world));
|
||||
}
|
||||
|
||||
public WorldSnapshot BuildSnapshot(SimulationWorld world)
|
||||
public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence)
|
||||
{
|
||||
PrimeDeltaBaseline(world);
|
||||
|
||||
return new WorldSnapshot(
|
||||
world.Label,
|
||||
world.Seed,
|
||||
sequence,
|
||||
world.TickIntervalMs,
|
||||
world.GeneratedAtUtc,
|
||||
world.Systems.Select((system) => new SystemSnapshot(
|
||||
system.Definition.Id,
|
||||
@@ -38,42 +63,44 @@ public sealed class SimulationEngine
|
||||
planet.Size,
|
||||
planet.Color,
|
||||
planet.HasRing)).ToList())).ToList(),
|
||||
world.Nodes.Select((node) => new ResourceNodeSnapshot(
|
||||
world.Nodes.Select(ToNodeDelta).Select((node) => new ResourceNodeSnapshot(
|
||||
node.Id,
|
||||
node.SystemId,
|
||||
ToDto(node.Position),
|
||||
node.Position,
|
||||
node.OreRemaining,
|
||||
node.MaxOre,
|
||||
node.ItemId)).ToList(),
|
||||
world.Stations.Select((station) => new StationSnapshot(
|
||||
world.Stations.Select(ToStationDelta).Select((station) => new StationSnapshot(
|
||||
station.Id,
|
||||
station.Definition.Label,
|
||||
station.Definition.Category,
|
||||
station.Label,
|
||||
station.Category,
|
||||
station.SystemId,
|
||||
ToDto(station.Position),
|
||||
station.Definition.Color,
|
||||
station.DockedShipIds.Count,
|
||||
station.Position,
|
||||
station.Color,
|
||||
station.DockedShips,
|
||||
station.OreStored,
|
||||
station.RefinedStock,
|
||||
station.FactionId)).ToList(),
|
||||
world.Ships.Select((ship) => new ShipSnapshot(
|
||||
world.Ships.Select(ToShipDelta).Select((ship) => new ShipSnapshot(
|
||||
ship.Id,
|
||||
ship.Definition.Label,
|
||||
ship.Definition.Role,
|
||||
ship.Definition.ShipClass,
|
||||
ship.Label,
|
||||
ship.Role,
|
||||
ship.ShipClass,
|
||||
ship.SystemId,
|
||||
ToDto(ship.Position),
|
||||
ship.Position,
|
||||
ship.Velocity,
|
||||
ship.TargetPosition,
|
||||
ship.State,
|
||||
ship.Order?.Kind,
|
||||
ship.DefaultBehavior.Kind,
|
||||
ship.ControllerTask.Kind,
|
||||
ship.OrderKind,
|
||||
ship.DefaultBehaviorKind,
|
||||
ship.ControllerTaskKind,
|
||||
ship.Cargo,
|
||||
ship.Definition.CargoCapacity,
|
||||
ship.Definition.CargoItemId,
|
||||
ship.CargoCapacity,
|
||||
ship.CargoItemId,
|
||||
ship.FactionId,
|
||||
ship.Health,
|
||||
ship.History.ToList())).ToList(),
|
||||
world.Factions.Select((faction) => new FactionSnapshot(
|
||||
ship.History)).ToList(),
|
||||
world.Factions.Select(ToFactionDelta).Select((faction) => new FactionSnapshot(
|
||||
faction.Id,
|
||||
faction.Label,
|
||||
faction.Color,
|
||||
@@ -84,7 +111,211 @@ public sealed class SimulationEngine
|
||||
faction.ShipsLost)).ToList());
|
||||
}
|
||||
|
||||
private void UpdateStations(SimulationWorld world, float deltaSeconds)
|
||||
public void PrimeDeltaBaseline(SimulationWorld world)
|
||||
{
|
||||
foreach (var node in world.Nodes)
|
||||
{
|
||||
node.LastDeltaSignature = BuildNodeSignature(node);
|
||||
}
|
||||
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
station.LastDeltaSignature = BuildStationSignature(station);
|
||||
}
|
||||
|
||||
foreach (var ship in world.Ships)
|
||||
{
|
||||
ship.LastDeltaSignature = BuildShipSignature(ship);
|
||||
}
|
||||
|
||||
foreach (var faction in world.Factions)
|
||||
{
|
||||
faction.LastDeltaSignature = BuildFactionSignature(faction);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ResourceNodeDelta> BuildNodeDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<ResourceNodeDelta>();
|
||||
foreach (var node in world.Nodes)
|
||||
{
|
||||
var signature = BuildNodeSignature(node);
|
||||
if (signature == node.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
node.LastDeltaSignature = signature;
|
||||
deltas.Add(ToNodeDelta(node));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<StationDelta> BuildStationDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<StationDelta>();
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
var signature = BuildStationSignature(station);
|
||||
if (signature == station.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
station.LastDeltaSignature = signature;
|
||||
deltas.Add(ToStationDelta(station));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ShipDelta> BuildShipDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<ShipDelta>();
|
||||
foreach (var ship in world.Ships)
|
||||
{
|
||||
var signature = BuildShipSignature(ship);
|
||||
if (signature == ship.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ship.LastDeltaSignature = signature;
|
||||
deltas.Add(ToShipDelta(ship));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<FactionDelta> BuildFactionDeltas(SimulationWorld world)
|
||||
{
|
||||
var deltas = new List<FactionDelta>();
|
||||
foreach (var faction in world.Factions)
|
||||
{
|
||||
var signature = BuildFactionSignature(faction);
|
||||
if (signature == faction.LastDeltaSignature)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
faction.LastDeltaSignature = signature;
|
||||
deltas.Add(ToFactionDelta(faction));
|
||||
}
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
private static string BuildNodeSignature(ResourceNodeRuntime node) =>
|
||||
$"{node.SystemId}|{node.OreRemaining:0.###}";
|
||||
|
||||
private static string BuildStationSignature(StationRuntime station) =>
|
||||
$"{station.SystemId}|{station.OreStored:0.###}|{station.RefinedStock:0.###}|{station.DockedShipIds.Count}";
|
||||
|
||||
private static string BuildShipSignature(ShipRuntime ship) =>
|
||||
string.Join("|",
|
||||
ship.SystemId,
|
||||
ship.Position.X.ToString("0.###"),
|
||||
ship.Position.Y.ToString("0.###"),
|
||||
ship.Position.Z.ToString("0.###"),
|
||||
ship.Velocity.X.ToString("0.###"),
|
||||
ship.Velocity.Y.ToString("0.###"),
|
||||
ship.Velocity.Z.ToString("0.###"),
|
||||
ship.TargetPosition.X.ToString("0.###"),
|
||||
ship.TargetPosition.Y.ToString("0.###"),
|
||||
ship.TargetPosition.Z.ToString("0.###"),
|
||||
ship.State,
|
||||
ship.Order?.Kind ?? "none",
|
||||
ship.DefaultBehavior.Kind,
|
||||
ship.ControllerTask.Kind,
|
||||
ship.Cargo.ToString("0.###"),
|
||||
ship.Health.ToString("0.###"));
|
||||
|
||||
private static string BuildFactionSignature(FactionRuntime faction) =>
|
||||
$"{faction.Credits:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}";
|
||||
|
||||
private static ResourceNodeDelta ToNodeDelta(ResourceNodeRuntime node) => new(
|
||||
node.Id,
|
||||
node.SystemId,
|
||||
ToDto(node.Position),
|
||||
node.OreRemaining,
|
||||
node.MaxOre,
|
||||
node.ItemId);
|
||||
|
||||
private static StationDelta ToStationDelta(StationRuntime station) => new(
|
||||
station.Id,
|
||||
station.Definition.Label,
|
||||
station.Definition.Category,
|
||||
station.SystemId,
|
||||
ToDto(station.Position),
|
||||
station.Definition.Color,
|
||||
station.DockedShipIds.Count,
|
||||
station.OreStored,
|
||||
station.RefinedStock,
|
||||
station.FactionId);
|
||||
|
||||
private static ShipDelta ToShipDelta(ShipRuntime ship) => new(
|
||||
ship.Id,
|
||||
ship.Definition.Label,
|
||||
ship.Definition.Role,
|
||||
ship.Definition.ShipClass,
|
||||
ship.SystemId,
|
||||
ToDto(ship.Position),
|
||||
ToDto(ship.Velocity),
|
||||
ToDto(ship.TargetPosition),
|
||||
ship.State,
|
||||
ship.Order?.Kind,
|
||||
ship.DefaultBehavior.Kind,
|
||||
ship.ControllerTask.Kind,
|
||||
ship.Cargo,
|
||||
ship.Definition.CargoCapacity,
|
||||
ship.Definition.CargoItemId,
|
||||
ship.FactionId,
|
||||
ship.Health,
|
||||
ship.History.ToList());
|
||||
|
||||
private static FactionDelta ToFactionDelta(FactionRuntime faction) => new(
|
||||
faction.Id,
|
||||
faction.Label,
|
||||
faction.Color,
|
||||
faction.Credits,
|
||||
faction.OreMined,
|
||||
faction.GoodsProduced,
|
||||
faction.ShipsBuilt,
|
||||
faction.ShipsLost);
|
||||
|
||||
private static void EmitShipStateEvents(
|
||||
ShipRuntime ship,
|
||||
string previousState,
|
||||
string previousBehavior,
|
||||
string previousTask,
|
||||
string controllerEvent,
|
||||
ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
var occurredAtUtc = DateTimeOffset.UtcNow;
|
||||
|
||||
if (previousState != ship.State)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Label} {previousState} -> {ship.State}", occurredAtUtc));
|
||||
}
|
||||
|
||||
if (previousBehavior != ship.DefaultBehavior.Kind)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "behavior-changed", $"{ship.Definition.Label} behavior {previousBehavior} -> {ship.DefaultBehavior.Kind}", occurredAtUtc));
|
||||
}
|
||||
|
||||
if (previousTask != ship.ControllerTask.Kind)
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, "task-changed", $"{ship.Definition.Label} task {previousTask} -> {ship.ControllerTask.Kind}", occurredAtUtc));
|
||||
}
|
||||
|
||||
if (controllerEvent != "none")
|
||||
{
|
||||
events.Add(new SimulationEventRecord("ship", ship.Id, controllerEvent, $"{ship.Definition.Label} {controllerEvent}", occurredAtUtc));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStations(SimulationWorld world, float deltaSeconds, ICollection<SimulationEventRecord> events)
|
||||
{
|
||||
foreach (var station in world.Stations)
|
||||
{
|
||||
@@ -102,6 +333,7 @@ public sealed class SimulationEngine
|
||||
station.ProcessTimer = 0f;
|
||||
station.OreStored -= 60f;
|
||||
station.RefinedStock += 60f;
|
||||
events.Add(new SimulationEventRecord("station", station.Id, "refined", $"{station.Definition.Label} refined 60 ore", DateTimeOffset.UtcNow));
|
||||
var faction = world.Factions.FirstOrDefault((candidate) => candidate.Id == station.FactionId);
|
||||
if (faction is not null)
|
||||
{
|
||||
@@ -251,6 +483,7 @@ public sealed class SimulationEngine
|
||||
{
|
||||
case "idle":
|
||||
ship.State = "idle";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
case "travel":
|
||||
return UpdateTravel(ship, world, deltaSeconds);
|
||||
@@ -264,6 +497,7 @@ public sealed class SimulationEngine
|
||||
return UpdateUndock(ship, world, deltaSeconds);
|
||||
default:
|
||||
ship.State = "idle";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
@@ -274,9 +508,11 @@ public sealed class SimulationEngine
|
||||
if (task.TargetPosition is null || task.TargetSystemId is null)
|
||||
{
|
||||
ship.State = "idle";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = task.TargetPosition.Value;
|
||||
var distance = ship.Position.DistanceTo(task.TargetPosition.Value);
|
||||
if (distance <= task.Threshold)
|
||||
{
|
||||
@@ -305,7 +541,6 @@ public sealed class SimulationEngine
|
||||
}
|
||||
|
||||
ship.Position = ship.Position.MoveToward(task.TargetPosition.Value, speed * deltaSeconds);
|
||||
ship.TargetPosition = task.TargetPosition.Value;
|
||||
return "none";
|
||||
}
|
||||
|
||||
@@ -316,9 +551,11 @@ public sealed class SimulationEngine
|
||||
if (node is null || task.TargetPosition is null)
|
||||
{
|
||||
ship.State = "idle";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = task.TargetPosition.Value;
|
||||
var distance = ship.Position.DistanceTo(task.TargetPosition.Value);
|
||||
if (distance > task.Threshold)
|
||||
{
|
||||
@@ -354,9 +591,11 @@ public sealed class SimulationEngine
|
||||
if (station is null || task.TargetPosition is null)
|
||||
{
|
||||
ship.State = "idle";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = task.TargetPosition.Value;
|
||||
var distance = ship.Position.DistanceTo(task.TargetPosition.Value);
|
||||
if (distance > task.Threshold)
|
||||
{
|
||||
@@ -377,6 +616,7 @@ public sealed class SimulationEngine
|
||||
ship.DockedStationId = station.Id;
|
||||
station.DockedShipIds.Add(ship.Id);
|
||||
ship.Position = station.Position;
|
||||
ship.TargetPosition = station.Position;
|
||||
return "docked";
|
||||
}
|
||||
|
||||
@@ -385,6 +625,7 @@ public sealed class SimulationEngine
|
||||
if (ship.DockedStationId is null)
|
||||
{
|
||||
ship.State = "idle";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
@@ -393,9 +634,11 @@ public sealed class SimulationEngine
|
||||
{
|
||||
ship.DockedStationId = null;
|
||||
ship.State = "idle";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = station.Position;
|
||||
ship.State = "transferring";
|
||||
var moved = MathF.Min(ship.Cargo, world.Balance.TransferRate * deltaSeconds);
|
||||
ship.Cargo -= moved;
|
||||
@@ -416,9 +659,11 @@ public sealed class SimulationEngine
|
||||
if (ship.DockedStationId is null || task.TargetPosition is null)
|
||||
{
|
||||
ship.State = "idle";
|
||||
ship.TargetPosition = ship.Position;
|
||||
return "none";
|
||||
}
|
||||
|
||||
ship.TargetPosition = task.TargetPosition.Value;
|
||||
var station = world.Stations.FirstOrDefault((candidate) => candidate.Id == ship.DockedStationId);
|
||||
station?.DockedShipIds.Remove(ship.Id);
|
||||
ship.DockedStationId = null;
|
||||
|
||||
@@ -1,27 +1,81 @@
|
||||
using System.Threading.Channels;
|
||||
using SpaceGame.Simulation.Api.Contracts;
|
||||
|
||||
namespace SpaceGame.Simulation.Api.Simulation;
|
||||
|
||||
public sealed class WorldService(IWebHostEnvironment environment)
|
||||
{
|
||||
private const int DeltaHistoryLimit = 256;
|
||||
|
||||
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 Queue<WorldDelta> _history = [];
|
||||
private SimulationWorld _world = new ScenarioLoader(environment.ContentRootPath).Load();
|
||||
private long _sequence;
|
||||
|
||||
public WorldSnapshot GetSnapshot()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _engine.BuildSnapshot(_world);
|
||||
return _engine.BuildSnapshot(_world, _sequence);
|
||||
}
|
||||
}
|
||||
|
||||
public (long Sequence, DateTimeOffset GeneratedAtUtc) GetStatus()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return (_sequence, _world.GeneratedAtUtc);
|
||||
}
|
||||
}
|
||||
|
||||
public ChannelReader<WorldDelta> Subscribe(long afterSequence, CancellationToken cancellationToken)
|
||||
{
|
||||
var channel = Channel.CreateUnbounded<WorldDelta>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
|
||||
Guid subscriberId;
|
||||
lock (_sync)
|
||||
{
|
||||
subscriberId = Guid.NewGuid();
|
||||
_subscribers.Add(subscriberId, channel);
|
||||
|
||||
foreach (var delta in _history.Where((candidate) => candidate.Sequence > afterSequence))
|
||||
{
|
||||
channel.Writer.TryWrite(delta);
|
||||
}
|
||||
}
|
||||
|
||||
cancellationToken.Register(() => Unsubscribe(subscriberId));
|
||||
return channel.Reader;
|
||||
}
|
||||
|
||||
public void Tick(float deltaSeconds)
|
||||
{
|
||||
WorldDelta? delta = null;
|
||||
lock (_sync)
|
||||
{
|
||||
_engine.Tick(_world, deltaSeconds);
|
||||
delta = _engine.Tick(_world, deltaSeconds, ++_sequence);
|
||||
if (!HasMeaningfulDelta(delta))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_history.Enqueue(delta);
|
||||
while (_history.Count > DeltaHistoryLimit)
|
||||
{
|
||||
_history.Dequeue();
|
||||
}
|
||||
|
||||
foreach (var subscriber in _subscribers.Values.ToList())
|
||||
{
|
||||
subscriber.Writer.TryWrite(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +84,48 @@ public sealed class WorldService(IWebHostEnvironment environment)
|
||||
lock (_sync)
|
||||
{
|
||||
_world = _loader.Load();
|
||||
return _engine.BuildSnapshot(_world);
|
||||
_sequence += 1;
|
||||
_history.Clear();
|
||||
|
||||
var resetDelta = new WorldDelta(
|
||||
_sequence,
|
||||
_world.TickIntervalMs,
|
||||
DateTimeOffset.UtcNow,
|
||||
true,
|
||||
[new SimulationEventRecord("world", "world", "reset", "World reset requested", DateTimeOffset.UtcNow)],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]);
|
||||
|
||||
_history.Enqueue(resetDelta);
|
||||
foreach (var subscriber in _subscribers.Values.ToList())
|
||||
{
|
||||
subscriber.Writer.TryWrite(resetDelta);
|
||||
}
|
||||
|
||||
return _engine.BuildSnapshot(_world, _sequence);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasMeaningfulDelta(WorldDelta delta) =>
|
||||
delta.RequiresSnapshotRefresh
|
||||
|| delta.Events.Count > 0
|
||||
|| delta.Nodes.Count > 0
|
||||
|| delta.Stations.Count > 0
|
||||
|| delta.Ships.Count > 0
|
||||
|| delta.Factions.Count > 0;
|
||||
|
||||
private void Unsubscribe(Guid subscriberId)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_subscribers.Remove(subscriberId, out var channel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
channel.Writer.TryComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user