Skip to content

Commit e31a516

Browse files
wormeymanclaude
andauthored
docs(cliffs): read crossingsForChunk whole - it matches the port (#77)
Records the last unread function in the cliff path (0x10160c9cc, 2244 bytes). Every structural element agrees with what is ported, so these are now confirmed rather than assumed: the bare sample lattice (chunkX*32 + i*grid, no grid_offset), the smoothing knots (lo = i & ~3, hi = min(lo + 4, w - 1), t = (i & 3) / (hi - lo), blend s*bilerp + (1-s)*raw with s at settings+0xd0), which register is smoothed (0x1e0) versus read raw (0x1e4), crossesCliff inlined with the cliffiness > 0.5 gate, both edge array shapes and strides, and the tail call to fixImpossibleCells(this, false). Three more causes falsified with denominators. float vs double is not it: replaying the whole rule with Math.fround at every step changes nothing, 0 of 12,480 raw edges per region and identical placement and orientation. The engine's (int) truncation where the port floors is a real textual difference but inert - the two differ only when max(a,b) < elevation_0, where neither sign case can fire. And the smoothing model is right IN KIND rather than merely unfalsified: cliff_smoothing = 0 makes the orientation error much worse (29.8 -> 71.3%, 8.1 -> 64.1%, 11.7 -> 54.5%). Also notes that the float table where a smoothing-weight table would live is just 0.0..31.0, the SIMD lane-index constant. What survives: fixImpossibleCells in detail, and the choice of expression - which no substitution can test, because the corner-field fixtures capture vulcanus_elevation and cliffiness_basic BY NAME. If a different expression compiles into settings+0x1e0, the substitution agrees with the port for the same reason the port is wrong. Settle that by reading 0x1e0, not by substituting more values. Documentation only. Claude-Session: https://claude.ai/code/session_013eczw9uoWDfN6Wc6kA7UBY Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5ca9734 commit e31a516

1 file changed

Lines changed: 134 additions & 2 deletions

File tree

docs/noise/cliffs-NOTES.md

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,117 @@ pins all of it.
325325

326326
**So the entire residual is in the RULE as ported**, and there is no longer any
327327
input left to suspect: `crossingsForChunk`'s sampling geometry, the
328-
`cliff_smoothing` knot model, or `crossesCliff` itself. `crossingsForChunk`
329-
(`0x10160c9cc`, 2244 bytes) is the one that has never been decompiled whole.
328+
`cliff_smoothing` knot model, or `crossesCliff` itself.
329+
330+
### `CliffGenerator::crossingsForChunk` - read whole 2026-07-30, and it MATCHES
331+
332+
`0x10160c9cc`, 2244 bytes, ends where `getModifiedElevationInterval` begins.
333+
Decompiled whole. Every structural element agrees with the port, so this section
334+
is a list of things that are now confirmed rather than assumed:
335+
336+
- **The sample lattice is bare.** The two input registers are filled with
337+
`x = chunkX*32 + i*gridX`, `y = chunkY*32 + j*gridY` over a 9x9 corner block.
338+
No `grid_offset` anywhere - #70's finding, seen from the producing side.
339+
- **The smoothing knots are exactly as ported.** Per axis:
340+
`lo = i & ~3`, `hi = lo + 4` clamped to `w - 1` (the `uVar25 = uVar25 - 1`
341+
before the loop is where the 7 comes from), `t = (i & 3) / (hi - lo)` with
342+
`t = 0` when the span is degenerate. Blend is
343+
`s * bilerp + (1 - s) * raw` with `s = cliff_smoothing` at `settings + 0xd0`.
344+
- **Which register is smoothed**: `settings + 0x1e0` (elevation) is smoothed into
345+
a fresh array; `settings + 0x1e4` (cliffiness) is read RAW at the same indices.
346+
- **`crossesCliff` is inlined here** and is our rule: both elevations `>= 0`, the
347+
band from `max(a,b)`, reject when the band lands below `elevation_0`, and the
348+
crossing gated on `cliffinessAvg > 0.5` with the two sign cases.
349+
- **The two edge arrays.** Horizontal (corner `(x,y)` to `(x+1,y)`) is
350+
`(h+1) x w` at index `x + w*y`; vertical (`(x,y)` to `(x,y+1)`) is `(w+1) x h`
351+
at index `x + y*(w+1)`. `generateCliffs` then reads L/R from the vertical array
352+
and T/B from the horizontal one. Our `vIndex`/`hIndex` and `cellCode` match.
353+
- Tail call is `fixImpossibleCells(this, false)`, confirming the `false`.
354+
355+
#### Three more causes falsified, with denominators
356+
357+
- **`float` vs `double` is NOT it.** The engine does all of this at 32 bits and
358+
the port at 64, which fits the signature (one corner flipping near a band
359+
boundary) well enough to be worth testing. Replaying the whole rule with
360+
`Math.fround` at every step changes **nothing**: 0 of 12,480 raw edges differ
361+
per region, and placement and orientation are identical.
362+
- **The `(int)` vs `Math.floor` difference is real but inert.** The engine
363+
truncates toward zero where the port floors. They differ only when
364+
`max(a,b) < elevation_0`, and there both give "no crossing" - the band lands at
365+
or below `elevation_0`, and both corners are below it, so neither sign case can
366+
fire. Included in the 0-of-12,480 above.
367+
- **The smoothing model is right IN KIND, not merely unfalsified.** Running
368+
Vulcanus with `cliff_smoothing = 0` makes the orientation error much worse -
369+
29.8% -> 71.3%, 8.1% -> 64.1%, 11.7% -> 54.5% - so the knot blend is doing real
370+
and correct work. That is the control the disassembly reading needed.
371+
- The float table at `0x102cf9cf0` that a smoothing-weight table would live in is
372+
just `0.0 .. 31.0`, the SIMD lane-index constant for the vectorised
373+
register-fill loop. There is no weight table.
374+
375+
#### What that leaves
376+
377+
Fields exact, rule structurally confirmed, precision irrelevant, smoothing
378+
confirmed - and 12.5% of shared cells still carry the wrong crossings. The
379+
candidates that survive:
380+
381+
1. **`fixImpossibleCells` in detail.** It only accounts for 1.8 points of the
382+
12.5 as ported, but "our sweep differs subtly from the game's" is not excluded
383+
by that - a different sweep could be both closer AND worth more.
384+
2. ~~**The choice of expression, which no substitution can test.**~~ **Closed
385+
from the data, 2026-07-30.** The worry was real - the corner-field fixtures
386+
capture `vulcanus_elevation` and `cliffiness_basic` **by name**, so if a
387+
different expression fed `settings + 0x1e0` the substitution would agree with
388+
the port for the same reason the port is wrong, exactly like the corner-fields
389+
fixture captured at the port's own assumed lattice. But the routing is
390+
explicit in `space-age/prototypes/planet/planet-map-gen.lua:13-14`, in the
391+
block carrying Vulcanus's `cliff_elevation_interval = 120` /
392+
`cliff_elevation_0 = 70`:
393+
394+
```lua
395+
cliffiness = "cliffiness_basic",
396+
cliff_elevation = "cliff_elevation_from_elevation",
397+
```
398+
399+
and `cliff_elevation_from_elevation` is literally `expression = "elevation"`
400+
(`core/prototypes/noise-programs.lua:288`), which Vulcanus routes at
401+
`vulcanus_elevation`. Worth knowing what the alternative was: the **default**
402+
`cliff_elevation` is `cliff_elevation_nauvis`
403+
(`10 + 30 * (nauvis_hills - nauvis_hills_cliff_level)`), a completely
404+
different field - so this was a live way to be wrong, in the same shape as the
405+
`cliff_smoothing` default that cost issue #18 two months.
406+
407+
### The FFF on cliffs (#219) - checked, and mostly confirms the binary
408+
409+
https://factorio.com/blog/post/fff-219 is the design writeup. Read 2026-07-30 so
410+
nobody spends the time again. It confirms the shape: an elevation threshold per
411+
cell edge, an **independent** `cliffiness` noise layer applied equally to the
412+
north-south and east-west edges ("Cliffiness only determines small-scale
413+
placement to ensure that there are passages through any sufficiently long cliff
414+
face"), a 4x4-tile cell, chunk-at-a-time generation, and that an earlier
415+
slope-based rule was dropped because it produced cells no cliff graphic could
416+
represent.
417+
418+
Two things to be careful with:
419+
420+
- **It is from 2017 and describes the pre-2.0 algorithm.** Where it and the
421+
2.1.12 binary disagree, the binary wins.
422+
- **Its description of the repair pass is a simplification, and following it
423+
would be a bug.** The FFF says the generator "removes edges marked as
424+
cliff-crossing until no cell has more than 2 'cliff-crossing' edges". That is
425+
strictly weaker than what the binary does. Of the 20 placing codes, 8 have one
426+
crossing and 12 have two - but there are `C(4,2) * 2 * 2 = 24` possible
427+
two-crossing codes, so **12 two-crossing codes are illegal**, the ones whose
428+
two crossings disagree in direction. A "count <= 2" predicate would leave those
429+
in place, and `generateCliffs` asserts and aborts on an illegal code, so the
430+
engine cannot be using it. `fixImpossibleCells`' real predicate, read off the
431+
jump table, is `isCliffPlaced(code)` plus code 0 - which is what is ported.
432+
433+
The FFF's one operational detail agrees with the port: it notes the pass
434+
prioritises chunk boundaries because neighbouring chunks must independently agree
435+
there, which is the same constraint as "a boundary edge is not clearable".
436+
437+
FFF #390 (Noise expressions 2.0) and #401 (new terrain, new planet) were checked
438+
for cliff-expression naming and have nothing on it.
330439

331440
### `EntityMapGenerationTask::generateCliffs` - full body read 2026-07-30
332441

@@ -378,6 +487,29 @@ for placement.
378487

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

490+
**FFF #386 explains why it exists and corroborates both readings**
491+
(https://factorio.com/blog/post/fff-386). Craters were originally going to be
492+
collidable decoratives, and were made cliffs instead because:
493+
494+
> "the collision boxes are always rectangles so hitting invisible corners is
495+
> annoying, and there's a flat part in the middle that looks buildable but isn't.
496+
> We also wanted some partial craters which would mean even more special
497+
> collision rules. The solution we came up with is more like a ring of special
498+
> cliffs where sections of the ring can randomly be removed."
499+
500+
Two things follow, and both match what was read out of the binary. **Cliff
501+
collision boxes really are plain rectangles** - said outright by the developers,
502+
which is the independent confirmation `CLIFF_ORIENTATION_COLLISION_BOX` and the
503+
`rotbb` AABB derivation never had. And a **ring with randomly dropped sections**
504+
is not a lattice structure at all, which is why these positions are fractional
505+
(`-1184.375, 814.98828125`) and why they carry orientations from the same 20-value
506+
enum without ever touching `crossingsForChunk`.
507+
508+
They also cannot be issue #18's residual, which is worth stating with the count
509+
rather than by argument: of the three cliff-entity regions, `[0,0]` has **zero**
510+
crater-cliffs and `[1500,1500]` has **zero**. All 8 are in `[-1200,800]`. The
511+
region with the worst crossing error has none of them.
512+
381513
`space-age/prototypes/decorative/decoratives-vulcanus.lua:2776` defines it
382514
through `scaled_cliff_crater` with `autoplace.probability_expression =
383515
"crater_cliff"`, and `planet-map-gen.lua:122` lists it under Vulcanus's **entity**

0 commit comments

Comments
 (0)