Skip to content

Confirm creative slot sets with a stats round trip instead of a fixed 400ms wait - #4031

Merged
rom1504 merged 4 commits into
masterfrom
fix/creative-slot-stats-confirm
Sep 6, 2026
Merged

Confirm creative slot sets with a stats round trip instead of a fixed 400ms wait#4031
rom1504 merged 4 commits into
masterfrom
fix/creative-slot-stats-confirm

Conversation

@u9g

@u9g u9g commented Aug 30, 2026

Copy link
Copy Markdown
Member

Problem

On noAckOnCreateSetSlotPacket versions (1.21.9+), setInventorySlot resolves success by sleeping a flat 400ms and treating silence as acceptance. Every successful call pays the full wait. Measured on the 26.1 external suite with a counter: 62 calls × 400ms = 24.8s per run, ~19% of the suite's wall time — and every creative-mode bot pays the same per slot write.

Change

A rejection is a set_slot correction the server sends while processing our packet on its main thread, in receive order. So after writing set_creative_slot, send client_command(request_stats) and wait for the statistics reply: it is generated after our packet was processed, and any correction was already delivered before it on the same ordered connection. Success now resolves in ~1 tick instead of 400ms.

  • Statistics replies carry no transaction id, so in-flight confirms resolve FIFO — matching the server answering requests in packet order.
  • The existing waitTimeout (400ms) now caps the wait for the reply: a server that never answers stats degrades to exactly the old behavior, never a hang.
  • request_stats is sent as { payload: 1 } on respawnIsPayload versions, { actionId: 1 } otherwise (same gate as health.js respawn).

Primitive choice was verified empirically against a vanilla 26.1 server: play-state ping_request is answered on the netty thread in 0–2ms, before earlier queued packets (10/10 runs — it proves nothing about processing), while a statistics reply consistently arrived after a just-sent chat echo (10/10 runs, 7–53ms), confirming main-thread FIFO handling.

Tests

Added an assertion to the creative external test (gated on noAckOnCreateSetSlotPacket): three sequential slot sets must finish under 1s. It fails on master's lib at exactly 1204ms (3×400ms) and passes with this change (~100ms). The creative test drops from ~6.5s to ~1.4s overall; the 26.1 suite loses ~25s.

Ran the creative external test locally on 1.12.2, 1.19.4 and 26.1 — all passing. Old versions are unaffected: the acked path is untouched and confirmServerProcessed is only called from the no-ack branch.

@u9g

u9g commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Pushed a second commit: making slot sets fast exposed a latent one-tick race in resetBlocksToSuperflat — its marker echo proves the fills executed, but command feedback is sent immediately while block changes flush at tick end, so the next test could still see pre-fill blocks (the old 400ms waits were absorbing exactly that tick). The reset now waits, event-driven, until the reset region is actually visible client-side, with a loud 5s deadline. Verified with a digAndBuild→placeBlock ordering that reproduced the stale-dirt failure before the hardening.

@u9g

u9g commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

Third commit: the strict reset check caught a second latent leak, this one a client/server desync. The sign test fired bot.activateBlock unawaited from its setTimeout, letting the use_item_on land in the same server tick as the next reset's fills — the sign editor's out-of-band re-broadcast then races the fill's air change, leaving the client permanently showing a sign the server no longer has (trace: sign re-asserted after the fills, no air update ever). The test now awaits the interact plus the server's editor-open ack before ending. sign+sound green ×3 runs.

@u9g

u9g commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

CI red was mine: the sign fix waited for an editor-reopen ack that only exists on 1.20+ (editable signs) — every group containing a pre-1.20 version timed out on it, which is also exactly why 1.21.3–1.21.6 was the one green group. Amended: the test just awaits the interact, and the reset now self-heals any client/server block desync (stale past 1.5s → two forced /setblock changes make the server rebroadcast, whichever side is wrong). Verified this round on 1.12.2, 1.19.4, and 26.1×2 including the desync-exercising sequence. The 1.21.8-group failure was an unrelated one-off login timeout (three of its four versions fully passed); the digAndBuild pickup race seen once locally is pre-existing and fixed in #4034.

