Skip to content

record: resync the view when the bot changes servers mid-recording - #70

Open
u9g wants to merge 2 commits into
mainfrom
record-relogin
Open

record: resync the view when the bot changes servers mid-recording#70
u9g wants to merge 2 commits into
mainfrom
record-relogin

Conversation

@u9g

@u9g u9g commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Stacked on #69 (base record-ffmpeg-exit).

A survival recording on 2026-09-08 transferred servers 7 s after it began and kept rendering the lobby it had left, for 42 minutes, until the daemon was OOM-killed.

Mechanism

On a Bungee transfer or dimension change mineflayer runs switchWorld() (blocks.js). When the bot already has a world — the normal case — it takes the in-place branch: it calls bot.world.unloadColumn(x, z) for every column (emitting chunkColumnUnload) and swaps the storage provider, without replacing bot.world. So the recorder's WorldView, constructed with that same world object, still reads the right world — but it has no chunkColumnUnload listener, so it never drops the geometry the server unloaded, and it never meshes the incoming columns.

Change

startRecording now watches the bot for a login immediately followed by a spawn — the signature of a transfer, distinct from a same-world death respawn, which fires spawn with no preceding login. On that spawn it unloads the chunks the viewer is still tracking and calls worldView.init(bot.entity.position) to reload from the current world. Both listeners are removed in stop() and in the ffmpeg-exit teardown from #69.

Scope

This is the harness-side fix for the vendored viewer as it stands. The durable fix is prismarine-viewer handling chunkColumnUnload itself (separate PR against PrismarineJS/prismarine-viewer); once that reaches the vendored build this becomes redundant, and the two are idempotent if both are present (unloading an already-unloaded chunk is a no-op).

Test

Driving the login/spawn handlers with the victim's real event sequence: transfer then death respawn → 1 reset; two transfers → 2; three deaths, no login → 0.

u9g added 2 commits September 8, 2026 10:20
video.done's rejection was swallowed by a bare .catch, so if ffmpeg died
mid-recording -- disk full, a crash, an outside kill -- nothing learned of
it. The tick loop kept rendering at the duty cycle forever, writing into a
closed pipe, and status still reported the recording as running.

An exit before stop() now tears the recording down -- stops the loop,
detaches the bot listeners, disposes the GL context -- and calls a new
onEnd callback so the daemon drops the recording it holds. stop() sets a
flag first so a deliberate stop is not mistaken for a crash, and becomes a
no-op if ffmpeg has already gone. A write in flight when ffmpeg exits
rejects async with EPIPE; a stdin error handler swallows it, since
video.done is the signal that matters.

stats() gains `ended`, carried in status, so a recording that stopped on
its own is visible rather than a loop spinning against a dead encoder.
On a Bungee transfer or dimension change the server unloads the bot's
world and streams a new one. mineflayer unloads every column of bot.world
in place; the viewer's WorldView, which has no chunkColumnUnload listener,
keeps the old geometry and never meshes the new world. A survival
recording that transferred seven seconds after it began kept rendering the
lobby it had left.

The recorder now watches for a login followed by a spawn -- the signature
of a transfer, as against a same-world death respawn, which spawns with no
login -- and on the spawn drops the chunks the viewer still holds and
reloads from the current world.

This is the harness-side fix for the vendored viewer as it stands;
prismarine-viewer handling the unload itself is the durable one.
Base automatically changed from record-ffmpeg-exit to main September 9, 2026 01:04
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.

1 participant