feat(composer): it invents strates that work -- 59/64, and §6.2 is CLOSED

The corpus was starved: the loader followed the active settings pool and
found ONE vector, so all 64 candidates were bit-identical and every number
was a placeholder. It now loads every UVoxelStrateDefinition via the Asset
Registry (Saved/Autosaves and Saved/Cooked excluded -- including them would
silently weight the corpus toward whatever was autosaved last) plus the
per-archetype defaults from VoxelStrateTypes.h, which are themselves
hand-tuned known-good vectors. Blending stays WITHIN an archetype;
different archetypes read different subsets of the 73 fields.

⭐ THE COMPOSER WORKS. 64 candidates blended from real parents, every one
measured:

  survival 59/64 (92.2%)
  non-vacuous 60 · largest-share >= .50  60 · primordial law 63 · roll
  failures 0

and they are genuinely different worlds, not one world 64 times:
  air fraction   .0137 -> 1.000
  walkable       .000  -> .389
  feature scale  4     -> 376 voxels
  archetypes     TunnelNetwork, VerticalShafts, FlatPlain, CrystalChamber,
                 SurfaceWorld, Underwater, FloatingIslands, Maze

⭐⭐ §6.2 IS CLOSED, and it was the real risk. Box-verdict bounds were only
ever proved against the parameters Jahni typed; the whole worry was that
invented parameters would violate a bound nobody had checked -- silently,
leaving no geometry and no collision. Against ROLLED parameters:

  1,224 boxes proved (549 AllSolid, 675 AllAir), 1,336 Mixed
  1,629,144 voxels brute-forced
  0 VIOLATIONS

The bounds hold across the rolled range, not just at authored points. The
isolation work in 8e346a3 is what made that true.

The 5 rejections are a real finding, not noise: FloatingIslands can jitter
its islands out of existence and return an all-air strate (air 1.000,
walkable 0.000, feature scale 0). That is exactly what reject-and-resample
(§3.5) is for -- the measurement caught it without anyone anticipating it,
which is the whole argument for measuring rather than trusting.

Roll+measure 112.6 s for 64 candidates. §3.1's budget is "runs overnight",
so there is room for thousands.

Build clean, 23 succeeded / 0 warnings / 0 failed (the deliberate vacuity
warning is gone because the check is no longer vacuous). Verified by me.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-04 08:32:01 +02:00
1 parent 358bd415a9
commit 190ff381c0
5 files changed
+1503 -249

No files matched your search

