Skip to content

docs(cliffs): the runtime probe is SAFE - all four cascade gates READ (#84) - #135

Merged
wormeyman merged 1 commit into
mainfrom
cliffs-probe-preconditions
Aug 4, 2026
Merged

docs(cliffs): the runtime probe is SAFE - all four cascade gates READ (#84)#135
wormeyman merged 1 commit into
mainfrom
cliffs-probe-preconditions

Conversation

@wormeyman

Copy link
Copy Markdown
Owner

#134 recorded Cliff::onDestroy's four cascade gates and warned that a Lua or editor destroy "need not" satisfy them - which would make #127's runtime probe vacuous rather than failed. That warning is withdrawn.

It was stated from inference ("map generation evidently satisfies all four, because #113 measured the cascade running"), and inference outrunning evidence is the recurring failure mode on this issue. Reading the bytes settles it, the other way.

All four gates, on both paths

gate cascade needs applyCliffs LuaSurface::luaCreateEntity
entity flag bit 5 of +0x6e clear params[0x80] = 0 params[0x80] = 0
this+0x82 set ctor writes 1 unconditionally same ctor
this+0x83 set place_as_crater == nullptr same
map->[0x240] zero only Map::~Map writes 1 same
  • Bit 5 is exactly params[0x80] != 0 at construction (0x1007a7abc). applyCliffs builds its EntityCreationParameters at sp+0x20 and zeroes that byte with str wzr, [x26] where x26 = params + 0x80 (0x101623d9c). luaCreateEntity builds its own at sp+0x228 and zeroes the same byte on both of its paths (strb wzr, [sp, #0x2a8] at 0x1019e5ba8 and 0x1019e5c18). That arm is identifiably the cliff one: it stores the sentinel mov w8, #0x14 into params+0x87 and then overwrites it with LuaTable::getDefault<CliffOrientation>("cliff_orientation").
  • map->[0x240] is "the map is being torn down". Map::~Map sets it to 1 as its first act, before ToDeleteList::clear (0x10163461c), and Map::resume bails out on it (0x10163b294). Dozens of unrelated onDestroy handlers read the same byte. Nothing during normal play sets it. (Cliff+0x78 is the Map: Entity::getGame is [[this+0x78]+0x490].)
  • this+0x82 is written 1 by the constructor with no condition (0x1007a7b14).

And the one path that would differ is unreachable

Cliff::destroyWithoutCorrection (0x1007aa568) zeroes +0x82 around the destroy, so it is the one way to get a no-cascade destroy. It has zero direct callers under a scan of every BL and B in __text, and no pointer in any vtable - the only four pointers to it in the whole file are in the STAB debug tables. entity.destroy() cannot land on it by accident.

A self-check that came free

That widened scan also re-ran #134's other caller claim, which had been made on BL alone and could have missed a tail call. It holds: updateAndFixConnections still has exactly one caller, CliffEditor::buildCliffs. No correction needed.

Result

The probe is unblocked and is the next thing to build. Documentation only - no code, no fixtures, no behaviour change. pnpm run verify green (182 files, 1507 tests).

🤖 Generated with Claude Code

https://claude.ai/code/session_01MmitdaFJxN9Wm8LkQu33kc

…#84)

#134 recorded `Cliff::onDestroy`'s four cascade gates and warned that a Lua or
editor destroy "need not" satisfy them, which would make #127's runtime probe
vacuous. **That warning is withdrawn.** It was stated from inference - "map
generation evidently satisfies all four, because #113 measured the cascade
running" - and inference is what keeps going wrong on this issue.

| gate | cascade needs | `applyCliffs` | `luaCreateEntity` |
| --- | --- | --- | --- |
| entity flag bit 5 of `+0x6e` | clear | `params[0x80] = 0` | `params[0x80] = 0` |
| `this+0x82` | set | ctor writes 1 unconditionally | same ctor |
| `this+0x83` | set | `place_as_crater == nullptr` | same |
| `map->[0x240]` | zero | only `Map::~Map` writes 1 | same |

- Bit 5 is exactly `params[0x80] != 0` at construction. `applyCliffs` builds its
  params at `sp+0x20` and zeroes that byte with `str wzr, [x26]`, `x26 = params
  + 0x80` (`0x101623d9c`). `luaCreateEntity` builds its own at `sp+0x228` and
  zeroes the same byte on both paths (`0x1019e5ba8`, `0x1019e5c18`) - and that
  arm is identifiably the cliff one, storing the `0x14` sentinel into
  `params+0x87` before `LuaTable::getDefault<CliffOrientation>`.
- `map->[0x240]` is "the map is being torn down": `Map::~Map` sets it as its
  first act, before `ToDeleteList::clear` (`0x10163461c`), and `Map::resume`
  bails on it. Dozens of unrelated `onDestroy` handlers read the same byte.
  `Cliff+0x78` is the `Map` - `Entity::getGame` is `[[this+0x78]+0x490]`.
- The one path that WOULD differ, `Cliff::destroyWithoutCorrection`, is
  unreachable: zero direct callers under a scan of every `BL` AND `B` in
  `__text`, and no pointer in any vtable - only four in the STAB tables.

The same widened scan re-confirms #134's other caller claim, which had been made
on `BL` alone and could have missed a tail call: `updateAndFixConnections` still
has exactly one caller, `CliffEditor::buildCliffs`.

Documentation only - no code, no fixtures, no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmitdaFJxN9Wm8LkQu33kc
@wormeyman
wormeyman merged commit ea810e9 into main Aug 4, 2026
6 checks passed
@wormeyman
wormeyman deleted the cliffs-probe-preconditions branch August 4, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant