Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/noise/cliffs-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,57 @@ The full port, the four extracted tables and the scoring are in
`src/noise/cliffs/cliffConnections.ts` and `test/cliffConnections.spec.ts`; the
result is in `vulcanus-cliffs-NOTES.md`.

### The connection system, decompiled whole (2026-08-03)

`updateConnections` was read end to end after #132 made it the lead. Seven facts
came out, none of which the port had modelled. They are in
`cliffConnections.ts`' module comment with their addresses; the ones that change
what anyone should try next:

- **`Cliff` gates its ENTIRE connection system on one byte**, `this+0x83`, which
the constructor sets to `proto->place_as_crater == nullptr` (`cset w8, eq` on
`proto+0xb90`, `0x1007a7b1c`). `CliffPrototype` has exactly one optional
pointer property and `scaled_cliff_crater` is the only thing that sets it. The
byte gates `updateConnections`, `getNeighbor`, `destroyEnd`, `onDestroy`'s
cascade, `connectEnd` and `getConnections`. So **`crater-cliff` has no
connections at all** - do not attribute anything crater-shaped to these rules.
- **`Cliff::onDestroy`'s cascade has FOUR gates, not zero** (`0x1007a8854`
onwards): entity flag bit 5 of `+0x6e` clear, `+0x82` set, `+0x83` set, and
`map->[0x240]` zero. Bit 5 is `params[0x80] != 0` at construction, `+0x82` is
what `destroyWithoutCorrection` zeroes. Map generation evidently satisfies all
four (#113 measured the cascade running), but **a Lua or editor probe will
not necessarily** - which is a live hazard for the runtime probe #127 asked
for, and the reason to check the four before believing such a probe.
- **There is no second connection pass during map generation.**
`Cliff::updateAndFixConnections` (`0x1007a94d0`) is called from exactly one
site in the whole `__text` segment - `CliffEditor::buildCliffs`
(`0x100371948`), found by scanning every `BL` encoding rather than by grep. A
later chunk never revisits an earlier chunk's cliffs.
- **`Cliff::destroyEnd` refuses to `forceDestroy`** when entity flag bit 4 of
`+0x6e` is set (`0x1007a8e40`); it returns with the orientation UNCHANGED, not
merely undestroyed. And after a successful shrink it re-searches its new
bounding box and destroys colliding non-cliff entities (`0x1007a8e9c`
onwards) - a side effect nothing in this repo models.
- **`Cliff::getNeighbor`** (`0x1007a8c58`) accepts an entity only on an exact
prototype-id match AND an exact position match, so a different cliff
prototype on an adjacent cell is not a neighbour.

### The tile half reads ZERO for an ungenerated chunk, and it is border-only

`Surface::wouldCollide` -> `constCollideWithTile` (`0x100732eec`) ->
`Surface::checkTileCollisions` (`0x101b579e0`), which per tile calls
`Surface::getEffectiveTileID` (`0x10049399c`) and **skips the tile when the id
is 0** (`tst w0, #0xffff; b.eq`). `getEffectiveTileID` returns exactly 0 when
the chunk is absent - every range and null arm from `0x100493a60` falls to
`mov w27, #0x0`.

That is a real border-only channel, because the largest cliff half-extent is
3.371 tiles and only the outer ring sits closer than that to a chunk edge (1.5 -
2.5 tiles, against 5.5 - 6.5 for every interior ring). **Measured, it is inert**
- see `test/cliffResidualBoxCrossesChunkEdge.spec.ts`. Note its direction before
reaching for it: an absent chunk makes the game KEEP a cliff, so its signature
is a false rejection, not a missed destruction.

### `crater-cliff` is not on the cliff lattice - confirmed, not assumed

**FFF #386 explains why it exists and corroborates both readings**
Expand Down
117 changes: 117 additions & 0 deletions docs/noise/vulcanus-cliffs-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Vulcanus cliffs - port notes

> ## UPDATE 10, 2026-08-03: the border effect is ORIENTATION-BLIND, and the connection system is decompiled
>
> Read with UPDATE 9, which it sharpens rather than replaces.
>
> **The border enrichment is orientation-blind.** UPDATE 9 said `updateConnections`
> is "the only rule that treats border cells differently at all". It is not: a
> cliff's collision box reaches 3.371 tiles and only the outer ring sits closer
> than that to a chunk edge, so 16 of 20 orientations cross an edge and no
> interior cell can. That rival was scored on data already on disk and is
> **inert** - 7 of 1407 crossing border cells unexplained against 14 of 2479
> non-crossing, where no-information predicts 7.6, with the losing condition
> registered first. The enrichment survives in both halves at 2.7x the interior
> rate. So the effect is blind to orientation, which is what a cell-index gate
> looks like and not what a geometric reach looks like, and
> `updateConnections` is the only border-only channel left unscored.
>
> **`Cliff`'s connection system is now decompiled whole** (seven facts, addresses
> in `cliffs-NOTES.md` and `cliffConnections.ts`). Three change what to try next:
>
> - **`onDestroy`'s cascade has FOUR gates**, including `map->[0x240] == 0` and an
> entity flag set from the creation parameters. Map generation satisfies all
> four; **a Lua or editor probe need not** - which is a hazard for exactly the
> runtime probe #127 asked for. Check the four before believing such a probe.
> - **`crater-cliff` has no connections at all.** The whole system is gated on
> `proto->place_as_crater == nullptr`. Nothing crater-shaped belongs to these
> rules.
> - **There is no second connection pass during map generation.**
> `updateAndFixConnections` is called from exactly one site in the binary,
> `CliffEditor::buildCliffs`.
>
> ## UPDATE 9, 2026-08-03: the residual is on CHUNK BORDERS (2.91 sigma), and the ore mechanism is a CLOSED IMPOSSIBILITY
>
> Two results supersede the framing below. Read this before acting on any earlier
Expand Down Expand Up @@ -3676,3 +3706,90 @@ the game exactly (686 = 686).

The residual is **concentrated, not a background rate** - which is itself a
constraint on any mechanism proposed for it.

## The border effect is ORIENTATION-BLIND: a rival mechanism found and REFUTED (2026-08-03, #84)

The section above hands `updateConnections` the lead on one argument: chunk
borders are its entire domain, and "nothing else in the pipeline treats border
cells differently at all". **That clause was false**, and finding the
counter-example is what makes the lead worth more rather than less.

`test/cliffResidualBoxCrossesChunkEdge.spec.ts`. No new capture - twelve regions
already on disk.

### The rival: the collision BOX also singles out the border ring

Pure arithmetic over two things already in the repo. The largest half-extent in
`CLIFF_ORIENTATION_COLLISION_BOX` is **3.371 tiles**, and:

| | distance from cell centre to the nearest chunk edge |
| --- | --- |
| border ring (`ix` or `iy` is 0 or 7) | 1.5 - 2.5 tiles |
| **every** interior ring | 5.5 - 6.5 tiles |

So **16 of the 20 orientations reach across a chunk edge from the outer ring and
none of them can from anywhere else.** Asserted against the real population, not
left as arithmetic: 1407 crossing cells, every one of them on a border.

And there is a rule waiting on the far side of that edge, read out of the binary
the same day. `applyCliffs` rejects through `Surface::wouldCollide`
(`0x10160c088`) -> `constCollideWithTile` (`0x100732eec`) ->
`Surface::checkTileCollisions` (`0x101b579e0`), which per tile calls
`Surface::getEffectiveTileID` (`0x10049399c`) and **skips the tile when the id
is 0**. `getEffectiveTileID` returns exactly 0 when the chunk is absent. So a box
reaching into a not-yet-generated chunk reads no tile and does not collide, while
this port reads the real tile everywhere.

### The discriminator, and its losing condition, both registered first

