Original Kkrieger movement consumer, source-only investigation

Run 20261015-kkrieger-long-input accepted ordinary W for 28.315 seconds, observed +0x15540 change 390→400→410, and observed host/guest release. Its idle images already shifted; directional displacement remains unqualified. The retained samples contain input and counter fields, no player positions. They cannot retrospectively prove collision, completed simulation or movement. No input correction, guest write or performance qualification follows.

This investigation uses the original 131072-byte unpacked image at scratch/runs/20261014-kkrieger-mesh-writer/unpacked-800000.bin, SHA256 42b8f545db0fce60f8f1b22035386c2a6b87653741bbb5ae4a9d24c2b6df76aa. Disassembly is retained alongside it as unpacked-disassembly.txt, SHA256 8b0026617d971d6c82606d4824cc512346515b84861f775f3b238829563dd0b6. Addresses below are original guest virtual addresses; byte offset in this image is VA minus 0x800000. The fixture remains ignored, never committed.

Time and completion

Callback 0x81e8cb obtains its clock through 0x801c05 (IAT slot 0x81f0b8 minus system-object +0x54), subtracts global 0x15289bc, and saves current time at 0x15289b8. This audit does not assert an API name for the unobserved live thunk. At 0x81eab4–0x81eacb it computes signed min(trunc(currentTime/10) - trunc(previousTime/10), 10), with previous time from 0x15289b0. It passes this integer to 0x81dd4c. There is no lower clamp in that sequence; positive values alone enter the simulation loop. After that function returns, 0x81eaeb–0x81eaf0 copies current time into previous time. Long clock gaps therefore do not imply proportionately many simulated steps.

The update first requires nonzero game-object +0x14960 and +0x14a18, then adds the argument to +0x15540 at 0x81dd74, before phase selection and all movement. Phase zero stores its context argument at object+0 at 0x81ddaf. The loop begins at 0x81e2e7 and increments its local substep index at 0x81e5c3, repeating until the index reaches the argument. Object+0 is cleared near the end at 0x81e619.

Thus delta 20 counts requested substeps, not 20 frames, nor 20 completed steps at the observation instant. With the observed forward value ~0.0024999976, 20 applications give nominal direct input of ~0.05 times the forward basis. This is not a measured displacement or a bound: sampling can begin/end inside updates, basis changes and inertia/collision alter the outcome, and accepted input between observations is not reconstructed. The small nominal quantity and ten-step cap are concrete reasons that a long wall-clock hold can produce little visible directional travel. Actual cause remains unmeasured.

Movement, body integration and camera

All offsets here are relative to the live game object at global 0x14a0998.

Field Evidence and meaning
+0x15358 / +0x1535c Accepted forward/strafe input, identified by prior key-handler audit
+0x15308 / +0x15328 Strafe/forward basis vectors generated by 0x805a9d at 0x81de9c
+0x149fc First player physics node; following linked-by-stride nodes use +0x80
+0x14a3c First node +0x40: integrated/current body xyz
+0x14a4c First node +0x50: collision reference/resolved xyz
+0x14a5c First node +0x60: previous integration xyz
+0x14948 Player xyz copied from body +0x14a3c at 0x81e510–0x81e521
+0x15380 Normal camera xyz; copied from player position, then y smoothed/offset
+0x14958 / +0x1495c Pitch/yaw; mouse may change these independently of position
+0x15364 Ground-contact byte set by downward-query/height branch

Each substep zeros a temporary vector then calls 0x80604f twice at 0x81e3c1 and 0x81e3da. The helper implements destination.xyz += basis.xyz * scalar. Strafe uses +0x15308, forward uses +0x15328. Vertical impulse +0x15354 is added and cleared; the downward query 0x81b45a, called at 0x81e37c with (0,-1,0), feeds the ground-height correction and contact flag. At 0x81e49c, 0x819d55 adds the temporary vector to each player's node+0x40 via 0x805f44, advancing by 0x80 while node+0 is zero.

