VUH-2808: Thorn floor investigation and tracing candidate
The zr103 archive establishes that the host's friend actor is already below the arena before the visual-bind refusal and motion fallback. It does not establish which native movement/collision operation first allowed that crossing. This change prepares the next run's tracing and adds the floor regression; it does not claim a recovery fix. AC1 needs the next trace, AC2 remains open, and AC3 has an offline archive control.
Retained evidence
Read the issue's full four-comment thread, including
Tamsin's interim and
the pause/handoff.
Read PC1's authorized handoff and zr103 logs/pilots over SSH/plain SFTP scp into
this worktree's .local/zr103/. PC1 was not modified. Raw inject logs and both
pilot streams are retained through evidence.json.
Original run:
20261025-035211_net_twilight_thorn_bots_dmgtrace600s254b_trace_react_1,
on 5c0a85e. Host log: kh2coop_inject_176172.log; friend:
kh2coop_inject_2608.log. Line numbers below refer to those unchanged files.
| Host line / frame | Observation | Implication |
|---|---|---|
| 9995 / 19709 | Clone (282,-181,388), motion 16 | Last sampled airborne pose before the role. +Y is down. |
| 10000–10006 / 19712 | Friend role 2147483658 begins; capture and queries qualify with matching D48 | Host native coroutine owns the friend actor. |
| 10020 / 19739 | Clone (81,70,430), motion 252 | Below floor while the capture motion is still present. |
| 10032 / 19769 | Clone (134,445,441), motion 252 | Descent precedes bank fallback. |
| 10034–10036 / 19779 | Motion-end query; native cancel returns with postD48=0 | Stored role thread no longer equals actor D48. |
| 10037–10039 / 19779 | role-not-current, then motion 252 absent from clone's banks, fallback 0 | Consequences of the ended coroutine; cannot explain the earlier crossing. |
| 10044–10045 / 19780 | coroutine-replaced, observed D48=0; release | Matches the failed current-role predicate. |
Friend lines 10151–10185 admit the same role and release it at local frame
14995. Its local sample at frame 14969 / approximate mirror cursor 19747 is
(81,236,438), motion 252. Cursor is the boss mirror's sampled display cursor,
not the source frame of the separate actor-owner op4 stream.
Comparison role 2147483650 begins at host frame 17270 and is still current
at retirement frame 17373: D48 equals its admitted thread. It retires because
the host's selected seat actor becomes zero (host-selected-actor, L8566),
then cleanup cancels its coroutine. Neither inject log contains a visual-bound
line for this role. The premise that this earlier role needed/built a visual
binding is not supported by zr103. It uses motion 257, not the later role's 252.
Code path
- Host
SmOwnerHookassociates its selected native actor/coroutine with the owner seat.OwnsPuppetreserves that host actor while the role is current. OwnerPostUpdatereads the host actor's post-native XYZ/facing and emits actor-owner op4. It also emits motion/time with op8.- Client
SmApplyOwneradmits the role and caches op4's four floats inrole.position. For the actual owning seat,OwnerPostUpdatecopies all XYZ/facing to the canonical local actor after native update and clears velocity, carried displacement and the acceleration block. There is no floor query/check or Y exclusion. Authority is host-owned. - EntityHook.cpp's post-update branch
skips generic puppet transform and
VisualPostUpdatewhileOwnsPuppetis true. Spectator visual binding is not required to drive the host's native command actor. Once D48 is cancelled, that reservation fails and generic playback can try binding the still-recorded, now-stale role. SmOwnerCurrentchecks D48 against the stored thread before other role predicates. Thus L10036's D48=0 explains L10037. The diagnostic now includes this predicate's reason in the existing refused-bind row, with no repeat native read and no change to admission.
Generic puppet placement sets actor +18C bit 0x40 to bypass terrain collision,
unless KeepNativeCollision preserves native collision. It saves the preexisting
bit. A current host role skips that maintenance. Whether the bad role inherited
that bit, whether native script movement disabled terrain independently, and
whether grounding was restored at exit remain hypotheses. Motion 0 fallback
is later than the observed failure and is not a causal fix target on this evidence.
Next authorized Thorn run
After Ione's full independent review and the owner's separate PC build/live OK,
set KH2COOP_THORN_FLOOR_TRACE=1 on both game processes at boot, alongside
the existing hit/lifecycle tracing. Batch with VUH-2921 as the lead planned.
No launch, PC build, or desk job is authorized by this document.
[thorn-floor] samples every actor-update boundary during friend-owned object
307 roles, plus 120 frames after the last such role, only in TT 02/22 (decimal
2/34). This covers the analogous 19712–19779 interval without relying on old
process frame numbers. The four stages are:
pre-native: immediately before the original actor update;post-native(or explicitlynative-held): immediately after it;post-owner: after owner pose replay/publication and logical-down pinning;post-puppet: after generic puppet motion/transform and spectator visuals.
Each row names side, native frame, approximate mirror cursor, load/transition,
updating actor identity, role/seat/connection/epoch, expected and observed D48,
cached role XYZ, current XYZ/motion, collision flags, native move/airborne state,
entity vertical velocity, actor velocity/carried displacement/acceleration block,
capture pointer, and the driver's saved collision bit/stream pose. Cached role
metadata never grants authority or causes a new source/thread pointer follow.
Reads reuse the existing ThornHitTrace.hpp per-update admission, including the
clone's current binding/metadata proof. Observation faults cannot skip product
work. The process emits at most 65,536 rows, then one explicit saturation row.
reads bits 0–9 respectively qualify collision, move, airborne, entityVy,
velocity vector, carried vector, six-float acceleration block, D48, capture and motion time.
reads=3FF means all those reads succeeded; zeros with a missing bit are
unavailable. The six acceleration-block values start at actor +A48; they are
the exact block cleared by existing replay, not a new interpretation as XYZ.
Move-state 2 and airborne-sub 0 are native state observations, not proof a
terrain query ran or found the floor. The trace does not call a terrain query.
Join rows by side/load/transition/frame/actor and role. Find the first positive
Y while the peer's authoritative actor crosses the arena: a change between
pre-native and post-native localizes movement to native update; a change
at post-owner identifies host-pose replay; post-puppet identifies generic
stream/visual placement. A change between frames' final and first samples localizes
movement outside the observed actor update, requiring a narrower native trace.
Inspect collision bit 0x40 and move/airborne transitions
against the preceding good friend-owned role and grounded host control. Follow
the 120-frame exit tail to distinguish a native floor snap from continued fall.
If those observations still cannot qualify the native collision operation,
identify the specific operation before choosing a fix; no speculative Y clamp.
Regression and checks
thorn_floor.py is called automatically
at scenario closure, after owned pilots stop. It retains thorn_floor.json and
adds a verdict to both reports. Either seat's sampled canonical playerPos
greater than Y=1000 in TT34 field gameplay fails an otherwise passing scenario.
The first crossing and worst sample survive later recovery. Existing scenario
errors/crash status remain intact, with the floor failure also in the report.
Camera positions, other rooms and explicit non-field loading/cinematic samples
are excluded. A camera RC admitted by the pilot's named297/298/299 rule is judged
using the retained initial room scope, menu255, living canonical player and
offered command. Other in-field event cameras cannot be classified from the
combined event bit: they retain an unavailable coverage interval and its first
line, never a floor pass after recovery. Missing/invalid observations also yield
unavailable. Non-Thorn runs are unaffected.
This reports sampled evidence; it does not continuously monitor between samples,
stop a run immediately, or prove an unsampled/missing seat stayed on the floor.
Offline archive command:
clankie heavy -- python3 -B tools/scenario/thorn_floor.py \
.local/zr103/host_thorn/pilot.jsonl .local/zr103/friend_thorn/pilot.jsonl \
--out .local/zr103/thorn_floor.json
Initial 9d63f084 archive control: expected FAIL, 572 canonical field samples (host290,
friend282). Host max Y=0. Friend first exceeds1000 at pilot line492,
Y=1085.043, about74.9s after fight start; worst Y=87034.672 at line1879.
Output: .local/zr103/thorn_floor.json. The initial seven floor tests pass,
including either seat falling and later recovering, the threshold/direction,
camera/room/event exclusions, unavailable samples and scenario status retention.
The initial all-target Mac build is unsupported: kh2coop_factory_return_counter_test
rejects __fastcall under AppleClang's -Werror; kh2coop_field_objects_test
and kh2coop_resync_test use #define private public under -Werror (also used
by kh2coop_causal_diagnostic_test); and
kh2coop_bridge_test includes Windows.h. These are unchanged baseline targets.
An attempted broad make -k sweep was stopped on lead direction (owned wrapper
PID34558, exit130); local warning workarounds were removed. Final validation is
limited to relevant portable targets and scoped ctest.
Mac builds do not compile the Windows inject DLL. Native trace compilation,
live grounding and recovery acceptance require the separately authorized PC run.
With normal warning settings restored, the focused build passes:
Built target kh2coop_thorn_hit_trace_test
Built target kh2coop_script_owner_command_test
Built target kh2coop_script_mirror_test
Initial 9d63f084 scoped ctest: 7 passed / 1 unsupported on Mac (exit8). The three targets
above pass, as do kh2coop_py_test_thorn_floor, kh2coop_py_test_scenario_fight,
kh2coop_py_test_scenario_launch and kh2coop_py_test_scenario_route_segments.
The unchanged kh2coop_py_test_pilot_thorn fails at import:
AttributeError: module 'ctypes' has no attribute 'WinDLL' in the native
route pilot. It was not bypassed or modified. No further full sweep was run.
Commands/output are retained in .local/vuh2808-focused-build.log and
.local/vuh2808-ctest.log.
clankie heavy -- cmake --build build --parallel 2 --target \
kh2coop_thorn_hit_trace_test kh2coop_script_owner_command_test kh2coop_script_mirror_test
clankie heavy -- ctest --test-dir build --output-on-failure -j 1 \
-R '^(kh2coop_thorn_hit_trace_test|kh2coop_script_owner_command_test|kh2coop_script_mirror_test|kh2coop_py_test_(thorn_floor|pilot_thorn|scenario_fight|scenario_launch|scenario_route_segments))$'
Ione review revision (e9d50194)
Ione requested changes because the original checker discarded playable camera RC samples along with cinematics. The tracing portion had no required finding and is unchanged in this revision.
| Finding | Change | Evidence |
|---|---|---|
| R1: event=true floor crossing could recover and pass | Judge retained states admitted by the native pilot's pure RC rule (297/298/299, same initial six-field room scope, field, menu255, living canonical player). | New controls assert the real pilot rule admits each middle sample; both seats' Y0→1709 during RC→0 fail evaluation and automatic closure, for all three commands. |
| An event-camera interval may lack classification proof | Retain unclassifiedEvents and firstUnclassifiedEvent; coverage stays unavailable after recovery. Event-only pilots are retained under any name. | Both-seat unknown-camera recovery controls; incomplete menu/HP/player/command/scope/field/event metadata; automatic report coverage. |
| Genuine non-gameplay must not become a floor failure | Explicit non-field loading/cinematic samples remain excluded. In-field unclassified cinematics are excluded from height judgment but leave unavailable coverage. | High-Y cinematic/loading controls do not produce firstFall; camera positions and other rooms remain excluded. |
Before the fix, the new controls ran10 test methods with 9 failing subcases:
six admitted RC fall/recover cases (both seats × three commands), two unknown
event/recovery cases and the unclassified in-field cinematic coverage case.
Red output: .local/vuh2808-r2-red.log.
After the fix, scoped ctest passes 4/4 suites: floor12/12, fight4/4,
launch9/9 and route segments7/7. Output: .local/vuh2808-r2-ctest.log and
build/Testing/Temporary/LastTest.log. No native or common source changed, so
the previous focused C++ build/test evidence and Ione's trace review remain
applicable; no broad build or Windows-only test retry was run.
The two original Ione counterexample files also return FAIL with all six
samples judged in each case (including both RC samples). Results:
.local/vuh2808-r2-host_thorn.json and .local/vuh2808-r2-friend_thorn.json.
The zr103 archive still returns expected FAIL, with the same first fall
Y1085.043 and worst Y87034.672, host maxY0. It now judges573 samples (host291,
friend282), including one additional qualified host camera RC. It also reports
17 host and24 friend unclassified camera observations; the known floor failure
still dominates unavailable coverage. Output: .local/zr103/thorn_floor-r2.json.
All commands ran through clankie heavy. AC1's native cause, AC2 recovery,
independent re-review and the authorized PC build/trace remain open.
