Implement roadmap phases 1 through 8
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user