@u9g
u9g force-pushed the fix/creative-slot-stats-confirm branch from fc08c64 to 4e7f371 Compare August 31, 2026 01:32
Comment thread lib/plugins/creative.js Outdated
// statistics packet settles the oldest waiter, and everything written
// before that waiter's request is processed by then.
const statsConfirms = []
bot._client.on('statistics', () => statsConfirms.shift()?.())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

statsConfirms.shift()?.()
What does this do?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed let's do something less weird here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rewrote it, should make more sense now

@u9g
u9g force-pushed the fix/creative-slot-stats-confirm branch from 4e7f371 to 0fe1fcc Compare September 4, 2026 18:17
u9g added 3 commits September 4, 2026 17:11
… wait

On noAckOnCreateSetSlotPacket versions (1.21.9+) setInventorySlot
resolved success by sleeping a flat 400ms and treating silence as
acceptance. That wait is pure latency on every successful call: the
26.1 test suite alone spends 24.8s in it (62 calls), and any creative
bot pays it per slot write.

A rejection is a set_slot correction the server sends while processing
our packet on its main thread, in receive order. A client_command
request_stats round trip on the same connection is therefore proof the
rejection window has passed: the statistics reply is generated after
our set_creative_slot was processed, and any correction was already
delivered before it. Statistics replies carry no transaction id, so
in-flight confirms resolve FIFO, which matches the server answering
requests in packet order. The old waitTimeout now caps how long we
wait for the reply before assuming success, preserving the previous
worst-case behavior on a server that never answers.

Measured on 26.1: ping_request is answered on the netty thread in
0-2ms before earlier queued packets (10/10 runs), so it proves nothing
about processing; request_stats consistently arrives after a
just-sent chat echo (10/10) in 7-53ms. The creative test drops from
6.5s to 1.4s.
The marker echo after resetBlocksToSuperflat's fills only proves the
commands executed: the server sends command feedback immediately but
flushes block changes at tick end, so the client can still hold
pre-fill blocks when the next test starts. The fixed 400ms
setInventorySlot waits at the top of most tests were absorbing that
tick; with server-confirmed slot sets the race surfaces as tests
reading stale blocks (placeBlock seeing a previous test's dirt).

After the echo, wait until every block in the reset region matches its
superflat layer, driven by blockUpdate/chunkColumnLoad events (past 64
changed blocks per section the correction arrives as a chunk resend,
not block updates), with a hard 5s deadline that fails loudly naming
the stale coordinate.
…syncs

The sign test fired bot.activateBlock unawaited from a setTimeout and
resolved immediately, so the use_item_on could land in the same server
tick as the next reset's fills. On 1.20+ (editable signs) destroying a
sign in the tick of its own editor interact makes the editor's
out-of-band sign re-broadcast race the fill's air change: the client
keeps the sign, the server has air, and no correction ever comes -
packet traces show the sign re-asserted after the fills with no air
update for that position. The strict reset check then fails the next
test with 'world not reset ... warped_sign'.

Await the interact, and teach the reset to recover from any such
client/server desync: when a block stays stale past 1.5s, two forced
/setblock changes (bedrock, then the expected block) make the server
rebroadcast it regardless of which side is wrong. Waiting for an
editor-reopen ack instead is not an option - only 1.20+ reopens the
editor on interact, so that wait times out on every older version.
@u9g
u9g force-pushed the fix/creative-slot-stats-confirm branch from 0fe1fcc to fcc0ead Compare September 4, 2026 21:11
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Tests more than 1.5x slower than master (durations are noisy, so this is informational):

   3513ms ->    8688ms  mineflayer_external 1.10.2v nether
  12936ms ->   25796ms  mineflayer_external 1.19v nether
   4955ms ->   14059ms  mineflayer_external 1.21.3v nether
   9092ms ->   14983ms  mineflayer_external 1.21.5v nether
   8689ms ->   14400ms  mineflayer_external 1.21.6v nether

@rom1504
rom1504 merged commit 53049d9 into master Sep 6, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants