D243 M-169: real teleport signal wired in, root mechanism confirmed decomp-faithful

Implements next-steps (b) and (c) from the D243 CONSOLIDATED NEXT STEPS
block. (c): static analysis shows the render_pos-cycling mechanism (chrTick's
cutscene-unaware animation dispatch) is unmodified decomp code with no gate
on either platform, so it's decomp-faithful by construction -- clears the
way to ship a port-layer fix without a rule-2 sign-off. (b): chrai.c's
AI_TRYTeleportingChrToPad now notifies a new epoch counter right after its
real setsuboffset() call; GE_D243X3/GE_D243X4 re-baseline on that signal
instead of freezing forever, targeting the M-168 "camera looks wrong way for
later shots" / "Bond still looks stuck" incompleteness.

Build/link/crash-safety verified headlessly (two 25-30s -level_09 smoke
runs, both crash-free). Live Dam-to-abseil playtest still owed before this
can replace the diagnostic gating with a shipped default.
This commit is contained in:
James Dansereau committed 2026-09-17 13:59:47 -04:00
1 parent b69cdbe6af
commit 8ba1be14f2
5 files changed
+78 -10

No files matched your search

+2 -1
View File
@@ -47,7 +47,8 @@ Two classes:
| `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/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. **M-163's live result: NEGATIVE for the visible shake** — `prop->pos` genuinely went smooth (log-verified) but the shake was unchanged, because the camera's look-at filter reads a *different* variable (`field_488.pos`, see `GE_D243X3`) that this experiment never touches. Kept in the tree as a real, useful (if ultimately not the visible-symptom fix) data point — always pair with `GE_D243M=1` so the `x2active=`/`x2raw=` fields on `D243M: onscreen` show whether/what it overrode. Do not ship. | **live, test-only** (D243) |
| `GE_D243X3` | `src/game/bondview2.c` (`playerTick`, the `field_488.pos` write site, gated on `g_CameraMode == CAMERAMODE_POSEND`) | **Not a probe — the experiment that actually confirmed the mechanism, off by default, NOT a fix.** M-167: skips the `field_488.pos.x/y/z` assignment (`bondview2.c:10495-10497`) entirely while POSEND is active, freezing it at whatever value it held the instant suppression engaged. `field_488.pos` — not `prop->pos` — is what the cutscene's leaky-integrator look-at filter (M-143, `field_3B8`/`field_3C4-8-C`) actually reads; it comes from `render_pos` (the model's animated-skeleton readback), an independent data path from `prop->pos`. **Live-confirmed positive: user — "yep camera shake is gone now."** Logs a companion `D243M: f488write frame=... idx=... x3active=... f488pos=x,y,z` line (always-on when `GE_D243M=1`, independent of whether `GE_D243X3` itself is set) so the freeze is log-verifiable. Bond's own rendered body still visibly duplicates/glitches even with this active — a separate consumer (`render_pos`'s draw-path read, not this camera-feed copy) of the same underlying cycling skeleton state, not yet addressed. Revert once a real root-cause fix replaces it; do not ship as-is. | **live, test-only** (D243) |
| `GE_D243X3` | `src/game/bondview2.c` (`playerTick`, the `field_488.pos` write site, gated on `g_CameraMode == CAMERAMODE_POSEND`) | **Not a probe — the experiment that actually confirmed the mechanism, off by default, NOT a fix.** M-167: skips the `field_488.pos.x/y/z` assignment (`bondview2.c:10495-10497`) entirely while POSEND is active, freezing it at whatever value it held the instant suppression engaged. `field_488.pos` — not `prop->pos` — is what the cutscene's leaky-integrator look-at filter (M-143, `field_3B8`/`field_3C4-8-C`) actually reads; it comes from `render_pos` (the model's animated-skeleton readback), an independent data path from `prop->pos`. **Live-confirmed positive: user — "yep camera shake is gone now."** Logs a companion `D243M: f488write frame=... idx=... x3active=... x3skip=... epoch=... f488pos=x,y,z` line (always-on when `GE_D243M=1`, independent of whether `GE_D243X3` itself is set) so the freeze is log-verifiable. **M-169: no longer freezes forever** — re-baselines (`x3skip=0`, write allowed through) exactly on the tick a legitimate shot-change teleport fires (`d243GetTeleportEpoch()`, bumped by `chrai.c`'s `AI_TRYTeleportingChrToPad` right after its real `setsuboffset()` call), addressing the M-168 postmortem's "camera looks the wrong way for later shots" incompleteness — untested live yet, next session's first move. Revert once a real root-cause fix replaces it; do not ship as-is. | **live, test-only** (D243) |
| `GE_D243X4` | `src/game/chr.c` (`chrTick`, the `render_pos` snapshot/restore, gated on `d243mProbeActive()`) | **Not a probe — the M-168 experiment applying X3's technique to the actual draw-path consumer, off by default, NOT a fix.** Snapshots `model->render_pos[]` after a 2-tick warm-up once POSEND engages, then holds it steady by copying the cached contents back over each subsequent tick's freshly-computed buffer — `subcalcmatrices()` always still runs first (never skipped; the first two attempts skipped it and crashed on a reused-pointer stale `-1` tombstone, see findings.md D243 M-168). Addresses Bond's own rendered-body duplication (the consumer X3 never touched). Crash-free, stable, but same forever-freeze incompleteness as X3 pre-M-169. **M-169: same teleport-epoch re-baseline as X3** — the warm-up counter now also resets (re-snapshots) whenever `d243GetTeleportEpoch()` changes, not only on the active/inactive edge. Untested live yet. Revert once a real root-cause fix replaces it; do not ship as-is. | **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=<n>` 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) |
+14 -5
View File
@@ -10992,12 +10992,21 @@ This entry is long and multi-pass (M-107 through M-168). This block is the singl
5. **Bond's model duplication: diagnostically addressed but incomplete.** The same freeze technique applied to `render_pos` (`GE_D243X4`, `chr.c`) is now crash-free (two real crashes fixed — see `porting-notes.md`'s two D11 updates for the postmortem: model pointers get reused by the allocator across different logical model instances, so any validity check based on a pointer's value or cached identity can be fooled) but **freezing unconditionally also suppresses the cutscene's legitimate per-shot repositioning** (each `AI_TRYTeleportingChrToPad` call is a real, intentional camera/model cut to a new shot) — so the camera now looks the wrong direction for later shots and Bond still looks stuck.
6. **Whether the root mechanism (item 2/3) is itself decomp-faithful is still OPEN.** `ai_24` (the level-exit trigger) contains no animation-set command for Bond (verified directly against `assets/obseg/setup/UsetupdamZ.c`); `bondviewFrozenMoveBond`'s own tick forces Bond's animation *identity* back to an idle/walk loop deterministically, but the *phase* within that loop is inherited from real-time-dependent prior gameplay, not reset to a fixed value — this phase is the most likely PC/N64 divergence point, not yet confirmed.
### What to actually try next (none of these attempted yet)
### What to actually try next
- **(a) Delta-threshold re-baseline.** Instead of freezing `field_488.pos`/`render_pos` forever once POSEND engages, re-baseline the frozen snapshot whenever the raw (pre-freeze) value jumps by more than some threshold clearly larger than the intra-shot cycling amplitude (already characterized in the M-153/M-160/M-163 captures — the cycling stays within a few hundred to ~1500 units; a real shot-change teleport should be much larger or otherwise distinguishable). Cheapest option, no new hooks needed, but the threshold is a guess that could misfire.
- **(b) Hook the real signal directly.** Re-baseline the freeze exactly when `AI_TRYTeleportingChrToPad` (`chrai.c:4064`) fires for this chr, rather than inferring it from position deltas. More precise, needs a small cross-file signal (a flag or callback) from `chrai.c` to the freeze logic in `chr.c`/`bondview2.c`.
- **(c) Find the actual root-cause divergence (item 6 above).** Log the animation's actual frame/index via the `aidx=`/`animframe=` fields already wired into `D243M: onscreen` (M-166) during a live capture, and determine whether the resulting phase is something that could plausibly also happen on N64 (in which case this is decomp-faithful and (a)/(b) above — or a bucket-(b) port-layer timing fix — are the right permanent answer) or whether it's clearly PC-specific (which would point at a different, more specific root cause still to be found). This determines whether (a)/(b) above are building toward a real fix or just a better-tuned workaround.
- **Do (c) before shipping (a) or (b) as a real fix** — per rule #2's diagnosis-before-fix discipline, and because the answer to (c) changes which of (a)/(b) is actually correct behavior to converge on.
- **(a) Delta-threshold re-baseline.** NOT attempted, superseded by (b) below — no longer needed as the primary mechanism, though could still be a fallback if (b)'s single-teleported-chr assumption ever breaks.
- **(b) Hook the real signal directly — IMPLEMENTED this session (M-169), untested live.** Re-baseline the freeze exactly when `AI_TRYTeleportingChrToPad` (`chrai.c:4064`) fires for this chr, via a new `d243NotifyTeleport()`/`d243GetTeleportEpoch()` pair (`bondview2.c`) called from `chrai.c` right after its real `setsuboffset()` call. `GE_D243X3`/`GE_D243X4` both now consume it. Build/link/crash-safety verified headlessly; needs a live Dam→abseil playtest to confirm it actually fixes the visible symptom (see M-169 below for the exact next capture to run).
- **(c) Find the actual root-cause divergence (item 6 above) — RESOLVED by static analysis this session (M-169), no live capture needed.** The root mechanism (items 2/3) is unmodified decomp code with no cutscene-mode gate on either platform, so it's decomp-faithful by construction; the actual PC/N64 divergence is narrower (animation *phase* at the cutscene-entry instant, inherently player-input-history-dependent on both platforms, not a meaningful frame-exact N64-match target) and doesn't block shipping a bucket-(b) port-layer fix like (b) above. Full reasoning in the M-169 entry below.
### M-169 (2026-09-17, this session): static determination on item 6 (root-mechanism decomp-faithfulness) + next-step (b) implemented, untested live
**Item 6 resolved by static analysis, no live capture needed.** Items 2/3 above already established, code-verified, that `chrTick`'s per-chr animation-locomotion dispatch (`chr.c:2541-2617`) "has no cutscene-mode awareness" — it is **unmodified decomp code, no `#ifdef PORT` anywhere in it**. Since the decomp is byte-matched to the N64 ROM, the same dispatch — with no cutscene gate — runs identically on real N64 hardware given the same character/animation state. **This makes the root render_pos-cycling mechanism (items 2/3) itself decomp-faithful by construction**: N64 has no special-casing here either, so this is not a PC-introduced code bug in the mechanism itself. The likely PC/N64 divergence is narrower than "the mechanism runs at all" — it's *which* animation phase the visible chr happens to be on the instant the cutscene's `AI_TRYTeleportingChrToPad` grabs it, which is downstream of real-time player-input history before the trigger (`bheadAdjustAnimation`, `bondhead.c:396` — also unmodified decomp, phase-interpolates across the two head-animation sets from `g_CurrentPlayer->field_5C0`, driven purely by movement `speed`). That phase is inherently non-deterministic input-history state on **either** platform — matching a specific N64 reference frame-for-frame isn't a meaningful target — so per rule #2's taxonomy this settles in bucket (b): a port-layer fix built *on top of* the decomp-verbatim mechanism, not a rule-2-sign-off-worthy behavioral change to it.
**Implemented next-step (b) from the list above** (the "hook the real signal directly" option, not (a)'s threshold guess): `chrai.c`'s `AI_TRYTeleportingChrToPad` case (`chrai.c:4064-4119`) already contains the real signal for "a legitimate shot-change just happened" — its own `setsuboffset(chr->model, &pos)` call (line 4106, unmodified decomp), which re-anchors the model's root joint for the new pad position. Added a one-line `#ifdef PORT` notify call right after it (`d243NotifyTeleport()`, new function in `bondview2.c` next to the other `d243mXxx` accessors) that bumps a monotonic epoch counter (`d243GetTeleportEpoch()`). `GE_D243X3` (`bondview2.c`, `field_488.pos` freeze) and `GE_D243X4` (`chr.c`, `render_pos` freeze) both now re-baseline (skip the freeze, let the real write through) exactly on the tick the epoch changes since their last check, instead of freezing forever once POSEND engages — directly targeting the M-168 postmortem's "camera looks the wrong way for later shots" / "Bond still looks stuck" incompleteness. Global epoch, not per-chr (sufficient for this cutscene's single-teleported-chr scope; revisit if a future cutscene needs more). New companion log fields on `D243M: f488write`: `x3skip=` (whether the freeze actually suppressed this write) and `epoch=`.
**Verified this session (headless, no live/Deck access):** clean incremental build (`cmake --build build-pc -j8`, MINGW64 shell) — zero new warnings/errors on any touched line (`chrai.c`, `chr.c`, `bondview2.c`); two independent 25-30s `-level_09` smoke runs post-rebuild, both crash-free (`ge007.crash.log` absent both times; deleted the file before each run to rule out a stale leftover). Env vars (`GE_D243M`/`GE_D243X3`/`GE_D243X4`) all default-unset in these runs, so this is a build/link/crash-safety check only — **not** a verification that the re-baseline actually fixes the visible symptom. **Not yet attempted:** navigating to the Dam abseil cutscene headlessly via `GE_INPUTSCRIPT` — per D236 pass 11 and D249's own notes this session, blind scripted navigation through a full level to a specific trigger has a poor track record in this repo and risks burning time for no data; a live human Dam→abseil playthrough remains the fastest path to a real answer.
**Next (live, needs the user):** `GE_D243M=1 GE_D243X3=1 GE_D243X4=1`, play Dam to the abseil, watch for (a) whether the camera now looks the right direction across all three shots (M-167's regression this was meant to fix) and (b) whether Bond's body still looks stuck/duplicated across shots. Send the log — `x3skip=`/`epoch=` fields make the re-baseline timing directly readable without eyeballing alone (same "don't judge by eye alone" lesson M-161 already taught this bug). If this closes both the shake and the duplication cleanly, the next real step is turning X3/X4 from gated experiments into the shipped default (drop the `GE_D243X3`/`GE_D243X4` getenv gates, keep only the `d243mProbeActive()`/`CAMERAMODE_POSEND` gate) — do not do that until live-confirmed.
### Applies to the wider family too
+38 -3
View File
@@ -378,6 +378,24 @@ int d243mProbeActive(void)
}
int d243mGetFrameCounter(void) { return g_d243mFrameCounter; }
/* D243 M-169: the "real signal" named as next-step (b) in findings.md's
* D243 CONSOLIDATED NEXT STEPS -- a monotonic epoch counter bumped by
* chrai.c's AI_TRYTeleportingChrToPad case (the exact decomp call, chrai.c
* ~4064-4119, that legitimately re-anchors a chr's model root joint via
* setsuboffset() for a real cutscene shot-change) right after that call.
* chr.c's GE_D243X4 (render_pos freeze) and this file's GE_D243X3
* (field_488.pos freeze) both poll this to re-baseline their frozen
* snapshot exactly on a legitimate shot-change instead of either freezing
* forever (losing real repositioning) or guessing a position-delta
* threshold (next-step (a), not attempted). Global, not per-chr: sufficient
* for this cutscene's scope (one chr teleported at a time); revisit if a
* future cutscene needs per-chr tracking. Diagnosis/experiment plumbing
* only -- zero cost and zero behaviour change when GE_D243M is unset, since
* nothing reads the epoch unless the X3/X4 experiments are also enabled. */
static u32 g_d243TeleportEpoch = 0;
void d243NotifyTeleport(void) { g_d243TeleportEpoch++; }
u32 d243GetTeleportEpoch(void) { return g_d243TeleportEpoch; }
#endif
void solo_char_load(void)
@@ -10513,8 +10531,25 @@ s32 playerTick(PropRecord *prop)
* NOT A FIX -- revert once it reports (see docs/dev/findings.md
* D243 M-167). */
static int s_d243x3 = -1;
static int s_d243x3WasActive = 0;
static u32 s_d243x3LastEpoch = 0;
extern u32 d243GetTeleportEpoch(void);
int wantFreeze;
u32 epoch;
int justRebaselined;
int skipWrite;
if (s_d243x3 < 0) { s_d243x3 = getenv("GE_D243X3") != NULL; }
if (!(s_d243x3 && (g_CameraMode == CAMERAMODE_POSEND)))
wantFreeze = s_d243x3 && (g_CameraMode == CAMERAMODE_POSEND);
epoch = d243GetTeleportEpoch();
/* M-169: same teleport-epoch re-baseline as chr.c's X4 --
* let this write through on the tick a legitimate
* shot-change teleport fires (and on first engaging POSEND),
* then resume freezing until the next teleport. */
justRebaselined = (!s_d243x3WasActive) || (epoch != s_d243x3LastEpoch);
skipWrite = wantFreeze && !justRebaselined;
if (wantFreeze) { s_d243x3LastEpoch = epoch; }
s_d243x3WasActive = wantFreeze;
if (!skipWrite)
{
#endif
g_playerPointers[index]->field_488.pos.x = mtx[12] + (mtx[4] * 7.0f);
@@ -10523,9 +10558,9 @@ s32 playerTick(PropRecord *prop)
#ifdef PORT
}
if (d243mEnabled()) {
osSyncPrintf("D243M: f488write frame=%d idx=%d x3active=%d f488pos=%.1f,%.1f,%.1f\n",
osSyncPrintf("D243M: f488write frame=%d idx=%d x3active=%d x3skip=%d epoch=%u f488pos=%.1f,%.1f,%.1f\n",
g_d243mFrameCounter, index,
(int) (s_d243x3 && (g_CameraMode == CAMERAMODE_POSEND)),
(int) wantFreeze, (int) skipWrite, (unsigned) epoch,
(double) g_playerPointers[index]->field_488.pos.x,
(double) g_playerPointers[index]->field_488.pos.y,
(double) g_playerPointers[index]->field_488.pos.z);
+10 -1
View File
@@ -2815,15 +2815,24 @@ after_position_update:
* TEST, NOT A FIX -- revert once it reports. */
{
extern int d243mProbeActive(void);
extern u32 d243GetTeleportEpoch(void);
static int s_d243x4 = -1;
static int s_d243x4WasActive = 0;
static int s_d243x4WarmTicks = 0;
static RenderPosView s_d243x4Snap[128];
static s16 s_d243x4Count = 0;
static u32 s_d243x4LastEpoch = 0;
if (s_d243x4 < 0) { s_d243x4 = getenv("GE_D243X4") != NULL; }
{
int active = s_d243x4 && d243mProbeActive();
if (active && !s_d243x4WasActive) { s_d243x4WarmTicks = 0; }
u32 epoch = d243GetTeleportEpoch();
if (active && !s_d243x4WasActive) { s_d243x4WarmTicks = 0; s_d243x4LastEpoch = epoch; }
/* M-169: a legitimate shot-change teleport fired since the
* last snapshot -- re-warm so the freeze re-baselines to the
* post-teleport pose instead of holding the pre-teleport
* one (which produced the "camera looks the wrong way for
* later shots" incompleteness noted in the M-168 postmortem). */
if (active && (epoch != s_d243x4LastEpoch)) { s_d243x4WarmTicks = 0; s_d243x4LastEpoch = epoch; }
s_d243x4WasActive = active;
if (active && (model->render_pos != NULL) && (model->obj != NULL))
{
+14
View File
@@ -4104,6 +4104,20 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType)
chr->chrflags = chr->chrflags | CHRFLAG_INIT;
setsubroty(chr->model, FacingDirection);
setsuboffset(chr->model, &pos);
#ifdef PORT
/* D243 M-169: signal the real shot-change moment
* to the X3/X4 render-pos freeze experiments --
* see the d243NotifyTeleport() definition
* (bondview2.c) for the full rationale. This is
* the exact decomp call (setsuboffset, line
* above, unmodified) that a legitimate cutscene
* teleport already performs; the notify is a
* side-channel observer only, no logic change. */
{
extern void d243NotifyTeleport(void);
d243NotifyTeleport();
}
#endif
chrDetectRooms(chr);
if (chr->prop == g_CurrentPlayer->prop)
{