diff --git a/docs/cultnet-transport-parity.md b/docs/cultnet-transport-parity.md index 84ec3fd5..af938585 100644 --- a/docs/cultnet-transport-parity.md +++ b/docs/cultnet-transport-parity.md @@ -242,9 +242,11 @@ Current progress: max in-flight frame pressure, and latest-frame cursors. - Kotlin now has a branded `CultMesh` facade matching the C#/TypeScript/Python entrypoint pattern for local nodes, Verse catalogs, peer catalogs, authority - lease catalogs, stream catalogs, and RUDP client/server construction. The - facade delegates to the same owners as the top-level helpers instead of - creating a second authority path. + lease catalogs, stream catalogs, and RUDP client/server construction. Kotlin + also parses advertised `rudp://host:port` endpoints and can build a RUDP + client directly from a peer card contact hint. The facade delegates to the + same owners as the top-level helpers instead of creating a second authority + path. - The remaining parity work is to move the remaining TCP/LiteNetLib/WebSocket service bodies behind their shared transport ports, then broaden RUDP interop into automatic reconnect orchestration and the wider schema-message matrix. diff --git a/packages/cultmesh-kotlin/README.md b/packages/cultmesh-kotlin/README.md index 2308aedf..1f970e63 100644 --- a/packages/cultmesh-kotlin/README.md +++ b/packages/cultmesh-kotlin/README.md @@ -145,11 +145,16 @@ CultMesh.createRudpServer( connectionId = 0x10203040, tuning = CultNetRudpSocketTuning(maxFragmentBytes = 1024), ).use { server -> - CultMesh.createRudpClient( + val peer = CultMeshPeerCard( + peerId = "kotlin-server", + verseId = "local", + endpoints = listOf("rudp://127.0.0.1:${server.localPort}"), + roles = listOf("schema"), + ) + CultMesh.createRudpClientForPeer( runtimeId = "kotlin-client", connectionId = 0x10203040, - remoteHost = "127.0.0.1", - remotePort = server.localPort, + peer = peer, ).use { client -> client.connect("join") check(pumpRudpPairUntilConnected(client, server)) @@ -167,6 +172,10 @@ that already has its own receive loop, use `connectAndWait(...)`; for two same-process transports, use `pumpRudpPairUntilConnected(...)` to drive both sides through the handshake. +For lower-level code, `CultMesh.parseRudpEndpoint("rudp://127.0.0.1:4100")` +returns the host/port pair, and `CultMesh.createRudpClient(...)` accepts either +that parsed endpoint or the endpoint string directly. + Kotlin also exposes the shared reconnect-policy helper for peers that need a portable backoff document without inventing a JVM-only delay dialect: diff --git a/packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt b/packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt index ab4c9a0c..36320c1c 100644 --- a/packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt +++ b/packages/cultmesh-kotlin/src/main/kotlin/org/gamecult/cultmesh/CultMesh.kt @@ -312,6 +312,54 @@ object CultMesh { bindPort = bindPort, tuning = tuning, ) + + fun parseRudpEndpoint(endpoint: String): CultNetRudpEndpoint = cultNetRudpEndpoint(endpoint) + + fun createRudpClient( + runtimeId: String, + connectionId: Long, + endpoint: CultNetRudpEndpoint, + bindHost: String = "127.0.0.1", + bindPort: Int = 0, + tuning: CultNetRudpSocketTuning = CultNetRudpSocketTuning(), + ): CultNetRudpSocketTransportConnection = createRudpClient( + runtimeId = runtimeId, + connectionId = connectionId, + remoteHost = endpoint.host, + remotePort = endpoint.port, + bindHost = bindHost, + bindPort = bindPort, + tuning = tuning, + ) + + fun createRudpClient( + runtimeId: String, + connectionId: Long, + endpoint: String, + bindHost: String = "127.0.0.1", + bindPort: Int = 0, + tuning: CultNetRudpSocketTuning = CultNetRudpSocketTuning(), + ): CultNetRudpSocketTransportConnection = createRudpClient( + runtimeId = runtimeId, + connectionId = connectionId, + endpoint = parseRudpEndpoint(endpoint), + bindHost = bindHost, + bindPort = bindPort, + tuning = tuning, + ) + + fun createRudpClientForPeer( + runtimeId: String, + connectionId: Long, + peer: CultMeshPeerCard, + bindHost: String = "127.0.0.1", + bindPort: Int = 0, + tuning: CultNetRudpSocketTuning = CultNetRudpSocketTuning(), + ): CultNetRudpSocketTransportConnection { + val endpoint = peer.endpoints.firstOrNull { it.startsWith("rudp://", ignoreCase = true) } + ?: throw IOException("Peer ${peer.peerId} does not advertise a RUDP endpoint") + return createRudpClient(runtimeId, connectionId, endpoint, bindHost, bindPort, tuning) + } } data class CultNetFrame(val opcode: Int, val payload: ByteArray) @@ -325,6 +373,10 @@ data class CultNetTransportStats( data class CultNetTransportFrame(val channelId: String, val payload: ByteArray) +data class CultNetRudpEndpoint(val host: String, val port: Int) { + val uri: String get() = "rudp://$host:$port" +} + data class CultNetReconnectPolicy( val schemaVersion: String = "cultnet.reconnect_policy.v0", val policyId: String = "default", @@ -2021,6 +2073,15 @@ data class CultNetRudpSocketTuning( val maxPendingReliablePackets: Int? = null, ) +fun cultNetRudpEndpoint(endpoint: String): CultNetRudpEndpoint { + val uri = URI(endpoint) + if (!uri.scheme.equals("rudp", ignoreCase = true)) throw IOException("RUDP endpoint must use the rudp scheme") + val host = uri.host ?: throw IOException("RUDP endpoint must include a host") + val port = uri.port + if (port <= 0 || port > 65535) throw IOException("RUDP endpoint must include a valid port") + return CultNetRudpEndpoint(host, port) +} + fun cultNetRudpServer( runtimeId: String, connectionId: Long, @@ -3193,11 +3254,19 @@ private fun rudpSocketTransportErgonomicFactoriesCarrySchemaFrames() { connectionId = connectionId, tuning = CultNetRudpSocketTuning(resendDelayMs = 25, maxFragmentBytes = 8, maxPendingReliablePackets = 16), ).use { server -> - CultMesh.createRudpClient( + val endpoint = CultMesh.parseRudpEndpoint("rudp://127.0.0.1:${server.localPort}") + check(endpoint.host == "127.0.0.1") + check(endpoint.port == server.localPort) + val peer = CultMeshPeerCard( + peerId = "kotlin-rudp-sugar-peer", + verseId = "local", + endpoints = listOf(endpoint.uri), + roles = listOf("schema"), + ) + CultMesh.createRudpClientForPeer( runtimeId = "kotlin-rudp-sugar-client", connectionId = connectionId, - remoteHost = "127.0.0.1", - remotePort = server.localPort, + peer = peer, tuning = CultNetRudpSocketTuning(resendDelayMs = 25, maxFragmentBytes = 8, maxPendingReliablePackets = 16), ).use { client -> client.connect("join")