Repository navigation
Realtime gameplay is KCP/UDP only (feat/wire/kcp-only, ADR-32 in
rpg-mmo-server backend/docs/NETWORKING.md). The game-server hop builds only KcpTransport; the gateway hop
stays TCP (optionally TLS). Behaviour change, minor bump: a gateway that still answers
enter_world_resp.transport empty or "tcp" is refused by name instead of dialled over TCP.
Pairs with the server leg (game server always listens KCP, registry and gateway always say
"kcp", optional TRANSPORT_KEY).
Netcode side of the prediction-offset fix (fix/wire/prediction-offset). Pairs with the server's
additive SnapshotMessage.ack_applied_tick (wire field 7, protocol 3 peers only, no protocol
version bump) and with com.cuvara.dots' LocalPredictionSystem, which now drives the APIs below.
Still needs sgl-v0.7.0. Measured on the new headless harness; numbers in
Documentation~/PREDICTION.md.
Added
-
NetworkSettings.TransportKey(64 hex chars, the server'sTRANSPORT_KEY; empty =
plaintext datagrams) for the KCP gameplay hop.RegisterNetworking()now hands it to
DefaultTransportFactory(it passednull), as doNetworkBootstrap(new
NetworkBootstrapConfig.TransportKeyfield) and the DOTS Sample (-cuvara-transport-key/
CUVARA_TRANSPORT_KEY). The E2E / WorldView / ReconnectPolicyDemo samples and the PlayMode
live tests readCUVARA_TRANSPORT_KEY. -
TransportKinds.ParseGameplay/TryParseGameplay/RequireGameplayand
TransportKinds.UnsupportedGameplayTransport("unsupported_gameplay_transport"), a permanent
failure for the join loop and the reconnect policy. -
DefaultTransportFactory.CreateGateway(useTls)/CreateGameplay(),HasTransportKey. -
KcpClientSession(internal, Unity-free): the KCP session stateKcpTransportdrives —
ARQ, crypto, reassembly, idle timeout, dead link — so it runs underdotnet test.
KcpTransportexposesConversation,IsEncrypted,DatagramsReceived,FailureReason. -
Tests.
KcpGameplayTransportTests(strict parsing, random non-zero conversation ids,
IPv4 preference, idle timeout, bounded receive buffer, invalid length, matching/wrong key, a
real loopback KCP/UDP echo with and without a key, a dead UDP port) and
GameplayTransportPolicyTests(Editor: the factory never builds TCP for gameplay, a TCP/TLS
assignment is refused before any factory is asked, the refusal is permanent, the join-timeout
message); twoNetworkingRegistrationTestsfor the key. The headless project now also
compilesKcp,KcpCrypto,KcpClientSession,TransportKind(s),NetworkEndpointand runs
KcpCoreTests,KcpCryptoTests,NetworkEndpointTests,KcpGameplayTransportTests
(352 tests). -
ack_applied_tickend to end.Runtime/Protocol/Generated/Wire.csis a byte copy of the
server's regenerated bindings (protoc 29.3).SnapshotMessage.AckAppliedTick, decoded by both
codecs (ack_applied_tickin JSON; absent = 0);ResolvedSnapshot.AckAppliedTickwith a new
seven-argument constructor (the existing ones pass 0);WorldState.AckAppliedTick, kept paired
withAckTickunder the merger's monotonic ack rule and cleared byReset. -
LocalMovePredictor.Reconcile(Vec2, long ackTick, long serverBaseTick, long ackAppliedTick)
andReconcile(Vec3, float, long, long, long): compare the snapshot at server tickTwith
the history atT + offset, where the offset (client base tick of the acked input minus the
server tick that applied it) is the median of the last 15 measured pairs, moved only when the
window has clearly moved. ZeroackAppliedTickbehaves exactly as the three-/four-argument
overloads. Diagnostics:AckTickOffset,LastMeasuredAckTickOffset,AckOffsetSamples,
AckOffsetChanges,AckOffsetUnresolved,SteerIntegralTicks. -
PredictionClockSteering: the clock half ofWorldViewBinder(tick-rate, staleness and
acknowledgement-floor estimators,RoundTripMs, rate feed-forward, measuredTargetLeadTicks,
phase steering) as its own type, moved unchanged, so a consumer that drives a predictor without
the binder steers with the same code:NoteInputSent,SampleTickRate,OnSnapshot,
TargetLeadTicks,Reset.WorldViewBinder.ClockSteeringexposes the binder's instance. -
Headless prediction tests.
Tests~/Headlessnow compiles the predictor, the binder, world
state and the estimators against Shared.GameLogic (-p:SglDir, default the sibling
rpg-mmo-server checkout; CI checks out thesgl-v*tagpackage.jsonpins) and runs the
prediction, steering and binder suites plus the newPredictionHarness/
AckAppliedTickReconcileTests(285 tests).
Fixed
-
KcpTransport.ConnectAsyncdoes not resolve an IP literal through DNS. The gateway
advertises IP addresses; the lookup only added an async hop that resumes on the player loop. -
A cancelled
KcpTransport.ReadFrameAsyncthrows instead of reporting end-of-stream. It returned
null, so the join's own deadline against an unreachable UDP port surfaced as "game server closed
the connection during the join" instead of the KCP/UDP connect-timeout message. Seen live with a
Windows client against a WSL2-NAT stack advertising 127.0.0.1 (Windows localhost forwarding is
TCP-only). Same contract asTcpTransport. -
KCP conversation id was a process-wide counter starting at 1. Now cryptographically
random and non-zero per session. -
KcpTransport's 60 s idle timeout was declared and never enforced. No inbound datagram
for 60 s now fails the session withKCP idle timeout: no UDP datagram from <host:port> ...,
surfaced as aTransportErrorclose with the reason. A KCP dead link fails the same way
instead of closing as if the peer had hung up. -
The KCP stream buffer grew without bound (it drained the whole ARQ receive queue on every
read). It now drains only while no complete frame is buffered and is hard-bounded at one
maximum frame plus one MTU; exceeding it fails the connection visibly and never drops bytes. -
DNS took
addresses[0]while the game server binds IPv4 Any, so a host resolving to::1
first dialled a port nobody listens on. The first IPv4 address is preferred. -
A dead or firewalled UDP port surfaced as a bare cancellation. A join that times out with
no datagram received now throwsNetworkException("KCP/UDP connect timeout to host:port after N s ... check the firewall and the UDP port mapping ... and the transport key"). -
Removed the misleading "send a tiny payload" comment:
ConnectAsyncsends nothing; the
join_tokenframe is the first datagram. -
Reconcile compared against the wrong history entry. It used the snapshot's own tick, but the
server applies an input on the tick that drains it, so the two tick lines are offset by about the
lead minus one plus clock skew, and every tick of it came back as a correction (rubber-banding on
curves, a whole step at starts and stops). Withack_applied_tick: 59.6 Hz server, 13 Hz jittered
sends, lead 6 - mean correction 0.0208 -> 0.0011 on a circle, largest at start/stop 4 steps -> 1;
a 60 Hz jitter-free run corrects nothing (was 38). -
An input tick's history entry was stale.
RecordInputchanged where the tick ended but the
entryAdvancewrote at the start of the tick was kept, giving +1/-1 step pairs at every start
and stop. It is now re-recorded after the input. -
A new tick dropped the unshown part of the step on screen.
Advancereplaced the current
step without carrying its remainder into the render offset (asRecordInputalready did), so the
rendered position jumped in one frame: 2.32x the normal per-frame motion on a curve and 16.67x at
a start or stop at 1 ms frames, now 1.31x / 1.34x. -
The clock steering drooped against a rate difference.
SteerToServerTickwas proportional
only, so a 0.7% slower server left it about a tick off for the session. It now has a clamped,
anti-windup integral term on the integer tick error; every tested ratio (1.02-1.103) settles at
zero with no fed-forward rate, and an in-step clock is left untouched.
Changed
-
Breaking: gameplay is KCP/UDP only.
TransportKinds.Parseis replaced byParseGameplay:
"kcp"(case-insensitive) is accepted, and empty /"tcp"/ anything else is a failure that
names the value — empty no longer means TCP.GatewayClientturns it into a
NetworkExceptionwithServerError = unsupported_gameplay_transport(not retried, ends a
reconnect).GameSessionClient.JoinAsyncrefuses anyMapAssignment.TransportbutKcp
before asking a factory, and asks forTransportKind.Kcponly.TransportKind.Tcp/
TcpTlsare documented as gateway-hop only. -
WebGL:
new KcpTransport(...)throwsNotSupportedException("KCP/UDP gameplay transport is not available on WebGL ...")in a WebGL player. No fallback. -
Docs:
NETCODE.md(hops table, "KCP transport (the only gameplay transport)", WebGL row,
headless coverage),WIRE-PROTOCOL.md,README.md. -
PredictionClockRateTests.WithoutTheRateTheSteeringDroopsByExactlyTheTextbookAmountis now
WithoutTheRateTheIntegralTermRemovesTheDroopToo: it pinned the proportional loop's droop,
which the integral term removes. -
Samples~/DOTSSample:RenderMotionProbereports step per second of frame (median and
worst/median), the frame time of the worst step and the longest frame, so a render jump and a
long frame can be told apart; the[DOTSNet/health]line addsackOffsetandsteerI.