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
@@ -9,6 +9,36 @@
|
||||
#include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo
|
||||
#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].
|
||||
// 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
|
||||
@@ -35,6 +65,11 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -240,6 +275,125 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
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
|
||||
//=============================================================================
|
||||
@@ -935,6 +1089,14 @@ ECaveGeneratorType UVoxelStrateManager::GetGeneratorTypeForChunk(const FIntVecto
|
||||
return ECaveGeneratorType::TunnelNetwork;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
if (const FVoxelStrateComposerSlotOverride* Override =
|
||||
FindComposerOverride(StrateLayout[SlotIdx].StrateIndex))
|
||||
{
|
||||
return Override->Archetype;
|
||||
}
|
||||
#endif
|
||||
|
||||
return StrateLayout[SlotIdx].Definition->GeneratorType;
|
||||
}
|
||||
|
||||
@@ -943,6 +1105,15 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord
|
||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||
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;
|
||||
if (!Def->bUseOperatorStack) { return false; }
|
||||
|
||||
@@ -1018,8 +1189,23 @@ FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVecto
|
||||
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||
|
||||
// Copy the designer-authored slab params from the strate definition.
|
||||
FSlabGenerationParams Result = Slot.Definition->SlabParams;
|
||||
// Copy the designer-authored slab params from the strate definition, unless an editor
|
||||
// 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
|
||||
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
|
||||
@@ -1034,32 +1220,111 @@ FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVecto
|
||||
// PER-ARCHETYPE PARAM GETTERS
|
||||
//=============================================================================
|
||||
// Each mirrors GetSlabParamsForChunk: copy designer params, fill runtime Z bounds.
|
||||
// No cross-boundary blending — archetypes meet at Hard boundaries. A macro keeps
|
||||
// the boilerplate (slot lookup + fallback + Z bounds) in one place.
|
||||
// No cross-boundary blending — archetypes meet at Hard boundaries.
|
||||
|
||||
#define VF_ARCHETYPE_PARAMS_GETTER(FnName, StructType, DefMember) \
|
||||
StructType UVoxelStrateManager::FnName(const FIntVector& ChunkCoord) const \
|
||||
{ \
|
||||
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z); \
|
||||
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) \
|
||||
{ \
|
||||
StructType Empty; \
|
||||
Empty.BaseDensity = -1.0f; \
|
||||
return Empty; \
|
||||
} \
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx]; \
|
||||
StructType Result = Slot.Definition->DefMember; \
|
||||
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * CHUNK_SIZE; \
|
||||
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * CHUNK_SIZE; \
|
||||
return Result; \
|
||||
FMazeGenerationParams UVoxelStrateManager::GetMazeParamsForChunk(const FIntVector& ChunkCoord) const
|
||||
{
|
||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition)
|
||||
{
|
||||
FMazeGenerationParams Empty;
|
||||
Empty.BaseDensity = -1.0f;
|
||||
return Empty;
|
||||
}
|
||||
|
||||
const FStrateSlot& Slot = StrateLayout[SlotIdx];
|
||||
FMazeGenerationParams Result = Slot.Definition->MazeParams;
|
||||
#if WITH_EDITOR
|
||||
if (const FVoxelStrateComposerSlotOverride* Override = FindComposerOverride(Slot.StrateIndex))
|
||||
{
|
||||
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)
|
||||
VF_ARCHETYPE_PARAMS_GETTER(GetSurfaceParamsForChunk, FSurfaceGenerationParams, SurfaceParams)
|
||||
VF_ARCHETYPE_PARAMS_GETTER(GetVerticalShaftParamsForChunk, FVerticalShaftParams, VerticalShaftParams)
|
||||
VF_ARCHETYPE_PARAMS_GETTER(GetFloatingIslandParamsForChunk, FFloatingIslandParams, FloatingIslandParams)
|
||||
FSurfaceGenerationParams UVoxelStrateManager::GetSurfaceParamsForChunk(const FIntVector& ChunkCoord) const
|
||||
{
|
||||
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
|
||||
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
|
||||
{
|
||||
@@ -1156,6 +1421,20 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
//=========================================================================
|
||||
|
||||
Reference in new issue
Block a user