PART B -- the thing Jahni asked for: "any way to test those in game?" A candidate existed only as measurements and PNGs. Now he can stand in one. Select the VoxelWorld actor in PIE -> Live Edit|Composer -> ComposerSeed / ComposerCandidateIndex / ComposerTargetStrateIndex / bComposerRollStructure -> click Apply Composer Candidate. The button and the automation tests call the SAME roll wrapper, which was the load-bearing requirement: a second roll path could drift, and then he would be judging a different world than the contact sheet describes. A determinism sample is logged on apply (seed, index, recipe, archetype, metrics, epoch, eight-point density verdict) so drift is caught the day it happens rather than in a month. Hazards handled, since this is the first composer code to touch the live runtime path: preflight materialisation before any live mutation, generation and decoration tasks drained, PassagesVersion bumped to invalidate thread_local caches, reuse of the existing RegenerateAllChunks / Epoch invalidation rather than a new path, no Initialize() or GeneratePassages() during apply (the re-entrancy trap), DiffLayer locks untouched, and custom recipes conservatively force ClassifyTile to Mixed. Scope is deliberate: a DENSITY-ONLY overlay. Layout height, authored passages, content, water and biome metadata stay from the existing slot; RebuildStrates() clears it. A full composed layout replacement needs a safe rebuild path that does not exist yet. Editor-only, verified: a Shipping build succeeds with the bridge excluded (96/96 actions), so §3.1's "offline only, never on a player's machine" still holds. ⚠️ UNVERIFIED BY DESIGN: a button cannot be tested headlessly and Codex was told not to claim it works. Jahni presses it and confirms the visual plus the "determinism=PASS" log line. PART A -- why rooms read angular, measured not guessed. One of three room shapes is VoxelSDF::RoundedBox: ShapeA = (.8 Rxy, .8 Rxy, .8 Rz) ShapeR = .25 Rxy so rounding is 31% of the horizontal half-extent but 78% of the vertical. Corners are rounded; the broad planar FACES survive. SmoothMin (blend 4) softens room/tunnel junctions not isolated corners; the cave warp bends geometry but preserves flat regions; roughness is surface-scale only. The name is accurate -- "rounded box" is not "blob". NOT ACTED ON. Jahni corrected his own read: the angular archetype is Maze (lattice source, rectilinear BY DESIGN and correct), and TunnelNetwork is "alright-ish". Codex's recommendation is recorded for if he ever wants it: ~5-10% boxes, 10-20% capsules, rest ellipsoids, or a larger ShapeR. Also closed REVIEW_FINDINGS EVoxelPassageType/Style as WONTFIX, enacting a 2026-08-16 judgement that recommended closure and was left open. Build clean (Editor + Shipping), 25 succeeded / 0 failed, run by me. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
80 KiB
COMPOSER-NOTES.md — the world composer
What this is: the design of the system that invents the depths, plus every code finding and hazard that constrains it. Engineering document.
What this is NOT: the game. The premise, the city, the economy, death, combat, seasons and the community layer live in GDD.md. Where this file needs one of those, it states the constraint and points there for the reasoning. Do not restate game design here — duplicated documents drift, and then neither is authoritative.
History: this file was a chronological transcript of the 2026-08-17 design conversation, ~1400
lines, game design and generator design tangled together. It was distilled on the same day. The full
original transcript is in git at commit b781e3b — go there for the reasoning behind anything that
reads as bald assertion here.
1. The vision
The system must INVENT strates and worlds from Jahni's tagged raw content plus a seed.
⚠️ Read this correction; it is the whole point. The first reading of the vision was that Jahni authors strates and the system arranges them. Wrong. His words:
"I'm not pretty happy to make strates forever, that's actually the opposite. If I could never make any new strate myself and have it do it itself would be genius."
The floor of authoring is RAW CONTENT + TAGS — operators, meshes, materials, sounds, creatures, music — tagged, with multiple fitting tags each. The overlaps between tags are where the system finds combinations he never planned that still cohere; single-tag buckets cannot do that.
Everything above that floor is composed and measured: strates, layouts, danger, rarity, names.
A strate becomes generated data, not an asset. UVoxelStrateDefinition survives as a struct the
composer fills in; nobody opens it in the editor any more.
This is op-stack "Phase 3", and it is REOPENED
The 2026-08-16 OPSTACK-HANDOFF.md says in bold "Do not start Phase 3." That is superseded.
Asked what the world was missing, Jahni described exactly Phase 3 plus a composer above it.
It is not a restart of the refactor. The eight operators are live, composable, and production. What never happened is anything composing them except a port of the old archetype switch. The machine was built and never fed.
Ambition level
The seed invents parameters within authored ranges — not preset-picking. Two seeds differ in everything: not the same strates spawn, nor the same details.
2. What the game requires of the generator
Constraints only. See GDD.md for why each exists.
| requirement | source |
|---|---|
| Every strate has an entrance and an exit, exposed to air, and findable. The one invariant that may never fail. | GDD §7 |
| Danger = depth × per-strate multiplier, rare exceptions in both directions. Drives creature, hazard and loot selection — never what kind of place a strate is. | GDD §7 |
| Danger must never scale generation parameters. It selects content and may bias which terrain ops are drawn. If depth multiplied a density parameter, strate 80 would be geometrically broken rather than hard. | GDD §9 |
Anomalous strates are rare and must NOT blend with their neighbours. EVoxelStrateTransition::Hard already expresses this. |
GDD §7 |
| A share of grounded, plausible strates is guaranteed — strangeness needs ordinary to be strange against. | GDD §7 |
| Fightable space is a generation guarantee, not a hope. Boss arena ≈ 3–4 chunks across, ordinary fight space ≈ 2, camera headroom ≈ 1 chunk (1 chunk = 32 voxels × 25 cm = 8 m). Spacious and vertical is the target. | GDD §12 |
| Essential materials need a placement DENSITY, not a location — "somewhere in a gigantic strate" is unfindable. Optional materials may be absent. | GDD §9 |
| The depths are bounded ⇒ a whole strate can be examined, so the entrance/exit law can be proved rather than sampled. | GDD §6 |
| No bottom ⇒ generate a deep buffer (~30 strates) offline and extend in a later batch. | GDD §6 |
Boss strates every ~5 slots ⇒ the layout needs a periodic fixed-slot rule; Settings->FixedStrates is a map of absolute indices today. |
GDD §6 |
| A season freezes generation; a wipe frees it. The composer may ship imperfect and improve every cycle. | GDD §13 |
| The manifest must support names, quests, fast travel, material lookup and death locations. | GDD §7, §10, §14 |
3. The composer
3.1 It runs ONCE PER SEASON, OFFLINE — this sets the whole budget
Everyone shares one seed per season, so there is exactly one set of depths per season, ever. The composer never runs on a player's machine and never at load. It runs once, on a build box.
The problem is not "fast enough for a loading screen" — it is "runs overnight." So it can generate a thousand candidate strates, measure them all, keep the best, validate at any resolution, and later run a model over the candidates at zero runtime cost. And Jahni reviews and vetoes the season before it publishes — the system does the work, he keeps the veto.
What ships is small: the season's seed plus the chosen strates' parameter vectors. Clients generate terrain from those, deterministically, exactly as today. No composition is shipped or run in the normal game path. The editor-only PIE inspection button described below is a deliberate debugging exception: it overlays one already-built slot so the owner can walk a measured candidate; it is not cooked, does not compose a season, and is cleared by RebuildStrates.
3.2 Structure: how a stack gets assembled
The current shipping builders, for reference (structural posts included; TunnelNetwork's builder is also the Underwater builder):
TunnelNetwork / Underwater (20) rock source -> room graph (SDF) -> SdfCarve -> 12 density modifiers -> worm source -> structural
FlatPlain / CrystalChamber (6) slab-void source -> grid columns -> structural
Maze (8) rock source -> lattice corridors (SDF) -> SdfRoughness (SDF) -> SdfCarve -> structural
SurfaceWorld (6) surface column -> overhang -> structural
VerticalShafts (9) rock source -> shaft field (SDF) -> SdfRoughness (SDF) -> SdfCarve -> ledges -> structural
FloatingIslands (8) VOID source -> island blobs (SDF) -> SdfRoughness (SDF) -> SdfFill -> structural
⭐ Most ordering is MECHANICAL — derive it, do not author it. Roughness sits on the SDF before conversion in Maze and on density after conversion in TunnelNetwork; CODEMAP confirms they are two different ops for exactly that reason. An op's legal position is fixed by which channel it reads and which it writes. ⇒ Every operator declares its channel reads/writes. Dependencies form a DAG and any topological sort of that DAG is a legal stack — no authored order, and it produces orderings nobody wrote down.
Built status — 2026-09-04: EVoxelOpChannel now names the two fields actually carried by
FVoxelOpSample (Density and Sdf); all 29 concrete density operators declare reads, writes, and
additive/transformative behavior. FVoxelOpStack::Add snapshots that metadata, and
ValidateChannelOrder checks the resulting channel DAG without entering the voxel loop.
VoxelForge.OpStack.ChannelDAG builds all eight shipping stacks and validates them: 22 tests
succeeded, 0 failed, 0 not run.
⭐ Root polarity is a one-bit identity lever. FloatingIslands is Maze's op classes rooted in a void source with a fill instead of a rock source with a carve — and you get islands instead of tunnels.
⛔ "Corrective ops must be FUSED" — WITHDRAWN 2026-09-04. The premise was false.
This section used to assert: "FFloorBiasMod exists only to undo what FCaveRoughnessMod did to
floors... fuse them into one op with two internal phases." It does not, and they must not be fused.
Read the math rather than the names. FCaveRoughnessMod adds its own displacement:
D += TotalRough. Eleven ops later, FFloorBiasMod computes an independent term
B = NormZ² × FloorBias from Sdf, room geometry and WorldZ, and adds it: D += B.
It never reads or subtracts TotalRough. It is not a correction to roughness at all — it acts on
the accumulated density and belongs late in the stack, exactly where it is.
⇒ No fuse. No corrects dependency either — there is nothing being corrected. The general
principle ("an operator whose only purpose is to correct another is not a separate operator") stands
as a rule for ops that genuinely are corrective; this simply was not an instance of it.
Two lessons, both cheap here and expensive elsewhere:
- The claim came from reading a name and a comment. Ten Density-writing ops sit between the two
(
FCaveTerraceMod,FLayerLineMod,FRibbingMod,FCaveOverhangMod,FCaveCliffMod,FScallopMod,FCaveArchMod,FRoomColumnMod,FDomeMod,FPinchMod) — a fact one grep would have shown before the design paragraph was written. - The old parenthetical "fusing with identical order and math leaves the field bit-identical" was also wrong: reordering across ten accumulating ops cannot be bit-identical, because IEEE-754 addition is not commutative. ⚠️ But do not let that argument decide anything on its own — bit-identity across builds was never required (see §8: peer determinism is same build, all machines). Had the fuse been semantically right, a slightly changed field would have been an acceptable price. It was the semantics that killed it, not the bits.
One declaration covers the rest: an op is additive (small displacement, commutes freely — shuffle at will) or transformative (clamps, multiplies, gates — position matters, needs explicit placement). “Commutes” here is algebraic: IEEE-754 float accumulation is not generally bitwise commutative, so a shuffle still needs the equivalence gate.
The structure roll:
root polarity -> shape source -> conversion (follows from polarity) -> draw k modifiers legal in the resulting channel space (k itself rolled) -> structural post appended automatically.
With ~5 shape sources and ~15 modifiers choosing 4–8, that is tens of thousands of structurally
distinct stacks before a single parameter is touched — which is what carries early variety while the
corpus is small.
⇒ Rule: vary STRUCTURE aggressively, vary PARAMETERS conservatively. Shift the ratio as the corpus grows.
Tier 4b implementation (2026-09-04): FVoxelOpStackRecipe is the shipped-manifest shape: it
stores the one-bit RootPolarity, root/source/conversion EVoxelStrateOpClass IDs, an ordered list
of modifier IDs, and an EVoxelStrateParamBlock on every entry. The four structural posts are
deliberately not a field in the recipe. VF_BuildStackFromRecipe appends spine, vertical seal,
passage carving, and XY edge seal in that fixed order, so an invented strate cannot omit primordial
law. The conversion ID is checked against polarity rather than rolled independently.
The roller's modifier catalogue is a deterministic array of declared ops, not a compatibility list.
It asks each op for its channel reads/writes, additive contract, required resources, and provided
resources, then draws 4–8 unique entries from the legal remainder. The channel declarations exposed
a real missing dependency: FCaveTerraceMod and the other room-relative cave modifiers use the
FRoomGraphSource room cache through a non-owning pointer, even though their channel masks only said
Density. FShaftLedgeMod similarly consumes shaft state, and FOverhangShelfMod consumes the
surface-column state. RequiredResources / ProvidedResources now declare and validate those
dependencies (RoomGeometry, ShaftGeometry, SurfaceColumn); the roller cannot place such an op before
its provider. FCaveRoughnessMod and FWormFieldSource were checked and do not require room state:
they read only their declared channels and parameters.
The parameter decision is deliberately visible: structure and parameter rolls are separate. Each
candidate rolls the six native family blocks independently from the exact-archetype corpus using the
Tier 4a conservative blend/jitter procedure. An op consumes the block named on its recipe entry;
structural posts consume the shape source's family block. There is no unsafe 73-field cross-family
blend. The current SurfaceWorld block is still rolled and carried even when a structure has no
surface-column op, so the recipe remains a complete family-parameter record.
The focused 64-candidate run produced 64 distinct recipes, 0 invalid recipes, and 0 box-verdict
violations. It checked 650,859 voxels across 489 proved boxes. Survival was 46/64 (71.9%) under the
same non-vacuous, largest-share ≥ 0.50, exact Connected criteria as Tier 4a; that lower rate is an
expected consequence of aggressive structural exploration, not a tuning target. The test also
rerolls every recipe and rebuilt stack, checks WorldRadiusVoxels == 0, and feeds the novel stack
through the offline Tier 2 sampler; no runtime generation path calls this API. The final focused
test took 76.466 s. The complete VoxelForge namespace then passed 24/24 tests (0 failed,
0 not run) in 339.797 s.
3.2a Editor walk-through hand-off (not a runtime composer)
AVoxelWorld::ApplyComposerCandidate is the one editor-only bridge from this offline API to a live PIE world. It calls VF_RollStrateCandidate(Corpus, ComposerSeed, ComposerCandidateIndex, bComposerRollStructure). That wrapper delegates to the exact parameter/recipe calls exercised by the offline composer tests, then installs the result as a temporary density override on ComposerTargetStrateIndex. The layout slot's authored definition, height, content, and generated passages are intentionally unchanged; this avoids calling GeneratePassages while Initialize is half-built. The action pauses/drains generation, bumps the manager layout version, and reuses RegenerateAllChunks so GenerationEpoch invalidates stale mesh results and the normal streamer rebuilds the world.
The Details-panel properties are ComposerSeed, ComposerCandidateIndex, ComposerTargetStrateIndex, and bComposerRollStructure, under Live Edit|Composer. The button is Apply Composer Candidate; it is usable only during PIE. RebuildStrates clears the temporary override. A post-apply eight-point density check compares the live generator with the same prepared candidate stack bit-for-bit when no diff edits, disturbance post, or surface-biome context makes the standalone oracle incomplete. The log line carries the recipe/archetype and the coarse metrics so it can be matched to a contact-sheet row. This hand-off is editor-only and is not part of the shipped composer contract.
3.3 Parameters: the range problem
Verified facts, not remembered: FStrateGenerationParams has 73 scalar fields (+1 bool) — not
hundreds. The VF_STRATE_PARAM_FIELDS X-macro covers all of them (Alpha is the Lerp function's
parameter, not a field). VoxelStrateTypes.h carries 135 ClampMin and 52 ClampMax across 216
UPROPERTYs, so most fields have no upper bound. Paired min/max fields exist
(MinRoomRadius/MaxRoomRadius, TunnelMin/MaxRadius, RoomFloorCutMin/Max).
Why rolling parameters fails, stated plainly: the coherent region of a 73-dimensional space is a vanishingly thin sheet, and uniform sampling in a box essentially never lands on it. The problem is not wrong ranges — independent rolling destroys the CORRELATIONS that make a parameter set coherent (room spacing ↔ room radius ↔ tunnel length ↔ strate height).
⭐ The design: do not roll parameters — BLEND KNOWN-GOOD ONES.
- Corpus = known-good parameter vectors, seeded by the existing hand-authored strate assets.
- Roll = pick 2–3 parents (weighted), blend with random weights, then jitter.
- Reuse
FStrateGenerationParams::Lerp— it already does exactly this, over exactly this field set, and the world working already tests it. Build nothing. - Constraint satisfaction is FREE: a convex combination preserves every relation that holds in both
parents (
Min <= Maxstays true). The correlations survive because they were never broken. - ⭐ Ranges come from the CORPUS SPREAD — per field, how much it varies across existing strates — not from authored numbers, and not from the clamps. Clamps are "don't type nonsense" limits, not "this makes a good world" limits. Measure, don't declare.
- Clamps are the hard safety net only, never the roll range.
Escaping the convex hull: blending alone only reaches between existing points. Jitter slightly past the corpus; extrapolate deliberately past a parent away from the centroid (riskier, validate harder); and — the real one — PROMOTION: every invented strate that measures well and survives players joins the corpus. Season 1 blends ~6 strates; season 5 blends 200 discovered ones.
⚠️ Caveats: UPROPERTY metadata is editor-only, so the clamp net must be baked at cook time by a
small commandlet walking the struct by reflection. Clamp coverage is partial. A few fields are not
tunables at all (StrateTopWorldZ / StrateBottomWorldZ are runtime Z bounds) and need an explicit
exclusion list. Bools cannot be blended — roll them by probability or inherit from the dominant parent.
✅ Tier 4a status — parameter roll built (2026-09-04)
Public/VoxelStrateComposer.h + Private/VoxelStrateComposer.cpp now implement the corpus and
the deterministic roll. Private/Tests/VoxelForgeComposerParameterRollTest.cpp is the offline
measurement harness. The roll uses the native family blend (FStrateGenerationParams::Lerp for
the tunnel family), chooses 2–3 weighted parents inside one exact archetype group, applies
jitter of ±15% of each field's measured corpus range (max-min), then applies reflected
ClampMin/ClampMax metadata. It does not use clamps as distribution ranges, does not touch
generation, and leaves WorldRadiusVoxels at its default 0. The sibling native parameter structs
are carried in the same corpus entry, so a SurfaceWorld vector is never blended with a
TunnelNetwork vector merely because both happen to be stored in a strate asset.
The original one-vector result was caused by the old loader following DA_Settings' fixed-strate
and pool references. An audit of those references finds one unique path,
/Game/VoxelForge/DA_Strate3.DA_Strate3, whose archetype is TunnelNetwork; that is why the
previous corpus only loaded one asset. The current loader instead asks the Asset Registry for every
project UVoxelStrateDefinition, sorts and de-duplicates the results, accepts /Game packages,
and ignores Saved/Autosaves and Saved/Cooked copies. The scan found 4 project assets, with
no saved-copy, outside-project, duplicate, or unresolved records:
| Asset | Archetype |
|---|---|
DA_Strate1 |
SurfaceWorld |
DA_Strate2 |
FloatingIslands |
DA_Strate3 |
TunnelNetwork |
DA_Strate4 |
TunnelNetwork |
The hand-authored C++ defaults in VoxelStrateTypes.h are also corpus members: one for each of the
8 supported archetypes (TunnelNetwork, FlatPlain, CrystalChamber, Maze, SurfaceWorld,
VerticalShafts, FloatingIslands, Underwater). The fed corpus is therefore 12 members in
8 exact-archetype groups. Group membership is TunnelNetwork=3, FlatPlain=1,
CrystalChamber=1, Maze=1, SurfaceWorld=2, VerticalShafts=1, FloatingIslands=2, and
Underwater=1.
The complete 253-descriptor spread table is emitted by the automation test, including authored and synthetic operation-default sample counts, min/max/mean/standard deviation, excluded/reflected status, and clamp metadata. The current run has 212 zero-range rows and 41 rows with real spread. The shape-spread rows below are retained from the prior report; the newly measured terrain-detail spread is listed explicitly after the liveness experiment.
| Archetype | Field | N | Min | Max | Mean | StdDev |
|---|---|---|---|---|---|---|
| TunnelNetwork | MinRoomRadius | 3 | 10 | 25 | 15 | 7.07106781 |
| TunnelNetwork | MaxRoomRadius | 3 | 30 | 125 | 61.6666667 | 44.7834295 |
| TunnelNetwork | OriginRoomRadius | 3 | 20 | 120 | 53.3333333 | 47.1404521 |
| TunnelNetwork | TunnelMinRadius | 3 | 3 | 9 | 5 | 2.82842712 |
| TunnelNetwork | TunnelMaxRadius | 3 | 7 | 15 | 9.66666667 | 3.77123617 |
| SurfaceWorld | BaseGroundRelative | 2 | 0.200000003 | 0.300000012 | 0.250000007 | 0.0500000045 |
| SurfaceWorld | ElevationRange | 2 | 60 | 200 | 130 | 70 |
| SurfaceWorld | ContinentFrequency | 2 | 0.00100000005 | 0.00600000005 | 0.00350000005 | 0.0025 |
| SurfaceWorld | MountainStrength | 2 | 0.5 | 1 | 0.75 | 0.25 |
| SurfaceWorld | MountainFrequency | 2 | 0.00200000009 | 0.0120000001 | 0.0070000001 | 0.005 |
| SurfaceWorld | SurfaceRoughness | 2 | 2 | 3 | 2.5 | 0.5 |
| SurfaceWorld | ReliefStrength | 2 | 0 | 0.699999988 | 0.349999994 | 0.349999994 |
| SurfaceWorld | ReliefContrast | 2 | 1.60000002 | 2 | 1.80000001 | 0.199999988 |
| SurfaceWorld | TerraceHeight | 2 | 12 | 20 | 16 | 4 |
| SurfaceWorld | BeachWidth | 2 | 8 | 200 | 104 | 96 |
| FloatingIslands | IslandSpacing | 2 | 95 | 1000 | 547.5 | 452.5 |
| FloatingIslands | IslandDensity | 2 | 0.100000001 | 0.5 | 0.300000001 | 0.199999999 |
| FloatingIslands | IslandMinRadius | 2 | 18 | 50 | 34 | 16 |
| FloatingIslands | IslandMaxRadius | 2 | 42 | 500 | 271 | 229 |
| FloatingIslands | ThicknessRatio | 2 | 0.100000001 | 0.699999988 | 0.399999995 | 0.299999993 |
| FloatingIslands | VerticalJitter | 2 | 0.200000003 | 0.600000024 | 0.400000013 | 0.20000001 |
| FloatingIslands | TopFlatten | 2 | 0.600000024 | 0.800000012 | 0.700000018 | 0.099999994 |
| FloatingIslands | SurfaceRoughness | 2 | 0 | 4 | 2 | 2 |
The 34 terrain-detail fields now have the following effective spread. N is shown as
authored+op-defaults=effective; the TunnelNetwork group has 3 authored vectors and Underwater
has 1. The default seed is present only for the 23 live fields, one matching operation type per
field. The values are the test's population statistics (the operation shape defaults happen to be
identical to the FStrate defaults, so only activation fields have non-zero spread here).
| Field | Tunnel N | Tunnel min..max | Tunnel mean | Tunnel SD | Underwater N | Underwater min..max | Underwater mean | Underwater SD | Result |
|---|---|---|---|---|---|---|---|---|---|
| TerraceStepHeight | 3+1=4 | 0..5 | 1.25 | 2.16506351 | 1+1=2 | 0..5 | 2.5 | 2.5 | LIVE |
| TerraceHardness | 3+1=4 | 0.5..0.5 | 0.5 | 0 | 1+1=2 | 0.5..0.5 | 0.5 | 0 | LIVE |
| TerraceNoiseDisplacement | 3+1=4 | 0.5..0.5 | 0.5 | 0 | 1+1=2 | 0.5..0.5 | 0.5 | 0 | LIVE |
| LayerLineSpacing | 3+1=4 | 0..4 | 1 | 1.73205081 | 1+1=2 | 0..4 | 2 | 2 | LIVE |
| LayerLineDepth | 3+1=4 | 0.300000012..0.300000012 | 0.300000012 | 0 | 1+1=2 | 0.300000012..0.300000012 | 0.300000012 | 0 | LIVE |
| OverhangStrength | 3+1=4 | 0..0.5 | 0.125 | 0.216506351 | 1+1=2 | 0..0.5 | 0.25 | 0.25 | LIVE |
| OverhangDepth | 3+1=4 | 5..5 | 5 | 0 | 1+1=2 | 5..5 | 5 | 0 | LIVE |
| OverhangFrequency | 3+1=4 | 0.0599999987..0.0599999987 | 0.0599999987 | 0 | 1+1=2 | 0.0599999987..0.0599999987 | 0.0599999987 | 0 | LIVE |
| RibbingSpacing | 3+1=4 | 0..3 | 0.75 | 1.29903811 | 1+1=2 | 0..3 | 1.5 | 1.5 | LIVE |
| RibbingDepth | 3+1=4 | 0.400000006..0.400000006 | 0.400000006 | 0 | 1+1=2 | 0.400000006..0.400000006 | 0.400000006 | 0 | LIVE |
| CliffStrength | 3+1=4 | 0..0.5 | 0.125 | 0.216506351 | 1+1=2 | 0..0.5 | 0.25 | 0.25 | LIVE |
| ScallopStrength | 3+1=4 | 0..0.5 | 0.125 | 0.216506351 | 1+1=2 | 0..0.5 | 0.25 | 0.25 | LIVE |
| ScallopFrequency | 3+1=4 | 0.100000001..0.100000001 | 0.100000001 | 0 | 1+1=2 | 0.100000001..0.100000001 | 0.100000001 | 0 | LIVE |
| ArchDensity | 3+1=4 | 0..0.100000001 | 0.0250000004 | 0.0433012708 | 1+1=2 | 0..0.100000001 | 0.0500000007 | 0.0500000007 | LIVE |
| ArchMinRadius | 3+1=4 | 3..3 | 3 | 0 | 1+1=2 | 3..3 | 3 | 0 | LIVE |
| ArchMaxRadius | 3+1=4 | 6..6 | 6 | 0 | 1+1=2 | 6..6 | 6 | 0 | LIVE |
| ColumnDensity | 3+0=3 | 0..0 | 0 | 0 | 1+0=1 | 0..0 | 0 | 0 | DEAD |
| ColumnMinRadius | 3+0=3 | 2..2 | 2 | 0 | 1+0=1 | 2..2 | 2 | 0 | DEAD |
| ColumnMaxRadius | 3+0=3 | 5..5 | 5 | 0 | 1+0=1 | 5..5 | 5 | 0 | DEAD |
| PitDensity | 3+0=3 | 0..0 | 0 | 0 | 1+0=1 | 0..0 | 0 | 0 | DEAD |
| PitMinRadius | 3+0=3 | 4..4 | 4 | 0 | 1+0=1 | 4..4 | 4 | 0 | DEAD |
| PitMaxRadius | 3+0=3 | 10..10 | 10 | 0 | 1+0=1 | 10..10 | 10 | 0 | DEAD |
| PitDepth | 3+0=3 | 25..25 | 25 | 0 | 1+0=1 | 25..25 | 25 | 0 | DEAD |
| ChimneyDensity | 3+0=3 | 0..0 | 0 | 0 | 1+0=1 | 0..0 | 0 | 0 | DEAD |
| ChimneyMinRadius | 3+0=3 | 2..2 | 2 | 0 | 1+0=1 | 2..2 | 2 | 0 | DEAD |
| ChimneyMaxRadius | 3+0=3 | 5..5 | 5 | 0 | 1+0=1 | 5..5 | 5 | 0 | DEAD |
| ChimneyHeight | 3+0=3 | 20..20 | 20 | 0 | 1+0=1 | 20..20 | 20 | 0 | DEAD |
| DomeDensity | 3+1=4 | 0..0.150000006 | 0.0375000015 | 0.0649519079 | 1+1=2 | 0..0.150000006 | 0.075000003 | 0.075000003 | LIVE |
| DomeMinRadius | 3+1=4 | 8..8 | 8 | 0 | 1+1=2 | 8..8 | 8 | 0 | LIVE |
| DomeMaxRadius | 3+1=4 | 15..15 | 15 | 0 | 1+1=2 | 15..15 | 15 | 0 | LIVE |
| DomeHeightRatio | 3+1=4 | 0.800000012..0.800000012 | 0.800000012 | 0 | 1+1=2 | 0.800000012..0.800000012 | 0.800000012 | 0 | LIVE |
| PinchDensity | 3+1=4 | 0..0.150000006 | 0.0375000015 | 0.0649519079 | 1+1=2 | 0..0.150000006 | 0.075000003 | 0.075000003 | LIVE |
| PinchStrength | 3+1=4 | 5..5 | 5 | 0 | 1+1=2 | 5..5 | 5 | 0 | LIVE |
| PinchLength | 3+1=4 | 12..12 | 12 | 0 | 1+1=2 | 12..12 | 12 | 0 | LIVE |
The first pass excluded all 34 terrain-detail fields. That was an unmeasured structural claim,
so VoxelForge.Composer.TerrainDetailLiveness now tests it directly. For both the legacy and
operator-stack GetDensityAt paths, it uses a fixed 4,096-point lattice (16×16×16, 4-voxel
spacing, the TunnelNetwork fixture), clears the terrain-operation pool, records a baseline, then
changes only one operation group to substantial values. The two paths produced bit-identical
results. The aggregate verdict is 9 live groups / 23 live fields and 3 dead groups / 11 dead
fields:
| Operation group | Changed samples / 4,096 | Sign changes | Sum abs delta | Max abs delta | Verdict |
|---|---|---|---|---|---|
| Terrace | 1,314 | 40 | 2,177.12156 | 5.09695816 | LIVE |
| LayerLines | 532 | 8 | 163.082606 | 0.64941144 | LIVE |
| Overhang | 569 | 12 | 598.018486 | 4.41974068 | LIVE |
| Ribbing | 933 | 6 | 213.929947 | 1.49233627 | LIVE |
| Cliff | 391 | 3 | 372.185603 | 2.80882835 | LIVE |
| Scallop | 38 | 0 | 5.78068841 | 1.01755953 | LIVE |
| Arch | 418 | 190 | 7,327.12817 | 36.0000019 | LIVE |
| Column | 0 | 0 | 0 | 0 | DEAD |
| Pit | 0 | 0 | 0 | 0 | DEAD |
| Chimney | 0 | 0 | 0 | 0 | DEAD |
| Dome | 227 | 44 | 3,262.50304 | 24.0000019 | LIVE |
| Pinch | 226 | 22 | 1,217.39489 | 16.3533421 | LIVE |
The result is identical for both paths, so each row above represents two measured rows. The
baseline mean was -3.86253725 for every group. Variant means were, respectively, -3.8647801,
-3.82272216, -4.00853786, -3.91476624, -3.86411419, -3.86112595, -5.6513869,
-3.86253725, -3.86253725, -3.86253725, -3.06602772, and -4.1597528 in the table's
order. The live fields are therefore removed from the exclusion list and are now eligible for the
roll. Only the 11 direct Column/Pit/Chimney transport fields plus the two runtime Z bounds remain
excluded.
The dead-field call chain is concrete. UVoxelStrateManager::BuildParamsFromDefinition
(Private/VoxelStrateManager.cpp:1399-1404) returns only Definition->GenerationParams; it does
not merge terrain-op assets. VoxelGenerator::GetDensityAt (Private/VoxelGenerator.cpp:1099-1109)
gets the active definition's TerrainOperations pool and passes it to
VoxelCaveMorphology::BuildChunkCache. The cache applies the selected
UVoxelTerrainOpDefinition at Private/VoxelCaveMorphology.cpp:1741, then consumes
PitDensity/shape at :1744-1764, ChimneyDensity/shape at :1767-1787, and
ColumnDensity/shape at :1790-1803. Sampling uses the resulting SDFCache.Pits and
SDFCache.Chimneys at Private/VoxelGenerator.cpp:1147-1206 and SDFCache.Columns at
:1730-1752; it does not read the direct strata Column/Pit/Chimney slots. The project contains
exactly 1 UVoxelTerrainOpDefinition asset, Content/VoxelForge/Blocks/NewDataAsset.uasset,
so a second asset corpus would have one member and no useful spread.
The composer now seeds each of the 23 live fields with one matching per-operation default from
the UVoxelTerrainOpDefinition header (applied through ApplyTo, with no duplicated constants),
then adds any authored strate values. The current TunnelNetwork group has 3 authored samples + 1
operation-default sample; Underwater has 1 + 1. The dead fields receive no synthetic seed and stay
excluded. The complete test table exposes authored, op-defaults, and effective samples.
Recommendation for the dead trio: do not build an asset corpus for the one current asset. The
right future composer abstraction is direct terrain-op parameter structs/configurations, seeded by
the per-operation defaults and any authored op values, with the rolled op payload carried alongside
FVoxelStrateRollInfo and injected into the transient candidate's terrain-op pool. That requires a
small candidate/materialisation API and a populated-pool liveness/box test; simply jittering the
current direct FStrate slots would remain a silent no-op. This task rolls every field that the
existing direct strata path can actually move and leaves the dead trio neutral until that API exists.
The bool policy is explicit: bTunnelsFlowTowardOrigin is inherited from the dominant parent.
OriginRoomMaxConnections and RoughnessNoiseType retain Lerp's existing SNAP behavior and
are not jittered. In this editor build clamp metadata was available while the table was built; it
is not promised in a cooked runtime, so the commandlet/cook-time bake remains owed.
The controlled 64-candidate run (step 4 sweep, radius 256, max 8,000,000 cells, fixture seed 1337)
kept the existing corpus hash/RNG stream while enabling the live detail fields. It produced
59/64 survivors (92.2%): 60 non-vacuous candidates, 60 with largest component share ≥ 0.50,
63 exact unsnapped arrival→departure law passes, and no roll failures. The complete 64-row table
is emitted by VoxelForge.Composer.ParameterRoll; each row reports archetype, weighted parents,
air fraction, largest share, walkable fraction, feature scale, and arrival→departure verdict.
Feature scale was 0→376 (the prior 190ff38 run was 4→376), while walkable fraction was
0.000000→0.388506 (prior 0.000→0.389). Thus detail variation widened the feature-scale
range only at the low end by four voxels; it did not increase the upper scale or the walkable
spread. The 17 TunnelNetwork/Underwater candidates all carried at least one non-zero live detail
activation, and each of Terrace/LayerLines/Overhang/Ribbing/Cliff/Scallop/Arch/Dome/Pinch appeared
non-zero in all 17. The measurement is therefore not broadly sensitive to these detail fields in
this 64-candidate corpus, which is an empirical result, not a reason to remove the live knobs. The
controlled run took 113.317 s total (0.015 s corpus load, 113.300 s roll/measurement).
The §6.2 check remains non-vacuous: 1,228 boxes proved, 1,634,468 voxels checked, and 0 violations. Its verdict mix was 1,332 Mixed, 553 AllSolid, and 675 AllAir; no candidate violated the box law. The box law used unit step and brute-forced every voxel of each uniform box, while the candidate sweep retained step 4. The test asserts zero violations and keeps the law open to future changed candidates rather than treating this clean run as a license to tune the survival rate.
Season-zero bootstrap follow-up (2026-09-04)
The homogeneous-field trap is now handled explicitly. A field whose measured corpus range is at
or below 1e-6 takes a bootstrap jitter of ±25% of its own magnitude, with a one-native-unit
floor for values near zero; fields with useful spread still take the real ±15% of max-min rule.
The safety clamps remain a final guard and never become roll ranges. FVoxelStrateRollInfo reports
both the number of bootstrap applications and the distinct archetype:field names. The comment in
the implementation makes the lifecycle explicit: this is a bootstrap for a starved season-zero
corpus, and Tier 5 promotion should retire it as the corpus grows.
The follow-up 64-roll measurement applied the bootstrap 1,422 times across 168 distinct fields. It did not broaden the measured distributions beyond the previous run: feature scale stayed 0→376 voxels and walkable fraction was 0.000000→0.286766, below the previous 0.388506 maximum. The bootstrap is therefore active, but these two measurements are insensitive to the changed fields in this sample; this is more useful than pretending the new knob created visible world variety. The run had 38/64 survivors, 60 non-vacuous candidates, 55 meeting the largest component threshold, and 42 exact law passes; those counts are observations, not tuning targets.
The wider structure-roll jitter remained sound: 659 boxes proved, 877,129 voxels checked, and 0 violations (1,901 Mixed, 266 AllSolid, 393 AllAir). The parameter-roll box check likewise proved 1,204 boxes, checked 1,602,524 voxels, and found 0 violations (1,356 Mixed, 608 AllSolid, 596 AllAir). A violation would reopen §6.2; this run did not.
3.4 The measurement pass — one grid, one flood fill, three jobs
Sample a candidate strate into a coarse voxel grid, flood-fill the air once, derive everything from that single grid. A 4–8× downsample preserves all of it. Because the depths are bounded, an entire strate can be covered rather than sampled.
Validation — is it garbage? air fraction · largest connected component share (low = swiss cheese: sealed pockets nobody reaches, generation paid for and never seen) · walkable surface (air above, solid below, survivable slope) · feature scale (median distance-transform of the air — catches "noise fog", structures smaller than a few voxels reading as static rather than rock) · vertical clearance · entrance→exit reachability · fightable-space count (GDD §12 dimensions).
Danger — same grid, different questions. fall exposure · openness (mean distance to solid from walkable positions — sightlines and room to retreat, which is most of what makes a fight good or bad) · traversal mix (walk/climb/swim) · tortuosity (path length ÷ straight line = maze-likeness).
Rarity — nothing new. The vector of all the above; distance from the corpus centroid. Outliers are rare because they are outliers, so a dreamcore strate is rare without anyone typing a weight. Self-correcting: author five dreamcore-ish strates and they stop being outliers, which is the right answer.
⚠️ Coarse sampling can LIE about connectivity. A wall thinner than the sample spacing vanishes and two sealed spaces look joined, so a strate could pass the primordial-law check while being impassable. The one metric where a false pass is dangerous. Fix: coarse to FIND the route, then re-verify that one path at full resolution — cheap, because only one corridor is verified.
⚠️ Sample where players actually go — near the spine and the passage mouths. A box measured far out describes a place nobody will stand in. Report distributions (median + spread), not single values.
This is also F1. fable-idea.md's top-pick 2D world-preview tool is this pass with a visualisation
on top. The measurement hand-off and deterministic preview are now built; they are needed anyway to
review a season before publishing.
✅ F1 measurement + preview built (2026-09-04)
FVoxelStrateSampleGrid is an opt-in export of both the exact Air polarity and the scalar Density
values already sampled by VF_MeasureStrate / VF_MeasureStrateWithSampler. The editor/automation
renderer consumes that buffer and never calls a density sampler, so each candidate's metrics and images
come from one window and one sample pass. The filled-cell view is paired with a marching-squares-style
density = 0 contour from the scalar field. The contour is not an anti-aliased or blurred coarse image:
it is a legibility aid alongside the honest sampled-cell evidence.
The step-4 measurement and 64-card contact sheet are unchanged in scope. Each card now shows the coarse
filled and contour XZ/XY pairs. VoxelForge.Composer.StructureRoll writes them, plus an optional fine
pass, to:
Saved/ComposerPreview/structure_seed_0_corpus_9dbcea85_step_4_radius_256_fine_step_1_fine_radius_64/
The fine pass is explicit and bounded: the current composer selects the 42 coarse survivors, samples
step 1 in a radius-64 (128×128 XY) ROI, and refuses before allocation when MaxCells=2,000,000 would be
exceeded. The default is radius 64 because it stays around the observed 1.8M-cell budget while exposing
voxel-scale detail; radius 128 is already roughly 7.1–7.5M cells and radius 256 exceeds the 8M coarse
cap. The public settings let another editor/automation caller choose step, radius, centre, margin, and
cap, or the writer can be called for an explicitly selected list. Fine filenames, captions, and each
card's text include the sample step and exact resolved XY/Z window, so they cannot be mistaken for the
coarse render. A refused fine render is shown as a refusal with its reason rather than silently omitted.
The vertical image is an XZ section through the sampled cell nearest the strate-window centre Y. The
plan image scans every sampled Z layer, keeps layers containing both air and solid, and chooses the
one with the largest number of XY solid/air boundary transitions; ties prefer more mixed cells, more
walkable cells, then the layer nearest the vertical centre. If no mixed layer exists it chooses the
most solid fallback layer, so a slab does not silently produce a blank mid-height sheet. The focused
run's first candidate was 128×48 for coarse vertical, 128×130 for fine vertical, and 128×148
for both plan images; dimensions vary with the resolved Z window. Both views use dark solid, light air,
orange walkable cells, a turquoise scalar contour, and a one-chunk footer scale bar. The page states
one chunk = 32 voxels = 8 m, the density sign (> 0 is air), every exact window, the 512-pixel cap
(including a 20-pixel footer), and that larger source grids aggregate already-sampled cells rather than
resampling. It also carries the required caveat: a 2D apparent join is not a 3D connectivity proof;
the separate arrival→departure verdict is the connectivity check.
The focused run measured 0.456 s for the coarse render, 70.271 s for the 42 fine scalar samples,
and 0.573 s for their filled/contour rendering. Peak fine Air + Density capture allocation was
9,338,880 bytes and the peak single RGBA raster was 75,776 bytes. The full StructureRoll test
completed in 184.659 s, with 64 coarse filled pairs, 64 coarse contour pairs, 42 fine filled pairs,
42 fine contour pairs, and 0 refusals.
3.5 Validation in three layers, and how "good" is ever judged
- Construct by design so the law cannot break — build the connections FIRST and generate around them. Reachability by construction, not by luck. Generate-then-check gets brutal at 8 strates each.
- Measure the accidents (§3.4). Most garbage is numerically bad, not aesthetically bad.
- Reject and resample — fold the attempt index into the hash (
hash(seed, slot, attempt)) so every seed still yields a valid world and peers stay in lockstep.
Judging "good" — the judge is the CORPUS, not a model. Measurements catch broken, not boring. But good never has to be defined, only collected:
- Rate at the STRATE level, not the world level. Twenty candidates thumbs up/down is twenty labels in minutes. Approved ones join the corpus and become blend parents. This loop works with zero machine learning; a model only makes it faster, later.
- Player behaviour in a live season — lingering, revisiting, rushing past. Free telemetry, but time spent somewhere can mean fascinating or lost and furious.
- ⭐ Where players choose to snapshot their home (GDD §11). An unambiguous "I love this place", with no UI, no prompt, and no way to game it. The strongest quality signal in the design, and it costs nothing to collect.
⚠️ Known risk: convergence to a comfortable middle. Promote only what you already liked, keep blending near the corpus, and the system slowly stops surprising you. Same failure mode as the build-vote pool, same counter: keep extrapolating past the corpus, and keep some unrated weird candidates in circulation.
4. The world manifest
⭐ It is TWO things, not one
Tier A — the SPINE. Finite, tiny, actually stored. Strate slots and passages. ~30 strates plus a few
passages each is roughly 100 entries — kilobytes. This already exists as StrateLayout plus the
passage list, computed once in UVoxelStrateManager::Initialize.
Tier B — the FIELD. Effectively unbounded in XY, therefore NEVER stored. Landmarks, rooms, material deposits, build sites. It is a function: give it a region, it enumerates what is there. Deterministic, hash-based, zero bytes.
That split is what makes "the manifest must not be expensive" true rather than aspirational.
⭐ The trick: THE ID ENCODES THE POSITION
Resolving an ID back to a place — a saved quest target, a fast-travel destination, a wiki citation — would normally need a lookup table, which is exactly what we cannot have. Landmarks are placed on a hash lattice, so build the ID from lattice cell + type + slot index within the cell. Then ID to position is arithmetic: no table, no database, no storage, resolvable offline on any machine forever.
Names fall out of the same thing: hash(id, seed) indexes the word pools. Every entry has a stable,
globally identical name that nobody stored and nobody generated in advance — including entries no player
will ever visit. That is the substrate that makes quests and wikis possible at all.
What earns an entry
One test: can a system need to ask "where is it?" or "what is it called?" If yes, it is an entry. Yes: strates, passage mouths, landmarks, notable rooms, material deposits, build sites, boss arenas, the (0,0) spine. No: individual rocks, every tunnel segment, terrain that merely emerged from noise.
ADDRESSABLE vs NOTABLE. Everything hash-placed is addressable for free — it has an ID by construction. Notability is a filter on top, and it is exactly what the measurement pass produces: the biggest chamber in a region, the one with a lake, the one at a passage mouth. No new placement logic, only a threshold.
Discovery sits ON TOP, never inside
A player's discovered set is a set of IDs — small integers, cheap to store and sync. Community discovery is the union. The manifest never changes; only who has seen what.
⚠️ You cannot hide it anyway. Clients generate terrain from the seed, so players hold the generator and can enumerate the manifest offline. Not a leak to prevent but a fact to design around, as Minecraft does with seed maps. It barely matters here: community knowledge spreading is the intent, and a map of season 4 is worthless in season 5.
⚠️ Two cautions
- The hash functions become part of the FROZEN SEASON CONTRACT. Change how a lattice cell hashes and every quest target in that season moves. These are not implementation details to tidy mid-season.
- Tier B is cheap, not FREE. Enumerating rooms in a region needs that region's placement pass. Fine for a beacon or nearby markers; a full-map view would hurt. LOD the query — coarse regions return only high-notability entries, which is what a zoomed-out map wants anyway.
5. Naming
Failure mode to avoid: slot-filling. "The Crystal Cavern of Whispers." Players learn the template in ten minutes; after that every name is noise and, worse, names stop distinguishing places, which defeats the point of having them.
- ⭐ Derive names from the MEASUREMENTS, not from a bag. A vast open chamber draws vast-open words, a flooded one draws water words, a tight vertical shaft draws shaft words. The name then fits the place, and a player who hears "the Sink" and later finds a huge drain-like chamber gets a small satisfying click.
- Several competing GRAMMARS, not one template. Compound (Blackreach, Stonefall); descriptive definite (The Long Dark, The Hollow); possessive (Varen's Fall — somebody was here and it went badly); functional (Shaft Nine — implies industry and records); opaque, from an invented phonology so it reads as a language rather than keyboard mash. Five grammars read as history. One reads as a generator.
- Name SCARCITY. Only notable entries get a name; everything else stays addressable and anonymous. Scarcity is what makes a named place feel like a place — a threshold, not a feature.
- Names must survive contact with a WIKI. Short, pronounceable on sight, no apostrophes or unusual glyphs. A name nobody can spell cannot become community knowledge, which is the only reason names exist in a shared world.
Two layers: the procedural name is the substrate — it exists before anyone visits. A community name may override it and propagates like a shared build. Nothing is ever nameless, and beloved names win.
⛔ Parked: varying the naming language by depth (deeper = older, stranger tongue). Liked, but it is still a gradient, and gradients are what Jahni rejected.
6. Code findings and hazards
All verified against the source on 2026-08-17 unless marked otherwise.
✅ 6.1 THE BLOCKER — EffectOverBox correct in isolation (resolved 2026-09-04)
The source+converter coupling is removed. FVoxelBoxHypotheses now carries an
FVoxelBoxSdfInterval; each SDF writer publishes only the interval for its own field, and each
converter/detail operator folds that interval through its own formula. FVoxelOpStack::VF_FoldOp
propagates the interval in stack order, so an operator cannot borrow a neighbour's reach or verdict.
The interval is deliberately a proof, not a likelihood: invalid, non-finite, or unimplemented
bounds become unknown and therefore cost a skip rather than manufacture AllSolid/AllAir.
Bounds use the supremum of Eval over the authored parameter range, including sup|FBM| = 1.5,
the max(Min, Max) radius envelope, warp·√2 where two axes are independent, and the SmoothMin
slack. The existing fold carries the interval; no parallel box-query path was needed.
The isolation deliverable is an unverified combination: ConstantVoid → LatticeCorridorSource → SdfRoughness → SdfFill, which no shipping builder emits. Across 256 boxes it proved 250 AllAir
and 6 Mixed; 332,750 voxels checked, 0 violations. The shipping archetype scans likewise
reported zero violations. This resolves §6.1 for the current operators, with one ongoing interface
rule: every future SDF writer must implement interval propagation or accept Mixed by default.
⚠️ 6.2 Box-verdict bounds are proved against HAND-SET params
T1.d / ClassifyTile bounds are proved today against the parameters Jahni typed. If the seed invents
parameters, every bound must hold across the whole authored RANGE, not a value. Same silent failure
mode as above. See voxelforge-noise-bounds — a bound must be the SUPREMUM of what Eval produces, not
the parameter named "max".
⚠️ 6.3 ClassifyTile will silently delete 3D caves
It proves anything below TerrainZ solid and skips meshing. Any future 3D generation inside mountains
needs a guard, or the caves vanish with no error.
🔨 6.4 The primordial law hole — bounded destination landing
UVoxelStrateManager::GeneratePassages places every inter-strate passage as independently salted
hashes of (seed, upper-strate index, connection index) → random angle around the (0,0) spine →
random distance in the config range → random reach into each strate. Nothing consults the
destination strate's cave layout. Carving is a structural-post invariant so the tube is air — but
whether its lower mouth joins the lower strate's connected space is pure luck. A player can descend
a passage and arrive in a sealed pocket.
Fix — make the strate OFFER a landing site, don't let the passage gamble. The source contract is
SuggestLandingPoint(DesiredX, DesiredY, MaxLateralSnap) -> optional FVector. Every source knows where
its own air is: FRoomGraphSource keeps the nearest room's vertical placement at the requested XY,
MakeSlabVoidSource answers in the void band, FIslandBlobSource finds a blob top, and the lattice/
shaft sources search their placement grid. Sparse sources may move laterally, but only within one
source spacing/cell; if that bounded search has no footing, falling back is the honest answer.
Passage placement remains deterministic and cheap, and the slanted control-point chain is bounded too.
Then the measurement pass flood fill is the net: does the upper mouth's air component reach the lower mouth's? If not, re-roll with the attempt folded into the hash.
6.5 Findability is mostly DATA, not code
ELandmarkAnchor::PassageMouth already exists — landmarks can anchor at passage mouths — and
FStrateLandmark already carries a Light-Orb block. A glowing landmark at every passage mouth is
authorable today with no code at all. Beyond that, cheapest first: light (a glow down a tunnel is the
strongest pull in a cave); sound (works around corners, which light does not — you hear the draft before
you see the hole); and ⭐ make the mouth a PLACE, not a hole — the SuggestLandingPoint fix already
lands the passage in a room, so make that room distinctive. Cracks in walls are forgettable; rooms are
landmarks, and that is what makes it findable on the second visit, which quests need.
✅ 6.6 The layout depends on the pool's CONTENTS, not its ORDER (BUILT + TESTS GREEN)
Before Tier 0, Initialize Fisher-Yates shuffled Settings->StratePool in editor order with
FRandomStream(WorldSeed), then cycled it (PoolCursor % Num) when TotalStrates exceeded the pool.
So reordering the pool asset in the editor changed every world, and a pool smaller than the strate
count repeated entries in a fixed order.
⇒ Tier 0 is now implemented: load the pool, sort it by the stable soft-asset path, then keep the existing Fisher-Yates shuffle and cycling algorithm. The pool is a set rather than an editor-ordered sequence: tidying the asset stops changing worlds. Adding or removing a strate type still changes worlds — unavoidable, and the reason content lands at wipes. A per-slot hash would be a different sampling algorithm and is intentionally deferred.
⚠️ 6.7 A bounded world needs an XY EDGE SEAL, which does not exist
Verified: VF_ApplyBoundarySeal(float& Density, float WorldZ, float StrateTopZ, float StrateBottomZ, float Thickness, float BaseDensity) is purely VERTICAL — it seals each strate's ceiling and floor so
nothing punches through except passages. There is no horizontal equivalent, because until the depths were
bounded the world had no sides.
⇒ Build it as a fourth structural invariant alongside spine → seal → passage: a forcing op,
appended automatically, not author-omittable. An invented strate then cannot forget it — and because
forcing ops prove AllSolid in their band, the outer shell becomes free to skip in tile
classification.
6.8 The (0,0) spine, exactly
VF_ApplyOriginSpine carves a guaranteed-open vertical column at actor-local XY (0,0), through every
strate, archetype-independent, appended automatically. It carves only the strate INTERIOR and leaves
the seals intact on purpose — its own comment: "so the player must still dig through to descend". It
provides a clean landing space, not a continuous open shaft.
⇒ The hand-authored descent shaft is the above-ground continuation of the spine. Nothing new to
build. The city's hole must sit above the VoxelWorld actor's origin (see §6.9), and the shaft's diameter
should match the spine's Radius or the join will visibly step.
✅ 6.9 Actor space — DONE (built and confirmed off-origin, 2026-08-17)
The field is authored in actor space: (0,0) is the AVoxelWorld actor's origin, not Unreal's world
origin. The plugin used to be only half actor-relative — biome/deco/water/density-volume/debug
converted; the streaming centre, the anchors, all six carve/fill entry points,
GetStrateAtPosition and UVoxelAtmosphereManager::UpdateForPlayer did not. It worked only because the
actor sat at the origin.
WorldToLocalCm / WorldToLocalVoxel / LocalVoxelToWorld are now the only sanctioned boundary
between Unreal coordinates and voxel coordinates, and the real deliverable is the invariant beside them:
no / VOXEL_SIZE applied to a parameter named World* outside those three functions. A grep can
check that — and the risk was never a missed site among the ones found, it is the next one nobody
notices. ⚠️ Translation only: rotation or non-unit scale breaks world-axis-aligned box maths
structurally (tiles, ClassifyBox, the clipmap window, culling); BeginPlay logs an Error.
⚠️ The trap avoided, worth remembering: the content managers already converted internally, so
converting PlayerLastPos once at the source would have double-converted four call sites. The fix
had to be per-boundary. Only visible after reading each consumer.
✅ 6.10 The X-macro guard — DONE (built green, 2026-08-17)
VoxelForge.Determinism.StrateParamBlendCoverage expands VF_STRATE_PARAM_FIELDS a third way, into a
name list, and diffs it against the struct's reflection. GExemptFieldNames is empty on purpose. Guards
the fact §3.3 depends on: Lerp blends the macro list, not the struct.
⚠️ 6.11 Client-side meshing makes determinism a RUNTIME requirement
The host owns collision; every client meshes its own view, which is only safe because every client
generates the same field. Two clients producing different floats means they disagree about where walls
are, and the host's collision is authoritative — so one player hits an invisible wall where another sees
a passage. ⇒ VoxelForge.Determinism.CrossPlatformDigest currently reports rather than asserts. This
architecture promotes it to must-be-green.
(Not a hazard: six players do not multiply streaming. RegisterStreamingAnchor defaults to a 3×3×2 box
= 18 level-0 tiles per remote player, against a clipmap of hundreds across LODs. CollisionOnly skips
rendering only — density, marching cubes and the collision cook still run, but at 18 tiles each that is
small. What does scale with party size is carve-driven re-meshing, which is bursty.)
7. Build order
The sequencing principle: every tier must be worth doing on its own, before the next one exists. Nothing gets built purely as machinery for a later tier. This is the direct antidote to the 2026-07/08 refactor, where three weeks of correct work produced a world unchanged by a single voxel.
Tier 0 — safety nets, true regardless of the composer
- ✅ X-macro guard test — DONE, green.
- ✅ Actor-space sweep — DONE, confirmed off-origin.
- ✅ Order-independent placement (§6.6) — DONE. Built clean,
VoxelForge.Determinism.LayoutOrderIndependencegreen. Standalone value: the strate pool asset can be reordered or tidied without changing every world. Small and isolated.
Tier 1 — the primordial law (a BUG FIX, not composer work)
- ✅
SuggestLandingPointon field sources; passages aim at it (§6.4) — DONE, both mouths. ⚠️ It shipped HALF BUILT for weeks: only the LOWER mouth was ever aimed. See §14.3 — the ring proxy could not see it, and only the Tier 2 connectivity measurement could. - ⬜ The passage mouth becomes a room plus a distinctive landmark (§6.5) — not started.
- ⬜
SurfaceWorldstill refuses the query — it needs manager-resolved biome context, which cannot be answered from a pure free function. Open.
Standalone value: fixes a live hole in the shipping game — a player could descend into a sealed pocket. Worth doing even if the composer never happens. It was real: see §14.
Tier 2 — the measurement pass (§3.4) — ✅ BUILT (2026-09-03), see §13
VoxelStrateMeasure.h/.cpp + VoxelForge.Generation.StrateConnectivity / ...Refinement.
Standalone value: it is F1, fable-idea.md's top-pick world-preview tool — the measurement
half plus the deterministic editor/automation visualisation described in §3.4.
It paid for itself immediately: it found the VerticalShafts primordial-law violation (§14),
which no amount of playing was likely to surface, and it caught three of its own artifacts before
they became "findings" (§13).
⬜ Remaining: the danger/rarity metric families from §3.4 (fall exposure, openness, traversal mix, tortuosity, corpus-centroid distance).
Tier 3 — op-system prerequisites for free composition
- ✅ Channel read/write declarations on every op, plus the stack DAG validator (§3.2; built 2026-09-04).
- ⏸ Fuse corrective ops (
FFloorBiasModintoFCaveRoughnessMod) — blocked by the ten interveningDensityreaders/writers under the bit-identity requirement; see §3.2. - ✅
EffectOverBoxcorrect in ISOLATION (§6.1; built and brute-force verified 2026-09-04). - The XY edge seal (§6.7).
Standalone value: the isolation fix is a correctness improvement to shipping code. The fuse remains an open semantic decision; the Part 0 analysis above concludes that FloorBias is not a correction of roughness and should stay late in the stack.
Tier 4 — the composer proper
- ✅ Parameter roll (§3.3, Tier 4a) — corpus spread + deterministic blend/jitter implementation and the 64-candidate measurement pass are built; the bootstrap follow-up is 38/64 survival with 0 box-verdict violations (the first-run corpus/vacuity findings remain recorded above).
- ✅ Structure roll (§3.2, Tier 4b) — serialisable recipes, root polarity, declaration-derived resource-safe modifier legality, mandatory posts, 64-candidate measurement, and direct box-verdict brute force are built; the bootstrap follow-up is 42/64 survival with 0 invalid recipes and 0 violations, plus 64 rendered preview pairs and an index.html contact sheet.
- ⬜ Reject-and-resample driven by Tier 2 (§3.5) · the offline season pipeline with Jahni's review and veto (§3.1).
Tier 5 — the long game
Promotion (good strates rejoin the corpus) · theme and tag draws for materials, creatures and audio · eventually the model. All optional, all compounding.
8. Corrections that must not be re-derived
Kept because each cost real time or would otherwise be repeated.
- ⛔ "Do not start Phase 3" is SUPERSEDED.
OPSTACK-HANDOFF.mdstill says it; believe this file. - ⛔ Jahni does not want to author strates either. The composer must invent them. An earlier reading had him authoring strates while the system arranged them — one level too shallow, and it shaped a whole afternoon of wrong design.
- ⛔ Peer determinism never required matching the OLD system. Same build, all machines — that is all it ever meant. The self-imposed bit-identity criterion cost three weeks. Version stability across builds is a separate property, and seasons make it a non-issue.
- ⛔ Digging is a reveal-and-shape verb, not a movement verb. An earlier pitch made it traversal (the Deep Rock model). That is not this game.
- ⛔ Six players do not multiply streaming. Raised as a 6× cost; wrong (§6.11).
- ⛔ Depth must not mean "weirder". An early pitch argued for geological coherence and depth-driven strangeness. That is a different game — see GDD §7.
- ⛔
Alphais not a struct field, it isLerp's parameter. An earlier note claimed the X-macro had one exception; it has none. - State acceptance criteria up front and have Jahni confirm them. Criteria chosen alone drift toward whatever can be self-verified, which is reliably the wrong one.
9. Open — generator side only
Game-design questions live in GDD.md §16. These are the ones that block generator work:
- Adjacency rules between strates — confirmed wanted, never specified. Proposed shape: a small
tag-rule list in settings (no two alike in a row;
Anomalyneeds aGroundedneighbour above; max run length), with the draw falling back to a designated safe entry so it can never fail. - How the mix rule is expressed — a global "total strangeness" cap was proposed; not ruled on.
- How theme coherence is drawn — operators, materials, decorations, creatures and audio pulled as a compatible set. Jahni expects his first big job to be creating themes and tagging content with multiple fitting tags.
- Where the corpus lives and how promotion is recorded between seasons.
- Whether op discovery (promoting good sub-stacks into named reusable units) is ever worth it. Parked: the existing parameter space is already vastly larger than a hundred seasons could explore. The scarce resource is judgment, not vocabulary.
10. Verification log
2026-08-17 — full suite run headless, first time this session. Built VoxelMEditor Win64 Development clean (no errors, no warnings), then:
UnrealEditor-Cmd.exe VoxelM.uproject -ExecCmds="Automation RunTests VoxelForge;Quit"
-unattended -nopause -nosplash -NullRHI -ReportExportPath=...
Result: 16 succeeded, 0 failed, 0 not run, 0 succeeded-with-warnings. 25.7 s.
Tests: BoxVerdictFold · ClassifyTileSoundness (×2) · CrossPlatformDigest · DensityPurity · DiffLayerContention · FloatingIslandEquivalence · LargeSeedSurvives · LayoutOrderIndependence · LiveEditInvalidation · MazeEquivalence · SlabEquivalence · StrateParamBlendCoverage · SurfaceHeightEquivalence · TunnelNetworkSpineEquivalence · VerticalShaftEquivalence.
2026-09-04 — Tier 3b validation run. The specified UE 5.7 VoxelMEditor Win64 Development
build succeeded (14 actions). The full headless suite reported 22 succeeded, 0 failed, 0 not run,
0 succeeded-with-warnings, including VoxelForge.OpStack.ChannelDAG; the existing equivalence,
determinism, box-verdict, and connectivity checks stayed green. The 4b→4h fuse was deliberately
not applied after the intervening-channel audit above.
⭐ This closes the pending-verification thread opened by OPSTACK-HANDOFF.md. That file flagged
commits 4d33321 (Sol's boundary fold) and 91585ea as built but NOT re-verified, with two
acceptance conditions. Both now met:
violations= 0 everywhere. Measured: TunnelNetwork at production defaults 12 of 40 tiles proved AllSolid, 15 972 voxels brute-forced, 0 violations; VerticalShafts 32 of 60 proved, 0 violations; dense fixture 0 proved / 40 Mixed, 0 violations.- All equivalence tests green — Maze, Slab, TunnelNetworkSpine, VerticalShaft, FloatingIsland, SurfaceHeight.
⇒ No revert of 4d33321 is needed. The 39 % tile-skip win stands, verified rather than assumed.
⚠️ Read the PROVED counts as measurements, never as contracts. What is asserted is that none of them is wrong — a false verdict leaves no geometry and no collision behind it.
11. Tier 1 status — passages aim at real open space (bounded full-point query; build not run)
Updated 2026-08-29. VF_SuggestLandingPoint (in VoxelCaveMorphology) is a pure free function
of (archetype, params, seed, desired XY, lateral budget) — it touches no operator stack, no
UVoxelStrateManager, no cache. That matters: GeneratePassages runs inside Initialize, when the
layout is half-built and LayoutVersion is mid-flight, so constructing anything holding a manager
pointer there is a re-entrancy trap.
GeneratePassages now asks the destination strate for a full open/footing point near the chosen XY.
Maze, VerticalShafts, and FloatingIslands may snap to a nearby lattice site, shaft, or island top;
the lower mouth uses the returned X/Y/Z, clamped strictly inside the interior (never into a seal band).
Where the archetype cannot prove footing inside its bound, today's random reach and original XY are
preserved unchanged.
⚠️ Coverage is source-dependent and the number is the point
The failing pre-fix measurement was:
7 inter-strate passages, 3 checked, 0 footing checks, 38/48 room/slab ring samples air, 4 query-false (4 unique archetypes; 1 unsupported, 3 supported-but-no-point).
The Tier 1 query now searches laterally for Maze, VerticalShafts, and FloatingIslands landing
sites within one configured placement period. SurfaceWorld remains intentionally refused.
The extended test uses vertical source footing brackets for those three instead of applying the
room/slab circumference ring to thin or void-dominated geometry. The post-fix measurement is deliberately
not claimed here because this change does not build or run the Unreal automation test.
Seed contracts, do not mix them
- TunnelNetwork / Underwater →
VoxelCaveMorphology::MakeStrateSeed(the strate's own room seed). - Slab / Maze / VerticalShafts / FloatingIslands → the generator world seed, which their source placement/noise uses.
- SurfaceWorld → no answer yet; its world seed alone does not identify the manager-resolved biome/per-column production context.
Seeding the query from the passage salt hash — which my spec originally asked for — would inspect a cave graph that does not exist and return confident answers about imaginary rooms. Codex caught it.
The test is a proxy, and says so
The first version sampled GetDensityAt at the mouth and asserted air — vacuous, because
VF_ApplyPassageCarving is a structural-post invariant, so the tube is air at its own mouth by
construction. It passed whether the aiming worked or not. Replaced with a 16-point lateral ring at
MouthRadius + 2 × PassageBlend (~14 voxels at defaults), requiring ≥ 8/16 air for room/slab
destinations. A mouth in a real room has open space around it; a mouth in bedrock has only the tube it
dug itself. Maze, VerticalShafts, and FloatingIslands use a manager-free source density bracket:
air at the landing and solid below it. A large void ring is not evidence of island footing.
⚠️ It is a proxy for connectivity, not a proof. The proof is a flood fill — Tier 2.
⚠️ A forked placement envelope, guarded by that test
VF_FindNearestHashRoomLandingPoint re-derives the room vertical placement envelope that BuildChunkCache also
computes. Calling the per-chunk cache builder from Initialize is impractical, so this is a deliberate
second copy. Both sites now carry a PLACEMENT CONTRACT comment naming the other, and the ring test
is the only thing standing between a placement change and silent bedrock passages.
12. Tier 1 FINAL — and the 92.8% defect a green suite hid
Built clean, suite green: 17 succeeded, 0 warnings, 0 failed (2026-08-30).
⚠️ Correct the record: commit 1ab8c0c overstated what worked
It reported "3 checked, 48/48 ring samples air" and read as Tier 1 working. A million-seed sweep later showed the room query left the landing point OUTSIDE the selected room 92.795% of the time. The fixture's 48/48 was luck, not correctness — seven sampled passages cannot see a systemic defect.
Cause, and it was my instruction: I specced MaxLateralSnap = 0 for rooms and slabs — "preserving
existing placement, do not regress them, they are green today." The room path found the nearest room,
then returned the caller's XY with that room's Z. It located a room and aimed beside it. That is the
exact defect I correctly identified for Maze/VerticalShafts/FloatingIslands and then explicitly exempted
rooms from.
The fix: return the ROOM CENTRE
VF_FindNearestHashRoomLandingPoint already computed the room's XY and discarded it. It now returns it.
- A room's centre is inside its own room by construction — no SDF evaluation, no warp handling needed. It is also the point with the largest margin against the production cave warp (~1.5 voxels of displacement: decisive at a room's edge, irrelevant at its centre). Simpler and more robust than the warped-SDF inside-proof I had been specifying.
- Caves get a real budget (
RoomSpacing): a room is an XY-sparse target, exactly like a maze corridor. Slabs correctly keep 0 — a slab's void band is XY-continuous, so the requested XY is already inside it.
⚠️ OPEN TRADE-OFF: the room budget is tight
Final measurement: 7 passages, 5 checked, 3 footing checks, 32/32 room/slab ring samples air, 2 query-false (1 unsupported, 1 supported-but-no-point) — and TunnelNetwork and Underwater declined entirely at this seed.
With RoomDensity < 1 not every cell holds a room, so the nearest is often 1.5–2 cells away and one
RoomSpacing refuses. Cave passages are back on random reach — honest, but not landing.
~2× spacing would likely restore cave coverage, at the cost of passages drifting further from the (0,0) spine — the property that makes them findable. Deliberately NOT tuned: "raise the number until coverage looks better" is the move rejected twice this session. Jahni's call.
A guard was softened, and why that is not threshold-tuning
PassageLandsInOpenSpace asserted that all four room/slab archetypes must answer within the fixture's
7 passages. That asserts a property of the sample, not the code — and it failed on a fix, since
a supported query is allowed to decline on budget. Now reported via AddInfo.
Still hard failures: vacuity (zero passages / zero checks / no footing exercised), every per-mouth
ring and footing assertion, endpoint-matches-query, and SurfaceWorld unexpectedly answering.
Same discipline the codebase applies to box-verdict PROVED counts: read the count as a measurement,
assert only that nothing it reports is wrong.
Process notes worth keeping
- ⚠️
codex execbegan hanging reliably — three runs, 0 bytes written, process alive, atmax/xhigh/high. A tinylow-effort probe worked, Jahni's interactive CLI works, and quota was confirmed fine. Cause unresolved; the room fix was completed by hand instead. - ⚠️ I waited 4.5 hours on a hung background task because I treated "no notification" as "still
working." Checking the output file's size costs nothing.
CodexGuidance.mdstates the rule from the other side: never wait silently; report if blocked more than two minutes. - ⚠️ A stream edit (
perl) corruptedVoxelStrateManager.cpp— a skip-until-terminator ran past its block because a secondUE_LOG(LogTemp, Warningexisted earlier in the file, deleting ~178 lines and an entire function. Git recovered it. Use targeted read-then-edit on source files; verify line count and brace balance after any structural edit. - Do not run builds while a Codex task is active —
CodexGuidance.md§Collaboration: one agent per working tree.
13. Tier 2 BUILT — and what measuring the world actually found (2026-09-03)
The measurement pass exists, is falsifiable, and earned itself on the first run by finding a live gameplay bug that six months of playing might not have surfaced.
VoxelStrateMeasure.h/.cpp — read-only, deterministic, headless. One coarse grid, one 6-connected
air flood fill, every metric derived from that same grid: air fraction, component count, largest
component share, walkable fraction, median feature scale, median vertical clearance, and connectivity
between two points. VoxelForge.Generation.StrateConnectivity (fast) and
...StrateConnectivityRefinement (the slow sweeps).
⭐ The single most important habit this produced
Every reported number states the volume it was measured in. ResolvedMarginVoxels, SampledMinZ,
SampledMaxZ are on the metrics struct and printed. This exists because the first version did not
have it, and the consequence is below.
⚠️ Three artifacts that looked exactly like findings
Each of these was, briefly, believed. Each was wrong. The pattern is identical every time: a number produced by a method nobody had questioned.
-
"FlatPlain is a 99.95% empty void." The interior window excluded a whole chunk (32 voxels) at each strate boundary to avoid a 4-voxel seal band — 8x too much. On 4-chunk strates that discarded half of every strate, floor included. Walkable fraction was reading 0.00044; with the window derived from the seal that actually exists (
2 x BoundarySealThickness), it is 0.0578. 132x. My spec's error. -
"5 of 15 passage endpoints are disconnected." The test's
LargestComponentAnchor()did not find the largest component — it returned a hardcoded guess near the (0,0) spine. And the question itself was wrong: "connected to the largest component" is not the primordial law. A strate can be 57 disconnected shafts and be perfectly good, provided the shaft you arrive in is the shaft you leave from. Corrected: 13 of 15 connected, and the real question isarrival → departure. My spec's error, twice over. -
"7 of 16 seeds fail the law."
VF_AreConnectedtook the shortest coarse BFS route, re-walked it at full resolution, and on hitting a wall returned false and gave up — never looking for another way round. Of those failures, only 2 wereNOT_CONNECTED; 7 wereCOARSE_LIED, i.e. one candidate route was blocked. A blocked route is not a disconnected pair. My verifier's error.
⇒ Rule: when a measurement reports something alarming, suspect the measurement first. It was the method three times out of three. The cost of checking is minutes; the cost of not checking was nearly a rewrite of generation that was not broken.
The guard is two-directional now, and the directions are NOT symmetric
- False POSITIVE (coarse says joined, reality is walled) → dangerous: no geometry, no collision,
a player falls through the floor.
FullResolutionPathIsAirre-walks the recovered route at one-voxel spacing. It fires: 2 of 21 routes on the first run were coarse false positives. - False NEGATIVE (a corridor thinner than
SampleStepvanishes) → merely wrong, but wrong in the direction that sends you "fixing" healthy code. Named explicitly asNotConnectedAtThisResolutionso a bare negative can never be read as proof.
⇒ CoarseLiedBudgetExhausted means UNKNOWN and must never be folded into "disconnected".
Settling a finding: refinement with the confound held fixed
VerticalShafts was declared a real bug only after step 4→2→1 (7.34M cells), radius 256→192→128, and
margin 8→4→2→0 (margin 0 includes the seal, which can only add solid, so it cannot invent a route) —
with step and radius varied independently so neither could explain the result. Then 16 seeds,
because a single fixture cannot distinguish one unlucky world from a broken archetype. See
tests-must-be-falsifiable; a 7-sample fixture already hid a defect that fired 92.8% of the time.
14. The VerticalShafts bug, end to end
Symptom: descend into a VerticalShafts strate and you cannot reach its exit. 15 of 16 seeds.
It was three bugs wearing one coat.
14.1 The spine was not the backbone there
The (0,0) spine (OriginSpineRadius 14) holds 97–100% of the air and is the largest component in
7 of 8 archetypes. In VerticalShafts it punched through as an isolated column holding 9.6%,
never the largest. Fixed: the spine now participates as a connector endpoint — not as a carving
shaft; VF_ApplyOriginSpine remains sole owner — and is excluded from ledge placement.
Necessary, not sufficient: it only joins its own 3x3 neighbourhood, and mouths land 60–200 voxels out.
14.2 ⭐ The shaft graph sat below its percolation threshold
The tell was a plateau: the largest component sat at ~0.22 in every variant — roughness 0, connector radius 8, baseline. Roughness changed the component count (it manufactures noise pockets) but never the giant component's share.
ShaftDensity 0.6 sites, CrossConnectChance 0.35 bonds, and a 1.6-cell pair cutoff that silently
drops many neighbour pairs once jitter spreads shafts 1.7 (orthogonal) to 2.4 (diagonal) cells apart.
⛔ Do NOT fix this by raising those three numbers. Percolation is a phase transition: tuning toward it buys a world connected at one seed and shattered at the next — which is the 1-in-16 behaviour. A phase transition is not a knob.
✅ Fixed structurally: a drainage tree. Every shaft emits a deterministic connector to the nearest shaft strictly closer to the origin, so chains drain inward and terminate at the spine. The probabilistic connectors stay as texture and loops — the tree guarantees you can get out, the noise keeps it from reading as a diagram.
- seed 1337: 30 components / .220 largest → 17 / .975
- 16 seeds: spine is the largest component 16/16 (was 8/16)
⚠️ Seam hazard, and the reason for the 7x7 window. A shaft's parent computed from a 3x3 window
would differ between chunks, so the connector would exist in one and not the other — invisible in the
metrics, very visible in game. Collect over 7x7, emit only for the inner 3x3, whose 5x5 candidate
windows are then fully contained. Guarded by VoxelForge.Generation.VerticalShaftSeamFreedom
(154 probes, 0 mismatches). The 49 hash rolls happen on cache rebuild, never per voxel
(1.634 us rebuild, 0.100 us hot call).
⚠️ Connector radius must exceed the proven roughness supremum
(SurfaceRoughness x VOXEL_NOISE_SCALE x 1.5 = 5.625 at defaults; sup|FBM| is 1.5, not 1.0 —
see voxelforge-noise-bounds). A guaranteed connector that noise can close is not a guarantee.
14.3 ⭐ Tier 1 was only ever HALF BUILT
VF_SuggestLandingPoint was called once per passage, for the LOWER mouth only. The upper mouth
fell out of the control-point chain with no landing query at all. Tier 1 fixed where a player
ARRIVES and never touched where they DEPART — and the measurement said so exactly:
departure → spine was 0 connected across all 16 seeds.
The ring proxy could never have caught this. It checks each mouth for open space individually, and an unaimed mouth still has open space around it because the passage tube carved some. Only a connectivity measurement could see it. This is the strongest argument in the whole project for Tier 2 existing at all.
✅ Fixed: each mouth queries its own strate independently — which is precisely what keeps the
layout a set, not a sequence. Passage i never reads passage i-1, so
LayoutOrderIndependence stays green. ⛔ The "obvious" fix (chain the mouths) would break it.
departure → spine: 0 → 9 of 16 ·arrival → departure: 4 → 7 of 16
Status: not yet 16/16. The remaining gap is mostly COARSE_LIED, i.e. §13's verifier defect, not
the world. Fix the measurement before touching generation again.
15. Working notes that cost something
- ✅ The
codex exechang was stdin. It printsReading additional input from stdin...and blocks forever on an inherited pipe. Always< /dev/null. Cost 4.5 h once. Read the banner'sreasoning effort:line back after every launch — Jahni asks formax. - A killed agent leaves unvalidated edits. A session teardown killed Codex mid-task; the edits were complete and correct but had never been built. Build and run them yourself rather than assume — a clean-looking diff is not a passing test.
- Codex has found six real errors in my specs across nine tasks. Asking for disagreement in every spec is the highest-value line in them. Three of today's corrections came from it, not me.
- Acceptance must be the measured property, never "the code was written." Twice a task returned "built, and it does not meet the criterion — here is the number." Both times that honesty pointed straight at the real cause.