0x81e508 calls 0x81c728 once per substep. Its node-table traversal invokes 0x81c341, which integrates node positions using current/previous vectors, damping at node+0x28 and gravity derived from game +0x15390/+0x15394. At 0x81c4a4 it calls 0x81b5bd(node,0). That function computes movement from node+0x50 to +0x40, calls sweep/query 0x81b45a (0x81b675 and further paths), and uses contact-normal projections/corrections. The success path copies its resolved vector into node+0x50 and, for argument zero, +0x40 at 0x81ba75–8a. The short-motion/blocked path at 0x81baa5 can instead restore +0x40 and +0x60 from +0x50. This is a concrete collision path; there is no retained observation that proves which branch the live player took. The full world/triangle query and node-table membership have not been dynamically traced.

After the physics call, the first body's +0x40 is copied into player +0x14948. That position is then copied to camera +0x15380, with camera y modified by smoothed height +0x1537c and eye offset +0x15348 at 0x81e527–0x81e55e. 0x819e7b uses +0x15380 for the normal camera (mode +0x14964 == 0); the alternate mode uses +0x14948 and an additional basis offset. Idle vertical settling or camera smoothing is consequently distinct from horizontal W movement.

Prepared read-only probe and bounded next test

tools/kkrieger-movement-state.js exports a serializable page installer and nine SHA256 code pins covering update, callback, vector addition, physics, collision, and camera-selection code. It is deliberately not wired into the capture controller yet. A fresh run owner can install it with page.evaluate(installMovementReader, movementPins) and sample with page.evaluate(() => window.__kkriegerMovementRead()) alongside the existing input reader. Remove it afterward with delete window.__kkriegerMovementRead. The installer fails before publication on mismatched code or unreadable state; later reads reject a stopped/replaced/Worker guest, replaced object, or unmapped byte. It invokes no guest API, trace hook, input poll or guest write.

Samples contain requested steps, context, current/previous clock, positions, bases, orientation, contact byte, sectors, camera mode and accepted input. betweenUpdates requires context zero and nonzero current time equal previous time. This avoids equating a newly incremented counter with completed body work or using the small context-clear-before-return window alone as completion. Initialization writes previous time zero at 0x81ecdb; equal zero clocks are explicitly excluded. Equality alone cannot prove a new update, so comparisons require at least two distinct completedTime values with a stable object. It is a source-derived observation guard, not an independent frame counter; use only phase-zero, stable-object samples for movement comparisons. Cooperative page execution makes each synchronous read coherent with respect to guest JS resumption; it can still observe a suspended guest halfway through its update. Such samples are retained, never accepted as completed movement endpoints. Clock wrap/reset must invalidate an interval.

A future fresh thread needs an explicit runtime grant and current canonical source/module/fixture checks. Keep the existing 20-minute absolute owner budget, ordinary 15-second Enter, visual menu/level gates and full 160-second admission. Reuse the existing 20-second idle, W maximum 60-second hold, release observation maximum 60 seconds and unconditional keyup; add this reader to those samples. Do not extend a hold to chase an arbitrary spatial distance. Require at least two distinct completion-clock samples in idle and held phases; if the existing stop condition ends first, report insufficient completed samples. Preserve before/held/released images and all intermediate state.

Compare x/z body displacement and its projection onto sampled forward basis against idle, along with yaw/pitch and camera differences. A nonzero body delta can support displacement; it alone does not prove unobstructed directional control. Stable body with changed camera y supports a camera/settling distinction. Changed body candidate versus collision reference in intermediate observations can motivate a separately bounded collision trace, but cannot identify an exact collision branch. No movement observed at this short simulated duration should remain inconclusive, not an input-delivery defect or a gameplay pass.

Validation and limitations

Six existing/extended input-progress tests pass with the original-image env variable and no skips. The new test independently verifies the complete image and nine spans, executes the serialized reader in a synthetic JS page, verifies distinct body/player/camera fields and byte-for-byte no guest-memory mutation, checks both completion guards independently and initialized zero clocks, rejects NaN/positive or negative Infinity in every position/basis vector, and rejects a modified consumer instruction, unmapped interior byte, replaced object and Worker guest. This tests reader behavior and provenance, not x86 execution or physics accuracy. Node syntax checks and git diff --check pass. No build, full VM, browser, Boat command, new game run, guest modification or public deployment occurred.