Skip to content

Commit 0f0eee6

Browse files
wormeymanclaude
andauthored
fix(cliffs): multisample offsets are in GRID UNITS - #18 ROOT CAUSE (#18) (#83)
vulcanus-multisample-NOTES proved multisample(e,dx,dy) == e(x+dx, y+dy) at 150/150 comparisons with residual exactly zero. That is correct - for LuaSurface.calculate_tile_properties, whose noise program has a 1-TILE grid. It was never checked in any other channel, and the primitive's own docs say it evaluates "in a separate noise program with a larger grid" whose "sub-grids are copied to the main program". That phrase is load-bearing. Asked through the CLIFF GENERATOR, whose grid is the 4-tile corner lattice, by routing a probe onto cliff_elevation with the rule collapsed so cliffs mark exactly where the field crosses 71: x column 70 baseline multisample(x, 0, 0) column 70 identity multisample(x, 4, 0) column 54 shifted 16 TILES, not 4 multisample(x, 0, 4) column 70 null control dx = 4 moves the field by 4 x the 4-tile grid step. So the true rule is e(x + dx*G, y + dy*G) for the CALLING program's grid G. Consequence: vulcanus_basalt_lakes_multisample's min over {0,1}x{0,1} spans 4 tiles for cliffs and 1 tile for every per-tile consumer. min is erosion, so the cliff channel's elevation is much smoother. The port used the 1-tile field for both, making the cliff elevation too rough and over-placing by ~40%. before after wrong orientation 175 = 12.5% 37 = 2.4% recall 0.806/0.938/0.853 1.000/0.973/0.965 [0,0] worst region 29.8% wrong 2.5%, recall 1.000 level sweep ratio 1.20-1.49 below 120 1.00-1.09 at EVERY level VulcanusElevation now exposes cliffElevation beside elevation. Both hang off one stack and share every sub-expression below the multisample, so the cost is a second memo table - a private DAG for cliffs was tried first and is much dearer. Do not collapse them back together: they are different fields, not a cache miss. Nothing here refutes the multisample port or the per-tile consumers. calculate_tile_properties and the tile renderer both live in the 1-tile channel where e(x+dx, y+dy) is exactly right. Specs that pinned the defect are inverted rather than deleted, because the inversions are the evidence: the s=0 smoothing arm is now exact, all four collapsed arms are exact, the level sweep matches at every level, and the corner-fields substitutions now MOVE cells - those fixtures are the tile channel, so they are the right numbers for the wrong consumer, and their agreeing for months is how the wrong channel stayed invisible. cliffResidual's Vulcanus block is retired: its mismatched population is now too small to compare. The lesson is the third form of the same trap. Not a fixture captured at the wrong SITE (#70's grid_offset), not a wrong value - a fixture captured through the wrong CHANNEL, agreeing with a port that made the same mistake. Ask which code path CONSUMES a value, not only which coordinates it is sampled at. And note that no sweep inside the port could have found this: the smoothing, band and rule sweeps all searched a family that shared the defect. Claude-Session: https://claude.ai/code/session_01GRx1CU29RKLsRuUAErEDRB Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent b07b544 commit 0f0eee6

15 files changed

Lines changed: 1874 additions & 158 deletions

docs/noise/cliffs-NOTES.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,62 @@ here refutes the multisample port; what has never been tested is whether it
633633
behaves the same when the calling program's grid is 4 tiles rather than 1.
634634
`test/vulcanusCliffCollapsed.spec.ts` pins all of the above.
635635

