Commit Graph
9 Commits
Author SHA1 Message Date
Claude f83c4bd8c1 refactor(generator): delete the pre-operator-stack archetype functions
With the operator stack as the only density path, the per-archetype
density functions have no caller left in the game:

- Delete UVoxelGenerator::GetSlabDensity, GetMazeDensity,
  GetSurfaceDensity, SurfaceDensityFromColumn, GetVerticalShaftDensity
  and GetFloatingIslandDensity (~1000 lines) with their declarations.
- Drop GetDensityWithParams' OutWarpedPosition out-parameter; only
  the native GetDensityAt branch removed in the previous commit used it.
- OpStack Slab/Maze/VerticalShaft/FloatingIsland and SurfaceHeight
  tests: remove the stack-vs-legacy comparisons; keep decomposition,
  finiteness, liveness, window-invariance, cache-cost and box-verdict
  checks (Shaft cache timing now measures the stack source).
- PassageLandsInOpenSpace: the source-only footing probe evaluates
  the archetype stack built without a strate manager instead of the
  deleted functions.
- Comments that cited the deleted functions now cite the operators
  (FSlabVoidSource, FLatticeCorridorSource, FIslandBlobSource, ...).
  GSurfColCache is documented as ClassifyTile's column cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URDGLsu7c4nunqNFPJEFPU
2026-09-28 16:04:51 +00:00
Claude 8117d118eb refactor(opstack): the operator stack is the only density path
Every strate now generates through its operator stack: authored
definitions, cooked-season entries (recipe or native) and editor
composer candidates alike. The per-strate opt-in and the archetype
switch it guarded are gone.

- Remove UVoxelStrateDefinition::bUseOperatorStack and the season
  wiring that set it; UsesOperatorStackForChunk now only says whether
  the slot has a known archetype.
- GetDensityAt always builds a stack (composer, then recipe, then the
  native archetype stack); a degenerate zero-height strate stays air.
- Remove the startup opt-in warning, the operator_stack startup-trace
  field and the four "Cave Bail Not Op Stack" stats (the remaining
  bail is counted by Cave Bail No Stack).
- Explore commandlet: drop -opstack and the operator_stack fields;
  VoxelForgeTest.ps1 no longer passes -opstack=1.
- Tests: FTestWorld::Build loses its bool parameter; legacy-vs-stack
  comparisons (MazeSeam, VerticalShaftSeam, TerrainDetailLiveness,
  WorldEdgeSeal, TilePostReach) collapse to the single stack path.

This changes terrain for any strate asset that did not already have
the opt-in ticked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URDGLsu7c4nunqNFPJEFPU
2026-09-28 15:56:19 +00:00
Fr0zkaandClaude Opus 5 fc43a86709 feat(scale): size the world to a human body -- and it is NOT enough
Jahni: "can't you try and make the world to scale?" Every length parameter
is now derived from the body and annotated with its metre value, so nobody
has to do the arithmetic to notice something is absurd.

  CorridorRadius   4 -> 8     1 m -> 2 m radius (4 m bore)
  CellSize        40 -> 64    keeps a real wall between wider corridors
  TunnelMinRadius  3 -> 6     0.75 m -> 1.5 m
  ShaftMinRadius   5 -> 8     1.25 m -> 2 m
  IslandMinRadius 18 -> 24    4.5 m -> 6 m
  MouthRadius      6 -> 8     passage mouths 4 m
  plus passage reach, spiral, cascade, bridges, ridges, chasms

The reasoning that motivated it still looks right: a 2 m circular bore for
a 1.76 m player clears the head only along the exact centreline, so almost
no cell has a floor with 7.04 voxels of clear air above it. A 4 m bore is
genuinely walkable.

⚠️ BUT IT DID NOT FIX TRAVERSABILITY, and this time it is not the
instrument. The player-fit law is measured at STEP 1 on a mouth-fitted
window -- full resolution, both mouths inside the box -- and across the
eight showcase archetypes it reports:

  6 NotConnectedAtThisResolution   (real negatives at full resolution)
  2 OutOfWindow                    (unmeasurable, not passing)
  1 StartCellNotPlayerFit          (the arrival mouth itself is too small)
  0 CONNECTED

And every archetype reports hard-gate survivors=0, so the showcase is
displaying the least-bad of an entirely failing set.