+16 -11
View File
@@ -438,16 +438,21 @@ One asset = one biome: identity + `DebugColor`, climate placement box (`ReliefMi
atmosphere override, `WaterMaterial`, `MaterialPaletteIndex` (F6 — baked to vertex colour, §8.15)), `GameplayTags`. Referenced from atmosphere override, `WaterMaterial`, `MaterialPaletteIndex` (F6 — baked to vertex colour, §8.15)), `GameplayTags`. Referenced from
`UVoxelStrateDefinition::Biomes[]`. Generator-agnostic (surface biomes now, cave biomes later). §8.14. `UVoxelStrateDefinition::Biomes[]`. Generator-agnostic (surface biomes now, cave biomes later). §8.14.
**`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (NEW, Tier 4a) — offline **`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (Tier 4a) — offline
`FVoxelStrateCorpus` and `VF_RollStrateParams`. Walks the settings asset's fixed/pool definition `FVoxelStrateCorpus` and `VF_RollStrateParams`. `LoadFromAssetRegistry` enumerates every project
references, admits only `TunnelNetwork` / `Underwater` vectors (the archetypes that author `UVoxelStrateDefinition` through the Asset Registry, excludes `Saved/Autosaves` and `Saved/Cooked`
`GenerationParams`), measures every X-macro field's min/max/mean/population stddev, and exposes the copies, and adds one `VoxelStrateTypes.h` default vector for each of the eight exact archetypes.
explicit non-tunable list. Rolls are pure in `(corpus contents, seed, index)`: weighted 2–3-parent The corpus is grouped by exact `ECaveGeneratorType`: the 2026-09-04 run loaded **4 project
same-archetype selection → `FStrateGenerationParams::Lerp` → ±15% of measured field range jitter → vectors + 8 defaults = 12 members** in **8 groups**. Sibling families use their native structs
editor-reflection clamps → ordered-pair repair. Bool `bTunnelsFlowTowardOrigin` inherits from the (`FSlabGenerationParams`, `FMazeGenerationParams`, `FSurfaceGenerationParams`,
dominant parent; integer/enum SNAP fields are not jittered. This API is not called by runtime `FVerticalShaftParams`, `FFloatingIslandParams`); no cross-archetype blend is attempted.
generation. The first project corpus run found one eligible `DA_Strate3` vector, so all measured Spreads are measured per group, including the plain-native tunnel transport fields for reporting.
spreads were zero; that limitation is recorded by the Tier 4a test/report rather than hidden. Rolls are pure in `(corpus contents, seed, index)`: weighted 2–3-parent same-archetype selection →
`FStrateGenerationParams::Lerp` or reflected native-family blend → ±15% of measured field range
jitter → editor-reflection clamps → ordered-pair repair. Bool `bTunnelsFlowTowardOrigin` inherits
from the dominant parent; integer/enum SNAP fields are not jittered. This API is not called by
runtime generation. The settings audit found **one unique path**, `/Game/VoxelForge/DA_Strate3`,
which explains the old one-vector corpus; the project assets were never in that pool.
### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp` ### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
`UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength; `UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength;
@@ -512,7 +517,7 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
| `VoxelForgeOpStackIslandTest.cpp` | `VoxelForge.OpStack.FloatingIslandEquivalence` | The port that runs the stack **backwards** — void + fill vs rock + carve, same classes with the opposite sign. Counts interior-solid and open-void samples separately (on this archetype an aggregate "N solid" is dominated by the seal bands and says nothing about the islands). Counts `AllSolid` and `AllAir` verdicts **separately** too: `AllAir` is the one no cave archetype could ever prove, and it is the entire perf argument here. | | `VoxelForgeOpStackIslandTest.cpp` | `VoxelForge.OpStack.FloatingIslandEquivalence` | The port that runs the stack **backwards** — void + fill vs rock + carve, same classes with the opposite sign. Counts interior-solid and open-void samples separately (on this archetype an aggregate "N solid" is dominated by the seal bands and says nothing about the islands). Counts `AllSolid` and `AllAir` verdicts **separately** too: `AllAir` is the one no cave archetype could ever prove, and it is the entire perf argument here. |
| `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | **Phase 1's load-bearing test.** The 7-op Maze stack vs `GetMazeDensity` over 20k points (aiming for bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers and brute force on every box verdict the stack emits. Reports how many tiles the stack can prove uniform — today's `ClassifyTile` proves **zero** for any cave archetype. | | `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | **Phase 1's load-bearing test.** The 7-op Maze stack vs `GetMazeDensity` over 20k points (aiming for bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers and brute force on every box verdict the stack emits. Reports how many tiles the stack can prove uniform — today's `ClassifyTile` proves **zero** for any cave archetype. |
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). | | `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Loads the hand-authored settings corpus, prints the complete per-field spread/exclusion/clamp table, asserts deterministic bit-identical rolls, measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law, and brute-forces every rolled production box verdict. The first run found one eligible vector (all spreads zero), 64/64 survival, and a deliberately reported vacuous §6.2 scan: 2,560 Mixed, 0 proved, 0 violations. | | `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. The final full-suite run: **59/64 survival (92.2%)**, **1,336 Mixed + 549 AllSolid + 675 AllAir = 1,224 proved boxes**, **1,629,144 lattice voxels checked, 0 violations**, 112.574 s total. |
| `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. | | `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. | | `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. | | `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. |
+91 -20
View File
@@ -200,18 +200,66 @@ exclusion list. Bools cannot be blended — roll them by probability or inherit
`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp` now implement the corpus and `Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp` now implement the corpus and
the deterministic roll. `Private/Tests/VoxelForgeComposerParameterRollTest.cpp` is the offline the deterministic roll. `Private/Tests/VoxelForgeComposerParameterRollTest.cpp` is the offline
measurement harness. The roll uses `FStrateGenerationParams::Lerp`, chooses 2–3 weighted parents measurement harness. The roll uses the native family blend (`FStrateGenerationParams::Lerp` for
inside one archetype group, applies jitter of **±15% of each field's measured corpus range the tunnel family), chooses 2–3 weighted parents **inside one exact archetype group**, applies
(`max-min`)**, then applies reflected `ClampMin`/`ClampMax` metadata. It does not use clamps as jitter of **±15% of each field's measured corpus range (`max-min`)**, then applies reflected
distribution ranges, does not touch generation, and leaves `WorldRadiusVoxels` at its default 0. `ClampMin`/`ClampMax` metadata. It does not use clamps as distribution ranges, does not touch
generation, and leaves `WorldRadiusVoxels` at its default 0. The sibling native parameter structs
are carried in the same corpus entry, so a `SurfaceWorld` vector is never blended with a
`TunnelNetwork` vector merely because both happen to be stored in a strate asset.
The loader walks the settings asset's fixed-strate and pool references, de-duplicates by asset path, The original one-vector result was caused by the old loader following `DA_Settings`' fixed-strate
and accepts only `TunnelNetwork` / `Underwater`: those are the only archetypes whose authored vector and pool references. An audit of those references finds **one unique path**,
is `GenerationParams`. The current `DA_Settings` asset contains **one unique resolved reference**, `/Game/VoxelForge/DA_Strate3.DA_Strate3`, whose archetype is `TunnelNetwork`; that is why the
`DA_Strate3`, and it is `TunnelNetwork`; the other authored archetype assets are not in that previous corpus only loaded one asset. The current loader instead asks the Asset Registry for every
settings corpus. Consequently every measured field has zero spread, all 64 rolls repeat the same project `UVoxelStrateDefinition`, sorts and de-duplicates the results, accepts `/Game` packages,
known-good vector (parent weights still roll deterministically), and this run did not exercise and ignores `Saved/Autosaves` and `Saved/Cooked` copies. The scan found **4 project assets**, with
non-zero jitter. That is a corpus-size finding, not a rate optimization. no saved-copy, outside-project, duplicate, or unresolved records:
| Asset | Archetype |
|---|---|
| `DA_Strate1` | `SurfaceWorld` |
| `DA_Strate2` | `FloatingIslands` |
| `DA_Strate3` | `TunnelNetwork` |
| `DA_Strate4` | `TunnelNetwork` |
The hand-authored C++ defaults in `VoxelStrateTypes.h` are also corpus members: one for each of the
8 supported archetypes (`TunnelNetwork`, `FlatPlain`, `CrystalChamber`, `Maze`, `SurfaceWorld`,
`VerticalShafts`, `FloatingIslands`, `Underwater`). The fed corpus is therefore **12 members** in
**8 exact-archetype groups**. Group membership is `TunnelNetwork=3`, `FlatPlain=1`,
`CrystalChamber=1`, `Maze=1`, `SurfaceWorld=2`, `VerticalShafts=1`, `FloatingIslands=2`, and
`Underwater=1`.
The complete 253-descriptor spread table is emitted by the automation test, including sample count,
min/max/mean/standard deviation, excluded/reflected status, and clamp metadata. The 230 zero-range
rows are intentionally omitted below; these are the **23 rows with real spread** (float formatting
is the test's measured output):
| Archetype | Field | N | Min | Max | Mean | StdDev |
|---|---|---:|---:|---:|---:|---:|
| TunnelNetwork | MinRoomRadius | 3 | 10 | 25 | 15 | 7.07106781 |
| TunnelNetwork | MaxRoomRadius | 3 | 30 | 125 | 61.6666667 | 44.7834295 |
| TunnelNetwork | OriginRoomRadius | 3 | 20 | 120 | 53.3333333 | 47.1404521 |
| TunnelNetwork | TunnelMinRadius | 3 | 3 | 9 | 5 | 2.82842712 |
| TunnelNetwork | TunnelMaxRadius | 3 | 7 | 15 | 9.66666667 | 3.77123617 |
| SurfaceWorld | BaseGroundRelative | 2 | 0.200000003 | 0.300000012 | 0.250000007 | 0.0500000045 |
| SurfaceWorld | ElevationRange | 2 | 60 | 200 | 130 | 70 |
| SurfaceWorld | ContinentFrequency | 2 | 0.00100000005 | 0.00600000005 | 0.00350000005 | 0.0025 |
| SurfaceWorld | MountainStrength | 2 | 0.5 | 1 | 0.75 | 0.25 |
| SurfaceWorld | MountainFrequency | 2 | 0.00200000009 | 0.0120000001 | 0.0070000001 | 0.005 |
| SurfaceWorld | SurfaceRoughness | 2 | 2 | 3 | 2.5 | 0.5 |
| SurfaceWorld | ReliefStrength | 2 | 0 | 0.699999988 | 0.349999994 | 0.349999994 |
| SurfaceWorld | ReliefContrast | 2 | 1.60000002 | 2 | 1.80000001 | 0.199999988 |
| SurfaceWorld | TerraceHeight | 2 | 12 | 20 | 16 | 4 |
| SurfaceWorld | BeachWidth | 2 | 8 | 200 | 104 | 96 |
| FloatingIslands | IslandSpacing | 2 | 95 | 1000 | 547.5 | 452.5 |
| FloatingIslands | IslandDensity | 2 | 0.100000001 | 0.5 | 0.300000001 | 0.199999999 |
| FloatingIslands | IslandMinRadius | 2 | 18 | 50 | 34 | 16 |
| FloatingIslands | IslandMaxRadius | 2 | 42 | 500 | 271 | 229 |
| FloatingIslands | ThicknessRatio | 2 | 0.100000001 | 0.699999988 | 0.399999995 | 0.299999993 |
| FloatingIslands | VerticalJitter | 2 | 0.200000003 | 0.600000024 | 0.400000013 | 0.20000001 |
| FloatingIslands | TopFlatten | 2 | 0.600000024 | 0.800000012 | 0.700000018 | 0.099999994 |
| FloatingIslands | SurfaceRoughness | 2 | 0 | 4 | 2 | 2 |
The reflection/use audit found **36 excluded fields**: the 34 terrain-op transport fields The reflection/use audit found **36 excluded fields**: the 34 terrain-op transport fields
`TerraceStepHeight`, `TerraceHardness`, `TerraceNoiseDisplacement`, `LayerLineSpacing`, `TerraceStepHeight`, `TerraceHardness`, `TerraceNoiseDisplacement`, `LayerLineSpacing`,
@@ -222,22 +270,45 @@ The reflection/use audit found **36 excluded fields**: the 34 terrain-op transpo
`ChimneyMaxRadius`, `ChimneyHeight`, `DomeDensity`, `DomeMinRadius`, `DomeMaxRadius`, `ChimneyMaxRadius`, `ChimneyHeight`, `DomeDensity`, `DomeMinRadius`, `DomeMaxRadius`,
`DomeHeightRatio`, `PinchDensity`, `PinchStrength`, `PinchLength` — populated per room by `DomeHeightRatio`, `PinchDensity`, `PinchStrength`, `PinchLength` — populated per room by
`UVoxelTerrainOpDefinition` — plus the manager-owned runtime bounds `StrateTopWorldZ` and `UVoxelTerrainOpDefinition` — plus the manager-owned runtime bounds `StrateTopWorldZ` and
`StrateBottomWorldZ`. The test prints the complete field-by-field table, including excluded fields, `StrateBottomWorldZ`. The tunnel transport fields are measured directly from the native
`FStrateGenerationParams` X-macro even though they are not reflected `UPROPERTY`s; their values are
reported but deliberately not jittered. Same-named fields in sibling structs are not automatically
excluded: the exclusion applies to the tunnel transport slots whose generation source is the
terrain-op pool. The test prints the complete field-by-field table, including excluded fields,
reflection status, and clamp metadata. reflection status, and clamp metadata.
The terrain-field source is now traced in code. `VoxelStrateManager.cpp` loads the active strate's
`TerrainOperations` soft-object pool, and `VoxelGenerator.cpp` passes that pool to
`VoxelCaveMorphology::BuildChunkCache`. For each nearest room, `FVoxelTerrainOpDefinition::ApplyTo`
overlays the selected operation onto a local copy (`FStrateGenerationParams LocalTerrainParams =
Params`) before the room operator runs. Thus the 34 values are transported through the strate
params, but their authored cave-detail values come from `UVoxelTerrainOpDefinition` assets selected
by the active strate's terrain-operation pool. Rolling those slots in the strate composer is not a
useful way to roll cave detail: selected room operations overwrite them. It is not a literal
absolute no-op, because the local base copy is still used where no operation overwrites a slot.
The correct future design is a **second corpus** over terrain-op assets plus the active pool's
membership/weights; this task does not build it. The project currently has **1**
`UVoxelTerrainOpDefinition` asset (the Asset Registry count used by the test).
The bool policy is explicit: `bTunnelsFlowTowardOrigin` is inherited from the dominant parent. The bool policy is explicit: `bTunnelsFlowTowardOrigin` is inherited from the dominant parent.
`OriginRoomMaxConnections` and `RoughnessNoiseType` retain `Lerp`'s existing SNAP behavior and `OriginRoomMaxConnections` and `RoughnessNoiseType` retain `Lerp`'s existing SNAP behavior and
are not jittered. In this editor build clamp metadata was available while the table was built; it are not jittered. In this editor build clamp metadata was available while the table was built; it
is **not promised in a cooked runtime**, so the commandlet/cook-time bake remains owed. is **not promised in a cooked runtime**, so the commandlet/cook-time bake remains owed.
The first 64-candidate run (step 4, radius 256, max 8,000,000 cells, fixture seed 1337) produced The fed 64-candidate run (step 4 sweep, radius 256, max 8,000,000 cells, fixture seed 1337)
64/64 non-vacuous candidates, 64/64 largest-component share ≥ 0.50, and 64/64 exact unsnapped produced **59/64 survivors (92.2%)**. There were 60 non-vacuous candidates, 60 with largest
arrival→departure law passes. Every row was `air=0.520272`, `largest share=0.999996`, component share ≥ 0.50, 63 exact unsnapped arrival→departure law passes, and no roll failures.
`walkable=0.057995`, `feature scale=20.000000`, `Connected`; total runtime was **542.220 s**. The complete 64-row table is emitted by `VoxelForge.Composer.ParameterRoll`; each row reports
The §6.2 check saw **2,560 Mixed, 0 AllSolid, 0 AllAir, 0 brute-force voxels, 0 violations**: archetype, weighted parents, air fraction, largest share, walkable fraction, feature scale, and
this is explicitly **vacuous**, not evidence that rolled bounds are sound, because the current cave arrival→departure verdict. The final full-suite run took **112.574 s** total (**0.015 s** corpus
box path emitted no uniform verdict. The test leaves §6.2 open and reports that fact rather than load, **112.556 s** roll/measurement).
turning it into a workaround.
The §6.2 check is no longer vacuous: **1,224 boxes proved**, **1,629,144 voxels checked**,
**0 violations**. Its verdict mix was 1,336 Mixed, 549 AllSolid, and 675 AllAir; the law check
reported no violating candidate. The box law used unit step and brute-forced every voxel of each
uniform box, while the candidate sweep retained step 4. The test asserts zero violations and keeps
the law open to future changed candidates rather than treating this clean run as a license to tune
the survival rate.
### 3.4 The measurement pass — one grid, one flood fill, three jobs ### 3.4 The measurement pass — one grid, one flood fill, three jobs
@@ -4,10 +4,15 @@
#include "Misc/AutomationTest.h" #include "Misc/AutomationTest.h"
#include "HAL/PlatformTime.h" #include "HAL/PlatformTime.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Modules/ModuleManager.h"
#include "VoxelForgeTestFixture.h" #include "VoxelForgeTestFixture.h"
#include "VoxelSettings.h"
#include "VoxelStrateComposer.h" #include "VoxelStrateComposer.h"
#include "VoxelStrateMeasure.h" #include "VoxelStrateMeasure.h"
#include "VoxelTerrainOpDefinition.h"
namespace namespace
{ {
@@ -53,6 +58,124 @@ namespace
*VF_FormatSpreadValue(Spread.ClampMax)); *VF_FormatSpreadValue(Spread.ClampMax));
} }
bool VF_IsProjectPackagePath(const FString& PackagePath)
{
return PackagePath == TEXT("/Game")
|| PackagePath.StartsWith(TEXT("/Game/"), ESearchCase::IgnoreCase);
}
bool VF_IsIgnoredSavedCopy(const FString& PackagePath)
{
return PackagePath.Contains(TEXT("/Saved/Autosaves"), ESearchCase::IgnoreCase)
|| PackagePath.Contains(TEXT("/Saved/Cooked"), ESearchCase::IgnoreCase);
}
TArray<FString> VF_GetProjectAssetPaths(UClass* AssetClass)
{
TArray<FString> Paths;
if (AssetClass == nullptr)
{
return Paths;
}
FAssetRegistryModule& AssetRegistryModule =
FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
TArray<FAssetData> Assets;
AssetRegistryModule.Get().GetAssetsByClass(AssetClass->GetClassPathName(), Assets, true);
TSet<FString> SeenPaths;
for (const FAssetData& Asset : Assets)
{
const FString PackagePath = Asset.PackageName.ToString();
const FString ObjectPath = Asset.GetObjectPathString();
if (!VF_IsProjectPackagePath(PackagePath)
|| VF_IsIgnoredSavedCopy(PackagePath)
|| VF_IsIgnoredSavedCopy(ObjectPath)
|| SeenPaths.Contains(ObjectPath))
{
continue;
}
SeenPaths.Add(ObjectPath);
Paths.Add(ObjectPath);
}
Paths.Sort();
return Paths;
}
FString VF_FormatSettingsAudit(const UVoxelSettings* Settings)
{
TArray<FString> Paths;
if (Settings != nullptr)
{
TSet<FString> UniquePaths;
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Pair : Settings->FixedStrates)
{
const FString Path = Pair.Value.ToSoftObjectPath().ToString();
if (!Path.IsEmpty())
{
UniquePaths.Add(Path);
}
}
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Reference : Settings->StratePool)
{
const FString Path = Reference.ToSoftObjectPath().ToString();
if (!Path.IsEmpty())
{
UniquePaths.Add(Path);
}
}
for (const FString& Path : UniquePaths)
{
Paths.Add(Path);
}
Paths.Sort();
}
FString Result = FString::Printf(TEXT("Settings corpus audit: %d unique fixed/pool paths"),
Paths.Num());
for (const FString& Path : Paths)
{
Result += FString::Printf(TEXT(" [%s]"), *Path);
}
Result += TEXT("; these settings references are not the corpus source.");
return Result;
}
bool VF_RollRuntimeZIsZero(const FVoxelStrateRollInfo& Roll)
{
switch (Roll.Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return Roll.ArchetypeParams.TunnelNetworkParams.StrateTopWorldZ == 0.0f
&& Roll.ArchetypeParams.TunnelNetworkParams.StrateBottomWorldZ == 0.0f;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
return Roll.ArchetypeParams.SlabParams.StrateTopWorldZ == 0.0f
&& Roll.ArchetypeParams.SlabParams.StrateBottomWorldZ == 0.0f;
case ECaveGeneratorType::Maze:
return Roll.ArchetypeParams.MazeParams.StrateTopWorldZ == 0.0f
&& Roll.ArchetypeParams.MazeParams.StrateBottomWorldZ == 0.0f;
case ECaveGeneratorType::SurfaceWorld:
return Roll.ArchetypeParams.SurfaceParams.StrateTopWorldZ == 0.0f
&& Roll.ArchetypeParams.SurfaceParams.StrateBottomWorldZ == 0.0f;
case ECaveGeneratorType::VerticalShafts:
return Roll.ArchetypeParams.VerticalShaftParams.StrateTopWorldZ == 0.0f
&& Roll.ArchetypeParams.VerticalShaftParams.StrateBottomWorldZ == 0.0f;
case ECaveGeneratorType::FloatingIslands:
return Roll.ArchetypeParams.FloatingIslandParams.StrateTopWorldZ == 0.0f
&& Roll.ArchetypeParams.FloatingIslandParams.StrateBottomWorldZ == 0.0f;
default:
return false;
}
}
bool VF_IsComposerDefault(const FVoxelStrateCorpusEntry& Entry)
{
return Entry.SourcePath.StartsWith(TEXT("/VoxelForge/ComposerDefaults/"),
ESearchCase::IgnoreCase);
}
FString VF_FormatParents(const FVoxelStrateCorpus& Corpus, FString VF_FormatParents(const FVoxelStrateCorpus& Corpus,
const FVoxelStrateRollInfo& Roll) const FVoxelStrateRollInfo& Roll)
{ {
@@ -233,21 +356,81 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
FVoxelStrateCorpus Corpus; FVoxelStrateCorpus Corpus;
FString CorpusReport; FString CorpusReport;
const double CorpusStartSeconds = FPlatformTime::Seconds(); const double CorpusStartSeconds = FPlatformTime::Seconds();
const bool bCorpusLoaded = Corpus.LoadFromSettings(AuthoredSettings, CorpusReport); const bool bCorpusLoaded = Corpus.LoadFromAssetRegistry(CorpusReport);
const double CorpusSeconds = FPlatformTime::Seconds() - CorpusStartSeconds; const double CorpusSeconds = FPlatformTime::Seconds() - CorpusStartSeconds;
AddInfo(FString::Printf(TEXT("%s Load time %.3fs; contents hash 0x%08x; clamp metadata at " AddInfo(FString::Printf(TEXT("%s Load time %.3fs; contents hash 0x%08x; clamp metadata at "
"build=%s, promised at cooked runtime=%s."), "build=%s, promised at cooked runtime=%s."),
*CorpusReport, CorpusSeconds, Corpus.GetContentsHash(), *CorpusReport, CorpusSeconds, Corpus.GetContentsHash(),
Corpus.AreClampMetadataAvailableAtBuild() ? TEXT("yes") : TEXT("no"), Corpus.AreClampMetadataAvailableAtBuild() ? TEXT("yes") : TEXT("no"),
Corpus.AreClampMetadataAvailableAtRuntime() ? TEXT("yes") : TEXT("no"))); Corpus.AreClampMetadataAvailableAtRuntime() ? TEXT("yes") : TEXT("no")));
AddInfo(VF_FormatSettingsAudit(AuthoredSettings));
const TArray<FString> ProjectStrateAssets =
VF_GetProjectAssetPaths(UVoxelStrateDefinition::StaticClass());
const TArray<FString> ProjectTerrainOpAssets =
VF_GetProjectAssetPaths(UVoxelTerrainOpDefinition::StaticClass());
int32 NumProjectCorpusEntries = 0;
int32 NumDefaultCorpusEntries = 0;
FString CorpusMembership = TEXT("CORPUS MEMBERSHIP\n");
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
{
if (VF_IsComposerDefault(Entry))
{
++NumDefaultCorpusEntries;
}
else
{
++NumProjectCorpusEntries;
}
CorpusMembership += FString::Printf(TEXT("%s | %s | %s\n"),
VF_IsComposerDefault(Entry) ? TEXT("default") : TEXT("project"),
VF_GetStrateArchetypeName(Entry.Archetype),
*Entry.SourcePath);
}
AddInfo(FString::Printf(
TEXT("Corpus membership: %d project strate assets discovered, %d project vectors loaded, "
"%d defaults, %d total members. Terrain-op Asset Registry count (not rolled in this "
"task): %d."),
ProjectStrateAssets.Num(), NumProjectCorpusEntries, NumDefaultCorpusEntries, Corpus.Num(),
ProjectTerrainOpAssets.Num()));
AddInfo(CorpusMembership);
for (const ECaveGeneratorType Archetype : {
ECaveGeneratorType::TunnelNetwork,
ECaveGeneratorType::FlatPlain,
ECaveGeneratorType::CrystalChamber,
ECaveGeneratorType::Maze,
ECaveGeneratorType::SurfaceWorld,
ECaveGeneratorType::VerticalShafts,
ECaveGeneratorType::FloatingIslands,
ECaveGeneratorType::Underwater})
{
int32 NumDefaultsForArchetype = 0;
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
{
if (Entry.Archetype == Archetype && VF_IsComposerDefault(Entry))
{
++NumDefaultsForArchetype;
}
}
TestEqual(FString::Printf(TEXT("one hand-tuned default for %s"),
VF_GetStrateArchetypeName(Archetype)),
NumDefaultsForArchetype, 1);
TestTrue(FString::Printf(TEXT("%s has a same-archetype corpus group"),
VF_GetStrateArchetypeName(Archetype)),
Corpus.NumForArchetype(Archetype) > 0);
}
TestEqual(TEXT("all project strate assets plus eight defaults are corpus members"),
Corpus.Num(), ProjectStrateAssets.Num() + 8);
FString SpreadTable = TEXT( FString SpreadTable = TEXT(
"CORPUS SPREAD (population stddev; jitter range = ±15% of max-min; clamps are safety only)\n" "CORPUS SPREAD (population stddev; jitter range = ±15% of max-min; clamps are safety only)\n"
"field | kind | excluded | reflected | samples | min | max | mean | stddev | clamp\n"); "archetype | struct | field | kind | excluded | reflected | samples | min | max | mean | stddev | clamp\n");
for (const FVoxelStrateFieldSpread& Spread : Corpus.GetFieldSpreads()) for (const FVoxelStrateFieldSpread& Spread : Corpus.GetFieldSpreads())
{ {
SpreadTable += FString::Printf( SpreadTable += FString::Printf(
TEXT("%s | %s | %s | %s | %d | %s | %s | %s | %s | %s\n"), TEXT("%s | %s | %s | %s | %s | %s | %d | %s | %s | %s | %s | %s\n"),
VF_GetStrateArchetypeName(Spread.Archetype),
*Spread.ParamStructName,
*Spread.FieldName, *Spread.FieldName,
VF_FieldKindName(Spread.Kind), VF_FieldKindName(Spread.Kind),
Spread.bExcluded ? TEXT("yes") : TEXT("no"), Spread.bExcluded ? TEXT("yes") : TEXT("no"),
@@ -270,9 +453,10 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
AddInfo(ExclusionTable); AddInfo(ExclusionTable);
AddInfo(TEXT( AddInfo(TEXT(
"Bool policy: bTunnelsFlowTowardOrigin is inherited from the dominant weighted parent; " "Bool policy: bTunnelsFlowTowardOrigin is inherited from the dominant weighted parent; "
"int/enum SNAP fields use FStrateGenerationParams::Lerp and are not jittered. Parent " "int/enum SNAP fields use the native family blend and are not jittered. Parent selection "
"selection is weight-1.0 (equal) because UVoxelStrateDefinition has no corpus-weight field; " "is weight-1.0 (equal) because UVoxelStrateDefinition has no corpus-weight field; parents "
"parents are selected only within one archetype group.")); "are selected only within one exact archetype group. FStrateGenerationParams terrain-op "
"transport fields remain excluded; the sibling families' authored fields are rolled."));
TestTrue(TEXT("the reflected parameter schema is covered or explicitly excluded"), TestTrue(TEXT("the reflected parameter schema is covered or explicitly excluded"),
Corpus.IsSchemaValid()); Corpus.IsSchemaValid());
@@ -281,7 +465,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
{ {
AddError(FString::Printf( AddError(FString::Printf(
TEXT("Cannot run the 64-candidate measurement: %s"), TEXT("Cannot run the 64-candidate measurement: %s"),
Corpus.IsSchemaValid() ? TEXT("the settings asset exposed no TunnelNetwork/Underwater GenerationParams") Corpus.IsSchemaValid() ? TEXT("the Asset Registry exposed no usable strate vectors")
: *Corpus.GetSchemaError())); : *Corpus.GetSchemaError()));
return false; return false;
} }
@@ -326,6 +510,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
int64 TotalBoxCheckedVoxels = 0; int64 TotalBoxCheckedVoxels = 0;
int32 TotalBoxViolations = 0; int32 TotalBoxViolations = 0;
TArray<int32> BoxViolationCandidates; TArray<int32> BoxViolationCandidates;
FString FirstBoxViolation;
const double RollAndMeasureStartSeconds = FPlatformTime::Seconds(); const double RollAndMeasureStartSeconds = FPlatformTime::Seconds();
for (int32 CandidateIndex = 0; CandidateIndex < NumCandidates; ++CandidateIndex) for (int32 CandidateIndex = 0; CandidateIndex < NumCandidates; ++CandidateIndex)
@@ -349,23 +534,47 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
continue; continue;
} }
bool bParentsMatchArchetype = true;
for (const int32 ParentIndex : Roll.ParentEntryIndices)
{
if (!Corpus.GetEntries().IsValidIndex(ParentIndex)
|| Corpus.GetEntries()[ParentIndex].Archetype != Roll.Archetype)
{
bParentsMatchArchetype = false;
break;
}
}
TestTrue(FString::Printf(TEXT("candidate %d parents stay within %s"),
CandidateIndex, VF_GetStrateArchetypeName(Roll.Archetype)),
bParentsMatchArchetype);
const FVoxelStrateRollInfo RepeatRoll = VF_RollStrateParamsDetailed(
Corpus, AuthoredSettings->Seed, CandidateIndex);
TestTrue(FString::Printf(TEXT("candidate %d is bit-identical on deterministic reroll"),
CandidateIndex),
RepeatRoll.bValid && RepeatRoll.Archetype == Roll.Archetype
&& VF_AreStrateArchetypeParamsBitIdentical(
RepeatRoll.ArchetypeParams, Roll.ArchetypeParams, Roll.Archetype)
&& RepeatRoll.ParentEntryIndices == Roll.ParentEntryIndices
&& RepeatRoll.ParentWeights == Roll.ParentWeights);
// Put the invented vector into a transient fixture definition. This is an offline test // Put the invented vector into a transient fixture definition. This is an offline test
// world only; the runtime manager/generation code and authored assets are untouched. // world only; the runtime manager/generation code and authored assets are untouched.
UVoxelStrateDefinition* CandidateDefinition = UVoxelStrateDefinition* CandidateDefinition =
World.Definitions[CandidateStrateIndex].Get(); World.Definitions[CandidateStrateIndex].Get();
CandidateDefinition->GeneratorType = Roll.Archetype; CandidateDefinition->GeneratorType = Roll.Archetype;
CandidateDefinition->GenerationParams = Roll.Params; CandidateDefinition->GenerationParams = Roll.ArchetypeParams.TunnelNetworkParams;
CandidateDefinition->SlabParams = Roll.ArchetypeParams.SlabParams;
CandidateDefinition->MazeParams = Roll.ArchetypeParams.MazeParams;
CandidateDefinition->SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
CandidateDefinition->VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
CandidateDefinition->FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
CandidateDefinition->bUseOperatorStack = true; CandidateDefinition->bUseOperatorStack = true;
CandidateDefinition->TransitionType = EVoxelStrateTransition::Hard; CandidateDefinition->TransitionType = EVoxelStrateTransition::Hard;
World.Reinitialize(); World.Reinitialize();
const FStrateGenerationParams RuntimeParams = World.StrateManager->GetGenerationParams(
FIntVector(0, 0,
World.StrateManager->GetLayout()[CandidateStrateIndex].BottomChunkZ
+ (World.StrateManager->GetLayout()[CandidateStrateIndex].TopChunkZ
- World.StrateManager->GetLayout()[CandidateStrateIndex].BottomChunkZ) / 2));
TestTrue(FString::Printf(TEXT("candidate %d keeps runtime Z out of the rolled vector"), CandidateIndex), TestTrue(FString::Printf(TEXT("candidate %d keeps runtime Z out of the rolled vector"), CandidateIndex),
Roll.Params.StrateTopWorldZ == 0.0f && Roll.Params.StrateBottomWorldZ == 0.0f); VF_RollRuntimeZIsZero(Roll));
const FVoxelStrateMetrics Metrics = VF_MeasureStrate( const FVoxelStrateMetrics Metrics = VF_MeasureStrate(
*World.Generator, *World.StrateManager, CandidateStrateIndex, MeasureSettings); *World.Generator, *World.StrateManager, CandidateStrateIndex, MeasureSettings);
@@ -438,6 +647,10 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
if (BoxReport.Violations > 0) if (BoxReport.Violations > 0)
{ {
BoxViolationCandidates.Add(CandidateIndex); BoxViolationCandidates.Add(CandidateIndex);
if (FirstBoxViolation.IsEmpty())
{
FirstBoxViolation = BoxReport.FirstViolation;
}
} }
CandidateTable += FString::Printf( CandidateTable += FString::Printf(
@@ -481,8 +694,8 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
AddError(FString::Printf( AddError(FString::Printf(
TEXT("ROLLED BOX-VERDICT VIOLATION: %d candidate(s) produced a false AllSolid/AllAir " TEXT("ROLLED BOX-VERDICT VIOLATION: %d candidate(s) produced a false AllSolid/AllAir "
"claim; first candidate index %d. This closes the test red because §6.2 bounds " "claim; first candidate index %d. This closes the test red because §6.2 bounds "
"were not sound for the rolled parameter space."), "were not sound for the rolled parameter space. First violation: %s"),
BoxViolationCandidates.Num(), BoxViolationCandidates[0])); BoxViolationCandidates.Num(), BoxViolationCandidates[0], *FirstBoxViolation));
} }
if (TotalBoxProved == 0) if (TotalBoxProved == 0)
{ {
@@ -504,4 +717,3 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
} }
#endif // WITH_DEV_AUTOMATION_TESTS #endif // WITH_DEV_AUTOMATION_TESTS
File diff suppressed because it is too large. Load diff
+47 -6
View File
@@ -28,9 +28,11 @@ struct VOXELFORGE_API FVoxelStrateFieldExclusion
FString Reason; FString Reason;
}; };
/** Measured statistics for one field across the currently loaded corpus. */ /** Measured statistics for one field in one archetype's corpus group. */
struct VOXELFORGE_API FVoxelStrateFieldSpread struct VOXELFORGE_API FVoxelStrateFieldSpread
{ {
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FString ParamStructName;
FString FieldName; FString FieldName;
EVoxelStrateFieldKind Kind = EVoxelStrateFieldKind::Continuous; EVoxelStrateFieldKind Kind = EVoxelStrateFieldKind::Continuous;
bool bExcluded = false; bool bExcluded = false;
@@ -50,12 +52,30 @@ struct VOXELFORGE_API FVoxelStrateFieldSpread
double ClampMax = 0.0; double ClampMax = 0.0;
}; };
/** Native parameter storage for every currently supported strate archetype. */
struct VOXELFORGE_API FVoxelStrateArchetypeParams
{
FStrateGenerationParams TunnelNetworkParams;
FSlabGenerationParams SlabParams;
FMazeGenerationParams MazeParams;
FSurfaceGenerationParams SurfaceParams;
FVerticalShaftParams VerticalShaftParams;
FFloatingIslandParams FloatingIslandParams;
};
/** One known-good authored vector and the archetype that gives it meaning. */ /** One known-good authored vector and the archetype that gives it meaning. */
struct VOXELFORGE_API FVoxelStrateCorpusEntry struct VOXELFORGE_API FVoxelStrateCorpusEntry
{ {
FString SourcePath; FString SourcePath;
FString SourceName; FString SourceName;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork; ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
// Native storage is selected by Archetype. Keeping every family here makes the corpus
// type-safe without changing any runtime generation parameter struct.
FVoxelStrateArchetypeParams ArchetypeParams;
// Compatibility view for callers of the original Tier 4a API. It is meaningful for
// TunnelNetwork/Underwater entries and is the same value as ArchetypeParams.TunnelNetworkParams.
FStrateGenerationParams Params; FStrateGenerationParams Params;
// The current asset format has no corpus weight field. Loaded hand-authored entries therefore // The current asset format has no corpus weight field. Loaded hand-authored entries therefore
@@ -71,6 +91,10 @@ struct VOXELFORGE_API FVoxelStrateRollInfo
FString FailureReason; FString FailureReason;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork; ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams ArchetypeParams;
// Compatibility view for the original tunnel-only API. For a non-tunnel roll, the native
// result is in ArchetypeParams and this member is not the active candidate vector.
FStrateGenerationParams Params; FStrateGenerationParams Params;
TArray<int32> ParentEntryIndices; TArray<int32> ParentEntryIndices;
@@ -81,17 +105,22 @@ struct VOXELFORGE_API FVoxelStrateRollInfo
/** /**
* Known-good strate vectors plus their measured field spread. * Known-good strate vectors plus their measured field spread.
* *
* `LoadFromSettings` walks both the pool and fixed-strate references, resolves each definition, * `LoadFromAssetRegistry` enumerates every project UVoxelStrateDefinition through the Asset
* de-duplicates it by asset path, and admits only TunnelNetwork/Underwater definitions because * Registry, explicitly excluding Saved/Autosaves and Saved/Cooked copies. It also adds one
* those are the two archetypes whose authored vector is `GenerationParams`. Other archetypes have * known-good default vector for every archetype/family in VoxelStrateTypes.h. Parents are selected
* different parameter structs; treating their default `GenerationParams` as authored data would * only inside the exact archetype group; no cross-archetype blend is attempted.
* manufacture a false corpus.
*/ */
class VOXELFORGE_API FVoxelStrateCorpus class VOXELFORGE_API FVoxelStrateCorpus
{ {
public: public:
void Reset(); void Reset();
bool LoadFromAssetRegistry(FString& OutReport);
/**
* Compatibility alias retained for callers compiled against the first Tier 4a pass. The
* settings argument is audited only for diagnostics; it is not a corpus input.
*/
bool LoadFromSettings(const UVoxelSettings* Settings, FString& OutReport); bool LoadFromSettings(const UVoxelSettings* Settings, FString& OutReport);
bool LoadFromDefinitions(const TArray<UVoxelStrateDefinition*>& Definitions, FString& OutReport); bool LoadFromDefinitions(const TArray<UVoxelStrateDefinition*>& Definitions, FString& OutReport);
@@ -99,6 +128,9 @@ public:
bool AddEntry(const FString& SourcePath, const FString& SourceName, bool AddEntry(const FString& SourcePath, const FString& SourceName,
ECaveGeneratorType Archetype, const FStrateGenerationParams& Params, ECaveGeneratorType Archetype, const FStrateGenerationParams& Params,
float Weight = 1.0f); float Weight = 1.0f);
bool AddEntry(const FString& SourcePath, const FString& SourceName,
ECaveGeneratorType Archetype, const FVoxelStrateArchetypeParams& Params,
float Weight = 1.0f);
bool IsValid() const { return bSchemaValid && Entries.Num() > 0; } bool IsValid() const { return bSchemaValid && Entries.Num() > 0; }
bool IsSchemaValid() const { return bSchemaValid; } bool IsSchemaValid() const { return bSchemaValid; }
@@ -109,6 +141,10 @@ public:
const TArray<FVoxelStrateFieldSpread>& GetFieldSpreads() const { return FieldSpreads; } const TArray<FVoxelStrateFieldSpread>& GetFieldSpreads() const { return FieldSpreads; }
const TArray<FString>& GetSkippedDefinitions() const { return SkippedDefinitions; } const TArray<FString>& GetSkippedDefinitions() const { return SkippedDefinitions; }
int32 NumForArchetype(ECaveGeneratorType Archetype) const;
const FVoxelStrateFieldSpread* FindSpread(
ECaveGeneratorType Archetype, const FString& FieldName) const;
const FVoxelStrateFieldSpread* FindSpread(const FString& FieldName) const; const FVoxelStrateFieldSpread* FindSpread(const FString& FieldName) const;
/** Stable hash of source paths, archetypes, weights, and every listed scalar field. */ /** Stable hash of source paths, archetypes, weights, and every listed scalar field. */
@@ -149,3 +185,8 @@ VOXELFORGE_API FStrateGenerationParams VF_RollStrateParams(
VOXELFORGE_API bool VF_AreStrateParamsBitIdentical( VOXELFORGE_API bool VF_AreStrateParamsBitIdentical(
const FStrateGenerationParams& A, const FStrateGenerationParams& B); const FStrateGenerationParams& A, const FStrateGenerationParams& B);
/** Bitwise comparison of the native vector for one archetype family. */
VOXELFORGE_API bool VF_AreStrateArchetypeParamsBitIdentical(
const FVoxelStrateArchetypeParams& A,
const FVoxelStrateArchetypeParams& B,
ECaveGeneratorType Archetype);