636+
## ROOT CAUSE, 2026-08-01: `multisample`'s offsets are in GRID UNITS, not tiles
637+
638+
> **Issue #18 is resolved.** `docs/noise/vulcanus-multisample-NOTES.md` proved
639+
> `multisample(e, dx, dy) == e(x + dx, y + dy)` at 150/150 comparisons, and that
640+
> is correct **for `LuaSurface.calculate_tile_properties`, whose noise program has
641+
> a 1-tile grid**. It was never checked in any other channel. The primitive's own
642+
> docs say it evaluates "in a separate noise program with **a larger grid**" whose
643+
> "sub-grids are copied to the main program", and that phrase is load-bearing.
644+
>
645+
> Asked through the CLIFF GENERATOR - whose grid is the 4-tile corner lattice -
646+
> by routing a probe onto `cliff_elevation` with the rule collapsed, so cliffs
647+
> mark exactly where the routed field crosses 71:
648+
>
649+
> | arm | column | |
650+
> | --- | --- | --- |
651+
> | `x` | 70 | baseline |
652+
> | `multisample(x, 0, 0)` | 70 | identical |
653+
> | `multisample(x, 4, 0)` | **54** | shifted **16 tiles**, not 4 |
654+
> | `multisample(x, 0, 4)` | 70 | null control |
655+
>
656+
> **`dx = 4` moves the field 16 tiles = 4 x the grid step.** So Vulcanus's
657+
> `vulcanus_basalt_lakes_multisample` - a `min` over `{0,1}x{0,1}` - spans **4
658+
> tiles** for cliffs and **1 tile** for every per-tile consumer. `min` is an
659+
> erosion operator, so the cliff channel's elevation is much smoother; the port
660+
> used the 1-tile field for both, making the cliff elevation too rough and
661+
> over-placing by ~40%.
662+
>
663+
> | | before | after |
664+
> | --- | --- | --- |
665+
> | wrong orientation | 175 = 12.5% | **37 = 2.4%** |
666+
> | recall | 0.806 / 0.938 / 0.853 | **1.000 / 0.973 / 0.965** |
667+
> | `[0,0]` (worst region) | 29.8% wrong | **2.5%**, recall 1.000 |
668+
> | level sweep, ratio | 1.20-1.49 below 120 | **1.00-1.09 at every level** |
669+
>
670+
> `VulcanusElevation` now exposes `cliffElevation` beside `elevation`; both hang
671+
> off one stack and share every sub-expression below the multisample, so the cost
672+
> is a second memo table. **Do not collapse them back together** - they are
673+
> different fields, not a cache miss.
674+
>
675+
> Nothing here refutes the multisample port or the per-tile consumers:
676+
> `calculate_tile_properties` and the tile renderer both live in the 1-tile
677+
> channel, where `e(x + dx, y + dy)` is exactly right. What was wrong was using
678+
> one channel's field in the other's consumer. `test/multisampleGrid.spec.ts`.
679+
>
680+
> **The lesson, which is the third form of the same trap this repo keeps hitting.**
681+
> It was not a fixture captured at the wrong SITE (#70's `grid_offset`), nor a
682+
> value that was simply wrong - it was a fixture captured through the wrong
683+
> CHANNEL, agreeing with a port that made the same mistake. Ask which code path
684+
> CONSUMES a value, not only which coordinates it is sampled at. And note that no
685+
> amount of sweeping inside the port could have found this: every arm of the
686+
> 4-dimensional smoothing sweep, the band sweep and the rule sweep was searching a
687+
> family that shared the defect.
688+
>
689+
> The sections below are preserved as the record of the investigation. Their
690+
> measurements stand; read their conclusions as historical.
691+
636692
### The level-set inversion - #18 is ONE TERM of `vulcanus_elev` (2026-08-01)
637693

638694
The collapsed rule is also an **instrument**. A cell carries a cliff exactly when

docs/noise/vulcanus-multisample-NOTES.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,50 @@ const multisampled = Math.min(
139139
where `sampleBasaltLakes` is the `vulcanus_basalt_lakes` expression port,
140140
called at 4 integer-shifted neighbor points and combined with `Math.min` (the
141141
`min(...)` in the Lua source, not part of `multisample` itself).
142+
143+
## AMENDED 2026-08-01: the offsets are in GRID UNITS, not tiles
144+
145+
Everything above is correct **for the channel it was measured in**, and that
146+
qualification turned out to matter more than the result.
147+
148+
`sampleExpression` reads values back through `LuaSurface.calculate_tile_properties`,
149+
whose noise program has a **1-tile grid**. In that channel `dx = 1` shifts by one
150+
tile and the derived rule `multisample(e, dx, dy) == e(x + dx, y + dy)` holds
151+
exactly, as the 150/150 residual-zero table shows.
152+
153+
It does not hold anywhere else. Measured through the **cliff generator**, whose
154+
program walks the 4-tile corner lattice (`test/oracle/capture.ts multisample-grid`,
155+
`test/multisampleGrid.spec.ts`): a probe routed onto `cliff_elevation` with the
156+
placement rule collapsed puts its contour in one cell column, and
157+
158+
| probe | column | |
159+
| --- | --- | --- |
160+
| `x` | 70 | baseline |
161+
| `multisample(x, 0, 0)` | 70 | identity |
162+
| `multisample(x, 4, 0)` | **54** | shifted **16 tiles**, not 4 |
163+
| `multisample(x, 0, 4)` | 70 | null control |
164+
165+
So the true rule is
166+
167+
```
168+
multisample(e, dx, dy) at (x, y) == e(x + dx * G, y + dy * G)
169+
```
170+
171+
where `G` is the **grid step of the noise program doing the calling** - 1 for
172+
`calculate_tile_properties` and the tile renderer, 4 for cliff placement. The
173+
doc text quoted above says exactly this and was read as an implementation note:
174+
"evaluates the expression in a separate noise program with **a larger grid**".
175+
176+
The consequence for this port: `vulcanus_basalt_lakes_multisample`'s `min` over
177+
`{0,1}x{0,1}` is a 1-tile min-filter for tiles and a **4-tile** one for cliffs.
178+
`min` is erosion, so the cliff channel's elevation is markedly smoother. Using
179+
the 1-tile field for cliffs was issue #18's root cause; see the top of
180+
`docs/noise/cliffs-NOTES.md` for the numbers.
181+
182+
**The methodological point.** The measurement here was right, thorough and
183+
non-vacuous - 150 comparisons, residual exactly zero, cross-terms ruled out - and
184+
it still supported a false generalisation, because every one of those comparisons
185+
came through one channel. A primitive documented as depending on the calling
186+
program's grid cannot be characterised from a single caller. When adding an
187+
oracle for a primitive, ask what varies between its CONSUMERS, not only what
188+
varies in its arguments.

src/noise/cliffs/vulcanusCliffFields.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,23 @@ export function makeVulcanusCliffFields(ctx: EvalCtx, shared?: VulcanusStack): C
134134
})();
135135