The two mechanisms differ in exactly one observable: **whether the ORIENTATION
matters.** `updateConnections`' gate is `!onChunkBorder`, a cell-index test that
is blind to orientation. The box mechanism fires only when the box actually
crosses. Written down before the run: *a non-crossing border rate that is not
lower than the crossing one refutes the box mechanism.*

### Result: refuted, and not marginally

| population | unexplained | rate |
| --- | --- | --- |
| interior | 9 / 4484 | 0.20% |
| border, box **crosses** an edge | 7 / 1407 | **0.50%** |
| border, box does **not** cross | 14 / 2479 | **0.56%** |

Spread the 21 border cells over the two populations in proportion and the
crossing share is **7.6**; observed **7**. The sharper predicate is not weaker
than hoped, it is indistinguishable from no information at all - and the
non-crossing rate is the higher of the two.

The mechanism's own direction fails independently. An absent chunk makes the game
KEEP a cliff we destroy, so its signature is a **false rejection** whose blocking
tile lies across a chunk edge. There are 9 false rejections here and **zero** of
them have any blocking tile on the far side of an edge. The predicate is not
vacuous - cells with a blocking tile across an edge do exist in the population.

### What this buys

**The border enrichment is orientation-blind**, which is what a cell-index gate
looks like and not what a geometric reach looks like. That is a property the
2.91-sigma result did not have, and it was not obtainable from the border
statistic alone. Both halves keep the 2.7x enrichment over the interior, so the
partition had something to find and found nothing.

Every border-only channel this port can name has now been scored. Only
`updateConnections` is left unscored, and #127 already showed why: it cannot be
scored from map-generation output at all.

### Seven engine facts came out of the same read - see `cliffs-NOTES.md`

The one that changes what to try next: **`Cliff::onDestroy`'s cascade has four
gates**, including `map->[0x240] == 0` and an entity flag set from the creation
parameters. Map generation satisfies all four - #113 measured the cascade
running - but **a Lua or editor probe need not**, and that is precisely the
runtime probe #127 asked for. Check the four before believing such a probe.

