feat(editor): walk a composed candidate in PIE + why rooms read angular
PART B -- the thing Jahni asked for: "any way to test those in game?" A candidate existed only as measurements and PNGs. Now he can stand in one. Select the VoxelWorld actor in PIE -> Live Edit|Composer -> ComposerSeed / ComposerCandidateIndex / ComposerTargetStrateIndex / bComposerRollStructure -> click Apply Composer Candidate. The button and the automation tests call the SAME roll wrapper, which was the load-bearing requirement: a second roll path could drift, and then he would be judging a different world than the contact sheet describes. A determinism sample is logged on apply (seed, index, recipe, archetype, metrics, epoch, eight-point density verdict) so drift is caught the day it happens rather than in a month. Hazards handled, since this is the first composer code to touch the live runtime path: preflight materialisation before any live mutation, generation and decoration tasks drained, PassagesVersion bumped to invalidate thread_local caches, reuse of the existing RegenerateAllChunks / Epoch invalidation rather than a new path, no Initialize() or GeneratePassages() during apply (the re-entrancy trap), DiffLayer locks untouched, and custom recipes conservatively force ClassifyTile to Mixed. Scope is deliberate: a DENSITY-ONLY overlay. Layout height, authored passages, content, water and biome metadata stay from the existing slot; RebuildStrates() clears it. A full composed layout replacement needs a safe rebuild path that does not exist yet. Editor-only, verified: a Shipping build succeeds with the bridge excluded (96/96 actions), so §3.1's "offline only, never on a player's machine" still holds. ⚠️ UNVERIFIED BY DESIGN: a button cannot be tested headlessly and Codex was told not to claim it works. Jahni presses it and confirms the visual plus the "determinism=PASS" log line. PART A -- why rooms read angular, measured not guessed. One of three room shapes is VoxelSDF::RoundedBox: ShapeA = (.8 Rxy, .8 Rxy, .8 Rz) ShapeR = .25 Rxy so rounding is 31% of the horizontal half-extent but 78% of the vertical. Corners are rounded; the broad planar FACES survive. SmoothMin (blend 4) softens room/tunnel junctions not isolated corners; the cave warp bends geometry but preserves flat regions; roughness is surface-scale only. The name is accurate -- "rounded box" is not "blob". NOT ACTED ON. Jahni corrected his own read: the angular archetype is Maze (lattice source, rectilinear BY DESIGN and correct), and TunnelNetwork is "alright-ish". Codex's recommendation is recorded for if he ever wants it: ~5-10% boxes, 10-20% capsules, rest ellipsoids, or a larger ShapeR. Also closed REVIEW_FINDINGS EVoxelPassageType/Style as WONTFIX, enacting a 2026-08-16 judgement that recommended closure and was left open. Build clean (Editor + Shipping), 25 succeeded / 0 failed, run by me. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
90c40c8d5a
commit
d717bc74ff
13 files changed
+1182
-32
No files matched your search
@@ -118,6 +118,15 @@ Provided per chunk by `StrateManager::GetDisturbanceParamsForChunk`.
|
|||||||
**If you add a connectivity rule with longer edges, the COLLECT region must still cover the
|
**If you add a connectivity rule with longer edges, the COLLECT region must still cover the
|
||||||
max edge reach, and decisions must not depend on the stored window.**
|
max edge reach, and decisions must not depend on the stored window.**
|
||||||
|
|
||||||
|
The editor composer walk-through preserves this invariant by avoiding a layout rebuild. AVoxelWorld::ApplyComposerCandidate
|
||||||
|
pauses and drains active generation, installs one temporary override on an existing slot, increments
|
||||||
|
the manager's layout/passages version, and then reuses RegenerateAllChunks to bump GenerationEpoch.
|
||||||
|
The slot's Z range and passages therefore remain stable; worker tasks copy the immutable override during
|
||||||
|
their versioned refetch. A parameter candidate uses the production native stack mapping, while a
|
||||||
|
structure candidate uses VF_BuildStackFromRecipe; custom recipes conservatively return Mixed from
|
||||||
|
ClassifyTile so no unproven box bound can skip their mesh. The bridge and its editable properties are
|
||||||
|
WITH_EDITOR/WITH_EDITORONLY_DATA only and do not exist in a shipping build.
|
||||||
|
|
||||||
### 8.5 Content scatter & water — `VoxelContentManager.h/.cpp` (NEW)
|
### 8.5 Content scatter & water — `VoxelContentManager.h/.cpp` (NEW)
|
||||||
`UVoxelContentManager` (owned by `AVoxelWorld`, game-thread). TWO INDEPENDENT subsystems:
|
`UVoxelContentManager` (owned by `AVoxelWorld`, game-thread). TWO INDEPENDENT subsystems:
|
||||||
|
|
||||||
@@ -658,6 +667,14 @@ driven by `EditorBrush*` props.
|
|||||||
### 8.11 Live tuning & debug (`AVoxelWorld`, CallInEditor / PIE)
|
### 8.11 Live tuning & debug (`AVoxelWorld`, CallInEditor / PIE)
|
||||||
- `RebuildStrates` — re-reads ALL of `VoxelSettings` and rebuilds layout/gap/passages/spine +
|
- `RebuildStrates` — re-reads ALL of `VoxelSettings` and rebuilds layout/gap/passages/spine +
|
||||||
regenerates. Use after changing those (plain `RegenerateAllChunks` keeps the old layout/passages).
|
regenerates. Use after changing those (plain `RegenerateAllChunks` keeps the old layout/passages).
|
||||||
|
- ApplyComposerCandidate — editor-only PIE walk-through for one offline candidate. Set
|
||||||
|
ComposerSeed, ComposerCandidateIndex, ComposerTargetStrateIndex (0 = topmost), and
|
||||||
|
bComposerRollStructure in Live Edit|Composer, then click Apply Composer Candidate. It uses the
|
||||||
|
shared composer roll, overlays density on the existing slot, pauses/drains workers, bumps the
|
||||||
|
manager version, and reuses RegenerateAllChunks for the epoch/re-stream. It does not rebuild
|
||||||
|
passages or change the authoritative Settings->Seed; RebuildStrates removes the overlay. The
|
||||||
|
action logs recipe, archetype, air/largest-component/walkable/feature metrics, and an eight-point
|
||||||
|
bit-level density verdict. The button itself still requires owner verification in PIE.
|
||||||
- `ValidateDeterminism` (F2) — one-click §8.4 regression test: samples chunk-boundary points under
|
- `ValidateDeterminism` (F2) — one-click §8.4 regression test: samples chunk-boundary points under
|
||||||
two different thread_local cache alignments (left-chunk warm vs right-chunk warm) + a repeat
|
two different thread_local cache alignments (left-chunk warm vs right-chunk warm) + a repeat
|
||||||
pass; every delta must be EXACTLY 0. Run it after any hot-path refactor that claims
|
pass; every delta must be EXACTLY 0. Run it after any hot-path refactor that claims
|
||||||
|
|||||||
+11
-2
@@ -413,13 +413,14 @@ Maps depth→strate at runtime; owns passages.
|
|||||||
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
||||||
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
|
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
|
||||||
| `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. |
|
| `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. |
|
||||||
| `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize`). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so editor rebuilds never serve stale data. |
|
| `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
|
||||||
| `GetStrateForChunk` | 466 | Chunk → definition. |
|
| `GetStrateForChunk` | 466 | Chunk → definition. |
|
||||||
| `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. |
|
| `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. |
|
||||||
| `UsesOperatorStackForChunk` | 559 | Chunk → should `GetDensityAt` take the operator stack? `bUseOperatorStack` on the definition **AND** archetype in the ported list — **now all 8 of 8** (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater). **That list is written down here and nowhere else.** With every archetype ported the flag is now the *only* thing that decides the path, so ticking the box is no longer a no-op anywhere — it is a real switch onto the operator stack for that strate. |
|
| `UsesOperatorStackForChunk` | 559 | Chunk → should `GetDensityAt` take the operator stack? `bUseOperatorStack` on the definition **AND** archetype in the ported list — **now all 8 of 8** (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater). **That list is written down here and nowhere else.** With every archetype ported the flag is now the *only* thing that decides the path, so ticking the box is no longer a no-op anywhere — it is a real switch onto the operator stack for that strate. |
|
||||||
| `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
| `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
||||||
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
||||||
| `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
| `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
||||||
|
| `SetComposerOverrideForStrate` / `GetComposerOverrideForChunk` | editor-only | Temporary density-only candidate overlay for one existing slot. Copies immutable params/recipe into the worker refetch path, leaves layout/content/passages intact, and bumps `PassagesVersion` to invalidate generator memos. |
|
||||||
| `BuildParamsFromDefinition` (static) | 1399 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
|
| `BuildParamsFromDefinition` (static) | 1399 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
|
||||||
|
|
||||||
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
|
||||||
@@ -462,7 +463,8 @@ selection → `FStrateGenerationParams::Lerp` or reflected native-family blend
|
|||||||
field range jitter (or the season-zero ±25% own-magnitude bootstrap for near-zero ranges) →
|
field range jitter (or the season-zero ±25% own-magnitude bootstrap for near-zero ranges) →
|
||||||
editor-reflection clamps → ordered-pair repair. Bool
|
editor-reflection clamps → ordered-pair repair. Bool
|
||||||
`bTunnelsFlowTowardOrigin` inherits from the dominant parent; integer/enum SNAP fields are not
|
`bTunnelsFlowTowardOrigin` inherits from the dominant parent; integer/enum SNAP fields are not
|
||||||
jittered. This API is not called by runtime generation. The settings audit found **one unique path**,
|
jittered. Normal runtime generation never calls this API; the editor-only AVoxelWorld bridge does so
|
||||||
|
only from its PIE button. The settings audit found **one unique path**,
|
||||||
`/Game/VoxelForge/DA_Strate3`, which explains the old one-vector corpus; the project assets were
|
`/Game/VoxelForge/DA_Strate3`, which explains the old one-vector corpus; the project assets were
|
||||||
never in that pool.
|
never in that pool.
|
||||||
|
|
||||||
@@ -475,6 +477,13 @@ 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.
|
||||||
|
|
||||||
|
Public/VoxelWorld.h + Private/VoxelWorld.cpp — ApplyComposerCandidate exposes the four
|
||||||
|
Live Edit|Composer properties and the CallInEditor button. It preflights the candidate, pauses
|
||||||
|
generation before installing the manager override, and calls the existing epoch-aware full reset.
|
||||||
|
The post-apply check compares eight densities against the shared native/recipe stack where the live
|
||||||
|
world has no extra diff, disturbance, or biome context. It reports SKIPPED rather than comparing
|
||||||
|
unlike fields.
|
||||||
|
|
||||||
**`Public/VoxelStratePreview.h` + `Private/VoxelStratePreview.cpp`** — editor/automation-only PNG
|
**`Public/VoxelStratePreview.h` + `Private/VoxelStratePreview.cpp`** — editor/automation-only PNG
|
||||||
renderer and self-contained contact sheet. It consumes `FVoxelStrateSampleGrid` from the measurement
|
renderer and self-contained contact sheet. It consumes `FVoxelStrateSampleGrid` from the measurement
|
||||||
pass, emits deterministic centre-Y XZ and data-selected XY slices via `IImageWrapper`, and shows filled-cell
|
pass, emits deterministic centre-Y XZ and data-selected XY slices via `IImageWrapper`, and shows filled-cell
|
||||||
|
|||||||
+24
-1
@@ -84,7 +84,10 @@ a model over the candidates at zero runtime cost. **And Jahni reviews and vetoes
|
|||||||
publishes** — the system does the work, he keeps the veto.
|
publishes** — the system does the work, he keeps the veto.
|
||||||
|
|
||||||
**What ships is small:** the season's seed plus the chosen strates' parameter vectors. Clients generate
|
**What ships is small:** the season's seed plus the chosen strates' parameter vectors. Clients generate
|
||||||
terrain from those, deterministically, exactly as today. **No runtime composition anywhere.**
|
terrain from those, deterministically, exactly as today. **No composition is shipped or run in the
|
||||||
|
normal game path.** The editor-only PIE inspection button described below is a deliberate debugging
|
||||||
|
exception: it overlays one already-built slot so the owner can walk a measured candidate; it is not
|
||||||
|
cooked, does not compose a season, and is cleared by RebuildStrates.
|
||||||
|
|
||||||
### 3.2 Structure: how a stack gets assembled
|
### 3.2 Structure: how a stack gets assembled
|
||||||
|
|
||||||
@@ -194,6 +197,26 @@ through the offline Tier 2 sampler; no runtime generation path calls this API. T
|
|||||||
test took **76.466 s**. The complete `VoxelForge` namespace then passed **24/24 tests (0 failed,
|
test took **76.466 s**. The complete `VoxelForge` namespace then passed **24/24 tests (0 failed,
|
||||||
0 not run)** in **339.797 s**.
|
0 not run)** in **339.797 s**.
|
||||||
|
|
||||||
|
### 3.2a Editor walk-through hand-off (not a runtime composer)
|
||||||
|
|
||||||
|
AVoxelWorld::ApplyComposerCandidate is the one editor-only bridge from this offline API to a live
|
||||||
|
PIE world. It calls VF_RollStrateCandidate(Corpus, ComposerSeed, ComposerCandidateIndex,
|
||||||
|
bComposerRollStructure). That wrapper delegates to the exact parameter/recipe calls exercised by
|
||||||
|
the offline composer tests, then installs the result
|
||||||
|
as a temporary density override on ComposerTargetStrateIndex. The layout slot's authored definition,
|
||||||
|
height, content, and generated passages are intentionally unchanged; this avoids calling
|
||||||
|
GeneratePassages while Initialize is half-built. The action pauses/drains generation, bumps the
|
||||||
|
manager layout version, and reuses RegenerateAllChunks so GenerationEpoch invalidates stale mesh
|
||||||
|
results and the normal streamer rebuilds the world.
|
||||||
|
|
||||||
|
The Details-panel properties are ComposerSeed, ComposerCandidateIndex, ComposerTargetStrateIndex,
|
||||||
|
and bComposerRollStructure, under Live Edit|Composer. The button is Apply Composer Candidate; it
|
||||||
|
is usable only during PIE. RebuildStrates clears the temporary override. A post-apply eight-point
|
||||||
|
density check compares the live generator with the same prepared candidate stack bit-for-bit when
|
||||||
|
no diff edits, disturbance post, or surface-biome context makes the standalone oracle incomplete.
|
||||||
|
The log line carries the recipe/archetype and the coarse metrics so it can be matched to a
|
||||||
|
contact-sheet row. This hand-off is editor-only and is not part of the shipped composer contract.
|
||||||
|
|
||||||
### 3.3 Parameters: the range problem
|
### 3.3 Parameters: the range problem
|
||||||
|
|
||||||
**Verified facts, not remembered:** `FStrateGenerationParams` has **73 scalar fields** (+1 bool) — not
|
**Verified facts, not remembered:** `FStrateGenerationParams` has **73 scalar fields** (+1 bool) — not
|
||||||
|
|||||||
+4
-1
@@ -90,7 +90,10 @@ Legend: ✅ verified against code · ◻️ checklist box.
|
|||||||
like Box/Capsule already did (one diff-layer + remesh dispatch). *(2026-07-04)*
|
like Box/Capsule already did (one diff-layer + remesh dispatch). *(2026-07-04)*
|
||||||
- [x] **`VoxelContentManager.cpp`** — duplicated drain/reset loops → `DrainDecoResults()` +
|
- [x] **`VoxelContentManager.cpp`** — duplicated drain/reset loops → `DrainDecoResults()` +
|
||||||
`ResetGridBuildState(FDecoGrid&)`. *(2026-07-04)*
|
`ResetGridBuildState(FDecoGrid&)`. *(2026-07-04)*
|
||||||
- [ ] **`EVoxelPassageType` vs `EVoxelPassageStyle`** — two overlapping passage-shape enums, both in
|
- [x] **`EVoxelPassageType` vs `EVoxelPassageStyle`** — ✅ **CLOSED 2026-09-04 as WONTFIX**, enacting
|
||||||
|
the 2026-08-16 judgement below, which recommended closure and was then left open by oversight. No
|
||||||
|
code change; the two enums stay.
|
||||||
|
Two overlapping passage-shape enums, both in
|
||||||
active use (11 refs). Consider consolidating to one. *(judgment call, not dead)*
|
active use (11 refs). Consider consolidating to one. *(judgment call, not dead)*
|
||||||
**JUDGED 2026-08-16 — LEAVE THEM. Recommendation: close this item rather than act on it.** They
|
**JUDGED 2026-08-16 — LEAVE THEM. Recommendation: close this item rather than act on it.** They
|
||||||
read as duplicates from the index and are not: `EVoxelPassageType` (`VoxelStrateTypes.h` ~42) is
|
read as duplicates from the index and are not: `EVoxelPassageType` (`VoxelStrateTypes.h` ~42) is
|
||||||
|
|||||||
@@ -816,8 +816,18 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
|||||||
const double RollAndMeasureStartSeconds = FPlatformTime::Seconds();
|
const double RollAndMeasureStartSeconds = FPlatformTime::Seconds();
|
||||||
for (int32 CandidateIndex = 0; CandidateIndex < NumCandidates; ++CandidateIndex)
|
for (int32 CandidateIndex = 0; CandidateIndex < NumCandidates; ++CandidateIndex)
|
||||||
{
|
{
|
||||||
|
#if WITH_EDITOR
|
||||||
|
// The PIE hand-off and this test deliberately cross the same seam. The wrapper only
|
||||||
|
// packages the exact parameter-roll call below; it must not grow a second RNG path.
|
||||||
|
const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate(
|
||||||
|
Corpus, AuthoredSettings->Seed, CandidateIndex, false);
|
||||||
|
const FVoxelStrateRollInfo& Roll = Candidate.ParameterRoll;
|
||||||
|
TestTrue(FString::Printf(TEXT("candidate %d hand-off roll is valid"), CandidateIndex),
|
||||||
|
Candidate.bValid && Roll.bValid);
|
||||||
|
#else
|
||||||
const FVoxelStrateRollInfo Roll = VF_RollStrateParamsDetailed(
|
const FVoxelStrateRollInfo Roll = VF_RollStrateParamsDetailed(
|
||||||
Corpus, AuthoredSettings->Seed, CandidateIndex);
|
Corpus, AuthoredSettings->Seed, CandidateIndex);
|
||||||
|
#endif
|
||||||
TestTrue(FString::Printf(TEXT("candidate %d roll is valid"), CandidateIndex), Roll.bValid);
|
TestTrue(FString::Printf(TEXT("candidate %d roll is valid"), CandidateIndex), Roll.bValid);
|
||||||
|
|
||||||
TotalBootstrapJitterFields += Roll.BootstrapJitterFieldCount;
|
TotalBootstrapJitterFields += Roll.BootstrapJitterFieldCount;
|
||||||
|
|||||||
@@ -367,8 +367,17 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
|||||||
const double RollAndMeasureStartSeconds = FPlatformTime::Seconds();
|
const double RollAndMeasureStartSeconds = FPlatformTime::Seconds();
|
||||||
for (int32 CandidateIndex = 0; CandidateIndex < NumCandidates; ++CandidateIndex)
|
for (int32 CandidateIndex = 0; CandidateIndex < NumCandidates; ++CandidateIndex)
|
||||||
{
|
{
|
||||||
|
#if WITH_EDITOR
|
||||||
|
// Keep the editor hand-off and the structure test on the same recipe/block roll seam.
|
||||||
|
const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate(
|
||||||
|
Corpus, AuthoredSettings->Seed, CandidateIndex, true);
|
||||||
|
TestTrue(FString::Printf(TEXT("candidate %d hand-off roll is valid"), CandidateIndex),
|
||||||
|
Candidate.bValid);
|
||||||
|
const FVoxelOpStackRecipe Recipe = Candidate.Recipe;
|
||||||
|
#else
|
||||||
const FVoxelOpStackRecipe Recipe = VF_RollStrateStructure(
|
const FVoxelOpStackRecipe Recipe = VF_RollStrateStructure(
|
||||||
AuthoredSettings->Seed, CandidateIndex);
|
AuthoredSettings->Seed, CandidateIndex);
|
||||||
|
#endif
|
||||||
const FVoxelOpStackRecipe RepeatRecipe = VF_RollStrateStructure(
|
const FVoxelOpStackRecipe RepeatRecipe = VF_RollStrateStructure(
|
||||||
AuthoredSettings->Seed, CandidateIndex);
|
AuthoredSettings->Seed, CandidateIndex);
|
||||||
TestTrue(FString::Printf(TEXT("candidate %d recipe reroll is identical"), CandidateIndex),
|
TestTrue(FString::Printf(TEXT("candidate %d recipe reroll is identical"), CandidateIndex),
|
||||||
@@ -404,11 +413,24 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
|||||||
FVoxelStrateArchetypeParams CandidateParams;
|
FVoxelStrateArchetypeParams CandidateParams;
|
||||||
bool bParameterRollsValid = true;
|
bool bParameterRollsValid = true;
|
||||||
double CandidateDistanceSquared = 0.0;
|
double CandidateDistanceSquared = 0.0;
|
||||||
for (const FBlockSpec& Spec : GBlockSpecs)
|
for (int32 BlockIndex = 0; BlockIndex < UE_ARRAY_COUNT(GBlockSpecs); ++BlockIndex)
|
||||||
{
|
{
|
||||||
|
#if WITH_EDITOR
|
||||||
|
const FBlockSpec& Spec = GBlockSpecs[BlockIndex];
|
||||||
|
if (!Candidate.StructureBlockRolls.IsValidIndex(BlockIndex))
|
||||||
|
{
|
||||||
|
bParameterRollsValid = false;
|
||||||
|
AddError(FString::Printf(TEXT("candidate %d missing parameter block %d"),
|
||||||
|
CandidateIndex, static_cast<int32>(Spec.Block)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const FVoxelStrateRollInfo& BlockRoll = Candidate.StructureBlockRolls[BlockIndex];
|
||||||
|
#else
|
||||||
|
const FBlockSpec& Spec = GBlockSpecs[BlockIndex];
|
||||||
const FVoxelStrateRollInfo BlockRoll = VF_RollStrateParamsDetailedForArchetype(
|
const FVoxelStrateRollInfo BlockRoll = VF_RollStrateParamsDetailedForArchetype(
|
||||||
Corpus, Spec.Archetype,
|
Corpus, Spec.Archetype,
|
||||||
AuthoredSettings->Seed ^ static_cast<int32>(Spec.SeedSalt), CandidateIndex);
|
AuthoredSettings->Seed ^ static_cast<int32>(Spec.SeedSalt), CandidateIndex);
|
||||||
|
#endif
|
||||||
if (!BlockRoll.bValid)
|
if (!BlockRoll.bValid)
|
||||||
{
|
{
|
||||||
bParameterRollsValid = false;
|
bParameterRollsValid = false;
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
|
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
|
||||||
#include "VoxelStats.h"
|
#include "VoxelStats.h"
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
#include "VoxelStrateComposer.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -589,6 +593,63 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
bool VF_BuildNativeStrateStackForCandidate(
|
||||||
|
ECaveGeneratorType Archetype,
|
||||||
|
const FVoxelStrateArchetypeParams& Params,
|
||||||
|
int32 InSeed,
|
||||||
|
float SpineRadius,
|
||||||
|
float InWorldRadiusVoxels,
|
||||||
|
float InEdgeSealThickness,
|
||||||
|
const UVoxelStrateManager* StrateManager,
|
||||||
|
FVoxelOpStack& OutStack,
|
||||||
|
FVoxelOpContext& OutContext)
|
||||||
|
{
|
||||||
|
FVoxelStackParamRefs Refs;
|
||||||
|
switch (Archetype)
|
||||||
|
{
|
||||||
|
case ECaveGeneratorType::FlatPlain:
|
||||||
|
case ECaveGeneratorType::CrystalChamber:
|
||||||
|
Refs.Slab = &Params.SlabParams;
|
||||||
|
break;
|
||||||
|
case ECaveGeneratorType::Maze:
|
||||||
|
Refs.Maze = &Params.MazeParams;
|
||||||
|
break;
|
||||||
|
case ECaveGeneratorType::SurfaceWorld:
|
||||||
|
// The live surface path can additionally carry biome-specific parameter arrays and a
|
||||||
|
// generator-owned biome field. The editor sanity check deliberately handles the exact
|
||||||
|
// no-biome/native case only; the caller reports a biome target as unavailable rather than
|
||||||
|
// comparing unlike fields.
|
||||||
|
Refs.Surface = &Params.SurfaceParams;
|
||||||
|
break;
|
||||||
|
case ECaveGeneratorType::VerticalShafts:
|
||||||
|
Refs.Vert = &Params.VerticalShaftParams;
|
||||||
|
break;
|
||||||
|
case ECaveGeneratorType::FloatingIslands:
|
||||||
|
Refs.Float = &Params.FloatingIslandParams;
|
||||||
|
break;
|
||||||
|
case ECaveGeneratorType::Underwater:
|
||||||
|
case ECaveGeneratorType::TunnelNetwork:
|
||||||
|
Refs.Tunnel = &Params.TunnelNetworkParams;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutContext = FVoxelOpContext();
|
||||||
|
OutContext.Seed = static_cast<uint32>(InSeed);
|
||||||
|
OutContext.LayoutVersion = StrateManager != nullptr ? StrateManager->GetLayoutVersion() : 0;
|
||||||
|
OutContext.WorldRadiusVoxels = InWorldRadiusVoxels;
|
||||||
|
OutContext.EdgeSealThickness = InEdgeSealThickness;
|
||||||
|
if (!VF_BuildOpStackForChunk(Archetype, Refs, InSeed, SpineRadius, StrateManager,
|
||||||
|
OutStack, OutContext))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
UVoxelGenerator::UVoxelGenerator()
|
UVoxelGenerator::UVoxelGenerator()
|
||||||
: DensityCacheOwnerId(GNextDensityCacheOwnerId.fetch_add(1, std::memory_order_relaxed) + 1)
|
: DensityCacheOwnerId(GNextDensityCacheOwnerId.fetch_add(1, std::memory_order_relaxed) + 1)
|
||||||
{
|
{
|
||||||
@@ -675,6 +736,15 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté.
|
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté.
|
||||||
thread_local FVoxelOpStack CP_OpStack;
|
thread_local FVoxelOpStack CP_OpStack;
|
||||||
thread_local bool CP_UseOpStack = false;
|
thread_local bool CP_UseOpStack = false;
|
||||||
|
#if WITH_EDITOR
|
||||||
|
// Editor-only composer override. The candidate recipe is copied into this worker-local
|
||||||
|
// cache on the same versioned refetch as the native params; no worker reads mutable editor
|
||||||
|
// state while the world is applying a new candidate.
|
||||||
|
thread_local bool CP_UseComposerRecipe = false;
|
||||||
|
thread_local int32 CP_ComposerSeed = 0;
|
||||||
|
thread_local FVoxelStrateArchetypeParams CP_ComposerParams;
|
||||||
|
thread_local FVoxelOpStackRecipe CP_ComposerRecipe;
|
||||||
|
#endif
|
||||||
|
|
||||||
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
|
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
|
||||||
const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId;
|
const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId;
|
||||||
@@ -691,6 +761,18 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
CP_Version = LayoutVersion;
|
CP_Version = LayoutVersion;
|
||||||
CP_Chunk = ChunkCoord;
|
CP_Chunk = ChunkCoord;
|
||||||
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
|
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
|
||||||
|
#if WITH_EDITOR
|
||||||
|
CP_UseComposerRecipe = false;
|
||||||
|
ECaveGeneratorType ComposerArchetype = ECaveGeneratorType::TunnelNetwork;
|
||||||
|
bool bComposerUseRecipe = false;
|
||||||
|
if (StrateManager->GetComposerOverrideForChunk(
|
||||||
|
ChunkCoord, CP_ComposerSeed, ComposerArchetype, CP_ComposerParams,
|
||||||
|
bComposerUseRecipe, CP_ComposerRecipe))
|
||||||
|
{
|
||||||
|
CP_GenType = ComposerArchetype;
|
||||||
|
CP_UseComposerRecipe = bComposerUseRecipe;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
switch (CP_GenType)
|
switch (CP_GenType)
|
||||||
{
|
{
|
||||||
case ECaveGeneratorType::FlatPlain:
|
case ECaveGeneratorType::FlatPlain:
|
||||||
@@ -725,6 +807,35 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
if (CP_UseOpStack)
|
if (CP_UseOpStack)
|
||||||
{
|
{
|
||||||
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
|
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
|
||||||
|
#if WITH_EDITOR
|
||||||
|
if (CP_UseComposerRecipe)
|
||||||
|
{
|
||||||
|
FVoxelOpContext ComposerContext;
|
||||||
|
if (VF_BuildStackFromRecipe(
|
||||||
|
CP_ComposerRecipe, CP_ComposerParams, Seed,
|
||||||
|
OriginSpineRadius, StrateManager, CP_OpStack, ComposerContext, nullptr))
|
||||||
|
{
|
||||||
|
ComposerContext.ChunkCoord = ChunkCoord;
|
||||||
|
ComposerContext.Step = 1;
|
||||||
|
ComposerContext.LayoutVersion = LayoutVersion;
|
||||||
|
// The recipe builder supplies the candidate's vertical seal to the
|
||||||
|
// structural post. The XY edge seal is global and uses live settings.
|
||||||
|
ComposerContext.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||||
|
ComposerContext.EdgeSealThickness = EdgeSealThickness;
|
||||||
|
CP_OpStack.PrepareChunk(ComposerContext);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The editor action validates before installing the override. Keep this
|
||||||
|
// fallback safe if a future recipe changes underneath PIE: the native
|
||||||
|
// candidate params remain available instead of yielding an uninitialised
|
||||||
|
// stack or a hole.
|
||||||
|
CP_UseOpStack = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
FVoxelOpContext OpCtx;
|
FVoxelOpContext OpCtx;
|
||||||
OpCtx.ChunkCoord = ChunkCoord;
|
OpCtx.ChunkCoord = ChunkCoord;
|
||||||
OpCtx.Seed = (uint32)Seed;
|
OpCtx.Seed = (uint32)Seed;
|
||||||
@@ -775,6 +886,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); }
|
if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention
|
// Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention
|
||||||
// MC (négatif = solide) comme les fonctions d'archétype, donc les disturbances et la couche
|
// MC (négatif = solide) comme les fonctions d'archétype, donc les disturbances et la couche
|
||||||
@@ -2784,6 +2896,16 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
|||||||
|
|
||||||
if (!StrateManager) return EVoxelTileClass::Mixed;
|
if (!StrateManager) return EVoxelTileClass::Mixed;
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
// A custom recipe is deliberately not fed to the native ClassifyBox proof yet. Returning
|
||||||
|
// Mixed keeps the live walk-through hole-safe: GenerateMesh samples the exact recipe stack,
|
||||||
|
// while no optimistic AllAir/AllSolid result can skip a candidate tile.
|
||||||
|
if (StrateManager->HasComposerRecipeOverride())
|
||||||
|
{
|
||||||
|
return EVoxelTileClass::Mixed;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
bool bCanSolid = true; // "tout le treillis est solide" encore prouvable
|
bool bCanSolid = true; // "tout le treillis est solide" encore prouvable
|
||||||
bool bCanAir = true; // "tout le treillis est air" encore prouvable
|
bool bCanAir = true; // "tout le treillis est air" encore prouvable
|
||||||
|
|
||||||
|
|||||||
@@ -2270,6 +2270,131 @@ FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index)
|
|||||||
return Recipe;
|
return Recipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct FComposerStructureBlockSpec
|
||||||
|
{
|
||||||
|
EVoxelStrateParamBlock Block;
|
||||||
|
ECaveGeneratorType Archetype;
|
||||||
|
uint32 SeedSalt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep these salts in the composer implementation so the editor action and the offline
|
||||||
|
// structure test cannot silently acquire different block streams.
|
||||||
|
static const FComposerStructureBlockSpec GComposerStructureBlocks[] =
|
||||||
|
{
|
||||||
|
{ EVoxelStrateParamBlock::TunnelNetwork, ECaveGeneratorType::TunnelNetwork, 0x1001u },
|
||||||
|
{ EVoxelStrateParamBlock::Slab, ECaveGeneratorType::FlatPlain, 0x1003u },
|
||||||
|
{ EVoxelStrateParamBlock::Maze, ECaveGeneratorType::Maze, 0x1005u },
|
||||||
|
{ EVoxelStrateParamBlock::Surface, ECaveGeneratorType::SurfaceWorld, 0x1007u },
|
||||||
|
{ EVoxelStrateParamBlock::VerticalShaft, ECaveGeneratorType::VerticalShafts, 0x1009u },
|
||||||
|
{ EVoxelStrateParamBlock::FloatingIsland, ECaveGeneratorType::FloatingIslands, 0x100Bu },
|
||||||
|
};
|
||||||
|
|
||||||
|
void VF_CopyComposerBlock(FVoxelStrateArchetypeParams& OutParams,
|
||||||
|
const FVoxelStrateRollInfo& Roll,
|
||||||
|
ECaveGeneratorType Archetype)
|
||||||
|
{
|
||||||
|
switch (Archetype)
|
||||||
|
{
|
||||||
|
case ECaveGeneratorType::TunnelNetwork:
|
||||||
|
case ECaveGeneratorType::Underwater:
|
||||||
|
OutParams.TunnelNetworkParams = Roll.ArchetypeParams.TunnelNetworkParams;
|
||||||
|
break;
|
||||||
|
case ECaveGeneratorType::FlatPlain:
|
||||||
|
case ECaveGeneratorType::CrystalChamber:
|
||||||
|
OutParams.SlabParams = Roll.ArchetypeParams.SlabParams;
|
||||||
|
break;
|
||||||
|
case ECaveGeneratorType::Maze:
|
||||||
|
OutParams.MazeParams = Roll.ArchetypeParams.MazeParams;
|
||||||
|
break;
|
||||||
|
case ECaveGeneratorType::SurfaceWorld:
|
||||||
|
OutParams.SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
|
||||||
|
break;
|
||||||
|
case ECaveGeneratorType::VerticalShafts:
|
||||||
|
OutParams.VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
|
||||||
|
break;
|
||||||
|
case ECaveGeneratorType::FloatingIslands:
|
||||||
|
OutParams.FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ECaveGeneratorType VF_ComposerArchetypeForRecipe(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStrateComposerCandidate VF_RollStrateCandidate(
|
||||||
|
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index, bool bRollStructure)
|
||||||
|
{
|
||||||
|
FVoxelStrateComposerCandidate Candidate;
|
||||||
|
Candidate.Seed = Seed;
|
||||||
|
Candidate.Index = Index;
|
||||||
|
Candidate.bStructureRoll = bRollStructure;
|
||||||
|
|
||||||
|
if (!bRollStructure)
|
||||||
|
{
|
||||||
|
// This is the exact Tier 4a call used by the parameter-roll measurement test.
|
||||||
|
Candidate.ParameterRoll = VF_RollStrateParamsDetailed(Corpus, Seed, Index);
|
||||||
|
if (!Candidate.ParameterRoll.bValid)
|
||||||
|
{
|
||||||
|
Candidate.FailureReason = Candidate.ParameterRoll.FailureReason;
|
||||||
|
return Candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Candidate.Archetype = Candidate.ParameterRoll.Archetype;
|
||||||
|
Candidate.ArchetypeParams = Candidate.ParameterRoll.ArchetypeParams;
|
||||||
|
Candidate.bValid = true;
|
||||||
|
return Candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the exact Tier 4b structure call plus the six independent native-family rolls used
|
||||||
|
// to materialise every structure candidate in the offline test.
|
||||||
|
Candidate.Recipe = VF_RollStrateStructure(Seed, Index);
|
||||||
|
Candidate.Archetype = VF_ComposerArchetypeForRecipe(Candidate.Recipe);
|
||||||
|
Candidate.StructureBlockRolls.Reserve(UE_ARRAY_COUNT(GComposerStructureBlocks));
|
||||||
|
|
||||||
|
bool bAllBlocksValid = true;
|
||||||
|
for (const FComposerStructureBlockSpec& Spec : GComposerStructureBlocks)
|
||||||
|
{
|
||||||
|
const FVoxelStrateRollInfo Roll = VF_RollStrateParamsDetailedForArchetype(
|
||||||
|
Corpus, Spec.Archetype, Seed ^ static_cast<int32>(Spec.SeedSalt), Index);
|
||||||
|
Candidate.StructureBlockRolls.Add(Roll);
|
||||||
|
if (!Roll.bValid)
|
||||||
|
{
|
||||||
|
bAllBlocksValid = false;
|
||||||
|
if (Candidate.FailureReason.IsEmpty())
|
||||||
|
{
|
||||||
|
Candidate.FailureReason = FString::Printf(
|
||||||
|
TEXT("parameter block %d (%s) failed: %s"),
|
||||||
|
static_cast<int32>(Spec.Block),
|
||||||
|
VF_GetStrateArchetypeName(Spec.Archetype),
|
||||||
|
*Roll.FailureReason);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
VF_CopyComposerBlock(Candidate.ArchetypeParams, Roll, Spec.Archetype);
|
||||||
|
}
|
||||||
|
|
||||||
|
Candidate.bValid = bAllBlocksValid;
|
||||||
|
return Candidate;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
FString VF_FormatStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe)
|
FString VF_FormatStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe)
|
||||||
{
|
{
|
||||||
FString Modifiers;
|
FString Modifiers;
|
||||||
|
|||||||
@@ -9,6 +9,36 @@
|
|||||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||||
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
#include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten)
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
#include "VoxelStrateComposer.h"
|
||||||
|
|
||||||
|
struct FVoxelStrateComposerSlotOverride
|
||||||
|
{
|
||||||
|
int32 CandidateSeed = 0;
|
||||||
|
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
|
||||||
|
FVoxelStrateArchetypeParams Params;
|
||||||
|
bool bUseRecipe = false;
|
||||||
|
FVoxelOpStackRecipe Recipe;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void VF_SetComposerRuntimeBounds(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Fractal Brownian Motion (layered Perlin) along a 1D parameter, ~[-1,1].
|
// Fractal Brownian Motion (layered Perlin) along a 1D parameter, ~[-1,1].
|
||||||
// Independent octaves at increasing frequency / decreasing amplitude give an organic,
|
// Independent octaves at increasing frequency / decreasing amplitude give an organic,
|
||||||
// non-repeating wander — the key to a worm that SQUIRMS instead of zig-zagging (1D) or
|
// non-repeating wander — the key to a worm that SQUIRMS instead of zig-zagging (1D) or
|
||||||
@@ -35,6 +65,11 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
}
|
}
|
||||||
|
|
||||||
StrateLayout.Empty();
|
StrateLayout.Empty();
|
||||||
|
#if WITH_EDITOR
|
||||||
|
// A full layout rebuild discards any temporary walk-through candidate. The world calls this
|
||||||
|
// under FScopedGenerationPause, so no worker can observe the map while it is being cleared.
|
||||||
|
ComposerOverrides.Reset();
|
||||||
|
#endif
|
||||||
|
|
||||||
const int32 TotalStrates = Settings->TotalStrates;
|
const int32 TotalStrates = Settings->TotalStrates;
|
||||||
|
|
||||||
@@ -240,6 +275,125 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
GeneratePassages();
|
GeneratePassages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
const FVoxelStrateComposerSlotOverride* UVoxelStrateManager::FindComposerOverride(
|
||||||
|
int32 StrateIndex) const
|
||||||
|
{
|
||||||
|
const TSharedPtr<FVoxelStrateComposerSlotOverride>* Found = ComposerOverrides.Find(StrateIndex);
|
||||||
|
return Found != nullptr ? Found->Get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVoxelStrateManager::SetComposerOverrideForStrate(
|
||||||
|
int32 StrateIndex, int32 CandidateSeed, ECaveGeneratorType Archetype,
|
||||||
|
const FVoxelStrateArchetypeParams& Params, bool bUseRecipe,
|
||||||
|
const FVoxelOpStackRecipe* Recipe, FString& OutError)
|
||||||
|
{
|
||||||
|
OutError.Reset();
|
||||||
|
|
||||||
|
const FStrateSlot* TargetSlot = nullptr;
|
||||||
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
|
{
|
||||||
|
if (Slot.StrateIndex == StrateIndex)
|
||||||
|
{
|
||||||
|
TargetSlot = &Slot;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (TargetSlot == nullptr || TargetSlot->Definition == nullptr)
|
||||||
|
{
|
||||||
|
OutError = FString::Printf(TEXT("Strate index %d is not present in the live layout."), StrateIndex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (Archetype)
|
||||||
|
{
|
||||||
|
case ECaveGeneratorType::TunnelNetwork:
|
||||||
|
case ECaveGeneratorType::FlatPlain:
|
||||||
|
case ECaveGeneratorType::CrystalChamber:
|
||||||
|
case ECaveGeneratorType::Maze:
|
||||||
|
case ECaveGeneratorType::SurfaceWorld:
|
||||||
|
case ECaveGeneratorType::VerticalShafts:
|
||||||
|
case ECaveGeneratorType::FloatingIslands:
|
||||||
|
case ECaveGeneratorType::Underwater:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
OutError = TEXT("The composer returned an unsupported strate archetype.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bUseRecipe && Recipe == nullptr)
|
||||||
|
{
|
||||||
|
OutError = TEXT("A structure candidate did not provide a recipe.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedPtr<FVoxelStrateComposerSlotOverride> Override =
|
||||||
|
MakeShared<FVoxelStrateComposerSlotOverride>();
|
||||||
|
Override->CandidateSeed = CandidateSeed;
|
||||||
|
Override->Archetype = Archetype;
|
||||||
|
Override->Params = Params;
|
||||||
|
VF_SetComposerRuntimeBounds(
|
||||||
|
Override->Params,
|
||||||
|
(float)(TargetSlot->TopChunkZ + 1) * CHUNK_SIZE,
|
||||||
|
(float)TargetSlot->BottomChunkZ * CHUNK_SIZE);
|
||||||
|
Override->bUseRecipe = bUseRecipe;
|
||||||
|
if (Recipe != nullptr)
|
||||||
|
{
|
||||||
|
Override->Recipe = *Recipe;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only one slot is overridden at a time. Keeping this map small also makes the worker-side
|
||||||
|
// copy-on-chunk-refetch cheap. Passage geometry is deliberately not regenerated: the layout
|
||||||
|
// and its passages are unchanged, and this is the same manager state used by the offline
|
||||||
|
// candidate measurement harness.
|
||||||
|
ComposerOverrides.Reset();
|
||||||
|
ComposerOverrides.Add(StrateIndex, MoveTemp(Override));
|
||||||
|
|
||||||
|
// This is both the passage-shortlist invalidation counter and the cache key used by the
|
||||||
|
// generator's per-chunk params/stack memos. The world has already paused all readers.
|
||||||
|
++PassagesVersion;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVoxelStrateManager::GetComposerOverrideForChunk(
|
||||||
|
const FIntVector& ChunkCoord, int32& OutCandidateSeed,
|
||||||
|
ECaveGeneratorType& OutArchetype, FVoxelStrateArchetypeParams& OutParams,
|
||||||
|
bool& bOutUseRecipe, FVoxelOpStackRecipe& OutRecipe) const
|
||||||
|
{
|
||||||
|
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
|
if (SlotIdx < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVoxelStrateComposerSlotOverride* Override =
|
||||||
|
FindComposerOverride(StrateLayout[SlotIdx].StrateIndex);
|
||||||
|
if (Override == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutCandidateSeed = Override->CandidateSeed;
|
||||||
|
OutArchetype = Override->Archetype;
|
||||||
|
OutParams = Override->Params;
|
||||||
|
bOutUseRecipe = Override->bUseRecipe;
|
||||||
|
OutRecipe = Override->Recipe;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVoxelStrateManager::HasComposerRecipeOverride() const
|
||||||
|
{
|
||||||
|
for (const TPair<int32, TSharedPtr<FVoxelStrateComposerSlotOverride>>& Pair : ComposerOverrides)
|
||||||
|
{
|
||||||
|
if (Pair.Value.IsValid() && Pair.Value->bUseRecipe)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// PASSAGE GENERATION
|
// PASSAGE GENERATION
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -935,6 +1089,14 @@ ECaveGeneratorType UVoxelStrateManager::GetGeneratorTypeForChunk(const FIntVecto
|
|||||||
return ECaveGeneratorType::TunnelNetwork;
|
return ECaveGeneratorType::TunnelNetwork;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
if (const FVoxelStrateComposerSlotOverride* Override =
|
||||||
|
FindComposerOverride(StrateLayout[SlotIdx].StrateIndex))
|
||||||
|
{
|
||||||
|
return Override->Archetype;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return StrateLayout[SlotIdx].Definition->GeneratorType;
|
return StrateLayout[SlotIdx].Definition->GeneratorType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,6 +1105,15 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord
|
|||||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) { return false; }
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) { return false; }
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
if (FindComposerOverride(StrateLayout[SlotIdx].StrateIndex) != nullptr)
|
||||||
|
{
|
||||||
|
// Temporary composer candidates are measured through the stack path. Do not inherit an
|
||||||
|
// unrelated asset flag from the slot being replaced.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
const UVoxelStrateDefinition* Def = StrateLayout[SlotIdx].Definition;
|
const UVoxelStrateDefinition* Def = StrateLayout[SlotIdx].Definition;
|
||||||
if (!Def->bUseOperatorStack) { return false; }
|
if (!Def->bUseOperatorStack) { return false; }
|
||||||
|
|
||||||
@@ -1018,8 +1189,23 @@ FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVecto
|
|||||||
|
|
||||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
|
|
||||||
// Copy the designer-authored slab params from the strate definition.
|
// Copy the designer-authored slab params from the strate definition, unless an editor
|
||||||
FSlabGenerationParams Result = Slot.Definition->SlabParams;
|
// composer candidate owns this slot.
|
||||||
|
FSlabGenerationParams Result;
|
||||||
|
#if WITH_EDITOR
|
||||||
|
if (const FVoxelStrateComposerSlotOverride* Override =
|
||||||
|
FindComposerOverride(Slot.StrateIndex))
|
||||||
|
{
|
||||||
|
Result = (Override->Archetype == ECaveGeneratorType::FlatPlain
|
||||||
|
|| Override->Archetype == ECaveGeneratorType::CrystalChamber)
|
||||||
|
? Override->Params.SlabParams
|
||||||
|
: Slot.Definition->SlabParams;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
Result = Slot.Definition->SlabParams;
|
||||||
|
}
|
||||||
|
|
||||||
// Fill in the runtime Z bounds (voxel coordinates, same convention as
|
// Fill in the runtime Z bounds (voxel coordinates, same convention as
|
||||||
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
||||||
@@ -1034,32 +1220,111 @@ FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVecto
|
|||||||
// PER-ARCHETYPE PARAM GETTERS
|
// PER-ARCHETYPE PARAM GETTERS
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// Each mirrors GetSlabParamsForChunk: copy designer params, fill runtime Z bounds.
|
// Each mirrors GetSlabParamsForChunk: copy designer params, fill runtime Z bounds.
|
||||||
// No cross-boundary blending — archetypes meet at Hard boundaries. A macro keeps
|
// No cross-boundary blending — archetypes meet at Hard boundaries.
|
||||||
// the boilerplate (slot lookup + fallback + Z bounds) in one place.
|
|
||||||
|
|
||||||
#define VF_ARCHETYPE_PARAMS_GETTER(FnName, StructType, DefMember) \
|
FMazeGenerationParams UVoxelStrateManager::GetMazeParamsForChunk(const FIntVector& ChunkCoord) const
|
||||||
StructType UVoxelStrateManager::FnName(const FIntVector& ChunkCoord) const \
|
{
|
||||||
{ \
|
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z); \
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition)
|
||||||
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) \
|
{
|
||||||
{ \
|
FMazeGenerationParams Empty;
|
||||||
StructType Empty; \
|
Empty.BaseDensity = -1.0f;
|
||||||
Empty.BaseDensity = -1.0f; \
|
return Empty;
|
||||||
return Empty; \
|
}
|
||||||
} \
|
|
||||||
const FStrateSlot& Slot = StrateLayout[SlotIdx]; \
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
StructType Result = Slot.Definition->DefMember; \
|
FMazeGenerationParams Result = Slot.Definition->MazeParams;
|
||||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE; \
|
#if WITH_EDITOR
|
||||||
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE; \
|
if (const FVoxelStrateComposerSlotOverride* Override = FindComposerOverride(Slot.StrateIndex))
|
||||||
return Result; \
|
{
|
||||||
|
if (Override->Archetype == ECaveGeneratorType::Maze)
|
||||||
|
{
|
||||||
|
Result = Override->Params.MazeParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
VF_ARCHETYPE_PARAMS_GETTER(GetMazeParamsForChunk, FMazeGenerationParams, MazeParams)
|
FSurfaceGenerationParams UVoxelStrateManager::GetSurfaceParamsForChunk(const FIntVector& ChunkCoord) const
|
||||||
VF_ARCHETYPE_PARAMS_GETTER(GetSurfaceParamsForChunk, FSurfaceGenerationParams, SurfaceParams)
|
{
|
||||||
VF_ARCHETYPE_PARAMS_GETTER(GetVerticalShaftParamsForChunk, FVerticalShaftParams, VerticalShaftParams)
|
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
VF_ARCHETYPE_PARAMS_GETTER(GetFloatingIslandParamsForChunk, FFloatingIslandParams, FloatingIslandParams)
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition)
|
||||||
|
{
|
||||||
|
FSurfaceGenerationParams Empty;
|
||||||
|
Empty.BaseDensity = -1.0f;
|
||||||
|
return Empty;
|
||||||
|
}
|
||||||
|
|
||||||
#undef VF_ARCHETYPE_PARAMS_GETTER
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
|
FSurfaceGenerationParams Result = Slot.Definition->SurfaceParams;
|
||||||
|
#if WITH_EDITOR
|
||||||
|
if (const FVoxelStrateComposerSlotOverride* Override = FindComposerOverride(Slot.StrateIndex))
|
||||||
|
{
|
||||||
|
if (Override->Archetype == ECaveGeneratorType::SurfaceWorld)
|
||||||
|
{
|
||||||
|
Result = Override->Params.SurfaceParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVerticalShaftParams UVoxelStrateManager::GetVerticalShaftParamsForChunk(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition)
|
||||||
|
{
|
||||||
|
FVerticalShaftParams Empty;
|
||||||
|
Empty.BaseDensity = -1.0f;
|
||||||
|
return Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
|
FVerticalShaftParams Result = Slot.Definition->VerticalShaftParams;
|
||||||
|
#if WITH_EDITOR
|
||||||
|
if (const FVoxelStrateComposerSlotOverride* Override = FindComposerOverride(Slot.StrateIndex))
|
||||||
|
{
|
||||||
|
if (Override->Archetype == ECaveGeneratorType::VerticalShafts)
|
||||||
|
{
|
||||||
|
Result = Override->Params.VerticalShaftParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
FFloatingIslandParams UVoxelStrateManager::GetFloatingIslandParamsForChunk(const FIntVector& ChunkCoord) const
|
||||||
|
{
|
||||||
|
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||||
|
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition)
|
||||||
|
{
|
||||||
|
FFloatingIslandParams Empty;
|
||||||
|
Empty.BaseDensity = -1.0f;
|
||||||
|
return Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
|
FFloatingIslandParams Result = Slot.Definition->FloatingIslandParams;
|
||||||
|
#if WITH_EDITOR
|
||||||
|
if (const FVoxelStrateComposerSlotOverride* Override = FindComposerOverride(Slot.StrateIndex))
|
||||||
|
{
|
||||||
|
if (Override->Archetype == ECaveGeneratorType::FloatingIslands)
|
||||||
|
{
|
||||||
|
Result = Override->Params.FloatingIslandParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
FBiomeContext UVoxelStrateManager::GetBiomeContextForChunk(const FIntVector& ChunkCoord) const
|
FBiomeContext UVoxelStrateManager::GetBiomeContextForChunk(const FIntVector& ChunkCoord) const
|
||||||
{
|
{
|
||||||
@@ -1156,6 +1421,20 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
if (const FVoxelStrateComposerSlotOverride* Override =
|
||||||
|
FindComposerOverride(Slot.StrateIndex))
|
||||||
|
{
|
||||||
|
FStrateGenerationParams Result = Override->Params.TunnelNetworkParams;
|
||||||
|
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * CHUNK_SIZE;
|
||||||
|
// A candidate is a hard replacement of this slot. Do not blend its rolled vector with an
|
||||||
|
// authored neighbour at a boundary; this is also how the offline fixture measures it.
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
FStrateGenerationParams BaseParams = BuildParamsFromDefinition(Slot.Definition);
|
FStrateGenerationParams BaseParams = BuildParamsFromDefinition(Slot.Definition);
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|||||||
@@ -33,6 +33,11 @@
|
|||||||
#include "Misc/Paths.h"
|
#include "Misc/Paths.h"
|
||||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
#include "VoxelStrateComposer.h"
|
||||||
|
#include "VoxelStrateMeasure.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
AVoxelWorld::AVoxelWorld()
|
AVoxelWorld::AVoxelWorld()
|
||||||
{
|
{
|
||||||
PrimaryActorTick.bCanEverTick = true;
|
PrimaryActorTick.bCanEverTick = true;
|
||||||
@@ -127,6 +132,228 @@ private:
|
|||||||
bool bAcquired = false;
|
bool bAcquired = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
void VF_SetComposerRuntimeBounds(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_ComposerBoundarySeal(const FVoxelStrateArchetypeParams& Params,
|
||||||
|
ECaveGeneratorType Archetype)
|
||||||
|
{
|
||||||
|
switch (Archetype)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
case ECaveGeneratorType::Underwater:
|
||||||
|
case ECaveGeneratorType::TunnelNetwork:
|
||||||
|
default:
|
||||||
|
return Params.TunnelNetworkParams.BoundarySealThickness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FComposerStackDensitySampler final : public IVoxelStrateDensitySampler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit FComposerStackDensitySampler(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;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool VF_BuildComposerReferenceStack(
|
||||||
|
const FVoxelStrateComposerCandidate& Candidate,
|
||||||
|
const UVoxelGenerator& Generator,
|
||||||
|
const UVoxelStrateManager& StrateManager,
|
||||||
|
int32 TargetTopChunkZ,
|
||||||
|
int32 TargetBottomChunkZ,
|
||||||
|
FVoxelOpStack& OutStack,
|
||||||
|
FVoxelOpContext& OutContext,
|
||||||
|
FString& OutError)
|
||||||
|
{
|
||||||
|
const float TopWorldZ = (float)(TargetTopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
const float BottomWorldZ = (float)TargetBottomChunkZ * CHUNK_SIZE;
|
||||||
|
FVoxelStrateArchetypeParams Params = Candidate.ArchetypeParams;
|
||||||
|
VF_SetComposerRuntimeBounds(Params, TopWorldZ, BottomWorldZ);
|
||||||
|
|
||||||
|
bool bBuilt = false;
|
||||||
|
if (Candidate.bStructureRoll)
|
||||||
|
{
|
||||||
|
bBuilt = VF_BuildStackFromRecipe(
|
||||||
|
Candidate.Recipe, Params, Generator.Seed, Generator.OriginSpineRadius,
|
||||||
|
&StrateManager, OutStack, OutContext, &OutError);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bBuilt = VF_BuildNativeStrateStackForCandidate(
|
||||||
|
Candidate.Archetype, Params, Generator.Seed, Generator.OriginSpineRadius,
|
||||||
|
Generator.WorldRadiusVoxels, Generator.EdgeSealThickness, &StrateManager,
|
||||||
|
OutStack, OutContext);
|
||||||
|
if (!bBuilt && OutError.IsEmpty())
|
||||||
|
{
|
||||||
|
OutError = TEXT("The production native candidate stack could not be materialised.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bBuilt)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The recipe builder deliberately defaults the world-edge context to 0 for offline
|
||||||
|
// measurements. A live candidate must use the actual world's edge law, while its
|
||||||
|
// vertical bounds and structural seal remain candidate-owned.
|
||||||
|
const int32 MidChunkZ = TargetBottomChunkZ
|
||||||
|
+ (TargetTopChunkZ - TargetBottomChunkZ) / 2;
|
||||||
|
OutContext.ChunkCoord = FIntVector(0, 0, MidChunkZ);
|
||||||
|
OutContext.Step = 1;
|
||||||
|
OutContext.LayoutVersion = StrateManager.GetLayoutVersion();
|
||||||
|
OutContext.WorldRadiusVoxels = Generator.WorldRadiusVoxels;
|
||||||
|
OutContext.EdgeSealThickness = Generator.EdgeSealThickness;
|
||||||
|
OutStack.PrepareChunk(OutContext);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VF_ComposerReferenceIsComparable(
|
||||||
|
const AVoxelWorld& World,
|
||||||
|
const FStrateSlot& TargetSlot,
|
||||||
|
ECaveGeneratorType Archetype,
|
||||||
|
const FIntVector& RepresentativeChunk,
|
||||||
|
FString& OutReason)
|
||||||
|
{
|
||||||
|
OutReason.Reset();
|
||||||
|
if (World.DiffLayer && World.DiffLayer->HasAnyMods())
|
||||||
|
{
|
||||||
|
OutReason = TEXT("diff-layer-modifications-present");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FStrateDisturbanceParams Disturbances =
|
||||||
|
World.StrateManager->GetDisturbanceParamsForChunk(RepresentativeChunk);
|
||||||
|
if (Disturbances.ChasmDensity > 0.0f
|
||||||
|
|| Disturbances.BridgeDensity > 0.0f
|
||||||
|
|| Disturbances.RidgeDensity > 0.0f)
|
||||||
|
{
|
||||||
|
OutReason = TEXT("live-disturbance-post-present");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The production SurfaceWorld stack may include a generator-owned biome field and
|
||||||
|
// per-biome params. The standalone candidate stack intentionally does not invent that
|
||||||
|
// context; fall back to the live metric and mark the density check as unavailable.
|
||||||
|
if (Archetype == ECaveGeneratorType::SurfaceWorld
|
||||||
|
&& TargetSlot.Definition != nullptr
|
||||||
|
&& TargetSlot.Definition->Biomes.Num() > 0)
|
||||||
|
{
|
||||||
|
OutReason = TEXT("surface-biome-context-present");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStrateMeasureSettings VF_ComposerMeasureSettings()
|
||||||
|
{
|
||||||
|
FVoxelStrateMeasureSettings Settings;
|
||||||
|
Settings.SampleStep = 4;
|
||||||
|
Settings.RadiusInVoxels = 256;
|
||||||
|
Settings.CenterXY = FVector2D::ZeroVector;
|
||||||
|
Settings.MaxCells = 8000000;
|
||||||
|
Settings.MaxRouteRetries = 16;
|
||||||
|
Settings.HeadroomCells = 2;
|
||||||
|
Settings.InteriorMarginVoxels = -1;
|
||||||
|
return Settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VF_ComposerDensitySanityCheck(
|
||||||
|
const UVoxelGenerator& Generator,
|
||||||
|
const FVoxelOpStack& ReferenceStack,
|
||||||
|
int32 TargetTopChunkZ,
|
||||||
|
int32 TargetBottomChunkZ,
|
||||||
|
float& OutMaxDelta,
|
||||||
|
int32& OutMismatches,
|
||||||
|
int32& OutSamples)
|
||||||
|
{
|
||||||
|
// Keep all probes inside one representative chunk column where possible. This exercises
|
||||||
|
// both the live thread-local refetch and the prepared reference stack without making the
|
||||||
|
// check depend on a particular room being present at a hand-picked coordinate.
|
||||||
|
static const FIntVector Offsets[] =
|
||||||
|
{
|
||||||
|
FIntVector(1, 1, -15),
|
||||||
|
FIntVector(7, 13, -7),
|
||||||
|
FIntVector(17, 3, 1),
|
||||||
|
FIntVector(29, 27, 9),
|
||||||
|
FIntVector(3, 23, 17),
|
||||||
|
FIntVector(19, 19, 25),
|
||||||
|
FIntVector(11, 29, 31),
|
||||||
|
FIntVector(27, 5, -23),
|
||||||
|
};
|
||||||
|
|
||||||
|
const int32 MidChunkZ = TargetBottomChunkZ
|
||||||
|
+ (TargetTopChunkZ - TargetBottomChunkZ) / 2;
|
||||||
|
const int32 BottomWorldZ = TargetBottomChunkZ * CHUNK_SIZE;
|
||||||
|
const int32 TopWorldZExclusive = (TargetTopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
const int32 MidWorldZ = MidChunkZ * CHUNK_SIZE + CHUNK_SIZE / 2;
|
||||||
|
|
||||||
|
OutMaxDelta = 0.0f;
|
||||||
|
OutMismatches = 0;
|
||||||
|
OutSamples = 0;
|
||||||
|
for (const FIntVector& Offset : Offsets)
|
||||||
|
{
|
||||||
|
const float X = (float)Offset.X;
|
||||||
|
const float Y = (float)Offset.Y;
|
||||||
|
const float Z = (float)FMath::Clamp(
|
||||||
|
MidWorldZ + Offset.Z, BottomWorldZ + 1, TopWorldZExclusive - 1);
|
||||||
|
const float Live = Generator.GetDensityAt(X, Y, Z);
|
||||||
|
const float Reference = ReferenceStack.EvalMC(X, Y, Z);
|
||||||
|
|
||||||
|
uint32 LiveBits = 0;
|
||||||
|
uint32 ReferenceBits = 0;
|
||||||
|
FMemory::Memcpy(&LiveBits, &Live, sizeof(LiveBits));
|
||||||
|
FMemory::Memcpy(&ReferenceBits, &Reference, sizeof(ReferenceBits));
|
||||||
|
const bool bBitEqual = LiveBits == ReferenceBits;
|
||||||
|
if (!bBitEqual)
|
||||||
|
{
|
||||||
|
++OutMismatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float Delta = (FMath::IsFinite(Live) && FMath::IsFinite(Reference))
|
||||||
|
? FMath::Abs(Live - Reference) : FLT_MAX;
|
||||||
|
OutMaxDelta = FMath::Max(OutMaxDelta, Delta);
|
||||||
|
++OutSamples;
|
||||||
|
}
|
||||||
|
return OutMismatches == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// LIVE EDIT — regenerate all chunks when params change in the Details panel
|
// LIVE EDIT — regenerate all chunks when params change in the Details panel
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -270,6 +497,210 @@ void AVoxelWorld::ValidateDeterminism()
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
|
void AVoxelWorld::ApplyComposerCandidate()
|
||||||
|
{
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (World == nullptr || !World->IsPlayInEditor())
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[VoxelWorld] ApplyComposerCandidate: run this button from a PIE world."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bShuttingDown.load(std::memory_order_acquire)
|
||||||
|
|| bGenerationPaused.load(std::memory_order_acquire))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[VoxelWorld] ApplyComposerCandidate: generation is already stopping or paused."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Settings || !Generator || !StrateManager)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[VoxelWorld] ApplyComposerCandidate: BeginPlay has not initialized Settings, Generator, and StrateManager."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<FStrateSlot>& Layout = StrateManager->GetLayout();
|
||||||
|
const FStrateSlot* TargetSlot = nullptr;
|
||||||
|
for (const FStrateSlot& Slot : Layout)
|
||||||
|
{
|
||||||
|
if (Slot.StrateIndex == ComposerTargetStrateIndex)
|
||||||
|
{
|
||||||
|
TargetSlot = &Slot;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (TargetSlot == nullptr || TargetSlot->Definition == nullptr)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[VoxelWorld] ApplyComposerCandidate: ComposerTargetStrateIndex=%d is not a live layout slot (layout has %d slots)."),
|
||||||
|
ComposerTargetStrateIndex, Layout.Num());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVoxelStrateCorpus Corpus;
|
||||||
|
FString CorpusReport;
|
||||||
|
if (!Corpus.LoadFromAssetRegistry(CorpusReport))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelWorld] ApplyComposerCandidate: composer corpus is unavailable: %s"),
|
||||||
|
*CorpusReport);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate(
|
||||||
|
Corpus, ComposerSeed, ComposerCandidateIndex, bComposerRollStructure);
|
||||||
|
if (!Candidate.bValid)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelWorld] ApplyComposerCandidate: seed=%d index=%d roll failed: %s"),
|
||||||
|
ComposerSeed, ComposerCandidateIndex, *Candidate.FailureReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 TargetTopChunkZ = TargetSlot->TopChunkZ;
|
||||||
|
const int32 TargetBottomChunkZ = TargetSlot->BottomChunkZ;
|
||||||
|
const float TargetTopWorldZ = (float)(TargetTopChunkZ + 1) * CHUNK_SIZE;
|
||||||
|
const float TargetBottomWorldZ = (float)TargetBottomChunkZ * CHUNK_SIZE;
|
||||||
|
const FIntVector RepresentativeChunk(
|
||||||
|
0, 0, TargetBottomChunkZ + (TargetTopChunkZ - TargetBottomChunkZ) / 2);
|
||||||
|
|
||||||
|
// Materialise before mutating the live manager. A bad recipe or unsupported native family
|
||||||
|
// therefore leaves the running world untouched.
|
||||||
|
FVoxelStrateArchetypeParams PreflightParams = Candidate.ArchetypeParams;
|
||||||
|
VF_SetComposerRuntimeBounds(PreflightParams, TargetTopWorldZ, TargetBottomWorldZ);
|
||||||
|
FVoxelOpStack PreflightStack;
|
||||||
|
FVoxelOpContext PreflightContext;
|
||||||
|
FString PreflightError;
|
||||||
|
bool bPreflightBuilt = false;
|
||||||
|
if (Candidate.bStructureRoll)
|
||||||
|
{
|
||||||
|
bPreflightBuilt = VF_BuildStackFromRecipe(
|
||||||
|
Candidate.Recipe, PreflightParams, Generator->Seed, Generator->OriginSpineRadius,
|
||||||
|
StrateManager, PreflightStack, PreflightContext, &PreflightError);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bPreflightBuilt = VF_BuildNativeStrateStackForCandidate(
|
||||||
|
Candidate.Archetype, PreflightParams, Generator->Seed, Generator->OriginSpineRadius,
|
||||||
|
Generator->WorldRadiusVoxels, Generator->EdgeSealThickness, StrateManager,
|
||||||
|
PreflightStack, PreflightContext);
|
||||||
|
if (!bPreflightBuilt)
|
||||||
|
{
|
||||||
|
PreflightError = TEXT("The production native candidate stack could not be materialised.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bPreflightBuilt)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelWorld] ApplyComposerCandidate: seed=%d index=%d was not applied: %s"),
|
||||||
|
ComposerSeed, ComposerCandidateIndex, *PreflightError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString ApplyError;
|
||||||
|
{
|
||||||
|
// The manager override is a density-only replacement. It does not call Initialize or
|
||||||
|
// GeneratePassages while the layout is half-built: the existing slot Z span, content
|
||||||
|
// definition, and passage geometry remain stable. The pause drains current workers before
|
||||||
|
// the map is changed, and the subsequent RegenerateAllChunks supplies the epoch bump.
|
||||||
|
FScopedGenerationPause Guard(this);
|
||||||
|
if (!Guard.Acquired())
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelWorld] ApplyComposerCandidate: generation pause timed out; no mutation applied."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StrateManager->SetComposerOverrideForStrate(
|
||||||
|
ComposerTargetStrateIndex, Candidate.Seed, Candidate.Archetype,
|
||||||
|
Candidate.ArchetypeParams, Candidate.bStructureRoll,
|
||||||
|
Candidate.bStructureRoll ? &Candidate.Recipe : nullptr, ApplyError))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error,
|
||||||
|
TEXT("[VoxelWorld] ApplyComposerCandidate: seed=%d index=%d was not applied: %s"),
|
||||||
|
ComposerSeed, ComposerCandidateIndex, *ApplyError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These are the same game-thread-owned side systems reset by RebuildStrates. The full
|
||||||
|
// chunk reset below also resets the density volume and preserves DiffLayer edits.
|
||||||
|
if (AtmosphereManager) { AtmosphereManager->Reset(); }
|
||||||
|
if (ContentManager) { ContentManager->ClearAll(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse the established seed/live-edit invalidation path: GenerationEpoch increments here,
|
||||||
|
// stale ProcessQueue results are dropped, and the normal Tick submits a fresh stream around
|
||||||
|
// the player. No async worker can observe the half-applied manager state.
|
||||||
|
RegenerateAllChunks();
|
||||||
|
|
||||||
|
FVoxelOpStack ReferenceStack;
|
||||||
|
FVoxelOpContext ReferenceContext;
|
||||||
|
FString ReferenceError;
|
||||||
|
const bool bReferenceBuilt = VF_BuildComposerReferenceStack(
|
||||||
|
Candidate, *Generator, *StrateManager, TargetTopChunkZ, TargetBottomChunkZ,
|
||||||
|
ReferenceStack, ReferenceContext, ReferenceError);
|
||||||
|
|
||||||
|
FString ComparableReason;
|
||||||
|
const bool bReferenceComparable = bReferenceBuilt
|
||||||
|
&& VF_ComposerReferenceIsComparable(
|
||||||
|
*this, *TargetSlot, Candidate.Archetype, RepresentativeChunk, ComparableReason);
|
||||||
|
|
||||||
|
const FVoxelStrateMeasureSettings MeasureSettings = VF_ComposerMeasureSettings();
|
||||||
|
FVoxelStrateMetrics Metrics;
|
||||||
|
FString MetricSource;
|
||||||
|
if (bReferenceComparable)
|
||||||
|
{
|
||||||
|
FComposerStackDensitySampler Sampler(ReferenceStack);
|
||||||
|
Metrics = VF_MeasureStrateWithSampler(
|
||||||
|
Sampler, (int32)TargetBottomWorldZ, (int32)TargetTopWorldZ,
|
||||||
|
VF_ComposerBoundarySeal(PreflightParams, Candidate.Archetype), MeasureSettings);
|
||||||
|
MetricSource = TEXT("candidate-stack");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// A live diff, disturbance post, or surface biome context makes a standalone stack an
|
||||||
|
// unlike oracle. Keep the owner's metric useful by measuring the actual applied generator;
|
||||||
|
// the log records why the bit-level oracle was skipped.
|
||||||
|
Metrics = VF_MeasureStrate(
|
||||||
|
*Generator, *StrateManager, ComposerTargetStrateIndex, MeasureSettings);
|
||||||
|
MetricSource = TEXT("live-generator");
|
||||||
|
}
|
||||||
|
|
||||||
|
float MaxDensityDelta = 0.0f;
|
||||||
|
int32 DensityMismatches = 0;
|
||||||
|
int32 DensitySamples = 0;
|
||||||
|
bool bDensityMatch = false;
|
||||||
|
if (bReferenceComparable)
|
||||||
|
{
|
||||||
|
bDensityMatch = VF_ComposerDensitySanityCheck(
|
||||||
|
*Generator, ReferenceStack, TargetTopChunkZ, TargetBottomChunkZ,
|
||||||
|
MaxDensityDelta, DensityMismatches, DensitySamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString RecipeText = Candidate.bStructureRoll
|
||||||
|
? VF_FormatStrateStructureRecipe(Candidate.Recipe)
|
||||||
|
: TEXT("parameter-roll");
|
||||||
|
const FString DeterminismText = !bReferenceBuilt
|
||||||
|
? FString::Printf(TEXT("UNAVAILABLE(%s)"), *ReferenceError)
|
||||||
|
: !bReferenceComparable
|
||||||
|
? FString::Printf(TEXT("SKIPPED(%s)"), *ComparableReason)
|
||||||
|
: FString::Printf(TEXT("%s/%d"), bDensityMatch ? TEXT("PASS") : TEXT("FAIL"),
|
||||||
|
DensitySamples);
|
||||||
|
const float AirFraction = Metrics.bValid ? Metrics.AirFraction : -1.0f;
|
||||||
|
const float LargestShare = Metrics.bValid ? Metrics.LargestComponentShare : -1.0f;
|
||||||
|
const float WalkableFraction = Metrics.bValid ? Metrics.WalkableFraction : -1.0f;
|
||||||
|
const float FeatureScale = Metrics.bValid ? Metrics.MedianFeatureScale : -1.0f;
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log,
|
||||||
|
TEXT("[VoxelWorld] ComposerCandidate applied seed=%d index=%d target=%d recipe=%s archetype=%s metrics=%s air=%.6f largest=%.6f walkable=%.6f feature=%.3f components=%d determinism=%s max_delta=%.9g mismatches=%d epoch=%u"),
|
||||||
|
Candidate.Seed, Candidate.Index, ComposerTargetStrateIndex, *RecipeText,
|
||||||
|
VF_GetStrateArchetypeName(Candidate.Archetype), *MetricSource,
|
||||||
|
AirFraction, LargestShare, WalkableFraction, FeatureScale,
|
||||||
|
Metrics.bValid ? Metrics.NumAirComponents : -1,
|
||||||
|
*DeterminismText, MaxDensityDelta, DensityMismatches, GenerationEpoch);
|
||||||
|
}
|
||||||
|
|
||||||
void AVoxelWorld::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
void AVoxelWorld::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||||
{
|
{
|
||||||
Super::PostEditChangeProperty(PropertyChangedEvent);
|
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
class UVoxelSettings;
|
class UVoxelSettings;
|
||||||
class UVoxelStrateDefinition;
|
class UVoxelStrateDefinition;
|
||||||
|
class UVoxelStrateManager;
|
||||||
|
|
||||||
/** The one-bit identity choice at the root of a rolled structure. */
|
/** The one-bit identity choice at the root of a rolled structure. */
|
||||||
UENUM(BlueprintType)
|
UENUM(BlueprintType)
|
||||||
@@ -211,6 +212,32 @@ struct VOXELFORGE_API FVoxelStrateRollInfo
|
|||||||
TArray<FString> BootstrapJitterFieldNames;
|
TArray<FString> BootstrapJitterFieldNames;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
/**
|
||||||
|
* One candidate materialised by the same deterministic calls used by the offline composer tests.
|
||||||
|
* This is an editor-only hand-off object: it is never a reflected asset and never part of the
|
||||||
|
* shipping generation contract.
|
||||||
|
*/
|
||||||
|
struct VOXELFORGE_API FVoxelStrateComposerCandidate
|
||||||
|
{
|
||||||
|
bool bValid = false;
|
||||||
|
FString FailureReason;
|
||||||
|
|
||||||
|
int32 Seed = 0;
|
||||||
|
int32 Index = 0;
|
||||||
|
bool bStructureRoll = false;
|
||||||
|
|
||||||
|
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
|
||||||
|
FVoxelStrateArchetypeParams ArchetypeParams;
|
||||||
|
FVoxelOpStackRecipe Recipe;
|
||||||
|
|
||||||
|
// Kept so automation/editor callers can audit the exact six independent structure blocks
|
||||||
|
// without re-rolling them through a second implementation.
|
||||||
|
FVoxelStrateRollInfo ParameterRoll;
|
||||||
|
TArray<FVoxelStrateRollInfo> StructureBlockRolls;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Known-good strate vectors plus their measured field spread.
|
* Known-good strate vectors plus their measured field spread.
|
||||||
*
|
*
|
||||||
@@ -293,6 +320,32 @@ VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype(
|
|||||||
/** Pure structure roll: no retained RNG state and no corpus/runtime dependency. */
|
/** Pure structure roll: no retained RNG state and no corpus/runtime dependency. */
|
||||||
VOXELFORGE_API FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index);
|
VOXELFORGE_API FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index);
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
/**
|
||||||
|
* Roll one candidate for the editor walk-through path. Parameter rolls call
|
||||||
|
* VF_RollStrateParamsDetailed; structure rolls call VF_RollStrateStructure and the exact six
|
||||||
|
* VF_RollStrateParamsDetailedForArchetype block rolls used by the structure test.
|
||||||
|
*/
|
||||||
|
VOXELFORGE_API FVoxelStrateComposerCandidate VF_RollStrateCandidate(
|
||||||
|
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index, bool bRollStructure);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a parameter-roll candidate through the same archetype-to-stack mapping used by production
|
||||||
|
* GetDensityAt/ClassifyTile. This is editor-only so the offline composer remains absent from a
|
||||||
|
* shipping build; the result is used only for the post-apply density sanity check.
|
||||||
|
*/
|
||||||
|
VOXELFORGE_API bool VF_BuildNativeStrateStackForCandidate(
|
||||||
|
ECaveGeneratorType Archetype,
|
||||||
|
const FVoxelStrateArchetypeParams& Params,
|
||||||
|
int32 Seed,
|
||||||
|
float SpineRadius,
|
||||||
|
float WorldRadiusVoxels,
|
||||||
|
float EdgeSealThickness,
|
||||||
|
const UVoxelStrateManager* StrateManager,
|
||||||
|
FVoxelOpStack& OutStack,
|
||||||
|
FVoxelOpContext& OutContext);
|
||||||
|
#endif
|
||||||
|
|
||||||
/** Stable compact representation and equality/hash helpers for manifests and reports. */
|
/** Stable compact representation and equality/hash helpers for manifests and reports. */
|
||||||
VOXELFORGE_API FString VF_FormatStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe);
|
VOXELFORGE_API FString VF_FormatStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe);
|
||||||
VOXELFORGE_API uint32 VF_HashStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe);
|
VOXELFORGE_API uint32 VF_HashStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe);
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
|
|
||||||
class UVoxelSettings;
|
class UVoxelSettings;
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
struct FVoxelStrateArchetypeParams;
|
||||||
|
struct FVoxelOpStackRecipe;
|
||||||
|
struct FVoxelStrateComposerSlotOverride;
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FVoxelPassage — A navigable connection between two strates.
|
* FVoxelPassage — A navigable connection between two strates.
|
||||||
*
|
*
|
||||||
@@ -143,6 +149,27 @@ public:
|
|||||||
*/
|
*/
|
||||||
void Initialize(UVoxelSettings* Settings, int32 WorldSeed);
|
void Initialize(UVoxelSettings* Settings, int32 WorldSeed);
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
/**
|
||||||
|
* Install an editor-only candidate over one already-built slot. The slot's definition,
|
||||||
|
* dimensions, content, and passages remain intact; only the density archetype/parameters
|
||||||
|
* (or the materialised recipe) are replaced. The caller must hold the world's generation pause.
|
||||||
|
*/
|
||||||
|
bool SetComposerOverrideForStrate(
|
||||||
|
int32 StrateIndex, int32 CandidateSeed, ECaveGeneratorType Archetype,
|
||||||
|
const FVoxelStrateArchetypeParams& Params, bool bUseRecipe,
|
||||||
|
const FVoxelOpStackRecipe* Recipe, FString& OutError);
|
||||||
|
|
||||||
|
/** Read the immutable editor override for a chunk into worker-local storage. */
|
||||||
|
bool GetComposerOverrideForChunk(
|
||||||
|
const FIntVector& ChunkCoord, int32& OutCandidateSeed,
|
||||||
|
ECaveGeneratorType& OutArchetype, FVoxelStrateArchetypeParams& OutParams,
|
||||||
|
bool& bOutUseRecipe, FVoxelOpStackRecipe& OutRecipe) const;
|
||||||
|
|
||||||
|
/** Custom recipes cannot be safely classified by the native ClassifyBox proof yet. */
|
||||||
|
bool HasComposerRecipeOverride() const;
|
||||||
|
#endif
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// QUERIES
|
// QUERIES
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -326,9 +353,9 @@ protected:
|
|||||||
// Passages connecting consecutive strates
|
// Passages connecting consecutive strates
|
||||||
TArray<FVoxelPassage> Passages;
|
TArray<FVoxelPassage> Passages;
|
||||||
|
|
||||||
// Bumped every time Passages is rebuilt (GeneratePassages). EvaluateModifierSDF keeps a
|
// Bumped every time Passages is rebuilt (GeneratePassages), and for an editor composer
|
||||||
// thread_local per-chunk shortlist of nearby passages and uses this to invalidate it when
|
// override. EvaluateModifierSDF and all generator thread_local memos use this to invalidate
|
||||||
// the passage set changes — so stale indices are never read after a rebuild.
|
// cached passage/strate data after either kind of live change.
|
||||||
uint32 PassagesVersion = 0;
|
uint32 PassagesVersion = 0;
|
||||||
|
|
||||||
// How many chunks at strate boundaries are blended (transition zone)
|
// How many chunks at strate boundaries are blended (transition zone)
|
||||||
@@ -360,6 +387,11 @@ protected:
|
|||||||
// Returns INDEX into StrateLayout, or -1 if not found.
|
// Returns INDEX into StrateLayout, or -1 if not found.
|
||||||
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
|
||||||
|
TMap<int32, TSharedPtr<FVoxelStrateComposerSlotOverride>> ComposerOverrides;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Build FStrateGenerationParams from a strate definition:
|
// Build FStrateGenerationParams from a strate definition:
|
||||||
// copies base GenerationParams, then applies all referenced terrain op assets.
|
// copies base GenerationParams, then applies all referenced terrain op assets.
|
||||||
// This is the single point where terrain op data assets are merged into params.
|
// This is the single point where terrain op data assets are merged into params.
|
||||||
|
|||||||
@@ -452,6 +452,30 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit")
|
||||||
bool bLiveEditStrates = false;
|
bool bLiveEditStrates = false;
|
||||||
|
|
||||||
|
#if WITH_EDITORONLY_DATA
|
||||||
|
/** Seed passed to the deterministic offline composer roll. This does not change Settings->Seed. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit|Composer")
|
||||||
|
int32 ComposerSeed = 0;
|
||||||
|
|
||||||
|
/** Candidate row passed to VF_RollStrateCandidate (the offline tests use the same index). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit|Composer", meta = (ClampMin = "0"))
|
||||||
|
int32 ComposerCandidateIndex = 0;
|
||||||
|
|
||||||
|
/** Existing layout slot to replace; 0 is the topmost strate. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit|Composer", meta = (ClampMin = "0"))
|
||||||
|
int32 ComposerTargetStrateIndex = 0;
|
||||||
|
|
||||||
|
/** False rolls one native parameter vector; true rolls the structure recipe and its six blocks. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Live Edit|Composer")
|
||||||
|
bool bComposerRollStructure = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
/** Roll and apply the selected offline composer candidate to one live PIE slot. */
|
||||||
|
UFUNCTION(CallInEditor, Category = "Live Edit|Composer")
|
||||||
|
void ApplyComposerCandidate();
|
||||||
|
#endif
|
||||||
|
|
||||||
/** Click to force-regenerate all chunks right now (useful during PIE). */
|
/** Click to force-regenerate all chunks right now (useful during PIE). */
|
||||||
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Live Edit")
|
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Live Edit")
|
||||||
void RegenerateAllChunks();
|
void RegenerateAllChunks();
|
||||||
|
|||||||
Reference in new issue
Block a user