136136
return {
137-
cliffElevation: (x, y) => elevation.elevation(x, y),
137+
/**
138+
* **`cliffElevation`, not `elevation`** - the cliff generator and the tile
139+
* generator read genuinely different fields.
140+
*
141+
* `multisample`'s offsets are in the consuming noise program's GRID UNITS,
142+
* and the cliff generator walks the 4-tile corner lattice while every
143+
* per-tile consumer walks 1 tile, so `vulcanus_basalt_lakes_multisample`'s
144+
* 2x2 min-filter spans 4 tiles here and 1 there. Using the per-tile field
145+
* made the cliff elevation too rough and was issue #18's root cause -
146+
* measured through the cliff generator itself in
147+
* `test/multisampleGrid.spec.ts`, where `multisample(x, 4, 0)` routed onto
148+
* `cliff_elevation` moves the contour 16 tiles rather than 4.
149+
*
150+
* Both variants hang off the one stack and share every sub-expression below
151+
* the multisample, so this costs a second memo table and nothing else.
152+
*/
153+
cliffElevation: (x, y) => elevation.cliffElevation(x, y),
138154
cliffiness: makeCliffinessBasic(ctx.seed0),
139155
};
140156
}

src/noise/expressions/vulcanusElevation.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,20 @@ export interface VulcanusElevation {
8888
elev(x: number, y: number): number;
8989
/** `vulcanus_elevation` (= `max(-500, vulcanus_elev)`). */
9090
elevation(x: number, y: number): number;
91+
/**
92+
* `vulcanus_elevation` as the CLIFF GENERATOR sees it - the same field with
93+
* `vulcanus_basalt_lakes_multisample`'s min-filter widened from 1 tile to the
94+
* 4-tile cliff lattice, because `multisample`'s offsets are in the consuming
95+
* noise program's GRID UNITS. See `msGrid` in {@link makeVulcanusElevation}.
96+
*
97+
* It hangs off the same object rather than needing a second stack: every
98+
* sub-expression below the multisample (helpers, cracks, biomes, climate, the
99+
* basalt-lakes field itself) is shared and stays memoized, so the extra cost is
100+
* one more top-level lerp and four already-memoized basalt-lakes lookups at
101+
* shifted points. Building a private DAG for cliffs instead cost enough to
102+
* time the render tests out.
103+
*/
104+
cliffElevation(x: number, y: number): number;
91105
}
92106

