From 447ef2216249b39b18bee152589e4eec9eb10917 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:47:49 -0400 Subject: [PATCH] D173 M-172 cont'd: cross-level replication (Facility) + ruled out two more candidates Facility (-level_34) reproduces an almost identical ~329-unit float vs Dam's ~328, despite completely different ground heights/positions -- strong evidence of a fixed constant rather than a level- or animation-scale-driven effect. Dispatched a local Qwen triage pass on solo_char_load; it raised modelSetAnimTranslationScale/pov as a candidate but correctly self-qualified it as inconsistent with the now-cross-level-confirmed near-constant offset. Own follow-up trace ruled out two more candidates: the puppet's position setup matches the normal guard init path exactly (setsuboffset after init_GUARDdata_with_set_values), and the puppet's first animation's nonzero start frame (~96) is a legitimate firing_animation_groups table constant, not carried-over garbage state. Leading theory now: the ~328-unit elevation is baked into that animation's own keyframe data (root/pelvis joint height), and whatever normally compensates for a chr's third-person render staying grounded isn't reached by this puppet's draw path. Next step (compare a normal guard's raw root-joint gap the same way) is static/no-live-access, logged in findings.md. --- docs/dev/findings.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 5152b287..b8615c57 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -10444,7 +10444,13 @@ crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` A **Status (from §F table):** OPEN — observed, not investigated. **M-87 user playtest: still present** — "Bond floating in the sky / cutscene glitch thing still exists." Related, newly reported: **in some cutscenes Bond spins in circles** (named instance: end of Cradle, when Trevelyan falls) — likely the same actor-position/anim-root class rather than a new bug; logged under D148/D160 (`GRAPHICS-BACKLOG.md`) since it's cutscene-triggered. **M-106 investigation (static only — no build/runtime access in this worktree, see note below):** checked the two most D209-shaped candidates for a puppet Y-position aliasing bug and RULED OUT both. (1) `chrSpawnAtPad`/`chrSpawnAtCoord`/`chrAdjustPosForSpawn` (`src/game/chraction.c:10653-10794`) — the puppet-spawn path a cutscene `guard_play_animation`-style AI cmd would use. `PadRecord.stan` (`src/bondtypes.h:1744-1751`) is a ROM-serialized *pointer* field (`StandTile *stan` at offset 0x28) — exactly the tail-pointer shape D122/D126/D132 warn about — and `tools_pc/d88_emit.py`'s `emit_pad()` zeroes it on conversion with a comment claiming it's "dead-on-load, always set before use." **Verified this claim is TRUE, not a converter bug**: every `PadRecord` literal in the N64 source asset tables (checked `assets/obseg/setup/UsetupdamZ.c:37-51`) initialises `stan` to the literal `0` — the ROM binary never carries a live pointer there on N64 either, so zeroing it on PC is byte-identical behaviour, not a regression. `chrAdjustPosForSpawn` receiving a NULL seed `StandTile*` is therefore the original game's normal case, not a port defect. (2) The anim-root-motion Y-accumulation chain (`modelTickAnim`→`modelSetAnimFrame2WithChrStuff`, `model.c:3060+`, `pos24 = pos34 + rotate(pos)` at `model.c:3322`) was already exhaustively audited for exactly this class of bug during D193/D209 (M-80/M-81) and found self-consistent, with the `padding[]`-union sweep that found D209/D210 explicitly noting `act_anim`/`act_dead` (the union arms active during a played-animation/dead puppet) are "layout-stable" (`porting-notes.md` §A, D210 entry) — i.e. that sweep already covers the cutscene-puppet actiontype, and it came back clean. So the two most probable D209-style aliasing sites for this specific symptom are both cold. **Did not find the actual root cause.** Did not get to: the *separate* Cuba end-credits case (`front.c` `init_menu18_displaycast`/`constructor_menu18_displaycast`, `front.c:7821-8320`+ — a **different**, single-actor "cast roster" display model shared by the intro roster and the credits roster, using `getsuboffset`/`subcalcpos`/`cast_rootpos_smoothed` root-motion smoothing, not the in-level chr path at all) — spot-checked `getsuboffset` (`model.c:684-700`) and it is `#MATCH`-verified clean, but did not trace the smoothing math or confirm this is even the code path the "JB above Natalya" report hits (that report implies two actors on screen at once, which this single-`cast_model` code doesn't obviously support — may be a distinct, not-yet-located two-actor cutscene-puppet path). **Confidence: LOW-MEDIUM that this is the D209-style struct-aliasing family at all** — the two most likely aliasing sites check out clean, which argues *against* the shared-root-cause theory for D173 specifically (unlike D160, where the analogous check also came back clean but for a different reason — no pointer fields to alias in the first place). Next investigator: get a live Cradle (`-level_41`) capture of Trevelyan's chr during the death sequence with a position-logging probe (none shipped yet — would need a new `#ifdef PORT` env-gated trace, not added this session per the no-build-verification constraint), and separately locate the actual two-actor Cuba credits code path (grep for wherever `intro_char_table`/cast display is invoked with two simultaneous models, if it exists — not found this session). **M-141: no new lead this session** (see D243's M-141 entry) — checked whether D243's stale-memory front-end/menu bug could share a mechanism with this puppet-position bug; no code-level link found, kept as an unconfirmed hypothesis to watch for during the next live cutscene capture, not a new suspect to chase on its own. **M-141: Steam Deck playtest confirmation** — user reports Bond floating at level start on both Dam and Facility ("chemical plant"), "but fine when it moves to first person" (matches this row's own framing exactly: player spawn/first-person is correct, only the third-person render-side puppet floats). No new mechanism; corroborates the bug is general across levels, not Dam-specific. **User confirms D292** (Bond sunk into the floor on death) **is the same underlying bug**, a third instance alongside level-start floating and the Cradle cutscene spin — see D292 for the consolidated cross-reference. **M-145 (see D243):** the Dam end-cutscene double-Bond + camera shake is now traced to the same family — the exit script moves Bond's own third-person model via `CHR_BOND_CINEMA` (`chr_try_teleporting_to_pad(0xFD, …)`, `bondconstants.h:4604`) while its rappel animation commands target a model-less BG driver chr; level-start floating (this row), death sinking (D292), and the cutscene double-image (D243) are one "third-person world model transform/lifecycle" bug class. -**M-172 (2026-09-17, this session, autonomous/no live human needed): first-ever quantified, reproducible measurement of the level-start float, isolated to the render side and ruled independent of which animation is attached.** The intro/pre-control puppet cutscene runs unattended (`CAMERAMODE_INTRO`/`SWIRL`, no player input required to progress), so `GE_D243M`'s existing tick/onscreen probes — built for D243, this session — could just be pointed at it directly: launched `-level_33` (Dam) with `GE_D243M=1` only (no X3/X4), captured the whole intro-puppet lifetime headlessly (`scratch/d173/m172_dam_intro_capture.log`, CREATE frame=427 → REMOVE frame=924, 499 `onscreen` samples for the tracked chr). **Result: `prop->pos.y` is rock-steady at 60.3 for the entire window (never changes) — this is the correct ground-level Y — while the `onscreen` probe's actual rendered root Y sits at 384–389 the whole time, a consistent ~324–328 unit gap.** Camera-side fields (`f488pos`/`f3C4`, the tick probe) track `prop->pos` exactly (`20197.7,60.3,16902.1`, correct) — so **the camera/game-logic position is right; only the model's own rendered skeleton root is floating**, and by a near-constant amount. Critically, **the puppet cycles through three distinct animations during this window** (`aidx=61`→`106`→`99`, `actiontype` 23→1→3) **and the Y-gap barely moves (388.4 → 388.8 → 384.3)** — this argues against "wrong/stale animation root motion" (M-160/M-164/M-165's leading theory below) as the primary driver here, since a genuinely animation-root-motion-driven offset should track *which* animation is attached, not stay flat across three different ones. `getsuboffset`/`setsuboffset` themselves are `#MATCH`-verified decomp-verbatim (`model.c:684/705`) — not the bug site. **Leading candidate now: a fixed root-joint/base-transform offset baked into how the puppet's skeleton is set up at spawn** (`solo_char_load`, `bondview2.c:401`, not yet traced in full — a long function, stopped after confirming the accessor functions are clean rather than push further without a specific lead to chase). **Next, concrete:** trace `solo_char_load`'s model/animation initialization order for wherever the root joint's rest-pose offset would normally get zeroed/corrected against `prop->pos`, and compare against how a normal in-level walking guard (whose third-person render is known-correct) initializes the same fields — the flat, animation-independent offset is the strongest lead D173 has had. No build/game-code changes made this pass — measurement only. +**M-172 (2026-09-17, this session, autonomous/no live human needed): first-ever quantified, reproducible measurement of the level-start float, isolated to the render side and ruled independent of which animation is attached.** The intro/pre-control puppet cutscene runs unattended (`CAMERAMODE_INTRO`/`SWIRL`, no player input required to progress), so `GE_D243M`'s existing tick/onscreen probes — built for D243, this session — could just be pointed at it directly: launched `-level_33` (Dam) with `GE_D243M=1` only (no X3/X4), captured the whole intro-puppet lifetime headlessly (`scratch/d173/m172_dam_intro_capture.log`, CREATE frame=427 → REMOVE frame=924, 499 `onscreen` samples for the tracked chr). **Result: `prop->pos.y` is rock-steady at 60.3 for the entire window (never changes) — this is the correct ground-level Y — while the `onscreen` probe's actual rendered root Y sits at 384–389 the whole time, a consistent ~324–328 unit gap.** Camera-side fields (`f488pos`/`f3C4`, the tick probe) track `prop->pos` exactly (`20197.7,60.3,16902.1`, correct) — so **the camera/game-logic position is right; only the model's own rendered skeleton root is floating**, and by a near-constant amount. Critically, **the puppet cycles through three distinct animations during this window** (`aidx=61`→`106`→`99`, `actiontype` 23→1→3) **and the Y-gap barely moves (388.4 → 388.8 → 384.3)** — this argues against "wrong/stale animation root motion" (M-160/M-164/M-165's leading theory below) as the primary driver here, since a genuinely animation-root-motion-driven offset should track *which* animation is attached, not stay flat across three different ones. `getsuboffset`/`setsuboffset` themselves are `#MATCH`-verified decomp-verbatim (`model.c:684/705`) — not the bug site. **Leading candidate now: a fixed root-joint/base-transform offset baked into how the puppet's skeleton is set up at spawn** (`solo_char_load`, `bondview2.c:401`, not yet traced in full — a long function, stopped after confirming the accessor functions are clean rather than push further without a specific lead to chase). **Cross-level replication (same session, same technique): Facility (`-level_34`) reproduces an almost identical offset.** `prop->pos.y` constant at 392.8 for the puppet's whole lifetime (frames 242–374); rendered `onscreen` Y settles at 721.9 — a gap of **~329.1 units**, versus Dam's ~324–328. Two different levels, completely different ground heights (60.3 vs 392.8) and world positions, landing on essentially the **same** offset magnitude — this is strong evidence the bug is a fixed constant somewhere in the puppet's spawn/setup path, not something that scales with level geometry, camera angle, or which animation happens to be attached. Archived alongside the Dam capture (`scratch/d173/m172_facility_intro_capture.log`). + +**Local-delegate (Qwen `triage`) spot-check, one candidate raised then correctly self-qualified as unconfirmed:** dispatched a read-only investigation into `solo_char_load` for the root-joint/animation init order. It flagged `chr_b.c:98`'s `modelSetAnimTranslationScale(model, pov)` (`pov = c_item_entries[body].pov`, `chr_b.c:37`) as a mechanism that scales decoded root-motion translation — but noted itself that this predicts an **animation-dependent** effect, which contradicts the now-two-level-confirmed near-constant offset, so it should not be treated as confirmed. Also honestly flagged that its own read of `solo_char_load` was partially evicted from context mid-session (lower confidence on the parts not directly quoted). Not spot-verified further this session — logged as a ruled-unlikely-but-not-fully-eliminated candidate, not a lead to chase first. + +**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-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.