docs(cliffs): the runtime probe is SAFE - all four cascade gates READ (#84) - #135
Merged
Conversation
…#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
This was referenced Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#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
applyCliffsLuaSurface::luaCreateEntity+0x6eparams[0x80] = 0params[0x80] = 0this+0x82this+0x83place_as_crater == nullptrmap->[0x240]Map::~Mapwrites 1params[0x80] != 0at construction (0x1007a7abc).applyCliffsbuilds itsEntityCreationParametersatsp+0x20and zeroes that byte withstr wzr, [x26]wherex26 = params + 0x80(0x101623d9c).luaCreateEntitybuilds its own atsp+0x228and zeroes the same byte on both of its paths (strb wzr, [sp, #0x2a8]at0x1019e5ba8and0x1019e5c18). That arm is identifiably the cliff one: it stores the sentinelmov w8, #0x14intoparams+0x87and then overwrites it withLuaTable::getDefault<CliffOrientation>("cliff_orientation").map->[0x240]is "the map is being torn down".Map::~Mapsets it to 1 as its first act, beforeToDeleteList::clear(0x10163461c), andMap::resumebails out on it (0x10163b294). Dozens of unrelatedonDestroyhandlers read the same byte. Nothing during normal play sets it. (Cliff+0x78is theMap:Entity::getGameis[[this+0x78]+0x490].)this+0x82is written 1 by the constructor with no condition (0x1007a7b14).And the one path that would differ is unreachable
Cliff::destroyWithoutCorrection(0x1007aa568) zeroes+0x82around the destroy, so it is the one way to get a no-cascade destroy. It has zero direct callers under a scan of everyBLandBin__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
BLalone and could have missed a tail call. It holds:updateAndFixConnectionsstill 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 verifygreen (182 files, 1507 tests).🤖 Generated with Claude Code
https://claude.ai/code/session_01MmitdaFJxN9Wm8LkQu33kc