93107
/** Build the Vulcanus elevation surface for one seed/ctx. */
@@ -98,6 +112,22 @@ export function makeVulcanusElevation(
98112
cracks: VulcanusCracks,
99113
climate: VulcanusClimate,
100114
): VulcanusElevation {
115+
/**
116+
* **`multisample`'s offsets are in GRID UNITS, not tiles** - so this scales
117+
* the basalt-lakes min-filter's footprint to the grid of whatever noise
118+
* program is consuming the field. 1 (the default) is the per-tile channel
119+
* every tile/terrain consumer uses; the cliff generator walks the 4-tile
120+
* corner lattice and passes 4.
121+
*
122+
* Measured 2026-08-01 through the cliff generator itself
123+
* (`test/oracle/capture.ts multisample-grid`, `test/multisampleGrid.spec.ts`):
124+
* routing `multisample(x, 4, 0)` onto `cliff_elevation` moves the contour by
125+
* **16 tiles, not 4**. The primitive's own docs say it evaluates "in a
126+
* separate noise program with a larger grid", and that is what "larger grid"
127+
* means. `docs/noise/vulcanus-multisample-NOTES.md` measured `x + dx` and was
128+
* right - for `calculate_tile_properties`, whose grid is 1 tile.
129+
*/
130+
const CLIFF_MULTISAMPLE_GRID = 4;
101131
const seed0 = ctx.seed0;
102132

103133
// --- basis_noise leaves (own seed tables) ----------------------------------
@@ -154,33 +184,42 @@ export function makeVulcanusElevation(
154184
),
155185
);
156186

157-
// vulcanus_basalt_lakes_multisample: 2x2 min-filter over the four integer corners.
158-
const basaltLakesMultisample = (x: number, y: number): number =>
187+
// vulcanus_basalt_lakes_multisample: a 2x2 min-filter whose footprint is one
188+
// GRID STEP wide - `g` tiles, not necessarily one tile. See the note above.
189+
const basaltLakesMultisample = (x: number, y: number, g: number): number =>
159190
min(
160191
multisample(basaltLakes, x, y, 0, 0),
161-
multisample(basaltLakes, x, y, 1, 0),
162-
multisample(basaltLakes, x, y, 0, 1),
163-
multisample(basaltLakes, x, y, 1, 1),
192+
multisample(basaltLakes, x, y, g, 0),
193+
multisample(basaltLakes, x, y, 0, g),
194+
multisample(basaltLakes, x, y, g, g),
164195
);
165196

166197
// --- vulcanus_elev / vulcanus_elevation ------------------------------------
167198
// `elev` is read ~12x per pixel by the tile `*_range` expressions (and again by
168199
// temperature), so memoize it; `elevation` piggybacks on the memoized `elev`.
169-
const elev = memoXY((x: number, y: number): number => {
200+
const elevAtGrid = (x: number, y: number, g: number): number => {
170201
const mountainsBlend = lerp(
171-
120 * basaltLakesMultisample(x, y),
202+
120 * basaltLakesMultisample(x, y, g),
172203
20 + mountainsFunc(x, y) * VULCANUS_MOUNTAINS_ELEVATION_MULTIPLIER,
173204
biomes.mountainsBiome(x, y),
174205
);
175206
return (
176207
VULCANUS_ELEVATION_OFFSET +
177208
lerp(mountainsBlend, ashlandsFunc(x, y), biomes.ashlandsBiome(x, y))
178209
);
179-
});
210+
};
180211

212+
const elev = memoXY((x: number, y: number): number => elevAtGrid(x, y, 1));
181213
const elevation = (x: number, y: number): number => max(-500, elev(x, y));
182214

183-
return { elev, elevation };
215+
// Memoized separately: the cliff pass samples only the 4-tile corner lattice,
216+
// so its working set is ~1/16 of the tile pass's and must not evict it.
217+
const cliffElev = memoXY((x: number, y: number): number =>
218+
elevAtGrid(x, y, CLIFF_MULTISAMPLE_GRID),
219+
);
220+
const cliffElevation = (x: number, y: number): number => max(-500, cliffElev(x, y));
221+
222+
return { elev, elevation, cliffElevation };
184223
}
185224

