test(diag): H1 dead, H3 measured -- player-sized space is SHATTERED
Diagnosis only, no generation or parameter changes. ⭐ H1 IS DEAD, confirmed by Jahni's playtest and by measurement. He walked the maze candidate: "i CAN go through it, the tunnels are just the right size with very tiny wiggle room." The radial probe agrees: measured bore span 3.62 m against a 3.677 m parameter. The carve produces what it claims; the scale change worked. ⭐ H3 IS WHAT THE NUMBERS SHOW. Player-sized volume EXISTS but is broken into hundreds of disconnected pockets, with the two mouths in different ones, tens to hundreds of metres apart (step 1, fitted window): archetype fit cells components largest arrival/departure gap Maze 2801 309 176 43 / 81 206 vox (51 m) TunnelNetwork 2748 467 1292 2 / 1 164 vox (41 m) VerticalShafts 1156 112 96 3 / 36 94 vox (24 m) SurfaceWorld 713 520 6 1 / 1 1101 vox (275 m) CrystalChamber 655 197 24 1 / 1 48 vox Only FloatingIslands connects, through FOUR cells. ⚠️ BUT I DO NOT ACCEPT "H3, therefore the world is shattered" YET, and the reason is Jahni. He WALKED a maze end to end that this metric says is 309 disconnected pockets whose largest holds 176 cells. Direct observation contradicts the instrument. An arrival mouth sitting in a THREE-CELL pocket is not a description of a cave; it is the signature of a stencil that fragments a continuous floor. H2 is the likely cause of that fragmentation, and Codex found it exactly: the fit test demands a solid cell at EXACTLY (X,Y,Z-1) -- no supporting- surface search, no slope tolerance, no step height. HeadroomCells applies only to the legacy walkable metric, not the capsule mask. VoxelStrateMeasure.cpp:697 (stencil), :729 (the exact-floor test) At step 1, any floor that slopes or is rough loses its solid cell at Z-1 under most of its air, so a walkable corridor dissolves into specks. ⇒ SEQUENCING: fix the stencil first, THEN re-measure H3. Until the instrument stops shredding continuous floors its component counts cannot be trusted. Fifth time this session that the measurement was the suspect. Recommended stencil, Codex's design and I agree: a bounded downward search for a COHERENT SUPPORTING SURFACE within a step-height tolerance, then re-test capsule clearance at that support height -- explicitly NOT "any solid within N cells", which would accept ledges and isolated support pixels. Fixed the mixed-resolution reporting that nearly cost me a false finding: fields are now coarse_step4_* and *_step1, never both unlabelled on one line. Recorded from the playtest, not acted on: - The maze is a GRID, not a maze. Edge selection is independent hash gating per +X/+Y/+Z edge (VoxelDensityOpStack.cpp:582/584/586), so every junction looks alike. A spanning subset would give dead ends and uneven runs. - TunnelNetwork DOES have explicit room-to-room connections (VoxelCaveMorphology.cpp:1406 backbone, :1465 tunnels) -- so Jahni's "lack of real passageway" applies to FlatPlain/CrystalChamber, which are slab voids with no room graph at all. - Two-abreast wants 2.5-3.0 m usable floor; a 4 m circular bore gives ~2 m. Build clean, 31 succeeded / 0 failed, run by me after Jahni closed the editor (the previous attempt could not link -- the DLL was held by his running editor). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K8V9my1PL68pmQb2gutZZV
This commit is contained in:
1 parent
fc43a86709
commit
cf5f83e9f0
2 files changed
+949
-16
No files matched your search
@@ -946,10 +946,14 @@ namespace
|
||||
const TCHAR* Label)
|
||||
{
|
||||
FString Report = FString::Printf(
|
||||
TEXT("%s (walkable floor area is the coarse 128 m x 128 m footprint proxy)\n"
|
||||
"archetype | PlayerFitFraction | TraversableComponentShare | "
|
||||
"player-fit arrival->departure | walkable floor area m2 | "
|
||||
"median vertical clearance m | roughness-to-feature ratio\n"), Label);
|
||||
TEXT("%s (player-fit fields are fitted SampleStep=1; walkable floor area and "
|
||||
"median vertical clearance are coarse SampleStep=4 proxies)\n"
|
||||
"archetype | player_fit_fraction_step1 | traversable_share_step1 | "
|
||||
"fit_cells_step1 | components_step1 | largest_component_cells_step1 | "
|
||||
"arrival_component_cells_step1 | departure_component_cells_step1 | "
|
||||
"mouth_component_gap_voxels_step1 | restricted_law_step1 | "
|
||||
"coarse_walkable_floor_area_m2 | coarse_median_vertical_clearance_m | "
|
||||
"roughness-to-feature ratio\n"), Label);
|
||||
for (const ECaveGeneratorType Archetype : GShowcaseArchetypes)
|
||||
{
|
||||
const FShowcaseSample* Found = Samples.FindByPredicate(
|
||||
@@ -961,21 +965,31 @@ namespace
|
||||
{
|
||||
Report += FString::Printf(
|
||||
TEXT("%s | no measured sample | no measured sample | no measured sample | "
|
||||
"no measured sample | no measured sample | no measured sample | "
|
||||
"no measured sample | no measured sample | no measured sample | "
|
||||
"n/a | n/a | n/a\n"),
|
||||
VF_GetStrateArchetypeName(Archetype));
|
||||
continue;
|
||||
}
|
||||
const FVoxelStrateMetrics& M = Found->FineMetrics;
|
||||
const FVoxelConnectivityDiagnostics& D = Found->PlayerFitLaw;
|
||||
const FString Law = M.bPlayerFitResolved
|
||||
? VF_ShowcaseConnectivityName(Found->PlayerFitLaw.Result)
|
||||
: TEXT("unresolved");
|
||||
const float FloorAreaM2 = Found->Metrics.WalkableFloorAreaFraction
|
||||
* 128.0f * 128.0f;
|
||||
Report += FString::Printf(
|
||||
TEXT("%s | %.6f | %.6f | %s | %.1f (%.6f footprint) | %.2f | %.3f\n"),
|
||||
TEXT("%s | %.6f | %.6f | %lld | %d | %lld | %lld | %lld | %.3f | %s | "
|
||||
"%.1f (%.6f footprint) | %.2f | %.3f\n"),
|
||||
VF_GetStrateArchetypeName(Archetype),
|
||||
M.bPlayerFitResolved ? M.PlayerFitFraction : 0.0f,
|
||||
M.bPlayerFitResolved ? M.TraversableComponentShare : 0.0f,
|
||||
static_cast<long long>(M.NumPlayerFitCells),
|
||||
M.NumTraversableComponents,
|
||||
static_cast<long long>(M.LargestTraversableComponentCells),
|
||||
static_cast<long long>(D.StartComponentCells),
|
||||
static_cast<long long>(D.GoalComponentCells),
|
||||
D.StartToGoalComponentDistanceCells,
|
||||
*Law, FloorAreaM2,
|
||||
Found->Metrics.WalkableFloorAreaFraction,
|
||||
Found->Metrics.MedianVerticalClearance * 0.25f,
|
||||
@@ -1875,20 +1889,35 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
const FString FineSummary = Sample.FineMetrics.bPlayerFitResolved
|
||||
? FString::Printf(
|
||||
TEXT("fit=%.6f traversable=%.6f fit_cells=%lld min_player_clearance=%.2f "
|
||||
"restricted_law=%s"),
|
||||
TEXT("fit_step1=%.6f traversable_step1=%.6f fit_cells_step1=%lld "
|
||||
"components_step1=%d largest_component_cells_step1=%lld "
|
||||
"arrival_component_cells_step1=%lld departure_component_cells_step1=%lld "
|
||||
"mouth_component_gap_step1=%.3f min_player_clearance_step1=%.2f "
|
||||
"restricted_law_step1=%s; coarse_step4_walkable=%.6f "
|
||||
"coarse_step4_clearance=%d voxels"),
|
||||
Sample.FineMetrics.PlayerFitFraction,
|
||||
Sample.FineMetrics.TraversableComponentShare,
|
||||
static_cast<long long>(Sample.FineMetrics.NumPlayerFitCells),
|
||||
Sample.FineMetrics.NumTraversableComponents,
|
||||
static_cast<long long>(Sample.FineMetrics.LargestTraversableComponentCells),
|
||||
static_cast<long long>(Sample.PlayerFitLaw.StartComponentCells),
|
||||
static_cast<long long>(Sample.PlayerFitLaw.GoalComponentCells),
|
||||
Sample.PlayerFitLaw.StartToGoalComponentDistanceCells,
|
||||
Sample.FineMetrics.MinimumPlayerClearanceVoxels,
|
||||
VF_ShowcaseConnectivityName(Sample.PlayerFitLaw.Result))
|
||||
VF_ShowcaseConnectivityName(Sample.PlayerFitLaw.Result),
|
||||
Sample.Metrics.WalkableFraction,
|
||||
Sample.Metrics.MedianVerticalClearance)
|
||||
: FString::Printf(
|
||||
TEXT("fit=UNRESOLVED reason=%s"),
|
||||
TEXT("fit_step1=UNRESOLVED reason=%s; coarse_step4_walkable=%.6f "
|
||||
"coarse_step4_clearance=%d voxels"),
|
||||
Sample.FineMetrics.PlayerFitRefusalReason.IsEmpty()
|
||||
? TEXT("not measured") : *Sample.FineMetrics.PlayerFitRefusalReason);
|
||||
? TEXT("not measured") : *Sample.FineMetrics.PlayerFitRefusalReason,
|
||||
Sample.Metrics.WalkableFraction,
|
||||
Sample.Metrics.MedianVerticalClearance);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("previous showcase %s seed=%d index=%d: %s; old_walkable=%.6f "
|
||||
"old_floor_area=%.6f old_largest_surface=%.6f old_law=%s; %s%s"),
|
||||
TEXT("previous showcase %s seed=%d index=%d: %s; coarse_step4_walkable=%.6f "
|
||||
"coarse_step4_floor_area=%.6f coarse_step4_largest_surface=%.6f "
|
||||
"coarse_step4_law=%s; %s%s"),
|
||||
VF_GetStrateArchetypeName(Previous.Archetype), Previous.Seed,
|
||||
Previous.CandidateIndex,
|
||||
bPassedNewGate ? TEXT("ACCEPTED by player-fit gate") : TEXT("REJECTED by player-fit gate"),
|
||||
@@ -2203,10 +2232,14 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
TEXT("%.6f (step=1)"), Selected.FineMetrics.TraversableComponentShare);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("%s: hard-gate survivors=%d, selected seed=%d index=%d, "
|
||||
"old_walkable=%.6f, old_floor_area=%.6f, old_clearance=%d voxels, "
|
||||
"old_largest_surface=%.6f, old_law=%s, player_fit_fraction=%.6f, "
|
||||
"traversable_component_share=%.6f, player_fit_cells=%lld, "
|
||||
"player_clearance=%.2f voxels, restricted_law=%s, window=%s"),
|
||||
"coarse_step4_walkable=%.6f, coarse_step4_floor_area=%.6f, "
|
||||
"coarse_step4_clearance=%d voxels, coarse_step4_largest_surface=%.6f, "
|
||||
"coarse_step4_law=%s, player_fit_fraction_step1=%.6f, "
|
||||
"traversable_component_share_step1=%.6f, player_fit_cells_step1=%lld, "
|
||||
"components_step1=%d, largest_component_cells_step1=%lld, "
|
||||
"arrival_component_cells_step1=%lld, departure_component_cells_step1=%lld, "
|
||||
"mouth_component_gap_voxels_step1=%.3f, player_clearance_step1=%.2f voxels, "
|
||||
"restricted_law_step1=%s, window=%s"),
|
||||
VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex]),
|
||||
Stats[ArchetypeIndex].Survivors.Num(),
|
||||
Selected.Candidate.Seed, Selected.Candidate.Index,
|
||||
@@ -2218,6 +2251,11 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
|
||||
Selected.FineMetrics.PlayerFitFraction,
|
||||
Selected.FineMetrics.TraversableComponentShare,
|
||||
static_cast<long long>(Selected.FineMetrics.NumPlayerFitCells),
|
||||
Selected.FineMetrics.NumTraversableComponents,
|
||||
static_cast<long long>(Selected.FineMetrics.LargestTraversableComponentCells),
|
||||
static_cast<long long>(Selected.PlayerFitLaw.StartComponentCells),
|
||||
static_cast<long long>(Selected.PlayerFitLaw.GoalComponentCells),
|
||||
Selected.PlayerFitLaw.StartToGoalComponentDistanceCells,
|
||||
Selected.FineMetrics.MinimumPlayerClearanceVoxels,
|
||||
VF_ShowcaseConnectivityName(Selected.PlayerFitLaw.Result),
|
||||
*PreviewCandidates[ArchetypeIndex].Window.Describe()));
|
||||
|
||||
@@ -0,0 +1,895 @@
|
||||
// Measurement-only scale diagnosis for the player-fit showcase.
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "VoxelCaveMorphology.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelTypes.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int32 DiagnosisSeed = 0;
|
||||
constexpr int32 MazeCandidateIndex = 50;
|
||||
constexpr int32 TunnelCandidateIndex = 61;
|
||||
// These are the current seed-0 showcase identities from the bounded roll set. The previous
|
||||
// report's VerticalShafts identity was seed 0 / index 34; index 15 belongs to another family
|
||||
// in the current corpus and would silently leave the shaft probe on fixture defaults.
|
||||
constexpr int32 ShaftCandidateIndex = 34;
|
||||
constexpr int32 RadialSampleMaxVoxels = 48;
|
||||
constexpr int32 FineDiagnosisMaxCells = 4000000;
|
||||
|
||||
const TCHAR* VF_DiagnosisConnectivityName(EVoxelConnectivityResult Result)
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case EVoxelConnectivityResult::Connected: return TEXT("Connected");
|
||||
case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnectedAtThisResolution");
|
||||
case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid");
|
||||
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid");
|
||||
case EVoxelConnectivityResult::StartCellNotPlayerFit: return TEXT("StartCellNotPlayerFit");
|
||||
case EVoxelConnectivityResult::GoalCellNotPlayerFit: return TEXT("GoalCellNotPlayerFit");
|
||||
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow");
|
||||
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudgetExhausted");
|
||||
}
|
||||
return TEXT("Unknown");
|
||||
}
|
||||
|
||||
struct FDensityCrossing
|
||||
{
|
||||
bool bFound = false;
|
||||
float AxisDensity = 0.0f;
|
||||
float CrossingRadiusVoxels = -1.0f;
|
||||
int32 FirstSolidStep = -1;
|
||||
float FirstSolidDensity = 0.0f;
|
||||
};
|
||||
|
||||
FDensityCrossing VF_SampleRadialRay(
|
||||
const FVector& AxisPoint,
|
||||
const FVector& UnitNormal,
|
||||
TFunctionRef<float(const FVector&)> SampleDensity,
|
||||
int32 MaxDistanceVoxels = RadialSampleMaxVoxels)
|
||||
{
|
||||
FDensityCrossing Result;
|
||||
float PreviousDensity = SampleDensity(AxisPoint);
|
||||
Result.AxisDensity = PreviousDensity;
|
||||
for (int32 Distance = 1; Distance <= MaxDistanceVoxels; ++Distance)
|
||||
{
|
||||
const float CurrentDensity = SampleDensity(
|
||||
AxisPoint + UnitNormal * static_cast<float>(Distance));
|
||||
// MC convention in this codebase: density >= 0 is air and density < 0 is solid.
|
||||
if (PreviousDensity >= 0.0f && CurrentDensity < 0.0f)
|
||||
{
|
||||
const float Denominator = PreviousDensity - CurrentDensity;
|
||||
const float Fraction = FMath::IsNearlyZero(Denominator)
|
||||
? 0.0f : FMath::Clamp(PreviousDensity / Denominator, 0.0f, 1.0f);
|
||||
Result.bFound = true;
|
||||
Result.CrossingRadiusVoxels = static_cast<float>(Distance - 1) + Fraction;
|
||||
Result.FirstSolidStep = Distance;
|
||||
Result.FirstSolidDensity = CurrentDensity;
|
||||
return Result;
|
||||
}
|
||||
PreviousDensity = CurrentDensity;
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
struct FRadialSection
|
||||
{
|
||||
FVector AxisPoint = FVector::ZeroVector;
|
||||
FVector UnitNormal = FVector::ZeroVector;
|
||||
FDensityCrossing Positive;
|
||||
FDensityCrossing Negative;
|
||||
bool bValid = false;
|
||||
};
|
||||
|
||||
FRadialSection VF_SampleSection(
|
||||
const FVector& AxisPoint,
|
||||
const FVector& UnitNormal,
|
||||
TFunctionRef<float(const FVector&)> SampleDensity)
|
||||
{
|
||||
FRadialSection Result;
|
||||
Result.AxisPoint = AxisPoint;
|
||||
Result.UnitNormal = UnitNormal.GetSafeNormal();
|
||||
if (Result.UnitNormal.IsNearlyZero())
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
Result.Positive = VF_SampleRadialRay(AxisPoint, Result.UnitNormal, SampleDensity);
|
||||
Result.Negative = VF_SampleRadialRay(AxisPoint, -Result.UnitNormal, SampleDensity);
|
||||
Result.bValid = Result.Positive.bFound && Result.Negative.bFound;
|
||||
return Result;
|
||||
}
|
||||
|
||||
float VF_SectionScore(const FRadialSection& Section)
|
||||
{
|
||||
return Section.bValid
|
||||
? FMath::Min(Section.Positive.CrossingRadiusVoxels,
|
||||
Section.Negative.CrossingRadiusVoxels)
|
||||
: -1.0f;
|
||||
}
|
||||
|
||||
FVector VF_PerpendicularTo(const FVector& Tangent)
|
||||
{
|
||||
const FVector Basis = FMath::Abs(Tangent.Z) < 0.9f
|
||||
? FVector::UpVector : FVector::RightVector;
|
||||
return FVector::CrossProduct(Tangent, Basis).GetSafeNormal();
|
||||
}
|
||||
|
||||
void VF_SetCandidateOnSlot(
|
||||
VoxelForgeTest::FTestWorld& World,
|
||||
const FVoxelStrateComposerCandidate& Candidate,
|
||||
int32 SlotIndex)
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = World.Definitions.IsValidIndex(SlotIndex)
|
||||
? World.Definitions[SlotIndex].Get() : nullptr;
|
||||
if (Definition == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Definition->GeneratorType = Candidate.Archetype;
|
||||
Definition->GenerationParams = Candidate.ArchetypeParams.TunnelNetworkParams;
|
||||
Definition->SlabParams = Candidate.ArchetypeParams.SlabParams;
|
||||
Definition->MazeParams = Candidate.ArchetypeParams.MazeParams;
|
||||
Definition->SurfaceParams = Candidate.ArchetypeParams.SurfaceParams;
|
||||
Definition->VerticalShaftParams = Candidate.ArchetypeParams.VerticalShaftParams;
|
||||
Definition->FloatingIslandParams = Candidate.ArchetypeParams.FloatingIslandParams;
|
||||
Definition->bUseOperatorStack = true;
|
||||
Definition->TransitionType = EVoxelStrateTransition::Hard;
|
||||
World.Reinitialize();
|
||||
}
|
||||
|
||||
int32 VF_MidChunkZ(const VoxelForgeTest::FTestWorld& World, int32 SlotIndex)
|
||||
{
|
||||
int32 TopVoxelZ = 0;
|
||||
int32 BottomVoxelZ = 0;
|
||||
if (!World.GetSlotVoxelZRange(SlotIndex, TopVoxelZ, BottomVoxelZ))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return FMath::FloorToInt(
|
||||
0.5f * static_cast<float>(TopVoxelZ + 1 + BottomVoxelZ)
|
||||
/ static_cast<float>(CHUNK_SIZE));
|
||||
}
|
||||
|
||||
struct FCoreStackProbe
|
||||
{
|
||||
FVoxelOpStack Stack;
|
||||
|
||||
void Prepare(int32 ChunkZ, float TopZ, float BottomZ,
|
||||
float WorldRadiusVoxels, float EdgeSealThickness,
|
||||
uint32 LayoutVersion)
|
||||
{
|
||||
FVoxelOpContext Context;
|
||||
Context.ChunkCoord = FIntVector(0, 0, ChunkZ);
|
||||
Context.Step = 1;
|
||||
Context.LayoutVersion = LayoutVersion;
|
||||
Context.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
Context.EdgeSealThickness = EdgeSealThickness;
|
||||
Context.StrateTopWorldZ = TopZ;
|
||||
Context.StrateBottomWorldZ = BottomZ;
|
||||
Stack.PrepareChunk(Context);
|
||||
}
|
||||
|
||||
float Sample(const FVector& Point) const
|
||||
{
|
||||
return Stack.EvalMC(Point.X, Point.Y, Point.Z);
|
||||
}
|
||||
};
|
||||
|
||||
bool VF_FindMazeAxis(
|
||||
const VoxelForgeTest::FTestWorld& World,
|
||||
const FMazeGenerationParams& Params,
|
||||
int32 SlotIndex,
|
||||
FRadialSection& OutFinalSection,
|
||||
FRadialSection& OutCoreSection,
|
||||
FString& OutDescription)
|
||||
{
|
||||
const float CellSize = FMath::Max(Params.CellSize, 1.0f);
|
||||
const uint32 Salt = static_cast<uint32>(World.Generator->Seed) ^ 0x4D617A65u;
|
||||
const int32 MidChunkZ = VF_MidChunkZ(World, SlotIndex);
|
||||
const int32 BaseCellZ = FMath::FloorToInt(
|
||||
(0.5f * (Params.StrateTopWorldZ + Params.StrateBottomWorldZ)) / CellSize);
|
||||
|
||||
FCoreStackProbe Core;
|
||||
VoxelDensityOps::BuildMazeStack(
|
||||
Core.Stack, Params, World.Generator->Seed, World.Generator->OriginSpineRadius,
|
||||
World.StrateManager.Get(), false);
|
||||
Core.Prepare(MidChunkZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
||||
World.Generator->WorldRadiusVoxels, World.Generator->EdgeSealThickness,
|
||||
World.StrateManager->GetLayoutVersion());
|
||||
|
||||
auto EdgeOpen = [Salt](int32 X, int32 Y, int32 Z,
|
||||
uint32 AxisSalt, float Threshold)
|
||||
{
|
||||
uint32 H = VoxelHash::Cell(X, Y, Salt ^ AxisSalt);
|
||||
H ^= VoxelHash::Mix(static_cast<uint32>(Z * 73856093) ^ AxisSalt);
|
||||
return VoxelHash::ToFloat01(VoxelHash::Mix(H)) < Threshold;
|
||||
};
|
||||
|
||||
auto FinalDensity = [&World](const FVector& Point)
|
||||
{
|
||||
return World.Generator->GetDensityAt(Point.X, Point.Y, Point.Z);
|
||||
};
|
||||
auto CoreDensity = [&Core](const FVector& Point)
|
||||
{
|
||||
return Core.Sample(Point);
|
||||
};
|
||||
|
||||
float BestDistanceSq = FLT_MAX;
|
||||
for (int32 Ring = 1; Ring <= 12; ++Ring)
|
||||
{
|
||||
for (int32 NodeY = -Ring; NodeY <= Ring; ++NodeY)
|
||||
{
|
||||
for (int32 NodeX = -Ring; NodeX <= Ring; ++NodeX)
|
||||
{
|
||||
if (FMath::Max(FMath::Abs(NodeX), FMath::Abs(NodeY)) != Ring)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int32 NodeZ = BaseCellZ - 1; NodeZ <= BaseCellZ + 1; ++NodeZ)
|
||||
{
|
||||
const FVector A(
|
||||
(NodeX + 0.5f) * CellSize,
|
||||
(NodeY + 0.5f) * CellSize,
|
||||
(NodeZ + 0.5f) * CellSize);
|
||||
const float DistanceSq = A.X * A.X + A.Y * A.Y;
|
||||
|
||||
struct FAxisSpec { uint32 Salt; float Threshold; FVector Delta; const TCHAR* Name; };
|
||||
const FAxisSpec Axes[] = {
|
||||
{ 0xA1u, Params.BranchProbability,
|
||||
FVector(CellSize, 0.0f, 0.0f), TEXT("X") },
|
||||
{ 0xB2u, Params.BranchProbability,
|
||||
FVector(0.0f, CellSize, 0.0f), TEXT("Y") },
|
||||
};
|
||||
for (const FAxisSpec& Axis : Axes)
|
||||
{
|
||||
if (!EdgeOpen(NodeX, NodeY, NodeZ, Axis.Salt, Axis.Threshold))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FVector B = A + Axis.Delta;
|
||||
const FVector AxisPoint = 0.5f * (A + B);
|
||||
if (AxisPoint.Z <= Params.StrateBottomWorldZ + Params.BoundarySealThickness + 8.0f
|
||||
|| AxisPoint.Z >= Params.StrateTopWorldZ - Params.BoundarySealThickness - 8.0f
|
||||
|| DistanceSq < FMath::Square(World.Generator->OriginSpineRadius + 24.0f))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FVector Normal = VF_PerpendicularTo((B - A).GetSafeNormal());
|
||||
const FRadialSection FinalSection = VF_SampleSection(
|
||||
AxisPoint, Normal, FinalDensity);
|
||||
const FRadialSection CoreSection = VF_SampleSection(
|
||||
AxisPoint, Normal, CoreDensity);
|
||||
if (!FinalSection.bValid || !CoreSection.bValid
|
||||
|| FinalSection.Positive.AxisDensity <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DistanceSq >= BestDistanceSq)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BestDistanceSq = DistanceSq;
|
||||
OutFinalSection = FinalSection;
|
||||
OutCoreSection = CoreSection;
|
||||
OutDescription = FString::Printf(
|
||||
TEXT("Maze edge axis=%s node=(%d,%d,%d) endpointA=(%.2f,%.2f,%.2f) "
|
||||
"endpointB=(%.2f,%.2f,%.2f)"),
|
||||
Axis.Name, NodeX, NodeY, NodeZ,
|
||||
A.X, A.Y, A.Z, B.X, B.Y, B.Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (OutFinalSection.bValid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct FTunnelAxisCandidate
|
||||
{
|
||||
bool bValid = false;
|
||||
float Score = -1.0f;
|
||||
float NominalRadius = 0.0f;
|
||||
FString Description;
|
||||
FRadialSection FinalSection;
|
||||
FRadialSection CoreSection;
|
||||
};
|
||||
|
||||
void VF_ConsiderTunnelSegment(
|
||||
const VoxelForgeTest::FTestWorld& World,
|
||||
const FStrateGenerationParams& Params,
|
||||
const FCoreStackProbe& Core,
|
||||
const FVector& A,
|
||||
const FVector& B,
|
||||
float RadiusA,
|
||||
float RadiusB,
|
||||
const FString& SegmentName,
|
||||
FTunnelAxisCandidate& InOutBest)
|
||||
{
|
||||
const FVector Tangent = (B - A).GetSafeNormal();
|
||||
const float Length = FVector::Dist(A, B);
|
||||
if (Tangent.IsNearlyZero() || Length < 24.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FVector Normal = VF_PerpendicularTo(Tangent);
|
||||
const FVector Binormal = FVector::CrossProduct(Tangent, Normal).GetSafeNormal();
|
||||
const float NominalRadius = FMath::Lerp(RadiusA, RadiusB, 0.5f);
|
||||
const int32 SearchExtent = 16;
|
||||
const int32 SearchStep = 4;
|
||||
const int32 MaxDistance = FMath::Clamp(
|
||||
FMath::CeilToInt(NominalRadius + FMath::Max(Params.SurfaceRoughness, 0.0f) + 20.0f),
|
||||
24, RadialSampleMaxVoxels);
|
||||
|
||||
auto FinalDensity = [&World](const FVector& Point)
|
||||
{
|
||||
return World.Generator->GetDensityAt(Point.X, Point.Y, Point.Z);
|
||||
};
|
||||
auto CoreDensity = [&Core](const FVector& Point)
|
||||
{
|
||||
return Core.Sample(Point);
|
||||
};
|
||||
|
||||
for (int32 OffsetB = -SearchExtent; OffsetB <= SearchExtent; OffsetB += SearchStep)
|
||||
{
|
||||
for (int32 OffsetN = -SearchExtent; OffsetN <= SearchExtent; OffsetN += SearchStep)
|
||||
{
|
||||
const FVector AxisPoint = 0.5f * (A + B)
|
||||
+ Binormal * static_cast<float>(OffsetB)
|
||||
+ Normal * static_cast<float>(OffsetN);
|
||||
if (AxisPoint.Z <= Params.StrateBottomWorldZ + Params.BoundarySealThickness + 8.0f
|
||||
|| AxisPoint.Z >= Params.StrateTopWorldZ - Params.BoundarySealThickness - 8.0f
|
||||
|| FVector2D(AxisPoint.X, AxisPoint.Y).SizeSquared()
|
||||
< FMath::Square(World.Generator->OriginSpineRadius + 24.0f))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FRadialSection FinalSection = VF_SampleSection(
|
||||
AxisPoint, Normal, FinalDensity);
|
||||
if (!FinalSection.bValid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FRadialSection CoreSection = VF_SampleSection(
|
||||
AxisPoint, Normal, CoreDensity);
|
||||
if (!CoreSection.bValid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float Score = VF_SectionScore(FinalSection);
|
||||
if (Score <= InOutBest.Score)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
InOutBest.bValid = true;
|
||||
InOutBest.Score = Score;
|
||||
InOutBest.NominalRadius = NominalRadius;
|
||||
InOutBest.Description = FString::Printf(
|
||||
TEXT("%s segment A=(%.2f,%.2f,%.2f) B=(%.2f,%.2f,%.2f) "
|
||||
"axisOffset=(%.2f,%.2f,%.2f)"),
|
||||
*SegmentName, A.X, A.Y, A.Z, B.X, B.Y, B.Z,
|
||||
AxisPoint.X - 0.5f * (A.X + B.X),
|
||||
AxisPoint.Y - 0.5f * (A.Y + B.Y),
|
||||
AxisPoint.Z - 0.5f * (A.Z + B.Z));
|
||||
InOutBest.FinalSection = FinalSection;
|
||||
InOutBest.CoreSection = CoreSection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool VF_FindTunnelAxis(
|
||||
const VoxelForgeTest::FTestWorld& World,
|
||||
const FStrateGenerationParams& Params,
|
||||
int32 SlotIndex,
|
||||
FTunnelAxisCandidate& OutBest)
|
||||
{
|
||||
const int32 MidChunkZ = VF_MidChunkZ(World, SlotIndex);
|
||||
FCoreStackProbe Core;
|
||||
VoxelDensityOps::BuildTunnelNetworkStack(
|
||||
Core.Stack, Params, World.Generator->Seed, World.Generator->OriginSpineRadius,
|
||||
World.StrateManager.Get(), false);
|
||||
Core.Prepare(MidChunkZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
||||
World.Generator->WorldRadiusVoxels, World.Generator->EdgeSealThickness,
|
||||
World.StrateManager->GetLayoutVersion());
|
||||
|
||||
FChunkSDFCache Cache;
|
||||
const float SearchExtent = FMath::Max(
|
||||
512.0f, FMath::Max(Params.RoomSpacing * 4.0f, Params.MaxTunnelLength * 2.0f));
|
||||
VoxelCaveMorphology::BuildChunkCache(
|
||||
Cache, -SearchExtent, -SearchExtent, SearchExtent, SearchExtent,
|
||||
Params, static_cast<uint32>(World.Generator->Seed), SlotIndex, nullptr);
|
||||
|
||||
for (int32 TunnelIndex = 0; TunnelIndex < Cache.Tunnels.Num(); ++TunnelIndex)
|
||||
{
|
||||
const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIndex];
|
||||
if (Tunnel.bHasMidpoint)
|
||||
{
|
||||
VF_ConsiderTunnelSegment(
|
||||
World, Params, Core, Tunnel.EndpointA, Tunnel.Midpoint,
|
||||
Tunnel.RadiusA, Tunnel.RadiusMid,
|
||||
FString::Printf(TEXT("tunnel[%d].A-mid"), TunnelIndex), OutBest);
|
||||
VF_ConsiderTunnelSegment(
|
||||
World, Params, Core, Tunnel.Midpoint, Tunnel.EndpointB,
|
||||
Tunnel.RadiusMid, Tunnel.RadiusB,
|
||||
FString::Printf(TEXT("tunnel[%d].mid-B"), TunnelIndex), OutBest);
|
||||
}
|
||||
else
|
||||
{
|
||||
VF_ConsiderTunnelSegment(
|
||||
World, Params, Core, Tunnel.EndpointA, Tunnel.EndpointB,
|
||||
Tunnel.RadiusA, Tunnel.RadiusB,
|
||||
FString::Printf(TEXT("tunnel[%d]"), TunnelIndex), OutBest);
|
||||
}
|
||||
}
|
||||
return OutBest.bValid;
|
||||
}
|
||||
|
||||
bool VF_FindShaftAxis(
|
||||
const VoxelForgeTest::FTestWorld& World,
|
||||
const FVerticalShaftParams& Params,
|
||||
int32 SlotIndex,
|
||||
FRadialSection& OutFinalSection,
|
||||
FRadialSection& OutCoreSection,
|
||||
float& OutNominalRadius,
|
||||
FString& OutDescription)
|
||||
{
|
||||
const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f);
|
||||
const uint32 Salt = static_cast<uint32>(World.Generator->Seed) ^ 0x53686674u;
|
||||
const int32 MidChunkZ = VF_MidChunkZ(World, SlotIndex);
|
||||
FCoreStackProbe Core;
|
||||
VoxelDensityOps::BuildVerticalShaftStack(
|
||||
Core.Stack, Params, World.Generator->Seed, World.Generator->OriginSpineRadius,
|
||||
World.StrateManager.Get(), false);
|
||||
Core.Prepare(MidChunkZ, Params.StrateTopWorldZ, Params.StrateBottomWorldZ,
|
||||
World.Generator->WorldRadiusVoxels, World.Generator->EdgeSealThickness,
|
||||
World.StrateManager->GetLayoutVersion());
|
||||
|
||||
auto FinalDensity = [&World](const FVector& Point)
|
||||
{
|
||||
return World.Generator->GetDensityAt(Point.X, Point.Y, Point.Z);
|
||||
};
|
||||
auto CoreDensity = [&Core](const FVector& Point)
|
||||
{
|
||||
return Core.Sample(Point);
|
||||
};
|
||||
|
||||
const float InnerBottom = Params.StrateBottomWorldZ + Params.BoundarySealThickness + 4.0f;
|
||||
const float InnerTop = Params.StrateTopWorldZ - Params.BoundarySealThickness - 4.0f;
|
||||
const float Period = FMath::Max(Params.LedgeSpacing, 32.0f);
|
||||
for (int32 Ring = 1; Ring <= 32; ++Ring)
|
||||
{
|
||||
for (int32 CellY = -Ring; CellY <= Ring; ++CellY)
|
||||
{
|
||||
for (int32 CellX = -Ring; CellX <= Ring; ++CellX)
|
||||
{
|
||||
if (FMath::Max(FMath::Abs(CellX), FMath::Abs(CellY)) != Ring)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const uint32 CellHash = VoxelHash::Cell(CellX, CellY, Salt);
|
||||
if (VoxelHash::ToFloat01(CellHash) > Params.ShaftDensity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float JitterX = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(CellHash ^ 0x12345678u));
|
||||
const float JitterY = VoxelHash::ToFloat01(
|
||||
VoxelHash::Mix(CellHash ^ 0x9ABCDEF0u));
|
||||
const float ShaftX = (CellX + 0.15f + JitterX * 0.7f) * Spacing;
|
||||
const float ShaftY = (CellY + 0.15f + JitterY * 0.7f) * Spacing;
|
||||
const float Radius = FMath::Lerp(
|
||||
Params.ShaftMinRadius, Params.ShaftMaxRadius,
|
||||
VoxelHash::ToFloat01(VoxelHash::Mix(CellHash ^ 0xBEEFu)));
|
||||
if (FMath::Square(ShaftX) + FMath::Square(ShaftY)
|
||||
< FMath::Square(World.Generator->OriginSpineRadius + 24.0f))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int32 Level = 0; Level < 16; ++Level)
|
||||
{
|
||||
const float Z = InnerBottom + (static_cast<float>(Level) + 0.5f) * Period;
|
||||
if (Z >= InnerTop)
|
||||
{
|
||||
break;
|
||||
}
|
||||
const FVector AxisPoint(ShaftX, ShaftY, Z);
|
||||
const FRadialSection FinalSection = VF_SampleSection(
|
||||
AxisPoint, FVector(1.0f, 0.0f, 0.0f), FinalDensity);
|
||||
const FRadialSection CoreSection = VF_SampleSection(
|
||||
AxisPoint, FVector(1.0f, 0.0f, 0.0f), CoreDensity);
|
||||
if (!FinalSection.bValid || !CoreSection.bValid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
OutFinalSection = FinalSection;
|
||||
OutCoreSection = CoreSection;
|
||||
OutNominalRadius = Radius;
|
||||
OutDescription = FString::Printf(
|
||||
TEXT("shaft cell=(%d,%d) center=(%.2f,%.2f) z=%.2f"),
|
||||
CellX, CellY, ShaftX, ShaftY, Z);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class FEmptyRoomSampler final : public IVoxelStrateDensitySampler
|
||||
{
|
||||
public:
|
||||
float SampleDensity(float, float, float WorldZ) const override
|
||||
{
|
||||
// A 20 m square room: z=0.5 is the one-cell solid floor and every sampled
|
||||
// cell above it is empty air. This is deliberately a control for the stencil,
|
||||
// not a generation change.
|
||||
return WorldZ < 1.0f ? -1.0f : 1.0f;
|
||||
}
|
||||
};
|
||||
|
||||
class FGeneratorSampler final : public IVoxelStrateDensitySampler
|
||||
{
|
||||
public:
|
||||
explicit FGeneratorSampler(const UVoxelGenerator& InGenerator)
|
||||
: Generator(InGenerator) {}
|
||||
|
||||
float SampleDensity(float WorldX, float WorldY, float WorldZ) const override
|
||||
{
|
||||
return Generator.GetDensityAt(WorldX, WorldY, WorldZ);
|
||||
}
|
||||
|
||||
private:
|
||||
const UVoxelGenerator& Generator;
|
||||
};
|
||||
|
||||
float VF_BoundarySealForCandidate(const FVoxelStrateComposerCandidate& Candidate)
|
||||
{
|
||||
switch (Candidate.Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater:
|
||||
return Candidate.ArchetypeParams.TunnelNetworkParams.BoundarySealThickness;
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber:
|
||||
return Candidate.ArchetypeParams.SlabParams.BoundarySealThickness;
|
||||
case ECaveGeneratorType::Maze:
|
||||
return Candidate.ArchetypeParams.MazeParams.BoundarySealThickness;
|
||||
case ECaveGeneratorType::SurfaceWorld:
|
||||
return Candidate.ArchetypeParams.SurfaceParams.BoundarySealThickness;
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
return Candidate.ArchetypeParams.VerticalShaftParams.BoundarySealThickness;
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
return Candidate.ArchetypeParams.FloatingIslandParams.BoundarySealThickness;
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void VF_ReportPlayerFitRow(
|
||||
FAutomationTestBase& Test,
|
||||
const TCHAR* Label,
|
||||
const FVoxelStrateMetrics& Metrics,
|
||||
const FVoxelConnectivityDiagnostics& Diagnostics)
|
||||
{
|
||||
Test.AddInfo(FString::Printf(
|
||||
TEXT("PLAYER_FIT step=1 fitted-window %s: fit_cells=%lld components=%d "
|
||||
"largest_component_cells=%lld arrival_component_cells=%lld "
|
||||
"departure_component_cells=%lld mouth_component_gap_voxels=%.3f "
|
||||
"fit_fraction=%.9f traversable_share=%.9f law=%s"),
|
||||
Label,
|
||||
static_cast<long long>(Metrics.NumPlayerFitCells),
|
||||
Metrics.NumTraversableComponents,
|
||||
static_cast<long long>(Metrics.LargestTraversableComponentCells),
|
||||
static_cast<long long>(Diagnostics.StartComponentCells),
|
||||
static_cast<long long>(Diagnostics.GoalComponentCells),
|
||||
Diagnostics.StartToGoalComponentDistanceCells,
|
||||
Metrics.PlayerFitFraction,
|
||||
Metrics.TraversableComponentShare,
|
||||
VF_DiagnosisConnectivityName(Diagnostics.Result)));
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeScaleDiagnosisTest,
|
||||
"VoxelForge.Composer.ScaleDiagnosis",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace VoxelForgeTest;
|
||||
(void)Parameters;
|
||||
|
||||
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
|
||||
nullptr, TEXT("/Game/VoxelForge/DA_Settings.DA_Settings"));
|
||||
if (AuthoredSettings == nullptr)
|
||||
{
|
||||
AddError(TEXT("Could not load /Game/VoxelForge/DA_Settings.DA_Settings."));
|
||||
return false;
|
||||
}
|
||||
|
||||
FVoxelStrateCorpus Corpus;
|
||||
FString CorpusReport;
|
||||
const bool bCorpusLoaded = Corpus.LoadFromAssetRegistry(CorpusReport);
|
||||
AddInfo(CorpusReport);
|
||||
TestTrue(TEXT("scale-diagnosis corpus is usable"), bCorpusLoaded && Corpus.IsValid());
|
||||
if (!bCorpusLoaded || !Corpus.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FTestWorld World;
|
||||
World.Build(DiagnosisSeed, 2, true, 8);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
|
||||
const FVoxelStrateComposerCandidate MazeCandidate = VF_RollStrateCandidate(
|
||||
Corpus, DiagnosisSeed, MazeCandidateIndex, false);
|
||||
const FVoxelStrateComposerCandidate TunnelCandidate = VF_RollStrateCandidate(
|
||||
Corpus, DiagnosisSeed, TunnelCandidateIndex, false);
|
||||
const FVoxelStrateComposerCandidate ShaftCandidate = VF_RollStrateCandidate(
|
||||
Corpus, DiagnosisSeed, ShaftCandidateIndex, false);
|
||||
TestTrue(TEXT("seed 0 index 50 is a valid Maze candidate"),
|
||||
MazeCandidate.bValid && MazeCandidate.Archetype == ECaveGeneratorType::Maze);
|
||||
TestTrue(TEXT("seed 0 index 61 is a valid TunnelNetwork candidate"),
|
||||
TunnelCandidate.bValid && TunnelCandidate.Archetype == ECaveGeneratorType::TunnelNetwork);
|
||||
TestTrue(TEXT("seed 0 index 34 is a valid VerticalShafts candidate"),
|
||||
ShaftCandidate.bValid && ShaftCandidate.Archetype == ECaveGeneratorType::VerticalShafts);
|
||||
|
||||
// H1 — direct GetDensityAt radial crossings. The Maze is the fourth one-based layout slot
|
||||
// (FTestWorld uses zero-based SlotMaze == 3), matching the requested "slot 4".
|
||||
const int32 MazeSlot = FTestWorld::SlotMaze;
|
||||
VF_SetCandidateOnSlot(World, MazeCandidate, MazeSlot);
|
||||
int32 MazeTop = 0;
|
||||
int32 MazeBottom = 0;
|
||||
TestTrue(TEXT("Maze diagnosis slot has a Z range"),
|
||||
World.GetSlotVoxelZRange(MazeSlot, MazeTop, MazeBottom));
|
||||
const FMazeGenerationParams MazeParams = World.StrateManager->GetMazeParamsForChunk(
|
||||
FIntVector(0, 0, VF_MidChunkZ(World, MazeSlot)));
|
||||
FRadialSection MazeFinal;
|
||||
FRadialSection MazeCore;
|
||||
FString MazeAxisDescription;
|
||||
const bool bHaveMazeAxis = VF_FindMazeAxis(
|
||||
World, MazeParams, MazeSlot, MazeFinal, MazeCore, MazeAxisDescription);
|
||||
TestTrue(TEXT("Maze diagnosis found an open corridor axis"), bHaveMazeAxis);
|
||||
if (bHaveMazeAxis)
|
||||
{
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("BORE maze seed=%d candidate=%d slot=%d(one-based 4) %s: "
|
||||
"parameter_radius=%.3f vox (%.3f m), parameter_bore=%.3f m; "
|
||||
"GetDensityAt +ray crossing=%.3f vox (%.3f m), first_solid_step=%d "
|
||||
"core_without_structural_posts +ray=%.3f vox (%.3f m); "
|
||||
"-ray final=%.3f vox (%.3f m), -ray core=%.3f vox (%.3f m)"),
|
||||
DiagnosisSeed, MazeCandidateIndex, MazeSlot, *MazeAxisDescription,
|
||||
MazeParams.CorridorRadius, MazeParams.CorridorRadius * 0.25f,
|
||||
MazeParams.CorridorRadius * 2.0f * 0.25f,
|
||||
MazeFinal.Positive.CrossingRadiusVoxels,
|
||||
MazeFinal.Positive.CrossingRadiusVoxels * 0.25f,
|
||||
MazeFinal.Positive.FirstSolidStep,
|
||||
MazeCore.Positive.CrossingRadiusVoxels,
|
||||
MazeCore.Positive.CrossingRadiusVoxels * 0.25f,
|
||||
MazeFinal.Negative.CrossingRadiusVoxels,
|
||||
MazeFinal.Negative.CrossingRadiusVoxels * 0.25f,
|
||||
MazeCore.Negative.CrossingRadiusVoxels,
|
||||
MazeCore.Negative.CrossingRadiusVoxels * 0.25f));
|
||||
}
|
||||
|
||||
// TunnelNetwork — use the current fixed showcase roll and recover a real cached tunnel
|
||||
// segment, then sample the final GetDensityAt field at a locally centred radial section.
|
||||
const int32 TunnelSlot = FTestWorld::SlotTunnelNetwork;
|
||||
VF_SetCandidateOnSlot(World, TunnelCandidate, TunnelSlot);
|
||||
const FStrateGenerationParams TunnelParams = World.StrateManager->GetGenerationParams(
|
||||
FIntVector(0, 0, VF_MidChunkZ(World, TunnelSlot)));
|
||||
FTunnelAxisCandidate TunnelAxis;
|
||||
const bool bHaveTunnelAxis = VF_FindTunnelAxis(
|
||||
World, TunnelParams, TunnelSlot, TunnelAxis);
|
||||
TestTrue(TEXT("TunnelNetwork diagnosis found a tunnel axis"), bHaveTunnelAxis);
|
||||
if (bHaveTunnelAxis)
|
||||
{
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("BORE tunnel seed=%d candidate=%d %s: parameter_radius_range=%.3f..%.3f vox "
|
||||
"(%.3f..%.3f m), nominal_radius_at_probe=%.3f vox (%.3f m); "
|
||||
"GetDensityAt +ray crossing=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m); "
|
||||
"core_without_structural_posts +ray=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m)"),
|
||||
DiagnosisSeed, TunnelCandidateIndex, *TunnelAxis.Description,
|
||||
TunnelParams.TunnelMinRadius, TunnelParams.TunnelMaxRadius,
|
||||
TunnelParams.TunnelMinRadius * 0.25f, TunnelParams.TunnelMaxRadius * 0.25f,
|
||||
TunnelAxis.NominalRadius, TunnelAxis.NominalRadius * 0.25f,
|
||||
TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels,
|
||||
TunnelAxis.FinalSection.Positive.CrossingRadiusVoxels * 0.25f,
|
||||
TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels,
|
||||
TunnelAxis.FinalSection.Negative.CrossingRadiusVoxels * 0.25f,
|
||||
TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels,
|
||||
TunnelAxis.CoreSection.Positive.CrossingRadiusVoxels * 0.25f,
|
||||
TunnelAxis.CoreSection.Negative.CrossingRadiusVoxels,
|
||||
TunnelAxis.CoreSection.Negative.CrossingRadiusVoxels * 0.25f));
|
||||
}
|
||||
|
||||
// VerticalShafts — recover the deterministic shaft centre from the same hash contract as
|
||||
// production, then sample horizontally at a non-ledge height.
|
||||
const int32 ShaftSlot = FTestWorld::SlotVerticalShafts;
|
||||
VF_SetCandidateOnSlot(World, ShaftCandidate, ShaftSlot);
|
||||
const FVerticalShaftParams ShaftParams = World.StrateManager->GetVerticalShaftParamsForChunk(
|
||||
FIntVector(0, 0, VF_MidChunkZ(World, ShaftSlot)));
|
||||
FRadialSection ShaftFinal;
|
||||
FRadialSection ShaftCore;
|
||||
float ShaftNominalRadius = 0.0f;
|
||||
FString ShaftAxisDescription;
|
||||
const bool bHaveShaftAxis = VF_FindShaftAxis(
|
||||
World, ShaftParams, ShaftSlot, ShaftFinal, ShaftCore,
|
||||
ShaftNominalRadius, ShaftAxisDescription);
|
||||
TestTrue(TEXT("VerticalShafts diagnosis found a shaft axis"), bHaveShaftAxis);
|
||||
if (bHaveShaftAxis)
|
||||
{
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("BORE shaft seed=%d candidate=%d %s: parameter_radius_range=%.3f..%.3f vox "
|
||||
"(%.3f..%.3f m), nominal_radius_at_probe=%.3f vox (%.3f m); "
|
||||
"GetDensityAt +ray crossing=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m); "
|
||||
"core_without_structural_posts +ray=%.3f vox (%.3f m), -ray=%.3f vox (%.3f m)"),
|
||||
DiagnosisSeed, ShaftCandidateIndex, *ShaftAxisDescription,
|
||||
ShaftParams.ShaftMinRadius, ShaftParams.ShaftMaxRadius,
|
||||
ShaftParams.ShaftMinRadius * 0.25f, ShaftParams.ShaftMaxRadius * 0.25f,
|
||||
ShaftNominalRadius, ShaftNominalRadius * 0.25f,
|
||||
ShaftFinal.Positive.CrossingRadiusVoxels,
|
||||
ShaftFinal.Positive.CrossingRadiusVoxels * 0.25f,
|
||||
ShaftFinal.Negative.CrossingRadiusVoxels,
|
||||
ShaftFinal.Negative.CrossingRadiusVoxels * 0.25f,
|
||||
ShaftCore.Positive.CrossingRadiusVoxels,
|
||||
ShaftCore.Positive.CrossingRadiusVoxels * 0.25f,
|
||||
ShaftCore.Negative.CrossingRadiusVoxels,
|
||||
ShaftCore.Negative.CrossingRadiusVoxels * 0.25f));
|
||||
}
|
||||
|
||||
// H2 — a known-open control with a solid floor. This must produce non-zero fit cells.
|
||||
FEmptyRoomSampler EmptyRoom;
|
||||
FVoxelStrateMeasureSettings ControlSettings;
|
||||
ControlSettings.SampleStep = 1;
|
||||
ControlSettings.RadiusInVoxels = 40; // 80 voxels = 20 m across.
|
||||
ControlSettings.CenterXY = FVector2D::ZeroVector;
|
||||
ControlSettings.MaxCells = 300000;
|
||||
ControlSettings.HeadroomCells = 2;
|
||||
ControlSettings.InteriorMarginVoxels = 0;
|
||||
FVoxelStrateMetrics ControlMetrics = VF_MeasureStrateWithSampler(
|
||||
EmptyRoom, 0, 32, 0.0f, ControlSettings, nullptr);
|
||||
TestTrue(TEXT("empty-room player-fit control resolved"),
|
||||
ControlMetrics.bValid && ControlMetrics.bPlayerFitResolved);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("CONTROL empty_room=20m_square_with_one_cell_floor step=1: "
|
||||
"sampled=%lld air=%lld fit_cells=%lld fit_fraction=%.9f "
|
||||
"components=%d largest_component_cells=%lld traversable_share=%.9f"),
|
||||
static_cast<long long>(ControlMetrics.NumSampled),
|
||||
static_cast<long long>(ControlMetrics.NumAir),
|
||||
static_cast<long long>(ControlMetrics.NumPlayerFitCells),
|
||||
ControlMetrics.PlayerFitFraction,
|
||||
ControlMetrics.NumTraversableComponents,
|
||||
static_cast<long long>(ControlMetrics.LargestTraversableComponentCells),
|
||||
ControlMetrics.TraversableComponentShare));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("PLAYER_STENCIL radius=%.3f voxels (%.2f m), half_height=%.3f voxels "
|
||||
"(%.2f m), height=%.3f voxels (%.2f m); rows=ceil(height)=%d "
|
||||
"relative rows 0..%d; floor=air anchor with exactly one solid cell at Z-1; "
|
||||
"no floor tolerance; horizontal integer offsets are center plus four cardinals "
|
||||
"in rows 0..6 and center-only forced in row 7; HeadroomCells=%d is legacy-only"),
|
||||
ControlSettings.PlayerCapsuleRadiusVoxels,
|
||||
ControlSettings.PlayerCapsuleRadiusVoxels * 0.25f,
|
||||
ControlSettings.PlayerCapsuleHalfHeightVoxels,
|
||||
ControlSettings.PlayerCapsuleHalfHeightVoxels * 0.25f,
|
||||
FVoxelPlayerCapsuleConstants::HeightVoxels,
|
||||
FVoxelPlayerCapsuleConstants::HeightVoxels * 0.25f,
|
||||
FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels),
|
||||
FMath::CeilToInt(2.0f * ControlSettings.PlayerCapsuleHalfHeightVoxels) - 1,
|
||||
ControlSettings.HeadroomCells));
|
||||
|
||||
// H3 — the eight fixed showcase identities, measured at step 1 on a mouth-fitted window.
|
||||
// The Maze identity is intentionally the requested seed 0 / index 50 / fourth one-based slot.
|
||||
struct FShowcaseSelection
|
||||
{
|
||||
ECaveGeneratorType Archetype;
|
||||
int32 Seed;
|
||||
int32 CandidateIndex;
|
||||
int32 TargetSlot;
|
||||
const TCHAR* Label;
|
||||
};
|
||||
const FShowcaseSelection Selections[] = {
|
||||
{ ECaveGeneratorType::CrystalChamber, 0, 36, FTestWorld::SlotFlatPlain, TEXT("CrystalChamber") },
|
||||
{ ECaveGeneratorType::FlatPlain, 0, 11, FTestWorld::SlotFlatPlain, TEXT("FlatPlain") },
|
||||
{ ECaveGeneratorType::FloatingIslands, 0, 15, FTestWorld::SlotFlatPlain, TEXT("FloatingIslands") },
|
||||
{ ECaveGeneratorType::Maze, 0, 50, FTestWorld::SlotMaze, TEXT("Maze") },
|
||||
{ ECaveGeneratorType::SurfaceWorld, 0, 10, FTestWorld::SlotSurfaceWorld, TEXT("SurfaceWorld") },
|
||||
{ ECaveGeneratorType::TunnelNetwork, 0, 61, FTestWorld::SlotFlatPlain, TEXT("TunnelNetwork") },
|
||||
{ ECaveGeneratorType::Underwater, 0, 63, FTestWorld::SlotFlatPlain, TEXT("Underwater") },
|
||||
{ ECaveGeneratorType::VerticalShafts, 0, 34, FTestWorld::SlotFlatPlain, TEXT("VerticalShafts") },
|
||||
};
|
||||
|
||||
for (const FShowcaseSelection& Selection : Selections)
|
||||
{
|
||||
const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate(
|
||||
Corpus, Selection.Seed, Selection.CandidateIndex, false);
|
||||
TestTrue(FString::Printf(TEXT("H3 %s candidate identity is valid"), Selection.Label),
|
||||
Candidate.bValid && Candidate.Archetype == Selection.Archetype);
|
||||
if (!Candidate.bValid || Candidate.Archetype != Selection.Archetype)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
VF_SetCandidateOnSlot(World, Candidate, Selection.TargetSlot);
|
||||
FVector ArrivalPoint = FVector::ZeroVector;
|
||||
FVector DeparturePoint = FVector::ZeroVector;
|
||||
int32 ArrivalCount = 0;
|
||||
int32 DepartureCount = 0;
|
||||
for (const FVoxelPassage& Passage : World.StrateManager->GetPassages())
|
||||
{
|
||||
if (Passage.LowerStrateIndex == Selection.TargetSlot
|
||||
&& Passage.UpperStrateIndex + 1 == Selection.TargetSlot)
|
||||
{
|
||||
ArrivalPoint = Passage.LowerPoint;
|
||||
++ArrivalCount;
|
||||
}
|
||||
if (Passage.UpperStrateIndex == Selection.TargetSlot
|
||||
&& Passage.LowerStrateIndex == Selection.TargetSlot + 1)
|
||||
{
|
||||
DeparturePoint = Passage.UpperPoint;
|
||||
++DepartureCount;
|
||||
}
|
||||
}
|
||||
if (ArrivalCount != 1 || DepartureCount != 1)
|
||||
{
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("PLAYER_FIT step=1 fitted-window %s: mouths unavailable arrival=%d departure=%d"),
|
||||
Selection.Label, ArrivalCount, DepartureCount));
|
||||
continue;
|
||||
}
|
||||
|
||||
FVoxelStrateMeasureSettings Settings;
|
||||
Settings.SampleStep = 1;
|
||||
Settings.RadiusInVoxels = 64;
|
||||
Settings.CenterXY = FVector2D(
|
||||
0.5f * (ArrivalPoint.X + DeparturePoint.X),
|
||||
0.5f * (ArrivalPoint.Y + DeparturePoint.Y));
|
||||
Settings.CoverPointA = FVector2D(ArrivalPoint.X, ArrivalPoint.Y);
|
||||
Settings.CoverPointB = FVector2D(DeparturePoint.X, DeparturePoint.Y);
|
||||
Settings.CoverMarginVoxels = FMath::CeilToFloat(
|
||||
Settings.PlayerCapsuleRadiusVoxels) + 2.0f;
|
||||
Settings.MaxCells = FineDiagnosisMaxCells;
|
||||
Settings.MaxRouteRetries = 16;
|
||||
Settings.HeadroomCells = 2;
|
||||
Settings.InteriorMarginVoxels = -1;
|
||||
|
||||
int32 TopVoxelZ = 0;
|
||||
int32 BottomVoxelZ = 0;
|
||||
if (!World.GetSlotVoxelZRange(Selection.TargetSlot, TopVoxelZ, BottomVoxelZ))
|
||||
{
|
||||
AddError(FString::Printf(TEXT("PLAYER_FIT %s: target slot has no Z range."),
|
||||
Selection.Label));
|
||||
continue;
|
||||
}
|
||||
FGeneratorSampler GeneratorSampler(*World.Generator);
|
||||
FVoxelStrateMetrics Metrics;
|
||||
const FVoxelConnectivityDiagnostics Diagnostics =
|
||||
VF_DiagnosePlayerFitConnectivityWithSampler(
|
||||
GeneratorSampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||
VF_BoundarySealForCandidate(Candidate), ArrivalPoint, DeparturePoint,
|
||||
Settings, &Metrics);
|
||||
VF_ReportPlayerFitRow(*this, Selection.Label, Metrics, Diagnostics);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR
|
||||
Reference in new issue
Block a user