⛔ DO NOT READ player_fit_fraction / player_clearance FROM THE SHOWCASE
LINE. Those come from the COARSE step-4 measurement in the same log line,
where the 1.36-voxel player is sub-cell and fit is unmeasurable --
player_clearance reads a uniform "1.00 voxels" for all eight archetypes,
which is one cell, not one voxel. Only restricted_law is at step 1 and
only it is trustworthy. Fixing that mixed-resolution reporting is next.

So scale was necessary and insufficient. My hypothesis was that bore
diameter versus body height was the binding constraint; doubling it moved
nothing measurable. Something else is preventing player-sized volume from
existing at all -- worth finding before tuning any further.

Codex applied these edits and then died mid-poll without validating them
(third time a killed agent left a clean-looking unbuilt diff). Build and
suite run by me: build clean, 30 succeeded / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
2026-09-06 00:43:33 +02:00
Fr0zkaandClaude Opus 5 9d2324879d feat(world): the XY edge seal -- the fourth structural invariant
VF_ApplyBoundarySeal was purely VERTICAL: it sealed each strate's ceiling
and floor and there was no horizontal equivalent, because until the depths
were bounded the world had no sides. The GDD now says "bounded, but
gigantic", so a player who walked far enough reached unterminated
generation running off into nothing.

Built as a FORCING op, appended automatically and not author-omittable, so
an invented strate cannot forget its own edge. Because a forcing op proves
AllSolid in its band, the outer shell also becomes free to skip in tile
classification -- correctness and a perf win from one change.

  order  spine -> vertical seal -> passage carving -> XY EDGE SEAL
         -> disturbances -> diff
  A near-rim passage may pierce a vertical seal; the XY seal runs after and
  wins. A passage must never punch a hole through the world's edge.

ClassifyBox/ForcedMarginOverBox proved via the closest AABB radius with a
one-voxel margin: 8 boxes proved, 1 tile proved, 600 voxels brute-forced,
0 violations. FXYEdgeSealOp is IsXYPure. Max rim discontinuity .187 against
a 1.0 threshold -- a ramp, not a cylinder wall.

⚠️ WorldRadiusVoxels DEFAULTS TO 0, and that is deliberate. Codex flagged
that DA_Settings and BP_VoxelSettings predate the property and so do not
serialize it -- they inherit the C++ default silently. A default of 8192
would have walled in every existing world on next editor open with nothing
in the asset to explain it. 0 is a bitwise no-op (45 interior comparisons
against the unbounded path prove it). The GDD's intended ~8192 is a value
to set in the asset, deliberately, once. Documented at the declaration.

Codex found four spec errors, three of them mine: VF_ApplyBoundarySeal
lives in VoxelDensityPrimitives.h not VoxelCaveMorphology; the asset
default trap above; and my "density increases through the band" wording
contradicts the MC sign convention (solid is more NEGATIVE) -- the test
measures increasing solidness.

Build clean, 21 succeeded / 0 failed, run and verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-04 01:17:12 +02:00
Fr0zka f90c4e56c3 fix(generator): key CP cache by world owner
Function-static thread_local CP state previously trusted only chunk coordinates and each manager's locally-reused layout version, allowing a worker to carry params, biome context, CP_UseOpStack, and the built stack into another world. Allocate each generator a monotonic owner ID and add one uint64 comparison to the hot key. This intentionally fixes only the proved CP_* path; other TLS caches remain outside this change.
2026-08-17 00:41:51 +02:00
Fr0zkaandClaude Opus 5 6a8390f11f feat(opstack): ClassifyTile consumes ClassifyBox for cave archetypes -- the T1.d prize, behind the same opt-in
⚠️ THIS IS THE ONE COMMIT OF THIS RUN WHOSE FAILURE MODE IS A HOLE, NOT A REGRESSION. A non-Mixed
verdict makes the world skip GenerateMesh entirely: no triangles, no collision, invisible until a
player falls through. Read VoxelForge.OpStack.ClassifyTileSoundness before trusting it.