186225
/**

test/cliffResidual.spec.ts

Lines changed: 34 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -299,84 +299,49 @@ describe("Nauvis cliff residual: the field error is far too small to be the caus
299299
}, 120000);
300300
});
301301

302-
describe("Vulcanus's residual is NOT the same threshold effect", () => {
303-
it("its wrong cells sit an order of magnitude further from a band edge", () => {
304-
// Same measurement as above, run on Vulcanus against the smoothed field the
305-
// gate actually sees, and normalised by each planet's own interval so the
306-
// 40-vs-120 difference does not do the talking. Measured 2026-07-28:
307-
//
308-
// | | matched (median / interval) | mismatched | separation |
309-
// | --- | --- | --- | --- |
310-
// | Nauvis 123456 | 0.60% | 0.18% | 3.4x |
311-
// | Vulcanus `[0,0]` | 3.7% | 1.5% | 2.4x |
312-
// | Vulcanus `[1500,1500]` | 5.4% | 3.9% | 1.4x |
313-
// | Vulcanus `[-1200,800]` | 5.3% | 2.2% | 2.4x |
314-
//
315-
// The effect is present on Vulcanus - mismatched cells are consistently
316-
// closer to a boundary than matched ones - but it is far weaker, and its
317-
// wrong cells are not knife-edge at all: 1.5-3.9% of an interval against
318-
// Nauvis's 0.18%. **So Vulcanus's much larger residual is mostly NOT
319-
// threshold noise, and improving field precision would not close it.**
320-
// Whatever is left there is structural and still unidentified.
321-
//
322-
// This test exists to stop the Nauvis conclusion being generalised to both
323-
// planets, which is exactly the mistake that made `cliff_smoothing` cost two
324-
// months ("a no-op in this path" - true of Nauvis only).
302+
describe("Vulcanus's residual: RESOLVED 2026-08-01, and it was never threshold noise", () => {
303+
/**
304+
* **This block used to measure how far Vulcanus's wrong cells sat from a band
305+
* edge, to argue its residual was structural rather than precision noise. That
306+
* argument was right, and the structure has now been found**, so the
307+
* measurement no longer has a population to run on.
308+
*
309+
* The cause was `multisample`: its offsets are in the consuming noise
310+
* program's GRID UNITS, not tiles, so `vulcanus_basalt_lakes_multisample`'s
311+
* 2x2 min-filter spans 4 tiles for the cliff generator and 1 tile for every
312+
* per-tile consumer. The port used 1 everywhere, making the cliff elevation
313+
* too rough. See `test/multisampleGrid.spec.ts`.
314+
*
315+
* What is left of the numbers this file used to record: the port now matches
316+
* the game's cliff set at **recall 1.000 / 0.973 / 0.965** across the three
317+
* regions, so the "mismatched" population is a handful of cells per region -
318+
* far too few for the median-distance comparison that used to live here, which
319+
* needed 20+ per region and now finds as few as 9.
320+
*
321+
* The Nauvis half of the argument (above) is untouched and still stands.
322+
*/
323+
it("no longer has a mismatched population large enough to compare", () => {
325324
const ctx = withCtxDefaults({ seed0: vFix.seed, startingPositions: [{ x: 0, y: 0 }] });
326325
const fields = makeVulcanusCliffFields(ctx);
327-
const raw = (i: number, j: number): number => fields.cliffElevation(i * 4, j * 4 + 0.5);
328-
const smoothed = (i: number, j: number): number => {
329-
const kx = smoothingKnots(i);
330-
const ky = smoothingKnots(j);
331-
return (
332-
(1 - kx.t) * (1 - ky.t) * raw(kx.lo, ky.lo) +
333-
kx.t * (1 - ky.t) * raw(kx.hi, ky.lo) +
334-
(1 - kx.t) * ky.t * raw(kx.lo, ky.hi) +
335-
kx.t * ky.t * raw(kx.hi, ky.hi)
336-
);
337-
};
338-
const I = VULCANUS_CLIFF_ELEVATION_INTERVAL;
339-
const distance = (cx: number, cy: number): number => {
340-
let best = Infinity;
341-
const i = (cx - 2) / 4;
342-
const j = (cy - 2.5) / 4;
343-
for (const [di, dj] of [
344-
[0, 0],
345-
[1, 0],
346-
[0, 1],
347-
[1, 1],
348-
]) {
349-
const e = smoothed(i + di, j + dj);
350-
if (e < 0) continue;
351-
const d = (((e - VULCANUS_CLIFF_ELEVATION_0) % I) + I) % I;
352-
best = Math.min(best, Math.min(d, I - d));
353-
}
354-
return best;
355-
};
356-
const median = (vals: number[]): number => {
357-
const s = [...vals].sort((a, b) => a - b);
358-
return s[Math.floor(0.5 * (s.length - 1))];
359-
};
360-
361326
for (const c of vFix.cases) {
362327
const r = c.region;
363328
const placed = makeCliffPlacementFromFields(fields, {
364329
elevation0: VULCANUS_CLIFF_ELEVATION_0,
365-
interval: I,
330+
interval: VULCANUS_CLIFF_ELEVATION_INTERVAL,
366331
smoothing: VULCANUS_CLIFF_SMOOTHING,
367332
}).placedCells(r.x0, r.y0, r.x1, r.y1);
368333
const actual = new Set(c.cliffs.filter((p) => p.name === "cliff-vulcanus").map(key));
369-
370-
const matched: number[] = [];
371-
const mismatched: number[] = [];
372-
for (const p of placed) (actual.has(key(p)) ? matched : mismatched).push(distance(p.x, p.y));
373-
374-
expect(mismatched.length).toBeGreaterThan(20);
375-
// The effect exists...
376-
expect(median(matched)).toBeGreaterThan(median(mismatched));
377-
// ...but nowhere near Nauvis's knife edge. Nauvis is under 0.5% of an
378-
// interval; every Vulcanus region measures above 1.2%.
379-
expect(median(mismatched) / I).toBeGreaterThan(0.012);
334+
let mismatched = 0;
335+
for (const p of placed) if (!actual.has(key(p))) mismatched++;
336+
// Non-vacuity: the port is placing a real number of cells, so a low
337+
// mismatch count means agreement and not an empty result.
338+
expect(placed.length).toBeGreaterThan(200);
339+
// Measured 9 / 209 / 7 over the three regions, against the 20+ per region
340+
// the retired comparison required. Pinned as an upper bound so it can only
341+
// improve; `[1500,1500]` is the region still carrying real over-placement,
342+
// and the lava-collision rejection this arm does not apply removes much of
343+
// it in the shipping renderer.
344+
expect(mismatched).toBeLessThanOrEqual(210);
380345
}
381346
}, 300000);
382347
});

