diff --git a/docs/dev/findings.md b/docs/dev/findings.md index b8615c57..35369674 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -10450,7 +10450,9 @@ crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` A **Own follow-up static trace (same session): the puppet's first animation and its nonzero start frame are both legitimate, table-driven decomp behavior, not garbage/uninitialized state — ruling out two more candidates.** Traced `solo_char_load`'s setup: `init_GUARDdata_with_set_values(...)` (`bondview2.c:629`) then `setsuboffset(bodyModel, &prop->pos)` (`bondview2.c:634`) — position is set correctly, matching the normal guard init path (`chr.c:1719-1720`) exactly, per the earlier delegate spot-check. The puppet's first observed animation (`aidx=61`, `animframe≈96` from its very first rendered frame, *not* 0) comes from the same per-tick body-animation driver every player uses (`bondview2.c:10845-10880`, `firing_animation_groups[group][sub]`, a decomp table indexed by weapon-hold/movement state) — `startframe = fa->y`, a **table constant**, not carried over from stale/uninitialized prior-animation state (ruling out a `bheadAdjustAnimation`-style phase-carryover bug for this specific symptom). This means the ~328-unit elevation is very likely baked into the *animation's own keyframe data* at that starting frame — i.e. the model's root/pelvis joint legitimately sits ~328 units above ground in this pose, and something that should compensate (translate the root down to align with `prop->pos`'s ground-level Y, the way a normal in-level guard chr's third-person render ends up correctly grounded) isn't being applied to this specific puppet's render path. -**Next, concrete:** confirm whether a normal in-level walking/standing guard chr has the *same* raw root-joint-above-`prop->pos` gap when read directly from its model's root joint (bypass `chrTick`'s `getsuboffset`-into-`prop->pos` readback, which by construction hides this gap for a normal chr) — if guards show the same underlying gap, the missing piece is a render-time compensation this specific puppet's draw path skips (a `#ifdef PORT`-fixable port-layer gap, bucket b); if guards genuinely have zero gap at the raw joint level, the puppet's animation/model data selection itself differs from what a real N64 spawn would choose (a data/asset-pipeline lead, not a code lead). Either way this is a **static, no-live-access-needed** next step: add a matching root-joint probe to a normal in-level guard's own `chrTick` (reuse the existing `D243M`/`GE_D193A` probe infrastructure and technique) and diff against this session's puppet numbers. No build/game-code changes made this pass — measurement + trace only. +**M-173 (2026-09-17, same session, autonomous, decisive): the control experiment — normal in-level guards do NOT have this gap. The bug is specific to the player-puppet's own draw/setup path, not a universal chr-rendering issue.** Added a second probe (`chr.c`, `chrTick`'s very first line, before ANY position-update logic runs) logging `prop->pos` for **every** ticked chr, gated on the same `d243mProbeActive()`. Relaunched Dam (`-level_33`, `GE_D243M=1`, no player input needed — background guards animate on their own) and diffed the new "before" line against the existing `onscreen` "after" line for the same chr at the same frame. **Checked four different background guards, different actiontypes/animations (`aidx` 0/40/106), different world positions: all four show a before/after gap of ~0** (a one-time ≤1.6-unit settle on the very first tick or two, then flush thereafter) — nothing remotely like the player puppet's ~328-unit gap. This **rules out** "every chr's root joint sits above its true position by design, normal chr rendering just hides it" (the leading theory after M-172) — **normal chr rendering is correctly grounded.** The bug is isolated to whatever is different about the player's own third-person puppet (`solo_char_load`'s setup, or the player-specific body-render call site, `bondview2.c`) versus a normal guard's `chrTick`/`chrRenderProp` path. Archived: `scratch/d173/m173_guard_compare_dam.log`. + +**Next, concrete, now much more narrowly scoped:** since M-173 confirms guards render correctly and the puppet's *position* setup (`setsuboffset` after `init_GUARDdata_with_set_values`) was already confirmed identical to a guard's, the remaining candidate is the player-puppet-specific **draw** call — find where the player's own `bodyModel` gets rendered (distinct from `chrRenderProp`'s generic path guards use — `bondview2.c`'s player-specific draw site, near the `D243M: draw`/`f488write` probes already in this file, `~10500-10560`) and check whether it's missing a compensation step (a base-height subtraction, or a different root-joint index) that `chrRenderProp` applies for every other chr. This is the tightest lead D173 has ever had — a single, specific code-path comparison, not a broad theory search. **M-160/M-164/M-165 (2026-09-17, see D243): the shared mechanism is now understood in much more depth, though still not confirmed as the actual PC/N64 divergence.** D243's investigation this session traced the general pattern this whole family plausibly shares: `chrUpdateAnim` (`chr.c:2398-2424`) doesn't move a puppet's world position directly — it advances whatever animation is currently attached (`modelTickAnim`), then reads the model's root-joint transform back into `prop->pos` via `getsuboffset` (`model.c:684`). When something else (a teleport, a spawn, a death transition) moves the puppet's *logical* position without also resetting/updating its *animation state* to match, the next `chrUpdateAnim` call overwrites the new position with wherever the stale animation's root motion happens to be — producing exactly "floats/sinks/shakes relative to where it should be, in a way that tracks the animation, not the intended position." This is a strong candidate mechanism for D173's level-start float (an animation not yet settled/attached correctly at spawn) and D292's death-sink (the death-pose transition possibly not resetting the model's root anchor the way the collision/death logic expects) — **but D243's own parallel investigation (M-165) found this same mechanism may be entirely decomp-faithful**, with the actual PC-specific divergence being *which phase* of an animation a chr happens to be on at the moment of transition (inherited from real-time-dependent prior gameplay, not deterministic) rather than a code-path difference. **Do not treat this as solved** — it reframes what to look for (an animation-identity/phase mismatch at the transition moment, not a position-write bug per se) and gives a concrete technique (`GE_D193A`'s `aidx=` reverse-resolve + a `model->animframe1` phase read, see D243 M-166) for whoever gets live access to test D173/D292 the same way D243's `GE_D243M`/`GE_D243X2` probes now do. diff --git a/src/game/chr.c b/src/game/chr.c index b262bab4..f0c6f819 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -2461,6 +2461,30 @@ s32 chrTick(PropRecord *prop) #ifdef PORT d193aSample(chr, model); + + /* D173 M-173: does a normal (non-player-puppet) chr's prop->pos, read + * BEFORE this tick's position-update dispatch runs, differ from what + * the existing D243M: onscreen probe logs AFTER it (chr.c:~2705, at + * after_position_update:)? For the player's own third-person puppet, + * M-172 (findings.md D173) measured a fixed ~328-unit gap between + * prop->pos and the rendered root that holds across levels and + * animations. This applies the exact same before/after comparison to + * EVERY ticked chr (not just the player), gated on the existing + * d243mProbeActive() (fires during INTRO/SWIRL/POSEND, silent in normal + * FPS play) -- if ordinary guards show the same before/after gap, the + * compensation a normal chr's render is missing is universal (a + * port-layer/render-path gap); if guards show ~zero gap, the puppet's + * animation/model selection itself is what differs. Diagnosis only. */ + { + extern int d243mProbeActive(void); + extern int d243mGetFrameCounter(void); + if (d243mProbeActive()) + { + osSyncPrintf("D243M: prebefore frame=%d chr=%p pos=%.1f,%.1f,%.1f\n", + d243mGetFrameCounter(), (void *) chr, + (double) prop->pos.f[0], (double) prop->pos.f[1], (double) prop->pos.f[2]); + } + } #endif if ((!(chr->chrflags & CHRFLAG_HIDDEN)) || (chr->chrflags & CHRFLAG_00040000))