Version hot-body demand generations

This commit is contained in:
Meta Vens 2026-08-19 14:10:13 +02:00
commit 21fd4e07eb
2 changed files with 54 additions and 1 deletions

View file

@ -225,9 +225,11 @@ namespace GameCult.Mesh
{
internal CultMeshBodyDemandPlan(
string bodyId,
long generation,
IReadOnlyList<CultMeshBodyConsumerRoute> consumers)
{
BodyId = bodyId;
Generation = generation;
Consumers = consumers;
RequiresSharedMemory = consumers.Any(value => value.Transport == CultMeshBodyTransportKind.SharedMemory);
RequiresSharedFileMapping = consumers.Any(value => value.Transport == CultMeshBodyTransportKind.SharedFileMapping);
@ -235,6 +237,11 @@ namespace GameCult.Mesh
}
public string BodyId { get; }
/// <summary>
/// Monotone generation of the exact routed consumer set. Publishers must rotate any
/// retained capability before writing after this value changes.
/// </summary>
public long Generation { get; }
public bool HasConsumers => Consumers.Count > 0;
public bool RequiresSharedMemory { get; }
public bool RequiresSharedFileMapping { get; }
@ -271,6 +278,8 @@ namespace GameCult.Mesh
private readonly object _gate = new();
private readonly Dictionary<string, CultNetDatabaseSubscriptionDemand> _demands =
new(StringComparer.Ordinal);
private readonly Dictionary<string, long> _bodyGenerations =
new(StringComparer.Ordinal);
private bool _disposed;
public CultMeshBodyDemandTracker(CultNetDatabaseSubscriptionServer subscriptions)
@ -298,7 +307,10 @@ namespace GameCult.Mesh
.OrderBy(value => value.ConsumerRuntimeId, StringComparer.Ordinal)
.ThenBy(value => value.SubscriptionId, StringComparer.Ordinal)
.ToArray();
return new CultMeshBodyDemandPlan(bodyId, routes);
return new CultMeshBodyDemandPlan(
bodyId,
_bodyGenerations.TryGetValue(bodyId, out var generation) ? generation : 0,
routes);
}
}
@ -310,6 +322,7 @@ namespace GameCult.Mesh
_disposed = true;
if (_subscriptions != null) _subscriptions.DemandChanged -= OnDemandChanged;
_demands.Clear();
_bodyGenerations.Clear();
}
}
@ -322,11 +335,43 @@ namespace GameCult.Mesh
lock (_gate)
{
if (_disposed) return;
_demands.TryGetValue(key, out var previous);
var affectedBodyIds = (previous?.BodyIds ?? Array.Empty<string>())
.Concat(demand.BodyIds)
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.Ordinal)
.ToArray();
var before = affectedBodyIds.ToDictionary(
bodyId => bodyId,
RoutedConsumerSignature,
StringComparer.Ordinal);
if (demand.Active) _demands[key] = demand;
else _demands.Remove(key);
foreach (var bodyId in affectedBodyIds)
{
var after = RoutedConsumerSignature(bodyId);
if (string.Equals(before[bodyId], after, StringComparison.Ordinal)) continue;
_bodyGenerations[bodyId] = checked(
(_bodyGenerations.TryGetValue(bodyId, out var generation) ? generation : 0) + 1);
}
}
}
private string RoutedConsumerSignature(string bodyId) => string.Join(
"\u001e",
_demands.Values
.Where(value => value.BodyIds.Contains(bodyId, StringComparer.Ordinal))
.Select(Route)
.Where(value => value != null)
.Cast<CultMeshBodyConsumerRoute>()
.OrderBy(value => value.ConsumerRuntimeId, StringComparer.Ordinal)
.ThenBy(value => value.SubscriptionId, StringComparer.Ordinal)
.Select(value => string.Join(
"\u001f",
value.ConsumerRuntimeId,
value.SubscriptionId,
value.Transport.ToString())));
private void OnDemandChanged(CultNetDatabaseSubscriptionDemand demand) => Observe(demand);
private static CultMeshBodyConsumerRoute? Route(CultNetDatabaseSubscriptionDemand demand)

View file

@ -318,7 +318,9 @@ public sealed class CultMeshBodyTransportTests
public void BodyDemand_SelectsSharedMemoryLocallyAndNetworkRemotelyWithoutDuplicateFallbackWork()
{
using var tracker = new CultMeshBodyDemandTracker();
tracker.Plan("eve:entity-soa:aetheria.daemon:pilot").Generation.Should().Be(0);
tracker.Observe(Demand("unity-local", "local", sameMachine: true));
tracker.Plan("eve:entity-soa:aetheria.daemon:pilot").Generation.Should().Be(1);
tracker.Observe(Demand("unity-remote", "remote", sameMachine: false));
var mixed = tracker.Plan("eve:entity-soa:aetheria.daemon:pilot");
@ -335,8 +337,14 @@ public sealed class CultMeshBodyTransportTests
tracker.Observe(Demand("unity-remote", "remote", sameMachine: false, active: false));
var localOnly = tracker.Plan("eve:entity-soa:aetheria.daemon:pilot");
localOnly.Generation.Should().Be(3);
localOnly.RequiresSharedMemory.Should().BeTrue();
localOnly.RequiresNetwork.Should().BeFalse();
tracker.Observe(Demand("unity-local", "local", sameMachine: true, active: false));
tracker.Plan("eve:entity-soa:aetheria.daemon:pilot").Generation.Should().Be(4);
tracker.Observe(Demand("unity-local", "local", sameMachine: true));
tracker.Plan("eve:entity-soa:aetheria.daemon:pilot").Generation.Should().Be(5);
}
[Test]