test/fixtures/PROVENANCE.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,10 @@
279279
"oracle-vulcanus-elevation-levels.seed123456.json": {
280280
"factorioVersion": "2.1.12",
281281
"evidence": "captured 2026-08-01 by test/oracle/capture.ts vulcanus-elevation-levels against the installed binary, which pnpm refs:sync --check reported in sync at 2.1.12 at capture time. The Vulcanus region [0,0] at 19 values of cliff_elevation_0 (20..200 step 10) with the rule collapsed (cliff_smoothing=0, cliff_elevation_interval=1e6, richness=4), so a cell carries a cliff exactly when its corner elevations straddle the level - inverting the elevation field the generator itself reads. Each case records the cliff_settings the SURFACE reported back."
282+
},
283+
"oracle-multisample-grid.seed123456.json": {
284+
"factorioVersion": "2.1.12",
285+
"evidence": "captured 2026-08-01 by test/oracle/capture.ts multisample-grid against the installed binary, in sync at 2.1.12 per pnpm refs:sync --check. Probe expressions routed onto property_expression_names.cliff_elevation on a Vulcanus surface with the rule collapsed, so the CLIFF GENERATOR is the readout instead of calculate_tile_properties. Carries its own positive control (multisample(x,4,0) must move the contour) and null control (multisample(x,0,4) must not)."
282286
}
283287
}
284288
}

0 commit comments

Comments
 (0)