Repository navigation
feat: surface world.tick's zone, frame_seq and kf, and add a resync request - #46
Merged
Merged
Conversation
…equest asobi v0.89.0 stamps three fields on world.tick. This SDK narrows the frame into a typed WorldTick, and that constructor read only `tick` and `updates` - so all three were being silently dropped before any game could see them. A typed model is a filter, and an additive server change reaches userland only if the model is widened to let it through. The important one is `zone`. A player is subscribed to an interest ring of several zones at once, each an independent server process, and frames from two of them have no order relative to each other. A crossing emits op:"r" from the zone being left and op:"a" from the zone being entered, so a game merging every zone into one entity map is last-writer-wins - and when the remove lands last the entity is gone for good, because the server will not re-add something already in its own baseline. Without `zone` exposed, a Dart game could not have kept per-zone state even if it knew to. The field's doc comment says so directly, because that is the part a game gets wrong. `frameSeq` makes loss detectable: unlike `tick` it never skips, whereas `tick` skips on the server's broadcast_interval and is suppressed on a tick that changed nothing, so a gap in it is ambiguous. `kf` marks a complete baseline for a zone, to replace rather than merge, and is documented as safe to adopt even when frameSeq moves backwards - a zone restart resets the sequence while the zone's identity is unchanged, so rejecting a lower value rejects the repair. Adds `resyncWorldZone(zone)`, which asks for a fresh baseline for one zone. One zone per call rather than the ring, since the ring is nine zones by default and the caller already knows which one gapped. The SDK deliberately does NOT detect the gap and resync on the game's behalf, unlike asobi-godot which does. The asymmetry is intentional but worth naming: the Godot SDK forwards the raw payload and holds no state either, so it can auto-ask harmlessly. Here the decision is left to the caller because this SDK's whole shape is typed models handed to a game that owns its own state, and only that model knows whether a gap cost it anything. If we later want parity, the hook is one call in the world.tick case. Decoding is defensive where it has to be. A malformed `zone` reads as absent rather than throwing, because this runs inside a stream handler where a raise takes the subscription down and the game stops receiving ticks entirely. `kf` is true only for a real JSON true, not any truthy value. Coordinates coerce to int, since they index a zone grid and a JSON number may arrive as double. Tests: 7 cases covering all three fields present, all absent reading as null rather than zero, zone [0,0] as a real zone, six malformed zone shapes, kf's strictness, float coordinate coercion, and updates still decoding alongside. They do not read the fixture corpus: the vendored copy still predates the server change and its sync PR is separate, so once that lands dispatch_test covers the same fields end to end. Checks: dart analyze (7 pre-existing infos, identical count on origin/main, none in this code) and dart test 162/162. Not reformatted: CI gates analyze and test but not format, and `dart format` wants to touch 32 of 40 files on this Dart version, which would bury the change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Dart's half of asobi#498 (v0.89.0), which stamps
zone,frame_seqandkfonworld.tick.Why a typed SDK needs a change at all
WorldTick.fromJsonread onlytickandupdates, so all three fields were being silently dropped before any game could see them. A typed model is a filter: an additive server change reaches userland only if the model is widened to let it through. That is the difference between this SDK and asobi-godot, which forwards the raw payload and got the fields for free.The field that matters
zone. A player is subscribed to an interest ring of several zones at once, each an independent server process, and frames from two of them have no order relative to each other. A crossing emitsop: "r"from the zone being left andop: "a"from the zone being entered, so a game merging every zone into one entity map is last-writer-wins - and when the remove lands last the entity is gone for good, because the server will not re-add something already in its own baseline.Without
zoneexposed, a Dart game could not have kept per-zone state even if it knew to. The doc comment on the field says this outright, because it is the part a game gets wrong.frameSeqmakes loss detectable, and the doc explains whytickcannot serve:tickskips on the server'sbroadcast_intervaland is suppressed on a tick that changed nothing, so a gap in it is ambiguous.kfmarks a complete baseline to replace rather than merge, documented as safe to adopt even whenframeSeqmoves backwards, since a zone restart resets the sequence while zone identity is unchanged and rejecting a lower value rejects the repair.resyncWorldZone(zone)Asks for a fresh baseline for one zone. One zone per call rather than the ring, since the ring is nine zones by default and the caller already knows which one gapped.
This SDK deliberately does not detect the gap and resync for you, unlike asobi-godot which does. The asymmetry is intentional and worth naming so it is a decision rather than an oversight: Godot forwards a raw payload and holds no state, so it can auto-ask harmlessly. Here the whole shape is typed models handed to a game that owns its own state, and only that model knows whether a gap cost it anything. If we want parity later, the hook is one call in the
world.tickcase.Defensive decoding, where it has to be
A malformed
zonereads as absent rather than throwing, because this runs inside a stream handler - a raise there takes the subscription down and the game stops receiving ticks entirely.kfis true only for a real JSONtrue, not any truthy value. Coordinates coerce toint, since they index a zone grid and a JSON number may arrive asdouble.Tests
7 new cases: all three fields present; all absent reading as null rather than zero (zero is a real
frame_seqand a real coordinate);zone: [0, 0]as a real zone; six malformedzoneshapes;kf's strictness; float coordinate coercion; andupdatesstill decoding alongside.They deliberately do not read the fixture corpus - the vendored copy still predates the server change and its sync PR (#45) is separate. Once that lands,
dispatch_testexercises the same fields end to end.Checks
dart analyze: 7 infos, identical count onorigin/main, none in this code.dart test: 162/162.Not reformatted. CI gates
analyzeandtestbut notformat, anddart formaton this Dart version (3.12.2) wants to touch 32 of 40 files, which would bury the change in unrelated churn. Worth deciding separately whether to do a one-off format sweep and add the gate.