Two more worth carrying: **`crater-cliff` has no connections at all** (the whole
system is gated on `proto->place_as_crater == nullptr`), and
`updateAndFixConnections` is called from exactly one site in the binary,
`CliffEditor::buildCliffs` - so **there is no second connection pass during map
generation**, and the "a later chunk cleans up after an earlier one" reading is
closed.
43 changes: 42 additions & 1 deletion src/noise/cliffs/cliffConnections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,64 @@
* chunk there is no placement-order dependence: every cell of the chunk exists
* by the time any of them checks its neighbours.
*
* `Cliff::updateConnections` (`0x1007a90d4`), reduced to the cliff arm:
* `Cliff::updateConnections` (`0x1007a90d4`), decompiled whole 2026-08-03:
*
* ```
* if (this->[0x83] == 0) return; // see the gate below
* for side in neighborSidesForOrientation(orientation): // the ORIGINAL orientation
* if chunk at position+4*dir(side) is absent or its status <= 0x31: continue
* n = getNeighbor(side)
* if n == null || !isCliffConnected(side, this->orientation, n->orientation):
* destroyEnd(side)
* ```
*
* The orientation the loop ITERATES is read once, before the loop; the one it
* COMPARES is re-read from `this+0x80` inside it (`0x1007a91e8`), so a
* `destroyEnd` earlier in the loop is visible to the sides after it. This module
* does the same, and that agreement is not accidental - it is why a cell can
* lose two ends in one pass.
*
* **The `this+0x83` gate is `proto->place_as_crater == nullptr`.** The
* constructor computes it in one instruction, `cmp x8, #0x0; cset w8, eq` on
* `proto + 0xb90` (`0x1007a7b1c`), and `CliffPrototype` has exactly one optional
* pointer-valued property, `place_as_crater`, set only by `scaled_cliff_crater`
* in `base/prototypes/entity/entity-util.lua:325`. The same byte gates
* `getNeighbor`, `destroyEnd`, `onDestroy`'s cascade, `connectEnd` and
* `getConnections`. So `cliff-vulcanus` runs all of it and **`crater-cliff` runs
* none of it** - craters are outside the connection system entirely, which is
* worth knowing before attributing anything crater-shaped to these rules.
*
* `Cliff::destroyEnd(side)` (`0x1007a8d40`) rewrites the orientation to the one
* with `side` replaced by `none`, and `forceDestroy()`s the cliff when that
* leaves no end at all. `Cliff::onDestroy` (`0x1007a8770`) then calls
* `destroyEnd(opposite(side))` on each connected neighbour - which is why a
* destroyed cell can take a non-border neighbour with it, and why this is a
* cascade rather than a filter.
*
* **Three conditions on that cascade are unmodelled here, deliberately.**
* `onDestroy` reaches its neighbour loop only when all four of these hold
* (`0x1007a8854`-`0x1007a8874`): entity flag bit 5 of `this+0x6e` CLEAR,
* `this+0x82` set, `this+0x83` set, and `map->[0x240]` zero. Bit 5 is
* `params[0x80] != 0` at construction and `this+0x82` is what
* `Cliff::destroyWithoutCorrection` (`0x1007aa568`) temporarily zeroes, so both
* are about who is doing the destroying rather than about the cliff. The port
* models map generation, where #113 measured the cascade running - 1058 of 1058
* at `[1500,1500]` - so all four evidently hold there. They are recorded because
* a future probe that destroys a cliff from Lua or the editor will NOT get the
* same behaviour, and that is exactly the probe #127 asked for.
*
* **There is no second connection pass during map generation.**
* `Cliff::updateAndFixConnections` (`0x1007a94d0`) looks like one and is not: a
* scan of every `BL` in `__text` finds it called from exactly one site,
* `CliffEditor::buildCliffs` (`0x100371948`). So a chunk generated later never
* revisits an earlier chunk's cliffs, and the "generation order lets a later
* pass clean up" reading is closed.
*
* `Cliff::getNeighbor` (`0x1007a8c58`) searches a one-tile box at the neighbour
* position and accepts an entity only on an exact prototype-id match
* (`0x1007a8ce4`) AND an exact position match (`0x1007a8cfc`). A `crater-cliff`
* sitting on a neighbouring cell is therefore not a neighbour.
*
* **None of the four tables below is a guess.** Each was extracted from the
* arm64 slice and each agrees exactly with what the orientation NAMES say, which
* is the check that they were read in the right order rather than assumed:
Expand Down
14 changes: 12 additions & 2 deletions test/cliffResidualBorderEnrichment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,18 @@ import { withCtxDefaults } from "../src/noise/eval/ctx";
*
* **Chunk borders are `Cliff::updateConnections`' entire domain.** `applyCliffs`
* gates it on `tryToAddCliff`'s fifth argument, which is `!onChunkBorder`, so it
* runs on the chunk's outer ring and nowhere else. It is the only rule in the
* pipeline that treats border cells differently at all.
* runs on the chunk's outer ring and nowhere else.
*
* > **CORRECTION, 2026-08-03 (#84):** this paragraph used to end "It is the only
* > rule in the pipeline that treats border cells differently at all." That was
* > wrong. A cliff's collision box reaches up to 3.371 tiles and a border cell's
* > centre is 1.5-2.5 tiles from the chunk edge, so 16 of the 20 orientations
* > reach across an edge from the outer ring and none can from anywhere else -
* > a second border-only channel. `cliffResidualBoxCrossesChunkEdge.spec.ts`
* > scores it and finds it **inert**: 7 of 1407 crossing border cells are
* > unexplained against 14 of 2479 non-crossing, where no-information predicts
* > 7.6. That strengthens the conclusion below rather than weakening it - the
* > enrichment is orientation-BLIND, which is what a cell-index gate looks like.
*
* And it is exactly the rule the port has the weakest grip on:
*
Expand Down
Loading