Add C# CultMesh connected RUDP clients
This commit is contained in:
parent
d131d0f42a
commit
5af8ed57f7
7 changed files with 150 additions and 21 deletions
|
|
@ -286,8 +286,11 @@ Current progress:
|
|||
TypeScript, Python, Kotlin, and Rust: it parses `rudp://host:port` endpoints,
|
||||
creates RUDP client/server transports from endpoint or peer-card contact
|
||||
hints, and can create a RUDP client from the first peer authorized by the
|
||||
authority lease catalog for a Verse role. RUDP packet/session behavior remains
|
||||
owned by `GameCult.Networking`.
|
||||
authority lease catalog for a Verse role. The facade can also return a
|
||||
connected RUDP client from a direct endpoint, peer card, or authorized peer
|
||||
lookup, so schema-message helpers can ride the RUDP pipe without caller-side
|
||||
handshake ceremony. RUDP packet/session behavior remains owned by
|
||||
`GameCult.Networking`.
|
||||
- TypeScript now has a `CultNetRudpReconnectLoop` that consumes the shared
|
||||
controller while keeping socket construction caller-owned. A closed RUDP
|
||||
transport schedules the next attempt with the portable policy, opens a fresh
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ CultMesh.
|
|||
|
||||
| Runtime | Role | Claimed parity | Not claimed |
|
||||
| --- | --- | --- | --- |
|
||||
| C# | Reference implementation and production game/server substrate | CultCache typed documents and SoA, CultNet schema-v0, LiteNetLib production path with adapter profile, shared transport profiles, shared RUDP socket transport, RUDP reconnect policy/controller and reconnect-loop helper, LiteNetLib client reconnect-controller adoption, schema-client transport port for service request/response clients, CultMesh node/session/catalog/stream/simulation/RUDP surfaces, authorized peer lookup | No need to abandon LiteNetLib before the CultNet adapter fully owns production service adoption |
|
||||
| C# | Reference implementation and production game/server substrate | CultCache typed documents and SoA, CultNet schema-v0, LiteNetLib production path with adapter profile, shared transport profiles, shared RUDP socket transport, connected RUDP CultMesh client helpers, RUDP reconnect policy/controller and reconnect-loop helper, LiteNetLib client reconnect-controller adoption, schema-client transport port for service request/response clients, CultMesh node/session/catalog/stream/simulation/RUDP surfaces, authorized peer lookup | No need to abandon LiteNetLib before the CultNet adapter fully owns production service adoption |
|
||||
| Python | Package runtime, local daemon endpoint, raw interop peer, typed state participant | CultCache/CultNet/CultMesh package surface, branded CultMesh node/schema/shard/Verse/peer/authority/stream/RUDP ergonomics, authorized peer lookup, shard catalogs/logs, schema-transport factory for raw client request/subscription flows, local TCP CultMesh server, session/prediction/simulation fact helpers, RUDP codec/session/socket transport, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence, benchmark evidence against C# cache paths | Universal C# throughput, full C# daemon/server stack, and RUDP as the only public service pipe |
|
||||
| TypeScript | Node/browser-adjacent projection and interop runtime | CultCache parity tests, CultNet schema-v0 peer, advertised schema/shard catalog wire contracts plus request/sync helpers, transport profiles, TCP-framed transport port and peer helper, shared RUDP socket transport, RUDP reconnect policy/controller, RUDP reconnect-loop helper, branded CultMesh node/schema/shard/Verse/peer/authority/stream/RUDP ergonomics, authorized peer lookup | Full C# game session/server stack |
|
||||
| Rust | Low-level cache/transport/runtime substrate | CultCache/CultNet interop lane, schema and shard catalog helpers, transport profiles, TCP-framed port, shared RUDP codec/session/socket transport, RUDP reconnect policy/controller and reconnect-loop helper, schema-v0 MessagePack over RUDP evidence, small CultMesh facade for schema registry, shard catalog, peer cards, authority leases, authorized peer lookup, RUDP endpoint parsing, and peer-card/authorized-peer RUDP client construction | Full CultMesh game-session/server facade, stream catalog, or production game-session server |
|
||||
|
|
|
|||
|
|
@ -224,6 +224,21 @@ namespace GameCult.Mesh
|
|||
public CultNetReconnectPolicy? ReconnectPolicy { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for CultMesh-branded connected RUDP client helpers.
|
||||
/// </summary>
|
||||
public sealed class CultMeshRudpClientOptions
|
||||
{
|
||||
/// <summary>Gets or sets the socket construction and transport profile options.</summary>
|
||||
public CultMeshRudpSocketOptions SocketOptions { get; set; } = new();
|
||||
/// <summary>Gets or sets the handshake payload.</summary>
|
||||
public byte[] ConnectPayload { get; set; } = Array.Empty<byte>();
|
||||
/// <summary>Gets or sets the handshake timeout.</summary>
|
||||
public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(1);
|
||||
/// <summary>Gets or sets the handshake polling interval.</summary>
|
||||
public TimeSpan PollInterval { get; set; } = TimeSpan.FromMilliseconds(5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public CultMesh entrypoints.
|
||||
/// </summary>
|
||||
|
|
@ -415,6 +430,82 @@ namespace GameCult.Mesh
|
|||
return CreateRudpClientForPeer(runtimeId, connectionId, peer, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and handshakes a CultNet RUDP client transport for a parsed endpoint.
|
||||
/// </summary>
|
||||
public static CultNetRudpSocketTransportConnection ConnectRudpClient(
|
||||
string runtimeId,
|
||||
uint connectionId,
|
||||
CultMeshRudpEndpoint endpoint,
|
||||
CultMeshRudpClientOptions? options = null)
|
||||
{
|
||||
options ??= new CultMeshRudpClientOptions();
|
||||
var client = CreateRudpClient(runtimeId, connectionId, endpoint, options.SocketOptions);
|
||||
if (!client.ConnectAndWait(options.ConnectPayload, options.ConnectTimeout, options.PollInterval))
|
||||
{
|
||||
client.Dispose();
|
||||
throw new TimeoutException($"Timed out waiting for RUDP client {runtimeId} to connect.");
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and handshakes a CultNet RUDP client transport for a rudp://host:port endpoint.
|
||||
/// </summary>
|
||||
public static CultNetRudpSocketTransportConnection ConnectRudpClient(
|
||||
string runtimeId,
|
||||
uint connectionId,
|
||||
string endpoint,
|
||||
CultMeshRudpClientOptions? options = null)
|
||||
{
|
||||
return ConnectRudpClient(runtimeId, connectionId, ParseRudpEndpoint(endpoint), options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and handshakes a CultNet RUDP client transport from a peer-card contact hint.
|
||||
/// </summary>
|
||||
public static CultNetRudpSocketTransportConnection ConnectRudpClientForPeer(
|
||||
string runtimeId,
|
||||
uint connectionId,
|
||||
CultMeshPeerCard peer,
|
||||
CultMeshRudpClientOptions? options = null)
|
||||
{
|
||||
if (peer == null) throw new ArgumentNullException(nameof(peer));
|
||||
var endpoint = peer.Endpoints.FirstOrDefault(value => value.StartsWith("rudp://", StringComparison.OrdinalIgnoreCase));
|
||||
if (endpoint == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Peer {peer.PeerId} does not advertise a RUDP endpoint.");
|
||||
}
|
||||
|
||||
return ConnectRudpClient(runtimeId, connectionId, endpoint, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and handshakes a CultNet RUDP client transport from the first peer authorized for a Verse role.
|
||||
/// </summary>
|
||||
public static CultNetRudpSocketTransportConnection ConnectRudpClientForAuthorizedPeer(
|
||||
string runtimeId,
|
||||
uint connectionId,
|
||||
CultMeshPeerCatalog peers,
|
||||
CultMeshAuthorityLeaseCatalog leases,
|
||||
string verseId,
|
||||
string role,
|
||||
string? shardId = null,
|
||||
DateTimeOffset? at = null,
|
||||
CultMeshRudpClientOptions? options = null)
|
||||
{
|
||||
if (peers == null) throw new ArgumentNullException(nameof(peers));
|
||||
if (leases == null) throw new ArgumentNullException(nameof(leases));
|
||||
var peer = peers.FirstAuthorized(verseId, role, leases, shardId, at);
|
||||
if (peer == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No authorized RUDP peer for role {role} in Verse {verseId}.");
|
||||
}
|
||||
|
||||
return ConnectRudpClientForPeer(runtimeId, connectionId, peer, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a committer for writing quorum simulation facts to a mesh database.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ peer lookup with the lease catalog, so contact gossip stays separate from
|
|||
trust. The branded RUDP helpers use the same boundary:
|
||||
|
||||
```csharp
|
||||
using var rudpClient = CultMesh.CreateRudpClientForAuthorizedPeer(
|
||||
using var rudpClient = CultMesh.ConnectRudpClientForAuthorizedPeer(
|
||||
"aetheria-client",
|
||||
connectionId: 0x10203040,
|
||||
peers,
|
||||
|
|
@ -128,9 +128,12 @@ using var rudpClient = CultMesh.CreateRudpClientForAuthorizedPeer(
|
|||
```
|
||||
|
||||
`CreateRudpServer(...)`, `ParseRudpEndpoint(...)`,
|
||||
`CreateRudpClient(...)`, and `CreateRudpClientForPeer(...)` delegate to the
|
||||
CultNet RUDP socket transport. CultMesh owns peer/authority ergonomics; CultNet
|
||||
owns packet reliability, resend pressure, and channel semantics.
|
||||
`ConnectRudpClient(...)`, and `ConnectRudpClientForPeer(...)` delegate to the
|
||||
CultNet RUDP socket transport and return the same schema-message-capable
|
||||
transport after handshake. `CreateRudpClient(...)` and
|
||||
`CreateRudpClientForPeer(...)` remain available when a caller intentionally owns
|
||||
the handshake and polling loop. CultMesh owns peer/authority ergonomics;
|
||||
CultNet owns packet reliability, resend pressure, and channel semantics.
|
||||
|
||||
Clients can fetch more peers from known peers:
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ Namespace: `GameCult.Mesh`
|
|||
- `CultMesh.CreateRudpClient(...)`
|
||||
- `CultMesh.CreateRudpClientForPeer(...)`
|
||||
- `CultMesh.CreateRudpClientForAuthorizedPeer(...)`
|
||||
- `CultMesh.ConnectRudpClient(...)`
|
||||
- `CultMesh.ConnectRudpClientForPeer(...)`
|
||||
- `CultMesh.ConnectRudpClientForAuthorizedPeer(...)`
|
||||
- `CultMesh.CreateSimulationFactCommitter(...)`
|
||||
- `CultMesh.CreateGameSession(...)`
|
||||
- `CultMesh.CreateClient(...)`
|
||||
|
|
@ -89,9 +92,12 @@ the world.
|
|||
`CultMesh.ParseRudpEndpoint(...)`, `CreateRudpServer(...)`,
|
||||
`CreateRudpClient(...)`, `CreateRudpClientForPeer(...)`, and
|
||||
`CreateRudpClientForAuthorizedPeer(...)` provide the branded CultMesh path to
|
||||
the native CultNet RUDP socket transport. These helpers parse endpoint contact
|
||||
hints and choose authorized peers; `GameCult.Networking` still owns the RUDP
|
||||
packet codec, session state, resend, fragmentation, and channel semantics.
|
||||
the native CultNet RUDP socket transport. `ConnectRudpClient(...)`,
|
||||
`ConnectRudpClientForPeer(...)`, and `ConnectRudpClientForAuthorizedPeer(...)`
|
||||
also perform the client handshake before returning the same transport. These
|
||||
helpers parse endpoint contact hints and choose authorized peers;
|
||||
`GameCult.Networking` still owns the RUDP packet codec, session state, resend,
|
||||
fragmentation, and channel semantics.
|
||||
|
||||
### Shard Authority
|
||||
|
||||
|
|
|
|||
|
|
@ -379,6 +379,11 @@ The native RUDP socket path also exposes `CultNetRudpReconnectLoop` for
|
|||
caller-owned game or service loops. The caller reports closure, calls
|
||||
`ReconnectIfDue(nowMs)` from its own scheduler, and the shared
|
||||
`CultNetReconnectController` owns retry attempts, delay, and exhaustion state.
|
||||
`GameCult.Mesh` adds discovery-shaped helpers such as
|
||||
`CultMesh.ConnectRudpClientForAuthorizedPeer(...)`, which perform authorized
|
||||
peer lookup and handshake before returning the same RUDP transport. Packet
|
||||
semantics, schema-channel send/receive helpers, and reconnect state still live
|
||||
in `GameCult.Networking`.
|
||||
|
||||
## Important Constraints
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Net;
|
|||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
|
|
@ -2146,7 +2147,18 @@ namespace GameCult.Networking.Tests
|
|||
now.AddSeconds(-1),
|
||||
now.AddSeconds(30)));
|
||||
|
||||
using var client = CultMesh.CreateRudpClientForAuthorizedPeer(
|
||||
using var serverPumpCts = new CancellationTokenSource();
|
||||
var serverPump = Task.Run(() =>
|
||||
{
|
||||
while (!server.Connected && !serverPumpCts.IsCancellationRequested)
|
||||
{
|
||||
_ = server.ReceiveOnce();
|
||||
server.PollResends();
|
||||
Thread.Sleep(TimeSpan.FromMilliseconds(5));
|
||||
}
|
||||
});
|
||||
|
||||
using var client = CultMesh.ConnectRudpClientForAuthorizedPeer(
|
||||
"csharp-cultmesh-rudp-client",
|
||||
connectionId,
|
||||
peers,
|
||||
|
|
@ -2154,22 +2166,31 @@ namespace GameCult.Networking.Tests
|
|||
"local",
|
||||
"schema",
|
||||
at: now,
|
||||
options: new CultMeshRudpSocketOptions
|
||||
options: new CultMeshRudpClientOptions
|
||||
{
|
||||
ResendDelayMs = 25,
|
||||
MaxFragmentBytes = 1024,
|
||||
MaxPendingReliablePackets = 16
|
||||
ConnectPayload = Encoding.UTF8.GetBytes("join"),
|
||||
SocketOptions = new CultMeshRudpSocketOptions
|
||||
{
|
||||
ResendDelayMs = 25,
|
||||
MaxFragmentBytes = 1024,
|
||||
MaxPendingReliablePackets = 16
|
||||
}
|
||||
});
|
||||
serverPumpCts.Cancel();
|
||||
Assert.That(serverPump.Wait(TimeSpan.FromSeconds(1)), Is.True);
|
||||
|
||||
client.Connect(Encoding.UTF8.GetBytes("join"));
|
||||
PumpRudpHandshake(client, server);
|
||||
Assert.That(client.Connected, Is.True);
|
||||
Assert.That(server.Connected, Is.True);
|
||||
|
||||
client.SendSchema(Encoding.UTF8.GetBytes("client-state"));
|
||||
var serverFrame = ReceiveRudpFrame(server);
|
||||
Assert.That(serverFrame.ChannelId, Is.EqualTo("schema"));
|
||||
Assert.That(Encoding.UTF8.GetString(serverFrame.Payload), Is.EqualTo("client-state"));
|
||||
client.SendSchemaMessage(new CultNetSchemaCatalogRequestMessage
|
||||
{
|
||||
MessageId = "csharp-cultmesh-rudp-schema-catalog",
|
||||
Kinds = ["document_payload"],
|
||||
IncludeSchemaJson = true
|
||||
});
|
||||
var request = ReceiveRudpSchemaMessage<CultNetSchemaCatalogRequestMessage>(server);
|
||||
Assert.That(request.MessageId, Is.EqualTo("csharp-cultmesh-rudp-schema-catalog"));
|
||||
Assert.That(request.Kinds, Is.EqualTo(new[] { "document_payload" }));
|
||||
Assert.That(server.Profile.Transports[0].Protocol, Is.EqualTo("rudp"));
|
||||
Assert.That(client.Profile.Transports[0].Protocol, Is.EqualTo("rudp"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue