D310 v2: scope PRINT sizing fix to global AI lists; D311: close D156 guard hole

D310 (RULE-2-SIGNOFF, user-confirmed fixed): the uniform 1-byte AI_PRINT
size broke level-local lists, which embed a NUL-terminated string after
the PRINT byte (Caverns Trevelyan ran circles with glitched anims).
chraiitemsize is back to byte-identical decomp; new PORT-only
d310ItemSize() applies the 1-byte size only for lists confirmed global
via chraiGetAIListID, used at ai()'s AI_PRINT step and chraiGoToLabel's
label scan. Local-list behavior unchanged from the original.

D311 (new, guarded): live freeze in Caverns diagnosed via GDB — Bond's
own anim tick spun ~3.1e9 iterations in modelSetAnimFrame2WithChrStuff:
animframe1 poisoned to -1.67e31 upstream, and the D156 guard validated
only frameb, so 'frameb = framea' passed both garbage values through and
floorFloatToInt saturated them to far-apart ints. Extend the guard to
validate both framea/frameb (snap bad one to the valid other, else 0)
and heal model->animframe1/2 before the fallback re-reads them. Same
file/pattern as the accepted D156 guards, #ifdef PORT only. Upstream
poison source (oldspeed=-1.75e18 at transition start; timespeed=0,
unkb0=0) recorded open in the D311 entry.

Post-fix Caverns playtest: guards dash-pause-resume, Trevelyan pathing
normal, Caverns->intro clean. Probe log: global PRINTs size=1/global=1
(hit once each), local PRINTs NUL-scanned/valid landings, zero
LABEL-NOT-FOUND, zero D156 fires, zero watchdog freezes.
This commit is contained in:
James Dansereau committed 2026-09-18 13:18:16 -04:00
1 parent 0c700852e2
commit c67479866c
4 files changed
+127 -43

No files matched your search

