Repository navigation
fix: send the world.input payload verbatim instead of wrapping it - #46
Merged
Merged
Conversation
`WorldInputAsync` wrapped the caller's JSON in `WsMatchInputPayload`, putting
it on the wire as `{"data":"{...}"}`. The `world.input` clause in
`asobi_ws_handler` takes the payload verbatim as the input map and, unlike
`match.input`, does not JSON-decode an inner `data` string: a non-object
`data` degrades to an empty map. Every world input from this SDK therefore
reached the zone empty, and because the ack is recorded before the input is
handled, `world.ack` still advanced and the failure was silent.
Send the caller's JSON as the payload, and move the outbound envelope text
into `WsFrame`, which has no `UnityEngine` dependency, so the wire shape is
covered by the licence-free .NET tests: `seq` beside `payload`, omitted when
unstamped, payload passed through untouched.
The `world.ack` comments on `WsWorldAckPayload` and on the two dispatch tests
claimed the payload fields deserialise directly. `OnWorldAck` hands over the
raw envelope, so `payload` has to be pulled out first; corrected in place.
Taure
force-pushed
the
fix/world-input-payload
branch
from
August 13, 2026 21:59
8891871 to
da39f2c
Compare
The fix in the previous commit had no automated coverage. The three tests added with it exercise `WsFrame.FireAndForget`, which is the envelope builder moved out verbatim; the changed code was in `WorldInputAsync`, and `AsobiRealtime.cs` is not linked into the licence-free .NET project, so reverting the fix left the suite green. Move the payload decision itself into `WsFrame.WorldInputPayload`, where the .NET suite can see it, and pin it: absent input becomes an empty map, an object goes out verbatim, a top-level `data` key is not rewritten. Reverting that decision to the old wrapping body now fails 13 tests. Reject an input that is not a JSON object. The old guard was `IsNullOrEmpty`, so `" "` and `"not json"` spliced onto the wire as invalid JSON while `"[1,2,3]"` and `"null"` reached the server as a badmap. `world.input` carries no `cid`, so none of that can be reported back and the game would fail silently for its whole life; throw `ArgumentException` on the developer's first frame instead. Checked by first non-whitespace character rather than parsed, since this is a per-input hot path. Route the cid-carrying envelope through `WsFrame.Request` as well. It was still hand-built inline in `SendAsync`, which is most of the 24 send methods, and therefore invisible to the licence-free gate. Document the reserved key. `data` is reserved at the top level of a `world.input` payload: the server unwraps it when it is a map, dropping every sibling, and discards the whole input when it is not (widgrensit/asobi#478). `WorldInputAsync`'s comment block is now XML docs so the contract reaches IntelliSense at the call site.
This was referenced Aug 14, 2026
The guard checked that the first non-whitespace character was a brace,
which accepts "{},\"seq\":999" - that splices a top-level seq the client
never stamped into the frame, and world.ack then acknowledges a sequence
number that does not exist, corrupting the prediction loop this change
set out to fix. It also accepted unterminated objects, trailing text and
a non-breaking space, all of which the server's decoder rejects with no
cid to correlate the failure to.
Scan the whole string instead: one balanced object, string-aware so a
brace inside a value does not unbalance it, nothing but JSON whitespace
after it. char.IsWhiteSpace is replaced with JSON's own whitespace set,
since U+00A0 is not legal between tokens.
Taure
force-pushed
the
fix/world-input-payload
branch
from
August 14, 2026 05:31
d0898eb to
5b3801f
Compare
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.
Split out of #45, which is now documentation only. This is the behaviour half:
a wire-format fix, so it carries a
fix:title and cuts its own release.The bug
WorldInputAsyncwrapped the caller's JSON inWsMatchInputPayload, so aworld.inputframe went out as:{"type":"world.input","seq":412,"payload":{"data":"{\"kind\":\"move\",\"x\":600}"}}The server's
world.inputclause (src/ws/asobi_ws_handler.erl) takes thepayload verbatim as the input map:
An inner
datais only honoured when it is itself an object. Unlikematch.input, which JSON-decodes adatastring,world.inputhas no suchclause, so a
datastring falls to_ -> #{}. Every world input this SDKsent reached
handle_input/3as an empty map.It failed silently, in the worst way for anyone building prediction on it:
asobi_zone:apply_inputs/4records the ack before it callshandle_input, soworld.ackkept advancing while nothing in the zone moved.The fix
WorldInputAsyncnow sends the caller's JSON as the payload, and the decisionabout what that payload is lives in
WsFrame.WorldInputPayload:{}, so the frame stays valid JSONArgumentExceptionThat last one is new. The old guard was
IsNullOrEmpty, which let" "and"not json"splice onto the wire as a malformed frame, and"[1,2,3]"and"null"reach the server as valid JSON it rejects with a badmap. TheJsonUtility.ToJsonpath this replaced was structurally immune to all four.world.inputis fire-and-forget with nocid, so the server has nothing tocorrelate a rejection to and none of it could ever be reported back: the game
would just fail silently for its whole life. Failing loudly on the developer's
first frame is the only place the mistake is visible. Checked by first
non-whitespace character rather than parsed, because this is a per-input hot
path.
Coverage
The outbound envelope text lives in
WsFrame, which has noUnityEnginedependency, so the wire shape is asserted by the licence-free .NET tests rather
than only by the Unity smoke run.
SendAsync's cid-carrying envelope was stillhand-built inline, which is most of the 24 send methods; it now goes through
WsFrame.Requestand is pinned too.125 tests pass (
dotnet test Tests/AsobiCore.NET/Asobi.Core.Tests.csproj),15 of them new:
cidandseqare siblings ofpayload, never fields inside it, andseqis omitted entirely when unstamped
world.inputis an empty map; an object is verbatim; a top-leveldatakey is still sent verbatim rather than quietly rewritten"not json","[1,2,3]"," [1,2,3]","null","42"and"\"move\""are all rejected
Mutation-checked: reverting
WorldInputPayloadto the old wrapping body fails13 of them, and dropping
cidfromWsFrame.Requestfails 2. The earlierrevision of this PR was green under the same revert.
Known gap. Reverting
WorldInputAsyncitself, rather than the decision itdelegates to, still keeps the suite green:
AsobiRealtime.csdepends onUnityEngine.JsonUtilityand cannot be linked into the .NET project at all, sono headless test can see the one-line call. The decision is pinned; the
delegation to it is not. Closing that needs a seam over
ClientWebSocketso atest can read the frame that was written, which is a bigger change than this
PR. Recorded in
Tests/AsobiCore.NET/README.mdnext to the reconnect-loop gap.The reserved key
datais reserved at the top level of aworld.inputpayload: the serverunwraps it when it is a map, dropping every sibling key, and discards the whole
input when it is not (
asobi_ws_handler.erl:849-854). Tracked aswidgrensit/asobi#478.
WorldInputAsync's//comment block is now/// <summary>XML docs so thatcontract reaches IntelliSense. This is the one method where a Unity consumer
most needs the rules at the call site: the payload is passed through untouched,
so every constraint on it is the caller's to respect.
Source-breaking
The parameter rename
data->inputJsonbreaks callers that pass it by name(
WorldInputAsync(data: json)). That is a compile error, not a silent change,so it surfaces on the next build rather than at runtime.
Also here
The
world.ackcomments onWsWorldAckPayload,DispatcherTestsandDispatchTestssaid the two payload fields deserialise directly. They do, butOnWorldAckhands over the raw envelope, sopayloadhas to be pulled outbefore
JsonUtilitysees it, or you silently gettick=0, seq=0. Corrected inplace. These are comment-only edits, kept here rather than in #45 so that #45
stays a pure
**.mddiff and does not tripdispatch.ymlorrelease.yml.Not merging
Opened for review only.