@@ -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
327327input 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
382514through ` 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