feat(composer): PROMOTION -- the corpus grows itself, and it did not collapse
Tier 5. §3.3's answer to Jahni's "I'd like the system to not rely at all on
the handmade ones": promotion does not REMOVE the authored seed, it dilutes
it. Every invented strate that measures well rejoins the corpus.
Five simulated seasons, 24 candidates each:
season corpus project/default/promoted survival spread
1 15 4/8/3 20.8% 0.869
2 20 4/8/8 54.2% 0.849
3 20 4/8/8 54.2% 0.849
4 21 4/8/9 45.8% 0.916
5 21 4/8/9 50.0% 0.916
Corpus 12 -> 21, of which 9 are discovered. Survival roughly doubles.
⭐ AND THE COLLAPSE RISK DID NOT MATERIALISE, which was the thing worth
measuring rather than assuming. Promotion selects survivors, and blending
survivors repeatedly can converge the corpus onto a narrow safe region --
the composer would get better at passing and worse at surprising. Spread
went 0.869 -> 0.916. It dipped and plateaued mid-run, but did not narrow.
Recorded honestly: seasons 3 and 5 promoted ZERO. The near-duplicate gate
(normalised metric distance < 0.20 over air, largest share, walkable,
feature scale, clearance) is doing its job, but it also means the corpus
saturates at this scale. Five seasons of 24 demonstrates the MECHANISM, not
§3.3's "season 5 blends 200 discovered ones" -- that needs the overnight
budget, not a test.
Safety: a promoted record is RE-MEASURED on load, never trusted. Verified by
corrupting a stored metric -- the loader discarded it and re-derived the
geometry. A stale or tampered record therefore cannot introduce an
unescapable strate into a future season, which is the one failure that would
matter. Provenance (project/default/promoted) is tracked and logged.
Codex's recommendation, and I agree: keep a small explicit OUTLIER QUOTA
among candidates that pass every hard gate. This run showed no collapse, but
survivor-only promotion keeps the long-term risk, and a quota costs nothing.
PART A: the fine ROI now centres on LargestComponentPoint instead of (0,0).
Blank fine renders 6/42 -> 3/42. The label now reads "largest open space",
not "strate centre" -- the image moved, so the caption had to.
Build clean, 27 succeeded / 0 failed, verified by me. WorldRadiusVoxels=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
43eaf1ef0e
commit
0b3ec18b43
10 files changed
+2611
-50
No files matched your search
+17
-3
@@ -445,7 +445,7 @@ 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`** (Tier 4a/4b) — offline
|
**`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (Tier 4a/4b + Tier 5) — offline
|
||||||
`FVoxelStrateCorpus`, `VF_RollStrateParams`, and `VF_RollStrateStructure`. `LoadFromAssetRegistry` enumerates every project
|
`FVoxelStrateCorpus`, `VF_RollStrateParams`, and `VF_RollStrateStructure`. `LoadFromAssetRegistry` enumerates every project
|
||||||
`UVoxelStrateDefinition` through the Asset Registry, excludes `Saved/Autosaves` and `Saved/Cooked`
|
`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.
|
copies, and adds one `VoxelStrateTypes.h` default vector for each of the eight exact archetypes.
|
||||||
@@ -477,6 +477,18 @@ it. `RequiredResources` / `ProvidedResources` close the room-state hole that cha
|
|||||||
could not express. `VoxelForgeComposerStructureRollTest` rolls 64 candidates, measures them through
|
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.
|
the offline sampler, and brute-forces every uniform box verdict from the novel stack itself.
|
||||||
|
|
||||||
|
Tier 5 adds `FVoxelStratePromotableRecord`, `FVoxelStratePromotionPolicy`, and
|
||||||
|
`FVoxelStratePromotionBatchResult`. `VF_SaveStratePromotedRecords` writes a sorted schema-2 JSON
|
||||||
|
corpus store containing the complete six-family parameter vector, recipe, archetype, gate evidence,
|
||||||
|
metrics, season/seed, and input corpus hash; `VF_SaveStrateSeasonManifest` writes one diffable JSON
|
||||||
|
summary beside it. `FVoxelStrateCorpus::LoadPromotedRecords` re-verifies every record through the
|
||||||
|
world-specific `IVoxelStratePromotionVerifier` before admission, remeasures project/default members
|
||||||
|
when that verifier supplies the optional audit, and tags membership `project` / `default` /
|
||||||
|
`promoted`. `VF_SelectStratePromotions` uses normalized measured-metric distance (threshold 0.20),
|
||||||
|
the existing corpus contents hash, deterministic RecordId ordering, and a six-record season cap.
|
||||||
|
The promotion test simulates five seasons and deliberately corrupts one stored metric to prove the
|
||||||
|
loader replaces it with a fresh measurement.
|
||||||
|
|
||||||
`EVoxelStrateCorpusFreeSamplingMode`, `VF_RollStrateParamsCorpusFree`, and
|
`EVoxelStrateCorpusFreeSamplingMode`, `VF_RollStrateParamsCorpusFree`, and
|
||||||
`VF_ValidateStrateCorpusFreeConstraints` are the offline corpus-free control/experiment. They never
|
`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
|
read a corpus entry or strate asset: `NaiveUniform` rolls every live scalar independently inside a
|
||||||
@@ -506,7 +518,8 @@ pass, emits deterministic centre-Y XZ and data-selected XY slices via `IImageWra
|
|||||||
and scalar density=0 contour views side by side without blurring the sampled raster. Each image is capped at
|
and scalar density=0 contour views side by side without blurring the sampled raster. Each image is capped at
|
||||||
512 pixels (including its scale footer). `FVoxelStrateFinePreviewSettings` is a separate caller-selected
|
512 pixels (including its scale footer). `FVoxelStrateFinePreviewSettings` is a separate caller-selected
|
||||||
fine pass: the composer selects only survivors at step 1, radius 64 (a 128×128 XY ROI), and `MaxCells=2,000,000`;
|
fine pass: the composer selects only survivors at step 1, radius 64 (a 128×128 XY ROI), and `MaxCells=2,000,000`;
|
||||||
the writer labels every image with its exact ROI and step and records cap refusals in the page. The 64-card
|
the caller centres it on each coarse survivor's `LargestComponentPoint` and the writer labels it
|
||||||
|
“largest open space” with its exact ROI and step; cap refusals are recorded in the page. The 64-card
|
||||||
sheet remains sorted by survivor status then descending corpus-centroid distance, repeats exact measurement
|
sheet remains sorted by survivor status then descending corpus-centroid distance, repeats exact measurement
|
||||||
windows, marks rejection reasons, explains the 2D-connectivity limitation, and has no runtime generation hook.
|
windows, marks rejection reasons, explains the 2D-connectivity limitation, and has no runtime generation hook.
|
||||||
|
|
||||||
@@ -575,7 +588,8 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
|||||||
| `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` | 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. |
|
| `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. |
|
| ″ | `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**. |
|
| `VoxelForgeComposerPromotionTest.cpp` | `VoxelForge.Composer.Promotion` | Re-measures the 12 project/default members, simulates five deterministic 24-candidate seasons with normalized measured-metric novelty (`<0.20`), cap 6, cumulative JSON promotion, provenance counts, corpus-hash checks, fresh-load gate verification, spread/survival reporting, and deliberate stale-metric corruption. Final run: **9 promoted**, corpus **4/8/9**, survival **20.8/54.2/54.2/45.8/50.0%**, spread **0.869214→0.916302**, **63.556 s**, 0 failures. |
|
||||||
|
| `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 centred on `LargestComponentPoint`, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Final run: **42/64 survival (65.6%)**, **64 distinct recipes**, **0 invalid recipes**, **64 coarse filled + 64 coarse contour pairs**, **42 fine filled + 42 fine contour pairs**, blank plan/card **6/42→3/42**, **243.649 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. |
|
| `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. |
|
| `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. |
|
||||||
|
|||||||
+86
-10
@@ -253,6 +253,50 @@ small commandlet walking the struct by reflection. Clamp coverage is partial. A
|
|||||||
tunables at all (`StrateTopWorldZ` / `StrateBottomWorldZ` are runtime Z bounds) and need an explicit
|
tunables at all (`StrateTopWorldZ` / `StrateBottomWorldZ` are runtime Z bounds) and need an explicit
|
||||||
exclusion list. Bools cannot be blended — roll them by probability or inherit from the dominant parent.
|
exclusion list. Bools cannot be blended — roll them by probability or inherit from the dominant parent.
|
||||||
|
|
||||||
|
### ✅ Tier 5 status — promotion is built and measured (2026-09-05)
|
||||||
|
|
||||||
|
Promotion is now a real corpus input, not a claim about a future approval UI. A surviving editor
|
||||||
|
candidate becomes a `FVoxelStratePromotableRecord`: complete native parameter vector (all six family
|
||||||
|
blocks), recipe, archetype tag, measured metric summary, season/seed/candidate identity, input corpus
|
||||||
|
hash, and the three gate results. `VF_SaveStratePromotedRecords` writes a sorted, pretty JSON store
|
||||||
|
(`schema_version=2`) beside one JSON season manifest per run. `FVoxelStrateCorpus` loads those records
|
||||||
|
alongside project assets and C++ defaults, tags each member `project`, `default`, or `promoted`, and
|
||||||
|
prints the provenance counts in the membership report. The stored metric is evidence for review only:
|
||||||
|
the world-specific verifier rebuilds the recipe and remeasures it on every load, and a failed fresh
|
||||||
|
non-vacuous, largest-component, or primordial arrival→departure gate cannot enter the corpus.
|
||||||
|
|
||||||
|
The provisional policy is deliberately explicit. It accepts only records that pass all three gates,
|
||||||
|
requires a normalized measured-metric distance of **at least 0.20** from every usable existing member
|
||||||
|
and already accepted record, and caps a season at **6** promotions. Distance is Euclidean over
|
||||||
|
`(air fraction, largest-component share, walkable fraction, median feature scale / 256 voxels,
|
||||||
|
median vertical clearance / 64 voxels)`. The 0.20 threshold is a review-sized novelty floor: it is
|
||||||
|
roughly a 20-point move in one bounded fraction, or a smaller combined move across dimensions, while
|
||||||
|
the physical normalizers make feature/clearance changes comparable. It is provisional, not the
|
||||||
|
owner's definition of good. One authored asset was measured as a valid but all-air window in this
|
||||||
|
fixture (`DA_Strate2`), so it is retained as measured-but-vacuous and uses conservative exact-parameter
|
||||||
|
identity fallback rather than a false metric distance; the other **11/12** base members participate
|
||||||
|
in the measured novelty space.
|
||||||
|
|
||||||
|
The five-season simulation used 24 structure candidates per season, the fixed sample window
|
||||||
|
(`step=8`, `radius=256`, `WorldRadiusVoxels=0`), and the same fresh verifier on every reload:
|
||||||
|
|
||||||
|
| Season | Input corpus hash | Corpus after | project/default/promoted | survivors / 24 | survival | measured spread | promoted | rejected near-dup / hash / cap |
|
||||||
|
|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||||
|
| 1 | `9dbcea85` | 15 | 4 / 8 / 3 | 5 | 20.8% | 0.869214 | 3 | 2 / 0 / 0 |
|
||||||
|
| 2 | `8b624d80` | 20 | 4 / 8 / 8 | 13 | 54.2% | 0.849043 | 5 | 8 / 0 / 0 |
|
||||||
|
| 3 | `cfbd74ec` | 20 | 4 / 8 / 8 | 13 | 54.2% | 0.849043 | 0 | 13 / 0 / 0 |
|
||||||
|
| 4 | `cfbd74ec` | 21 | 4 / 8 / 9 | 11 | 45.8% | 0.916302 | 1 | 10 / 0 / 0 |
|
||||||
|
| 5 | `c108a276` | 21 | 4 / 8 / 9 | 12 | 50.0% | 0.916302 | 0 | 12 / 0 / 0 |
|
||||||
|
|
||||||
|
The curve does **not** show a monotonic survival improvement: `20.8%, 54.2%, 54.2%, 45.8%,
|
||||||
|
50.0%`. Measured spread also dips and plateaus, but ends wider: **0.869214 → 0.916302
|
||||||
|
(+0.047089)**. This five-season run therefore found no collapse toward a mean, but it is not evidence
|
||||||
|
that collapse cannot happen at a different seed or policy. The current recommendation is to keep the
|
||||||
|
novelty floor and add a small, explicitly reviewable outlier quota in a later policy revision—promote
|
||||||
|
some distant survivors even when their quality score is merely acceptable—rather than tune this run
|
||||||
|
to look smoother. The owner still needs the preview before defining “good”; promotion is the dilution
|
||||||
|
mechanism, not that missing judgment.
|
||||||
|
|
||||||
### ✅ Tier 4a status — parameter roll built (2026-09-04)
|
### ✅ Tier 4a status — parameter roll built (2026-09-04)
|
||||||
|
|
||||||
`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp` now implement the corpus and
|
`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp` now implement the corpus and
|
||||||
@@ -654,6 +698,13 @@ cap, or the writer can be called for an explicitly selected list. Fine filenames
|
|||||||
card's text include the sample step and exact resolved XY/Z window, so they cannot be mistaken for the
|
card's text include the sample step and exact resolved XY/Z window, so they cannot be mistaken for the
|
||||||
coarse render. A refused fine render is shown as a refusal with its reason rather than silently omitted.
|
coarse render. A refused fine render is shown as a refusal with its reason rather than silently omitted.
|
||||||
|
|
||||||
|
The fine ROI is now centred on each survivor's deterministic `LargestComponentPoint` from the coarse
|
||||||
|
air flood fill and is labelled **“largest open space”** on the page. It is not the middle of the
|
||||||
|
strate and not the world origin. In the final 42-survivor run, the origin-centred baseline had **6/42**
|
||||||
|
uniform fine plan projections (and 6/42 complete two-view cards); the largest-open-space pass had
|
||||||
|
**3/42** in both counts. The reduction is real but not total: a largest open space can itself be
|
||||||
|
larger than a radius-64 ROI, so a uniform card is not proof that the strate is empty.
|
||||||
|
|
||||||
The vertical image is an XZ section through the sampled cell nearest the strate-window centre Y. The
|
The vertical image is an XZ section through the sampled cell nearest the strate-window centre Y. The
|
||||||
plan image scans every sampled Z layer, keeps layers containing both air and solid, and chooses the
|
plan image scans every sampled Z layer, keeps layers containing both air and solid, and chooses the
|
||||||
one with the largest number of XY solid/air boundary transitions; ties prefer more mixed cells, more
|
one with the largest number of XY solid/air boundary transitions; ties prefer more mixed cells, more
|
||||||
@@ -667,11 +718,11 @@ orange walkable cells, a turquoise scalar contour, and a one-chunk footer scale
|
|||||||
resampling. It also carries the required caveat: a 2D apparent join is not a 3D connectivity proof;
|
resampling. It also carries the required caveat: a 2D apparent join is not a 3D connectivity proof;
|
||||||
the separate arrival→departure verdict is the connectivity check.
|
the separate arrival→departure verdict is the connectivity check.
|
||||||
|
|
||||||
The focused run measured **0.456 s** for the coarse render, **70.271 s** for the 42 fine scalar samples,
|
The final run measured **0.300 s** for the coarse render, **133.272 s** for the origin-plus-centred
|
||||||
and **0.573 s** for their filled/contour rendering. Peak fine `Air` + `Density` capture allocation was
|
fine scalar samples, and **1.113 s** for their filled/contour rendering. Peak fine `Air` + `Density`
|
||||||
**9,338,880 bytes** and the peak single RGBA raster was **75,776 bytes**. The full StructureRoll test
|
capture allocation was **9,338,880 bytes** and the peak single RGBA raster was **75,776 bytes**. The
|
||||||
completed in **184.659 s**, with 64 coarse filled pairs, 64 coarse contour pairs, 42 fine filled pairs,
|
full StructureRoll test completed in **243.649 s**, with 64 coarse filled pairs, 64 coarse contour
|
||||||
42 fine contour pairs, and 0 refusals.
|
pairs, 42 fine filled pairs, 42 fine contour pairs, and 0 refusals.
|
||||||
|
|
||||||
### 3.5 Validation in three layers, and how "good" is ever judged
|
### 3.5 Validation in three layers, and how "good" is ever judged
|
||||||
|
|
||||||
@@ -696,7 +747,10 @@ But *good* never has to be defined, only **collected**:
|
|||||||
⚠️ **Known risk: convergence to a comfortable middle.** Promote only what you already liked, keep
|
⚠️ **Known risk: convergence to a comfortable middle.** Promote only what you already liked, keep
|
||||||
blending near the corpus, and the system slowly stops surprising you. Same failure mode as the
|
blending near the corpus, and the system slowly stops surprising you. Same failure mode as the
|
||||||
build-vote pool, same counter: keep extrapolating past the corpus, and keep some unrated weird
|
build-vote pool, same counter: keep extrapolating past the corpus, and keep some unrated weird
|
||||||
candidates in circulation.
|
candidates in circulation. The first five-season promotion measurement did **not** collapse: mean
|
||||||
|
normalized spread ended 0.047089 wider than it started, but survival was not monotonic. That is one
|
||||||
|
seed and one provisional policy, not a dispensation from the risk; the next policy should reserve a
|
||||||
|
reviewable outlier quota instead of assuming survivor promotion preserves surprise.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -981,13 +1035,21 @@ roughness and should stay late in the stack.
|
|||||||
- ✅ **Structure roll (§3.2, Tier 4b)** — serialisable recipes, root polarity, declaration-derived
|
- ✅ **Structure roll (§3.2, Tier 4b)** — serialisable recipes, root polarity, declaration-derived
|
||||||
resource-safe modifier legality, mandatory posts, 64-candidate measurement, and direct box-verdict
|
resource-safe modifier legality, mandatory posts, 64-candidate measurement, and direct box-verdict
|
||||||
brute force are built; the bootstrap follow-up is 42/64 survival with 0 invalid recipes and 0
|
brute force are built; the bootstrap follow-up is 42/64 survival with 0 invalid recipes and 0
|
||||||
violations, plus 64 rendered preview pairs and an index.html contact sheet.
|
violations, plus 64 rendered preview pairs and an index.html contact sheet. The fine survivor ROI
|
||||||
|
is centred on `LargestComponentPoint` and labelled largest open space; the final before/after
|
||||||
|
blank-plan count is 6/42 → 3/42.
|
||||||
- ⬜ Reject-and-resample driven by Tier 2 (§3.5) · the **offline season pipeline** with Jahni's review
|
- ⬜ Reject-and-resample driven by Tier 2 (§3.5) · the **offline season pipeline** with Jahni's review
|
||||||
and veto (§3.1).
|
and veto (§3.1).
|
||||||
|
|
||||||
### Tier 5 — the long game
|
### Tier 5 — the long game
|
||||||
**Promotion** (good strates rejoin the corpus) · theme and tag draws for materials, creatures and audio ·
|
- ✅ **Promotion** (good strates rejoin the corpus) — JSON promotable records and season manifests,
|
||||||
eventually the model. All optional, all compounding.
|
project/default/promoted provenance, fresh-load re-verification of the three gates, measured
|
||||||
|
near-duplicate rejection, per-season cap, and a deterministic five-season simulation. The run ended
|
||||||
|
at 21 corpus members (4 project, 8 default, 9 promoted); spread widened overall but survival was
|
||||||
|
non-monotonic. The owner still needs to define “good” from the preview; promotion does not invent
|
||||||
|
that judgment.
|
||||||
|
- ⬜ Theme and tag draws for materials, creatures, and audio · eventually the model. All optional,
|
||||||
|
all compounding.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1025,7 +1087,10 @@ Game-design questions live in **[GDD.md](GDD.md) §16**. These are the ones that
|
|||||||
3. **How theme coherence is drawn** — operators, materials, decorations, creatures and audio pulled as a
|
3. **How theme coherence is drawn** — operators, materials, decorations, creatures and audio pulled as a
|
||||||
compatible set. Jahni expects his first big job to be creating themes and tagging content with
|
compatible set. Jahni expects his first big job to be creating themes and tagging content with
|
||||||
multiple fitting tags.
|
multiple fitting tags.
|
||||||
4. **Where the corpus lives** and how promotion is recorded between seasons.
|
4. ✅ **Where the corpus lives** and how promotion is recorded between seasons: the cumulative store is
|
||||||
|
`Saved/ComposerPromotion/seed_0_step_8_radius_256/promoted_strates.json` in the simulation, with
|
||||||
|
`season_01_manifest.json` … `season_05_manifest.json` alongside it; the default runtime/editor
|
||||||
|
location is `Saved/VoxelForge/StrateCorpus/promoted_strates.json`.
|
||||||
5. **Whether op discovery** (promoting good sub-stacks into named reusable units) is ever worth it.
|
5. **Whether op discovery** (promoting good sub-stacks into named reusable units) is ever worth it.
|
||||||
Parked: the existing parameter space is already vastly larger than a hundred seasons could explore.
|
Parked: the existing parameter space is already vastly larger than a hundred seasons could explore.
|
||||||
**The scarce resource is judgment, not vocabulary.**
|
**The scarce resource is judgment, not vocabulary.**
|
||||||
@@ -1071,6 +1136,17 @@ them is *wrong* — a false verdict leaves no geometry and no collision behind i
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**2026-09-05 — Tier 5 + fine-ROI validation.** The specified UE 5.7 `VoxelMEditor Win64
|
||||||
|
Development` build succeeded. The full headless `VoxelForge` namespace reported **27 succeeded,
|
||||||
|
0 failed, 0 not run, 0 succeeded-with-warnings** in **613.277 s**. The focused promotion test was
|
||||||
|
green in **63.556 s** and the focused StructureRoll test was green in **243.649 s**. Promotion ended
|
||||||
|
with 9 cumulative records, fresh-load stale-metric replacement, `WorldRadiusVoxels=0`, and the table
|
||||||
|
in §3.3. The StructureRoll preview measured fine blank plan/card counts **6/42 → 3/42** and rendered
|
||||||
|
42 fine filled pairs plus 42 contour pairs with 0 refusals. No existing generation/equivalence test
|
||||||
|
failed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 11. Tier 1 status — passages aim at real open space (bounded full-point query; build not run)
|
## 11. Tier 1 status — passages aim at real open space (bounded full-point query; build not run)
|
||||||
|
|
||||||
**Updated 2026-08-29.** `VF_SuggestLandingPoint` (in `VoxelCaveMorphology`) is a **pure free function**
|
**Updated 2026-08-29.** `VF_SuggestLandingPoint` (in `VoxelCaveMorphology`) is a **pure free function**
|
||||||
|
|||||||
@@ -620,30 +620,26 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
|||||||
VF_GetProjectAssetPaths(UVoxelStrateDefinition::StaticClass());
|
VF_GetProjectAssetPaths(UVoxelStrateDefinition::StaticClass());
|
||||||
const TArray<FString> ProjectTerrainOpAssets =
|
const TArray<FString> ProjectTerrainOpAssets =
|
||||||
VF_GetProjectAssetPaths(UVoxelTerrainOpDefinition::StaticClass());
|
VF_GetProjectAssetPaths(UVoxelTerrainOpDefinition::StaticClass());
|
||||||
int32 NumProjectCorpusEntries = 0;
|
const int32 NumProjectCorpusEntries = Corpus.NumForProvenance(
|
||||||
int32 NumDefaultCorpusEntries = 0;
|
EVoxelStrateCorpusProvenance::Project);
|
||||||
|
const int32 NumDefaultCorpusEntries = Corpus.NumForProvenance(
|
||||||
|
EVoxelStrateCorpusProvenance::Default);
|
||||||
|
const int32 NumPromotedCorpusEntries = Corpus.NumForProvenance(
|
||||||
|
EVoxelStrateCorpusProvenance::Promoted);
|
||||||
FString CorpusMembership = TEXT("CORPUS MEMBERSHIP\n");
|
FString CorpusMembership = TEXT("CORPUS MEMBERSHIP\n");
|
||||||
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
|
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
|
||||||
{
|
{
|
||||||
if (VF_IsComposerDefault(Entry))
|
|
||||||
{
|
|
||||||
++NumDefaultCorpusEntries;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
++NumProjectCorpusEntries;
|
|
||||||
}
|
|
||||||
CorpusMembership += FString::Printf(TEXT("%s | %s | %s\n"),
|
CorpusMembership += FString::Printf(TEXT("%s | %s | %s\n"),
|
||||||
VF_IsComposerDefault(Entry) ? TEXT("default") : TEXT("project"),
|
VF_GetStrateCorpusProvenanceName(Entry.Provenance),
|
||||||
VF_GetStrateArchetypeName(Entry.Archetype),
|
VF_GetStrateArchetypeName(Entry.Archetype),
|
||||||
*Entry.SourcePath);
|
*Entry.SourcePath);
|
||||||
}
|
}
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Corpus membership: %d project strate assets discovered, %d project vectors loaded, "
|
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 "
|
"%d defaults, %d promoted, %d total members. Terrain-op Asset Registry count (not rolled in this "
|
||||||
"task; live fields use header defaults instead): %d."),
|
"task; live fields use header defaults instead): %d."),
|
||||||
ProjectStrateAssets.Num(), NumProjectCorpusEntries, NumDefaultCorpusEntries, Corpus.Num(),
|
ProjectStrateAssets.Num(), NumProjectCorpusEntries, NumDefaultCorpusEntries,
|
||||||
ProjectTerrainOpAssets.Num()));
|
NumPromotedCorpusEntries, Corpus.Num(), ProjectTerrainOpAssets.Num()));
|
||||||
AddInfo(CorpusMembership);
|
AddInfo(CorpusMembership);
|
||||||
for (const ECaveGeneratorType Archetype : {
|
for (const ECaveGeneratorType Archetype : {
|
||||||
ECaveGeneratorType::TunnelNetwork,
|
ECaveGeneratorType::TunnelNetwork,
|
||||||
@@ -712,11 +708,11 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
|||||||
FString SpreadTable = TEXT(
|
FString SpreadTable = TEXT(
|
||||||
"CORPUS SPREAD (population stddev; spread jitter = ±15% of max-min; near-zero spread "
|
"CORPUS SPREAD (population stddev; spread jitter = ±15% of max-min; near-zero spread "
|
||||||
"uses ±25% of field magnitude with a one-unit floor; clamps are safety only)\n"
|
"uses ±25% of field magnitude with a one-unit floor; clamps are safety only)\n"
|
||||||
"archetype | struct | field | kind | excluded | reflected | authored | op-defaults | samples | min | max | mean | stddev | clamp\n");
|
"archetype | struct | field | kind | excluded | reflected | authored | promoted | op-defaults | 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 | %s | %s | %d | %d | %d | %s | %s | %s | %s | %s\n"),
|
TEXT("%s | %s | %s | %s | %s | %s | %d | %d | %d | %d | %s | %s | %s | %s | %s\n"),
|
||||||
VF_GetStrateArchetypeName(Spread.Archetype),
|
VF_GetStrateArchetypeName(Spread.Archetype),
|
||||||
*Spread.ParamStructName,
|
*Spread.ParamStructName,
|
||||||
*Spread.FieldName,
|
*Spread.FieldName,
|
||||||
@@ -724,6 +720,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
|||||||
Spread.bExcluded ? TEXT("yes") : TEXT("no"),
|
Spread.bExcluded ? TEXT("yes") : TEXT("no"),
|
||||||
Spread.bReflected ? TEXT("yes") : TEXT("no"),
|
Spread.bReflected ? TEXT("yes") : TEXT("no"),
|
||||||
Spread.AuthoredSampleCount,
|
Spread.AuthoredSampleCount,
|
||||||
|
Spread.PromotedSampleCount,
|
||||||
Spread.DefaultSeedCount,
|
Spread.DefaultSeedCount,
|
||||||
Spread.SampleCount,
|
Spread.SampleCount,
|
||||||
*VF_FormatSpreadValue(Spread.Min),
|
*VF_FormatSpreadValue(Spread.Min),
|
||||||
|
|||||||
@@ -0,0 +1,714 @@
|
|||||||
|
// Tier 5 — promotion, corpus persistence, re-verification, and compounding simulation.
|
||||||
|
|
||||||
|
#if WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR
|
||||||
|
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "HAL/PlatformTime.h"
|
||||||
|
#include "Math/RandomStream.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
|
||||||
|
#include "VoxelForgeTestFixture.h"
|
||||||
|
#include "VoxelDensityOpStack.h"
|
||||||
|
#include "VoxelSettings.h"
|
||||||
|
#include "VoxelStrateComposer.h"
|
||||||
|
#include "VoxelStrateMeasure.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr int32 NumSimulatedSeasons = 5;
|
||||||
|
constexpr int32 CandidatesPerSeason = 24;
|
||||||
|
constexpr int32 PromotionCapPerSeason = 6;
|
||||||
|
constexpr float LargestComponentSurvivalThreshold = 0.50f;
|
||||||
|
constexpr int32 SimulationSampleStep = 8;
|
||||||
|
constexpr int32 SimulationRadius = 256;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
float VF_GetBoundarySeal(const FVoxelStrateArchetypeParams& Params,
|
||||||
|
ECaveGeneratorType Archetype)
|
||||||
|
{
|
||||||
|
switch (Archetype)
|
||||||
|
{
|
||||||
|
case ECaveGeneratorType::TunnelNetwork:
|
||||||
|
case ECaveGeneratorType::Underwater:
|
||||||
|
return Params.TunnelNetworkParams.BoundarySealThickness;
|
||||||
|
case ECaveGeneratorType::FlatPlain:
|
||||||
|
case ECaveGeneratorType::CrystalChamber:
|
||||||
|
return Params.SlabParams.BoundarySealThickness;
|
||||||
|
case ECaveGeneratorType::Maze:
|
||||||
|
return Params.MazeParams.BoundarySealThickness;
|
||||||
|
case ECaveGeneratorType::SurfaceWorld:
|
||||||
|
return Params.SurfaceParams.BoundarySealThickness;
|
||||||
|
case ECaveGeneratorType::VerticalShafts:
|
||||||
|
return Params.VerticalShaftParams.BoundarySealThickness;
|
||||||
|
case ECaveGeneratorType::FloatingIslands:
|
||||||
|
return Params.FloatingIslandParams.BoundarySealThickness;
|
||||||
|
default:
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ECaveGeneratorType VF_ArchetypeForRecipe(const FVoxelOpStackRecipe& Recipe)
|
||||||
|
{
|
||||||
|
switch (Recipe.StructuralParamBlock)
|
||||||
|
{
|
||||||
|
case EVoxelStrateParamBlock::TunnelNetwork: return ECaveGeneratorType::TunnelNetwork;
|
||||||
|
case EVoxelStrateParamBlock::Slab: return ECaveGeneratorType::FlatPlain;
|
||||||
|
case EVoxelStrateParamBlock::Maze: return ECaveGeneratorType::Maze;
|
||||||
|
case EVoxelStrateParamBlock::Surface: return ECaveGeneratorType::SurfaceWorld;
|
||||||
|
case EVoxelStrateParamBlock::VerticalShaft: return ECaveGeneratorType::VerticalShafts;
|
||||||
|
case EVoxelStrateParamBlock::FloatingIsland: return ECaveGeneratorType::FloatingIslands;
|
||||||
|
default: return ECaveGeneratorType::TunnelNetwork;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FPromotionVerifier final : public IVoxelStratePromotionVerifier
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FPromotionVerifier(const VoxelForgeTest::FTestWorld& InWorld,
|
||||||
|
int32 InTopVoxelZ,
|
||||||
|
int32 InBottomVoxelZ,
|
||||||
|
const FVector& InArrivalPoint,
|
||||||
|
const FVector& InDeparturePoint,
|
||||||
|
const FVoxelStrateMeasureSettings& InMeasureSettings,
|
||||||
|
float InLargestThreshold)
|
||||||
|
: World(InWorld)
|
||||||
|
, TopVoxelZ(InTopVoxelZ)
|
||||||
|
, BottomVoxelZ(InBottomVoxelZ)
|
||||||
|
, ArrivalPoint(InArrivalPoint)
|
||||||
|
, DeparturePoint(InDeparturePoint)
|
||||||
|
, MeasureSettings(InMeasureSettings)
|
||||||
|
, LargestThreshold(InLargestThreshold)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Verify(const FVoxelStratePromotableRecord& Record,
|
||||||
|
FVoxelStratePromotionVerification& OutVerification) const override
|
||||||
|
{
|
||||||
|
++NumVerifications;
|
||||||
|
OutVerification = FVoxelStratePromotionVerification();
|
||||||
|
|
||||||
|
if (Record.Archetype != VF_ArchetypeForRecipe(Record.Recipe))
|
||||||
|
{
|
||||||
|
OutVerification.FailureReason = TEXT("record archetype does not match recipe post block");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStrateArchetypeParams Params = Record.Params;
|
||||||
|
VF_SetAllRuntimeBounds(Params, static_cast<float>(TopVoxelZ) + 1.0f,
|
||||||
|
static_cast<float>(BottomVoxelZ));
|
||||||
|
|
||||||
|
FVoxelOpStack Stack;
|
||||||
|
FVoxelOpContext Context;
|
||||||
|
FString BuildError;
|
||||||
|
if (!VF_BuildStackFromRecipe(
|
||||||
|
Record.Recipe, Params, Record.Seed, 14.0f,
|
||||||
|
World.StrateManager.Get(), Stack, Context, &BuildError))
|
||||||
|
{
|
||||||
|
OutVerification.FailureReason = FString::Printf(
|
||||||
|
TEXT("recipe rebuild failed: %s"), *BuildError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Context.WorldRadiusVoxels != 0.0f)
|
||||||
|
{
|
||||||
|
OutVerification.FailureReason = TEXT("re-verified recipe changed WorldRadiusVoxels");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stack.PrepareChunk(Context);
|
||||||
|
FStackDensitySampler Sampler(Stack);
|
||||||
|
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
|
||||||
|
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||||
|
MeasureSettings, nullptr);
|
||||||
|
OutVerification.MeasuredMetrics = VF_SummarizeStrateMetrics(Metrics);
|
||||||
|
OutVerification.bPassedNonVacuous = Metrics.bValid
|
||||||
|
&& Metrics.NumSampled > 0 && Metrics.NumAir > 0 && Metrics.NumSolid > 0;
|
||||||
|
OutVerification.bPassedLargestComponent = OutVerification.bPassedNonVacuous
|
||||||
|
&& Metrics.LargestComponentShare >= LargestThreshold;
|
||||||
|
|
||||||
|
const FVoxelConnectivityDiagnostics Law = VF_DiagnoseConnectivityWithSampler(
|
||||||
|
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||||
|
ArrivalPoint, DeparturePoint, MeasureSettings);
|
||||||
|
OutVerification.bPassedPrimordialLaw = Law.bValid
|
||||||
|
&& Law.Result == EVoxelConnectivityResult::Connected
|
||||||
|
&& !Law.bStartSnapped && !Law.bGoalSnapped;
|
||||||
|
|
||||||
|
if (!OutVerification.PassedAllGates())
|
||||||
|
{
|
||||||
|
OutVerification.FailureReason = FString::Printf(
|
||||||
|
TEXT("fresh gates failed: non-vacuous=%s largest=%s law=%s"),
|
||||||
|
OutVerification.bPassedNonVacuous ? TEXT("yes") : TEXT("no"),
|
||||||
|
OutVerification.bPassedLargestComponent ? TEXT("yes") : TEXT("no"),
|
||||||
|
VF_ConnectivityName(Law.Result));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MeasureCorpusEntry(const FVoxelStrateCorpusEntry& Entry,
|
||||||
|
FVoxelStrateMeasuredMetrics& OutMetrics) const override;
|
||||||
|
|
||||||
|
mutable int32 NumVerifications = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const VoxelForgeTest::FTestWorld& World;
|
||||||
|
int32 TopVoxelZ = 0;
|
||||||
|
int32 BottomVoxelZ = 0;
|
||||||
|
FVector ArrivalPoint = FVector::ZeroVector;
|
||||||
|
FVector DeparturePoint = FVector::ZeroVector;
|
||||||
|
FVoxelStrateMeasureSettings MeasureSettings;
|
||||||
|
float LargestThreshold = 0.50f;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool VF_MeasureNativeCorpusEntry(
|
||||||
|
const VoxelForgeTest::FTestWorld& World,
|
||||||
|
const FVoxelStrateCorpusEntry& Entry,
|
||||||
|
int32 TopVoxelZ,
|
||||||
|
int32 BottomVoxelZ,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
|
FVoxelStrateMeasuredMetrics& OutMetrics)
|
||||||
|
{
|
||||||
|
FVoxelStrateArchetypeParams Params = Entry.ArchetypeParams;
|
||||||
|
VF_SetAllRuntimeBounds(Params, static_cast<float>(TopVoxelZ) + 1.0f,
|
||||||
|
static_cast<float>(BottomVoxelZ));
|
||||||
|
|
||||||
|
FVoxelOpStack Stack;
|
||||||
|
FVoxelOpContext Context;
|
||||||
|
if (!VF_BuildNativeStrateStackForCandidate(
|
||||||
|
Entry.Archetype, Params, 1337, 14.0f, 0.0f,
|
||||||
|
VF_GetBoundarySeal(Params, Entry.Archetype),
|
||||||
|
World.StrateManager.Get(), Stack, Context)
|
||||||
|
|| Context.WorldRadiusVoxels != 0.0f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stack.PrepareChunk(Context);
|
||||||
|
FStackDensitySampler Sampler(Stack);
|
||||||
|
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
|
||||||
|
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||||
|
Settings, nullptr);
|
||||||
|
OutMetrics = VF_SummarizeStrateMetrics(Metrics);
|
||||||
|
return Metrics.bValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FPromotionVerifier::MeasureCorpusEntry(
|
||||||
|
const FVoxelStrateCorpusEntry& Entry,
|
||||||
|
FVoxelStrateMeasuredMetrics& OutMetrics) const
|
||||||
|
{
|
||||||
|
return VF_MeasureNativeCorpusEntry(
|
||||||
|
World, Entry, TopVoxelZ, BottomVoxelZ, MeasureSettings, OutMetrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStratePromotableRecord VF_EvaluateCandidate(
|
||||||
|
const VoxelForgeTest::FTestWorld& World,
|
||||||
|
const FVoxelStrateComposerCandidate& Candidate,
|
||||||
|
int32 Season,
|
||||||
|
uint32 CorpusHash,
|
||||||
|
int32 TopVoxelZ,
|
||||||
|
int32 BottomVoxelZ,
|
||||||
|
const FVector& ArrivalPoint,
|
||||||
|
const FVector& DeparturePoint,
|
||||||
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
|
float LargestThreshold,
|
||||||
|
bool& bOutSurvivor)
|
||||||
|
{
|
||||||
|
bOutSurvivor = false;
|
||||||
|
FVoxelStratePromotableRecord Record = VF_MakeStratePromotableRecord(
|
||||||
|
Candidate, FVoxelStrateMetrics(), Season, CorpusHash, false, false, false);
|
||||||
|
if (!Candidate.bValid)
|
||||||
|
{
|
||||||
|
return Record;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStrateArchetypeParams Params = Candidate.ArchetypeParams;
|
||||||
|
VF_SetAllRuntimeBounds(Params, static_cast<float>(TopVoxelZ) + 1.0f,
|
||||||
|
static_cast<float>(BottomVoxelZ));
|
||||||
|
Record.Params = Params;
|
||||||
|
|
||||||
|
FVoxelOpStack Stack;
|
||||||
|
FVoxelOpContext Context;
|
||||||
|
FString BuildError;
|
||||||
|
if (!VF_BuildStackFromRecipe(
|
||||||
|
Candidate.Recipe, Params, Candidate.Seed, 14.0f,
|
||||||
|
World.StrateManager.Get(), Stack, Context, &BuildError)
|
||||||
|
|| Context.WorldRadiusVoxels != 0.0f)
|
||||||
|
{
|
||||||
|
return Record;
|
||||||
|
}
|
||||||
|
Stack.PrepareChunk(Context);
|
||||||
|
FStackDensitySampler Sampler(Stack);
|
||||||
|
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
|
||||||
|
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||||
|
Settings, nullptr);
|
||||||
|
const bool bNonVacuous = Metrics.bValid
|
||||||
|
&& Metrics.NumSampled > 0 && Metrics.NumAir > 0 && Metrics.NumSolid > 0;
|
||||||
|
const bool bLargestEnough = bNonVacuous
|
||||||
|
&& Metrics.LargestComponentShare >= LargestThreshold;
|
||||||
|
const FVoxelConnectivityDiagnostics Law = VF_DiagnoseConnectivityWithSampler(
|
||||||
|
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||||
|
ArrivalPoint, DeparturePoint, Settings);
|
||||||
|
const bool bLawPass = Law.bValid
|
||||||
|
&& Law.Result == EVoxelConnectivityResult::Connected
|
||||||
|
&& !Law.bStartSnapped && !Law.bGoalSnapped;
|
||||||
|
|
||||||
|
Record = VF_MakeStratePromotableRecord(
|
||||||
|
Candidate, Metrics, Season, CorpusHash,
|
||||||
|
bNonVacuous, bLargestEnough, bLawPass);
|
||||||
|
Record.Params = Params;
|
||||||
|
bOutSurvivor = bNonVacuous && bLargestEnough && bLawPass;
|
||||||
|
return Record;
|
||||||
|
}
|
||||||
|
|
||||||
|
double VF_MeanMeasuredSpread(const FVoxelStrateCorpus& Corpus,
|
||||||
|
const TArray<FVoxelStratePromotableRecord>& NewRecords,
|
||||||
|
const FVoxelStratePromotionPolicy& Policy)
|
||||||
|
{
|
||||||
|
TArray<FVoxelStrateMeasuredMetrics> Metrics;
|
||||||
|
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
|
||||||
|
{
|
||||||
|
if (Entry.bHasMeasuredMetrics && Entry.MeasuredMetrics.IsUsable())
|
||||||
|
{
|
||||||
|
Metrics.Add(Entry.MeasuredMetrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const FVoxelStratePromotableRecord& Record : NewRecords)
|
||||||
|
{
|
||||||
|
if (Record.MeasuredMetrics.IsUsable())
|
||||||
|
{
|
||||||
|
Metrics.Add(Record.MeasuredMetrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Metrics.Num() < 2)
|
||||||
|
{
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double Sum = 0.0;
|
||||||
|
int64 PairCount = 0;
|
||||||
|
for (int32 A = 0; A < Metrics.Num(); ++A)
|
||||||
|
{
|
||||||
|
for (int32 B = A + 1; B < Metrics.Num(); ++B)
|
||||||
|
{
|
||||||
|
const double Distance = VF_NormalizedMeasuredMetricDistance(
|
||||||
|
Metrics[A], Metrics[B], Policy);
|
||||||
|
if (FMath::IsFinite(Distance) && Distance < TNumericLimits<double>::Max())
|
||||||
|
{
|
||||||
|
Sum += Distance;
|
||||||
|
++PairCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PairCount > 0 ? Sum / static_cast<double>(PairCount) : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VF_PromotionBatchesIdentical(
|
||||||
|
const FVoxelStratePromotionBatchResult& A,
|
||||||
|
const FVoxelStratePromotionBatchResult& B)
|
||||||
|
{
|
||||||
|
if (A.Promoted.Num() != B.Promoted.Num())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int32 Index = 0; Index < A.Promoted.Num(); ++Index)
|
||||||
|
{
|
||||||
|
const FVoxelStratePromotableRecord& Left = A.Promoted[Index];
|
||||||
|
const FVoxelStratePromotableRecord& Right = B.Promoted[Index];
|
||||||
|
if (Left.RecordId != Right.RecordId
|
||||||
|
|| Left.Archetype != Right.Archetype
|
||||||
|
|| !VF_AreStrateStructureRecipesIdentical(Left.Recipe, Right.Recipe)
|
||||||
|
|| !VF_AreStrateArchetypeParamsBitIdentical(
|
||||||
|
Left.Params, Right.Params, Left.Archetype)
|
||||||
|
|| Left.MeasuredMetrics.AirFraction != Right.MeasuredMetrics.AirFraction
|
||||||
|
|| Left.MeasuredMetrics.LargestComponentShare
|
||||||
|
!= Right.MeasuredMetrics.LargestComponentShare)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FVoxelForgeComposerPromotionTest,
|
||||||
|
"VoxelForge.Composer.Promotion",
|
||||||
|
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FVoxelForgeComposerPromotionTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
using namespace VoxelForgeTest;
|
||||||
|
(void)Parameters;
|
||||||
|
const double TestStartSeconds = FPlatformTime::Seconds();
|
||||||
|
|
||||||
|
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
|
||||||
|
nullptr, TEXT("/Game/VoxelForge/DA_Settings.DA_Settings"));
|
||||||
|
if (AuthoredSettings == nullptr)
|
||||||
|
{
|
||||||
|
AddError(TEXT("Could not load authored settings; promotion simulation did not run."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FTestWorld World;
|
||||||
|
World.Build(1337, 2, true);
|
||||||
|
if (!World.IsValid())
|
||||||
|
{
|
||||||
|
AddError(World.WhyInvalid());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("promotion 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("promotion fixture has no candidate Z range"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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("promotion fixture has exactly one arrival mouth"), ArrivalCount, 1);
|
||||||
|
TestEqual(TEXT("promotion fixture has exactly one departure mouth"), DepartureCount, 1);
|
||||||
|
if (ArrivalCount != 1 || DepartureCount != 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStrateMeasureSettings MeasureSettings;
|
||||||
|
MeasureSettings.SampleStep = SimulationSampleStep;
|
||||||
|
MeasureSettings.RadiusInVoxels = SimulationRadius;
|
||||||
|
MeasureSettings.CenterXY = FVector2D::ZeroVector;
|
||||||
|
MeasureSettings.MaxCells = 8000000;
|
||||||
|
MeasureSettings.MaxRouteRetries = 16;
|
||||||
|
MeasureSettings.HeadroomCells = 2;
|
||||||
|
// Use one fixed window for both authored/default members and candidates. Deriving a margin
|
||||||
|
// from each member's boundary seal can make a small authored strate have no measurable
|
||||||
|
// interior, which would make the duplicate metric space member-dependent.
|
||||||
|
MeasureSettings.InteriorMarginVoxels = 0;
|
||||||
|
|
||||||
|
FPromotionVerifier Verifier(
|
||||||
|
World, TopVoxelZ, BottomVoxelZ, ArrivalPoint, DeparturePoint,
|
||||||
|
MeasureSettings, LargestComponentSurvivalThreshold);
|
||||||
|
|
||||||
|
// The fixed path is keyed by the authored corpus hash and simulation constants. Starting with
|
||||||
|
// an empty JSON object makes reruns independent of an older test artifact without deleting it.
|
||||||
|
const FString SimulationDirectory = FPaths::ProjectSavedDir() / TEXT("ComposerPromotion")
|
||||||
|
/ FString::Printf(TEXT("seed_%d_step_%d_radius_%d"),
|
||||||
|
AuthoredSettings->Seed, SimulationSampleStep, SimulationRadius);
|
||||||
|
const FString PromotedStorePath = SimulationDirectory / TEXT("promoted_strates.json");
|
||||||
|
const FString CorruptStorePath = SimulationDirectory / TEXT("corrupt_promoted_strates.json");
|
||||||
|
FString StoreReport;
|
||||||
|
TestTrue(TEXT("promotion simulation can initialise its empty JSON store"),
|
||||||
|
VF_SaveStratePromotedRecords(
|
||||||
|
PromotedStorePath, TArray<FVoxelStratePromotableRecord>(), StoreReport));
|
||||||
|
AddInfo(StoreReport);
|
||||||
|
|
||||||
|
FVoxelStrateCorpus BaseCorpus;
|
||||||
|
FString BaseReport;
|
||||||
|
TestTrue(TEXT("promotion simulation loads its base project/default corpus"),
|
||||||
|
BaseCorpus.LoadFromAssetRegistry(BaseReport, PromotedStorePath, &Verifier));
|
||||||
|
AddInfo(BaseReport);
|
||||||
|
if (!BaseCorpus.IsValid())
|
||||||
|
{
|
||||||
|
AddError(TEXT("base corpus was invalid; promotion simulation did not run."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TMap<FString, FVoxelStrateMeasuredMetrics> BaseMetrics;
|
||||||
|
int32 NumVacuousBaseMetrics = 0;
|
||||||
|
for (const FVoxelStrateCorpusEntry& Entry : BaseCorpus.GetEntries())
|
||||||
|
{
|
||||||
|
if (Entry.Provenance == EVoxelStrateCorpusProvenance::Promoted)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TestTrue(FString::Printf(TEXT("base member %s is measured before policy selection"),
|
||||||
|
*Entry.SourcePath),
|
||||||
|
Entry.bHasMeasuredMetrics && Entry.MeasuredMetrics.bValid);
|
||||||
|
if (Entry.bHasMeasuredMetrics && !Entry.MeasuredMetrics.IsUsable())
|
||||||
|
{
|
||||||
|
++NumVacuousBaseMetrics;
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("base member %s measured but vacuous in the fixed promotion window: "
|
||||||
|
"air=%lld solid=%lld components=%d"),
|
||||||
|
*Entry.SourcePath, Entry.MeasuredMetrics.NumAir,
|
||||||
|
Entry.MeasuredMetrics.NumSolid, Entry.MeasuredMetrics.NumAirComponents));
|
||||||
|
}
|
||||||
|
if (Entry.bHasMeasuredMetrics && Entry.MeasuredMetrics.IsUsable())
|
||||||
|
{
|
||||||
|
BaseMetrics.Add(Entry.SourcePath, Entry.MeasuredMetrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("base project/default members have measured metrics for spread"),
|
||||||
|
BaseMetrics.Num() > 0);
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("Base metric audit for duplicate policy: %d usable members, %d measured-but-vacuous "
|
||||||
|
"members; vacuous members use exact parameter identity as the conservative fallback."),
|
||||||
|
BaseMetrics.Num(), NumVacuousBaseMetrics));
|
||||||
|
|
||||||
|
FVoxelStratePromotionPolicy Policy;
|
||||||
|
Policy.MinimumNormalizedMeasuredMetricDistance = 0.20;
|
||||||
|
Policy.MaxPromotionsPerSeason = PromotionCapPerSeason;
|
||||||
|
Policy.FeatureScaleNormalizationVoxels = 256.0f;
|
||||||
|
Policy.ClearanceNormalizationVoxels = 64.0f;
|
||||||
|
|
||||||
|
TArray<FVoxelStratePromotableRecord> AllPromoted;
|
||||||
|
FString SeasonTable = TEXT(
|
||||||
|
"season | input corpus | corpus after | project/default/promoted | survivors/candidates "
|
||||||
|
"| survival | measured spread | promoted | near-dup/hash/cap\n");
|
||||||
|
double FirstSpread = -1.0;
|
||||||
|
double LastSpread = -1.0;
|
||||||
|
int32 NumSeasonLoadsWithPromotions = 0;
|
||||||
|
|
||||||
|
for (int32 Season = 1; Season <= NumSimulatedSeasons; ++Season)
|
||||||
|
{
|
||||||
|
FVoxelStrateCorpus Corpus;
|
||||||
|
FString CorpusReport;
|
||||||
|
const bool bLoaded = Corpus.LoadFromAssetRegistry(
|
||||||
|
CorpusReport, PromotedStorePath, &Verifier);
|
||||||
|
AddInfo(FString::Printf(TEXT("Season %d corpus load: %s"), Season, *CorpusReport));
|
||||||
|
TestTrue(FString::Printf(TEXT("season %d reloads the cumulative corpus safely"), Season),
|
||||||
|
bLoaded && Corpus.IsValid());
|
||||||
|
if (!bLoaded || !Corpus.IsValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const TPair<FString, FVoxelStrateMeasuredMetrics>& Pair : BaseMetrics)
|
||||||
|
{
|
||||||
|
TestTrue(FString::Printf(TEXT("season %d restores base metric %s"),
|
||||||
|
Season, *Pair.Key),
|
||||||
|
Corpus.SetMeasuredMetrics(Pair.Key, Pair.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AllPromoted.Num() > 0)
|
||||||
|
{
|
||||||
|
++NumSeasonLoadsWithPromotions;
|
||||||
|
TestTrue(FString::Printf(TEXT("season %d re-verifies every stored promotion"), Season),
|
||||||
|
Verifier.NumVerifications >= AllPromoted.Num());
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32 InputCorpusHash = Corpus.GetContentsHash();
|
||||||
|
const uint32 SeasonSeedBits = static_cast<uint32>(AuthoredSettings->Seed)
|
||||||
|
^ (static_cast<uint32>(Season) * 0x9E3779B9u);
|
||||||
|
const int32 SeasonSeed = static_cast<int32>(SeasonSeedBits);
|
||||||
|
TArray<FVoxelStratePromotableRecord> CandidateRecords;
|
||||||
|
CandidateRecords.Reserve(CandidatesPerSeason);
|
||||||
|
int32 NumSurvivors = 0;
|
||||||
|
for (int32 CandidateIndex = 0;
|
||||||
|
CandidateIndex < CandidatesPerSeason;
|
||||||
|
++CandidateIndex)
|
||||||
|
{
|
||||||
|
const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate(
|
||||||
|
Corpus, SeasonSeed, CandidateIndex, true);
|
||||||
|
bool bSurvivor = false;
|
||||||
|
CandidateRecords.Add(VF_EvaluateCandidate(
|
||||||
|
World, Candidate, Season, InputCorpusHash, TopVoxelZ, BottomVoxelZ,
|
||||||
|
ArrivalPoint, DeparturePoint, MeasureSettings,
|
||||||
|
LargestComponentSurvivalThreshold, bSurvivor));
|
||||||
|
if (bSurvivor)
|
||||||
|
{
|
||||||
|
++NumSurvivors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVoxelStratePromotionBatchResult Batch = VF_SelectStratePromotions(
|
||||||
|
Corpus, CandidateRecords, Policy);
|
||||||
|
const FVoxelStratePromotionBatchResult RepeatBatch = VF_SelectStratePromotions(
|
||||||
|
Corpus, CandidateRecords, Policy);
|
||||||
|
TestTrue(FString::Printf(TEXT("season %d promotion selection is deterministic"), Season),
|
||||||
|
VF_PromotionBatchesIdentical(Batch, RepeatBatch));
|
||||||
|
TestTrue(FString::Printf(TEXT("season %d uses a valid explicit promotion policy"), Season),
|
||||||
|
Batch.bPolicyValid);
|
||||||
|
TestTrue(FString::Printf(TEXT("season %d respects its promotion cap"), Season),
|
||||||
|
Batch.Promoted.Num() <= PromotionCapPerSeason);
|
||||||
|
|
||||||
|
const double Spread = VF_MeanMeasuredSpread(Corpus, Batch.Promoted, Policy);
|
||||||
|
if (FirstSpread < 0.0) { FirstSpread = Spread; }
|
||||||
|
LastSpread = Spread;
|
||||||
|
|
||||||
|
const int32 ProjectCount = Corpus.NumForProvenance(EVoxelStrateCorpusProvenance::Project);
|
||||||
|
const int32 DefaultCount = Corpus.NumForProvenance(EVoxelStrateCorpusProvenance::Default);
|
||||||
|
const int32 ExistingPromotedCount = Corpus.NumForProvenance(
|
||||||
|
EVoxelStrateCorpusProvenance::Promoted);
|
||||||
|
const int32 CorpusAfter = Corpus.Num() + Batch.Promoted.Num();
|
||||||
|
const int32 PromotedAfter = ExistingPromotedCount + Batch.Promoted.Num();
|
||||||
|
const float SurvivalRate = CandidateRecords.Num() > 0
|
||||||
|
? static_cast<float>(NumSurvivors) / static_cast<float>(CandidateRecords.Num())
|
||||||
|
: 0.0f;
|
||||||
|
SeasonTable += FString::Printf(
|
||||||
|
TEXT("%d | %08x | %d | %d/%d/%d | %d/%d | %.1f%% | %.6f | %d | %d/%d/%d\n"),
|
||||||
|
Season, InputCorpusHash, CorpusAfter, ProjectCount, DefaultCount, PromotedAfter,
|
||||||
|
NumSurvivors, CandidateRecords.Num(), 100.0f * SurvivalRate, Spread,
|
||||||
|
Batch.Promoted.Num(), Batch.NumNearDuplicateRejected,
|
||||||
|
Batch.NumCorpusHashRejected, Batch.NumCapRejected);
|
||||||
|
|
||||||
|
FVoxelStrateSeasonManifest Manifest;
|
||||||
|
Manifest.Season = Season;
|
||||||
|
Manifest.Seed = SeasonSeed;
|
||||||
|
Manifest.InputCorpusHash = InputCorpusHash;
|
||||||
|
Manifest.CandidateCount = CandidateRecords.Num();
|
||||||
|
Manifest.SurvivorCount = NumSurvivors;
|
||||||
|
Manifest.PromotedCount = Batch.Promoted.Num();
|
||||||
|
Manifest.CorpusSize = CorpusAfter;
|
||||||
|
Manifest.ProjectCount = ProjectCount;
|
||||||
|
Manifest.DefaultCount = DefaultCount;
|
||||||
|
Manifest.PromotedCorpusCount = PromotedAfter;
|
||||||
|
Manifest.SurvivalRate = SurvivalRate;
|
||||||
|
Manifest.MeasuredSpread = Spread;
|
||||||
|
FString ManifestReport;
|
||||||
|
const FString ManifestPath = SimulationDirectory
|
||||||
|
/ FString::Printf(TEXT("season_%02d_manifest.json"), Season);
|
||||||
|
TestTrue(FString::Printf(TEXT("season %d manifest is written beside the store"), Season),
|
||||||
|
VF_SaveStrateSeasonManifest(ManifestPath, Manifest, ManifestReport));
|
||||||
|
AddInfo(ManifestReport);
|
||||||
|
|
||||||
|
AllPromoted.Append(Batch.Promoted);
|
||||||
|
TestTrue(FString::Printf(TEXT("season %d cumulative promoted store is written"), Season),
|
||||||
|
VF_SaveStratePromotedRecords(PromotedStorePath, AllPromoted, StoreReport));
|
||||||
|
}
|
||||||
|
|
||||||
|
AddInfo(SeasonTable);
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("Promotion policy: normalized measured-metric distance < %.2f rejects a near duplicate; "
|
||||||
|
"dimensions=(air, largest share, walkable, feature/%.0f voxels, clearance/%.0f voxels); "
|
||||||
|
"cap=%d per season; records=%d; reloads with promotions=%d."),
|
||||||
|
Policy.MinimumNormalizedMeasuredMetricDistance,
|
||||||
|
Policy.FeatureScaleNormalizationVoxels, Policy.ClearanceNormalizationVoxels,
|
||||||
|
Policy.MaxPromotionsPerSeason, AllPromoted.Num(), NumSeasonLoadsWithPromotions));
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("Measured spread is mean pairwise distance in that normalized five-dimensional space: "
|
||||||
|
"season 1=%.6f, season 5=%.6f, delta=%+.6f."),
|
||||||
|
FirstSpread, LastSpread, LastSpread - FirstSpread));
|
||||||
|
|
||||||
|
TestEqual(TEXT("five simulated seasons completed"), NumSeasonLoadsWithPromotions + 1,
|
||||||
|
NumSimulatedSeasons);
|
||||||
|
TestTrue(TEXT("promotion compounding produced at least one promoted member"),
|
||||||
|
AllPromoted.Num() > 0);
|
||||||
|
TestEqual(TEXT("promotion cap was never exceeded in cumulative output"),
|
||||||
|
FMath::Min(AllPromoted.Num(), NumSimulatedSeasons * PromotionCapPerSeason),
|
||||||
|
AllPromoted.Num());
|
||||||
|
TestTrue(TEXT("promotion corpus contains project members"),
|
||||||
|
BaseCorpus.NumForProvenance(EVoxelStrateCorpusProvenance::Project) > 0);
|
||||||
|
TestTrue(TEXT("promotion corpus contains default members"),
|
||||||
|
BaseCorpus.NumForProvenance(EVoxelStrateCorpusProvenance::Default) > 0);
|
||||||
|
|
||||||
|
// Deliberately corrupt only the stored metric. The loader must still admit the record only
|
||||||
|
// after rebuilding it, and the corpus entry must expose the fresh value rather than the JSON.
|
||||||
|
if (AllPromoted.Num() > 0)
|
||||||
|
{
|
||||||
|
FVoxelStratePromotableRecord CorruptRecord = AllPromoted[0];
|
||||||
|
const float OriginalAir = CorruptRecord.MeasuredMetrics.AirFraction;
|
||||||
|
CorruptRecord.MeasuredMetrics.AirFraction = OriginalAir > 0.5f ? 0.01f : 0.99f;
|
||||||
|
TArray<FVoxelStratePromotableRecord> CorruptRecords;
|
||||||
|
CorruptRecords.Add(CorruptRecord);
|
||||||
|
FString CorruptReport;
|
||||||
|
TestTrue(TEXT("corrupt-metric promotion store can be written for the safety test"),
|
||||||
|
VF_SaveStratePromotedRecords(CorruptStorePath, CorruptRecords, CorruptReport));
|
||||||
|
|
||||||
|
FVoxelStrateCorpus CorruptCorpus;
|
||||||
|
FString CorruptLoadReport;
|
||||||
|
TestTrue(TEXT("corrupt stored metric is reverified before corpus admission"),
|
||||||
|
CorruptCorpus.LoadFromAssetRegistry(
|
||||||
|
CorruptLoadReport, CorruptStorePath, &Verifier));
|
||||||
|
AddInfo(CorruptLoadReport);
|
||||||
|
TestTrue(TEXT("corrupt stored metric was detected and replaced"),
|
||||||
|
CorruptLoadReport.Contains(TEXT("stored metrics replaced for 1 records")));
|
||||||
|
|
||||||
|
FVoxelStratePromotionVerification ExpectedVerification;
|
||||||
|
TestTrue(TEXT("safety verifier can independently rebuild the corrupted record"),
|
||||||
|
Verifier.Verify(CorruptRecord, ExpectedVerification));
|
||||||
|
bool bFoundReverifiedEntry = false;
|
||||||
|
for (const FVoxelStrateCorpusEntry& Entry : CorruptCorpus.GetEntries())
|
||||||
|
{
|
||||||
|
if (Entry.Provenance == EVoxelStrateCorpusProvenance::Promoted
|
||||||
|
&& Entry.PromotionRecordId == CorruptRecord.RecordId)
|
||||||
|
{
|
||||||
|
bFoundReverifiedEntry = true;
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("stale metric audit %s: stored/corpus air=%.9g, fresh air=%.9g, "
|
||||||
|
"corrupted JSON air=%.9g"),
|
||||||
|
*CorruptRecord.RecordId,
|
||||||
|
Entry.MeasuredMetrics.AirFraction,
|
||||||
|
ExpectedVerification.MeasuredMetrics.AirFraction,
|
||||||
|
CorruptRecord.MeasuredMetrics.AirFraction));
|
||||||
|
TestTrue(TEXT("loaded promoted metric equals fresh measurement, not stale JSON"),
|
||||||
|
FMath::IsNearlyEqual(
|
||||||
|
Entry.MeasuredMetrics.AirFraction,
|
||||||
|
ExpectedVerification.MeasuredMetrics.AirFraction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("corrupt safety test found its promoted corpus entry"),
|
||||||
|
bFoundReverifiedEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const double TestSeconds = FPlatformTime::Seconds() - TestStartSeconds;
|
||||||
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("Promotion total runtime %.3fs; WorldRadiusVoxels=0; JSON store=%s."),
|
||||||
|
TestSeconds, *PromotedStorePath));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR
|
||||||
@@ -344,6 +344,10 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
|||||||
int32 NumFineRendered = 0;
|
int32 NumFineRendered = 0;
|
||||||
int32 NumFineContourRendered = 0;
|
int32 NumFineContourRendered = 0;
|
||||||
int32 NumFineRefused = 0;
|
int32 NumFineRefused = 0;
|
||||||
|
int32 NumFinePlanBlankBeforeCentering = 0;
|
||||||
|
int32 NumFinePlanBlankAfterCentering = 0;
|
||||||
|
int32 NumFineCardBlankBeforeCentering = 0;
|
||||||
|
int32 NumFineCardBlankAfterCentering = 0;
|
||||||
int64 TotalFineCells = 0;
|
int64 TotalFineCells = 0;
|
||||||
int64 PeakFineCaptureBytes = 0;
|
int64 PeakFineCaptureBytes = 0;
|
||||||
int64 PeakFineRasterBytes = 0;
|
int64 PeakFineRasterBytes = 0;
|
||||||
@@ -625,11 +629,50 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
|||||||
if (bCandidateSurvivor)
|
if (bCandidateSurvivor)
|
||||||
{
|
{
|
||||||
++NumFineRequested;
|
++NumFineRequested;
|
||||||
|
|
||||||
|
// Render the old origin-centred pass into the same filenames first. The temporary
|
||||||
|
// record gives an exact before count from the renderer's content-variation check; the
|
||||||
|
// corrected capture below overwrites those files. The two grids are sequential, so
|
||||||
|
// peak retained memory remains one fine capture.
|
||||||
|
FVoxelStrateSampleGrid OriginFineGrid;
|
||||||
|
const double OriginFineMeasureStartSeconds = FPlatformTime::Seconds();
|
||||||
|
const FVoxelStrateMetrics OriginFineMetrics = VF_MeasureStrateWithSampler(
|
||||||
|
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||||
|
Context.EdgeSealThickness, FineMeasureSettings, &OriginFineGrid);
|
||||||
|
FineMeasureSeconds += FPlatformTime::Seconds() - OriginFineMeasureStartSeconds;
|
||||||
|
FVoxelStratePreviewCandidate OriginFinePreview;
|
||||||
|
FString OriginFinePreviewError;
|
||||||
|
const double OriginFineRenderStartSeconds = FPlatformTime::Seconds();
|
||||||
|
const bool bOriginFineWritten = VF_WriteStratePreviewFineCandidate(
|
||||||
|
PreviewDirectory, CandidateIndex, OriginFineGrid,
|
||||||
|
FineMeasureSettings.HeadroomCells,
|
||||||
|
OriginFineMetrics.bValid ? FString() : OriginFineMetrics.RefusalReason,
|
||||||
|
OriginFinePreview, OriginFinePreviewError);
|
||||||
|
FineRenderSeconds += FPlatformTime::Seconds() - OriginFineRenderStartSeconds;
|
||||||
|
TestTrue(FString::Printf(TEXT("candidate %d origin baseline fine render records"),
|
||||||
|
CandidateIndex),
|
||||||
|
bOriginFineWritten);
|
||||||
|
if (OriginFinePreview.bFineRendered && !OriginFinePreview.bFinePlanContentVaries)
|
||||||
|
{
|
||||||
|
++NumFinePlanBlankBeforeCentering;
|
||||||
|
}
|
||||||
|
if (OriginFinePreview.bFineRendered && OriginFinePreview.bFineBlank)
|
||||||
|
{
|
||||||
|
++NumFineCardBlankBeforeCentering;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStrateMeasureSettings CenteredFineMeasureSettings = FineMeasureSettings;
|
||||||
|
if (Metrics.bValid && FMath::IsFinite(Metrics.LargestComponentPoint.X)
|
||||||
|
&& FMath::IsFinite(Metrics.LargestComponentPoint.Y))
|
||||||
|
{
|
||||||
|
CenteredFineMeasureSettings.CenterXY = FVector2D(
|
||||||
|
Metrics.LargestComponentPoint.X, Metrics.LargestComponentPoint.Y);
|
||||||
|
}
|
||||||
FVoxelStrateSampleGrid FineGrid;
|
FVoxelStrateSampleGrid FineGrid;
|
||||||
const double FineMeasureStartSeconds = FPlatformTime::Seconds();
|
const double FineMeasureStartSeconds = FPlatformTime::Seconds();
|
||||||
const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler(
|
const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler(
|
||||||
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||||
Context.EdgeSealThickness, FineMeasureSettings, &FineGrid);
|
Context.EdgeSealThickness, CenteredFineMeasureSettings, &FineGrid);
|
||||||
FineMeasureSeconds += FPlatformTime::Seconds() - FineMeasureStartSeconds;
|
FineMeasureSeconds += FPlatformTime::Seconds() - FineMeasureStartSeconds;
|
||||||
|
|
||||||
const FString FineFailureReason = FineMetrics.bValid
|
const FString FineFailureReason = FineMetrics.bValid
|
||||||
@@ -645,6 +688,14 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
|||||||
TestTrue(FString::Printf(TEXT("candidate %d fine preview records without a second renderer sample"),
|
TestTrue(FString::Printf(TEXT("candidate %d fine preview records without a second renderer sample"),
|
||||||
CandidateIndex),
|
CandidateIndex),
|
||||||
bFineWritten);
|
bFineWritten);
|
||||||
|
if (PreviewCandidate.bFineRendered && !PreviewCandidate.bFinePlanContentVaries)
|
||||||
|
{
|
||||||
|
++NumFinePlanBlankAfterCentering;
|
||||||
|
}
|
||||||
|
if (PreviewCandidate.bFineRendered && PreviewCandidate.bFineBlank)
|
||||||
|
{
|
||||||
|
++NumFineCardBlankAfterCentering;
|
||||||
|
}
|
||||||
TestTrue(FString::Printf(TEXT("candidate %d fine capture keeps scalar density"),
|
TestTrue(FString::Printf(TEXT("candidate %d fine capture keeps scalar density"),
|
||||||
CandidateIndex),
|
CandidateIndex),
|
||||||
!FineMetrics.bValid || FineGrid.HasScalarDensity());
|
!FineMetrics.bValid || FineGrid.HasScalarDensity());
|
||||||
@@ -768,11 +819,15 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
|||||||
FirstBoxViolation.IsEmpty() ? TEXT("No violations.") : *FirstBoxViolation));
|
FirstBoxViolation.IsEmpty() ? TEXT("No violations.") : *FirstBoxViolation));
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("Preview cost: coarse step=%d 64-card pass render %.3fs; fine ROI requested=%d "
|
TEXT("Preview cost: coarse step=%d 64-card pass render %.3fs; fine ROI requested=%d "
|
||||||
"(survivors=%d), rendered=%d, contour pairs=%d, refused=%d; fine sample %.3fs + "
|
"(survivors=%d), rendered=%d, contour pairs=%d, refused=%d; "
|
||||||
|
"blank plan origin=%d, blank plan largest-open-space=%d; blank card origin=%d, "
|
||||||
|
"blank card largest-open-space=%d; fine sample %.3fs + "
|
||||||
"render %.3fs; fine cells sampled=%lld; peak fine Air+Density capture=%lld bytes; "
|
"render %.3fs; fine cells sampled=%lld; peak fine Air+Density capture=%lld bytes; "
|
||||||
"peak single RGBA raster=%lld bytes; ROI step=%d radius=%d MaxCells=%d."),
|
"peak single RGBA raster=%lld bytes; ROI step=%d radius=%d MaxCells=%d."),
|
||||||
MeasureSettings.SampleStep, CoarsePreviewSeconds,
|
MeasureSettings.SampleStep, CoarsePreviewSeconds,
|
||||||
NumFineRequested, NumSurvivors, NumFineRendered, NumFineContourRendered, NumFineRefused,
|
NumFineRequested, NumSurvivors, NumFineRendered, NumFineContourRendered, NumFineRefused,
|
||||||
|
NumFinePlanBlankBeforeCentering, NumFinePlanBlankAfterCentering,
|
||||||
|
NumFineCardBlankBeforeCentering, NumFineCardBlankAfterCentering,
|
||||||
FineMeasureSeconds, FineRenderSeconds, TotalFineCells,
|
FineMeasureSeconds, FineRenderSeconds, TotalFineCells,
|
||||||
PeakFineCaptureBytes, PeakFineRasterBytes,
|
PeakFineCaptureBytes, PeakFineRasterBytes,
|
||||||
FinePreviewSettings.SampleStep, FinePreviewSettings.RadiusInVoxels,
|
FinePreviewSettings.SampleStep, FinePreviewSettings.RadiusInVoxels,
|
||||||
@@ -815,7 +870,8 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
|||||||
"vertical=%dx%d, plan=%dx%d, "
|
"vertical=%dx%d, plan=%dx%d, "
|
||||||
"plan Z chosen from each candidate's measured layer with the highest solid/air "
|
"plan Z chosen from each candidate's measured layer with the highest solid/air "
|
||||||
"boundary count (first candidate Z=%d); index=%s; first-candidate window=%s; "
|
"boundary count (first candidate Z=%d); index=%s; first-candidate window=%s; "
|
||||||
"fine previews are linked on survivor cards; image cap=512 px including 20 px "
|
"fine previews are linked on survivor cards and centred on each largest open "
|
||||||
|
"space point (never the strate midpoint); image cap=512 px including 20 px "
|
||||||
"scale footer."),
|
"scale footer."),
|
||||||
PreviewCandidates.Num(),
|
PreviewCandidates.Num(),
|
||||||
NumRenderedPairs,
|
NumRenderedPairs,
|
||||||
@@ -840,6 +896,15 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
|||||||
NumFineRequested, NumSurvivors);
|
NumFineRequested, NumSurvivors);
|
||||||
TestEqual(TEXT("fine preview outcomes account for every requested survivor"),
|
TestEqual(TEXT("fine preview outcomes account for every requested survivor"),
|
||||||
NumFineRendered + NumFineRefused, NumFineRequested);
|
NumFineRendered + NumFineRefused, NumFineRequested);
|
||||||
|
AddInfo(FString::Printf(TEXT(
|
||||||
|
"Fine blank baseline: plan projection %d/%d origin-centred versus %d/%d centred on "
|
||||||
|
"LargestComponentPoint; complete two-projection cards %d/%d versus %d/%d. A blank "
|
||||||
|
"projection/card means its filled-cell pixels were uniform (scale footer excluded), "
|
||||||
|
"not that the measurement was refused."),
|
||||||
|
NumFinePlanBlankBeforeCentering, NumFineRequested,
|
||||||
|
NumFinePlanBlankAfterCentering, NumFineRequested,
|
||||||
|
NumFineCardBlankBeforeCentering, NumFineRequested,
|
||||||
|
NumFineCardBlankAfterCentering, NumFineRequested));
|
||||||
TestEqual(TEXT("every rendered fine preview has a scalar contour pair"),
|
TestEqual(TEXT("every rendered fine preview has a scalar contour pair"),
|
||||||
NumFineContourRendered, NumFineRendered);
|
NumFineContourRendered, NumFineRendered);
|
||||||
TestTrue(TEXT("structure roller emits 64 distinct structural recipes"),
|
TestTrue(TEXT("structure roller emits 64 distinct structural recipes"),
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -177,6 +177,28 @@ namespace
|
|||||||
return bAnyWalkable ? GWalkableColor : bAnyAir ? GAirColor : GSolidColor;
|
return bAnyWalkable ? GWalkableColor : bAnyAir ? GAirColor : GSolidColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool VF_HasContentVariation(const TArray<FColor>& Pixels,
|
||||||
|
int32 Width,
|
||||||
|
int32 ContentHeight)
|
||||||
|
{
|
||||||
|
if (Width <= 0 || ContentHeight <= 0 || Pixels.Num() < Width * ContentHeight)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const FColor First = Pixels[0];
|
||||||
|
for (int32 Y = 0; Y < ContentHeight; ++Y)
|
||||||
|
{
|
||||||
|
for (int32 X = 0; X < Width; ++X)
|
||||||
|
{
|
||||||
|
if (Pixels[Y * Width + X] != First)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
FPlanSliceChoice VF_ChoosePlanSlice(const FVoxelStrateSampleGrid& Grid,
|
FPlanSliceChoice VF_ChoosePlanSlice(const FVoxelStrateSampleGrid& Grid,
|
||||||
int32 HeadroomCells)
|
int32 HeadroomCells)
|
||||||
{
|
{
|
||||||
@@ -864,6 +886,8 @@ bool VF_WriteStratePreviewFineCandidate(
|
|||||||
CandidateIndex);
|
CandidateIndex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
InOutCandidate.bFineVerticalContentVaries = VF_HasContentVariation(
|
||||||
|
Pixels, Width, Height - GPreviewScaleBarFooterPixels);
|
||||||
|
|
||||||
const bool bHasContour = FineGrid.HasScalarDensity();
|
const bool bHasContour = FineGrid.HasScalarDensity();
|
||||||
if (bHasContour)
|
if (bHasContour)
|
||||||
@@ -896,6 +920,8 @@ bool VF_WriteStratePreviewFineCandidate(
|
|||||||
CandidateIndex);
|
CandidateIndex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
InOutCandidate.bFinePlanContentVaries = VF_HasContentVariation(
|
||||||
|
Pixels, Width, Height - GPreviewScaleBarFooterPixels);
|
||||||
if (bHasContour)
|
if (bHasContour)
|
||||||
{
|
{
|
||||||
int32 ContourWidth = 0;
|
int32 ContourWidth = 0;
|
||||||
@@ -914,6 +940,8 @@ bool VF_WriteStratePreviewFineCandidate(
|
|||||||
|
|
||||||
InOutCandidate.bFineRendered = true;
|
InOutCandidate.bFineRendered = true;
|
||||||
InOutCandidate.bFineContourRendered = bHasContour;
|
InOutCandidate.bFineContourRendered = bHasContour;
|
||||||
|
InOutCandidate.bFineBlank = !InOutCandidate.bFineVerticalContentVaries
|
||||||
|
&& !InOutCandidate.bFinePlanContentVaries;
|
||||||
InOutCandidate.FinePlanSliceCellZ = Plan.CellZ;
|
InOutCandidate.FinePlanSliceCellZ = Plan.CellZ;
|
||||||
InOutCandidate.FinePlanSliceWorldZ = VF_CellWorldCoordinate(
|
InOutCandidate.FinePlanSliceWorldZ = VF_CellWorldCoordinate(
|
||||||
FineGrid.MinZ, FineGrid.MaxZ, FineGrid.SampleStep, Plan.CellZ);
|
FineGrid.MinZ, FineGrid.MaxZ, FineGrid.SampleStep, Plan.CellZ);
|
||||||
@@ -1021,8 +1049,10 @@ bool VF_WriteStratePreviewIndex(
|
|||||||
"<span><i class=\"swatch\" style=\"background:#dae1e7\"></i>air</span>"
|
"<span><i class=\"swatch\" style=\"background:#dae1e7\"></i>air</span>"
|
||||||
"<span><i class=\"swatch\" style=\"background:#f5a636\"></i>walkable cell</span>"
|
"<span><i class=\"swatch\" style=\"background:#f5a636\"></i>walkable cell</span>"
|
||||||
"<span><i class=\"swatch\" style=\"background:#4de2be\"></i>density=0 contour</span></div>\n"
|
"<span><i class=\"swatch\" style=\"background:#4de2be\"></i>density=0 contour</span></div>\n"
|
||||||
"<p class=\"note\"><strong>Fine ROI:</strong> the separate step-1 pass is requested only "
|
"<p class=\"note\"><strong>Fine ROI — largest open space:</strong> the separate step-1 pass "
|
||||||
"for selected candidates (this run selects coarse survivors). Its exact window is printed "
|
"is requested only for selected candidates (this run selects coarse survivors). Its XY "
|
||||||
|
"window is centred on the deterministic <code>LargestComponentPoint</code> from the coarse "
|
||||||
|
"air flood fill, not on the strate midpoint or the world origin. Its exact window is printed "
|
||||||
"on the card. If its cell cap is exceeded, no fine allocation is made and the refusal is "
|
"on the card. If its cell cap is exceeded, no fine allocation is made and the refusal is "
|
||||||
"shown on that card.</p>\n"
|
"shown on that card.</p>\n"
|
||||||
"<p class=\"note\">Ordering: survivors first, then each group by descending distance from "
|
"<p class=\"note\">Ordering: survivors first, then each group by descending distance from "
|
||||||
@@ -1115,7 +1145,7 @@ bool VF_WriteStratePreviewIndex(
|
|||||||
Html += FString::Printf(TEXT("<div class=\"no-image\">No coarse image: %s</div>\n"),
|
Html += FString::Printf(TEXT("<div class=\"no-image\">No coarse image: %s</div>\n"),
|
||||||
*VF_HtmlEscape(Candidate.RenderFailureReason));
|
*VF_HtmlEscape(Candidate.RenderFailureReason));
|
||||||
}
|
}
|
||||||
Html += TEXT("</div>\n<div class=\"fine-section\"><h3>FINE ROI — filled cells and density=0 contour</h3>\n");
|
Html += TEXT("</div>\n<div class=\"fine-section\"><h3>FINE ROI — largest open space; filled cells and density=0 contour</h3>\n");
|
||||||
if (!Candidate.bFineRequested)
|
if (!Candidate.bFineRequested)
|
||||||
{
|
{
|
||||||
Html += TEXT("<p class=\"not-requested\">Fine ROI not requested for this candidate; it did not pass the coarse survivor screen.</p>\n");
|
Html += TEXT("<p class=\"not-requested\">Fine ROI not requested for this candidate; it did not pass the coarse survivor screen.</p>\n");
|
||||||
@@ -1132,7 +1162,7 @@ bool VF_WriteStratePreviewIndex(
|
|||||||
{
|
{
|
||||||
const FString FineWindowDescription = Candidate.FineWindow.Describe();
|
const FString FineWindowDescription = Candidate.FineWindow.Describe();
|
||||||
Html += FString::Printf(
|
Html += FString::Printf(
|
||||||
TEXT("<p class=\"window\">Exact fine ROI window: %s; slice Y=%d voxels.</p>\n"
|
TEXT("<p class=\"window\">Largest-open-space fine ROI; exact window: %s; slice Y=%d voxels.</p>\n"
|
||||||
"<div class=\"pair\"><figure><img src=\"%s\" alt=\"candidate %d fine XZ filled cells\"><figcaption>"
|
"<div class=\"pair\"><figure><img src=\"%s\" alt=\"candidate %d fine XZ filled cells\"><figcaption>"
|
||||||
"FINE ROI · filled cells · step=%d</figcaption></figure>\n"),
|
"FINE ROI · filled cells · step=%d</figcaption></figure>\n"),
|
||||||
*VF_HtmlEscape(FineWindowDescription), Candidate.FineVerticalSliceWorldY,
|
*VF_HtmlEscape(FineWindowDescription), Candidate.FineVerticalSliceWorldY,
|
||||||
@@ -1154,7 +1184,7 @@ bool VF_WriteStratePreviewIndex(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Html += FString::Printf(
|
Html += FString::Printf(
|
||||||
TEXT("<p class=\"window\">Exact fine ROI window: %s; selected Z=%d voxels.</p>\n"
|
TEXT("<p class=\"window\">Largest-open-space fine ROI; exact window: %s; selected Z=%d voxels.</p>\n"
|
||||||
"<div class=\"pair\"><figure><img src=\"%s\" alt=\"candidate %d fine XY filled cells\"><figcaption>"
|
"<div class=\"pair\"><figure><img src=\"%s\" alt=\"candidate %d fine XY filled cells\"><figcaption>"
|
||||||
"FINE ROI · filled cells · step=%d</figcaption></figure>\n"),
|
"FINE ROI · filled cells · step=%d</figcaption></figure>\n"),
|
||||||
*VF_HtmlEscape(FineWindowDescription), Candidate.FinePlanSliceWorldZ,
|
*VF_HtmlEscape(FineWindowDescription), Candidate.FinePlanSliceWorldZ,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "VoxelStrateTypes.h"
|
#include "VoxelStrateTypes.h"
|
||||||
#include "VoxelDensityOpStack.h"
|
#include "VoxelDensityOpStack.h"
|
||||||
|
#include "VoxelStrateMeasure.h"
|
||||||
|
|
||||||
#include "VoxelStrateComposer.generated.h"
|
#include "VoxelStrateComposer.generated.h"
|
||||||
|
|
||||||
@@ -142,6 +143,7 @@ struct VOXELFORGE_API FVoxelStrateFieldSpread
|
|||||||
// Actual authored corpus samples and synthetic per-operation default samples are kept
|
// Actual authored corpus samples and synthetic per-operation default samples are kept
|
||||||
// separate in the report. SampleCount is their effective total used for the statistics.
|
// separate in the report. SampleCount is their effective total used for the statistics.
|
||||||
int32 AuthoredSampleCount = 0;
|
int32 AuthoredSampleCount = 0;
|
||||||
|
int32 PromotedSampleCount = 0;
|
||||||
int32 DefaultSeedCount = 0;
|
int32 DefaultSeedCount = 0;
|
||||||
int32 SampleCount = 0;
|
int32 SampleCount = 0;
|
||||||
double Min = 0.0;
|
double Min = 0.0;
|
||||||
@@ -157,6 +159,54 @@ struct VOXELFORGE_API FVoxelStrateFieldSpread
|
|||||||
double ClampMax = 0.0;
|
double ClampMax = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Where a member of the offline corpus came from. */
|
||||||
|
enum class EVoxelStrateCorpusProvenance : uint8
|
||||||
|
{
|
||||||
|
Project,
|
||||||
|
Default,
|
||||||
|
Promoted,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The finite, diffable metric summary stored with a promoted strate. */
|
||||||
|
struct VOXELFORGE_API FVoxelStrateMeasuredMetrics
|
||||||
|
{
|
||||||
|
bool bValid = false;
|
||||||
|
|
||||||
|
int64 NumSampled = 0;
|
||||||
|
int64 NumAir = 0;
|
||||||
|
int64 NumSolid = 0;
|
||||||
|
float AirFraction = 0.0f;
|
||||||
|
int32 NumAirComponents = 0;
|
||||||
|
float LargestComponentShare = 0.0f;
|
||||||
|
FVector LargestComponentPoint = FVector::ZeroVector;
|
||||||
|
int64 LargestComponentCells = 0;
|
||||||
|
int32 NumComponentsAtLeast1Pct = 0;
|
||||||
|
float WalkableFraction = 0.0f;
|
||||||
|
float MedianFeatureScale = 0.0f;
|
||||||
|
int32 MedianVerticalClearance = 0;
|
||||||
|
|
||||||
|
int32 ResolvedMarginVoxels = 0;
|
||||||
|
int32 SampledMinZ = 0;
|
||||||
|
int32 SampledMaxZ = 0;
|
||||||
|
int32 SampledNumX = 0;
|
||||||
|
int32 SampledNumY = 0;
|
||||||
|
int32 SampledNumZ = 0;
|
||||||
|
float SampledMinX = 0.0f;
|
||||||
|
float SampledMaxX = 0.0f;
|
||||||
|
float SampledMinY = 0.0f;
|
||||||
|
float SampledMaxY = 0.0f;
|
||||||
|
|
||||||
|
// This is the component-size audit trail, not a retained density grid. It is bounded by the
|
||||||
|
// number of discovered air components and is useful when reviewing a promoted record.
|
||||||
|
TArray<int64> AirComponentCells;
|
||||||
|
|
||||||
|
bool IsUsable() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Copy the measured facts that are safe and useful to persist; no sampler or grid is retained. */
|
||||||
|
VOXELFORGE_API FVoxelStrateMeasuredMetrics VF_SummarizeStrateMetrics(
|
||||||
|
const FVoxelStrateMetrics& Metrics);
|
||||||
|
|
||||||
/** Native parameter storage for every currently supported strate archetype. */
|
/** Native parameter storage for every currently supported strate archetype. */
|
||||||
struct VOXELFORGE_API FVoxelStrateArchetypeParams
|
struct VOXELFORGE_API FVoxelStrateArchetypeParams
|
||||||
{
|
{
|
||||||
@@ -187,6 +237,129 @@ struct VOXELFORGE_API FVoxelStrateCorpusEntry
|
|||||||
// use 1.0. The field remains explicit so a future offline corpus can assign weights without
|
// use 1.0. The field remains explicit so a future offline corpus can assign weights without
|
||||||
// changing the roll algorithm.
|
// changing the roll algorithm.
|
||||||
float Weight = 1.0f;
|
float Weight = 1.0f;
|
||||||
|
|
||||||
|
EVoxelStrateCorpusProvenance Provenance = EVoxelStrateCorpusProvenance::Project;
|
||||||
|
|
||||||
|
// Promoted entries retain their recipe and re-measured facts so the next policy decision can
|
||||||
|
// compare measured geometry, not only parameter-space distance. Project/default entries leave
|
||||||
|
// these fields unset unless an offline simulation explicitly measures them.
|
||||||
|
bool bHasRecipe = false;
|
||||||
|
FVoxelOpStackRecipe Recipe;
|
||||||
|
bool bHasMeasuredMetrics = false;
|
||||||
|
FVoxelStrateMeasuredMetrics MeasuredMetrics;
|
||||||
|
|
||||||
|
FString PromotionRecordId;
|
||||||
|
int32 PromotionSeason = INDEX_NONE;
|
||||||
|
int32 PromotionSeed = 0;
|
||||||
|
int32 PromotionCandidateIndex = INDEX_NONE;
|
||||||
|
uint32 PromotionInputCorpusHash = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A candidate record that is eligible to be considered for corpus promotion. */
|
||||||
|
struct VOXELFORGE_API FVoxelStratePromotableRecord
|
||||||
|
{
|
||||||
|
FString RecordId;
|
||||||
|
int32 Season = 0;
|
||||||
|
int32 Seed = 0;
|
||||||
|
int32 CandidateIndex = INDEX_NONE;
|
||||||
|
uint32 InputCorpusHash = 0;
|
||||||
|
|
||||||
|
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
|
||||||
|
FVoxelStrateArchetypeParams Params;
|
||||||
|
FVoxelOpStackRecipe Recipe;
|
||||||
|
FVoxelStrateMeasuredMetrics MeasuredMetrics;
|
||||||
|
|
||||||
|
// These are recorded evidence for review. The loader deliberately ignores the stored metric
|
||||||
|
// and gate values and asks its verifier to rebuild/re-measure the recipe instead.
|
||||||
|
bool bPassedNonVacuous = false;
|
||||||
|
bool bPassedLargestComponent = false;
|
||||||
|
bool bPassedPrimordialLaw = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Fresh validation output used by the safe promoted-record loader. */
|
||||||
|
struct VOXELFORGE_API FVoxelStratePromotionVerification
|
||||||
|
{
|
||||||
|
bool bPassedNonVacuous = false;
|
||||||
|
bool bPassedLargestComponent = false;
|
||||||
|
bool bPassedPrimordialLaw = false;
|
||||||
|
FVoxelStrateMeasuredMetrics MeasuredMetrics;
|
||||||
|
FString FailureReason;
|
||||||
|
|
||||||
|
bool PassedAllGates() const
|
||||||
|
{
|
||||||
|
return bPassedNonVacuous && bPassedLargestComponent && bPassedPrimordialLaw
|
||||||
|
&& MeasuredMetrics.IsUsable();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* World-specific re-measurement seam for promoted records.
|
||||||
|
*
|
||||||
|
* A corpus store cannot know the active fixture, mouths, or validation window. Its caller must
|
||||||
|
* provide this verifier; a null verifier means promoted records are skipped rather than trusted.
|
||||||
|
*/
|
||||||
|
class VOXELFORGE_API IVoxelStratePromotionVerifier
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IVoxelStratePromotionVerifier() = default;
|
||||||
|
virtual bool Verify(const FVoxelStratePromotableRecord& Record,
|
||||||
|
FVoxelStratePromotionVerification& OutVerification) const = 0;
|
||||||
|
|
||||||
|
// Optional companion audit for project/default members. A policy pass can then compare a
|
||||||
|
// candidate against the measured corpus rather than falling back to parameter identity. A
|
||||||
|
// world-specific verifier may return false when that fixture cannot measure the member.
|
||||||
|
virtual bool MeasureCorpusEntry(const FVoxelStrateCorpusEntry& Entry,
|
||||||
|
FVoxelStrateMeasuredMetrics& OutMetrics) const
|
||||||
|
{
|
||||||
|
(void)Entry;
|
||||||
|
(void)OutMetrics;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Explicit, provisional Tier 5 policy. The owner can change these after seeing the preview. */
|
||||||
|
struct VOXELFORGE_API FVoxelStratePromotionPolicy
|
||||||
|
{
|
||||||
|
// Five normalized measured dimensions are compared with Euclidean distance. 0.20 is large
|
||||||
|
// enough to reject close copies while still admitting genuinely different geometry; it is a
|
||||||
|
// provisional review threshold, not a claim that this is the owner's final definition of good.
|
||||||
|
double MinimumNormalizedMeasuredMetricDistance = 0.20;
|
||||||
|
int32 MaxPromotionsPerSeason = 6;
|
||||||
|
|
||||||
|
// Physical normalization for the two unbounded-in-principle metric dimensions. Air fraction,
|
||||||
|
// largest share, and walkable fraction already live in [0,1].
|
||||||
|
float FeatureScaleNormalizationVoxels = 256.0f;
|
||||||
|
float ClearanceNormalizationVoxels = 64.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Counters and accepted records from one deterministic policy pass. */
|
||||||
|
struct VOXELFORGE_API FVoxelStratePromotionBatchResult
|
||||||
|
{
|
||||||
|
bool bPolicyValid = false;
|
||||||
|
TArray<FVoxelStratePromotableRecord> Promoted;
|
||||||
|
int32 NumCandidates = 0;
|
||||||
|
int32 NumValidationRejected = 0;
|
||||||
|
int32 NumCorpusHashRejected = 0;
|
||||||
|
int32 NumNearDuplicateRejected = 0;
|
||||||
|
int32 NumCapRejected = 0;
|
||||||
|
double MinimumAcceptedDistance = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** One line of the diffable season manifest written beside the cumulative record store. */
|
||||||
|
struct VOXELFORGE_API FVoxelStrateSeasonManifest
|
||||||
|
{
|
||||||
|
int32 Season = 0;
|
||||||
|
int32 Seed = 0;
|
||||||
|
uint32 InputCorpusHash = 0;
|
||||||
|
int32 CandidateCount = 0;
|
||||||
|
int32 SurvivorCount = 0;
|
||||||
|
int32 PromotedCount = 0;
|
||||||
|
int32 CorpusSize = 0;
|
||||||
|
int32 ProjectCount = 0;
|
||||||
|
int32 DefaultCount = 0;
|
||||||
|
int32 PromotedCorpusCount = 0;
|
||||||
|
float SurvivalRate = 0.0f;
|
||||||
|
double MeasuredSpread = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Result of one deterministic roll, including provenance for the offline report. */
|
/** Result of one deterministic roll, including provenance for the offline report. */
|
||||||
@@ -243,15 +416,20 @@ struct VOXELFORGE_API FVoxelStrateComposerCandidate
|
|||||||
*
|
*
|
||||||
* `LoadFromAssetRegistry` enumerates every project UVoxelStrateDefinition through the Asset
|
* `LoadFromAssetRegistry` enumerates every project UVoxelStrateDefinition through the Asset
|
||||||
* Registry, explicitly excluding Saved/Autosaves and Saved/Cooked copies. It also adds one
|
* 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
|
* known-good default vector for every archetype/family in VoxelStrateTypes.h, then optionally
|
||||||
* only inside the exact archetype group; no cross-archetype blend is attempted.
|
* loads the cumulative promoted-record JSON store. Promoted records enter only after the caller's
|
||||||
|
* world-specific verifier rebuilds and remeasures them; with no verifier they are skipped safely.
|
||||||
|
* Parents are selected only inside the exact archetype group; no cross-archetype blend is attempted.
|
||||||
*/
|
*/
|
||||||
class VOXELFORGE_API FVoxelStrateCorpus
|
class VOXELFORGE_API FVoxelStrateCorpus
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void Reset();
|
void Reset();
|
||||||
|
|
||||||
bool LoadFromAssetRegistry(FString& OutReport);
|
bool LoadFromAssetRegistry(
|
||||||
|
FString& OutReport,
|
||||||
|
const FString& PromotedStorePath = FString(),
|
||||||
|
const IVoxelStratePromotionVerifier* PromotionVerifier = nullptr);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compatibility alias retained for callers compiled against the first Tier 4a pass. The
|
* Compatibility alias retained for callers compiled against the first Tier 4a pass. The
|
||||||
@@ -268,6 +446,15 @@ public:
|
|||||||
ECaveGeneratorType Archetype, const FVoxelStrateArchetypeParams& Params,
|
ECaveGeneratorType Archetype, const FVoxelStrateArchetypeParams& Params,
|
||||||
float Weight = 1.0f);
|
float Weight = 1.0f);
|
||||||
|
|
||||||
|
/** Load cumulative promoted records, re-verifying every record before it enters Entries. */
|
||||||
|
bool LoadPromotedRecords(const FString& StorePath,
|
||||||
|
const IVoxelStratePromotionVerifier* PromotionVerifier,
|
||||||
|
FString& OutReport);
|
||||||
|
|
||||||
|
/** Attach an offline measurement to an existing member without changing corpus identity. */
|
||||||
|
bool SetMeasuredMetrics(const FString& SourcePath,
|
||||||
|
const FVoxelStrateMeasuredMetrics& Metrics);
|
||||||
|
|
||||||
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; }
|
||||||
const FString& GetSchemaError() const { return SchemaError; }
|
const FString& GetSchemaError() const { return SchemaError; }
|
||||||
@@ -278,6 +465,7 @@ public:
|
|||||||
const TArray<FString>& GetSkippedDefinitions() const { return SkippedDefinitions; }
|
const TArray<FString>& GetSkippedDefinitions() const { return SkippedDefinitions; }
|
||||||
|
|
||||||
int32 NumForArchetype(ECaveGeneratorType Archetype) const;
|
int32 NumForArchetype(ECaveGeneratorType Archetype) const;
|
||||||
|
int32 NumForProvenance(EVoxelStrateCorpusProvenance Provenance) const;
|
||||||
|
|
||||||
const FVoxelStrateFieldSpread* FindSpread(
|
const FVoxelStrateFieldSpread* FindSpread(
|
||||||
ECaveGeneratorType Archetype, const FString& FieldName) const;
|
ECaveGeneratorType Archetype, const FString& FieldName) const;
|
||||||
@@ -296,6 +484,18 @@ public:
|
|||||||
bool AreClampMetadataAvailableAtRuntime() const { return false; }
|
bool AreClampMetadataAvailableAtRuntime() const { return false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool AddEntryInternal(const FString& SourcePath, const FString& SourceName,
|
||||||
|
ECaveGeneratorType Archetype,
|
||||||
|
const FVoxelStrateArchetypeParams& Params,
|
||||||
|
float Weight, EVoxelStrateCorpusProvenance Provenance,
|
||||||
|
const FVoxelOpStackRecipe* Recipe,
|
||||||
|
const FVoxelStrateMeasuredMetrics* Metrics,
|
||||||
|
const FString& PromotionRecordId,
|
||||||
|
int32 PromotionSeason,
|
||||||
|
int32 PromotionSeed,
|
||||||
|
int32 PromotionCandidateIndex,
|
||||||
|
uint32 PromotionInputCorpusHash,
|
||||||
|
bool bRebuild);
|
||||||
void RebuildSpreads();
|
void RebuildSpreads();
|
||||||
|
|
||||||
TArray<FVoxelStrateCorpusEntry> Entries;
|
TArray<FVoxelStrateCorpusEntry> Entries;
|
||||||
@@ -308,6 +508,8 @@ private:
|
|||||||
|
|
||||||
/** Human-readable name used by the offline report. */
|
/** Human-readable name used by the offline report. */
|
||||||
VOXELFORGE_API const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archetype);
|
VOXELFORGE_API const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archetype);
|
||||||
|
VOXELFORGE_API const TCHAR* VF_GetStrateCorpusProvenanceName(
|
||||||
|
EVoxelStrateCorpusProvenance Provenance);
|
||||||
|
|
||||||
/** Full provenance result. Pure with respect to the corpus: no global or retained RNG state. */
|
/** Full provenance result. Pure with respect to the corpus: no global or retained RNG state. */
|
||||||
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailed(
|
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailed(
|
||||||
@@ -349,6 +551,30 @@ VOXELFORGE_API bool VF_ValidateStrateCorpusFreeConstraints(
|
|||||||
float StrateHeightInVoxels,
|
float StrateHeightInVoxels,
|
||||||
FString& OutViolation);
|
FString& OutViolation);
|
||||||
|
|
||||||
|
/** Normalized Euclidean distance in the five policy metric dimensions. */
|
||||||
|
VOXELFORGE_API double VF_NormalizedMeasuredMetricDistance(
|
||||||
|
const FVoxelStrateMeasuredMetrics& A,
|
||||||
|
const FVoxelStrateMeasuredMetrics& B,
|
||||||
|
const FVoxelStratePromotionPolicy& Policy);
|
||||||
|
|
||||||
|
/** Select deterministic, validated, varied records for one season. */
|
||||||
|
VOXELFORGE_API FVoxelStratePromotionBatchResult VF_SelectStratePromotions(
|
||||||
|
const FVoxelStrateCorpus& Corpus,
|
||||||
|
const TArray<FVoxelStratePromotableRecord>& Candidates,
|
||||||
|
const FVoxelStratePromotionPolicy& Policy);
|
||||||
|
|
||||||
|
/** Write/readable cumulative JSON store; records are sorted by RecordId before writing. */
|
||||||
|
VOXELFORGE_API bool VF_SaveStratePromotedRecords(
|
||||||
|
const FString& StorePath,
|
||||||
|
const TArray<FVoxelStratePromotableRecord>& Records,
|
||||||
|
FString& OutReport);
|
||||||
|
|
||||||
|
/** Write the matching season summary beside the promoted-record store. */
|
||||||
|
VOXELFORGE_API bool VF_SaveStrateSeasonManifest(
|
||||||
|
const FString& ManifestPath,
|
||||||
|
const FVoxelStrateSeasonManifest& Manifest,
|
||||||
|
FString& OutReport);
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
/**
|
/**
|
||||||
* Roll one candidate for the editor walk-through path. Parameter rolls call
|
* Roll one candidate for the editor walk-through path. Parameter rolls call
|
||||||
@@ -373,6 +599,16 @@ VOXELFORGE_API bool VF_BuildNativeStrateStackForCandidate(
|
|||||||
const UVoxelStrateManager* StrateManager,
|
const UVoxelStrateManager* StrateManager,
|
||||||
FVoxelOpStack& OutStack,
|
FVoxelOpStack& OutStack,
|
||||||
FVoxelOpContext& OutContext);
|
FVoxelOpContext& OutContext);
|
||||||
|
|
||||||
|
/** Build the persisted record from the exact editor candidate and its just-measured gate facts. */
|
||||||
|
VOXELFORGE_API FVoxelStratePromotableRecord VF_MakeStratePromotableRecord(
|
||||||
|
const FVoxelStrateComposerCandidate& Candidate,
|
||||||
|
const FVoxelStrateMetrics& Metrics,
|
||||||
|
int32 Season,
|
||||||
|
uint32 InputCorpusHash,
|
||||||
|
bool bPassedNonVacuous,
|
||||||
|
bool bPassedLargestComponent,
|
||||||
|
bool bPassedPrimordialLaw);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/** Stable compact representation and equality/hash helpers for manifests and reports. */
|
/** Stable compact representation and equality/hash helpers for manifests and reports. */
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ struct VOXELFORGE_API FVoxelStrateFinePreviewSettings
|
|||||||
// The default is deliberately one voxel: the pass exists to expose detail that step 4 cannot.
|
// The default is deliberately one voxel: the pass exists to expose detail that step 4 cannot.
|
||||||
int32 SampleStep = 1;
|
int32 SampleStep = 1;
|
||||||
// radius=64 gives a 128x128 XY ROI and stays below the default two-million-cell cap for the
|
// radius=64 gives a 128x128 XY ROI and stays below the default two-million-cell cap for the
|
||||||
// four-chunk fixture while retaining the same origin-centred frame as the coarse sweep.
|
// four-chunk fixture. The caller may re-centre it on the deterministic largest open space
|
||||||
|
// point found by the coarse air flood fill; it is not inherently the strate midpoint.
|
||||||
int32 RadiusInVoxels = 64;
|
int32 RadiusInVoxels = 64;
|
||||||
int32 MaxCells = 2000000;
|
int32 MaxCells = 2000000;
|
||||||
FVector2D CenterXY = FVector2D::ZeroVector;
|
FVector2D CenterXY = FVector2D::ZeroVector;
|
||||||
@@ -96,6 +97,9 @@ struct VOXELFORGE_API FVoxelStratePreviewCandidate
|
|||||||
bool bFineRequested = false;
|
bool bFineRequested = false;
|
||||||
bool bFineRendered = false;
|
bool bFineRendered = false;
|
||||||
bool bFineContourRendered = false;
|
bool bFineContourRendered = false;
|
||||||
|
bool bFineVerticalContentVaries = false;
|
||||||
|
bool bFinePlanContentVaries = false;
|
||||||
|
bool bFineBlank = false;
|
||||||
|
|
||||||
float AirFraction = 0.0f;
|
float AirFraction = 0.0f;
|
||||||
float LargestComponentShare = 0.0f;
|
float LargestComponentShare = 0.0f;
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ public class VoxelForge : ModuleRules
|
|||||||
// Private dependencies - only used in our .cpp files, not exposed in headers
|
// Private dependencies - only used in our .cpp files, not exposed in headers
|
||||||
PrivateDependencyModuleNames.AddRange(new string[]
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
{
|
{
|
||||||
|
"Json", // Diffable Tier 5 promoted-record and season-manifest stores
|
||||||
"ImageWrapper", // PNG encode for the biome-map preview bake (BakeBiomePreview)
|
"ImageWrapper", // PNG encode for the biome-map preview bake (BakeBiomePreview)
|
||||||
"RHI", // Texture3D create + RHIUpdateTexture3D for the density volume (mini-sun shadows)
|
"RHI", // Texture3D create + RHIUpdateTexture3D for the density volume (mini-sun shadows)
|
||||||
"RenderCore", // ENQUEUE_RENDER_COMMAND for the volume upload
|
"RenderCore", // ENQUEUE_RENDER_COMMAND for the volume upload
|
||||||
|
|||||||
Reference in new issue
Block a user