WHAT CHANGED
ClassifyTile used to `return Mixed` without a call for every cave archetype ("archétype cave [...]
pas prouvable en v1"). It now builds that strate's stack and folds ClassifyBox. SurfaceWorld and
bedrock gaps keep their hand-written proofs untouched -- the exact-lattice column test is strictly
better than any box bound, so the stack has nothing to add there.

ONE DEFINITION OF THE STACK MAPPING, and that is the load-bearing part
GetDensityAt's ~90-line build switch is extracted into VF_BuildOpStackForChunk and both callers now
use it. A second copy would be the worst bug available here: a tile skipped on the verdict of a
stack that is not the one producing its density is precisely a hole. A "keep these in sync" comment
would not have been enough; there had to be only one. Params are passed in by pointer, never fetched
inside, because both callers already have them.

SIX GUARDS, ALL FAILING TO MIXED, none giving the benefit of the doubt
1. The strate must actually opt in -- on EVERY chunk coord the box touches, not just the one that
   triggered the attempt. Otherwise the classifier judges a field the mesher will not produce.
2. One cave slot per tile. Two slots means two param sets, and a stack answers only for its strate.
3. No mixed cave/surface/gap tile: the cave stack's box would cover Z belonging to another strate.
4. The params must be BIT-IDENTICAL across every chunk coord the box touches. This is the guard that
   matters, and it exists because of the finding committed earlier today: GetGenerationParams blends
   params inside a strate (Alpha depends on chunk Z for Gradient, and on chunk XY as well for
   Interleaved), so one stack genuinely cannot represent a tile that straddles a transition band.
   Memcmp on POD: differing padding can only produce a false MISMATCH, i.e. one Mixed too many.
5. A 27-chunk-coord cap, so a very wide tile does not pay for the check. We give up the gain, never
   the safety.
6. The disturbances are folded by hand (chasm ⇒ CarveOnly, bridge/ridge ⇒ FillOnly), because
   DECOMPOSITION 10.2 leaves them OUTSIDE the stack -- GetDensityAt applies them after the negate.
   A verdict that ignored them would be wrong exactly where they act. Same inequalities the
   SurfaceWorld branch already uses.

The diff layer needs no new guard: ClassifyTile already returns Mixed for any tile with player mods
in range, before any of this.

TEST: VoxelForge.OpStack.ClassifyTileSoundness -- the same brute-force oracle as the existing
ClassifyTileSoundness, on a world where every strate opted in. It does not check the fold (that is
BoxVerdictFold) or the operators (those are the eight equivalence tests); it checks the WIRING. The
number to read first is the count of tiles actually brute-forced: zero non-Mixed verdicts would mean
the test proved nothing, so that case is an ERROR rather than a quiet pass. Its failure message
lists the four suspects in the order worth checking.

FIXTURE: FTestWorld::Build gains a bUseOperatorStack parameter (default false, so the thirteen
existing tests still exercise the switch), and every test world now gets a PROCESS-UNIQUE
LayoutVersion. That second change fixes a real cross-test hazard that was only ever hidden by an
accident: PassagesVersion is per-instance and starts at 0, so two FTestWorlds both reported 1, and
GetDensityAt's per-chunk caches are keyed on (ChunkCoord, LayoutVersion) -- one world could be
served the previous world's params AND its CP_UseOpStack flag. Invisible while every world agreed
the flag was false. The first world that ticks it removes that coincidence, in both directions.

WHAT THIS BUYS TODAY: Maze, FlatPlain/CrystalChamber, VerticalShafts and FloatingIslands can now
prove tiles in production, which is where the measured skipping (Maze 23/60, slabs 36-40/60) turns
into frames. TunnelNetwork and Underwater still prove nothing: their chain dies at FRoomGraphSource,
which answers Both with unknown amplitude. Making it answer spatially means building the SDF cache
for the queried box -- now worth doing, since a skipped tile saves 30k+ density evaluations, and the
amplitude fold plus the modifiers' Identity inheritance are already in place to receive it. That is
the next piece.

And nothing here changes Jahni's current world: no strate asset has bUseOperatorStack ticked, so
every guard above is unreachable in his project until he ticks one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 04:05:42 +02:00
Fr0zkaandClaude Opus 5 cd4cf216f5 fix: AUDIT C1 — bounded, site-salted seed offsets across all 85 noise sites
The op stack had already inherited this three times and every remaining port would
copy it again, so fixing it now is cheaper than after.

The audit's documented fix was wrong: bounding SeedF while keeping the * 97.7f
multiplier still reaches 1.6e6, where the ULP is 0.19 — 9.5x the per-voxel step.
Less spectacular, still broken, ticket closed.

VoxelHash::SeedOffset(Seed, SiteKey) inverts the roles: the multiplier no longer
decorrelates by amplifying, it IDENTIFIES the site, and the hash decorrelates.
Output is in final units, bounded to [0, 16383], so the ULP is 10% of a voxel step.
Site-salted, so two seeds must collide at all ~50 sites rather than sharing one
global bucket.

Safe to apply without compiling because the transformation is a pure regex and the
literal stays visible at the call site, so each line remains eye-checkable against
the original. Applied to all three files in one pass so the archetype switch and the
ported ops changed identically — had they not, the three equivalence tests would say
so. 62 + 7 + 16 sites, none left, plus two bare `+ SeedF` worm sites by hand.

New test VoxelForge.Determinism.LargeSeedSurvives (seeds up to 2e9) because the
equivalence tests are structurally blind to this: they compare the stack against the
switch, both read the same faulty expression, so at a large seed both collapse
identically — bit-identical, green, and both flat. An oracle that shares the bug
cannot see it. This test asserts a property instead of a comparison.

EXPECT EVERY WORLD TO LOOK DIFFERENT: this re-rolls every noise offset in the plugin.
Intended, and covered by OPSTACK-PLAN 2.6.1.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 16:33:36 +02:00
Fr0zkaandClaude Opus 5 beb66e06d4 test: unit-test the op-stack fold, and make the build actually see VoxelDensityOp.h
VoxelDensityOp.h was included by no .cpp, so the compiler would never have
looked at it -- a header committed as "ready to build" that the build ignores.
The ClassifyTile test now includes it, which is also the right home for the
fold's own test: the fold claims to reproduce the hand-written ClassifyTile, and
that claim is pure logic with no world, noise or threads behind it.

VoxelForge.OpStack.BoxVerdictFold walks the correspondence case by case,
including the one that justifies ClassifyBox existing at all: a box entirely
inside the top seal band, where the source says AllAir and the seal forces
AllSolid. A pure FillOnly would resolve that to Mixed and silently lose a tile
T1.d skips today.

Also casts UE_ARRAY_COUNT to int32 in the fixture -- signed/unsigned comparison
in a loop condition is a warning, and UE builds warnings as errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 02:09:17 +02:00
Fr0zkaandClaude Opus 5 6eec796403 test: Phase 0.5 — the three automation tests (density purity, ClassifyTile, DiffLayer)
The plugin had zero tests, and the docs make dozens of "bit-identical" and
"conservative verdict" claims that nothing machine-checks. OPSTACK-PLAN §4
Phase 0.5 asks for these before any op-stack work is built on top.

- VoxelForgeTestFixture.h — headless world (transient strate definitions ->
  UVoxelSettings -> a real UVoxelStrateManager::Initialize), so the tests hit
  UVoxelGenerator::GetDensityAt where the ~30 thread_local caches actually live.
  One strate per archetype, pinned via FixedStrates so slot index -> archetype
  is stable across seeds.

- DensityPurity — 10k points re-sampled in shuffled order on the same thread and
  on N worker threads, asserting BIT equality. AVoxelWorld::ValidateDeterminism
  is game-thread only and structurally cannot see worker-cache divergence, which
  is how AUDIT C2 stayed hidden. Includes a flat-field canary so a collapsed
  noise field (AUDIT C1) can't make the test pass vacuously, and a diff-layer
  pass that exercises the direct-mapped DiffSlots cache.

- ClassifyTileSoundness — scans tiles for a non-Mixed verdict, then brute-forces
  the exact mesher lattice (g in [-1, Cells+1], margin included) and asserts every
  sample is on the claimed side of IsoLevel 0. A false AllSolid/AllAir is an
  invisible collisionless hole; T1.d v1 was reverted for exactly that in June.
  Errors out rather than passing if no tile yielded a verdict to check.

- DiffLayerContention — N reader threads running the worker call mix while the
  game thread writes and Clear()s, asserting survival and a monotonic ModsVersion.

UNVERIFIED: never compiled — this module has never pulled in AutomationTest.h
and Private/Tests/ is new. See OPSTACK-PROGRESS.md for the likely error spots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:50:26 +02:00