Add CultNet simulation observation server

This commit is contained in:
Meta Vens 2026-05-19 18:48:53 +01:00
commit 9fa85d755f
5 changed files with 138 additions and 3 deletions

View file

@ -176,6 +176,7 @@ Shard catalog exchange now exists:
- simulation witness observations and deterministic consensus candidates - simulation witness observations and deterministic consensus candidates
- schema-v0 simulation observation and candidate messages - schema-v0 simulation observation and candidate messages
- reactive observation hub for witness gossip and consensus candidates - reactive observation hub for witness gossip and consensus candidates
- server-side observation bridge for observation messages and candidate replies
## Next Cut ## Next Cut
@ -186,7 +187,7 @@ Build durable authoritative mutation logs:
- return explicit resync-required responses when requested history is gone - return explicit resync-required responses when requested history is gone
- then add simulation-frame rollback and resimulation helpers on top of - then add simulation-frame rollback and resimulation helpers on top of
predicted input streams predicted input streams
- add network fanout for observation gossip and candidate propagation - add peer-to-peer fanout for observation gossip and candidate propagation
That is the next foundation needed before membership, Raft-style failover, or That is the next foundation needed before membership, Raft-style failover, or
CRDT policy work can be honest. CRDT policy work can be honest.

View file

@ -66,6 +66,8 @@ Implemented:
- schema-v0 simulation observation and consensus candidate messages - schema-v0 simulation observation and consensus candidate messages
- reactive simulation observation hub that receives witness gossip and emits - reactive simulation observation hub that receives witness gossip and emits
consensus candidates consensus candidates
- server-side observation bridge that accepts observation messages and replies
with current candidate messages
Not implemented yet: Not implemented yet:
@ -73,7 +75,7 @@ Not implemented yet:
- leader election or automatic shard failover - leader election or automatic shard failover
- durable log persistence and compaction - durable log persistence and compaction
- snapshot fallback for compacted log history - snapshot fallback for compacted log history
- network fanout for observation gossip and candidate propagation - peer-to-peer fanout for observation gossip and candidate propagation
- rollback/resimulation helpers for simulation frames - rollback/resimulation helpers for simulation frames
- declared CRDT merge policies - declared CRDT merge policies
@ -108,7 +110,7 @@ Add durable mutation-log storage and snapshot fallback:
- return `ResyncRequired` when a replica asks for compacted history - return `ResyncRequired` when a replica asks for compacted history
- after that, add simulation-frame rollback/resimulation helpers around - after that, add simulation-frame rollback/resimulation helpers around
predicted input streams predicted input streams
- add network fanout for observation gossip and candidate propagation - add peer-to-peer fanout for observation gossip and candidate propagation
The log is wire-readable, replicas can apply it explicitly, a pull loop can The log is wire-readable, replicas can apply it explicitly, a pull loop can
drive non-primary shards from primary endpoints, and replica cursors can survive drive non-primary shards from primary endpoints, and replica cursors can survive

View file

@ -0,0 +1,79 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using LiteNetLib;
namespace GameCult.Networking
{
/// <summary>
/// Bridges schema-v0 simulation observation messages into a local observation hub.
/// </summary>
public sealed class CultNetSimulationObservationServer : IDisposable
{
private readonly Server _server;
private readonly CultNetSimulationObservationHub _hub;
private readonly Func<CultNetSimulationObservationMessage, NetPeer, Task> _observationHandler;
private bool _disposed;
/// <summary>
/// Creates and attaches a simulation observation bridge.
/// </summary>
public CultNetSimulationObservationServer(
Server server,
CultNetSimulationObservationHub hub)
{
_server = server ?? throw new ArgumentNullException(nameof(server));
_hub = hub ?? throw new ArgumentNullException(nameof(hub));
_observationHandler = HandleObservationAsync;
_server.OnCultNet(_observationHandler);
}
/// <summary>
/// Gets the observation hub used by this bridge.
/// </summary>
public CultNetSimulationObservationHub Hub => _hub;
/// <summary>
/// Applies an observation message and returns current candidate messages for the same subject.
/// </summary>
public CultNetSimulationConsensusCandidateMessage[] CreateCandidateMessages(
CultNetSimulationObservationMessage message)
{
if (message == null) throw new ArgumentNullException(nameof(message));
return _hub.Submit(message)
.Select(candidate => CultNetSimulationConsensusCandidateMessage.FromCandidate(
message.MessageId,
candidate))
.ToArray();
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_server.RemoveCultNetMessageListener<CultNetSimulationObservationMessage>(_observationHandler);
}
private Task HandleObservationAsync(CultNetSimulationObservationMessage message, NetPeer peer)
{
try
{
foreach (var candidate in CreateCandidateMessages(message))
{
peer.SendCultNet(candidate);
}
}
catch (Exception ex)
{
peer.SendCultNet(new CultNetErrorMessage { Error = ex.Message });
}
return Task.CompletedTask;
}
}
}

