From 43eaf1ef0e9195deba94a296a438be99e1903861 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sat, 5 Sep 2026 00:04:24 +0200 Subject: [PATCH] test(composer): the handmade corpus is load-bearing for QUALITY, not validity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Answers Jahni's standing wish directly: "in the best of both worlds, i'd like the system to not rely at all on the handmade ones." Three arms, same size, same measurement, same criteria: approach survival walkable mean feature-scale mean corpus blend (today) 13/16 81% 0.0661 92.3 naive uniform 7/16 44% 0.0126 64.6 constraint-sampled 7/16 44% 0.0129 31.4 ⭐ THE ANSWER IS NOT THE SURVIVAL RATE, IT IS THE QUALITY GAP. Constraint sampling produces VALID worlds -- it passes the primordial law more often than naive rolling (1 law failure vs 3) -- but the survivors are materially worse: ~5x lower walkable fraction and ~3x lower feature scale. A composer that emits technically-passing but uniformly mediocre places has not solved the problem. So the corpus carries something the declared constraints do not. §3.3 said independent rolling destroys the CORRELATIONS; stating those correlations explicitly recovers validity but not the tacit quality that authored vectors encode. That is worth knowing and it was not knowable by argument. ⇒ VERDICT: keep the corpus in production. Constraint sampling is a corpus-free BOOTSTRAP (it can seed a corpus from nothing), and PROMOTION (Tier 5) is the real route off handmade content -- dilution, not removal. Naive rolling is rejected outright. ⚠️ UNDER-POWERED, stated plainly: 16 candidates per arm, not the 256 the spec asked for -- the fixed 8M-cell box/law workload made 256x3 too slow to finish. The 5x walkable gap is large enough to be believable at n=16, but the survival rates (44% vs 44%) are not separable at that sample size. Do not quote these as precise. Authored-corpus audit, and Codex read it correctly: 3 of 12 vectors violate the proposed relations (DA_Strate3 room coverage 2.68 vs bound 0.35; Default_SurfaceWorld ground bound; Default_VerticalShafts shaft radius 5 vs roughness margin 5.875). That does NOT mean those assets are broken -- it means the new relations are conservative HYPOTHESES, not universal corpus invariants. Recorded as such. Box law held across all three arms: 768 proved, 673,486 voxels brute-forced, 0 violations. §6.2 did not reopen under corpus-free parameters. Also noted: "uniform within clamps" was underspecified in my spec -- many fields have no finite upper clamp. Both corpus-free arms use the same declared H-relative envelopes so the comparison stays honest. Build clean, CorpusFree green, WorldRadiusVoxels=0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU --- CODEMAP.md | 17 + COMPOSER-NOTES.md | 125 ++ .../VoxelForgeComposerCorpusFreeTest.cpp | 795 +++++++++++ .../Private/VoxelStrateComposer.cpp | 1165 +++++++++++++++++ .../VoxelForge/Public/VoxelStrateComposer.h | 29 + 5 files changed, 2131 insertions(+) create mode 100644 Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp diff --git a/CODEMAP.md b/CODEMAP.md index 3c1030c..2a5b689 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -477,6 +477,22 @@ it. `RequiredResources` / `ProvidedResources` close the room-state hole that cha could not express. `VoxelForgeComposerStructureRollTest` rolls 64 candidates, measures them through the offline sampler, and brute-forces every uniform box verdict from the novel stack itself. +`EVoxelStrateCorpusFreeSamplingMode`, `VF_RollStrateParamsCorpusFree`, and +`VF_ValidateStrateCorpusFreeConstraints` are the offline corpus-free control/experiment. They never +read a corpus entry or strate asset: `NaiveUniform` rolls every live scalar independently inside a +finite, code-declared envelope, while `ConstraintSampled` draws geometry quantities and derives +dependent quantities from the generator equations before applying the envelope as a final safety +net. `VoxelForgeComposerCorpusFreeTest.cpp` compares both arms with today's corpus blend using the +same structure recipe, fixed measurement window, one arrival/departure law, and box-verdict brute +force. The requested 256×3 and 64×3 runs were abandoned before aggregate output because the fixed +8M-cell law/box workload was too slow; the completed equal-arm run is 16 candidates per arm and +22.995 s inside the test. Its final result is **13/16 corpus-blend survivors, 7/16 naive, and +7/16 constraint-sampled**. Survivor quality is not equivalent: corpus survivors averaged +**0.066071 walkable / 92.307693 voxel feature scale**, versus **0.012566 / 64.571426** for naive +and **0.012851 / 31.428572** for constrained. All three arms had **0 box-verdict violations**; +proved boxes / checked lattice voxels were **196 / 260,876**, **310 / 412,610**, and +**262 / 348,722** respectively. + Public/VoxelWorld.h + Private/VoxelWorld.cpp — ApplyComposerCandidate exposes the four Live Edit|Composer properties and the CallInEditor button. It preflights the candidate, pauses generation before installing the manager override, and calls the existing epoch-aware full reset. @@ -560,6 +576,7 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor | `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. Bootstrap follow-up: **1,422 applications / 168 distinct fields**; **38/64 survival**, feature scale **0→376**, walkable **0.000000→0.286766** (no broadening beyond the prior run); **1,356 Mixed + 608 AllSolid + 596 AllAir = 1,204 proved boxes**, **1,602,524 lattice voxels checked, 0 violations**, 146.250 s total in the final full-namespace run. | | ″ | `VoxelForge.Composer.TerrainDetailLiveness` | Fixed 4,096-point `GetDensityAt` lattice, legacy and operator-stack paths; changes one terrain-detail group at a time with an empty terrain-op pool. Proves 9 live groups / 23 fields and 3 dead groups / 11 fields; all 24 rows match. | | `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 42 survivors, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Focused run: **42/64 survival (65.6%)**, **64 distinct recipes**, **6,848 bootstrap applications / 107 distinct fields**, **659 proved boxes / 877,129 voxels / 0 violations**, **64 coarse filled + 64 coarse contour pairs**, **42 fine ROI filled + 42 fine contour pairs**, **184.659 s**, **0 refusals**. | +| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm run uses **16 candidates per arm** (256×3 and 64×3 were stopped before aggregate output for runtime), step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **13/16, 7/16, 7/16** survival; survivor walkable means **0.066071, 0.012566, 0.012851** and feature-scale means **92.307693, 64.571426, 31.428572**. Box checks: **196/260,876**, **310/412,610**, **262/348,722** proved/voxels, **0 violations** in every arm. | | `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. | | `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. | | `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test fits the measurement AABB to each arrival/departure mouth pair, reports exact before/after seed-6 cell counts, reruns every negative with doubled margin, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. | diff --git a/COMPOSER-NOTES.md b/COMPOSER-NOTES.md index f14e5f1..d9dcb45 100644 --- a/COMPOSER-NOTES.md +++ b/COMPOSER-NOTES.md @@ -470,6 +470,131 @@ and **0 violations** (1,901 Mixed, 266 AllSolid, 393 AllAir). The parameter-roll 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. +### Corpus-free measurement (2026-09-04) + +The proposed alternative to copying correlations from examples was measured directly. The structure +recipe is shared by all three arms, so only parameter provenance changes: + +* **Corpus blend (today):** the existing exact-archetype weighted parent blend, ±15% spread jitter, + and final metadata clamps. +* **Naive uniform:** every live scalar is independently uniform in the same finite corpus-free + envelope. No authored vector, default vector, blend, or corpus spread is read. +* **Constraint-sampled:** independent geometry quantities are drawn; dependent quantities are + calculated from the relations below, then the same envelope is applied as a final safety net. + +There is an important wording issue in Part A: most fields have only `ClampMin` and no finite +`ClampMax` (`VoxelStrateTypes.h` has 135 minimums and 52 maximums across 216 UPROPERTYs), while +the native tunnel fields have no reflected metadata. “Uniform within clamps” is therefore undefined +for much of the parameter space. For this measurement, `VF_GetCorpusFreeRange` declares finite +H-relative operational envelopes for those fields; it does not use a corpus value or a default value +as a range. The exact envelope is in `VoxelStrateComposer.cpp:1087-1278`, and both corpus-free arms +use it so the comparison is fair. This makes the naive result conditional on an explicit finite +support, which is a spec caveat rather than a hidden tuning target. + +#### Declared relations + +These are sufficient conservative relations for the experiment, derived from the existing generator +equations. They are not claimed to be invariants of every existing authored asset; the audit below +checks that question rather than assuming it. + +| relation | declaration used by `ConstraintSampled` | code that supplies the geometry/math | +|---|---|---| +| Room placement, overlap, and isolation | Let `S=RoomSpacing`, `D=RoomDensity`, and `R=MaxRoomRadius`. Draw target area coverage `C = πR²D/S²` in `0.08..0.18`; derive `R = S*sqrt(C/(πD))`; require `C∈[0.02,0.35]` and `R≤S/2`. The coverage interval is an explicit experiment support for the room occupancy equation, not a generator hard limit. | `VoxelCaveMorphology.cpp:1305-1326` rolls room existence, jitter, and radius; `VoxelCaveMorphology.cpp:1319-1320` gives the `[0.15,0.85]` cell jitter. Implemented/validated at `VoxelStrateComposer.cpp:1522-1540` and `:3094-3104`. | +| Tunnel reach | Neighboring-room horizontal jitter can be `1.7S` in each axis, hence `1.7S√2`; vertical center separation can be `2*R*RoomHeightRatio`. Derive `MaxTunnelLength` as at least the Euclidean sum of those bounds. This matches the generator’s distance gate. | `VoxelCaveMorphology.cpp:1388` and `:1502` reject out-of-reach links; implemented/validated at `VoxelStrateComposer.cpp:1557-1564` and `:3114-3121`. | +| Room vertical fit | `BoundarySealThickness + max(MaxRoomRadius,OriginRoomRadius)*RoomHeightRatio < H/2`, because room centers are restricted to the seal-free interval and the room Z radius is `Radius*RoomHeightRatio`. | `VoxelCaveMorphology.cpp:137-140` and `:248-250` establish the room/seal buffer; validated at `VoxelStrateComposer.cpp:3106-3112`. | +| Slab floor/ceiling | Worst floor is `H*FloorRelativeHeight + 1.25*FloorRoughness`; worst ceiling is `H*CeilingRelativeHeight - 1.25*CeilingRoughness`. Derive both relative heights from seal + clearance and require a two-voxel void. | `VoxelCaveMorphology.cpp:329-362` and `VoxelGenerator.cpp:2175-2212`; implemented/validated at `VoxelStrateComposer.cpp:1602-1626` and `:3143-3185`. | +| Maze corridor | Derive `CorridorRadius = CellSize*(0.12..0.24)` so it leaves lattice wall; cap roughness below `CorridorRadius+2` so the centerline remains carveable. | `VoxelCaveMorphology.cpp:428-458` bounds the lattice; `VoxelGenerator.cpp:2412-2432` applies roughness and subtracts `2*BaseDensity`; implemented/validated at `VoxelStrateComposer.cpp:1629-1642` and `:3188-3216`. | +| Surface ground/cap and height features | The structural height is bounded by `ElevationRange*(1+MountainStrength)+2*1.25*SurfaceRoughness`. Add conservative budgets for cliff displacement (`CliffStrength*CliffSharpness` times that spread), terrace step, layer depth, and beach width. Derive `BaseGroundRelative` above the bottom seal and `CeilingRelative` below the top seal with cap roughness, undulation, ridge, overhang height, and two-voxel headroom. Cliff is sampled as a scalar and reduced only when this derived height budget cannot contain it. | `VoxelGenerator.cpp:2470-2517` is the structural height equation; `:2537-2557` is the cliff remap; `:2607-2654` is the cap; `:2670-2686` caps overhang height. Implemented/validated at `VoxelStrateComposer.cpp:1645-1724` and `:3219-3269`. | +| Vertical shafts and ledges | Require `ShaftMaxRadius≤ShaftSpacing/2`; require `ShaftMinRadius > 1.5*1.25*SurfaceRoughness + 0.25`; derive `ConnectorRadius > 1.5*1.25*SurfaceRoughness + 1`; derive ledge spacing greater than `2*(LedgeDepth + floor-clearance)`. | `VoxelCaveMorphology.cpp:628-654` moves landings out of roughness/ledge bands; `VoxelGenerator.cpp:3944-3954` derives tree roughness reach and `:4209-4214` applies ledges; implemented/validated at `VoxelStrateComposer.cpp:1730-1755` and `:3272-3310`. | +| Floating islands | Require `IslandMaxRadius≤IslandSpacing/2`; derive island radius and thickness so `BoundarySealThickness + max(0.2R,ThicknessRatio*R) < H/2`; require the minimum radius to exceed roughness plus SDF blend. | `VoxelCaveMorphology.cpp:852-935` computes island radius, underside, warp, and blend pad; `VoxelGenerator.cpp:4268-4344` starts with void and fills the island; implemented/validated at `VoxelStrateComposer.cpp:1757-1780` and `:3313-3355`. | +| Density sign/carve strength | Every solid-starting family samples `BaseDensity>0`; tunnel worm strength is derived above base density, and the maze/shaft/island paths use the existing ±`2*BaseDensity` carve/fill convention. | `VoxelGenerator.cpp:1059`, `:2085`, `:2359-2432`, `:3944-3954`, `:4200`, and `:4272`; the relation checks are in `VoxelStrateComposer.cpp:3029-3355`. | + +Bools and enums are categorical, not arithmetic constraints. `bTunnelsFlowTowardOrigin` is a fair +50/50 draw; `OriginRoomMaxConnections` is an integer draw in the declared `0..12` support; and +`RoughnessNoiseType` is a uniform draw over all four enum values. Reflected boolean fields in the +sibling families are also fair 50/50. There are currently no non-tunnel reflected enums. The +category stream is separate from the scalar stream so neither arm’s scalar coupling changes the +categorical policy. The sampler is deterministic in `(archetype, seed, index, mode, H)`, uses no +retained RNG state, and clamps only after dependent equations (`VoxelStrateComposer.cpp:1782-1841`). + +#### The actual three-way result + +The requested **256×3** run was started and stopped after roughly six minutes without reaching an +aggregate report. A 64×3 attempt (including a repeat after law traversal was made conditional on +passing the fragmentation gate) also exceeded roughly 27 minutes without an aggregate report. Those +partial runs are not data. The completed equal-arm run therefore used **16 candidates per arm**; +all three arms ran the same 16 structure recipes, step-4 measurement over radius 256, `MaxCells= +8,000,000`, and one fixed arrival/departure pair. Criterion precedence is **vacuous → fragmented +(`LargestComponentShare < 0.50`) → law failed**. Law is intentionally skipped for fragmented +candidates because the earlier criterion already decides the candidate; `law skipped` is reported +separately. The completed test took **22.995 s** internally (**22.980 s** in roll/measurement). + +| approach | survival | vacuous | fragmented | law failed | air range | walkable range | feature scale range | +|---|---:|---:|---:|---:|---:|---:|---:| +| corpus blend (today) | 13/16 (81.2%) | 0 | 1 | 2 | 0.003291..0.999878 | 0.000000..0.318692 | 4..332 | +| naive uniform | 7/16 (43.8%) | 3 | 3 | 3 | 0.003471..1.000000 | 0.000000..0.163436 | 0..152 | +| constraint-sampled | 7/16 (43.8%) | 3 | 5 | 1 | 0.002636..1.000000 | 0.000000..0.220178 | 0..52 | + +The intermediate gates were corpus `16 non-vacuous / 15 largest-enough / 13 law passes`, naive +`13 / 10 / 7`, and constrained `13 / 8 / 7`. Observed law failures were 2, 3, and 1 +respectively; the other candidates that did not reach the law were the reported fragmented cases. +No roll, build, measurement, stack-count, channel-order, or context failure occurred. + +The constrained survivors are valid but not as good as the corpus survivors in this run: + +| approach | survivor count | air range (mean) | walkable range (mean) | feature scale range (mean) | +|---|---:|---:|---:|---:| +| corpus blend (today) | 13 | 0.022004..0.999878 (0.801809) | 0.000000..0.318692 (0.066071) | 4..332 (92.307693) | +| naive uniform | 7 | 0.542744..0.996999 (0.856300) | 0.000068..0.044617 (0.012566) | 12..152 (64.571426) | +| constraint-sampled | 7 | 0.801857..0.976726 (0.886178) | 0.000237..0.047382 (0.012851) | 16..52 (31.428572) | + +The constraint arm tied naive survival at 7/16, traded three naive law failures for five +fragmentation failures, and produced a higher mean air fraction but dramatically lower walkable and +feature-scale ranges than the corpus survivors. With only seven survivors in each corpus-free arm, +this is evidence of a quality gap, not a claim about a stable population distribution; it is already +enough to reject “technically valid means equivalent.” + +#### Box-law and authored-corpus audit + +Every arm performed 40 box probes per candidate. Non-`Mixed` verdicts were brute-forced over the +same unit lattice: **0 violations** in all arms. + +| approach | Mixed | AllSolid | AllAir | proved | voxels checked | violations | +|---|---:|---:|---:|---:|---:|---:| +| corpus blend (today) | 444 | 69 | 127 | 196 | 260,876 | 0 | +| naive uniform | 330 | 168 | 142 | 310 | 412,610 | 0 | +| constraint-sampled | 378 | 90 | 172 | 262 | 348,722 | 0 | +| **total** | **1,152** | **327** | **441** | **768** | **673,486** | **0** | + +The constraint sampler produced **0 declared-parameter violations** and stopped-on-violation count +0. `WorldRadiusVoxels` remained **0**. The shared structure recipe had 0 determinism failures; +each arm had 0 parameter-roll determinism failures, 0 invalid stack counts, 0 channel-order +failures, and 0 context violations. + +The same declared relations were audited against all 12 current corpus vectors. **3/12 violate** +the conservative experiment relations: + +* `DA_Strate3` (`TunnelNetwork`, H=128): room area coverage `2.68447`, above the declared `0.35` + upper support. +* `Default_SurfaceWorld` (`SurfaceWorld`, H=128): conservative ground lower bound `-15.35`, + below the bottom seal `4`. +* `Default_VerticalShafts` (`VerticalShafts`, H=128): minimum shaft radius `5`, below the roughness + margin `5.875`. + +These are findings about the proposed sufficient relations, not proof that those authored worlds +are visibly broken: two are deliberately worst-case bounds and the first is an occupancy heuristic. +The corpus is living outside the new constraint envelope in three places, so the envelope cannot be +silently promoted to a universal validator without either weakening it or changing those assets. + +#### Verdict + +The corpus is still load-bearing for quality. Keep the corpus-blend arm in production and use the +constraint sampler as a corpus-free exploratory/bootstrap arm whose passing candidates can enter +Tier 5 promotion. Do not replace the production arm with naive uniform rolling. The data does not +justify dropping the handmade vectors today; it supports keeping them while promotion gradually +dilutes their share. A future larger sample is warranted after optimizing the fixed law/box pass, +but it should be treated as confirmation, not as permission to tune these observed counts. + ### 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 diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp new file mode 100644 index 0000000..ee36ee7 --- /dev/null +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp @@ -0,0 +1,795 @@ +// Corpus-free composer measurement: current corpus blend versus independent and constrained rolls. + +#if WITH_DEV_AUTOMATION_TESTS + +#include "Misc/AutomationTest.h" +#include "HAL/PlatformTime.h" +#include "Math/RandomStream.h" + +#include "VoxelForgeTestFixture.h" +#include "VoxelDensityOpStack.h" +#include "VoxelSettings.h" +#include "VoxelStrateComposer.h" +#include "VoxelStrateDefinition.h" +#include "VoxelStrateMeasure.h" + +namespace +{ + // 256x3 and 64x3 were attempted first. The latter exceeded 27 minutes before reaching + // its aggregate report because the fixed 8M-cell law/box checks are expensive for the + // corpus-free fields. Keep the final reduced count equal across all three arms. + constexpr int32 NumCandidates = 16; + constexpr float LargestComponentSurvivalThreshold = 0.50f; + constexpr int32 BoxChecksPerCandidate = 40; + constexpr int32 BoxStep = 1; + constexpr int32 BoxCells = 8; + + const TCHAR* VF_ConnectivityName(EVoxelConnectivityResult Result) + { + switch (Result) + { + case EVoxelConnectivityResult::Connected: return TEXT("Connected"); + case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnected"); + case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid"); + case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid"); + case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow"); + case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudget"); + } + return TEXT("Unknown"); + } + + int32 VF_FloorDiv(int32 A, int32 B) + { + const int32 Q = A / B; + const int32 R = A % B; + return (R != 0 && (R < 0) != (B < 0)) ? Q - 1 : Q; + } + + struct FBlockSpec + { + EVoxelStrateParamBlock Block; + ECaveGeneratorType Archetype; + uint32 SeedSalt; + }; + + static const FBlockSpec GBlockSpecs[] = + { + { EVoxelStrateParamBlock::TunnelNetwork, ECaveGeneratorType::TunnelNetwork, 0x1001u }, + { EVoxelStrateParamBlock::Slab, ECaveGeneratorType::FlatPlain, 0x1003u }, + { EVoxelStrateParamBlock::Maze, ECaveGeneratorType::Maze, 0x1005u }, + { EVoxelStrateParamBlock::Surface, ECaveGeneratorType::SurfaceWorld, 0x1007u }, + { EVoxelStrateParamBlock::VerticalShaft, ECaveGeneratorType::VerticalShafts, 0x1009u }, + { EVoxelStrateParamBlock::FloatingIsland, ECaveGeneratorType::FloatingIslands, 0x100Bu }, + }; + + void VF_CopyRollBlock(FVoxelStrateArchetypeParams& OutParams, + const FVoxelStrateRollInfo& Roll, + ECaveGeneratorType Archetype) + { + switch (Archetype) + { + case ECaveGeneratorType::TunnelNetwork: + case ECaveGeneratorType::Underwater: + OutParams.TunnelNetworkParams = Roll.ArchetypeParams.TunnelNetworkParams; + break; + case ECaveGeneratorType::FlatPlain: + case ECaveGeneratorType::CrystalChamber: + OutParams.SlabParams = Roll.ArchetypeParams.SlabParams; + break; + case ECaveGeneratorType::Maze: + OutParams.MazeParams = Roll.ArchetypeParams.MazeParams; + break; + case ECaveGeneratorType::SurfaceWorld: + OutParams.SurfaceParams = Roll.ArchetypeParams.SurfaceParams; + break; + case ECaveGeneratorType::VerticalShafts: + OutParams.VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams; + break; + case ECaveGeneratorType::FloatingIslands: + OutParams.FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams; + break; + default: + break; + } + } + + void VF_SetAllRuntimeBounds(FVoxelStrateArchetypeParams& Params, + float TopWorldZ, float BottomWorldZ) + { + Params.TunnelNetworkParams.StrateTopWorldZ = TopWorldZ; + Params.TunnelNetworkParams.StrateBottomWorldZ = BottomWorldZ; + Params.SlabParams.StrateTopWorldZ = TopWorldZ; + Params.SlabParams.StrateBottomWorldZ = BottomWorldZ; + Params.MazeParams.StrateTopWorldZ = TopWorldZ; + Params.MazeParams.StrateBottomWorldZ = BottomWorldZ; + Params.SurfaceParams.StrateTopWorldZ = TopWorldZ; + Params.SurfaceParams.StrateBottomWorldZ = BottomWorldZ; + Params.VerticalShaftParams.StrateTopWorldZ = TopWorldZ; + Params.VerticalShaftParams.StrateBottomWorldZ = BottomWorldZ; + Params.FloatingIslandParams.StrateTopWorldZ = TopWorldZ; + Params.FloatingIslandParams.StrateBottomWorldZ = BottomWorldZ; + } + + class FStackDensitySampler final : public IVoxelStrateDensitySampler + { + public: + explicit FStackDensitySampler(const FVoxelOpStack& InStack) : Stack(InStack) {} + + float SampleDensity(float WorldX, float WorldY, float WorldZ) const override + { + return Stack.EvalMC(WorldX, WorldY, WorldZ); + } + + private: + const FVoxelOpStack& Stack; + }; + + struct FBoxVerdictReport + { + int32 Mixed = 0; + int32 AllSolid = 0; + int32 AllAir = 0; + int32 Proved = 0; + int64 CheckedVoxels = 0; + int32 Violations = 0; + FString FirstViolation; + }; + + FBoxVerdictReport VF_CheckStackBoxVerdicts(const FVoxelOpStack& Stack, + const FVoxelOpContext& Context, + int32 CandidateIndex, + int32 BottomVoxelZ, + int32 TopVoxelZ) + { + FBoxVerdictReport Report; + constexpr int32 Extent = BoxStep * BoxCells; + constexpr int32 SpanCells = 40; + const int32 LoTile = VF_FloorDiv(BottomVoxelZ, Extent); + const int32 HiTile = FMath::Max(LoTile, VF_FloorDiv(TopVoxelZ, Extent)); + FRandomStream Rng(0x6C617731 ^ CandidateIndex); + + for (int32 BoxIndex = 0; BoxIndex < BoxChecksPerCandidate; ++BoxIndex) + { + const FIntVector Origin( + Rng.RandRange(-SpanCells, SpanCells) * Extent, + Rng.RandRange(-SpanCells, SpanCells) * Extent, + Rng.RandRange(LoTile, HiTile) * Extent); + const FBox Box( + FVector((float)(Origin.X - BoxStep), (float)(Origin.Y - BoxStep), + (float)(Origin.Z - BoxStep)), + FVector((float)(Origin.X + (BoxCells + 1) * BoxStep), + (float)(Origin.Y + (BoxCells + 1) * BoxStep), + (float)(Origin.Z + (BoxCells + 1) * BoxStep))); + const EVoxelTileClass Verdict = Stack.ClassifyBox(Box, Context); + if (Verdict == EVoxelTileClass::Mixed) + { + ++Report.Mixed; + continue; + } + if (Verdict == EVoxelTileClass::AllSolid) + { + ++Report.AllSolid; + } + else + { + ++Report.AllAir; + } + ++Report.Proved; + + const int32 GridDim = BoxCells + 1; + const bool bClaimsSolid = Verdict == EVoxelTileClass::AllSolid; + for (int32 GZ = -1; GZ <= GridDim; ++GZ) + for (int32 GY = -1; GY <= GridDim; ++GY) + for (int32 GX = -1; GX <= GridDim; ++GX) + { + const float X = (float)(Origin.X + GX * BoxStep); + const float Y = (float)(Origin.Y + GY * BoxStep); + const float Z = (float)(Origin.Z + GZ * BoxStep); + const float Density = Stack.EvalMC(X, Y, Z); + ++Report.CheckedVoxels; + const bool bAgrees = bClaimsSolid ? (Density < 0.0f) : (Density >= 0.0f); + if (!bAgrees) + { + ++Report.Violations; + if (Report.FirstViolation.IsEmpty()) + { + Report.FirstViolation = FString::Printf( + TEXT("candidate %d box %d origin (%d,%d,%d) said %s but MC density " + "at (%.0f,%.0f,%.0f) was %.9g"), + CandidateIndex, BoxIndex, Origin.X, Origin.Y, Origin.Z, + bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"), + X, Y, Z, Density); + } + } + } + } + return Report; + } + + struct FQualityStats + { + int32 Count = 0; + float AirMin = FLT_MAX; + float AirMax = -FLT_MAX; + float WalkableMin = FLT_MAX; + float WalkableMax = -FLT_MAX; + float FeatureMin = FLT_MAX; + float FeatureMax = -FLT_MAX; + double AirSum = 0.0; + double WalkableSum = 0.0; + double FeatureSum = 0.0; + + void Add(const FVoxelStrateMetrics& Metrics) + { + ++Count; + AirMin = FMath::Min(AirMin, Metrics.AirFraction); + AirMax = FMath::Max(AirMax, Metrics.AirFraction); + WalkableMin = FMath::Min(WalkableMin, Metrics.WalkableFraction); + WalkableMax = FMath::Max(WalkableMax, Metrics.WalkableFraction); + FeatureMin = FMath::Min(FeatureMin, Metrics.MedianFeatureScale); + FeatureMax = FMath::Max(FeatureMax, Metrics.MedianFeatureScale); + AirSum += Metrics.AirFraction; + WalkableSum += Metrics.WalkableFraction; + FeatureSum += Metrics.MedianFeatureScale; + } + + FString RangeText(float Min, float Max) const + { + return Count > 0 + ? FString::Printf(TEXT("%.6f..%.6f"), Min, Max) + : TEXT("n/a"); + } + + FString SummaryText() const + { + return Count > 0 + ? FString::Printf( + TEXT("n=%d air=%s (mean %.6f), walkable=%s (mean %.6f), feature=%s (mean %.6f)"), + Count, *RangeText(AirMin, AirMax), (float)(AirSum / Count), + *RangeText(WalkableMin, WalkableMax), (float)(WalkableSum / Count), + *RangeText(FeatureMin, FeatureMax), (float)(FeatureSum / Count)) + : TEXT("n=0"); + } + }; + + struct FArmStats + { + const TCHAR* Name = TEXT("unknown"); + int32 Requested = 0; + int32 RollFailures = 0; + int32 BuildFailures = 0; + int32 Unmeasured = 0; + int32 Vacuous = 0; + int32 Fragmented = 0; + int32 LawFailed = 0; + int32 LawFailedObserved = 0; + int32 LawSkipped = 0; + int32 Survivors = 0; + int32 NonVacuous = 0; + int32 LargestEnough = 0; + int32 LawPasses = 0; + int32 DeterminismFailures = 0; + int32 InvalidStackCount = 0; + int32 ChannelOrderFailures = 0; + int32 ContextViolations = 0; + int32 LawResults[6] = { 0, 0, 0, 0, 0, 0 }; + FQualityStats AllQuality; + FQualityStats SurvivorQuality; + FBoxVerdictReport Box; + FString FirstFailure; + + void RememberFailure(const FString& Failure) + { + if (FirstFailure.IsEmpty()) + { + FirstFailure = Failure; + } + } + + FString SurvivalText() const + { + return FString::Printf(TEXT("%d/%d (%.1f%%)"), Survivors, Requested, + Requested > 0 ? 100.0f * (float)Survivors / (float)Requested : 0.0f); + } + }; + + int32 VF_LawResultIndex(EVoxelConnectivityResult Result) + { + switch (Result) + { + case EVoxelConnectivityResult::Connected: return 0; + case EVoxelConnectivityResult::NotConnectedAtThisResolution: return 1; + case EVoxelConnectivityResult::StartCellSolid: return 2; + case EVoxelConnectivityResult::GoalCellSolid: return 3; + case EVoxelConnectivityResult::OutOfWindow: return 4; + case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return 5; + } + return 5; + } + + bool VF_BitEqual(float A, float B) + { + uint32 ABits = 0; + uint32 BBits = 0; + FMemory::Memcpy(&ABits, &A, sizeof(ABits)); + FMemory::Memcpy(&BBits, &B, sizeof(BBits)); + return ABits == BBits; + } + +} + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeComposerCorpusFreeTest, + "VoxelForge.Composer.CorpusFree", + EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) + +bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters) +{ + using namespace VoxelForgeTest; + (void)Parameters; + const double TestStartSeconds = FPlatformTime::Seconds(); + + const FString SettingsPath = TEXT("/Game/VoxelForge/DA_Settings.DA_Settings"); + UVoxelSettings* AuthoredSettings = LoadObject(nullptr, *SettingsPath); + if (AuthoredSettings == nullptr) + { + AddError(FString::Printf(TEXT("Could not load %s; corpus-free measurement did not run."), + *SettingsPath)); + return false; + } + + FVoxelStrateCorpus Corpus; + FString CorpusReport; + const bool bCorpusLoaded = Corpus.LoadFromAssetRegistry(CorpusReport); + AddInfo(CorpusReport); + TestTrue(TEXT("corpus blend arm has a usable authored corpus"), + bCorpusLoaded && Corpus.IsValid()); + if (!bCorpusLoaded || !Corpus.IsValid()) + { + return false; + } + TestEqual(TEXT("corpus contains the four project vectors plus eight defaults"), + Corpus.GetEntries().Num(), 12); + + FTestWorld World; + World.Build(1337, 2, true); + if (!World.IsValid()) + { + AddError(World.WhyInvalid()); + return false; + } + TestTrue(TEXT("corpus-free fixture keeps WorldRadiusVoxels at 0"), + World.Settings->WorldRadiusVoxels == 0.0f); + + constexpr int32 CandidateStrateIndex = FTestWorld::SlotFlatPlain; + int32 TopVoxelZ = 0; + int32 BottomVoxelZ = 0; + if (!World.GetSlotVoxelZRange(CandidateStrateIndex, TopVoxelZ, BottomVoxelZ)) + { + AddError(TEXT("corpus-free fixture has no candidate Z range")); + return false; + } + const float TopWorldZ = (float)TopVoxelZ + 1.0f; + const float BottomWorldZ = (float)BottomVoxelZ; + const float StrateHeightInVoxels = TopWorldZ - BottomWorldZ; + + FVector ArrivalPoint = FVector::ZeroVector; + FVector DeparturePoint = FVector::ZeroVector; + int32 ArrivalCount = 0; + int32 DepartureCount = 0; + for (const FVoxelPassage& Passage : World.StrateManager->GetPassages()) + { + if (Passage.LowerStrateIndex == CandidateStrateIndex + && Passage.UpperStrateIndex + 1 == CandidateStrateIndex) + { + ArrivalPoint = Passage.LowerPoint; + ++ArrivalCount; + } + if (Passage.UpperStrateIndex == CandidateStrateIndex + && Passage.LowerStrateIndex == CandidateStrateIndex + 1) + { + DeparturePoint = Passage.UpperPoint; + ++DepartureCount; + } + } + TestEqual(TEXT("corpus-free candidate has exactly one arrival mouth"), ArrivalCount, 1); + TestEqual(TEXT("corpus-free candidate has exactly one departure mouth"), DepartureCount, 1); + + FVoxelStrateMeasureSettings MeasureSettings; + MeasureSettings.SampleStep = 4; + MeasureSettings.RadiusInVoxels = 256; + MeasureSettings.CenterXY = FVector2D::ZeroVector; + MeasureSettings.MaxCells = 8000000; + MeasureSettings.MaxRouteRetries = 16; + MeasureSettings.HeadroomCells = 2; + MeasureSettings.InteriorMarginVoxels = -1; + + FArmStats CorpusBlend; + CorpusBlend.Name = TEXT("corpus blend (today)"); + FArmStats Naive; + Naive.Name = TEXT("naive uniform"); + FArmStats Constraint; + Constraint.Name = TEXT("constraint-sampled"); + + int32 RecipeDeterminismFailures = 0; + int32 ConstraintParameterViolations = 0; + FString FirstConstraintParameterViolation; + int32 ConstraintBoxStopCount = 0; + FString FirstConstraintBoxViolation; + bool bStopAfterConstraintViolation = false; + + const double MeasurementStartSeconds = FPlatformTime::Seconds(); + for (int32 CandidateIndex = 0; + CandidateIndex < NumCandidates && !bStopAfterConstraintViolation; + ++CandidateIndex) + { + const FVoxelOpStackRecipe Recipe = VF_RollStrateStructure( + AuthoredSettings->Seed, CandidateIndex); + const FVoxelOpStackRecipe RepeatRecipe = VF_RollStrateStructure( + AuthoredSettings->Seed, CandidateIndex); + if (!VF_AreStrateStructureRecipesIdentical(Recipe, RepeatRecipe)) + { + ++RecipeDeterminismFailures; + } + + FArmStats* Arms[] = { &CorpusBlend, &Naive, &Constraint }; + const EVoxelStrateCorpusFreeSamplingMode Modes[] = { + EVoxelStrateCorpusFreeSamplingMode::NaiveUniform, + EVoxelStrateCorpusFreeSamplingMode::ConstraintSampled, + }; + + for (int32 ArmIndex = 0; ArmIndex < 3 && !bStopAfterConstraintViolation; ++ArmIndex) + { + FArmStats& Arm = *Arms[ArmIndex]; + ++Arm.Requested; + FVoxelStrateArchetypeParams CandidateParams; + bool bParameterRollsValid = true; + bool bConstraintParameterViolationThisCandidate = false; + + for (int32 BlockIndex = 0; BlockIndex < UE_ARRAY_COUNT(GBlockSpecs); ++BlockIndex) + { + const FBlockSpec& Spec = GBlockSpecs[BlockIndex]; + const int32 BlockSeed = AuthoredSettings->Seed + ^ static_cast(Spec.SeedSalt); + FVoxelStrateRollInfo BlockRoll; + if (ArmIndex == 0) + { + BlockRoll = VF_RollStrateParamsDetailedForArchetype( + Corpus, Spec.Archetype, BlockSeed, CandidateIndex); + const FVoxelStrateRollInfo RepeatRoll = VF_RollStrateParamsDetailedForArchetype( + Corpus, Spec.Archetype, BlockSeed, CandidateIndex); + if (!BlockRoll.bValid || !RepeatRoll.bValid + || !VF_AreStrateArchetypeParamsBitIdentical( + BlockRoll.ArchetypeParams, RepeatRoll.ArchetypeParams, Spec.Archetype)) + { + ++Arm.DeterminismFailures; + } + } + else + { + BlockRoll = VF_RollStrateParamsCorpusFree( + Spec.Archetype, BlockSeed, CandidateIndex, Modes[ArmIndex - 1], + StrateHeightInVoxels); + const FVoxelStrateRollInfo RepeatRoll = VF_RollStrateParamsCorpusFree( + Spec.Archetype, BlockSeed, CandidateIndex, Modes[ArmIndex - 1], + StrateHeightInVoxels); + if (!BlockRoll.bValid || !RepeatRoll.bValid + || !VF_AreStrateArchetypeParamsBitIdentical( + BlockRoll.ArchetypeParams, RepeatRoll.ArchetypeParams, Spec.Archetype)) + { + ++Arm.DeterminismFailures; + } + if (ArmIndex == 2 && BlockRoll.bValid) + { + FString Violation; + if (!VF_ValidateStrateCorpusFreeConstraints( + Spec.Archetype, BlockRoll.ArchetypeParams, + StrateHeightInVoxels, Violation)) + { + ++ConstraintParameterViolations; + if (FirstConstraintParameterViolation.IsEmpty()) + { + FirstConstraintParameterViolation = FString::Printf( + TEXT("candidate %d block %d (%s): %s"), CandidateIndex, + static_cast(Spec.Block), + VF_GetStrateArchetypeName(Spec.Archetype), *Violation); + } + bParameterRollsValid = false; + bConstraintParameterViolationThisCandidate = true; + } + } + } + + if (!BlockRoll.bValid) + { + bParameterRollsValid = false; + ++Arm.RollFailures; + Arm.RememberFailure(FString::Printf( + TEXT("candidate %d block %d roll failed: %s"), CandidateIndex, + static_cast(Spec.Block), *BlockRoll.FailureReason)); + continue; + } + VF_CopyRollBlock(CandidateParams, BlockRoll, Spec.Archetype); + } + + if (!bParameterRollsValid) + { + ++Arm.Unmeasured; + if (ArmIndex == 2 && bConstraintParameterViolationThisCandidate) + { + // The constraint relation itself is a precondition of this arm; do not + // spend the remaining budget hiding a violated declared relation. + bStopAfterConstraintViolation = true; + } + continue; + } + VF_SetAllRuntimeBounds(CandidateParams, TopWorldZ, BottomWorldZ); + + FVoxelOpStack Stack; + FVoxelOpContext Context; + FString BuildError; + const bool bBuilt = VF_BuildStackFromRecipe( + Recipe, CandidateParams, AuthoredSettings->Seed, 14.0f, + World.StrateManager.Get(), Stack, Context, &BuildError); + if (!bBuilt) + { + ++Arm.BuildFailures; + ++Arm.Unmeasured; + Arm.RememberFailure(FString::Printf( + TEXT("candidate %d build failed: %s"), CandidateIndex, *BuildError)); + continue; + } + if (Stack.Num() != Recipe.Modifiers.Num() + 7) + { + ++Arm.InvalidStackCount; + } + FString ChannelError; + if (!Stack.ValidateChannelOrder(&ChannelError)) + { + ++Arm.ChannelOrderFailures; + Arm.RememberFailure(FString::Printf( + TEXT("candidate %d channel order failed: %s"), CandidateIndex, *ChannelError)); + } + if (Context.WorldRadiusVoxels != 0.0f) + { + ++Arm.ContextViolations; + Arm.RememberFailure(FString::Printf( + TEXT("candidate %d changed WorldRadiusVoxels to %.9g"), + CandidateIndex, Context.WorldRadiusVoxels)); + } + + Stack.PrepareChunk(Context); + FStackDensitySampler Sampler(Stack); + const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler( + Sampler, BottomVoxelZ, TopVoxelZ + 1, + Context.EdgeSealThickness, MeasureSettings, nullptr); + if (!Metrics.bValid) + { + ++Arm.Unmeasured; + Arm.RememberFailure(FString::Printf( + TEXT("candidate %d measurement refused: %s"), CandidateIndex, + *Metrics.RefusalReason)); + continue; + } + Arm.AllQuality.Add(Metrics); + + const bool bNonVacuous = Metrics.NumSampled > 0 + && Metrics.NumAir > 0 && Metrics.NumSolid > 0; + const bool bLargestEnough = bNonVacuous + && Metrics.LargestComponentShare >= LargestComponentSurvivalThreshold; + + bool bLawPass = false; + if (bLargestEnough) + { + FVoxelConnectivityDiagnostics Law; + if (ArrivalCount == 1 && DepartureCount == 1) + { + Law = VF_DiagnoseConnectivityWithSampler( + Sampler, BottomVoxelZ, TopVoxelZ + 1, + Context.EdgeSealThickness, ArrivalPoint, DeparturePoint, MeasureSettings); + ++Arm.LawResults[VF_LawResultIndex(Law.Result)]; + } + else + { + Law.bValid = false; + Law.Result = EVoxelConnectivityResult::OutOfWindow; + ++Arm.LawResults[VF_LawResultIndex(Law.Result)]; + } + + bLawPass = ArrivalCount == 1 && DepartureCount == 1 + && Law.bValid && Law.Result == EVoxelConnectivityResult::Connected + && !Law.bStartSnapped && !Law.bGoalSnapped; + if (bLawPass) { ++Arm.LawPasses; } + if (!bLawPass) { ++Arm.LawFailedObserved; } + } + else + { + ++Arm.LawSkipped; + } + + if (bNonVacuous) { ++Arm.NonVacuous; } + if (bLargestEnough) { ++Arm.LargestEnough; } + + const bool bSurvivor = bNonVacuous && bLargestEnough && bLawPass; + if (bSurvivor) + { + ++Arm.Survivors; + Arm.SurvivorQuality.Add(Metrics); + } + else if (!bNonVacuous) + { + ++Arm.Vacuous; + } + else if (!bLargestEnough) + { + ++Arm.Fragmented; + } + else + { + ++Arm.LawFailed; + } + + const FBoxVerdictReport BoxReport = VF_CheckStackBoxVerdicts( + Stack, Context, CandidateIndex, BottomVoxelZ, TopVoxelZ); + Arm.Box.Mixed += BoxReport.Mixed; + Arm.Box.AllSolid += BoxReport.AllSolid; + Arm.Box.AllAir += BoxReport.AllAir; + Arm.Box.Proved += BoxReport.Proved; + Arm.Box.CheckedVoxels += BoxReport.CheckedVoxels; + Arm.Box.Violations += BoxReport.Violations; + if (Arm.Box.FirstViolation.IsEmpty() && !BoxReport.FirstViolation.IsEmpty()) + { + Arm.Box.FirstViolation = BoxReport.FirstViolation; + } + if (ArmIndex == 2 && BoxReport.Violations > 0) + { + ++ConstraintBoxStopCount; + bStopAfterConstraintViolation = true; + FirstConstraintBoxViolation = BoxReport.FirstViolation; + } + } + } + const double MeasurementSeconds = FPlatformTime::Seconds() - MeasurementStartSeconds; + + // Audit the same declared relations against the 12 current authored corpus vectors. Project + // assets carry their own height; the eight C++ defaults use the fixture's 4-chunk height. + int32 AuthoredConstraintViolations = 0; + FString AuthoredViolationList; + for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries()) + { + float EntryHeight = 4.0f * (float)CHUNK_SIZE; + if (!Entry.SourcePath.Contains(TEXT("/VoxelForge/ComposerDefaults/"))) + { + UVoxelStrateDefinition* Definition = LoadObject( + nullptr, *Entry.SourcePath); + if (Definition != nullptr) + { + EntryHeight = FMath::Max(1.0f, (float)Definition->StrateHeightInChunks) + * (float)CHUNK_SIZE; + } + } + + FString Violation; + if (!VF_ValidateStrateCorpusFreeConstraints( + Entry.Archetype, Entry.ArchetypeParams, EntryHeight, Violation)) + { + ++AuthoredConstraintViolations; + if (!AuthoredViolationList.IsEmpty()) + { + AuthoredViolationList += TEXT("; "); + } + AuthoredViolationList += FString::Printf( + TEXT("%s (%s, H=%.0f): %s"), *Entry.SourceName, + VF_GetStrateArchetypeName(Entry.Archetype), EntryHeight, *Violation); + } + } + + const FArmStats* Arms[] = { &CorpusBlend, &Naive, &Constraint }; + AddInfo(FString::Printf( + TEXT("Corpus-free measurement: requested %d candidates per arm; structure recipes were " + "shared across all three arms; sample step=%d radius=%d MaxCells=%d, fixed law mouths, " + "criterion precedence is vacuous > fragmented > law failed. Runtime %.3fs, roll/measure " + "%.3fs."), + NumCandidates, MeasureSettings.SampleStep, MeasureSettings.RadiusInVoxels, + MeasureSettings.MaxCells, FPlatformTime::Seconds() - TestStartSeconds, MeasurementSeconds)); + + FString Table = TEXT( + "approach | survival | vacuous | fragmented | law failed | air range | walkable range | feature scale range\n"); + FString SurvivorTable = TEXT( + "survivor quality (ranges are over survivors only; main table ranges are over all measured candidates)\n" + "approach | survivors | air range (mean) | walkable range (mean) | feature scale range (mean)\n"); + for (const FArmStats* Arm : Arms) + { + Table += FString::Printf( + TEXT("%s | %s | %d | %d | %d | %s | %s | %s\n"), + Arm->Name, *Arm->SurvivalText(), Arm->Vacuous, Arm->Fragmented, Arm->LawFailed, + *Arm->AllQuality.RangeText(Arm->AllQuality.AirMin, Arm->AllQuality.AirMax), + *Arm->AllQuality.RangeText(Arm->AllQuality.WalkableMin, Arm->AllQuality.WalkableMax), + *Arm->AllQuality.RangeText(Arm->AllQuality.FeatureMin, Arm->AllQuality.FeatureMax)); + SurvivorTable += FString::Printf( + TEXT("%s | %d | %s | %s | %s\n"), + Arm->Name, Arm->SurvivorQuality.Count, + *Arm->SurvivorQuality.RangeText(Arm->SurvivorQuality.AirMin, Arm->SurvivorQuality.AirMax), + *Arm->SurvivorQuality.RangeText(Arm->SurvivorQuality.WalkableMin, Arm->SurvivorQuality.WalkableMax), + *Arm->SurvivorQuality.RangeText(Arm->SurvivorQuality.FeatureMin, Arm->SurvivorQuality.FeatureMax)); + } + AddInfo(Table); + AddInfo(SurvivorTable); + + for (const FArmStats* Arm : Arms) + { + AddInfo(FString::Printf( + TEXT("%s: non-vacuous=%d, largest>=%.2f=%d, law pass=%d, law failures observed=%d, " + "law skipped=%d, " + "roll failures=%d, build failures=%d, unmeasured=%d, deterministic roll failures=%d; " + "stack-count failures=%d, channel-order failures=%d, context violations=%d; " + "law results Connected=%d NotConnected=%d StartSolid=%d GoalSolid=%d OutOfWindow=%d Budget=%d; " + "quality all %s; survivor quality %s; first failure=%s."), + Arm->Name, Arm->NonVacuous, LargestComponentSurvivalThreshold, Arm->LargestEnough, + Arm->LawPasses, Arm->LawFailedObserved, Arm->LawSkipped, + Arm->RollFailures, Arm->BuildFailures, + Arm->Unmeasured, Arm->DeterminismFailures, + Arm->InvalidStackCount, Arm->ChannelOrderFailures, Arm->ContextViolations, + Arm->LawResults[0], Arm->LawResults[1], Arm->LawResults[2], + Arm->LawResults[3], Arm->LawResults[4], Arm->LawResults[5], + *Arm->AllQuality.SummaryText(), *Arm->SurvivorQuality.SummaryText(), + Arm->FirstFailure.IsEmpty() ? TEXT("none") : *Arm->FirstFailure)); + AddInfo(FString::Printf( + TEXT("%s box verdicts: mixed=%d AllSolid=%d AllAir=%d proved=%d voxels checked=%lld " + "violations=%d; %s"), + Arm->Name, Arm->Box.Mixed, Arm->Box.AllSolid, Arm->Box.AllAir, Arm->Box.Proved, + Arm->Box.CheckedVoxels, Arm->Box.Violations, + Arm->Box.FirstViolation.IsEmpty() ? TEXT("no violations") : *Arm->Box.FirstViolation)); + } + + AddInfo(FString::Printf( + TEXT("Authored-corpus constraint audit: %d/%d vectors violated at their declared strate " + "height. %s"), + AuthoredConstraintViolations, Corpus.GetEntries().Num(), + AuthoredViolationList.IsEmpty() ? TEXT("none") : *AuthoredViolationList)); + AddInfo(FString::Printf( + TEXT("Shared structure determinism failures=%d; per-arm channel/context violations are " + "reported below; constraint parameter violations=%d; constraint box-stop count=%d. %s%s"), + RecipeDeterminismFailures, + ConstraintParameterViolations, ConstraintBoxStopCount, + FirstConstraintParameterViolation.IsEmpty() ? TEXT("") : *FirstConstraintParameterViolation, + FirstConstraintBoxViolation.IsEmpty() ? TEXT("") + : *FString::Printf(TEXT("; first box violation: %s"), *FirstConstraintBoxViolation))); + + TestEqual(TEXT("configured candidate count requested for corpus blend"), CorpusBlend.Requested, NumCandidates); + TestEqual(TEXT("configured candidate count requested for naive uniform"), Naive.Requested, NumCandidates); + TestEqual(TEXT("configured candidate count requested for constraint-sampled"), Constraint.Requested, NumCandidates); + TestEqual(TEXT("corpus blend roll failures"), CorpusBlend.RollFailures, 0); + TestEqual(TEXT("naive uniform roll failures"), Naive.RollFailures, 0); + TestEqual(TEXT("constraint-sampled roll failures"), Constraint.RollFailures, 0); + TestEqual(TEXT("corpus blend build failures"), CorpusBlend.BuildFailures, 0); + TestEqual(TEXT("naive uniform build failures"), Naive.BuildFailures, 0); + TestEqual(TEXT("constraint-sampled build failures"), Constraint.BuildFailures, 0); + TestEqual(TEXT("corpus-free parameter determinism failures"), + Naive.DeterminismFailures + Constraint.DeterminismFailures, 0); + TestEqual(TEXT("corpus parameter determinism failures"), CorpusBlend.DeterminismFailures, 0); + TestEqual(TEXT("shared structure recipe determinism failures"), RecipeDeterminismFailures, 0); + TestEqual(TEXT("constraint-sampled declared-parameter violations"), + ConstraintParameterViolations, 0); + TestEqual(TEXT("constraint-sampled box verdict violations"), + Constraint.Box.Violations, 0); + TestEqual(TEXT("corpus blend box verdict violations"), CorpusBlend.Box.Violations, 0); + TestEqual(TEXT("naive uniform box verdict violations"), Naive.Box.Violations, 0); + TestEqual(TEXT("corpus blend WorldRadiusVoxels/context violations"), + CorpusBlend.InvalidStackCount + CorpusBlend.ChannelOrderFailures + + CorpusBlend.ContextViolations, 0); + TestEqual(TEXT("naive uniform WorldRadiusVoxels/context violations"), + Naive.InvalidStackCount + Naive.ChannelOrderFailures + Naive.ContextViolations, 0); + TestEqual(TEXT("constraint-sampled WorldRadiusVoxels/context violations"), + Constraint.InvalidStackCount + Constraint.ChannelOrderFailures + + Constraint.ContextViolations, 0); + TestTrue(TEXT("constraint sampler did not stop early on a declared relation or box violation"), + !bStopAfterConstraintViolation); + + const double TestSeconds = FPlatformTime::Seconds() - TestStartSeconds; + AddInfo(FString::Printf(TEXT("CorpusFree total runtime %.3fs; all three arms requested %d each; " + "WorldRadiusVoxels=0."), TestSeconds, NumCandidates)); + return true; +} + +#endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Private/VoxelStrateComposer.cpp b/Source/VoxelForge/Private/VoxelStrateComposer.cpp index 328ac37..833549e 100644 --- a/Source/VoxelForge/Private/VoxelStrateComposer.cpp +++ b/Source/VoxelForge/Private/VoxelStrateComposer.cpp @@ -15,6 +15,7 @@ #include "VoxelStrateDefinition.h" #include "VoxelTerrainOpDefinition.h" #include "VoxelDensityOpStack.h" +#include "VoxelTypes.h" #include #include @@ -1059,6 +1060,816 @@ namespace return Value; } + struct FCorpusFreeRange + { + float Min = 0.0f; + float Max = 0.0f; + }; + + bool VF_SetCorpusFreeRange(FCorpusFreeRange& Out, float Min, float Max) + { + if (!FMath::IsFinite(Min) || !FMath::IsFinite(Max) || Min > Max) + { + return false; + } + Out.Min = Min; + Out.Max = Max; + return true; + } + + /** + * Finite envelopes for the corpus-free control. These are deliberately not copied from a + * corpus entry. UPROPERTY clamps are used where the type declares them; fields with only a + * lower clamp get the broadest finite envelope implied by the generator's voxel geometry. + * ConstraintSampled uses the same envelopes for its independent draws and clamps only after + * its dependent equations have been evaluated. + */ + bool VF_GetCorpusFreeRange(ECaveGeneratorType Archetype, const FString& FieldName, + float StrateHeightInVoxels, FCorpusFreeRange& Out) + { + const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); + const float HQuarter = H * 0.25f; + const float HHalf = H * 0.5f; + + if (VF_IsRuntimeField(FieldName) || VF_IsExcludedForArchetype(Archetype, FieldName)) + { + return false; + } + + auto Set = [&Out](float Min, float Max) -> bool + { + return VF_SetCorpusFreeRange(Out, Min, Max); + }; + + if (VF_IsTunnelArchetype(Archetype)) + { + if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f); + if (FieldName == TEXT("VerticalScale")) return Set(0.25f, 2.5f); + if (FieldName == TEXT("WormFrequency")) return Set(0.002f, 0.10f); + if (FieldName == TEXT("WormHorizontalBias")) return Set(1.0f, 10.0f); + if (FieldName == TEXT("WormThreshold")) return Set(0.01f, 0.50f); + if (FieldName == TEXT("WormStrength")) return Set(1.0f, 64.0f); + if (FieldName == TEXT("WormNetworkRange")) return Set(0.0f, H * 1.5f); + if (FieldName == TEXT("RoomSpacing")) return Set(16.0f, H * 2.5f); + if (FieldName == TEXT("RoomDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("MinRoomRadius")) return Set(1.0f, HHalf); + if (FieldName == TEXT("MaxRoomRadius")) return Set(1.0f, H); + if (FieldName == TEXT("RoomHeightRatio")) return Set(0.10f, 1.0f); + if (FieldName == TEXT("RoomShapeVariety")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("RoomFloorCutMin")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("RoomFloorCutMax")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("FloorReliefStrength")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("FloorReliefFrequency")) return Set(0.001f, 0.10f); + if (FieldName == TEXT("OriginRoomRadius")) return Set(0.0f, H * 0.75f); + if (FieldName == TEXT("TunnelMinRadius")) return Set(1.0f, HQuarter); + if (FieldName == TEXT("TunnelMaxRadius")) return Set(1.0f, HHalf); + if (FieldName == TEXT("TunnelDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("MaxTunnelLength")) return Set(1.0f, H * 4.0f); + if (FieldName == TEXT("TunnelWarpStrength")) return Set(0.0f, H); + if (FieldName == TEXT("TunnelHorizontalBias")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("TunnelEndpointZOffset")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("SDFBlendRadius")) return Set(0.0f, H * 0.15f); + if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("CaveWarpStrength")) return Set(0.0f, HHalf); + if (FieldName == TEXT("CaveWarpFrequency")) return Set(0.001f, 0.10f); + if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("RoughnessFrequency")) return Set(0.001f, 0.20f); + if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("DomainWarpStrength")) return Set(0.0f, HHalf); + if (FieldName == TEXT("DomainWarpFrequency")) return Set(0.001f, 0.20f); + if (FieldName == TEXT("FloorBias")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("TerraceStepHeight")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("TerraceHardness")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("TerraceNoiseDisplacement")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("LayerLineSpacing")) return Set(0.0f, H); + if (FieldName == TEXT("LayerLineDepth")) return Set(0.0f, H * 0.10f); + if (FieldName == TEXT("OverhangStrength")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("OverhangDepth")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("OverhangFrequency")) return Set(0.001f, 0.20f); + if (FieldName == TEXT("RibbingSpacing")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("RibbingDepth")) return Set(0.0f, H * 0.10f); + if (FieldName == TEXT("CliffStrength")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("ScallopStrength")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("ScallopFrequency")) return Set(0.001f, 0.20f); + if (FieldName == TEXT("ArchDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("ArchMinRadius")) return Set(1.0f, H * 0.20f); + if (FieldName == TEXT("ArchMaxRadius")) return Set(1.0f, H * 0.20f); + if (FieldName == TEXT("ColumnDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("ColumnMinRadius")) return Set(1.0f, H * 0.20f); + if (FieldName == TEXT("ColumnMaxRadius")) return Set(1.0f, H * 0.40f); + if (FieldName == TEXT("PitDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("PitMinRadius")) return Set(1.0f, H * 0.20f); + if (FieldName == TEXT("PitMaxRadius")) return Set(1.0f, H * 0.40f); + if (FieldName == TEXT("PitDepth")) return Set(0.0f, HHalf); + if (FieldName == TEXT("ChimneyDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("ChimneyMinRadius")) return Set(1.0f, H * 0.20f); + if (FieldName == TEXT("ChimneyMaxRadius")) return Set(1.0f, H * 0.40f); + if (FieldName == TEXT("ChimneyHeight")) return Set(0.0f, HHalf); + if (FieldName == TEXT("DomeDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("DomeMinRadius")) return Set(1.0f, H * 0.30f); + if (FieldName == TEXT("DomeMaxRadius")) return Set(1.0f, H * 0.50f); + if (FieldName == TEXT("DomeHeightRatio")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("PinchDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("PinchStrength")) return Set(0.0f, H); + if (FieldName == TEXT("PinchLength")) return Set(0.0f, H * 2.0f); + return false; + } + + switch (Archetype) + { + case ECaveGeneratorType::FlatPlain: + case ECaveGeneratorType::CrystalChamber: + if (FieldName == TEXT("FloorRelativeHeight")) return Set(0.0f, 0.95f); + if (FieldName == TEXT("CeilingRelativeHeight")) return Set(0.05f, 1.0f); + if (FieldName == TEXT("FloorRoughness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("FloorRoughnessFrequency")) return Set(0.001f, 0.15f); + if (FieldName == TEXT("CeilingRoughness")) return Set(0.0f, HHalf); + if (FieldName == TEXT("CeilingRoughnessFrequency")) return Set(0.001f, 0.15f); + if (FieldName == TEXT("ColumnDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("ColumnMinRadius")) return Set(1.0f, H * 0.20f); + if (FieldName == TEXT("ColumnMaxRadius")) return Set(1.0f, H * 0.40f); + if (FieldName == TEXT("ColumnSpacing")) return Set(10.0f, H * 1.5f); + if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f); + return false; + + case ECaveGeneratorType::Maze: + if (FieldName == TEXT("CellSize")) return Set(8.0f, H * 2.0f); + if (FieldName == TEXT("CorridorRadius")) return Set(1.0f, HQuarter); + if (FieldName == TEXT("BranchProbability")) return Set(0.05f, 1.0f); + if (FieldName == TEXT("Verticality")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f); + return false; + + case ECaveGeneratorType::SurfaceWorld: + if (FieldName == TEXT("BaseGroundRelative")) return Set(0.05f, 0.90f); + if (FieldName == TEXT("ElevationRange")) return Set(0.0f, H * 2.0f); + if (FieldName == TEXT("ContinentFrequency")) return Set(0.0005f, 0.10f); + if (FieldName == TEXT("MountainStrength")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("MountainFrequency")) return Set(0.0005f, 0.10f); + if (FieldName == TEXT("DetailFrequency")) return Set(0.0005f, 0.20f); + if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("HeightWarpStrength")) return Set(0.0f, H); + if (FieldName == TEXT("HeightWarpFrequency")) return Set(0.0005f, 0.10f); + if (FieldName == TEXT("ReliefFrequency")) return Set(0.0002f, 0.02f); + if (FieldName == TEXT("ReliefStrength")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("ReliefContrast")) return Set(0.25f, 4.0f); + if (FieldName == TEXT("TerraceStrength")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("TerraceHeight")) return Set(1.0f, HHalf); + if (FieldName == TEXT("TerraceHardness")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("LayerLineDepth")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("LayerLineSpacing")) return Set(1.0f, H); + if (FieldName == TEXT("CliffStrength")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("CliffSlopeThreshold")) return Set(0.05f, 2.0f); + if (FieldName == TEXT("CliffSharpness")) return Set(0.0f, 4.0f); + if (FieldName == TEXT("CliffSampleDist")) return Set(0.5f, HQuarter); + if (FieldName == TEXT("OverhangStrength")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("OverhangReach")) return Set(0.0f, HHalf); + if (FieldName == TEXT("OverhangHeight")) return Set(0.0f, HHalf); + if (FieldName == TEXT("OverhangFrequency")) return Set(0.0005f, 0.20f); + if (FieldName == TEXT("OverhangZScale")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("OverhangSlopeThreshold")) return Set(0.05f, 2.0f); + if (FieldName == TEXT("WaterLevelRelative")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("BeachWidth")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("CeilingRelative")) return Set(0.30f, 1.0f); + if (FieldName == TEXT("CeilingRoughness")) return Set(0.0f, HHalf); + if (FieldName == TEXT("CeilingRoughnessFrequency")) return Set(0.0005f, 0.20f); + if (FieldName == TEXT("CeilingUndulation")) return Set(0.0f, HHalf); + if (FieldName == TEXT("CeilingUndulationFrequency")) return Set(0.0005f, 0.10f); + if (FieldName == TEXT("CeilingRidgeStrength")) return Set(0.0f, HHalf); + if (FieldName == TEXT("CeilingRidgeFrequency")) return Set(0.0005f, 0.20f); + if (FieldName == TEXT("CeilingWarpStrength")) return Set(0.0f, H); + if (FieldName == TEXT("CeilingWarpFrequency")) return Set(0.0005f, 0.10f); + if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f); + return false; + + case ECaveGeneratorType::VerticalShafts: + if (FieldName == TEXT("ShaftSpacing")) return Set(10.0f, H * 2.0f); + if (FieldName == TEXT("ShaftDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("ShaftMinRadius")) return Set(1.0f, H * 0.20f); + if (FieldName == TEXT("ShaftMaxRadius")) return Set(1.0f, H * 0.40f); + if (FieldName == TEXT("CrossConnectChance")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("ConnectorRadius")) return Set(1.0f, H * 0.20f); + if (FieldName == TEXT("LedgeSpacing")) return Set(0.0f, H); + if (FieldName == TEXT("LedgeDepth")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f); + return false; + + case ECaveGeneratorType::FloatingIslands: + if (FieldName == TEXT("IslandSpacing")) return Set(20.0f, H * 3.0f); + if (FieldName == TEXT("IslandDensity")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("IslandMinRadius")) return Set(2.0f, H * 0.30f); + if (FieldName == TEXT("IslandMaxRadius")) return Set(2.0f, H * 0.75f); + if (FieldName == TEXT("ThicknessRatio")) return Set(0.10f, 1.5f); + if (FieldName == TEXT("VerticalJitter")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("TopFlatten")) return Set(0.0f, 1.0f); + if (FieldName == TEXT("SurfaceRoughness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("SDFBlendRadius")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("BoundarySealThickness")) return Set(0.0f, HQuarter); + if (FieldName == TEXT("BaseDensity")) return Set(1.0f, 32.0f); + return false; + + default: + return false; + } + } + + uint32 VF_CorpusFreeRollSeed(ECaveGeneratorType Archetype, int32 Seed, int32 Index) + { + uint32 Value = 0xC0A5FEE1u ^ (static_cast(static_cast(Archetype)) * 0x9E3779B9u); + Value = VF_Avalanche(Value ^ static_cast(Seed)); + return VF_Avalanche(Value ^ (static_cast(Index) + 0x85EBCA6Bu)); + } + + float VF_RollCorpusFreeFloat(FRandomStream& Rng, const FCorpusFreeRange& Range) + { + return FMath::Lerp(Range.Min, Range.Max, Rng.FRand()); + } + + bool VF_IsConstraintDerivedTunnelField(const TCHAR* FieldName) + { + return FCString::Strcmp(FieldName, TEXT("BoundarySealThickness")) == 0 + || FCString::Strcmp(FieldName, TEXT("WormStrength")) == 0 + || FCString::Strcmp(FieldName, TEXT("WormNetworkRange")) == 0 + || FCString::Strcmp(FieldName, TEXT("MinRoomRadius")) == 0 + || FCString::Strcmp(FieldName, TEXT("MaxRoomRadius")) == 0 + || FCString::Strcmp(FieldName, TEXT("RoomFloorCutMax")) == 0 + || FCString::Strcmp(FieldName, TEXT("OriginRoomRadius")) == 0 + || FCString::Strcmp(FieldName, TEXT("TunnelMinRadius")) == 0 + || FCString::Strcmp(FieldName, TEXT("TunnelMaxRadius")) == 0 + || FCString::Strcmp(FieldName, TEXT("MaxTunnelLength")) == 0 + || FCString::Strcmp(FieldName, TEXT("TunnelWarpStrength")) == 0 + || FCString::Strcmp(FieldName, TEXT("SDFBlendRadius")) == 0 + || FCString::Strcmp(FieldName, TEXT("CaveWarpStrength")) == 0 + || FCString::Strcmp(FieldName, TEXT("SurfaceRoughness")) == 0 + || FCString::Strcmp(FieldName, TEXT("FloorBias")) == 0 + || FCString::Strcmp(FieldName, TEXT("TerraceStepHeight")) == 0 + || FCString::Strcmp(FieldName, TEXT("LayerLineSpacing")) == 0 + || FCString::Strcmp(FieldName, TEXT("LayerLineDepth")) == 0 + || FCString::Strcmp(FieldName, TEXT("OverhangDepth")) == 0 + || FCString::Strcmp(FieldName, TEXT("ArchMinRadius")) == 0 + || FCString::Strcmp(FieldName, TEXT("ArchMaxRadius")) == 0 + || FCString::Strcmp(FieldName, TEXT("DomeMinRadius")) == 0 + || FCString::Strcmp(FieldName, TEXT("DomeMaxRadius")) == 0 + || FCString::Strcmp(FieldName, TEXT("DomeHeightRatio")) == 0 + || FCString::Strcmp(FieldName, TEXT("PinchLength")) == 0; + } + + bool VF_SampleTunnelContinuousField(FStrateGenerationParams& Params, + const TCHAR* FieldName, + FRandomStream& ScalarRng, + float StrateHeightInVoxels, + bool bConstraintSampled) + { + const FString Name(FieldName); + if (VF_IsExcludedForArchetype(ECaveGeneratorType::TunnelNetwork, Name)) + { + return true; + } + if (bConstraintSampled && VF_IsConstraintDerivedTunnelField(FieldName)) + { + return true; + } + + FCorpusFreeRange Range; + if (!VF_GetCorpusFreeRange(ECaveGeneratorType::TunnelNetwork, Name, + StrateHeightInVoxels, Range)) + { + return false; + } + return VF_WriteFStrateField(Params, Name, VF_RollCorpusFreeFloat(ScalarRng, Range)); + } + + bool VF_SampleTunnelDiscreteField(FStrateGenerationParams& Params, + const TCHAR* FieldName, + FRandomStream& CategoryRng) + { + if (FCString::Strcmp(FieldName, TEXT("OriginRoomMaxConnections")) == 0) + { + Params.OriginRoomMaxConnections = CategoryRng.RandRange(0, 12); + return true; + } + if (FCString::Strcmp(FieldName, TEXT("bTunnelsFlowTowardOrigin")) == 0) + { + Params.bTunnelsFlowTowardOrigin = CategoryRng.FRand() < 0.5f; + return true; + } + if (FCString::Strcmp(FieldName, TEXT("RoughnessNoiseType")) == 0) + { + Params.RoughnessNoiseType = static_cast(CategoryRng.RandRange(0, 3)); + return true; + } + return false; + } + + bool VF_SampleTunnelFields(FStrateGenerationParams& Params, + FRandomStream& ScalarRng, + FRandomStream& CategoryRng, + float StrateHeightInVoxels, + bool bConstraintSampled) + { + bool bValid = true; +#define VF_SAMPLE_CORPUS_FREE_LERP(Name) \ + { \ + const bool bFieldValid = VF_SampleTunnelContinuousField( \ + Params, TEXT(#Name), ScalarRng, StrateHeightInVoxels, bConstraintSampled); \ + bValid = bFieldValid && bValid; \ + } +#define VF_SAMPLE_CORPUS_FREE_SNAP(Name) \ + { \ + const bool bFieldValid = VF_SampleTunnelDiscreteField(Params, TEXT(#Name), CategoryRng); \ + bValid = bFieldValid && bValid; \ + } + VF_STRATE_PARAM_FIELDS(VF_SAMPLE_CORPUS_FREE_LERP, VF_SAMPLE_CORPUS_FREE_SNAP) +#undef VF_SAMPLE_CORPUS_FREE_LERP +#undef VF_SAMPLE_CORPUS_FREE_SNAP + return bValid; + } + + bool VF_SampleReflectedFields(ECaveGeneratorType Archetype, + void* Memory, + FRandomStream& ScalarRng, + FRandomStream& CategoryRng, + float StrateHeightInVoxels) + { + UStruct* Struct = VF_GetParamStruct(Archetype); + if (Struct == nullptr || Memory == nullptr) + { + return false; + } + + TMap Properties; + VF_CollectProperties(Struct, Properties); + TArray FieldNames; + FieldNames.Reserve(Properties.Num()); + for (const TPair& Pair : Properties) + { + FieldNames.Add(Pair.Key); + } + FieldNames.Sort([](const FString& A, const FString& B) { return A < B; }); + + bool bValid = true; + for (const FString& FieldName : FieldNames) + { + FProperty* const* PropertyPtr = Properties.Find(FieldName); + FProperty* Property = PropertyPtr != nullptr ? *PropertyPtr : nullptr; + if (Property == nullptr || VF_IsRuntimeField(FieldName) + || VF_IsExcludedForArchetype(Archetype, FieldName)) + { + continue; + } + if (!VF_IsScalarProperty(Property)) + { + bValid = false; + continue; + } + + if (VF_PropertyKind(Property) == EVoxelStrateFieldKind::Boolean) + { + bValid = VF_WritePropertyValue(Property, Memory, + CategoryRng.FRand() < 0.5f ? 0.0 : 1.0) && bValid; + continue; + } + if (VF_PropertyKind(Property) == EVoxelStrateFieldKind::Enum) + { + // No current non-tunnel family has an enum. Keep the failure explicit if one is + // added without a legal-value declaration instead of silently writing a default. + bValid = false; + continue; + } + + FCorpusFreeRange Range; + if (!VF_GetCorpusFreeRange(Archetype, FieldName, + StrateHeightInVoxels, Range)) + { + bValid = false; + continue; + } + bValid = VF_WritePropertyValue(Property, Memory, + VF_RollCorpusFreeFloat(ScalarRng, Range)) && bValid; + } + return bValid; + } + + float VF_CorpusFreeU(FRandomStream& Rng, float Min, float Max) + { + return FMath::Lerp(Min, Max, Rng.FRand()); + } + + void VF_ClampCorpusFreeField(void* Memory, ECaveGeneratorType Archetype, + const TCHAR* FieldName, float StrateHeightInVoxels) + { + if (Memory == nullptr || VF_IsRuntimeField(FieldName) + || VF_IsExcludedForArchetype(Archetype, FString(FieldName))) + { + return; + } + FCorpusFreeRange Range; + if (!VF_GetCorpusFreeRange(Archetype, FString(FieldName), StrateHeightInVoxels, Range)) + { + return; + } + double Value = 0.0; + if (VF_ReadNamedField(Memory, Archetype, FString(FieldName), Value)) + { + Value = FMath::Clamp(Value, static_cast(Range.Min), static_cast(Range.Max)); + VF_WriteNamedField(Memory, Archetype, FString(FieldName), Value); + } + } + + void VF_ClampCorpusFreeFields(FVoxelStrateArchetypeParams& Params, + ECaveGeneratorType Archetype, + float StrateHeightInVoxels) + { + void* Memory = VF_GetParamMemory(Params, Archetype); + if (VF_IsTunnelArchetype(Archetype)) + { +#define VF_CLAMP_CORPUS_FREE_LERP(Name) \ + VF_ClampCorpusFreeField(Memory, Archetype, TEXT(#Name), StrateHeightInVoxels); +#define VF_CLAMP_CORPUS_FREE_SNAP(Name) \ + do { } while (false); + VF_STRATE_PARAM_FIELDS(VF_CLAMP_CORPUS_FREE_LERP, VF_CLAMP_CORPUS_FREE_SNAP) +#undef VF_CLAMP_CORPUS_FREE_LERP +#undef VF_CLAMP_CORPUS_FREE_SNAP + return; + } + + UStruct* Struct = VF_GetParamStruct(Archetype); + if (Memory == nullptr || Struct == nullptr) + { + return; + } + TMap Properties; + VF_CollectProperties(Struct, Properties); + for (const TPair& Pair : Properties) + { + VF_ClampCorpusFreeField(Memory, Archetype, *Pair.Key, StrateHeightInVoxels); + } + } + + bool VF_FailCorpusFreeConstraint(FString& OutViolation, const TCHAR* Relation, + float Left, float Right) + { + OutViolation = FString::Printf(TEXT("%s (%.6g vs %.6g)"), Relation, Left, Right); + return false; + } + + bool VF_ApplyConstraintTunnel(FStrateGenerationParams& P, + FRandomStream& Rng, + float StrateHeightInVoxels) + { + const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); + const float SpacingMin = FMath::Min(48.0f, H * 0.35f); + const float SpacingMax = FMath::Max(SpacingMin + 8.0f, FMath::Min(160.0f, H * 0.94f)); + const float Coverage = VF_CorpusFreeU(Rng, 0.08f, 0.18f); + + P.BoundarySealThickness = VF_CorpusFreeU( + Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); + P.RoomSpacing = VF_CorpusFreeU(Rng, SpacingMin, SpacingMax); + P.RoomDensity = VF_CorpusFreeU(Rng, 0.25f, 0.75f); + const float RadiusRatio = FMath::Sqrt( + Coverage / (PI * FMath::Max(P.RoomDensity, 0.001f))); + P.MaxRoomRadius = P.RoomSpacing * RadiusRatio; + P.MinRoomRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.35f, 0.75f); + P.RoomHeightRatio = VF_CorpusFreeU(Rng, 0.25f, 0.85f); + P.OriginRoomRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.60f, 0.95f); + + P.RoomFloorCutMin = VF_CorpusFreeU(Rng, 0.35f, 0.85f); + P.RoomFloorCutMax = P.RoomFloorCutMin + VF_CorpusFreeU( + Rng, 0.0f, 1.0f - P.RoomFloorCutMin); + P.FloorReliefStrength = VF_CorpusFreeU( + Rng, 0.0f, + FMath::Min(12.0f, P.MaxRoomRadius * P.RoomHeightRatio * 0.25f)); + + const float TunnelMinCap = FMath::Max(1.5f, P.MinRoomRadius * 0.45f); + P.TunnelMinRadius = FMath::Max( + 1.5f, P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.25f, 0.45f)); + P.TunnelMinRadius = FMath::Min(P.TunnelMinRadius, TunnelMinCap); + P.TunnelMaxRadius = FMath::Max( + P.TunnelMinRadius, + P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.55f, 0.90f)); + + // Adjacent cells can differ in both X and Y. The placement jitter is [0.15, 0.85] + // cell, so the worst local diagonal separation is 1.7 * S * sqrt(2), not the + // axis-aligned 1.7 * S case. + const float HorizontalReach = 1.7f * P.RoomSpacing * FMath::Sqrt(2.0f); + const float VerticalReach = 2.0f * P.MaxRoomRadius * P.RoomHeightRatio; + const float NeighborReach = FMath::Sqrt( + HorizontalReach * HorizontalReach + VerticalReach * VerticalReach); + P.MaxTunnelLength = NeighborReach * VF_CorpusFreeU(Rng, 1.0f, 1.40f); + P.TunnelWarpStrength = P.MaxTunnelLength * VF_CorpusFreeU(Rng, 0.02f, 0.12f); + P.WormStrength = P.BaseDensity * VF_CorpusFreeU(Rng, 1.10f, 1.80f); + P.WormNetworkRange = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.20f, 0.60f); + P.CaveWarpStrength = P.RoomSpacing * VF_CorpusFreeU(Rng, 0.02f, 0.12f); + + P.SDFBlendRadius = VF_CorpusFreeU( + Rng, 0.5f, FMath::Max(0.5f, FMath::Min(4.0f, P.TunnelMinRadius * 0.35f))); + const float RoughnessMax = FMath::Max( + 0.0f, (P.TunnelMinRadius - P.SDFBlendRadius - 0.25f) / VOXEL_NOISE_SCALE); + P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessMax * 0.70f); + P.FloorBias = VF_CorpusFreeU( + Rng, 0.0f, P.MaxRoomRadius * P.RoomHeightRatio * 0.25f); + + // Internal terrain-operation transport is kept within the same room/strate envelope. + P.TerraceStepHeight = VF_CorpusFreeU( + Rng, 0.0f, FMath::Min(16.0f, P.MaxRoomRadius * P.RoomHeightRatio * 0.35f)); + P.LayerLineSpacing = VF_CorpusFreeU( + Rng, 4.0f, FMath::Max(4.0f, FMath::Min(H * 0.25f, P.MaxRoomRadius))); + P.LayerLineDepth = VF_CorpusFreeU(Rng, 0.0f, P.BaseDensity * 0.10f); + P.OverhangDepth = VF_CorpusFreeU( + Rng, 0.0f, FMath::Min(P.MaxRoomRadius * 0.25f, H * 0.20f)); + P.ArchMinRadius = VF_CorpusFreeU( + Rng, 1.0f, FMath::Max(1.0f, P.TunnelMinRadius)); + P.ArchMaxRadius = FMath::Max( + P.ArchMinRadius, + P.MinRoomRadius * VF_CorpusFreeU(Rng, 0.20f, 0.45f)); + P.DomeMaxRadius = P.MaxRoomRadius * VF_CorpusFreeU(Rng, 0.30f, 0.60f); + P.DomeMinRadius = P.DomeMaxRadius * VF_CorpusFreeU(Rng, 0.35f, 0.75f); + P.DomeHeightRatio = FMath::Min( + 1.0f, + (P.MaxRoomRadius * P.RoomHeightRatio) + / FMath::Max(P.DomeMaxRadius, 0.001f) + * VF_CorpusFreeU(Rng, 0.35f, 0.90f)); + P.PinchLength = P.MaxTunnelLength * VF_CorpusFreeU(Rng, 0.10f, 0.40f); + return true; + } + + bool VF_ApplyConstraintSlab(FSlabGenerationParams& P, + FRandomStream& Rng, + float StrateHeightInVoxels) + { + const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); + P.BoundarySealThickness = VF_CorpusFreeU( + Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); + P.FloorRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.04f); + P.CeilingRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.08f); + + const float FloorNoise = VOXEL_NOISE_SCALE * P.FloorRoughness; + const float CeilingNoise = VOXEL_NOISE_SCALE * P.CeilingRoughness; + const float FloorClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.16f); + const float CeilingClearance = VF_CorpusFreeU(Rng, H * 0.08f, H * 0.16f); + P.FloorRelativeHeight = (P.BoundarySealThickness + FloorNoise + FloorClearance) / H; + P.CeilingRelativeHeight = 1.0f + - (P.BoundarySealThickness + CeilingNoise + CeilingClearance) / H; + + P.ColumnSpacing = VF_CorpusFreeU( + Rng, FMath::Max(24.0f, H * 0.30f), FMath::Max(32.0f, H * 0.75f)); + P.ColumnMinRadius = VF_CorpusFreeU(Rng, 1.0f, P.ColumnSpacing * 0.08f); + P.ColumnMaxRadius = FMath::Max( + P.ColumnMinRadius, + FMath::Min(P.ColumnSpacing * 0.40f, H * 0.38f)); + return true; + } + + bool VF_ApplyConstraintMaze(FMazeGenerationParams& P, + FRandomStream& Rng, + float StrateHeightInVoxels) + { + const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); + P.CellSize = VF_CorpusFreeU( + Rng, FMath::Max(24.0f, H * 0.25f), FMath::Min(96.0f, H * 0.75f)); + P.CorridorRadius = P.CellSize * VF_CorpusFreeU(Rng, 0.12f, 0.24f); + const float RoughnessCap = FMath::Max( + 0.0f, (P.CorridorRadius + 2.0f) / (VOXEL_NOISE_SCALE * 1.25f)); + P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessCap * 0.70f); + P.BoundarySealThickness = VF_CorpusFreeU( + Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); + return true; + } + + bool VF_ApplyConstraintSurface(FSurfaceGenerationParams& P, + FRandomStream& Rng, + float StrateHeightInVoxels) + { + const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); + P.BoundarySealThickness = VF_CorpusFreeU( + Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); + P.ElevationRange = VF_CorpusFreeU(Rng, H * 0.06f, H * 0.16f); + P.MountainStrength = VF_CorpusFreeU(Rng, 0.20f, 0.80f); + P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.02f); + + const float GroundLowerDeviation = P.ElevationRange * 0.50f + + P.SurfaceRoughness * VOXEL_NOISE_SCALE; + const float GroundUpperDeviation = P.ElevationRange * (0.50f + P.MountainStrength) + + P.SurfaceRoughness * VOXEL_NOISE_SCALE; + const float GroundClearance = VF_CorpusFreeU(Rng, H * 0.04f, H * 0.08f); + P.CeilingRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.02f); + P.CeilingUndulation = VF_CorpusFreeU(Rng, 0.0f, H * 0.02f); + P.CeilingRidgeStrength = VF_CorpusFreeU(Rng, 0.0f, H * 0.02f); + + P.TerraceStrength = VF_CorpusFreeU(Rng, 0.0f, 0.50f); + P.TerraceHeight = VF_CorpusFreeU( + Rng, 1.0f, FMath::Max(1.0f, FMath::Min(10.0f, H * 0.08f))); + P.LayerLineDepth = VF_CorpusFreeU(Rng, 0.0f, FMath::Min(2.0f, H * 0.02f)); + P.LayerLineSpacing = VF_CorpusFreeU( + Rng, FMath::Min(4.0f, H * 0.01f), FMath::Min(32.0f, H * 0.25f)); + P.OverhangStrength = VF_CorpusFreeU(Rng, 0.0f, 0.50f); + P.OverhangReach = VF_CorpusFreeU( + Rng, FMath::Min(4.0f, H * 0.01f), FMath::Min(20.0f, H * 0.15f)); + P.OverhangHeight = VF_CorpusFreeU( + Rng, FMath::Min(2.0f, H * 0.02f), FMath::Min(10.0f, H * 0.05f)); + P.BeachWidth = VF_CorpusFreeU(Rng, 0.0f, FMath::Min(16.0f, H * 0.10f)); + + const float CapClearance = VF_CorpusFreeU(Rng, H * 0.04f, H * 0.08f); + const float CapLowering = P.CeilingRoughness * VOXEL_NOISE_SCALE + + P.CeilingUndulation * VOXEL_NOISE_SCALE + P.CeilingRidgeStrength; + // Cliff is a height-space operation, not a categorical switch. Its equation pushes the + // structural height away from the four-sample local mean by + // CliffStrength * CliffSharpness. The structural field's total possible spread is + // bounded by ElevationRange * (1 + MountainStrength) + 2 * noiseScale * roughness. + // Reserve room for that bound, then reduce the requested strength if the remaining + // bottom/cap budget cannot contain it. This is a derived relation, not the final envelope + // clamp below; the other height operations receive the same budget treatment. + const float CliffSpread = P.ElevationRange * (1.0f + P.MountainStrength) + + 2.0f * VOXEL_NOISE_SCALE * P.SurfaceRoughness; + const float CliffCoefficient = FMath::Max(P.CliffSharpness, 0.0f) * CliffSpread; + const float OtherGroundFeatureBudget = FMath::Max(P.TerraceHeight, 0.0f) + + FMath::Abs(P.LayerLineDepth) + FMath::Max(P.BeachWidth, 0.0f); + const float RequiredGap = CapClearance + FMath::Max(P.OverhangHeight, 0.0f) + + 2.0f; + const float GroundWithoutFeatures = P.BoundarySealThickness + GroundClearance + + GroundLowerDeviation + GroundUpperDeviation; + constexpr float FitMargin = 1.0f; + const float FeatureCapacityFromBase = H * 0.90f + - (P.BoundarySealThickness + GroundClearance + GroundLowerDeviation) + - OtherGroundFeatureBudget - FitMargin; + const float FeatureCapacityFromCap = 0.5f * ( + H - P.BoundarySealThickness - RequiredGap - GroundWithoutFeatures - FitMargin) + - OtherGroundFeatureBudget; + const float MaxCliffBudget = FMath::Max(0.0f, + FMath::Min(FeatureCapacityFromBase, FeatureCapacityFromCap)); + if (CliffCoefficient > 0.0f) + { + P.CliffStrength = FMath::Min( + FMath::Max(P.CliffStrength, 0.0f), MaxCliffBudget / CliffCoefficient); + } + else + { + P.CliffStrength = 0.0f; + } + + const float CliffBudget = FMath::Max(P.CliffStrength, 0.0f) * CliffCoefficient; + const float GroundFeatureBudget = OtherGroundFeatureBudget + CliffBudget; + P.BaseGroundRelative = (P.BoundarySealThickness + GroundClearance + + GroundLowerDeviation + GroundFeatureBudget) / H; + const float GroundUpper = P.BoundarySealThickness + GroundClearance + + GroundLowerDeviation + GroundUpperDeviation + 2.0f * GroundFeatureBudget; + // Compute the cap BASE from the worst terrain height. The generator subtracts + // CapLowering afterwards (VoxelGenerator.cpp:2627-2652), so adding it here makes the + // final cap, rather than the unlowered base line, the dependent quantity. + P.CeilingRelative = (GroundUpper + RequiredGap + CapLowering) / H; + + return true; + } + + bool VF_ApplyConstraintVertical(FVerticalShaftParams& P, + FRandomStream& Rng, + float StrateHeightInVoxels) + { + const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); + P.BoundarySealThickness = VF_CorpusFreeU( + Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); + P.ShaftSpacing = VF_CorpusFreeU( + Rng, FMath::Max(32.0f, H * 0.30f), + FMath::Max(32.0f, FMath::Min(112.0f, H * 0.90f))); + P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, H * 0.04f); + const float RoughnessReach = P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f; + P.ShaftMinRadius = RoughnessReach + 0.5f + VF_CorpusFreeU(Rng, 1.0f, 4.0f); + P.ShaftMaxRadius = FMath::Max( + P.ShaftMinRadius + 0.5f, + FMath::Min(P.ShaftSpacing * 0.40f, P.ShaftMinRadius + 16.0f)); + P.ConnectorRadius = RoughnessReach + 1.0f + VF_CorpusFreeU(Rng, 0.0f, 3.0f); + P.LedgeDepth = VF_CorpusFreeU( + Rng, 0.5f, FMath::Max(0.5f, FMath::Min(4.0f, P.ShaftSpacing * 0.08f))); + const float FloorClearance = FMath::Max( + 1.0f, P.SurfaceRoughness * VOXEL_NOISE_SCALE + 1.0f); + P.LedgeSpacing = 2.0f * (P.LedgeDepth + FloorClearance) + + VF_CorpusFreeU( + Rng, FMath::Min(4.0f, H * 0.02f), FMath::Min(16.0f, H * 0.10f)); + return true; + } + + bool VF_ApplyConstraintFloating(FFloatingIslandParams& P, + FRandomStream& Rng, + float StrateHeightInVoxels) + { + const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); + P.BoundarySealThickness = VF_CorpusFreeU( + Rng, FMath::Min(3.0f, H * 0.02f), FMath::Min(8.0f, H * 0.08f)); + P.IslandSpacing = VF_CorpusFreeU( + Rng, FMath::Max(40.0f, H * 0.30f), + FMath::Max(40.0f, FMath::Min(120.0f, H * 0.75f))); + P.ThicknessRatio = VF_CorpusFreeU(Rng, 0.30f, 0.90f); + const float MaxByHeight = (H * 0.50f - P.BoundarySealThickness) + / FMath::Max(P.ThicknessRatio, 0.25f); + const float MaxRadius = FMath::Max( + 2.0f, FMath::Min(P.IslandSpacing * 0.45f, MaxByHeight * 0.85f)); + P.IslandMaxRadius = MaxRadius; + P.IslandMinRadius = P.IslandMaxRadius * VF_CorpusFreeU(Rng, 0.45f, 0.80f); + P.SDFBlendRadius = VF_CorpusFreeU( + Rng, 0.5f, FMath::Min(6.0f, P.IslandMinRadius * 0.20f)); + const float RoughnessMax = FMath::Max( + 0.0f, (P.IslandMinRadius - P.SDFBlendRadius) / VOXEL_NOISE_SCALE); + P.SurfaceRoughness = VF_CorpusFreeU(Rng, 0.0f, RoughnessMax * 0.70f); + return true; + } + + bool VF_ApplyCorpusFreeConstraints(ECaveGeneratorType Archetype, + FVoxelStrateArchetypeParams& Params, + FRandomStream& Rng, + float StrateHeightInVoxels) + { + switch (Archetype) + { + case ECaveGeneratorType::TunnelNetwork: + case ECaveGeneratorType::Underwater: + return VF_ApplyConstraintTunnel(Params.TunnelNetworkParams, Rng, + StrateHeightInVoxels); + case ECaveGeneratorType::FlatPlain: + case ECaveGeneratorType::CrystalChamber: + return VF_ApplyConstraintSlab(Params.SlabParams, Rng, + StrateHeightInVoxels); + case ECaveGeneratorType::Maze: + return VF_ApplyConstraintMaze(Params.MazeParams, Rng, + StrateHeightInVoxels); + case ECaveGeneratorType::SurfaceWorld: + return VF_ApplyConstraintSurface(Params.SurfaceParams, Rng, + StrateHeightInVoxels); + case ECaveGeneratorType::VerticalShafts: + return VF_ApplyConstraintVertical(Params.VerticalShaftParams, Rng, + StrateHeightInVoxels); + case ECaveGeneratorType::FloatingIslands: + return VF_ApplyConstraintFloating(Params.FloatingIslandParams, Rng, + StrateHeightInVoxels); + default: + return false; + } + } + + FVoxelStrateRollInfo VF_RollCorpusFreeInternal( + ECaveGeneratorType Archetype, int32 Seed, int32 Index, + EVoxelStrateCorpusFreeSamplingMode Mode, float StrateHeightInVoxels) + { + FVoxelStrateRollInfo Result; + Result.Archetype = Archetype; + if (!VF_IsSupportedArchetype(Archetype)) + { + Result.FailureReason = TEXT("The requested archetype is not supported."); + return Result; + } + if (!FMath::IsFinite(StrateHeightInVoxels) || StrateHeightInVoxels <= 0.0f) + { + Result.FailureReason = TEXT("StrateHeightInVoxels must be finite and positive."); + return Result; + } + + const uint32 BaseSeed = VF_CorpusFreeRollSeed(Archetype, Seed, Index); + FRandomStream ScalarRng(static_cast(VF_Avalanche(BaseSeed ^ 0x51A1A1A1u))); + // Keep categorical choices on a mode-independent stream. The comparison then changes + // only scalar coupling, never the bool/enum draw policy. + FRandomStream CategoryRng(static_cast(VF_Avalanche(BaseSeed ^ 0xC47E6070u))); + FVoxelStrateArchetypeParams Params; + const bool bConstraintSampled = + Mode == EVoxelStrateCorpusFreeSamplingMode::ConstraintSampled; + + bool bValid = false; + if (VF_IsTunnelArchetype(Archetype)) + { + bValid = VF_SampleTunnelFields(Params.TunnelNetworkParams, ScalarRng, + CategoryRng, StrateHeightInVoxels, + bConstraintSampled); + } + else + { + bValid = VF_SampleReflectedFields(Archetype, VF_GetParamMemory(Params, Archetype), + ScalarRng, CategoryRng, StrateHeightInVoxels); + } + if (bValid && bConstraintSampled) + { + bValid = VF_ApplyCorpusFreeConstraints(Archetype, Params, ScalarRng, + StrateHeightInVoxels); + } + if (!bValid) + { + Result.FailureReason = TEXT("Corpus-free field schema has no declared roll range."); + return Result; + } + + // Last safety net only: all dependent values above are derived before this envelope + // clamp, and no corpus/default value is consulted. + VF_ClampCorpusFreeFields(Params, Archetype, StrateHeightInVoxels); + VF_ResetExcludedFields(Params, Archetype); + Result.ArchetypeParams = Params; + Result.Params = Params.TunnelNetworkParams; + Result.bValid = true; + return Result; + } + uint32 VF_RollSeed(uint32 CorpusHash, int32 Seed, int32 Index) { uint32 Value = CorpusHash ^ 0x9e3779b9U; @@ -2194,6 +3005,360 @@ FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype( return Result; } +FVoxelStrateRollInfo VF_RollStrateParamsCorpusFree( + ECaveGeneratorType Archetype, int32 Seed, int32 Index, + EVoxelStrateCorpusFreeSamplingMode Mode, float StrateHeightInVoxels) +{ + const FVoxelStrateRollInfo Result = VF_RollCorpusFreeInternal( + Archetype, Seed, Index, Mode, StrateHeightInVoxels); + +#if DO_CHECK + if (Result.bValid) + { + const FVoxelStrateRollInfo Repeat = VF_RollCorpusFreeInternal( + Archetype, Seed, Index, Mode, StrateHeightInVoxels); + checkf(Repeat.bValid && Repeat.Archetype == Result.Archetype + && VF_AreStrateArchetypeParamsBitIdentical( + Repeat.ArchetypeParams, Result.ArchetypeParams, Archetype), + TEXT("VF_RollStrateParamsCorpusFree lost determinism.")); + } +#endif + return Result; +} + +bool VF_ValidateStrateCorpusFreeConstraints( + ECaveGeneratorType Archetype, + const FVoxelStrateArchetypeParams& Params, + float StrateHeightInVoxels, + FString& OutViolation) +{ + OutViolation.Empty(); + if (!VF_IsSupportedArchetype(Archetype) + || !FMath::IsFinite(StrateHeightInVoxels) || StrateHeightInVoxels <= 0.0f) + { + OutViolation = TEXT("unsupported archetype or non-positive strate height"); + return false; + } + + const float H = FMath::Max(StrateHeightInVoxels, static_cast(CHUNK_SIZE)); + auto Fail = [&OutViolation](const TCHAR* Relation, float Left, float Right) -> bool + { + return VF_FailCorpusFreeConstraint(OutViolation, Relation, Left, Right); + }; + + switch (Archetype) + { + case ECaveGeneratorType::TunnelNetwork: + case ECaveGeneratorType::Underwater: + { + const FStrateGenerationParams& P = Params.TunnelNetworkParams; + if (!FMath::IsFinite(P.BaseDensity) || P.BaseDensity <= 0.0f) + { + return Fail(TEXT("BaseDensity must be > 0"), P.BaseDensity, 0.0f); + } + if (!FMath::IsFinite(P.RoomSpacing) || P.RoomSpacing <= 0.0f) + { + return Fail(TEXT("RoomSpacing must be > 0"), P.RoomSpacing, 0.0f); + } + if (!FMath::IsFinite(P.RoomDensity) || P.RoomDensity <= 0.0f + || P.RoomDensity > 1.0f) + { + return Fail(TEXT("RoomDensity must be in (0,1]"), P.RoomDensity, 1.0f); + } + if (!FMath::IsFinite(P.MinRoomRadius) || !FMath::IsFinite(P.MaxRoomRadius) + || P.MinRoomRadius <= 0.0f || P.MaxRoomRadius < P.MinRoomRadius) + { + return Fail(TEXT("room radius interval is ordered and positive"), + P.MinRoomRadius, P.MaxRoomRadius); + } + if (!FMath::IsFinite(P.RoomHeightRatio) || P.RoomHeightRatio <= 0.0f + || P.RoomHeightRatio > 1.0f) + { + return Fail(TEXT("RoomHeightRatio must be in (0,1]"), P.RoomHeightRatio, 1.0f); + } + if (P.RoomFloorCutMin < 0.0f || P.RoomFloorCutMax < P.RoomFloorCutMin + || P.RoomFloorCutMax > 1.0f) + { + return Fail(TEXT("room floor-cut interval is ordered in [0,1]"), + P.RoomFloorCutMin, P.RoomFloorCutMax); + } + if (P.TunnelMinRadius <= 0.0f || P.TunnelMaxRadius < P.TunnelMinRadius) + { + return Fail(TEXT("tunnel radius interval is ordered and positive"), + P.TunnelMinRadius, P.TunnelMaxRadius); + } + if (P.TunnelMaxRadius > P.MinRoomRadius) + { + return Fail(TEXT("TunnelMaxRadius must fit through the smallest room"), + P.TunnelMaxRadius, P.MinRoomRadius); + } + const float RoomCoverage = PI * P.MaxRoomRadius * P.MaxRoomRadius + * P.RoomDensity / (P.RoomSpacing * P.RoomSpacing); + if (RoomCoverage < 0.02f || RoomCoverage > 0.35f) + { + return Fail(TEXT("room area coverage must stay in [0.02,0.35]"), + RoomCoverage, 0.35f); + } + if (P.MaxRoomRadius > P.RoomSpacing * 0.50f) + { + return Fail(TEXT("MaxRoomRadius must be <= half the room-cell spacing"), + P.MaxRoomRadius, P.RoomSpacing * 0.50f); + } + const float LargestRoomRadius = FMath::Max(P.MaxRoomRadius, P.OriginRoomRadius); + const float RoomZBuffer = LargestRoomRadius * P.RoomHeightRatio; + if (P.BoundarySealThickness < 0.0f + || P.BoundarySealThickness + RoomZBuffer >= H * 0.50f) + { + return Fail(TEXT("seal + tallest room half-height must fit in half the strate"), + P.BoundarySealThickness + RoomZBuffer, H * 0.50f); + } + const float HorizontalReach = 1.7f * P.RoomSpacing * FMath::Sqrt(2.0f); + const float VerticalReach = 2.0f * P.MaxRoomRadius * P.RoomHeightRatio; + const float RequiredTunnelReach = FMath::Sqrt( + HorizontalReach * HorizontalReach + VerticalReach * VerticalReach); + if (P.MaxTunnelLength < RequiredTunnelReach) + { + return Fail(TEXT("MaxTunnelLength must reach a jittered neighbouring room"), + P.MaxTunnelLength, RequiredTunnelReach); + } + if (P.WormStrength <= P.BaseDensity) + { + return Fail(TEXT("WormStrength must overcome BaseDensity"), + P.WormStrength, P.BaseDensity); + } + if (P.DomeMaxRadius > P.MaxRoomRadius) + { + return Fail(TEXT("DomeMaxRadius must fit inside MaxRoomRadius"), + P.DomeMaxRadius, P.MaxRoomRadius); + } + if (P.DomeHeightRatio * P.DomeMaxRadius + > P.RoomHeightRatio * P.MaxRoomRadius) + { + return Fail(TEXT("dome height must fit the room vertical radius"), + P.DomeHeightRatio * P.DomeMaxRadius, + P.RoomHeightRatio * P.MaxRoomRadius); + } + return true; + } + + case ECaveGeneratorType::FlatPlain: + case ECaveGeneratorType::CrystalChamber: + { + const FSlabGenerationParams& P = Params.SlabParams; + const float FloorWorst = H * P.FloorRelativeHeight + + VOXEL_NOISE_SCALE * FMath::Max(P.FloorRoughness, 0.0f); + const float CeilingWorst = H * P.CeilingRelativeHeight + - VOXEL_NOISE_SCALE * FMath::Max(P.CeilingRoughness, 0.0f); + if (P.FloorRelativeHeight < 0.0f || P.CeilingRelativeHeight > 1.0f + || P.FloorRelativeHeight >= P.CeilingRelativeHeight) + { + return Fail(TEXT("slab floor must be below slab ceiling"), + P.FloorRelativeHeight, P.CeilingRelativeHeight); + } + if (P.BoundarySealThickness < 0.0f + || FloorWorst <= P.BoundarySealThickness + || CeilingWorst >= H - P.BoundarySealThickness) + { + return Fail(TEXT("slab roughness must stay inside the seal bands"), + FloorWorst, CeilingWorst); + } + if (FloorWorst + 2.0f >= CeilingWorst) + { + return Fail(TEXT("slab worst-case noise must leave two voxels of void"), + FloorWorst + 2.0f, CeilingWorst); + } + if (P.ColumnMinRadius <= 0.0f || P.ColumnMaxRadius < P.ColumnMinRadius) + { + return Fail(TEXT("slab column radius interval is ordered and positive"), + P.ColumnMinRadius, P.ColumnMaxRadius); + } + if (P.ColumnSpacing <= 0.0f || P.ColumnMaxRadius > P.ColumnSpacing * 0.50f) + { + return Fail(TEXT("slab columns must fit within half a column cell"), + P.ColumnMaxRadius, P.ColumnSpacing * 0.50f); + } + if (P.BaseDensity <= 0.0f) + { + return Fail(TEXT("BaseDensity must be > 0"), P.BaseDensity, 0.0f); + } + return true; + } + + case ECaveGeneratorType::Maze: + { + const FMazeGenerationParams& P = Params.MazeParams; + if (P.CellSize <= 0.0f || P.CorridorRadius <= 0.0f) + { + return Fail(TEXT("maze CellSize and CorridorRadius must be > 0"), + P.CellSize, P.CorridorRadius); + } + if (P.CorridorRadius > P.CellSize * 0.25f) + { + return Fail(TEXT("CorridorRadius must leave lattice walls"), + P.CorridorRadius, P.CellSize * 0.25f); + } + if (P.SurfaceRoughness * VOXEL_NOISE_SCALE + >= P.CorridorRadius + 2.0f) + { + return Fail(TEXT("maze roughness must not erase the corridor centreline"), + P.SurfaceRoughness * VOXEL_NOISE_SCALE, + P.CorridorRadius + 2.0f); + } + if (P.BoundarySealThickness < 0.0f + || P.BoundarySealThickness * 2.0f >= H) + { + return Fail(TEXT("maze seals must leave an interior"), + P.BoundarySealThickness * 2.0f, H); + } + if (P.BaseDensity <= 0.0f) + { + return Fail(TEXT("BaseDensity must be > 0"), P.BaseDensity, 0.0f); + } + return true; + } + + case ECaveGeneratorType::SurfaceWorld: + { + const FSurfaceGenerationParams& P = Params.SurfaceParams; + const float RoughBound = VOXEL_NOISE_SCALE * FMath::Max(P.SurfaceRoughness, 0.0f); + const float CliffSpread = P.ElevationRange + * (1.0f + FMath::Max(P.MountainStrength, 0.0f)) + + 2.0f * RoughBound; + const float CliffBudget = FMath::Max(P.CliffStrength, 0.0f) + * FMath::Max(P.CliffSharpness, 0.0f) * FMath::Max(CliffSpread, 0.0f); + const float GroundFeatureBudget = CliffBudget + + FMath::Max(P.TerraceHeight, 0.0f) + + FMath::Abs(P.LayerLineDepth) + + FMath::Max(P.BeachWidth, 0.0f); + const float GroundLower = H * P.BaseGroundRelative + - P.ElevationRange * 0.50f - RoughBound - GroundFeatureBudget; + const float GroundUpper = H * P.BaseGroundRelative + + P.ElevationRange * (0.50f + FMath::Max(P.MountainStrength, 0.0f)) + + RoughBound + GroundFeatureBudget; + const float CapLower = H * P.CeilingRelative + - VOXEL_NOISE_SCALE * FMath::Max(P.CeilingUndulation, 0.0f) + - VOXEL_NOISE_SCALE * FMath::Max(P.CeilingRoughness, 0.0f) + - FMath::Max(P.CeilingRidgeStrength, 0.0f); + if (P.BoundarySealThickness < 0.0f + || GroundLower <= P.BoundarySealThickness) + { + return Fail(TEXT("surface ground lower bound must clear the bottom seal"), + GroundLower, P.BoundarySealThickness); + } + if (CapLower >= H - P.BoundarySealThickness) + { + return Fail(TEXT("surface cap lower bound must stay below the top seal"), + CapLower, H - P.BoundarySealThickness); + } + if (GroundUpper + 2.0f >= CapLower) + { + return Fail(TEXT("surface ground and cap worst-case bounds need two voxels"), + GroundUpper + 2.0f, CapLower); + } + if (P.OverhangStrength > 0.0f + && GroundUpper + P.OverhangHeight + 2.0f >= CapLower) + { + return Fail(TEXT("surface overhang height must fit below the cap"), + GroundUpper + P.OverhangHeight + 2.0f, CapLower); + } + if (P.BaseDensity <= 0.0f) + { + return Fail(TEXT("BaseDensity must be > 0"), P.BaseDensity, 0.0f); + } + return true; + } + + case ECaveGeneratorType::VerticalShafts: + { + const FVerticalShaftParams& P = Params.VerticalShaftParams; + const float RoughnessReach = FMath::Max(P.SurfaceRoughness, 0.0f) + * VOXEL_NOISE_SCALE * 1.5f; + const float FloorClearance = FMath::Max( + 1.0f, P.SurfaceRoughness * VOXEL_NOISE_SCALE + 1.0f); + if (P.ShaftSpacing <= 0.0f || P.ShaftMinRadius <= 0.0f + || P.ShaftMaxRadius < P.ShaftMinRadius) + { + return Fail(TEXT("shaft spacing/radius values must be ordered and positive"), + P.ShaftMinRadius, P.ShaftMaxRadius); + } + if (P.ShaftMaxRadius > P.ShaftSpacing * 0.50f) + { + return Fail(TEXT("shaft radius must leave the neighbouring cell wall"), + P.ShaftMaxRadius, P.ShaftSpacing * 0.50f); + } + if (P.ShaftMinRadius <= RoughnessReach + 0.25f) + { + return Fail(TEXT("shaft radius must exceed the landing roughness margin"), + P.ShaftMinRadius, RoughnessReach + 0.25f); + } + if (P.ConnectorRadius < RoughnessReach + 1.0f) + { + return Fail(TEXT("ConnectorRadius must exceed the tree roughness margin"), + P.ConnectorRadius, RoughnessReach + 1.0f); + } + if (P.LedgeSpacing > 0.0f && P.LedgeDepth > 0.0f + && P.LedgeSpacing <= 2.0f * (P.LedgeDepth + FloorClearance)) + { + return Fail(TEXT("ledge spacing must leave a safe band on both sides"), + P.LedgeSpacing, 2.0f * (P.LedgeDepth + FloorClearance)); + } + if (P.BoundarySealThickness < 0.0f + || P.BoundarySealThickness * 2.0f >= H) + { + return Fail(TEXT("shaft seals must leave an interior"), + P.BoundarySealThickness * 2.0f, H); + } + if (P.BaseDensity <= 0.0f) + { + return Fail(TEXT("BaseDensity must be > 0"), P.BaseDensity, 0.0f); + } + return true; + } + + case ECaveGeneratorType::FloatingIslands: + { + const FFloatingIslandParams& P = Params.FloatingIslandParams; + const float UnderDepth = P.IslandMaxRadius * FMath::Max(P.ThicknessRatio, 0.25f); + const float TopHalf = P.IslandMaxRadius * 0.20f; + const float MaxIslandHalfHeight = FMath::Max(TopHalf, UnderDepth); + if (P.IslandSpacing <= 0.0f || P.IslandMinRadius <= 0.0f + || P.IslandMaxRadius < P.IslandMinRadius) + { + return Fail(TEXT("island spacing/radius values must be ordered and positive"), + P.IslandMinRadius, P.IslandMaxRadius); + } + if (P.IslandMaxRadius > P.IslandSpacing * 0.50f) + { + return Fail(TEXT("island radius must leave a cell-scale separation budget"), + P.IslandMaxRadius, P.IslandSpacing * 0.50f); + } + if (P.BoundarySealThickness < 0.0f + || P.BoundarySealThickness + MaxIslandHalfHeight >= H * 0.50f) + { + return Fail(TEXT("island thickness + seal must fit in half the strate"), + P.BoundarySealThickness + MaxIslandHalfHeight, H * 0.50f); + } + if (P.IslandMinRadius + <= P.SurfaceRoughness * VOXEL_NOISE_SCALE + P.SDFBlendRadius) + { + return Fail(TEXT("island minimum radius must survive roughness and SDF blend"), + P.IslandMinRadius, + P.SurfaceRoughness * VOXEL_NOISE_SCALE + P.SDFBlendRadius); + } + if (P.BaseDensity <= 0.0f) + { + return Fail(TEXT("BaseDensity must be > 0"), P.BaseDensity, 0.0f); + } + return true; + } + + default: + break; + } + OutViolation = TEXT("no constraint rule for archetype"); + return false; +} + FStrateGenerationParams VF_RollStrateParams(const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index) { diff --git a/Source/VoxelForge/Public/VoxelStrateComposer.h b/Source/VoxelForge/Public/VoxelStrateComposer.h index 31e98d4..31a8a7b 100644 --- a/Source/VoxelForge/Public/VoxelStrateComposer.h +++ b/Source/VoxelForge/Public/VoxelStrateComposer.h @@ -320,6 +320,35 @@ VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype( /** Pure structure roll: no retained RNG state and no corpus/runtime dependency. */ VOXELFORGE_API FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index); +/** + * Provenance for a parameter block that does not read the authored corpus. + * + * NaiveUniform is the §3.3 control: every scalar is rolled independently inside the same + * finite code-declared envelope used by the constraint arm. ConstraintSampled draws the + * independent geometry quantities and derives the coupled quantities from generator equations. + * Neither mode reads a strate asset or a corpus entry. + */ +enum class EVoxelStrateCorpusFreeSamplingMode : uint8 +{ + NaiveUniform, + ConstraintSampled, +}; + +/** Roll one native parameter family without consulting FVoxelStrateCorpus. */ +VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsCorpusFree( + ECaveGeneratorType Archetype, + int32 Seed, + int32 Index, + EVoxelStrateCorpusFreeSamplingMode Mode, + float StrateHeightInVoxels); + +/** Validate the hard geometry relations used by ConstraintSampled and the corpus audit. */ +VOXELFORGE_API bool VF_ValidateStrateCorpusFreeConstraints( + ECaveGeneratorType Archetype, + const FVoxelStrateArchetypeParams& Params, + float StrateHeightInVoxels, + FString& OutViolation); + #if WITH_EDITOR /** * Roll one candidate for the editor walk-through path. Parameter rolls call