diff --git a/docs/cultnet-transport-parity.md b/docs/cultnet-transport-parity.md
index c8cb034c..5f2c01e4 100644
--- a/docs/cultnet-transport-parity.md
+++ b/docs/cultnet-transport-parity.md
@@ -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
diff --git a/docs/runtime-parity-scope.md b/docs/runtime-parity-scope.md
index 6548bf61..bda1c36d 100644
--- a/docs/runtime-parity-scope.md
+++ b/docs/runtime-parity-scope.md
@@ -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 |
diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs
index e672f8bd..9cc40ca2 100644
--- a/src/GameCult.Mesh/CultMesh.cs
+++ b/src/GameCult.Mesh/CultMesh.cs
@@ -224,6 +224,21 @@ namespace GameCult.Mesh
public CultNetReconnectPolicy? ReconnectPolicy { get; set; }
}
+ ///
+ /// Options for CultMesh-branded connected RUDP client helpers.
+ ///
+ public sealed class CultMeshRudpClientOptions
+ {
+ /// Gets or sets the socket construction and transport profile options.
+ public CultMeshRudpSocketOptions SocketOptions { get; set; } = new();
+ /// Gets or sets the handshake payload.
+ public byte[] ConnectPayload { get; set; } = Array.Empty();
+ /// Gets or sets the handshake timeout.
+ public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(1);
+ /// Gets or sets the handshake polling interval.
+ public TimeSpan PollInterval { get; set; } = TimeSpan.FromMilliseconds(5);
+ }
+
///
/// Public CultMesh entrypoints.
///
@@ -415,6 +430,82 @@ namespace GameCult.Mesh
return CreateRudpClientForPeer(runtimeId, connectionId, peer, options);
}
+ ///
+ /// Creates and handshakes a CultNet RUDP client transport for a parsed endpoint.
+ ///
+ 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;
+ }
+
+ ///
+ /// Creates and handshakes a CultNet RUDP client transport for a rudp://host:port endpoint.
+ ///
+ public static CultNetRudpSocketTransportConnection ConnectRudpClient(
+ string runtimeId,
+ uint connectionId,
+ string endpoint,
+ CultMeshRudpClientOptions? options = null)
+ {
+ return ConnectRudpClient(runtimeId, connectionId, ParseRudpEndpoint(endpoint), options);
+ }
+
+ ///
+ /// Creates and handshakes a CultNet RUDP client transport from a peer-card contact hint.
+ ///
+ 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);
+ }
+
+ ///
+ /// Creates and handshakes a CultNet RUDP client transport from the first peer authorized for a Verse role.
+ ///
+ 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);
+ }
+
///
/// Creates a committer for writing quorum simulation facts to a mesh database.
///
diff --git a/src/GameCult.Mesh/README.md b/src/GameCult.Mesh/README.md
index f08d3066..6b1a4f31 100644
--- a/src/GameCult.Mesh/README.md
+++ b/src/GameCult.Mesh/README.md
@@ -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:
diff --git a/src/GameCult.Mesh/docs/public-api.md b/src/GameCult.Mesh/docs/public-api.md
index 7145d433..6089d331 100644
--- a/src/GameCult.Mesh/docs/public-api.md
+++ b/src/GameCult.Mesh/docs/public-api.md
@@ -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
diff --git a/src/GameCult.Networking/README.md b/src/GameCult.Networking/README.md
index 60d47d99..bb5cedd0 100644
--- a/src/GameCult.Networking/README.md
+++ b/src/GameCult.Networking/README.md
@@ -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
diff --git a/tests/GameCult.Networking.Tests/NetworkingTests.cs b/tests/GameCult.Networking.Tests/NetworkingTests.cs
index b5efa9db..0be77bed 100644
--- a/tests/GameCult.Networking.Tests/NetworkingTests.cs
+++ b/tests/GameCult.Networking.Tests/NetworkingTests.cs
@@ -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(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"));
}