View file

@ -163,6 +163,9 @@ path, not committed world state by themselves. The schema-v0 wire contracts are
`cultnet.simulation_observation.v0` and `cultnet.simulation_observation.v0` and
`cultnet.simulation_consensus_candidate.v0`. `CultNetSimulationObservationHub` `cultnet.simulation_consensus_candidate.v0`. `CultNetSimulationObservationHub`
collects incoming observations and emits candidate updates reactively. collects incoming observations and emits candidate updates reactively.
`CultNetSimulationObservationServer` attaches that hub to a server so incoming
observation messages update the hub and receive current candidate messages in
reply.
The first coherent shard policy is primary ownership. Each shard has one The first coherent shard policy is primary ownership. Each shard has one
authoritative writer at a time. Clients may connect to any node; a non-owner authoritative writer at a time. Clients may connect to any node; a non-owner

View file

@ -1431,6 +1431,56 @@ namespace GameCult.Networking.Tests
Assert.That(candidates.Exists(candidate => candidate.HasQuorum), Is.True); Assert.That(candidates.Exists(candidate => candidate.HasQuorum), Is.True);
} }
[Test]
public void CultNetSimulationObservationServer_Creates_CandidateMessages()
{
var cache = new CultCache();
using var server = new Server(cache, DevelopmentServerSecurity);
using var hub = new CultNetSimulationObservationHub(new CultNetSimulationConsensusOptions
{
MinimumWitnesses = 2,
QuorumRatio = 1d
});
using var observationServer = new CultNetSimulationObservationServer(server, hub);
var claimHash = CultNetSimulationObservation.ComputeClaimHash("hit", "alice", "bob", "frame:100");
var first = new CultNetSimulationObservationMessage
{
MessageId = "observe-1",
Observation = new CultNetSimulationObservation
{
WitnessRuntimeId = "watcher-1",
ShardId = "arena",
ShardEpoch = 1,
Frame = 100,
SubjectId = "bob",
ClaimKind = "hit",
ClaimHash = claimHash
}
};
var second = new CultNetSimulationObservationMessage
{
MessageId = "observe-2",
Observation = new CultNetSimulationObservation
{
WitnessRuntimeId = "watcher-2",
ShardId = "arena",
ShardEpoch = 1,
Frame = 100,
SubjectId = "bob",
ClaimKind = "hit",
ClaimHash = claimHash
}
};
observationServer.CreateCandidateMessages(first);
var candidates = observationServer.CreateCandidateMessages(second);
Assert.That(candidates, Has.Length.EqualTo(1));
Assert.That(candidates[0].MessageId, Is.EqualTo("observe-2"));
Assert.That(candidates[0].ClaimHash, Is.EqualTo(claimHash));
Assert.That(candidates[0].HasQuorum, Is.True);
}
[Test] [Test]
public void CultNetDatabaseServer_Creates_Filtered_SubscriptionChange() public void CultNetDatabaseServer_Creates_Filtered_SubscriptionChange()
{ {