From d9c77056544370ffae0640ef4208ee22b64d67b8 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:49:19 -0400 Subject: [PATCH] D243 M-162..M-166: corrected decisive experiment + deep root-cause tracing Two third-party reviews caught a real flaw in the earlier M-161 negative experiment: it restored prop->pos AFTER chrTick() returned, but render_pos/field_488.pos are computed INSIDE that same call -- so M-161's "no change" result never actually tested M-160's overwrite hypothesis. Verified this directly against the capture data (a clean period-4 same-frame before/after-chrTick offset proves the lane-advance happens entirely inside chrTick) before accepting the review. M-162: corrected save/restore placement (chr.c), Z-only. Live playtest confirmed the mechanism -- Z went smooth in the log -- but the visible glitch persisted because that capture's chr cycled on Y instead. M-163: widened the experiment to the whole prop->pos vector. Built and smoke-tested; not yet live-playtested (exe was busy/user stepped away) -- this is the ready-to-test build for the next session. M-164/M-165/M-166 (static tracing, no build needed): chrUpdateAnim reads the model's root-joint position back into prop->pos after advancing whatever animation is attached (getsuboffset) -- not an arbitrary overwrite. The cutscene's teleport (AI_TRYTeleportingChrToPad) re-anchors the root joint but never touches animation state; the real rappel animation commands target a separate invisible driver chr (M-145), not the visible one. Traced the Dam exit script (UsetupdamZ.c) directly and found no animation-set command for Bond anywhere in it -- his head animation gets forced back to an idle/walk loop by bondviewFrozenMoveBond's own tick, but the *phase* within that loop is inherited, not reset. This raises a real possibility that the mechanism is decomp-faithful and the actual PC/N64 divergence is upstream of the cutscene code entirely (which animation phase Bond happens to be on when the cutscene grabs him). Extended D243M: onscreen with aidx=/animframe= (reusing GE_D193A's reverse-resolve technique) so the next live capture can test this directly. Docs updated throughout (findings.md, findings-index.csv, GE-ENV-PROBES.md); HANDOFF.md refreshed with full session state (local, gitignored). --- docs/dev/GE-ENV-PROBES.md | 3 +- docs/dev/findings-index.csv | 2 +- docs/dev/findings.md | 32 ++++++++++++++- src/game/chr.c | 82 +++++++++++++++++++++++++++++++++++-- 4 files changed, 113 insertions(+), 6 deletions(-) diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 1776e60e..793fc97f 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -45,7 +45,8 @@ Two classes: | `GE_D160` | `src/boss.c:744`, `src/game/bondview2.c:791`, `src/aicommands.def:10055,10181,10236,10242,10247` (all `#ifdef PORT`) | Dam exit-cutscene (D148/D160) diagnostic: trace points in `bossReturnTitleStage`, `bondviewSetCameraMode`, and AI cmds `EndLevel`/`exit_level`/`CameraLookAtBondFromPad`/`CameraSwitch`. **Investigation in progress** — user owes a `GE_D160=1` Dam run. | **live** | | `GE_D243` | `src/game/front.c` (`menu_init`, cached `s_d243`) | Per-menu-transition trace with a local per-tick frame counter (D243 stale-DL-burst screen identification). Counter is NOT numerically identical to the port's internal frame count — cross-reference by elapsed offset. | **live** (D243) | | `GE_D243CAM` | `src/game/bondview2.c` (`bondviewFrozenCameraTick`, three `CAMERAMODE_POSEND` branches) | Dumps eye/look-at positions (+theta/verta on the `gBondViewCutscene` branch, +dt/angle/pad on the orbit branch) per call. **Found M-143**: the `gBondViewCutscene` branch fires and its look-at (`field_3C4/8/C`) sweeps through a filter catch-up after the cutscene warp — the shake mechanism. | **live** (D243) | -| `GE_D243M` | `src/game/bondview2.c` (`solo_char_load`, `bondviewRemovePlayerBody`, `playerTick`; cached `d243mEnabled`) + `src/game/objecthandler.c` (`drawjointlist`, both region variants) + `src/game/chr.c` (`chrTick`, `after_position_update:`) | M-151/M-154/M-159 probe for the Dam-abseil "frozen Bonds + shake": `D243M: CREATE/REMOVE` at both `bodyModel` sites; `D243M: tick` (idx/model/**propptr/chrptr**/cam modes/`prop->pos`/`field_488.pos`/`field_3B8`/`field_3C4-8-C`/**rp** = render_pos pointer, **never dereferenced** — M-154 fix 2: tick-time `render_pos` holds arbitrary stale slot contents until the first draw pass, so the pre-view root translation is logged by `chrdraw` only) while the model exists or POSEND/INTRO is active; `D243M: draw` per on-screen player render submission; `D243M: chrdraw` — M-154 per-frame model-draw census, one line per CHR render submission (Model* + raw root translation) while a scripted camera mode (POSEND/INTRO/SWIRL/FADESWIRL) is active; a second Bond instance = extra lines with *distinct* model pointers at the same frame (same model is drawn twice/frame — two passes — dedupe by (frame, model)); `D243M: onscreen` (M-159, `chr.c`) — one line per `chrTick` call while the probe is active: `vis=` `posIsOnScreen`'s result (the gate that controls whether `render_pos` refreshes that tick) + the live `prop->pos`, to test whether freeze/teleport boundaries in the tick/chrdraw data line up with `vis` flipping 0→1. Silent during FPS play. Note: `field_3B8` x/z read at 10× world units in the smoke capture — not plain world position. | **live** (D243) | +| `GE_D243M` | `src/game/bondview2.c` (`solo_char_load`, `bondviewRemovePlayerBody`, `playerTick`; cached `d243mEnabled`) + `src/game/objecthandler.c` (`drawjointlist`, both region variants) + `src/game/chr.c` (`chrTick`, `after_position_update:`) | M-151/M-154/M-159 probe for the Dam-abseil "frozen Bonds + shake": `D243M: CREATE/REMOVE` at both `bodyModel` sites; `D243M: tick` (idx/model/**propptr/chrptr**/cam modes/`prop->pos`/`field_488.pos`/`field_3B8`/`field_3C4-8-C`/**rp** = render_pos pointer, **never dereferenced** — M-154 fix 2: tick-time `render_pos` holds arbitrary stale slot contents until the first draw pass, so the pre-view root translation is logged by `chrdraw` only) while the model exists or POSEND/INTRO is active; `D243M: draw` per on-screen player render submission; `D243M: chrdraw` — M-154 per-frame model-draw census, one line per CHR render submission (Model* + raw root translation) while a scripted camera mode (POSEND/INTRO/SWIRL/FADESWIRL) is active; a second Bond instance = extra lines with *distinct* model pointers at the same frame (same model is drawn twice/frame — two passes — dedupe by (frame, model)); `D243M: onscreen` (M-159/M-162/M-163/M-166, `chr.c`) — one line per `chrTick` call while the probe is active: `vis=` `posIsOnScreen`'s result (M-159's frustum hypothesis, REFUTED by M-160) + the live `prop->pos` + `actiontype=`/`anim=` (M-160/M-164: which of `chrTick`'s branches fired, the attached-animation pointer) + `aidx=`/`animframe=` (M-166: the animation reverse-resolved to its `animation_table_ptrs1` index, GE_D193A's technique, + its current playback phase — tests whether the visible chr's animation *phase* at cutscene-entry, not just its identity, is what differs from N64) + `x2active=`/`x2raw=` (M-162/M-163, gated on `GE_D243X2`: whether the decisive save/restore experiment ran this tick, and the raw pre-restore position it overrode — the whole `prop->pos` vector as of M-163, not just Z). Silent during FPS play, silent on the `GE_D243X2` fields unless that env var is also set. Note: `field_3B8` x/z read at 10× world units in the smoke capture — not plain world position. | **live** (D243) | +| `GE_D243X2` | `src/game/chr.c` (`chrTick`, gated additionally on `d243mProbeActive()`) | **Not a probe — a decisive behavioral experiment, off by default, NOT a fix.** M-162/M-163: saves `prop->pos` right before `chrTick`'s position-update dispatch (`chr.c:2541-2617`) runs, restores it at `after_position_update:` — before the render-matrix computation later in the same tick can see the cycled value (unlike M-161, which restored too late to have any effect). Tests whether `chrTick`'s own animation-position write (M-160/M-164) is what produces the Dam-abseil shake. M-162 was Z-only (confirmed the mechanism on that axis, but the visible glitch persisted because that capture's chr cycled on Y instead); M-163 widened it to the whole vector. Always pair with `GE_D243M=1` so the `x2active=`/`x2raw=` fields on `D243M: onscreen` show whether/what it overrode — judge by those numbers, not by eye (a lesson from M-161's inconclusive-by-eye negative). Revert once the experiment reports; do not ship. | **live, test-only** (D243) | | `GE_D193` | `src/game/frametiming.c` (`waitForNextFrame`, `#ifdef PORT`) | Per-wall-second line: `render=` rendered frames, `sim=` sim ticks delivered (Σ of the `deltaFrames`/`g_ClockTimer` fed downstream), `clamped=` frames whose catch-up hit `FRAMETIMING_PORT_MAX_CATCHUP`, `maxraw=` largest pre-clamp delta. Healthy steady state = `render=30 sim=60 clamped=0 maxraw=2` (measured M-80, all levels idle). Use to check whether the wall-clock timer under-advances under real AI load (D193). Cheap; safe for a full playtest. | **live** (D193) | | `GE_D193A` | `src/game/chr.c` (`chrTick` → `d193aSample`, plus `d193aMoved` in `chrUpdateAnim`; `#ifdef PORT`) | Per-wall-second per-ticked-chr locomotion telemetry (D193/D209): `chr=` chrnum, `act=` ACT_TYPE, `speed=` world-units travelled/s, `gtd=` g_GlobalTimerDelta, `tier=` `act_gopos.unk59` SPEED tier (0 walk / 1 run / 2 sprint; `-1` when not ACT_GOPOS), `gspd=` `act_gopos.speed` (**a turn rate, not a travel rate** — 0 means running straight), `typ=` PROP_TYPE (3=CHR, 6=VIEWER), `tick=`/`move=`/`frz=` chrTick entries vs `chrUpdateAnim` calls vs freeze-gated (move « tick = the chr is being visibility-culled out of its own locomotion; `move=0` at high `speed` = WAYMODE_MAGIC), `hid=` CHRFLAG_HIDDEN, then the root-motion scalars `scale` / `xlscale` / `animrate` / `playspeed` / `mspeed` (model->speed) / `endframe` / `anim` ptr / `aidx=` that pointer reverse-resolved to its `animation_table_ptrs1` index (40 walking, 41 sprinting, 42 running, 107 walking_unarmed — map via `src/game/initanitable.c`). `GE_D193A_CHR=` restricts to one chrnum. **This probe found D209**: Cradle showed `tier=1` (RUN) with `aidx=40/107` (walk anims) on every sample. | **live** (D193/D209) | | `GE_D193B` | `src/game/model.c` (`sub_GAME_7F06D3F4`, `#ifdef PORT`) | Rate-limited (1/97) raw anim root-motion decode trace (D193): `joint=` / `flip=` / `base=` mtx channel index / `frame=` / `tmp=` decoded s16 translation triple from the anim bitstream / `angle=` / `anim` ptr / `bd=`+`bs=` the D32 u32 bitDescriptors/bitStream addresses / `njoints`. M-80 Facility: `base=0`, `angle=0` **always** (root-motion facing delta never non-zero — top lead), `tmp` ≈ (±small, ~1086 hip-height, ±small). Compare `tmp`/`angle` magnitudes against a clean N64 decomp build of the same anim+frame. | **live** (D193) | diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index c11f41b2..57180fba 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -139,7 +139,7 @@ D239,?,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass ( D240,OPEN,"Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).","OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment." D241,OPEN,Player gunshot SFX goes silent under some conditions —,"OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug." D242,?,Player-death sequence UX regression vs N64 original:,NOT A PORT-LOGIC BUG as far as static analysis can tell. -D243,FIXED,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):","PARKED (M-161, diminishing returns after ~15 session-passes; resume only with a new lead —" +D243,OPEN,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):","OPEN, M-166 build ready for next live session (M-163:" D245,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — REOPENED, M-142 headless probe found the tc-shift k measurably flips during ordinary view rotation (new root-cause evidence); an opt-in GE_D245_FIXEDSHIFT candidate is build-verified but needs…" D246,OPEN,"Tiny pixel strips at the left and right edges of the screen, ""like overscan"" —","OPEN — one candidate ruled out (M-116 follow-up), root cause not found." D247,RESOLVED,Black bars top and bottom of screen —,"NOT A BUG — confirmed faithful, unmodified N64 decomp behaviour, no code change made." diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 863b2cf3..b3af85b6 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -546,7 +546,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D240 | **Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).** — full `## D240` entry at file tail | **OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment.** Confidence LOW-MEDIUM that this is a *new* bug distinct from the D155/D186/D193/D204/D209 timing-keystone family (all of which gate the identical `deltaFrames→speedgraphframes→g_ClockTimer→g_GlobalTimer` chain this SFX trigger consumes).… | | D241 | **Player gunshot SFX goes silent under some conditions — reported on Bunker with the alarm sounding, "or some other factor" — but only for PP7 and AK47, Klobb…** — full `## D241` entry at file tail | **OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug.** Recommend folding this repro into D207 (or closing D241 as a duplicate once D207 is reopened) rather than tracking separately.… | | D242 | **Player-death sequence UX regression vs N64 original: PC requires an LMB click to proceed to the mission-failed/file-select screen; user recalls N64 auto-plays…** — full `## D242` entry at file tail | **NOT A PORT-LOGIC BUG as far as static analysis can tell.** Confidence HIGH that the LMB-click skip path is faithful original behavior (Z_TRIG-mapped, exists unmodified in `bondview2.c`), not a defect to remove.… | -| D243 | **Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): the camera violently shakes/"spazzes" during each of the scripted camera-angle…** — full `## D243` entry at file tail | PARKED (M-161, diminishing returns after ~15 session-passes; resume only with a new lead — see below) — M-144: CONFIRMED PC-only (user, direct N64 comparison) — real N64 does not show this shake. Mechanism (M-143): the `gBondViewCutscene` look-at branch reads `field_3C4/8/C`, a leaky-integrator low-pass filter of Bond's real position that isn't reset when the scripted cutscene warps him, producing a ~1s look-at snap. Filter math is decomp-exact and `g_ClockTimer`-scaled, so the bug must be upstream: either the position warp itself is wrong/bigger on PC, or a call-ordering desync around the `CAMERAMODE_INTRO` transition. M-153: live `GE_D243M` capture (full Dam → abseil; `scratch/d243/m153_dam_abseil_capture.log`) REFUTES the double-instance hypothesis (single model, one draw/frame, no remove/recreate window) and shows the real divergence: the model's rendered position (`field_488.pos` = render readback) freezes on dam-surface points then locks into a strict period-4 cycle through four fixed world points, while `prop->pos` executes the smooth scripted void sweep — the lagged filter (`field_3C4`) chasing that cycle is exactly the observed shake. Next: code dive into `chrTick`→`render_pos` for what produces the four fixed points; D173/D292 are the same family. D146 still fully decoupled (zero hits, 4 runs). M-154: user sharpened the symptom (one new frozen Bond per shot; old copies persist); log re-read corrects M-153 — in phase 3 `prop->pos` itself cycles A,A,B,C through three z-lanes while y falls smoothly, and the rendered model tracks it (per-lane pose offsets), so it teleports every frame rather than freezing; H1 (multiple Model instances) vs H2 (framebuffer accumulation / subcam ghosts) now discriminated by a new `D243M: chrdraw` per-frame model-draw census in `drawjointlist` + extended tick fields (propptr/chrptr/rp); probe crashed twice inside its own `render_pos` read (stale slot contents, not a uniform tombstone) — final fix: tick probe never dereferences render_pos (raw= moved to chrdraw-only where the game guarantees validity); awaiting re-capture. M-159 (static code-dive, no build): proposed `chrTick`'s frustum-visibility gate as the mechanism — REFUTED by M-160's live capture (`vis=1` throughout the observed freeze). M-160 (live `D243M: onscreen` capture, user playtest): found `chrTick` overwriting `prop->pos` with a stale/stepped value every tick via its own `chrUpdateAnim`/`modelTickAnim` animation-position logic (`chr.c:2578-2616`, no POSEND/cutscene check), downstream of and competing with the AI script's smooth write to the same field. **M-161 (decisive experiment, live playtest): NEGATIVE.** Neutralizing M-160's overwrite (save/restore `prop->pos` around `chrTick` during POSEND, `GE_D243X` env-gated hack, reverted after) produced **no visible change** — user: "still happened, looked the same as it always does." M-160's mechanism is real but is not the (or not the whole) visible cause. **Parked** per user's call (diminishing returns after ~15 session-passes) — see D148 umbrella; resume only with a specific new lead. | +| D243 | **Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): the camera violently shakes/"spazzes" during each of the scripted camera-angle…** — full `## D243` entry at file tail | OPEN, M-166 build ready for next live session (M-163: full-vector suppression built/smoke-tested, awaiting a live playtest verdict; M-164/M-165/M-166 static work reframed the open question as "is the visible chr's inherited animation *phase* at cutscene-entry PC/N64-divergent" — not yet answerable without a live capture; M-166 extended the probe with `aidx=`/`animframe=` so one more playtest can answer it) — M-144: CONFIRMED PC-only (user, direct N64 comparison) — real N64 does not show this shake. Mechanism (M-143): the `gBondViewCutscene` look-at branch reads `field_3C4/8/C`, a leaky-integrator low-pass filter of Bond's real position that isn't reset when the scripted cutscene warps him, producing a ~1s look-at snap. Filter math is decomp-exact and `g_ClockTimer`-scaled, so the bug must be upstream: either the position warp itself is wrong/bigger on PC, or a call-ordering desync around the `CAMERAMODE_INTRO` transition. M-153: live `GE_D243M` capture (full Dam → abseil; `scratch/d243/m153_dam_abseil_capture.log`) REFUTES the double-instance hypothesis (single model, one draw/frame, no remove/recreate window) and shows the real divergence: the model's rendered position (`field_488.pos` = render readback) freezes on dam-surface points then locks into a strict period-4 cycle through four fixed world points, while `prop->pos` executes the smooth scripted void sweep — the lagged filter (`field_3C4`) chasing that cycle is exactly the observed shake. Next: code dive into `chrTick`→`render_pos` for what produces the four fixed points; D173/D292 are the same family. D146 still fully decoupled (zero hits, 4 runs). M-154: user sharpened the symptom (one new frozen Bond per shot; old copies persist); log re-read corrects M-153 — in phase 3 `prop->pos` itself cycles A,A,B,C through three z-lanes while y falls smoothly, and the rendered model tracks it (per-lane pose offsets), so it teleports every frame rather than freezing; H1 (multiple Model instances) vs H2 (framebuffer accumulation / subcam ghosts) now discriminated by a new `D243M: chrdraw` per-frame model-draw census in `drawjointlist` + extended tick fields (propptr/chrptr/rp); probe crashed twice inside its own `render_pos` read (stale slot contents, not a uniform tombstone) — final fix: tick probe never dereferences render_pos (raw= moved to chrdraw-only where the game guarantees validity); awaiting re-capture. M-159 (static code-dive, no build): proposed `chrTick`'s frustum-visibility gate as the mechanism — REFUTED by M-160's live capture (`vis=1` throughout the observed freeze). M-160 (live `D243M: onscreen` capture, user playtest): found `chrTick` overwriting `prop->pos` with a stale/stepped value every tick via its own `chrUpdateAnim`/`modelTickAnim` animation-position logic (`chr.c:2578-2616`, no POSEND/cutscene check), downstream of and competing with the AI script's smooth write to the same field. **M-161 (decisive experiment, live playtest): NEGATIVE.** Neutralizing M-160's overwrite (save/restore `prop->pos` around `chrTick` during POSEND, `GE_D243X` env-gated hack, reverted after) produced **no visible change** — user: "still happened, looked the same as it always does." M-160's mechanism is real but is not the (or not the whole) visible cause. **Parked** per user's call (diminishing returns after ~15 session-passes) — see D148 umbrella; resume only with a specific new lead. | | D245 | **IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — "looks almost exactly like skybox" (M-116, user…** — full `## D245` entry at file tail | OPEN — REOPENED, M-142 headless probe found the tc-shift `k` measurably flips during ordinary view rotation (new root-cause evidence); an opt-in `GE_D245_FIXEDSHIFT` candidate is build-verified but needs a live A/B, not shipped as default.… | | D246 | **Tiny pixel strips at the left and right edges of the screen, "like overscan" — look like raw pixels, not 3D graphics (M-116, user QA report).** — full `## D246` entry at file tail | **OPEN — one candidate ruled out (M-116 follow-up), root cause not found.** Step (1) from the original plan is done… | | D247 | **Black bars top and bottom of screen — possible aspect-ratio defect (M-116, user QA report).** — full `## D247` entry at file tail | **NOT A BUG — confirmed faithful, unmodified N64 decomp behaviour, no code change made.** `GE_PCDUMP` captures (both against the pre-existing save and a fresh-EEPROM run, to rule out a stale `Screen` option from earlier test sessions) show the exact same inset on Frigate… | @@ -10919,6 +10919,36 @@ if that's recorded anywhere in the D280 write-up above. **Status assessment after M-161:** this bug has now had ~15 session-passes (M-107 through M-161) across multiple sessions without a landed fix, each pass narrowing the mechanism further without closing it. Per the user's own call this session (diminishing returns, "going at this without much luck for a while"), **recommend parking D243 under the D148 umbrella** (general cutscene-bug tracking entry, `docs/dev/GRAPHICS-BACKLOG.md`) rather than continuing further open-ended diagnostic passes, unless a specific new lead or live capture surfaces. The mechanism knowledge gained (M-143's filter, M-145's teleport architecture, M-160's competing-writer bug) remains valid and should inform any *future* pass — this is a park, not a reset — but the next session should default to other backlog items unless the user explicitly wants another D243 cycle. +**UNPARKED same session: a third-party review found a real methodological flaw in M-161, and the corrected re-test (M-162) produced a genuinely new, decisive result — see below.** + +**M-162 (2026-09-17, same session, after a second third-party review): M-161's negative result was invalid — the restore was placed AFTER `chrTick()` returned, but `render_pos`/`field_488.pos` are computed INSIDE that same call, from whatever `prop->pos` was at that point. So M-161 never actually prevented the stale value from reaching the render.** Verified directly (not just accepted from the review): compared `D243M: tick`'s pre-`chrTick` z-read against `D243M: onscreen`'s post-`chrTick` z-read for the **identical frame number** across the cycling window (`m160_dam_abseil_onscreen_capture.log`, frames 9600-9611) — the pattern is a clean, deterministic period-4 offset applied *within* the same tick: `-4.0, +543.8, +305.5, -855.2`, repeating exactly. This proves the lane-advance happens **inside** `chrTick`, every tick, unconditionally — the strongest evidence yet for M-160's mechanism. + +Corrected the experiment: added `d243x2SavedPos`/`d243x2Active` (top-of-function locals, `chr.c:2451-2453`) — save `prop->pos.f[2]` (Z only, since M-160's capture showed X stable and Y the smooth AI-script descent) right before the `chrTick` position-update dispatch (`chr.c:2541-2617`, inside the `CHRFLAG_HIDDEN`-else block), restore it at `after_position_update:` — **before** the `D243M: onscreen` log line and before anything else in the tick can see the cycled value, unlike M-161. Also extended the probe with `actiontype=`/`anim=` (which of `chrTick`'s branches fired, model's current animation pointer) and `x2active=`/`x2rawz=` (whether suppression ran, and the raw cycled Z it overrode) so the result could be read from the log, not just judged by eye — per the reviewer's explicit "bake this in or risk another inconclusive negative" advice. Gated `GE_D243X2`, `#ifdef PORT`. Compiled clean, linked, 37,823-line bare-boot smoke test with zero crashes. + +**Live playtest result: MIXED — confirms the mechanism, but reveals the fix (as scoped) was incomplete.** User played two full Dam runs with `GE_D243M=1 GE_D243X2=1`; visible glitch looked "about the same" both times. But the log (`scratch/d243/m162_dam_abseil_playtest.log`, second abseil CREATE at frame 11878) shows the suppression **worked exactly as designed this time**: `x2rawz` still shows the same violent period-4 Z-lane cycling (e.g. frames 12100-12130: raw z jumping between ≈-3111, ≈-2500-2570, ≈-1930-2260), but the final rendered `pos.z` is now smooth and monotonic (-2227.6 → -3111.6 → -3107.6 → ... declining ~4 units/frame, no lane jumps at all) — the Z suppression genuinely neutralized the Z-axis cycling this time, unlike M-161. + +**But `pos.y` is violently cycling instead, completely untouched** (same window: `176.2 → -1174.6 → 175.6 → 176.2` repeating, amplitude ~1350 units, with an early transition spike to -12823.6) — this run's chr logged `actiontype=3` (not logged in the original M-160 capture, so no direct comparison, but evidently a different branch/state than whatever produced M-160's "Y descends smoothly, Z cycles" pattern). **This fully explains the unchanged visible glitch**: the Z-only suppression fixed exactly the axis that wasn't dominant in this particular playthrough. The underlying mechanism (chrTick overwriting `prop->pos` with a cycling value every tick, independent of any external input) is now confirmed on a second axis too — not refuted, just incompletely tested. + +**Next (M-163, in progress): extend the suppression to the whole `prop->pos` vector** (not just Z) to test whether stopping cycling on every axis finally kills the visible shake. This will also freeze whatever legitimate motion exists (accepted for a decisive test, not viable as a real fix — see the M-162 code comment). If positive: the mechanism is airtight-confirmed, and the real work becomes root-causing *why* this decomp-faithful write path emits cycling values on PC (candidates per M-154/M-160: an attached keyframe animation without interpolation, a pad-table lookup, an actiontype/POSEND gate the port isn't honoring — `chr->actiontype`/`model->anim` are now logged and should narrow this). If the shake persists even with the whole vector pinned: the mechanism, while real, still isn't the (full) visible cause, and D243 goes back to parked with this ruled out more completely than M-161 managed. + +**Both third-party reviews net assessment:** the first (local Qwen `triage` agent) made one fabricated/confused claim (conflating `g_CameraMode` with a same-numbered `subcam` flag) that I verified and refuted directly against capture data before acting on it — see the now-superseded framing above. The second (unspecified third-party agent) made a data-grounded, directly-verified, and ultimately correct critique of M-161's methodology that materially advanced the investigation. Both were checked against real captures before being trusted, per this project's own standing discipline (`docs/porting-notes.md` §E: verify a cited scratch/log actually contains what a claim says before trusting it) — worth noting as a working example of that discipline paying off in both directions (catching a wrong claim, and correctly accepting a right one). + +**M-164 (2026-09-17, same session, static-only — no build, while the exe was locked by the user's own playtest): a complete mechanical theory tying M-145/M-154/M-160/M-162 together, naming a specific fix candidate.** M-162's capture logged `actiontype=3` for the visible Bond chr during the cycling — per `bondconstants.h:3945-3962`'s `ACT_TYPE` enum (`ACT_INIT`=0, `ACT_STAND`=1, `ACT_KNEEL`=2, `ACT_ANIM`=3), this is **`ACT_ANIM`**, one of the three branches M-160 traced the overwrite to (`chr.c:2578`: `else if ((chr->actiontype == ACT_ANIM) && ...)`, calling `chrUpdateAnim`). Traced `chrUpdateAnim` itself (`chr.c:2398-2424`): it is not an arbitrary overwrite — it calls `modelTickAnim` (advance the currently-attached animation), `subcalcpos` (recompute joint world transforms from that animation), then `getsuboffset(model, &prop->pos)` (`model.c:684`, `getpartoffset(objinst, objinst->obj->RootNode, offset)`) — i.e. it **faithfully reads the model's root-joint world position back into `prop->pos`**, which is exactly what this function is supposed to do for a normally-animating character. The "overwrite" is `chrUpdateAnim` doing its ordinary job on a chr whose animation state was never told about the cutscene. + +Cross-referencing M-145's already-established architecture: `AI_TRYTeleportingChrToPad` (`chrai.c:4064-4119`, the `CHR_BOND_CINEMA` mechanism) writes `chr->prop->pos` **and** calls `setsubroty`/`setsuboffset(chr->model, &pos)` (`chrai.c:4105-4106`) — so the model's root-joint anchor genuinely is re-anchored to the new pad position at each teleport, confirmed by direct code read. **But nothing in that teleport touches `chr->act_anim`/`model->anim`.** M-145 already established that the actual rappel/exit `guard_play_animation` commands run on a separate, invisible "false GUARD" driver chr (`alloc_false_GUARDdata_to_exec_global_action`), not this visible one. **Putting these together: the visible Bond chr keeps ticking whatever animation it already had equipped from before the cutscene hijacked its position** (a locomotion/idle loop, going by `ACT_ANIM` + a real `model->anim` pointer being logged) — every tick, `chrUpdateAnim` advances that stale animation and reads its own root-motion cycle back into `prop->pos`, anchored at wherever the last teleport left the root joint. A short-period root-motion track (a walk-in-place or idle-sway loop) read back on top of a fixed anchor produces exactly the observed "period-4 cycling in place" — mechanically explaining M-153/M-154's lane pattern and M-162's Y-axis finding as two faces of the same thing (whichever axis that specific animation's root motion moves on). + +**This reframes M-162/M-163's suppression experiments correctly: they are not "randomly neutralizing a bug," they are directly testing this exact theory** (block the stale-animation readback from reaching the render, see if the shake stops) — a positive M-163 result would be strong additional confirmation, not merely "the write stops," of this complete mechanism. **The real fix candidate this points to (not yet attempted, needs its own investigation pass):** find where/whether the cutscene AI script is supposed to change `chr->actiontype`/`chr->act_anim` (or otherwise silence `chrUpdateAnim`'s position write) for the *visible* chr when `AI_TRYTeleportingChrToPad` fires — the decomp symbol for `guard_play_animation` referenced in M-145 was not located this pass (grepped, no match for that exact name — likely a different actual symbol in `bondaicommands.c`/`aicommands2.c`; the AI command name is probably decoded/human-friendly, not literal). If N64 has a code path that either (a) redirects that animation command to the *visible* chr instead of the false-GUARD driver, or (b) sets `noTranslate`/freezes root motion on the visible chr during POSEND, and PC is silently skipping it — that is the actual bug, likely bucket (a)/(b) per the diagnosis framework, not a game-logic difference. **Next:** locate the real `guard_play_animation`/animation-assignment AI command handler and trace whether it's meant to target the chr that `AI_TRYTeleportingChrToPad` just moved. + +**M-165 (2026-09-17, same session, static-only, continuing while the exe was locked): found the animation-command handler M-164 flagged as unlocated — it confirms the "wrong chr" targeting is a structural, likely decomp-faithful property of this AI-script design, which reframes where the real PC/N64 divergence must be.** `check_if_able_to_then_perform_animation` (`chraction.c:10263`, called from both `chrai.c:1084` and `aicommands.def:750`) is passed `ChrEntityp` — the AI list's own driver chr — as its `self`/target, not a resolved `CHR_NUM` lookup. Per M-145, the abseil's AI list runs on the invisible false-GUARD driver (`alloc_false_GUARDdata_to_exec_global_action`), so this command (and by extension `guard_play_animation`) **structurally can never reach the visible Bond chr `AI_TRYTeleportingChrToPad` teleports** — this is how the script is written, and since both are byte-identical decomp code, this mismatch is almost certainly present on N64 too, not a PC-only defect. Also checked whether any game-code path gates the visible chr's own `chrTick` animation dispatch during a scripted camera mode: grepped `chr.c` for `CAMERAMODE_POSEND`/`CAMERAMODE_INTRO` (zero hits — `chr.c` has no camera-mode awareness at all) and checked `PLAYERFLAG_NOCONTROL` (set by `AI_BondDisableControl`, `chrai.c:4040`) — it only drives HUD/UI display (`bondviewSetUpperTextDisplayFlag`, `hudmsgsSetOn`, `countdownTimerSetVisible`), never referenced in `chr.c`/`chraction.c`'s position-update path. **There is no game-code mechanism anywhere that suppresses the visible chr's ordinary locomotion-animation tick during a cutscene** — `chrTick` runs its normal dispatch on the player's chr throughout POSEND regardless. + +**This reframes the open question, honestly: if the "wrong-chr animation targeting" and "no cutscene suppression" are both decomp-faithful (high confidence, direct code reads, not conjecture), then the mechanism itself may be completely harmless on N64 — simply because N64's chr happens to have a near-static animation attached (e.g. an idle stand pose with negligible root motion) at the exact moment the cutscene grabs it, while PC's chr has ended up with a different, more mobile animation attached due to some earlier, unrelated divergence in animation-state history** (input timing, dt/tick-count differences, or a genuinely separate bug upstream of the cutscene entirely) — not a bug in the cutscene/teleport code itself. This is a real, open possibility that M-160 through M-164's investigation has not yet ruled in or out, and would require either (a) knowing what animation SHOULD be attached at this moment (checking `ai_24`'s auto-walk-off-the-edge trigger, `chrai.c` — does it explicitly set a specific animation via `chrUpdateAnim`/`modelSetAnimation` as Bond is walked off the ledge, making it deterministic regardless of platform?), or (b) a live capture logging the actual animation ID (not just the `model->anim` pointer, which is opaque without a lookup) attached at the moment `g_CameraMode` flips to `POSEND`, cross-referenced against the animation-table index the same way `GE_D193A`'s `aidx=` field already does elsewhere in this codebase (`docs/dev/GE-ENV-PROBES.md`, `GE_D193A` row) — reusing that established technique here (reverse-resolve `model->anim` against `animation_table_ptrs1`) would let a future capture name the exact animation, not just its address, and settle whether it's a stand-still pose or something with real root motion. + +**Status: genuinely open, not resolved by static reading alone.** This does not change M-162/M-163's experimental value (they still directly test whether suppressing the readback stops the visible symptom) but does mean a **positive M-163 result would not, by itself, prove this is a PC bug** — it would prove the mechanism, which M-164/M-165 have now shown could plausibly be decomp-faithful and only different in its *practical effect* (which animation happens to be attached) rather than its *code path*. Whoever picks this up next should treat M-163's live result as confirming/refuting the *mechanism*, and treat "is this actually a PC-vs-N64 divergence" as a still-separate, still-open question needing either N64 reference footage of this exact moment or the animation-ID capture described above. + +**M-166 (2026-09-17, same session, while the exe stayed locked by the user's own playtest — local Qwen `triage` delegate dispatched to answer M-165's open question, findings spot-checked directly): the visible chr's animation *identity* during the level-exit walk-off is script-forced, but its *phase* is not — narrowing M-165's open question to a specific, testable claim.** Traced the Dam exit AI script directly (`assets/obseg/setup/UsetupdamZ.c`): `ai_24[]` (`:2622+`) — after `hud_hide_and_lock_controls_and_pause_mission_time`/`bond_equip_item`, calls `bond_set_locked_velocity(0x00, 0x04)` then loops on `if_bond_y_pos_less_than(0x98bd, ...)`/`if_local_timer_greater_than` until the fall completes, then `chr_hide_all` and on into the exit chain — **confirmed no animation-set command anywhere in this list** (spot-checked directly; the delegate's report cited slightly wrong hex parameters for `bond_set_locked_velocity`, `(0x0f,0xff)` vs the real `(0x00,0x04)`, a minor transcription error, but the structural claim — no animation command present — checked out on direct read). `AI_BondSetLockedVelocity` (`chrai.c:4259-4271` per the delegate, not independently re-verified this pass) only writes `g_ForceBondMoveOffset`; consumed by `bondviewFrozenMoveBond` (`bondview2.c:7868`+), which the delegate traced as calling `bondviewMoveAnimationTick(0,0,0)` → `bheadAdjustAnimation(0)` every frozen-walk frame. **Spot-checked:** `bheadAdjustAnimation` exists at `bondhead.c:396`, and `PTR_ANIM_bond_eye_walk` is a real symbol at `bondhead.c:21` (part of a breathing/head-animation table, `{PTR_ANIM_bond_eye_walk, 9.5f, 27.0f, 0.0f, 0.0f, 1.5f}`) — both confirmed to exist as cited. Per the delegate's (unverified-past-this-point) trace: at zero speed this function re-attaches `PTR_ANIM_bond_eye_walk` to the head model if it wasn't already the active animation, but computes the re-attach **startframe by phase-interpolating from whatever frame the previous animation was on** (`bondhead.c:411-416`) — i.e. the animation *identity* Bond ends up with during the fall is deterministic (always the idle/walk loop), but the exact **frame/phase** within that loop depends on real-time elapsed (`g_GlobalTimerDelta`-driven fall duration) and whatever animation state existed the instant `ai_24` grabbed control — neither guaranteed identical between N64 and PC. + +**This sharpens M-165's open question into something a single targeted capture can answer.** If the visible shake tracks with animation *phase* drift (a few-frame difference in where the idle/walk loop happens to be when POSEND engages), that would explain a PC-specific divergence entirely upstream of the cutscene/teleport code, consistent with everything else being decomp-faithful. **Concrete next step, ready for the next live session:** extend the `D243M: onscreen` probe (`chr.c`) to log the animation's current frame/phase, not just the `model->anim` pointer — reuse the reverse-resolve-against-`animation_table_ptrs1` technique `GE_D193A`'s `aidx=` field already established elsewhere in this codebase (`docs/dev/GE-ENV-PROBES.md`, `GE_D193A` row) so the capture names an actual frame number, not an opaque address. This was not implemented this pass (probe-design work only, no code changed for this specific addition) — left for the next build/probe cycle alongside M-163's own live re-test. + **Status (from §F table):** **OPEN — M-109 re-analysis of the same session's raw logs corrects the timing/pattern claims above; the D134/pendingGfx race hypothesis is likely wrong.** Confidence MEDIUM on the correction, LOW-MEDIUM on the new lead. Re-reading `data/playtest_d160.log`/`_run2.log` at the `g_framesRendered` level (not just grepping for `D146`) shows the D146 burst fires **~1600-3900 frames (tens of real seconds) *after*** `bossReturnTitleStage`/the `camera_switch(7)` calls, not during them, in **both** runs — "right in the camera-cut window" and "run 1 only" are both wrong as stated above. This also weakens the D134/`pendingGfx` citation: `osSpTaskStartGo` runs `gfx_run()` synchronously and inline (`port/src/libultra.c:1232-1258`), so nothing from the cutscene's own DL submission is still in flight by the time any teardown code runs — the port doesn't have the async-RSP race the hypothesis above assumes. **New lead:** whatever screen renders in that multi-thousand-frame gap after mission-complete is the more likely site — probably `MENU_MISSION_COMPLETE` (`front.c:7267`, `constructor_menu0D_missioncomplete`) or an adjacent briefing/file-select screen in that chain, same general class as D75 (front-end model absence) / D141 (menu bad-pointer bug), not a cutscene-render race. Of the 4 repeating fault addresses, 3 read as literal zero words and only 1 matches the `0xfafbfbfc…` pattern, arguing against a deliberate poison-fill (none found anywhere in `port/`) and for either genuinely-unwritten memory or a decode landing inside unrelated valid data. Next: an env-gated `front.c` menu-transition trace (`MENU_*` id + `g_framesRendered` on every transition after `bossReturnTitleStage`) plus a `GE_PCDUMP` window over frames ~4500-8000 (not the camera-cut window) to name the exact screen/segment. Cross-ref D160 (this supersedes its stale "cutscene skipped" framing with a "cutscene renders corrupted" one, now further corrected to "corruption isn't in the cutscene render at all") and D173 (Cradle's Bond-floating/spinning cutscene bug — same subsystem family, unconfirmed shared root cause). **M-120: the `GE_D243` trace above was BUILT (statically, i.e. written) on branch `investigate/d243-cutscene-race` (`764a1f26`, pushed, not merged) — `src/game/front.c` only, `#ifdef PORT`, logs the target `MENU_*` name + a local per-tick counter on every `menu_init()` state transition. NOT build-verified: two independent sessions (the dispatched subagent, then this session re-verifying in a fresh worktree) both hit the same reproducible failure — `cmake`'s C-compiler ABI check fails with "Cannot create temporary file in C:\Windows\: Permission denied" when gcc is invoked *through* CMake/ninja, even though a bare `gcc -c` invocation from the same shell, same session, succeeds and correctly resolves its temp dir to `%TEMP%`. This is a genuine, reproducible environment issue distinct from the diff's content (static review of the diff found nothing wrong: enum cases all verified to exist in `bondconstants.h`, brace/`#ifdef` balance checked, follows the existing `GE_D160`/`GE_D178` env-gated trace idiom used elsewhere in the same file) — worth a session with a working toolchain investigating the CMake/ninja-vs-bare-gcc environment discrepancy itself as its own item, separate from D243. Next: build `investigate/d243-cutscene-race` on a session where `./build-pc.sh` actually completes, then run the `-level_33` Dam repro with `GE_D243=1` (and the existing `GE_D160`/`GE_PCDUMP` mechanisms) to correlate the trace against the D146 burst's frame range. **M-141: the M-120 toolchain blocker is EXPLAINED and worked around, GE_D243 is now build-verified.** The "Cannot create temporary file in C:\Windows\" failure isn't specific to CMake/ninja vs. bare `gcc -c` — it's this Claude Code session's `Bash` tool shell (Git-for-Windows Bash, not a real MSYS2 MINGW64 window) not exposing a usable `TMP`/`TEMP` to child processes the way an interactive MSYS2 shell does; a bare `gcc -c` from the same shell happened to not hit the temp-file path M-120 tested, which is why it looked clean while the full build wasn't. Fix: explicitly `export TMP=... TEMP=...` (Windows-style path) alongside the already-known `PATH=/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH` fix, both before invoking `./build-pc.sh`/`ninja`/`cmake` — now also recorded in `porting-notes.md` §E as a general session-start gotcha, not just a D243-specific note. Cherry-picked the `GE_D243` trace commit (`764a1f26`) cleanly onto `release/v0.3.0` (branch `investigate/d243-fresh`), fixed one thing the original diff got wrong per this project's own D250 rule (`menu_init()` runs ~once/frame during front-end states — the diff's three `getenv("GE_D243")` calls were uncached; now a single cached `static int s_d243` check), and confirmed it builds clean. **Smoke-tested, not fully exercised:** a bare `-level_33` boot runs 1100+ frames with zero crashes and (correctly) zero `D243:` log lines — bare `-level_XX` skips the front end entirely (per this doc's own M-24-era note), so `menu_init()` never runs; this only proves the probe doesn't regress normal boot, not that it captures anything useful yet. **Still blocked on the same thing M-107/M-109/M-120 all were: an actual interactive playthrough** (main menu → Dam → abseil cutscene → mission-complete → whatever screen the D146 burst fires on, ~1600-3900 frames later) with both `GE_D243=1` and `GE_D160=1` set, which needs a human with input, not available in this session. **Possible connection to D173 worth checking in that same playthrough** (per user, M-141): D173 (floating/spinning Bond puppet in cutscenes, e.g. Cradle/Trevelyan) and D243 (stale-memory DL read some seconds after a cutscene-driven `bossReturnTitleStage`) both sit in the scripted-cutscene/level-transition neighborhood and both ultimately touch "Bond's (or a puppet's) in-world model" — no code-level link found this session (D173's own M-106 pass already ruled out the two most likely struct-aliasing sites, and D243's mechanism looks like a stale/freed buffer read in a *different* subsystem, front-end/menu memory rather than a chr/puppet transform), so treat this as an unconfirmed hypothesis to keep in mind while capturing the next live trace, not an established shared root cause. **Steam Deck playtest confirmation, same M-141 session** — user independently reported "cutscene at end of dam level is badly glitched" without prompting, matching this row's own "camera violently shakes" symptom exactly. Second independent confirmation this is real and reproducible, not a one-off; still needs the interactive `GE_D243=1`+`GE_D160=1` capture this row already calls for. ## D244 — Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the… diff --git a/src/game/chr.c b/src/game/chr.c index 81026614..ec08028e 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -2447,6 +2447,11 @@ s32 chrTick(PropRecord *prop) s32 headSwitchVisible; s32 headVisible; s32 tickamount; +#ifdef PORT + int d243x2Active = 0; /* D243 M-162/M-163, see the CHRFLAG_HIDDEN else block below */ + coord3d d243x2SavedPos = {0}; + coord3d d243x2CycledPos = {0}; +#endif renderdata = D_8002CC6C; chr = prop->chr; @@ -2522,6 +2527,47 @@ s32 chrTick(PropRecord *prop) } else { +#ifdef PORT + /* D243 M-162/M-163: corrected decisive experiment. M-161 restored + * prop->pos AFTER this chrTick() call returned -- but render_pos/ + * field_488.pos are computed INSIDE this same call (subcalcmatrices, + * further down this function), from whatever prop->pos is at THAT + * point, so M-161 never actually prevented the stale value from + * being rendered -- its "no change" result was inconclusive, not a + * real falsification (see docs/dev/findings.md D243 M-162). + * + * Save pos right here, before the position-update dispatch below + * runs (chr.c:2541-2617, the ACT_PATROL/ACT_ANIM/ACT_STAND/else + * chain M-160 traced the overwrite to), and restore it at + * after_position_update -- before anything later in this tick + * (including the render-matrix computation) can see the cycled + * value. + * + * M-162 (Z-only) confirmed the mechanism -- Z stopped cycling and + * went smooth -- but the visible glitch was UNCHANGED, because this + * capture's chr (actiontype=3, different from M-160's capture) turned + * out to cycle Y instead of Z, and Y was left untouched. M-163: the + * whole vector, to see if suppressing every axis finally stops the + * visible shake. This DOES also freeze whatever legitimate motion + * exists on any axis (e.g. a real descent) -- acceptable for a + * decisive test, not for a fix. + * + * Gated on GE_D243X2 + the existing d243mProbeActive() cutscene-mode + * gate (POSEND/INTRO/SWIRL/FADESWIRL) -- g_CameraMode was verified + * POSEND for the entire abseil capture (GE_D243M data), so this is + * scoped correctly without needing a new g_CameraMode extern here. + * + * THIS IS A TEST, NOT A FIX. If it confirms the cycling stops: the + * real root cause is still open -- this is decomp-faithful game + * code, so find WHY it emits cycling values on PC before touching + * anything for real (rule #2's diagnosis-before-fix discipline). + * Revert this block once the experiment reports. */ + extern int d243mProbeActive(void); + static int s_d243x2 = -1; + if (s_d243x2 < 0) { s_d243x2 = getenv("GE_D243X2") != NULL; } + d243x2Active = s_d243x2 && d243mProbeActive(); + d243x2SavedPos = prop->pos; +#endif if (((prop->type == PROP_TYPE_VIEWER) && (g_playerPointers[getPlayerPointerIndex(prop)]->cameramode == 1)) || (chr->chrflags & CHRFLAG_CULL_USING_HITBOX)) { headSwitchVisible = 1; @@ -2618,6 +2664,17 @@ s32 chrTick(PropRecord *prop) after_position_update: #ifdef PORT + /* D243 M-163: apply the save/restore decided above, BEFORE anything else + * in this tick (including the D243M: onscreen log right below, so it + * reports what will actually be used downstream -- and before the + * render-matrix computation further down this function) can see the + * cycled position. Whole vector this pass (M-162 was Z-only; see the + * comment at the save site for why). */ + { + d243x2CycledPos = prop->pos; + if (d243x2Active) { prop->pos = d243x2SavedPos; } + } + /* D243 M-159/M-160: does render_pos only refresh on ticks that pass the * on-screen visibility gate? Log the gate's result + the live prop * position every chrTick call while a scripted cutscene camera is @@ -2625,15 +2682,34 @@ after_position_update: * in POSEND/INTRO/SWIRL/FADESWIRL, so this is silent during FPS play). * If the freeze/teleport boundaries line up with headSwitchVisible * flipping 0->1, this confirms the mechanism named in findings.md D243 - * M-159. Diagnosis only; zero behaviour change when GE_D243M is unset. */ + * M-159. M-162/M-163: actiontype= and anim= (current attached animation + * pointer) added -- M-160 flagged "which of chrTick's branches fires" + * as the open next step toward the actual root cause once the + * suppression experiment reports; x2active=/x2raw= show whether the + * suppression ran this tick and what raw position it overrode, so a + * positive result can be read straight from this log instead of by eye. + * Diagnosis only; zero behaviour change when GE_D243M/GE_D243X2 unset. */ { extern int d243mProbeActive(void); extern int d243mGetFrameCounter(void); if (d243mProbeActive()) { - osSyncPrintf("D243M: onscreen frame=%d chr=%p vis=%d pos=%.1f,%.1f,%.1f\n", + /* M-166: aidx=/animframe= reuse the GE_D193A reverse-resolve + * technique (d193aAnimIndex, chr.c:67) + model->animframe1 (the + * actual current playback phase, bondtypes.h:1578) -- named per + * M-165/M-166's open question: is the visible chr's animation + * IDENTITY deterministic but its PHASE inherited from whatever + * gameplay preceded the cutscene (not guaranteed identical + * cross-platform)? A live capture with this field can now + * answer that directly instead of needing another round-trip. */ + osSyncPrintf("D243M: onscreen frame=%d chr=%p vis=%d pos=%.1f,%.1f,%.1f " + "actiontype=%d anim=%p aidx=%d animframe=%.2f x2active=%d x2raw=%.1f,%.1f,%.1f\n", d243mGetFrameCounter(), (void *) chr, (int) headSwitchVisible, - (double) prop->pos.f[0], (double) prop->pos.f[1], (double) prop->pos.f[2]); + (double) prop->pos.f[0], (double) prop->pos.f[1], (double) prop->pos.f[2], + (int) chr->actiontype, (void *) model->anim, + d193aAnimIndex((void *) model->anim), (double) model->animframe1, + (int) d243x2Active, + (double) d243x2CycledPos.f[0], (double) d243x2CycledPos.f[1], (double) d243x2CycledPos.f[2]); } } #endif