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
+1506 -252

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
`UVoxelStrateDefinition::Biomes[]`. Generator-agnostic (surface biomes now, cave biomes later). §8.14.
**`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (NEW, Tier 4a) — offline
`FVoxelStrateCorpus` and `VF_RollStrateParams`. Walks the settings asset's fixed/pool definition
references, admits only `TunnelNetwork` / `Underwater` vectors (the archetypes that author
`GenerationParams`), measures every X-macro field's min/max/mean/population stddev, and exposes the
explicit non-tunable list. Rolls are pure in `(corpus contents, seed, index)`: weighted 2–3-parent
same-archetype selection → `FStrateGenerationParams::Lerp` → ±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 first project corpus run found one eligible `DA_Strate3` vector, so all measured
spreads were zero; that limitation is recorded by the Tier 4a test/report rather than hidden.
**`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (Tier 4a) — offline
`FVoxelStrateCorpus` and `VF_RollStrateParams`. `LoadFromAssetRegistry` enumerates every project
`UVoxelStrateDefinition` through the Asset Registry, excludes `Saved/Autosaves` and `Saved/Cooked`
copies, and adds one `VoxelStrateTypes.h` default vector for each of the eight exact archetypes.
The corpus is grouped by exact `ECaveGeneratorType`: the 2026-09-04 run loaded **4 project
vectors + 8 defaults = 12 members** in **8 groups**. Sibling families use their native structs
(`FSlabGenerationParams`, `FMazeGenerationParams`, `FSurfaceGenerationParams`,
`FVerticalShaftParams`, `FFloatingIslandParams`); no cross-archetype blend is attempted.
Spreads are measured per group, including the plain-native tunnel transport fields for reporting.
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`
`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. |
| `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`). |
| `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. |
| `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. |
+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
the deterministic roll. `Private/Tests/VoxelForgeComposerParameterRollTest.cpp` is the offline
measurement harness. The roll uses `FStrateGenerationParams::Lerp`, chooses 2–3 weighted parents
inside one archetype group, applies jitter of **±15% of each field's measured corpus range
(`max-min`)**, then applies reflected `ClampMin`/`ClampMax` metadata. It does not use clamps as
distribution ranges, does not touch generation, and leaves `WorldRadiusVoxels` at its default 0.
measurement harness. The roll uses the native family blend (`FStrateGenerationParams::Lerp` for
the tunnel family), chooses 2–3 weighted parents **inside one exact archetype group**, applies
jitter of **±15% of each field's measured corpus range (`max-min`)**, then applies reflected
`ClampMin`/`ClampMax` metadata. It does not use clamps as distribution ranges, does not touch
generation, and leaves `WorldRadiusVoxels` at its default 0. The sibling native parameter structs
are carried in the same corpus entry, so a `SurfaceWorld` vector is never blended with a
`TunnelNetwork` vector merely because both happen to be stored in a strate asset.
The loader walks the settings asset's fixed-strate and pool references, de-duplicates by asset path,
and accepts only `TunnelNetwork` / `Underwater`: those are the only archetypes whose authored vector
is `GenerationParams`. The current `DA_Settings` asset contains **one unique resolved reference**,
`DA_Strate3`, and it is `TunnelNetwork`; the other authored archetype assets are not in that
settings corpus. Consequently every measured field has zero spread, all 64 rolls repeat the same
known-good vector (parent weights still roll deterministically), and this run did not exercise
non-zero jitter. That is a corpus-size finding, not a rate optimization.
The original one-vector result was caused by the old loader following `DA_Settings`' fixed-strate
and pool references. An audit of those references finds **one unique path**,
`/Game/VoxelForge/DA_Strate3.DA_Strate3`, whose archetype is `TunnelNetwork`; that is why the
previous corpus only loaded one asset. The current loader instead asks the Asset Registry for every
project `UVoxelStrateDefinition`, sorts and de-duplicates the results, accepts `/Game` packages,
and ignores `Saved/Autosaves` and `Saved/Cooked` copies. The scan found **4 project assets**, with
no saved-copy, outside-project, duplicate, or unresolved records:
| Asset | Archetype |
|---|---|
| `DA_Strate1` | `SurfaceWorld` |
| `DA_Strate2` | `FloatingIslands` |
| `DA_Strate3` | `TunnelNetwork` |
| `DA_Strate4` | `TunnelNetwork` |
The hand-authored C++ defaults in `VoxelStrateTypes.h` are also corpus members: one for each of the
8 supported archetypes (`TunnelNetwork`, `FlatPlain`, `CrystalChamber`, `Maze`, `SurfaceWorld`,
`VerticalShafts`, `FloatingIslands`, `Underwater`). The fed corpus is therefore **12 members** in
**8 exact-archetype groups**. Group membership is `TunnelNetwork=3`, `FlatPlain=1`,
`CrystalChamber=1`, `Maze=1`, `SurfaceWorld=2`, `VerticalShafts=1`, `FloatingIslands=2`, and
`Underwater=1`.
The complete 253-descriptor spread table is emitted by the automation test, including 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
`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`,
`DomeHeightRatio`, `PinchDensity`, `PinchStrength`, `PinchLength` — populated per room by
`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.
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.
`OriginRoomMaxConnections` and `RoughnessNoiseType` retain `Lerp`'s existing SNAP behavior and
are not jittered. In this editor build clamp metadata was available while the table was built; it
is **not promised in a cooked runtime**, so the commandlet/cook-time bake remains owed.
The first 64-candidate run (step 4, radius 256, max 8,000,000 cells, fixture seed 1337) produced
64/64 non-vacuous candidates, 64/64 largest-component share ≥ 0.50, and 64/64 exact unsnapped
arrival→departure law passes. Every row was `air=0.520272`, `largest share=0.999996`,
`walkable=0.057995`, `feature scale=20.000000`, `Connected`; total runtime was **542.220 s**.
The §6.2 check saw **2,560 Mixed, 0 AllSolid, 0 AllAir, 0 brute-force voxels, 0 violations**:
this is explicitly **vacuous**, not evidence that rolled bounds are sound, because the current cave
box path emitted no uniform verdict. The test leaves §6.2 open and reports that fact rather than
turning it into a workaround.
The fed 64-candidate run (step 4 sweep, radius 256, max 8,000,000 cells, fixture seed 1337)
produced **59/64 survivors (92.2%)**. There were 60 non-vacuous candidates, 60 with largest
component share ≥ 0.50, 63 exact unsnapped arrival→departure law passes, and no roll failures.
The complete 64-row table is emitted by `VoxelForge.Composer.ParameterRoll`; each row reports
archetype, weighted parents, air fraction, largest share, walkable fraction, feature scale, and
arrival→departure verdict. The final full-suite run took **112.574 s** total (**0.015 s** corpus
load, **112.556 s** roll/measurement).
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
@@ -4,10 +4,15 @@
#include "Misc/AutomationTest.h"
#include "HAL/PlatformTime.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Modules/ModuleManager.h"
#include "VoxelForgeTestFixture.h"
#include "VoxelSettings.h"
#include "VoxelStrateComposer.h"
#include "VoxelStrateMeasure.h"
#include "VoxelTerrainOpDefinition.h"
namespace
{
@@ -53,6 +58,124 @@ namespace
*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,
const FVoxelStrateRollInfo& Roll)
{
@@ -233,21 +356,81 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
FVoxelStrateCorpus Corpus;
FString CorpusReport;
const double CorpusStartSeconds = FPlatformTime::Seconds();
const bool bCorpusLoaded = Corpus.LoadFromSettings(AuthoredSettings, CorpusReport);
const bool bCorpusLoaded = Corpus.LoadFromAssetRegistry(CorpusReport);
const double CorpusSeconds = FPlatformTime::Seconds() - CorpusStartSeconds;
AddInfo(FString::Printf(TEXT("%s Load time %.3fs; contents hash 0x%08x; clamp metadata at "
"build=%s, promised at cooked runtime=%s."),
*CorpusReport, CorpusSeconds, Corpus.GetContentsHash(),
Corpus.AreClampMetadataAvailableAtBuild() ? 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(
"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())
{
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,
VF_FieldKindName(Spread.Kind),
Spread.bExcluded ? TEXT("yes") : TEXT("no"),
@@ -270,9 +453,10 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
AddInfo(ExclusionTable);
AddInfo(TEXT(
"Bool policy: bTunnelsFlowTowardOrigin is inherited from the dominant weighted parent; "
"int/enum SNAP fields use FStrateGenerationParams::Lerp and are not jittered. Parent "
"selection is weight-1.0 (equal) because UVoxelStrateDefinition has no corpus-weight field; "
"parents are selected only within one archetype group."));
"int/enum SNAP fields use the native family blend and are not jittered. Parent selection "
"is weight-1.0 (equal) because UVoxelStrateDefinition has no corpus-weight field; parents "
"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"),
Corpus.IsSchemaValid());
@@ -281,7 +465,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
{
AddError(FString::Printf(
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()));
return false;
}
@@ -326,6 +510,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
int64 TotalBoxCheckedVoxels = 0;
int32 TotalBoxViolations = 0;
TArray<int32> BoxViolationCandidates;
FString FirstBoxViolation;
const double RollAndMeasureStartSeconds = FPlatformTime::Seconds();
for (int32 CandidateIndex = 0; CandidateIndex < NumCandidates; ++CandidateIndex)
@@ -349,23 +534,47 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
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
// world only; the runtime manager/generation code and authored assets are untouched.
UVoxelStrateDefinition* CandidateDefinition =
World.Definitions[CandidateStrateIndex].Get();
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->TransitionType = EVoxelStrateTransition::Hard;
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),
Roll.Params.StrateTopWorldZ == 0.0f && Roll.Params.StrateBottomWorldZ == 0.0f);
VF_RollRuntimeZIsZero(Roll));
const FVoxelStrateMetrics Metrics = VF_MeasureStrate(
*World.Generator, *World.StrateManager, CandidateStrateIndex, MeasureSettings);
@@ -438,6 +647,10 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
if (BoxReport.Violations > 0)
{
BoxViolationCandidates.Add(CandidateIndex);
if (FirstBoxViolation.IsEmpty())
{
FirstBoxViolation = BoxReport.FirstViolation;
}
}
CandidateTable += FString::Printf(
@@ -481,8 +694,8 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
AddError(FString::Printf(
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 "
"were not sound for the rolled parameter space."),
BoxViolationCandidates.Num(), BoxViolationCandidates[0]));
"were not sound for the rolled parameter space. First violation: %s"),
BoxViolationCandidates.Num(), BoxViolationCandidates[0], *FirstBoxViolation));
}
if (TotalBoxProved == 0)
{
@@ -504,4 +717,3 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
}
#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;
};
/** 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
{
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FString ParamStructName;
FString FieldName;
EVoxelStrateFieldKind Kind = EVoxelStrateFieldKind::Continuous;
bool bExcluded = false;
@@ -50,12 +52,30 @@ struct VOXELFORGE_API FVoxelStrateFieldSpread
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. */
struct VOXELFORGE_API FVoxelStrateCorpusEntry
{
FString SourcePath;
FString SourceName;
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;
// The current asset format has no corpus weight field. Loaded hand-authored entries therefore
@@ -71,6 +91,10 @@ struct VOXELFORGE_API FVoxelStrateRollInfo
FString FailureReason;
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;
TArray<int32> ParentEntryIndices;
@@ -81,17 +105,22 @@ struct VOXELFORGE_API FVoxelStrateRollInfo
/**
* Known-good strate vectors plus their measured field spread.
*
* `LoadFromSettings` walks both the pool and fixed-strate references, resolves each definition,
* de-duplicates it by asset path, and admits only TunnelNetwork/Underwater definitions because
* those are the two archetypes whose authored vector is `GenerationParams`. Other archetypes have
* different parameter structs; treating their default `GenerationParams` as authored data would
* manufacture a false corpus.
* `LoadFromAssetRegistry` enumerates every project UVoxelStrateDefinition through the Asset
* Registry, explicitly excluding Saved/Autosaves and Saved/Cooked copies. It also adds one
* known-good default vector for every archetype/family in VoxelStrateTypes.h. Parents are selected
* only inside the exact archetype group; no cross-archetype blend is attempted.
*/
class VOXELFORGE_API FVoxelStrateCorpus
{
public:
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 LoadFromDefinitions(const TArray<UVoxelStrateDefinition*>& Definitions, FString& OutReport);
@@ -99,6 +128,9 @@ public:
bool AddEntry(const FString& SourcePath, const FString& SourceName,
ECaveGeneratorType Archetype, const FStrateGenerationParams& Params,
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 IsSchemaValid() const { return bSchemaValid; }
@@ -109,6 +141,10 @@ public:
const TArray<FVoxelStrateFieldSpread>& GetFieldSpreads() const { return FieldSpreads; }
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;
/** 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(
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);