+3 -2
View File
@@ -186,5 +186,6 @@ D305,FIXED,"Steam Deck SIGSEGV on the v0.3.0 pre-release build (Cavern, early in
D306,OPEN,"Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).","OPEN — reported only, not investigated."
D307,OPEN,"In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g.","OPEN — reported only, not investigated."
D308,OPEN,"Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).","OPEN — reported/confirmed by the user, not investigated."
D309,FIXED,Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen —,"FIXED via D310 (RULE-2-SIGNOFF, 2026-09-18) —"
D310,FIXED,"RULE-2-SIGNOFF: chraiitemsize() mis-sizes 1-byte AI_PRINT records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT —","FIXED (signed off + implemented 2026-09-18, #ifdef PORT, N64 arm unchanged) —"
D309,FIXED,Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen —,FIXED via D310 — user-playtest-confirmed 2026-09-18 (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log).
D310,FIXED,"RULE-2-SIGNOFF: chraiitemsize() mis-sizes 1-byte AI_PRINT records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT —",FIXED — user-playtest-confirmed 2026-09-18 (v2:
D311,PARTIAL,Complete freeze in modelSetAnimFrame2WithChrStuff root-motion loop —,"GUARDED (#ifdef PORT, D156-guard extension in the same file) —"
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
186 D306
187 D307
188 D308
189 D309
190 D310
191 D311
+30 -6
View File
@@ -594,8 +594,9 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D306 | **Dam intro/truck wheels render with odd transparent-looking areas, possibly z-fighting (user QA, 2026-09-18).** — full `## D306` entry at file tail | OPEN — reported only, not investigated. Explicitly not for v0.3.0. |
| D307 | **In-level security camera entities can be flipped facing the wrong direction (backwards) on some levels, e.g. Bunker (user QA, 2026-09-18) — NOT the player's view camera.** — full `## D307` entry at file tail | OPEN — reported only, not investigated. Explicitly not for v0.3.0. |
| D308 | **Occasional z-fighting confirmed on some levels' geometry, general/broader than the D306 truck-wheel instance (user QA, 2026-09-18).** — full `## D308` entry at file tail | OPEN — reported/confirmed by the user, not investigated. Explicitly not for v0.3.0; D306 may be one instance of this. |
| D309 | **Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen — boss thread spins forever in `ai()` on `m_RunToBondPersistent` via a phantom `GotoNext(lblZero)` created by `chraiitemsize`'s `AI_PRINT` NUL-scan mis-sizing a stringless PRINT record (user report + live GDB diagnosis, 2026-09-18).** — full `## D309` entry at file tail | FIXED via D310 (RULE-2-SIGNOFF, 2026-09-18) — the mis-sizing landmine is neutralized, so the phantom-`GotoNext(0)` spin path no longer exists; pending user live re-verification in Caverns. The teardown-ordering open question (why PC presents the unreachable-Bond state) remains documented but is now harmless. |
| D310 | **RULE-2-SIGNOFF: `chraiitemsize()` mis-sizes 1-byte `AI_PRINT` records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT — fixes D309's freeze mechanism and the Caverns standing-still guards (`Stop`+`PRINT("wait")` mis-lands on EndList, re-executed every tick; user report 2026-09-18).** — full `## D310` entry at file tail | FIXED (signed off + implemented 2026-09-18, `#ifdef PORT`, N64 arm unchanged) — pending user live re-verification of both symptoms in Caverns. PD precedent: PRINT removed, slot 0xAD reassigned. |
| D309 | **Complete non-self-recovering freeze (Windows) returning from Caverns to the intro screen — boss thread spins forever in `ai()` on `m_RunToBondPersistent` via a phantom `GotoNext(lblZero)` created by `chraiitemsize`'s `AI_PRINT` NUL-scan mis-sizing a stringless PRINT record (user report + live GDB diagnosis, 2026-09-18).** — full `## D309` entry at file tail | **FIXED via D310 — user-playtest-confirmed 2026-09-18** (Caverns→intro clean; the spin record itself observed resuming correctly in the post-fix probe log). The teardown-ordering open question (why PC presents the unreachable-Bond state) remains documented but is now harmless. |
| D310 | **RULE-2-SIGNOFF: `chraiitemsize()` mis-sizes 1-byte `AI_PRINT` records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT — fixes D309's freeze mechanism and the Caverns standing-still guards (`Stop`+`PRINT("wait")` mis-lands on EndList, re-executed every tick; user report 2026-09-18).** — full `## D310` entry at file tail | **FIXED — user-playtest-confirmed 2026-09-18** (v2: global-list-only sizing via `d310ItemSize`; guards resume, Trevelyan pathing intact, Caverns→intro clean; probe log verified both list classes). PD precedent: PRINT removed, slot 0xAD reassigned. |
| D311 | **Complete freeze in `modelSetAnimFrame2WithChrStuff` root-motion loop — D156 guard hole: poisoned `framea` (`model->animframe1`) passes the frameb-only clamp and saturates to a ~3.1e9-iteration spin (Caverns, live GDB diagnosis, 2026-09-18).** — full `## D311` entry at file tail | GUARDED (`#ifdef PORT`, D156-guard extension in the same file) — freeze class neutralized; upstream speed/frame poison source OPEN (level reload clears displaced model state). No AI involvement — distinct from D309/D310. |
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software
@@ -11900,7 +11901,7 @@ do {
**Fix bucket:** NOT an ABI/layout exception (no pointer-width involvement at all). The decomp's code+data are faithful; the divergence is in *when* the state occurs, i.e. PC teardown/scheduling. A port-side fix (ensure guards stop ticking AI before/while Bond+level state goes invalid on level exit) is the natural home and needs its own investigation of the teardown ordering (`gesched.c` / boss-thread vs level-unload race). Any `src/game` edit would require rule-2 sign-off.
**Status:** OPEN — root cause identified with full evidence; no fix made. **Release impact:** user decision owed — fix before the v0.3.0 tag, or ship with a known-issue note (complete freeze on Caverns→intro transition).
**Status:** **FIXED via D310 — user-playtest-confirmed 2026-09-18** (Caverns→intro transition clean; the spin record `PRINT("no go!")`@+4 itself was observed in the post-fix probe log stepping `size=1 global=1` and resuming at `Label(27)` as intended). The teardown-ordering open question (why PC presents the unreachable-Bond state) remains documented but is now harmless — reaching that state no longer spins.
## D173/D292 — user confirmation, 2026-09-18: mostly fixed as a side effect of D243's root cause.
@@ -11914,7 +11915,9 @@ This confirms the M-160/M-164/M-165 hypothesis (`## D173`'s own consolidated-nex
## D310 — RULE-2-SIGNOFF: `chraiitemsize()` mis-sizes 1-byte `AI_PRINT` records (NUL-scans forward), corrupting AI execution for any list containing an active PRINT — fixes D309's freeze mechanism and the Caverns standing-still guards (signed off + implemented, 2026-09-18).
**Status:** **RULE-2-SIGNOFF** / FIXED (pending user live re-verification of both symptoms in Caverns). One-line `src/game` behavior change under `#ifdef PORT`; N64 build untouched.
**Status:** **RULE-2-SIGNOFF / FIXED — user-playtest-confirmed (2026-09-18).** All three checks passed live in Caverns: standing-still guards resume, Trevelyan follows his path, Caverns→intro no longer freezes. `src/game` behavior change under `#ifdef PORT`; N64 build untouched.
**Post-fix probe log (clean session, 989 lines):** global-list PRINTs (`m_RunToBondPersistent`, chr=4/24/72) all step `size=1 global=1` — including the D309 spin record itself (`off=4 "no go!" landcmd=0x02`=Label, resumed correctly) and the standing-still record (`off=167 "wait" landcmd=0x95`=UnsetMyFlags2), each hit exactly once (no per-tick re-execution). Local-list PRINTs (Trevelyan chr=88, fake-GUARD driver chr=254) all keep NUL-scan sizing (`size=4/7 global=0`) with valid landing commands. Zero `LABEL-NOT-FOUND`, zero D156 fires, zero watchdog freezes.
**The defect.** The `PRINT()` macro (`src/aicommands2.h:4913`) emits exactly **one command byte** into the built AI list — its string argument is discarded at list-build time. But `chraiitemsize()`'s `case AI_PRINT:` (`src/game/chrai.c:682`, original) sized the record by scanning forward to the next NUL byte, so stepping on a PRINT always skips ~20 bytes into whatever records follow and resumes at an arbitrary byte pattern. Only 4 PRINTs exist in all of `src/game/chraidata.c`; two are commented out (`m_DeadAI:74`, `m_StandardGuard:136`) and **both active ones are in `m_RunToBondPersistent`** — so exactly one AI list in the game is affected.
@@ -11930,8 +11933,29 @@ This confirms the M-160/M-164/M-165 hypothesis (`## D173`'s own consolidated-nex
**Grant (2026-09-18, in-thread):** "I want to proceed with your implementation plan here to fix this bug." (User's reply to the full §7 write-up; asked only whether a fresh session was needed — answered no, granted in this session.)
**Resulting diff:** `src/game/chrai.c` `chraiitemsize()` — `case AI_PRINT:` now `return 1;` under `#ifdef PORT`, original NUL-scan moved to the `#else` arm unchanged (commit following this entry). Verification: full-list decode of `m_RunToBondPersistent` with the fix (`scratch/decode_printfix.c`) walks +0→EndList@223 with every record valid, and the post-`"wait"` sequence matches `chraidata.c:666+` line-for-line (`UnsetMyFlags2(4)`, `IFNewRandomGreaterThan(160)`, `SetMyFlags2(4)`, `MyTimerStart`, sit-rep block, `GotoFirst(lblRun)`); post-`"no go!"` resumes at `Label(27)+Yield` as in source.
**Resulting diff v1 (REGRESSED, superseded):** `chraiitemsize()` `case AI_PRINT:` → `return 1;` under `#ifdef PORT`. Fixed the guards, but live testing then broke **Caverns' Trevelyan (chr=88)**: ran in circles with glitched animations. Memory dump of his list (`0x701cca30`, a level-LOCAL list from `g_CurrentSetup.ailists`) shows local lists **embed a NUL-terminated string after the PRINT byte** — live bytes `AD 44 0A 00` (PRINT + "D\n"), `AD 57 0A 00`, `AD 34 0A 00` at offsets 50/288/302 — so the NUL-scan is CORRECT for local lists; the uniform 1-byte size landed mid-string (garbage commands + a `LABEL-NOT-FOUND` spin, off=403 label=0, probe-capped). Only the macro-built GLOBAL lists (`g_GlobalAILists`, chraidata.c) discard their PRINT strings.
**Scope note (§7 drift check):** affects exactly one AI list (the only one with active PRINTs). Both observed symptoms are inside that scope; no other list's behavior changes.
**Resulting diff v2 (current):** `chraiitemsize()` left byte-identical to the decomp. New PORT-only `d310ItemSize(AIList, offset, isGlobalAIList)` returns 1 for a PRINT only when `chraiGetAIListID()` confirms the list is global; otherwise falls through to the original NUL-scan. Applied at both record-walk sites: `ai()`'s AI_PRINT step and `chraiGoToLabel`'s label scan (a backward `GotoFirst` crossing a global PRINT would mis-scan even with v1's sizer, since the scan also steps records). Lists found in neither table (teardown) default to the original NUL-scan. Verification: full-list decode of `m_RunToBondPersistent` with 1-byte global PRINTs (`scratch/decode_printfix.c`) walks +0→EndList@223 with every record valid, and the post-`"wait"` sequence matches `chraidata.c:666+` line-for-line (`UnsetMyFlags2(4)`, `IFNewRandomGreaterThan(160)`, `SetMyFlags2(4)`, `MyTimerStart`, sit-rep block, `GotoFirst(lblRun)`); post-`"no go!"` resumes at `Label(27)+Yield` as in source.
**Scope note (§7 drift check):** v1's sign-off scope ("exactly one AI list affected") was wrong in one respect — PRINT records also exist in level-local lists, but with a different shape (embedded string), where the original NUL-scan is already correct. v2's *behavioral* change is still confined to global-list PRINTs: `m_RunToBondPersistent`'s two records. Local-list behavior is byte-identical to the original decomp — Trevelyan reverts to his pre-fix (working) pathing. Drift disclosed to user in-thread when the regression was reported; same sign-off intent (fix global PRINT sizing, no other behavior change).
**Cross-refs:** D309 (this fix neutralizes its spin mechanism — the teardown-ordering open question in D309's "why PC and not N64" remains, but reaching that state is now harmless); GE_D309 probes (`chrai.c` `ai()`/`chraiGoToLabel`, `chraction.c` `chrGoToBond`) remain as diagnostic tools.
## D311 — Complete freeze in `modelSetAnimFrame2WithChrStuff` root-motion loop: D156 guard hole — poisoned `framea` (`model->animframe1`) passes through the frameb-only clamp and saturates to a ~3.1e9-iteration spin (Caverns, live GDB diagnosis, 2026-09-18).
**Reported:** user playtest of the D310 v2 build in Caverns — "it's frozen" shortly after launch (watchdog: no frame rendered for 8014 ms at frames=8424; ~2.3 min into the session).
**Live evidence (GDB attach, PID 46948, mainThread):**
- Stack: `sincosf` ← `modelSetAnimFrame2WithChrStuff` (`model.c:3349`, anim-merge branch) ← `modelTickAnim` ← `bheadUpdate` ← `bondviewMoveAnimationTick` ← `MoveBond` — the **player's own** model anim tick. Zero `D309:` probe lines in the log → no AI-list involvement; this is a different bug from D309/D310.
- Locals at the spin site: `curframe = -966612173`, `endframe = 2147483647` (INT_MAX) → ~3.1e9 loop iterations; `pos34 = (-6.7e30, ?, 2.9e32)` — root-motion accumulator already blown up; `model->animframe1 = -1.6708e+31`; `model->endframe = 17` (the anim has 17 frames); `speed = 0.22` (normal), but `newspeed = -3.2e7`, `oldspeed = -1.75e18`, `timespeed = 0`, `unkb0 = 0`; `anim2 != NULL` with healthy frame2 (18.8→20.9).
- The existing D156 guard **had fired** earlier in the same session (`[D156] bad anim frame guarded: frame=-1.6708e+31 animframe1=-1.6708e+31 ... oldspeed=-1.75136e+18 timespeed=0 unkb0=0`) — i.e. `animframe1` was already poisoned *before* the guarded tick, and the guard's fallback (`frame = model->animframe1`) plus the callee's frameb-only clamp (`if bad frameb: frameb = framea`) passed the poison straight through.
**Mechanism (the guard hole):** D156's guard in `modelSetAnimFrame2WithChrStuff` validates only `frameb`. With `framea = frameb = -1.67e31`, `floorFloatToInt` saturates the two near-equal huge floats to far-apart ints (`curframe = -966612173`, `endframe = INT_MAX`), so the `while(1)` root-motion loop walks ~3.1e9 frames — each doing a joint transform + bitstream decode + `sincosf` — i.e. a permanent hang. The N64 never presents this state (fixed timestep, bounded speeds); the poison is PC-side upstream corruption of the model speed/frame fields (see open question).
**Fix (landed, `#ifdef PORT`, same file/pattern as the accepted D156 guards — port-side defensive clamp against non-finite state, no game-logic change):**
1. `modelSetAnimFrame2WithChrStuff`: validate **both** `framea` and `frameb` against ±1e6; a bad one snaps to the other if that is valid, else 0. Both-bad → both 0 → loop does ~0 iterations; the in-loop `animframe1` rewrite (`model.c:3447`) then heals stored state on subsequent ticks.
2. `modelTickAnim`: heal `model->animframe1`/`animframe2` (snap to 0) **before** the bad-frame fallback reads them, so the poison source itself is cleared instead of being re-derived every tick.
**Open question (deeper root cause, NOT fixed):** what wrote `-1.75e18` into `oldspeed` / poisoned `animframe1` in the first place? `oldspeed = model->speed` at transition start (`model.c:2975`) means `speed` was already garbage upstream; `timespeed = 0` and `unkb0 = 0` at the guarded tick are suspicious division denominators (D156's original note flagged `/ unkb0` and `/ timespeed` as guarded `> 0` but not against tiny values). Also, the model's persistent root-motion header (`header->unk34/unk24`) is poisoned to ~1e30–1e32 in this instance, so **Bond's rendered model may be displaced even after the freeze is prevented** — a level reload clears it. Needs its own investigation (dump speed-field write sites during the triggering transition) before any further fix.
**Status:** GUARDED (freeze class neutralized; upstream poison source OPEN). Disclosed to user in-thread as a D156-guard extension following the already-accepted D156 precedent in this same file; if the project wants formal rule-2 sign-off for `src/game` edits, this is the entry to reference — revert is two small blocks. Cross-refs: D156 (original guard + Facility outro hang), D309/D310 (ruled out for this freeze — no AI involvement).
+67 -34
View File
@@ -680,27 +680,14 @@ s32 chraiitemsize(u8 *AIList, s32 offset)
case AI_ObjectRocketLaunch:
return sizeof(AiObjectRocketLaunchRecord);
case AI_PRINT:
#ifdef PORT
/* D310 / RULE-2-SIGNOFF (2026-09-18, grant: "I want to proceed
* with your implementation plan here"): the PRINT() macro emits
* a 1-byte record (its string is discarded at list-build time),
* but the original sizer below NUL-scans forward, so stepping on
* a PRINT skips ~20 bytes into whatever follows. Both active
* PRINTs in the data are in m_RunToBondPersistent: "no go!" mis-
* lands on 00 00 (phantom GotoNext(0) -> label-not-found -> PC
* restart -> infinite spin, D309's Caverns->intro freeze), and
* "wait" mis-lands on arg bytes reading Label+EndList -> ai()
* returns without saving the offset -> Stop re-issued every tick
* (guards stand still forever, user report 2026-09-18). Sizing
* the record at its true 1-byte size resumes at the next real
* record, exactly as the list source intends (full-list decode
* verified clean to EndList; post-"wait" sequence matches
* chraidata.c line-for-line). Same-engine precedent: PD removed
* PRINT and reassigned slot 0xAD (aiChrCopyProperties).
* See docs/dev/findings.md D310. */
return 1;
#else
{
/* D310: for GLOBAL lists the PRINT record is really 1 byte (the
* macro discards its string at list-build time) and this NUL-scan
* mis-skips into later records -- see d310ItemSize() below, which
* applies the 1-byte size on PORT for global lists only. LOCAL
* (level-setup) lists embed a NUL-terminated string after the
* command byte, so the scan is correct for them and must stay.
* This function itself is left byte-identical to the decomp. */
s32 pos = offset + 1;
while (AIList[pos] != 0)
{
@@ -708,7 +695,6 @@ s32 chraiitemsize(u8 *AIList, s32 offset)
}
return (pos - offset) + 1;
}
#endif
default:
#if defined(ENABLE_LOG)
osSyncPrintf("chraiitemsize: unknown type %d!\n", *AIList);
@@ -717,6 +703,37 @@ s32 chraiitemsize(u8 *AIList, s32 offset)
}
}
#ifdef PORT
/* D310 / RULE-2-SIGNOFF (2026-09-18, grant: "I want to proceed with your
* implementation plan here"): AI_PRINT records differ by list origin.
* Global lists (g_GlobalAILists, macro-built in chraidata.c) discard the
* PRINT string at build time -> the record is 1 byte and chraiitemsize's
* NUL-scan mis-skips ~20 bytes into later records: m_RunToBondPersistent's
* "no go!" lands on 00 00 (phantom GotoNext(0) -> label-not-found -> PC
* restart -> infinite spin, D309's Caverns->intro freeze) and its "wait"
* lands on arg bytes reading Label+EndList (ai() returns without saving the
* offset -> Stop re-issued every tick -> guards stand still forever).
* Level-local lists (g_CurrentSetup.ailists) embed a NUL-terminated string
* after the command byte (e.g. AD 'D' '\n' 00 in Caverns' Trevelyan list),
* so the NUL-scan is CORRECT for them -- sizing those at 1 lands mid-string
* and corrupts execution (Trevelyan ran circles with glitched anims when a
* uniform 1-byte size was tried, live-observed 2026-09-18). Use this
* instead of chraiitemsize at AI record-walk sites; it applies the 1-byte
* size only for lists confirmed global via chraiGetAIListID. Full-list
* decode of m_RunToBondPersistent with the fix walks clean to EndList and
* matches chraidata.c line-for-line. Same-engine precedent: PD removed
* PRINT and reassigned slot 0xAD (aiChrCopyProperties). See findings D310. */
static s32 d310ItemSize(u8 *AIList, s32 offset, bool isGlobalAIList)
{
if (isGlobalAIList && AIList[offset] == AI_PRINT)
{
return 1;
}
return chraiitemsize(AIList, offset);
}
#endif
/**
* Get ID of AIList
* @param AIList: Ailist to get ID of
@@ -760,8 +777,13 @@ s32 chraiGoToLabel(AIRecord *AIList, s32 Offset, u8 LabelNum)
{
s32 listID;
char *debAIListTypeString;
bool isGlobalAIList;
bool isGlobalAIList = FALSE;
#ifdef PORT
/* D310: the record walk below must size PRINT per list origin (global =
* 1 byte, local = embedded string); resolve it once up front. */
(void)chraiGetAIListID(AIList, &isGlobalAIList);
#endif
for (;;)
{
if (AIList[Offset].cmd == AI_Label)
@@ -810,7 +832,11 @@ s32 chraiGoToLabel(AIRecord *AIList, s32 Offset, u8 LabelNum)
return 0;
}
#ifdef PORT
Offset += d310ItemSize((u8 *)AIList, Offset, isGlobalAIList);
#else
Offset += chraiitemsize(AIList, Offset);
#endif
}
}
@@ -3380,15 +3406,17 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType)
case AI_PRINT:
{
#ifdef PORT
/* D309 (diagnosis only, GE_D309=1): every active PRINT in
* the AI data is a 1-byte record (the macro drops its
* string), but chraiitemsize sizes it by NUL-scanning
* forward -- so stepping on one always skips past several
* real records and resumes at whatever byte pattern lands
* there. Log each step-on with the landing command byte:
* landcmd==0x00 (AI_GotoNext) is the D309 spin variant;
* a harmless no-op landing is the suspected "guard stands
* still" variant (Caverns, user report 2026-09-18). */
bool d310global = FALSE;
/* D310: size the PRINT per list origin (see d310ItemSize). */
(void)chraiGetAIListID(AiListp, &d310global);
/* D309 (diagnosis only, GE_D309=1): log each step on a
* PRINT record with the size actually used and the
* command byte we land on. A repeating line (same
* chr/list/off) means ai() re-executes the same path every
* tick without advancing -- the standing-still signature;
* landcmd==0x00 (AI_GotoNext) is the D309 spin variant.
* Capped so a stuck loop can't flood stderr. */
{
static int s_d309p = -1;
static int s_d309pn = 0;
@@ -3396,12 +3424,13 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType)
if (s_d309p < 0) { s_d309p = getenv("GE_D309") != NULL; }
if (s_d309p && s_d309pn < 400)
{
s32 sz = chraiitemsize(AiListp, Offset);
s32 sz = d310ItemSize(AiListp, Offset, d310global);
osSyncPrintf("D309: PRINT chr=%d list=%p off=%d size=%d landcmd=0x%02x\n",
osSyncPrintf("D309: PRINT chr=%d list=%p off=%d size=%d landcmd=0x%02x global=%d\n",
ChrEntityp ? (int)ChrEntityp->chrnum : -1,
(void *)AiListp, (int)Offset, (int)sz,
(int)(AiListp[Offset + sz].cmd & 0xff));
(int)(AiListp[Offset + sz].cmd & 0xff),
(int)d310global);
s_d309pn++;
}
}
@@ -3420,7 +3449,11 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType)
}
#endif
#endif
#ifdef PORT
Offset += d310ItemSize(AiListp, Offset, d310global);
#else
Offset += chraiitemsize(AiListp, Offset);
#endif
break;
}
case AI_MyTimerStart:
+27 -1
View File
@@ -3225,7 +3225,20 @@ void modelSetAnimFrame2WithChrStuff(Model *model, f32 framea, f32 frameb, f32 fr
* NaN. */
if (!(frameb > -1.0e6f && frameb < 1.0e6f))
{
frameb = framea;
frameb = (framea > -1.0e6f && framea < 1.0e6f) ? framea : 0.0f;
}
/* D311: the original guard only validated frameb. If framea
* (model->animframe1) is ALREADY poisoned, "frameb = framea" left
* both garbage and floorFloatToInt saturated the two near-equal
* huge floats to far-apart ints -- observed live in Caverns:
* framea=frameb=-1.67e31 -> curframe=-966612173, endframe=INT_MAX,
* ~3.1e9 loop iterations = permanent hang (D156 guard had fired
* upstream but passed the poison through). Validate framea too;
* with both snapped to 0 the loop does ~0 iterations and the
* animframe1 rewrite at :3447 heals the stored state next tick. */
if (!(framea > -1.0e6f && framea < 1.0e6f))
{
framea = (frameb > -1.0e6f && frameb < 1.0e6f) ? frameb : 0.0f;
}
#endif
@@ -3850,8 +3863,21 @@ void modelTickAnim(struct Model *model, s32 numticks, s32 update_chrstuff)
(void *)model->anim);
fflush(stderr);
}
/* D311: heal the stored state BEFORE falling back to it -- if
* animframe1 is the poison source, "frame = animframe1" would just
* re-derive garbage every tick and pin the anim forever. Snapping
* it to 0 (a valid frame; modelConstrainOrWrapAnimFrame clamps
* further) lets normal accumulation resume next tick. */
if (!(model->animframe1 > -1.0e6f && model->animframe1 < 1.0e6f))
{
model->animframe1 = 0.0f;
}
frame = model->animframe1;
}
if (!(model->animframe2 > -1.0e6f && model->animframe2 < 1.0e6f))
{
model->animframe2 = 0.0f;
}
if (!(frame2 > -1.0e6f && frame2 < 1.0e6f))
{
frame2 = model->animframe2;