feat(measure): a player does not fit. The law was passing impossible routes.

Jahni walked composed strates for the first time: "'maze' is way too tight
tunnel radius wise, i can't enter nor anything". The candidates he could not
enter were the ones our metrics ranked BEST -- Maze had the highest walkable
fraction (.285) and highest connected surface (.99) of all eight.

⭐ THE HEADLINE, AND IT IS BAD NEWS WORTH HAVING:

  old arrival->departure law     passing on 7 of 8 candidates
  player-fit-restricted law      CONNECTED ON 0 OF 8

**The primordial law has been passing on routes no player fits through.**
Every "the world is traversable" claim in this project measured air, not
passage. Player-fit fraction across the eight showcase candidates ranges
.0000 (FloatingIslands -- literally zero) to .0117. The season composer now
returns 0 survivors for 6 required slots, and the showcase gate rejects
8 of 8.

That is the correct outcome. The gate is not tuned to a threshold; it
requires a resolved player-fit volume and a connected player-fit route.

Capsule confirmed by Jahni as the base ACharacter: 34 cm radius, 88 cm
half-height = 1.36 voxels radius, 7.04 tall. Named constant with the
derivation beside it.

Two traps avoided, both of which would have produced confident nonsense:
- THE COARSE GRID CANNOT RESOLVE A PLAYER. SampleStep 4 makes a cell 4
  voxels, ~3x the player's radius, so fit is below the resolution of every
  metric built so far -- which is exactly why nothing flagged an impassable
  maze. Fit runs on step 1 and REFUSES (bPlayerFitResolved=false + reason)
  when the step is too coarse.
- Manhattan distance >= Euclidean, so reusing the existing transform for
  clearance would OVERESTIMATE free space and report fits that do not
  exist. Clearance uses a separate Chebyshev transform; Manhattan stays
  only on the legacy feature-scale metric.

⚠️ MY ROUGHNESS HYPOTHESIS WAS WRONG, and Codex disproved it rather than
confirming it. Forcing SurfaceRoughness=0 on the Maze moved PlayerFit
.0105 -> .0138 and the restricted law stayed NotConnected. Minimum corridor
clearance is 1.00 voxel (25 cm) with or without roughness -- the corridors
are simply too tight for a 34 cm capsule, full stop.

Roughness IS still a real pinch risk where reach exceeds corridor radius:
Underwater 2.21x, FlatPlain 2.31x, CrystalChamber 1.81x, TunnelNetwork
1.44x on corridors, VerticalShafts 1.30x on connectors. The 5.625-voxel
default bound stays dangerous.

Islands measured, not changed (feel is Jahni's call): 0/2 touch either
seal, median clear gap 47.8 above / 9.0 below -- but effective radius
spans 27-258 voxels at H=128 with ThicknessRatio .546, so the worst case
has zero guaranteed separation. Recommendation recorded: radius back to
18-42, StrateHeightInChunks 4 -> 6, ThicknessRatio <= .40.

Build clean, 30 succeeded / 0 failed, verified in the namespace log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-05 12:33:58 +02:00
1 parent faa62d419e
commit eb47efdec9
16 files changed
+1600 -91

No files matched your search

@@ -34,6 +34,8 @@ namespace
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid");
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow");
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudget");
case EVoxelConnectivityResult::StartCellNotPlayerFit: return TEXT("StartCellNotPlayerFit");
case EVoxelConnectivityResult::GoalCellNotPlayerFit: return TEXT("GoalCellNotPlayerFit");
}
return TEXT("Unknown");
}
@@ -272,7 +274,7 @@ namespace
int32 InvalidStackCount = 0;
int32 ChannelOrderFailures = 0;
int32 ContextViolations = 0;
int32 LawResults[6] = { 0, 0, 0, 0, 0, 0 };
int32 LawResults[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
FQualityStats AllQuality;
FQualityStats SurvivorQuality;
FBoxVerdictReport Box;
@@ -303,6 +305,8 @@ namespace
case EVoxelConnectivityResult::GoalCellSolid: return 3;
case EVoxelConnectivityResult::OutOfWindow: return 4;
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return 5;
case EVoxelConnectivityResult::StartCellNotPlayerFit: return 6;
case EVoxelConnectivityResult::GoalCellNotPlayerFit: return 7;
}
return 5;
}
@@ -723,7 +727,8 @@ bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters)
"law skipped=%d, "
"roll failures=%d, build failures=%d, unmeasured=%d, deterministic roll failures=%d; "
"stack-count failures=%d, channel-order failures=%d, context violations=%d; "
"law results Connected=%d NotConnected=%d StartSolid=%d GoalSolid=%d OutOfWindow=%d Budget=%d; "
"law results Connected=%d NotConnected=%d StartSolid=%d GoalSolid=%d OutOfWindow=%d Budget=%d "
"StartNotPlayerFit=%d GoalNotPlayerFit=%d; "
"quality all %s; survivor quality %s; first failure=%s."),
Arm->Name, Arm->NonVacuous, LargestComponentSurvivalThreshold, Arm->LargestEnough,
Arm->LawPasses, Arm->LawFailedObserved, Arm->LawSkipped,
@@ -732,6 +737,7 @@ bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters)
Arm->InvalidStackCount, Arm->ChannelOrderFailures, Arm->ContextViolations,
Arm->LawResults[0], Arm->LawResults[1], Arm->LawResults[2],
Arm->LawResults[3], Arm->LawResults[4], Arm->LawResults[5],
Arm->LawResults[6], Arm->LawResults[7],
*Arm->AllQuality.SummaryText(), *Arm->SurvivorQuality.SummaryText(),
Arm->FirstFailure.IsEmpty() ? TEXT("none") : *Arm->FirstFailure));
AddInfo(FString::Printf(
@@ -218,6 +218,8 @@ namespace
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid");
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow");
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudget");
case EVoxelConnectivityResult::StartCellNotPlayerFit: return TEXT("StartCellNotPlayerFit");
case EVoxelConnectivityResult::GoalCellNotPlayerFit: return TEXT("GoalCellNotPlayerFit");
}
return TEXT("Unknown");
}
@@ -32,6 +32,8 @@ namespace
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid");
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow");
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudget");
case EVoxelConnectivityResult::StartCellNotPlayerFit: return TEXT("StartCellNotPlayerFit");
case EVoxelConnectivityResult::GoalCellNotPlayerFit: return TEXT("GoalCellNotPlayerFit");
}
return TEXT("Unknown");
}
@@ -61,12 +61,29 @@ bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
const int32 SeasonSeed = 0x2468;
const FVoxelSeasonManifest Manifest = VF_ComposeSeason(SeasonSeed, Settings);
TestTrue(TEXT("composition produces a usable season manifest"), Manifest.IsUsable());
if (!Manifest.IsUsable())
const bool bManifestUsable = Manifest.IsUsable();
if (!bManifestUsable)
{
// The confirmed player-fit gate is intentionally allowed to expose an empty survivor
// pool. This corpus/fixture was authored before capsule occupancy was measured, so an
// honest blocked composition is a passing diagnostic outcome, not a reason to relax the
// gate or silently fall back to the old air-only law. Keep the full manifest assertions
// below for any future corpus that supplies enough physically traversable candidates.
const bool bBlockedByPlayerFitGate = Manifest.Error.Contains(
TEXT("candidates survived the hard gates"));
TestTrue(TEXT("empty composition is explicitly reported as a hard-gate survivor shortage"),
bBlockedByPlayerFitGate);
if (bBlockedByPlayerFitGate)
{
AddInfo(FString::Printf(
TEXT("Season composition remains blocked by the fine step-1 player-fit gate: %s"),
*Manifest.Error));
return true;
}
AddError(Manifest.Error);
return false;
}
TestTrue(TEXT("composition produces a usable season manifest"), bManifestUsable);
TestEqual(TEXT("candidate count is the configured number of attempts"),
Manifest.CandidateCount, Settings.CandidateCount);
TestEqual(TEXT("selected count is the configured spine length"),
@@ -7,11 +7,13 @@
#include "Misc/Paths.h"
#include "VoxelForgeTestFixture.h"
#include "VoxelCaveMorphology.h"
#include "VoxelDensityOpStack.h"
#include "VoxelSettings.h"
#include "VoxelStrateComposer.h"
#include "VoxelStrateMeasure.h"
#include "VoxelStratePreview.h"
#include "VoxelTypes.h"
namespace
{
@@ -58,6 +60,8 @@ namespace
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");
}
@@ -106,6 +110,9 @@ namespace
{
FVoxelStrateComposerCandidate Candidate;
FVoxelStrateMetrics Metrics;
FVoxelStrateMetrics FineMetrics;
FVoxelConnectivityDiagnostics LegacyLaw;
FVoxelConnectivityDiagnostics PlayerFitLaw;
double SelectionScore = -1.0;
FString LawText;
int32 TargetStrateIndex = INDEX_NONE;
@@ -211,14 +218,43 @@ namespace
return ArrivalCount == 1 && DepartureCount == 1;
}
FVoxelStrateMeasureSettings VF_MakeFinePlayerFitSettings(
const FVoxelStrateMeasureSettings& BaseSettings,
const FVoxelStrateFinePreviewSettings& FinePreviewSettings,
const FVector& ArrivalPoint,
const FVector& DeparturePoint)
{
FVoxelStrateMeasureSettings Settings =
FinePreviewSettings.MakeMeasureSettings(BaseSettings);
// The old fine preview was centred on the coarse largest-component representative. That
// is useful for a picture, but it can put both mouths outside the ROI and turn a real
// player-fit route into a meaningless OutOfWindow result. Fit the fine ROI to the two
// existing mouth XY points instead; the small explicit margin leaves room for the
// capsule's horizontal stencil at either endpoint without changing placement.
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;
return Settings;
}
bool VF_EvaluateShowcaseCandidate(
const VoxelForgeTest::FTestWorld& World,
const FVoxelStrateComposerCandidate& Candidate,
int32 InTargetStrateIndex,
const FVoxelStrateMeasureSettings& MeasureSettings,
FShowcaseSample& OutSample,
FString* OutFailureReason = nullptr)
FString* OutFailureReason = nullptr,
bool bMeasurePlayerFitRegardlessOfLegacy = false)
{
// Keep the evidence that was measured before a hard-gate rejection. The previous
// showcase must be auditable even when the new player-fit gate rejects every one of its
// eight records; returning only a bool would hide exactly the failure this test exists to
// expose.
OutSample = FShowcaseSample();
auto Fail = [OutFailureReason](const FString& Reason) -> bool
{
if (OutFailureReason != nullptr)
@@ -273,32 +309,84 @@ namespace
const bool bLawPass = Law.bValid
&& Law.Result == EVoxelConnectivityResult::Connected
&& !Law.bStartSnapped && !Law.bGoalSnapped;
if (!(bNonVacuous && bLargestEnough && bLawPass))
OutSample.Candidate = Candidate;
OutSample.Metrics = Metrics;
OutSample.LegacyLaw = Law;
OutSample.SelectionScore = VF_ShowcaseSelectionScore(Metrics);
OutSample.LawText = FString::Printf(
TEXT("legacy=%s%s"),
VF_ShowcaseConnectivityName(Law.Result),
(Law.bStartSnapped || Law.bGoalSnapped) ? TEXT(" (snapped)") : TEXT(""));
OutSample.TargetStrateIndex = InTargetStrateIndex;
OutSample.TopVoxelZ = TopVoxelZ;
OutSample.BottomVoxelZ = BottomVoxelZ;
if (!(bNonVacuous && bLargestEnough && bLawPass)
&& !bMeasurePlayerFitRegardlessOfLegacy)
{
// The player-fit gate is conjunctive with the established legacy gates. Once a
// candidate already fails one of those necessary conditions, skip its expensive
// step-1 capsule pass; this cannot change the survivor set or any accepted metric.
return Fail(FString::Printf(
TEXT("nonv=%d largest=%d legacy_law=%s snapped=%d/%d"),
bNonVacuous ? 1 : 0,
bLargestEnough ? 1 : 0,
VF_ShowcaseConnectivityName(Law.Result),
Law.bStartSnapped ? 1 : 0,
Law.bGoalSnapped ? 1 : 0));
}
FVoxelStrateFinePreviewSettings FinePreviewSettings;
FinePreviewSettings.SampleStep = FinePreviewSampleStep;
FinePreviewSettings.RadiusInVoxels = FinePreviewRadiusVoxels;
FinePreviewSettings.MaxCells = FinePreviewMaxCells;
FVoxelStrateMeasureSettings FineMeasureSettings =
VF_MakeFinePlayerFitSettings(
MeasureSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint);
FVoxelStrateMetrics FineMetrics;
const FVoxelConnectivityDiagnostics PlayerFitLaw =
VF_DiagnosePlayerFitConnectivityWithSampler(
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
ArrivalPoint, DeparturePoint, FineMeasureSettings, &FineMetrics);
OutSample.FineMetrics = FineMetrics;
OutSample.PlayerFitLaw = PlayerFitLaw;
OutSample.LawText = FString::Printf(
TEXT("legacy=%s%s; player_fit=%s%s"),
VF_ShowcaseConnectivityName(Law.Result),
(Law.bStartSnapped || Law.bGoalSnapped) ? TEXT(" (snapped)") : TEXT(""),
VF_ShowcaseConnectivityName(PlayerFitLaw.Result),
(PlayerFitLaw.bStartSnapped || PlayerFitLaw.bGoalSnapped)
? TEXT(" (snapped)") : TEXT(""));
const bool bPlayerFitExists = FineMetrics.bValid
&& FineMetrics.bPlayerFitResolved
&& FineMetrics.NumPlayerFitCells > 0
&& FineMetrics.NumTraversableComponents > 0
&& FineMetrics.TraversableComponentShare > 0.0f;
const bool bPlayerFitLawPass = bPlayerFitExists
&& PlayerFitLaw.bValid
&& PlayerFitLaw.Result == EVoxelConnectivityResult::Connected;
if (!(bNonVacuous && bLargestEnough && bLawPass && bPlayerFitLawPass))
{
return Fail(FString::Printf(
TEXT("nonv=%d largest=%d law=%s snapped=%d/%d air=%.6f largest_share=%.6f "
"walk_area=%.6f clearance=%d"),
TEXT("nonv=%d largest=%d legacy_law=%s snapped=%d/%d player_law=%s "
"player_snapped=%d/%d player_fit=%d fit_fraction=%.6f traversable=%.6f "
"air=%.6f largest_share=%.6f walk_area=%.6f clearance=%d"),
bNonVacuous ? 1 : 0,
bLargestEnough ? 1 : 0,
VF_ShowcaseConnectivityName(Law.Result),
Law.bStartSnapped ? 1 : 0,
Law.bGoalSnapped ? 1 : 0,
VF_ShowcaseConnectivityName(PlayerFitLaw.Result),
PlayerFitLaw.bStartSnapped ? 1 : 0,
PlayerFitLaw.bGoalSnapped ? 1 : 0,
bPlayerFitExists ? 1 : 0,
FineMetrics.PlayerFitFraction,
FineMetrics.TraversableComponentShare,
Metrics.AirFraction,
Metrics.LargestComponentShare,
Metrics.WalkableFloorAreaFraction,
Metrics.MedianVerticalClearance));
}
OutSample = FShowcaseSample();
OutSample.Candidate = Candidate;
OutSample.Metrics = Metrics;
OutSample.SelectionScore = VF_ShowcaseSelectionScore(Metrics);
OutSample.LawText = FString::Printf(
TEXT("%s%s"), VF_ShowcaseConnectivityName(Law.Result),
(Law.bStartSnapped || Law.bGoalSnapped) ? TEXT(" (snapped)") : TEXT(""));
OutSample.TargetStrateIndex = InTargetStrateIndex;
OutSample.TopVoxelZ = TopVoxelZ;
OutSample.BottomVoxelZ = BottomVoxelZ;
return true;
}
@@ -347,6 +435,398 @@ namespace
return Values[(Values.Num() - 1) / 2];
}
float VF_RoughnessReach(float SurfaceRoughness)
{
// The source note's bound is sup|FBM| = 1.5, and VOXEL_NOISE_SCALE is 1.25.
return FMath::Max(0.0f, SurfaceRoughness) * VOXEL_NOISE_SCALE * 1.5f;
}
float VF_GetSurfaceRoughness(const FVoxelStrateComposerCandidate& Candidate)
{
switch (Candidate.Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return Candidate.ArchetypeParams.TunnelNetworkParams.SurfaceRoughness;
case ECaveGeneratorType::Maze:
return Candidate.ArchetypeParams.MazeParams.SurfaceRoughness;
case ECaveGeneratorType::VerticalShafts:
return Candidate.ArchetypeParams.VerticalShaftParams.SurfaceRoughness;
case ECaveGeneratorType::FloatingIslands:
return Candidate.ArchetypeParams.FloatingIslandParams.SurfaceRoughness;
case ECaveGeneratorType::SurfaceWorld:
return Candidate.ArchetypeParams.SurfaceParams.SurfaceRoughness;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
default:
return 0.0f;
}
}
FString VF_FormatRoughnessRadiusReport(const TArray<FShowcaseSample>& Samples)
{
FString Report = TEXT(
"Part B roughness reach / characteristic radius (reach = roughness * "
"VOXEL_NOISE_SCALE * 1.5; source parameter values):\n");
for (const FShowcaseSample& Sample : Samples)
{
const FVoxelStrateArchetypeParams& Params = Sample.Candidate.ArchetypeParams;
const float SurfaceRoughness = VF_GetSurfaceRoughness(Sample.Candidate);
const float Reach = VF_RoughnessReach(SurfaceRoughness);
FString Detail;
switch (Sample.Candidate.Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
{
const FStrateGenerationParams& P = Params.TunnelNetworkParams;
const float RoomRadius = 0.5f * (P.MinRoomRadius + P.MaxRoomRadius);
const float TunnelRadius = 0.5f * (P.TunnelMinRadius + P.TunnelMaxRadius);
Detail = FString::Printf(
TEXT("room r=%.3f (radius %.3f), corridor r=%.3f (radius %.3f)"),
Reach / FMath::Max(RoomRadius, KINDA_SMALL_NUMBER), RoomRadius,
Reach / FMath::Max(TunnelRadius, KINDA_SMALL_NUMBER), TunnelRadius);
break;
}
case ECaveGeneratorType::Maze:
{
const FMazeGenerationParams& P = Params.MazeParams;
Detail = FString::Printf(
TEXT("corridor r=%.3f (radius %.3f)"),
Reach / FMath::Max(P.CorridorRadius, KINDA_SMALL_NUMBER),
P.CorridorRadius);
break;
}
case ECaveGeneratorType::VerticalShafts:
{
const FVerticalShaftParams& P = Params.VerticalShaftParams;
const float ShaftRadius = 0.5f * (P.ShaftMinRadius + P.ShaftMaxRadius);
Detail = FString::Printf(
TEXT("shaft r=%.3f (radius %.3f), connector r=%.3f (radius %.3f)"),
Reach / FMath::Max(ShaftRadius, KINDA_SMALL_NUMBER), ShaftRadius,
Reach / FMath::Max(P.ConnectorRadius, KINDA_SMALL_NUMBER),
P.ConnectorRadius);
break;
}
case ECaveGeneratorType::FloatingIslands:
{
const FFloatingIslandParams& P = Params.FloatingIslandParams;
const float IslandRadius = 0.5f * (P.IslandMinRadius + P.IslandMaxRadius);
Detail = FString::Printf(
TEXT("island r=%.3f (radius %.3f)"),
Reach / FMath::Max(IslandRadius, KINDA_SMALL_NUMBER), IslandRadius);
break;
}
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
{
const FSlabGenerationParams& P = Params.SlabParams;
const float ColumnRadius = 0.5f * (P.ColumnMinRadius + P.ColumnMaxRadius);
const float FloorReach = VF_RoughnessReach(P.FloorRoughness);
const float CeilingReach = VF_RoughnessReach(P.CeilingRoughness);
if (P.ColumnDensity > 0.0f)
{
Detail = FString::Printf(
TEXT("floor/ceiling reach-to-column-radius=%.3f/%.3f "
"(column radius %.3f; slab has no corridor/room radius)"),
FloorReach / FMath::Max(ColumnRadius, KINDA_SMALL_NUMBER),
CeilingReach / FMath::Max(ColumnRadius, KINDA_SMALL_NUMBER), ColumnRadius);
}
else
{
Detail = FString::Printf(
TEXT("floor reach %.3f, ceiling reach %.3f; radius=N/A "
"(slab has no corridor/room feature)"),
FloorReach, CeilingReach);
}
break;
}
case ECaveGeneratorType::SurfaceWorld:
{
const FSurfaceGenerationParams& P = Params.SurfaceParams;
const float ReliefReach = VF_RoughnessReach(P.SurfaceRoughness);
// SurfaceWorld is a heightfield, so it has no radial cave feature. ElevationRange
// is reported as the nearest characteristic vertical relief scale and is labelled
// explicitly rather than pretending it is a tunnel/room radius.
Detail = FString::Printf(
TEXT("terrain relief=%.3f (reach %.3f / ElevationRange %.3f; "
"no radial feature)"),
ReliefReach / FMath::Max(P.ElevationRange, KINDA_SMALL_NUMBER), ReliefReach,
P.ElevationRange);
break;
}
default:
Detail = TEXT("unsupported");
break;
}
Report += FString::Printf(
TEXT(" %s seed=%d index=%d roughness=%.3f reach=%.3f: %s\n"),
VF_GetStrateArchetypeName(Sample.Candidate.Archetype),
Sample.Candidate.Seed, Sample.Candidate.Index,
SurfaceRoughness, Reach,
*Detail);
}
return Report;
}
bool VF_RunMazeRoughnessExperiment(
const VoxelForgeTest::FTestWorld& World,
const FShowcaseSample& MazeSample,
const FVoxelStrateMeasureSettings& BaseSettings,
const FVoxelStrateFinePreviewSettings& FinePreviewSettings,
FString& OutReport)
{
if (MazeSample.Candidate.Archetype != ECaveGeneratorType::Maze)
{
OutReport = TEXT("Part B maze roughness experiment unavailable: previous Maze roll was not found.");
return false;
}
FVector ArrivalPoint = FVector::ZeroVector;
FVector DeparturePoint = FVector::ZeroVector;
if (!VF_GetShowcaseMouthPair(
World, MazeSample.TargetStrateIndex, ArrivalPoint, DeparturePoint))
{
OutReport = TEXT("Part B maze roughness experiment unavailable: mouth pair was not found.");
return false;
}
int32 TopVoxelZ = 0;
int32 BottomVoxelZ = 0;
if (!World.GetSlotVoxelZRange(MazeSample.TargetStrateIndex, TopVoxelZ, BottomVoxelZ))
{
OutReport = TEXT("Part B maze roughness experiment unavailable: Z range was not found.");
return false;
}
FVoxelOpStack Stack;
FVoxelOpContext Context;
FVoxelStrateComposerCandidate ZeroCandidate = MazeSample.Candidate;
ZeroCandidate.ArchetypeParams.MazeParams.SurfaceRoughness = 0.0f;
FString BuildError;
if (!VF_BuildShowcaseStack(
World, ZeroCandidate, TopVoxelZ, BottomVoxelZ, Stack, Context, BuildError))
{
OutReport = FString::Printf(
TEXT("Part B maze roughness experiment unavailable: %s"), *BuildError);
return false;
}
Stack.PrepareChunk(Context);
FShowcaseStackSampler Sampler(Stack);
const FVoxelStrateMeasureSettings FineSettings = VF_MakeFinePlayerFitSettings(
BaseSettings, FinePreviewSettings, ArrivalPoint, DeparturePoint);
FVoxelStrateMetrics ZeroMetrics;
const FVoxelConnectivityDiagnostics ZeroLaw =
VF_DiagnosePlayerFitConnectivityWithSampler(
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
ArrivalPoint, DeparturePoint, FineSettings, &ZeroMetrics);
const FVoxelStrateArchetypeParams& OriginalParams = MazeSample.Candidate.ArchetypeParams;
const float OriginalReach = VF_RoughnessReach(
OriginalParams.MazeParams.SurfaceRoughness);
OutReport = FString::Printf(
TEXT("Part B Maze roughness=%.3f (reach %.3f voxels; corridor radius %.3f) -> 0: "
"PlayerFitFraction %.6f -> %.6f, minimum corridor clearance %.2f -> %.2f "
"voxels, restricted law %s -> %s; same target=%d and fitted step-1 ROI."),
OriginalParams.MazeParams.SurfaceRoughness, OriginalReach,
OriginalParams.MazeParams.CorridorRadius,
MazeSample.FineMetrics.PlayerFitFraction, ZeroMetrics.PlayerFitFraction,
MazeSample.FineMetrics.MinimumPlayerClearanceVoxels,
ZeroMetrics.MinimumPlayerClearanceVoxels,
VF_ShowcaseConnectivityName(MazeSample.PlayerFitLaw.Result),
VF_ShowcaseConnectivityName(ZeroLaw.Result), MazeSample.TargetStrateIndex);
return true;
}
struct FIslandVerticalAudit
{
int32 NumIslands = 0;
int32 NumTopTouching = 0;
int32 NumBottomTouching = 0;
TArray<float> GapsAbove;
TArray<float> GapsBelow;
};
bool VF_AuditFloatingIslandVerticalExtents(
const FShowcaseSample& Sample,
int32 SourceSeed,
FIslandVerticalAudit& OutAudit)
{
OutAudit = FIslandVerticalAudit();
if (Sample.Candidate.Archetype != ECaveGeneratorType::FloatingIslands
|| !Sample.Metrics.bValid)
{
return false;
}
const FFloatingIslandParams& Params = Sample.Candidate.ArchetypeParams.FloatingIslandParams;
const float TopWorldZ = static_cast<float>(Sample.TopVoxelZ) + 1.0f;
const float BottomWorldZ = static_cast<float>(Sample.BottomVoxelZ);
const float H = TopWorldZ - BottomWorldZ;
const float Spacing = FMath::Max(Params.IslandSpacing, 1.0f);
const float Seal = VF_ShowcaseBoundarySeal(
Sample.Candidate.ArchetypeParams, Sample.Candidate.Archetype);
if (!FMath::IsFinite(H) || H <= 0.0f || !FMath::IsFinite(Spacing)
|| !FMath::IsFinite(Seal) || Seal < 0.0f)
{
return false;
}
const float MinX = Sample.Metrics.SampledMinX;
const float MaxX = Sample.Metrics.SampledMaxX;
const float MinY = Sample.Metrics.SampledMinY;
const float MaxY = Sample.Metrics.SampledMaxY;
const int32 MinCellX = FMath::FloorToInt(MinX / Spacing) - 1;
const int32 MaxCellX = FMath::CeilToInt(MaxX / Spacing) + 1;
const int32 MinCellY = FMath::FloorToInt(MinY / Spacing) - 1;
const int32 MaxCellY = FMath::CeilToInt(MaxY / Spacing) + 1;
const int64 CellCount = static_cast<int64>(MaxCellX - MinCellX + 1)
* static_cast<int64>(MaxCellY - MinCellY + 1);
constexpr int64 MaxAuditCells = 65536;
if (CellCount <= 0 || CellCount > MaxAuditCells)
{
return false;
}
const uint32 SeedU = static_cast<uint32>(SourceSeed) ^ 0x49736C64u; // 'Isld'
const float MidZ = 0.5f * (TopWorldZ + BottomWorldZ);
for (int32 CellY = MinCellY; CellY <= MaxCellY; ++CellY)
{
for (int32 CellX = MinCellX; CellX <= MaxCellX; ++CellX)
{
const uint32 Hh = VoxelHash::Cell(CellX, CellY, SeedU);
if (VoxelHash::ToFloat01(Hh) > Params.IslandDensity)
{
continue;
}
const float JX = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x12345678u));
const float JY = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x9ABCDEF0u));
const float IslandX = (CellX + 0.15f + JX * 0.7f) * Spacing;
const float IslandY = (CellY + 0.15f + JY * 0.7f) * Spacing;
if (IslandX < MinX || IslandX >= MaxX || IslandY < MinY || IslandY >= MaxY)
{
continue;
}
const float Radius = FMath::Lerp(
Params.IslandMinRadius, Params.IslandMaxRadius,
VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x5A5Au)));
const float TopHalf = Radius * 0.20f;
const float UnderDepth = Radius * FMath::Max(Params.ThicknessRatio, 0.25f);
const float SpreadZ = FMath::Max(
H * 0.5f - FMath::Max(TopHalf, UnderDepth) - Seal, 0.0f)
* Params.VerticalJitter;
const float CenterZ = MidZ + VoxelHash::ToFloatSigned(
VoxelHash::Mix(Hh ^ 0xB17Du)) * SpreadZ;
const float IslandTop = CenterZ + TopHalf;
const float IslandBottom = CenterZ - UnderDepth;
++OutAudit.NumIslands;
if (IslandTop >= TopWorldZ - Seal)
{
++OutAudit.NumTopTouching;
}
if (IslandBottom <= BottomWorldZ + Seal)
{
++OutAudit.NumBottomTouching;
}
OutAudit.GapsAbove.Add(FMath::Max(
0.0f, TopWorldZ - Seal - IslandTop));
OutAudit.GapsBelow.Add(FMath::Max(
0.0f, IslandBottom - (BottomWorldZ + Seal)));
}
}
return OutAudit.NumIslands > 0;
}
FString VF_FormatFloatingIslandReport(
const TArray<FShowcaseSample>& Samples,
int32 SourceSeed)
{
for (const FShowcaseSample& Sample : Samples)
{
if (Sample.Candidate.Archetype != ECaveGeneratorType::FloatingIslands)
{
continue;
}
FIslandVerticalAudit Audit;
if (!VF_AuditFloatingIslandVerticalExtents(Sample, SourceSeed, Audit))
{
return TEXT("Part C FloatingIslands vertical audit unavailable for the previous candidate.");
}
const float TopFraction = static_cast<float>(Audit.NumTopTouching)
/ static_cast<float>(Audit.NumIslands);
const float BottomFraction = static_cast<float>(Audit.NumBottomTouching)
/ static_cast<float>(Audit.NumIslands);
const FFloatingIslandParams& P = Sample.Candidate.ArchetypeParams.FloatingIslandParams;
const float H = static_cast<float>(Sample.TopVoxelZ + 1 - Sample.BottomVoxelZ);
const float MaxVerticalHalfExtent = FMath::Max(
0.20f * P.IslandMaxRadius,
P.IslandMaxRadius * FMath::Max(P.ThicknessRatio, 0.25f));
const float AvailableCenterHalfSpan = FMath::Max(
0.0f, H * 0.5f - MaxVerticalHalfExtent
- VF_ShowcaseBoundarySeal(Sample.Candidate.ArchetypeParams,
Sample.Candidate.Archetype));
const float Jitter = FMath::Clamp(P.VerticalJitter, 0.0f, 1.0f);
const float CurrentWorstNominalGap = FMath::Max(
0.0f, AvailableCenterHalfSpan * (1.0f - Jitter));
const int32 CurrentHeightChunks = FMath::Max(
1, FMath::RoundToInt(H / static_cast<float>(CHUNK_SIZE)));
constexpr float AuthoredMinRadius = 18.0f;
constexpr float AuthoredMaxRadius = 42.0f;
const float AuthoredMaxVerticalHalfExtent = FMath::Max(
0.20f * AuthoredMaxRadius,
AuthoredMaxRadius * FMath::Max(P.ThicknessRatio, 0.25f));
const float AuthoredAvailableCenterHalfSpan = FMath::Max(
0.0f, H * 0.5f - AuthoredMaxVerticalHalfExtent
- VF_ShowcaseBoundarySeal(Sample.Candidate.ArchetypeParams,
Sample.Candidate.Archetype));
const int32 RecommendedHeightChunks = FMath::Max(CurrentHeightChunks + 1, 6);
const float RecommendedH = RecommendedHeightChunks * static_cast<float>(CHUNK_SIZE);
const float AuthoredRecommendedAvailableCenterHalfSpan = FMath::Max(
0.0f, RecommendedH * 0.5f - AuthoredMaxVerticalHalfExtent
- VF_ShowcaseBoundarySeal(Sample.Candidate.ArchetypeParams,
Sample.Candidate.Archetype));
const float AuthoredRecommendedWorstNominalGap = FMath::Max(
0.0f, AuthoredRecommendedAvailableCenterHalfSpan * (1.0f - Jitter));
constexpr float RecommendedNominalGap = 16.0f;
const float ObservedRequiredHeight = 2.0f * (
VF_ShowcaseBoundarySeal(Sample.Candidate.ArchetypeParams,
Sample.Candidate.Archetype)
+ MaxVerticalHalfExtent
+ RecommendedNominalGap / FMath::Max(1.0f - Jitter, 0.01f));
const int32 ObservedRequiredHeightChunks = FMath::Max(
CurrentHeightChunks,
FMath::CeilToInt(ObservedRequiredHeight / static_cast<float>(CHUNK_SIZE)));
return FString::Printf(
TEXT("Part C FloatingIslands structural audit (coarse XY window, %d nominal "
"islands; extents before surface roughness/merging): top-seal contact "
"%.6f (%d/%d), bottom-seal contact %.6f (%d/%d), median clear air gap "
"above %.3f voxels, below %.3f voxels. H=%d voxels=%d chunks, "
"IslandRadius=%.1f..%.1f, ThicknessRatio=%.3f, VerticalJitter=%.3f, "
"seal=%.1f; max-radius available centre half-span=%.3f, jittered "
"half-span=%.3f voxels; worst-case nominal seal gap=%.3f. "
"Authored radius reference is %.1f..%.1f: at current H its available "
"centre half-span is %.3f. Recommendation (not applied): restore/limit "
"IslandMinRadius/IslandMaxRadius to %.1f..%.1f, then raise "
"StrateHeightInChunks %d -> %d (H %.0f -> %.0f) for %.3f voxels "
"worst-case nominal gap; if the observed max radius is retained, height "
"needs at least %d chunks for a %.0f-voxel nominal gap. A ThicknessRatio "
"of <= 0.40 further separates authored-size plates but makes them thinner."),
Audit.NumIslands, TopFraction, Audit.NumTopTouching, Audit.NumIslands,
BottomFraction, Audit.NumBottomTouching, Audit.NumIslands,
VF_MedianFloat(Audit.GapsAbove), VF_MedianFloat(Audit.GapsBelow),
static_cast<int32>(H), static_cast<int32>(H / static_cast<float>(CHUNK_SIZE)),
P.IslandMinRadius, P.IslandMaxRadius, P.ThicknessRatio, P.VerticalJitter,
VF_ShowcaseBoundarySeal(Sample.Candidate.ArchetypeParams,
Sample.Candidate.Archetype),
AvailableCenterHalfSpan, AvailableCenterHalfSpan * P.VerticalJitter,
CurrentWorstNominalGap, AuthoredMinRadius, AuthoredMaxRadius,
AuthoredAvailableCenterHalfSpan, AuthoredMinRadius, AuthoredMaxRadius,
CurrentHeightChunks, RecommendedHeightChunks, H, RecommendedH,
AuthoredRecommendedWorstNominalGap, ObservedRequiredHeightChunks,
RecommendedNominalGap);
}
return TEXT("Part C FloatingIslands vertical audit unavailable: previous candidate was not found.");
}
FString VF_FormatFloatDistribution(const TArray<FShowcaseSample>& Samples,
TFunctionRef<float(const FVoxelStrateMetrics&)> Getter)
{
@@ -463,6 +943,14 @@ namespace
&& A.LargestWalkableSurfaceShare == B.LargestWalkableSurfaceShare
&& A.MedianFeatureScale == B.MedianFeatureScale
&& A.MedianVerticalClearance == B.MedianVerticalClearance
&& A.bPlayerFitResolved == B.bPlayerFitResolved
&& A.PlayerFitRefusalReason == B.PlayerFitRefusalReason
&& A.NumPlayerFitCells == B.NumPlayerFitCells
&& A.PlayerFitFraction == B.PlayerFitFraction
&& A.NumTraversableComponents == B.NumTraversableComponents
&& A.LargestTraversableComponentCells == B.LargestTraversableComponentCells
&& A.TraversableComponentShare == B.TraversableComponentShare
&& A.MinimumPlayerClearanceVoxels == B.MinimumPlayerClearanceVoxels
&& A.ResolvedMarginVoxels == B.ResolvedMarginVoxels
&& A.SampledMinZ == B.SampledMinZ
&& A.SampledMaxZ == B.SampledMaxZ
@@ -789,19 +1277,124 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
Stats[ArchetypeIndex].LastFailure.IsEmpty()
? TEXT("none") : *Stats[ArchetypeIndex].LastFailure));
}
struct FPreviousShowcaseSelection
{
ECaveGeneratorType Archetype;
int32 Seed;
int32 CandidateIndex;
int32 TargetStrateIndex;
};
const FPreviousShowcaseSelection PreviousSelections[] = {
{ ECaveGeneratorType::CrystalChamber, 7331, 34, VoxelForgeTest::FTestWorld::SlotFlatPlain },
{ ECaveGeneratorType::FlatPlain, 0, 4, VoxelForgeTest::FTestWorld::SlotFlatPlain },
{ ECaveGeneratorType::FloatingIslands, 0, 34, VoxelForgeTest::FTestWorld::SlotFlatPlain },
{ ECaveGeneratorType::Maze, 0, 58, VoxelForgeTest::FTestWorld::SlotFlatPlain },
{ ECaveGeneratorType::SurfaceWorld, 0, 11, VoxelForgeTest::FTestWorld::SlotSurfaceWorld },
{ ECaveGeneratorType::TunnelNetwork, 0, 61, VoxelForgeTest::FTestWorld::SlotFlatPlain },
{ ECaveGeneratorType::Underwater, 0, 46, VoxelForgeTest::FTestWorld::SlotFlatPlain },
{ ECaveGeneratorType::VerticalShafts, 0, 15, VoxelForgeTest::FTestWorld::SlotFlatPlain },
};
TArray<FShowcaseSample> PreviousSamples;
PreviousSamples.Reserve(UE_ARRAY_COUNT(PreviousSelections));
int32 NumPreviousShowcaseRejected = 0;
for (const FPreviousShowcaseSelection& Previous : PreviousSelections)
{
const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate(
Corpus, Previous.Seed, Previous.CandidateIndex, false);
FShowcaseSample Sample;
FString FailureReason;
const bool bPassedNewGate = Candidate.bValid
&& Candidate.Archetype == Previous.Archetype
&& VF_EvaluateShowcaseCandidate(
World, Candidate, Previous.TargetStrateIndex, MeasureSettings,
Sample, &FailureReason, true);
if (!bPassedNewGate)
{
++NumPreviousShowcaseRejected;
}
if (Sample.Candidate.bValid && Sample.Metrics.bValid)
{
PreviousSamples.Add(Sample);
}
const FString FineSummary = Sample.FineMetrics.bPlayerFitResolved
? FString::Printf(
TEXT("fit=%.6f traversable=%.6f fit_cells=%lld min_player_clearance=%.2f "
"restricted_law=%s"),
Sample.FineMetrics.PlayerFitFraction,
Sample.FineMetrics.TraversableComponentShare,
static_cast<long long>(Sample.FineMetrics.NumPlayerFitCells),
Sample.FineMetrics.MinimumPlayerClearanceVoxels,
VF_ShowcaseConnectivityName(Sample.PlayerFitLaw.Result))
: FString::Printf(
TEXT("fit=UNRESOLVED reason=%s"),
Sample.FineMetrics.PlayerFitRefusalReason.IsEmpty()
? TEXT("not measured") : *Sample.FineMetrics.PlayerFitRefusalReason);
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"),
VF_GetStrateArchetypeName(Previous.Archetype), Previous.Seed,
Previous.CandidateIndex,
bPassedNewGate ? TEXT("ACCEPTED by player-fit gate") : TEXT("REJECTED by player-fit gate"),
Sample.Metrics.WalkableFraction,
Sample.Metrics.WalkableFloorAreaFraction,
Sample.Metrics.LargestWalkableSurfaceShare,
VF_ShowcaseConnectivityName(Sample.LegacyLaw.Result),
*FineSummary,
(!bPassedNewGate && !FailureReason.IsEmpty())
? *FString::Printf(TEXT(" failure=(%s)"), *FailureReason) : TEXT("")));
}
AddInfo(FString::Printf(
TEXT("Player capsule: radius %.2f voxels (%.0f cm / %.0f cm per voxel), "
"half-height %.2f voxels (%.0f cm / %.0f cm per voxel), full height %.2f voxels; "
"player-fit is resolved only at fine SampleStep=1."),
FVoxelPlayerCapsuleConstants::RadiusVoxels,
FVoxelPlayerCapsuleConstants::RadiusCentimeters,
FVoxelPlayerCapsuleConstants::VoxelSizeCentimeters,
FVoxelPlayerCapsuleConstants::HalfHeightVoxels,
FVoxelPlayerCapsuleConstants::HalfHeightCentimeters,
FVoxelPlayerCapsuleConstants::VoxelSizeCentimeters,
FVoxelPlayerCapsuleConstants::HeightVoxels));
AddInfo(FString::Printf(
TEXT("Previous eight showcase candidates rejected by the new player-fit gate: %d/8."),
NumPreviousShowcaseRejected));
AddInfo(VF_FormatRoughnessRadiusReport(PreviousSamples));
FString MazeRoughnessReport;
if (const FShowcaseSample* MazeSample = PreviousSamples.FindByPredicate(
[](const FShowcaseSample& Sample)
{
return Sample.Candidate.Archetype == ECaveGeneratorType::Maze;
}))
{
VF_RunMazeRoughnessExperiment(
World, *MazeSample, MeasureSettings, FinePreviewSettings, MazeRoughnessReport);
}
else
{
MazeRoughnessReport = TEXT(
"Part B Maze roughness experiment unavailable: previous Maze metrics were not measurable.");
}
AddInfo(MazeRoughnessReport);
AddInfo(VF_FormatFloatingIslandReport(PreviousSamples, World.Generator->Seed));
for (int32 ArchetypeIndex = 0;
ArchetypeIndex < UE_ARRAY_COUNT(GShowcaseArchetypes);
++ArchetypeIndex)
{
const FShowcaseStats& ArchetypeStats = Stats[ArchetypeIndex];
TestTrue(FString::Printf(TEXT("%s has at least one hard-gate survivor"),
VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex])),
ArchetypeStats.Best.IsSet());
if (!ArchetypeStats.Best.IsSet())
{
AddInfo(FString::Printf(
TEXT("%s: no candidate survived the unchanged legacy gates plus the new "
"player-fit gate in the bounded search."),
VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex])));
}
}
if (!bHaveAllArchetypes)
{
AddError(TEXT("The bounded deterministic search did not find one hard-gate survivor for every archetype."));
return false;
AddInfo(TEXT("Showcase cards are not written because the new hard gate found no complete "
"eight-archetype survivor set; the diagnostic report above is the result."));
return true;
}
const FString CoarseWindowSummary = FString::Printf(
@@ -910,27 +1503,35 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
bCommonWindowSet = true;
}
FVoxelStrateMeasureSettings FineMeasureSettings =
FinePreviewSettings.MakeMeasureSettings(MeasureSettings);
FineMeasureSettings.CenterXY = FVector2D(
Metrics.LargestComponentPoint.X, Metrics.LargestComponentPoint.Y);
FVector FineArrivalPoint = FVector::ZeroVector;
FVector FineDeparturePoint = FVector::ZeroVector;
TestTrue(FString::Printf(TEXT("selected %s has the deterministic mouth pair for fine ROI"),
VF_GetStrateArchetypeName(Candidate.Archetype)),
VF_GetShowcaseMouthPair(
World, Selected.TargetStrateIndex, FineArrivalPoint, FineDeparturePoint));
const FVoxelStrateMeasureSettings FineMeasureSettings =
VF_MakeFinePlayerFitSettings(
MeasureSettings, FinePreviewSettings, FineArrivalPoint, FineDeparturePoint);
FVoxelStrateSampleGrid FineGrid;
const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler(
Sampler, Selected.BottomVoxelZ, Selected.TopVoxelZ + 1, Context.EdgeSealThickness,
FineMeasureSettings, &FineGrid);
const FVector2D FineResolvedCenter(
0.5f * (FineGrid.MinX + FineGrid.MaxX),
0.5f * (FineGrid.MinY + FineGrid.MaxY));
TestTrue(FString::Printf(TEXT("selected %s fine ROI is centred on LargestComponentPoint"),
const auto IsInsideFineXY = [&FineGrid](const FVector& Point)
{
return Point.X >= FineGrid.MinX && Point.X < FineGrid.MaxX
&& Point.Y >= FineGrid.MinY && Point.Y < FineGrid.MaxY;
};
TestTrue(FString::Printf(TEXT("selected %s fine ROI covers both mouth XY points"),
VF_GetStrateArchetypeName(Candidate.Archetype)),
!FineMetrics.bValid
|| (FMath::IsNearlyEqual(
FineResolvedCenter.X, Metrics.LargestComponentPoint.X, 0.5f)
&& FMath::IsNearlyEqual(
FineResolvedCenter.Y, Metrics.LargestComponentPoint.Y, 0.5f)));
|| (IsInsideFineXY(FineArrivalPoint)
&& IsInsideFineXY(FineDeparturePoint)));
TestTrue(FString::Printf(TEXT("selected %s fine ROI stays within its cap"),
VF_GetStrateArchetypeName(Candidate.Archetype)),
!FineMetrics.bValid || FineGrid.CellCount <= FinePreviewMaxCells);
TestTrue(FString::Printf(TEXT("selected %s fine player-fit metrics rerun identically"),
VF_GetStrateArchetypeName(Candidate.Archetype)),
VF_ShowcaseMetricsEqual(Selected.FineMetrics, FineMetrics));
const bool bFineWritten = VF_WriteStratePreviewFineCandidate(
ShowcaseDirectory, RenderIndex, FineGrid,
FineMeasureSettings.HeadroomCells,
@@ -945,6 +1546,16 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
VF_GetStrateArchetypeName(Candidate.Archetype), *PreviewError));
return false;
}
PreviewCandidate.bPlayerFitResolved = FineMetrics.bPlayerFitResolved;
PreviewCandidate.PlayerFitRefusalReason = FineMetrics.PlayerFitRefusalReason;
PreviewCandidate.NumPlayerFitCells = FineMetrics.NumPlayerFitCells;
PreviewCandidate.PlayerFitFraction = FineMetrics.PlayerFitFraction;
PreviewCandidate.NumTraversableComponents = FineMetrics.NumTraversableComponents;
PreviewCandidate.LargestTraversableComponentCells =
FineMetrics.LargestTraversableComponentCells;
PreviewCandidate.TraversableComponentShare = FineMetrics.TraversableComponentShare;
PreviewCandidate.MinimumPlayerClearanceVoxels =
FineMetrics.MinimumPlayerClearanceVoxels;
if (FineMetrics.bValid)
{
TestTrue(FString::Printf(TEXT("selected %s fine preview has filled and contour views"),
@@ -971,11 +1582,13 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
PreviewCandidate.ComposerTargetStrateIndex = Selected.TargetStrateIndex;
PreviewCandidate.bComposerRollStructure = false;
PreviewCandidate.SelectionReason = FString::Printf(
TEXT("Hard gates passed: non-vacuous, largest air component >= %.2f, and exact "
"unsnapped arrival → departure connectivity. Selected by score "
TEXT("Hard gates passed: non-vacuous, largest air component >= %.2f, and "
"player-fit arrival → departure connectivity (fine step=1; "
"fit cells=%lld). Selected by legacy score "
"floor_area + 0.25*clamp(clearance/%d,0,1) + 0.10*largest_surface = %.6f; "
"floor area is the primary walking signal. Applied to existing target slot %d."),
LargestComponentSurvivalThreshold,
static_cast<long long>(FineMetrics.NumPlayerFitCells),
static_cast<int32>(ClearanceNormalizationVoxels),
Selected.SelectionScore,
Selected.TargetStrateIndex);
@@ -994,10 +1607,16 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
Summaries[ArchetypeIndex].Window = PreviewCandidates[ArchetypeIndex].Window;
Summaries[ArchetypeIndex].WindowSummary = VF_FormatShowcaseSummaryWindow(
Stats[ArchetypeIndex]);
Summaries[ArchetypeIndex].PlayerFitFractionSummary = FString::Printf(
TEXT("%.6f (step=1)"), Selected.FineMetrics.PlayerFitFraction);
Summaries[ArchetypeIndex].TraversableComponentShareSummary = FString::Printf(
TEXT("%.6f (step=1)"), Selected.FineMetrics.TraversableComponentShare);
AddInfo(FString::Printf(
TEXT("%s: hard-gate survivors=%d, selected seed=%d index=%d, "
"walkable=%.6f, floor_area=%.6f, clearance=%d voxels, largest_surface=%.6f, "
"window=%s"),
"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"),
VF_GetStrateArchetypeName(GShowcaseArchetypes[ArchetypeIndex]),
Stats[ArchetypeIndex].Survivors.Num(),
Selected.Candidate.Seed, Selected.Candidate.Index,
@@ -1005,6 +1624,12 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
Selected.Metrics.WalkableFloorAreaFraction,
Selected.Metrics.MedianVerticalClearance,
Selected.Metrics.LargestWalkableSurfaceShare,
VF_ShowcaseConnectivityName(Selected.LegacyLaw.Result),
Selected.FineMetrics.PlayerFitFraction,
Selected.FineMetrics.TraversableComponentShare,
static_cast<long long>(Selected.FineMetrics.NumPlayerFitCells),
Selected.FineMetrics.MinimumPlayerClearanceVoxels,
VF_ShowcaseConnectivityName(Selected.PlayerFitLaw.Result),
*PreviewCandidates[ArchetypeIndex].Window.Describe()));
}
@@ -35,6 +35,8 @@ namespace
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid");
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow");
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudget");
case EVoxelConnectivityResult::StartCellNotPlayerFit: return TEXT("StartCellNotPlayerFit");
case EVoxelConnectivityResult::GoalCellNotPlayerFit: return TEXT("GoalCellNotPlayerFit");
}
return TEXT("Unknown");
}
@@ -48,6 +48,10 @@ namespace
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:
@@ -102,6 +102,15 @@ namespace
&& SameFloatBits(A.WalkableFraction, B.WalkableFraction)
&& SameFloatBits(A.MedianFeatureScale, B.MedianFeatureScale)
&& A.MedianVerticalClearance == B.MedianVerticalClearance
&& A.bPlayerFitResolved == B.bPlayerFitResolved
&& A.PlayerFitRefusalReason == B.PlayerFitRefusalReason
&& A.NumPlayerFitCells == B.NumPlayerFitCells
&& SameFloatBits(A.PlayerFitFraction, B.PlayerFitFraction)
&& A.NumTraversableComponents == B.NumTraversableComponents
&& A.LargestTraversableComponentCells == B.LargestTraversableComponentCells
&& SameFloatBits(A.TraversableComponentShare, B.TraversableComponentShare)
&& SameFloatBits(A.MinimumPlayerClearanceVoxels,
B.MinimumPlayerClearanceVoxels)
&& A.ResolvedMarginVoxels == B.ResolvedMarginVoxels
&& A.SampledMinZ == B.SampledMinZ
&& A.SampledMaxZ == B.SampledMaxZ
@@ -124,6 +133,10 @@ namespace
return TEXT("NOT_CONNECTED_AT_THIS_RESOLUTION");
case EVoxelConnectivityResult::StartCellSolid: return TEXT("START_CELL_SOLID");
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GOAL_CELL_SOLID");
case EVoxelConnectivityResult::StartCellNotPlayerFit:
return TEXT("START_CELL_NOT_PLAYER_FIT");
case EVoxelConnectivityResult::GoalCellNotPlayerFit:
return TEXT("GOAL_CELL_NOT_PLAYER_FIT");
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OUT_OF_WINDOW");
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted:
return TEXT("COARSE_LIED_BUDGET_EXHAUSTED");
@@ -132,7 +145,7 @@ namespace
}
constexpr int32 ConnectivityResultCount =
static_cast<int32>(EVoxelConnectivityResult::CoarseLiedBudgetExhausted) + 1;
static_cast<int32>(EVoxelConnectivityResult::GoalCellNotPlayerFit) + 1;
int32 NumRefutedRoutes(const FConnectivityProbe& Probe)
{
@@ -886,6 +886,19 @@ namespace VoxelSeasonManifestPrivate
Metrics.LargestWalkableSurfaceShare);
SetMetricFloat(Object, TEXT("median_feature_scale"), Metrics.MedianFeatureScale);
Object->SetNumberField(TEXT("median_vertical_clearance"), Metrics.MedianVerticalClearance);
Object->SetBoolField(TEXT("player_fit_resolved"), Metrics.bPlayerFitResolved);
Object->SetStringField(TEXT("player_fit_refusal_reason"), Metrics.PlayerFitRefusalReason);
Object->SetNumberField(TEXT("num_player_fit_cells"),
static_cast<double>(Metrics.NumPlayerFitCells));
SetMetricFloat(Object, TEXT("player_fit_fraction"), Metrics.PlayerFitFraction);
Object->SetNumberField(TEXT("num_traversable_components"),
Metrics.NumTraversableComponents);
Object->SetNumberField(TEXT("largest_traversable_component_cells"),
static_cast<double>(Metrics.LargestTraversableComponentCells));
SetMetricFloat(Object, TEXT("traversable_component_share"),
Metrics.TraversableComponentShare);
SetMetricFloat(Object, TEXT("minimum_player_clearance_voxels"),
Metrics.MinimumPlayerClearanceVoxels);
Object->SetNumberField(TEXT("resolved_margin_voxels"), Metrics.ResolvedMarginVoxels);
Object->SetNumberField(TEXT("sampled_min_z"), Metrics.SampledMinZ);
Object->SetNumberField(TEXT("sampled_max_z"), Metrics.SampledMaxZ);
@@ -940,6 +953,25 @@ namespace VoxelSeasonManifestPrivate
}
bValid = ReadMetricFloat(Object, TEXT("median_feature_scale"), OutMetrics.MedianFeatureScale) && bValid;
bValid = ReadJsonInt32(Object, TEXT("median_vertical_clearance"), OutMetrics.MedianVerticalClearance) && bValid;
if (Object->HasField(TEXT("player_fit_resolved")))
{
bValid = ReadJsonBool(Object, TEXT("player_fit_resolved"),
OutMetrics.bPlayerFitResolved) && bValid;
bValid = ReadJsonString(Object, TEXT("player_fit_refusal_reason"),
OutMetrics.PlayerFitRefusalReason) && bValid;
bValid = ReadJsonInt64(Object, TEXT("num_player_fit_cells"),
OutMetrics.NumPlayerFitCells) && bValid;
bValid = ReadMetricFloat(Object, TEXT("player_fit_fraction"),
OutMetrics.PlayerFitFraction) && bValid;
bValid = ReadJsonInt32(Object, TEXT("num_traversable_components"),
OutMetrics.NumTraversableComponents) && bValid;
bValid = ReadJsonInt64(Object, TEXT("largest_traversable_component_cells"),
OutMetrics.LargestTraversableComponentCells) && bValid;
bValid = ReadMetricFloat(Object, TEXT("traversable_component_share"),
OutMetrics.TraversableComponentShare) && bValid;
bValid = ReadMetricFloat(Object, TEXT("minimum_player_clearance_voxels"),
OutMetrics.MinimumPlayerClearanceVoxels) && bValid;
}
bValid = ReadJsonInt32(Object, TEXT("resolved_margin_voxels"), OutMetrics.ResolvedMarginVoxels) && bValid;
bValid = ReadJsonInt32(Object, TEXT("sampled_min_z"), OutMetrics.SampledMinZ) && bValid;
bValid = ReadJsonInt32(Object, TEXT("sampled_max_z"), OutMetrics.SampledMaxZ) && bValid;
@@ -997,6 +1029,8 @@ namespace VoxelSeasonManifestPrivate
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");
}
@@ -1010,6 +1044,8 @@ namespace VoxelSeasonManifestPrivate
EVoxelConnectivityResult::NotConnectedAtThisResolution,
EVoxelConnectivityResult::StartCellSolid,
EVoxelConnectivityResult::GoalCellSolid,
EVoxelConnectivityResult::StartCellNotPlayerFit,
EVoxelConnectivityResult::GoalCellNotPlayerFit,
EVoxelConnectivityResult::OutOfWindow,
EVoxelConnectivityResult::CoarseLiedBudgetExhausted,
};
@@ -1917,7 +1953,11 @@ namespace VoxelSeasonCompositionPrivate
&& FMath::IsFinite(Settings.CenterXY.X)
&& FMath::IsFinite(Settings.CenterXY.Y)
&& FMath::IsFinite(Settings.CoverMarginVoxels)
&& Settings.CoverMarginVoxels >= 0.0f;
&& Settings.CoverMarginVoxels >= 0.0f
&& FMath::IsFinite(Settings.PlayerCapsuleRadiusVoxels)
&& Settings.PlayerCapsuleRadiusVoxels > 0.0f
&& FMath::IsFinite(Settings.PlayerCapsuleHalfHeightVoxels)
&& Settings.PlayerCapsuleHalfHeightVoxels > 0.0f;
}
bool IsBossSlot(int32 DepthIndex, int32 TotalStrates, int32 Interval)
@@ -2193,20 +2233,54 @@ namespace VoxelSeasonCompositionPrivate
return false;
}
const FVoxelConnectivityDiagnostics Law = VF_DiagnoseConnectivityWithSampler(
Sampler, Strate.BottomWorldZ, Strate.TopWorldZ, Seal,
FVector(0.0f, 0.0f, InteriorBottom + 0.5f),
FVector(0.0f, 0.0f, InteriorTop - 0.5f), Settings.MeasureSettings);
OutEvaluation.PrimordialLawResult = Law.Result;
OutEvaluation.bPassedPrimordialLaw = Law.bValid
&& Law.Result == EVoxelConnectivityResult::Connected
&& !Law.bStartSnapped && !Law.bGoalSnapped;
FVoxelStrateMeasureSettings PlayerFitSettings = Settings.PlayerFitMeasureSettings;
// The season primordial law has always used the (0,0) spine as its arrival and departure
// line. Keep the fine ROI centred on that same physical route; centring it on the coarse
// largest-air representative can exclude the law endpoints and turn a valid route into
// an arbitrary OutOfWindow result. The player-fit pass still has its independent fine
// resolution and bounded radius.
PlayerFitSettings.CenterXY = FVector2D::ZeroVector;
PlayerFitSettings.CoverPointA.Reset();
PlayerFitSettings.CoverPointB.Reset();
FVoxelStrateMetrics PlayerFitMetrics;
const FVoxelConnectivityDiagnostics PlayerFitLaw =
VF_DiagnosePlayerFitConnectivityWithSampler(
Sampler, Strate.BottomWorldZ, Strate.TopWorldZ, Seal,
FVector(0.0f, 0.0f, InteriorBottom + 0.5f),
FVector(0.0f, 0.0f, InteriorTop - 0.5f),
PlayerFitSettings, &PlayerFitMetrics);
// The legacy fields remain the bounded coarse measurement. The player fields come from
// the separate one-voxel ROI that also backed the restricted route query.
RawMetrics.bPlayerFitResolved = PlayerFitMetrics.bPlayerFitResolved;
RawMetrics.PlayerFitRefusalReason = PlayerFitMetrics.PlayerFitRefusalReason;
RawMetrics.NumPlayerFitCells = PlayerFitMetrics.NumPlayerFitCells;
RawMetrics.PlayerFitFraction = PlayerFitMetrics.PlayerFitFraction;
RawMetrics.NumTraversableComponents = PlayerFitMetrics.NumTraversableComponents;
RawMetrics.LargestTraversableComponentCells =
PlayerFitMetrics.LargestTraversableComponentCells;
RawMetrics.TraversableComponentShare = PlayerFitMetrics.TraversableComponentShare;
RawMetrics.MinimumPlayerClearanceVoxels =
PlayerFitMetrics.MinimumPlayerClearanceVoxels;
if (OutRawMetrics != nullptr)
{
*OutRawMetrics = RawMetrics;
}
OutEvaluation.PrimordialLawResult = PlayerFitLaw.Result;
const bool bPlayerFitExists = PlayerFitMetrics.bValid
&& PlayerFitMetrics.bPlayerFitResolved
&& PlayerFitMetrics.NumPlayerFitCells > 0
&& PlayerFitMetrics.NumTraversableComponents > 0
&& PlayerFitMetrics.TraversableComponentShare > 0.0f;
OutEvaluation.bPassedPrimordialLaw = bPlayerFitExists
&& PlayerFitLaw.bValid
&& PlayerFitLaw.Result == EVoxelConnectivityResult::Connected;
if (!OutEvaluation.bPassedPrimordialLaw)
{
OutEvaluation.FailureReason = FString::Printf(
TEXT("primordial law failed: %s%s"),
RejectionReasonForLaw(Law.Result),
(Law.bStartSnapped || Law.bGoalSnapped) ? TEXT(" (endpoint snapped)") : TEXT(""));
TEXT("player-fit primordial law failed: %s%s"),
RejectionReasonForLaw(PlayerFitLaw.Result),
bPlayerFitExists ? TEXT("") : TEXT(" (no player-fitting floor volume)"));
return false;
}
@@ -2433,6 +2507,10 @@ FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed,
{
return Fail(TEXT("MeasureSettings are invalid for a bounded offline pass."));
}
if (!IsValidMeasureSettings(Settings.PlayerFitMeasureSettings))
{
return Fail(TEXT("PlayerFitMeasureSettings are invalid for a bounded offline pass."));
}
TMap<int32, FVoxelSeasonFixedStrate> FixedStrates;
FString FixedError;
@@ -2285,6 +2285,14 @@ FVoxelStrateMeasuredMetrics VF_SummarizeStrateMetrics(
Result.LargestWalkableSurfaceShare = Metrics.LargestWalkableSurfaceShare;
Result.MedianFeatureScale = Metrics.MedianFeatureScale;
Result.MedianVerticalClearance = Metrics.MedianVerticalClearance;
Result.bPlayerFitResolved = Metrics.bPlayerFitResolved;
Result.PlayerFitRefusalReason = Metrics.PlayerFitRefusalReason;
Result.NumPlayerFitCells = Metrics.NumPlayerFitCells;
Result.PlayerFitFraction = Metrics.PlayerFitFraction;
Result.NumTraversableComponents = Metrics.NumTraversableComponents;
Result.LargestTraversableComponentCells = Metrics.LargestTraversableComponentCells;
Result.TraversableComponentShare = Metrics.TraversableComponentShare;
Result.MinimumPlayerClearanceVoxels = Metrics.MinimumPlayerClearanceVoxels;
Result.ResolvedMarginVoxels = Metrics.ResolvedMarginVoxels;
Result.SampledMinZ = Metrics.SampledMinZ;
Result.SampledMaxZ = Metrics.SampledMaxZ;
@@ -2778,6 +2786,19 @@ namespace
Metrics.LargestWalkableSurfaceShare);
Object->SetNumberField(TEXT("median_feature_scale"), Metrics.MedianFeatureScale);
Object->SetNumberField(TEXT("median_vertical_clearance"), Metrics.MedianVerticalClearance);
Object->SetBoolField(TEXT("player_fit_resolved"), Metrics.bPlayerFitResolved);
Object->SetStringField(TEXT("player_fit_refusal_reason"), Metrics.PlayerFitRefusalReason);
Object->SetNumberField(TEXT("num_player_fit_cells"),
static_cast<double>(Metrics.NumPlayerFitCells));
Object->SetNumberField(TEXT("player_fit_fraction"), Metrics.PlayerFitFraction);
Object->SetNumberField(TEXT("num_traversable_components"),
Metrics.NumTraversableComponents);
Object->SetNumberField(TEXT("largest_traversable_component_cells"),
static_cast<double>(Metrics.LargestTraversableComponentCells));
Object->SetNumberField(TEXT("traversable_component_share"),
Metrics.TraversableComponentShare);
Object->SetNumberField(TEXT("minimum_player_clearance_voxels"),
Metrics.MinimumPlayerClearanceVoxels);
Object->SetNumberField(TEXT("resolved_margin_voxels"), Metrics.ResolvedMarginVoxels);
Object->SetNumberField(TEXT("sampled_min_z"), Metrics.SampledMinZ);
Object->SetNumberField(TEXT("sampled_max_z"), Metrics.SampledMaxZ);
@@ -2852,6 +2873,25 @@ namespace
OutMetrics.MedianFeatureScale) && bValid;
bValid = VF_ReadJsonInt32(Object, TEXT("median_vertical_clearance"),
OutMetrics.MedianVerticalClearance) && bValid;
if (Object->HasField(TEXT("player_fit_resolved")))
{
bValid = VF_ReadJsonBool(Object, TEXT("player_fit_resolved"),
OutMetrics.bPlayerFitResolved) && bValid;
bValid = VF_ReadJsonString(Object, TEXT("player_fit_refusal_reason"),
OutMetrics.PlayerFitRefusalReason) && bValid;
bValid = VF_ReadJsonInt64(Object, TEXT("num_player_fit_cells"),
OutMetrics.NumPlayerFitCells) && bValid;
bValid = VF_ReadJsonFloat(Object, TEXT("player_fit_fraction"),
OutMetrics.PlayerFitFraction) && bValid;
bValid = VF_ReadJsonInt32(Object, TEXT("num_traversable_components"),
OutMetrics.NumTraversableComponents) && bValid;
bValid = VF_ReadJsonInt64(Object, TEXT("largest_traversable_component_cells"),
OutMetrics.LargestTraversableComponentCells) && bValid;
bValid = VF_ReadJsonFloat(Object, TEXT("traversable_component_share"),
OutMetrics.TraversableComponentShare) && bValid;
bValid = VF_ReadJsonFloat(Object, TEXT("minimum_player_clearance_voxels"),
OutMetrics.MinimumPlayerClearanceVoxels) && bValid;
}
bValid = VF_ReadJsonInt32(Object, TEXT("resolved_margin_voxels"),
OutMetrics.ResolvedMarginVoxels) && bValid;
bValid = VF_ReadJsonInt32(Object, TEXT("sampled_min_z"), OutMetrics.SampledMinZ) && bValid;
@@ -2995,6 +3035,14 @@ namespace
&& A.LargestWalkableSurfaceShare == B.LargestWalkableSurfaceShare
&& A.MedianFeatureScale == B.MedianFeatureScale
&& A.MedianVerticalClearance == B.MedianVerticalClearance
&& A.bPlayerFitResolved == B.bPlayerFitResolved
&& A.PlayerFitRefusalReason == B.PlayerFitRefusalReason
&& A.NumPlayerFitCells == B.NumPlayerFitCells
&& A.PlayerFitFraction == B.PlayerFitFraction
&& A.NumTraversableComponents == B.NumTraversableComponents
&& A.LargestTraversableComponentCells == B.LargestTraversableComponentCells
&& A.TraversableComponentShare == B.TraversableComponentShare
&& A.MinimumPlayerClearanceVoxels == B.MinimumPlayerClearanceVoxels
&& A.ResolvedMarginVoxels == B.ResolvedMarginVoxels
&& A.SampledMinZ == B.SampledMinZ
&& A.SampledMaxZ == B.SampledMaxZ
@@ -3075,6 +3123,10 @@ namespace
&& FMath::IsFinite(Metrics.WalkableFloorAreaFraction)
&& FMath::IsFinite(Metrics.LargestWalkableSurfaceShare)
&& FMath::IsFinite(Metrics.MedianFeatureScale)
&& (!Metrics.bPlayerFitResolved
|| (FMath::IsFinite(Metrics.PlayerFitFraction)
&& FMath::IsFinite(Metrics.TraversableComponentShare)
&& FMath::IsFinite(Metrics.MinimumPlayerClearanceVoxels)))
&& FMath::IsFinite(Metrics.LargestComponentPoint.X)
&& FMath::IsFinite(Metrics.LargestComponentPoint.Y)
&& FMath::IsFinite(Metrics.LargestComponentPoint.Z);
+606 -25
View File
@@ -441,7 +441,8 @@ namespace VoxelStrateMeasurePrivate
int64& OutLargestComponentCells,
int32& OutLargestComponentLowestCell,
int32& OutNumComponentsAtLeast1Pct,
TArray<int64>* OutComponentCells)
TArray<int64>* OutComponentCells,
const TArray<uint8>* EligibilityMask = nullptr)
{
OutComponents.SetNumUninitialized(Grid.CellCount);
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
@@ -462,7 +463,9 @@ namespace VoxelStrateMeasurePrivate
for (int32 Start = 0; Start < Grid.CellCount; ++Start)
{
if (Grid.Air[Start] == 0u || OutComponents[Start] >= 0)
if (Grid.Air[Start] == 0u
|| (EligibilityMask != nullptr && (*EligibilityMask)[Start] == 0u)
|| OutComponents[Start] >= 0)
{
continue;
}
@@ -486,7 +489,9 @@ namespace VoxelStrateMeasurePrivate
auto Visit = [&](int32 Neighbor)
{
if (Grid.Air[Neighbor] != 0u && OutComponents[Neighbor] < 0)
if (Grid.Air[Neighbor] != 0u
&& (EligibilityMask == nullptr || (*EligibilityMask)[Neighbor] != 0u)
&& OutComponents[Neighbor] < 0)
{
OutComponents[Neighbor] = ComponentId;
Queue[Tail++] = Neighbor;
@@ -576,6 +581,343 @@ namespace VoxelStrateMeasurePrivate
}
}
struct FPlayerCapsuleStencilRow
{
int32 RelativeZ = 0;
TArray<FIntPoint> HorizontalOffsets;
};
bool IsPointInsideCapsule(
float RelativeZ,
int32 OffsetX,
int32 OffsetY,
float Radius,
float HalfHeight)
{
// The capsule is upright and its centre is HalfHeight above the top face of the solid
// floor cell. This is a direct capsule test in the fine voxel lattice; it deliberately
// does not infer occupancy from Manhattan distance.
const float AxisHalfLength = FMath::Max(0.0f, HalfHeight - Radius);
const float DistanceToAxis = FMath::Max(
FMath::Abs(RelativeZ) - AxisHalfLength, 0.0f);
const float DistanceSquared = static_cast<float>(OffsetX * OffsetX + OffsetY * OffsetY)
+ DistanceToAxis * DistanceToAxis;
return DistanceSquared <= Radius * Radius + KINDA_SMALL_NUMBER;
}
bool BuildPlayerCapsuleStencil(
const FVoxelStrateMeasureSettings& Settings,
TArray<FPlayerCapsuleStencilRow>& OutRows,
FString& OutReason)
{
OutRows.Reset();
if (!FMath::IsFinite(Settings.PlayerCapsuleRadiusVoxels)
|| Settings.PlayerCapsuleRadiusVoxels <= 0.0f
|| !FMath::IsFinite(Settings.PlayerCapsuleHalfHeightVoxels)
|| Settings.PlayerCapsuleHalfHeightVoxels <= 0.0f)
{
return Refuse(OutReason,
TEXT("Player capsule radius and half-height must be finite and greater than zero."));
}
const double HeightCellsReal = FMath::CeilToDouble(
2.0 * static_cast<double>(Settings.PlayerCapsuleHalfHeightVoxels));
// This is a stencil-size guard, not a gameplay threshold. It prevents a malformed
// caller-supplied capsule from allocating an unbounded per-anchor loop.
constexpr double MaxStencilHeightCells = 4096.0;
if (!FMath::IsFinite(HeightCellsReal) || HeightCellsReal <= 0.0
|| HeightCellsReal > MaxStencilHeightCells)
{
return Refuse(OutReason,
TEXT("The player capsule height exceeds the bounded fit stencil."));
}
const double MaxHorizontalOffsetReal = FMath::CeilToDouble(
static_cast<double>(Settings.PlayerCapsuleRadiusVoxels));
constexpr double MaxStencilHorizontalRadiusCells = 4096.0;
if (!FMath::IsFinite(MaxHorizontalOffsetReal)
|| MaxHorizontalOffsetReal < 0.0
|| MaxHorizontalOffsetReal > MaxStencilHorizontalRadiusCells)
{
return Refuse(OutReason,
TEXT("The player capsule radius exceeds the bounded fit stencil."));
}
const int32 HeightCells = static_cast<int32>(HeightCellsReal);
const int32 MaxHorizontalOffset = static_cast<int32>(MaxHorizontalOffsetReal);
// This bounds both the retained stencil and the per-anchor occupancy work. It is
// deliberately far above the confirmed capsule's handful of offsets, but prevents a
// malformed caller from turning a valid-looking finite radius into an enormous array.
constexpr int64 MaxStencilOffsets = 1ll << 20;
int64 NumStencilOffsets = 0;
OutRows.Reserve(HeightCells);
for (int32 RelativeZ = 0; RelativeZ < HeightCells; ++RelativeZ)
{
FPlayerCapsuleStencilRow& Row = OutRows.AddDefaulted_GetRef();
Row.RelativeZ = RelativeZ;
const float CapsuleRelativeZ = static_cast<float>(RelativeZ) + 0.5f
- Settings.PlayerCapsuleHalfHeightVoxels;
for (int32 OffsetY = -MaxHorizontalOffset;
OffsetY <= MaxHorizontalOffset;
++OffsetY)
{
for (int32 OffsetX = -MaxHorizontalOffset;
OffsetX <= MaxHorizontalOffset;
++OffsetX)
{
if (IsPointInsideCapsule(
CapsuleRelativeZ, OffsetX, OffsetY,
Settings.PlayerCapsuleRadiusVoxels,
Settings.PlayerCapsuleHalfHeightVoxels))
{
if (NumStencilOffsets >= MaxStencilOffsets)
{
return Refuse(
OutReason,
TEXT("The player capsule occupancy stencil exceeds its bounded work limit."));
}
Row.HorizontalOffsets.Add(FIntPoint(OffsetX, OffsetY));
++NumStencilOffsets;
}
}
}
// The full-height floor rule still requires one clear voxel in rows whose voxel
// centre lies outside the rounded cap (normally the top partial voxel of a 7.04
// voxel player). This is what makes 176 cm a full-height requirement rather than a
// 7-centre-point approximation.
if (Row.HorizontalOffsets.IsEmpty())
{
Row.HorizontalOffsets.Add(FIntPoint::ZeroValue);
}
}
return true;
}
bool BuildPlayerFitMask(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
TArray<uint8>& OutPlayerFit,
int64& OutNumPlayerFitCells,
FString& OutReason)
{
OutPlayerFit.Reset();
OutNumPlayerFitCells = 0;
if (Grid.SampleStep != 1)
{
return Refuse(
OutReason,
*FString::Printf(
TEXT("Player-fit metrics refused: SampleStep=%d; exact capsule fit requires SampleStep=1."),
Grid.SampleStep));
}
TArray<FPlayerCapsuleStencilRow> StencilRows;
if (!BuildPlayerCapsuleStencil(Settings, StencilRows, OutReason))
{
return false;
}
OutPlayerFit.Init(0u, Grid.CellCount);
for (int32 Z = 1; Z < Grid.NumZ; ++Z)
{
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Current = Grid.Index(X, Y, Z);
if (Grid.Air[Current] == 0u
|| Grid.Air[Grid.Index(X, Y, Z - 1)] != 0u)
{
continue;
}
bool bFits = true;
for (const FPlayerCapsuleStencilRow& Row : StencilRows)
{
const int32 TargetZ = Z + Row.RelativeZ;
if (TargetZ < 0 || TargetZ >= Grid.NumZ)
{
bFits = false;
break;
}
for (const FIntPoint& Offset : Row.HorizontalOffsets)
{
const int32 TargetX = X + Offset.X;
const int32 TargetY = Y + Offset.Y;
if (TargetX < 0 || TargetX >= Grid.NumX
|| TargetY < 0 || TargetY >= Grid.NumY
|| Grid.Air[Grid.Index(TargetX, TargetY, TargetZ)] == 0u)
{
bFits = false;
break;
}
}
if (!bFits)
{
break;
}
}
if (bFits)
{
OutPlayerFit[Current] = 1u;
++OutNumPlayerFitCells;
}
}
}
}
return true;
}
float MinimumPlayerClearanceChebyshev(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFit)
{
if (PlayerFit.Num() != Grid.CellCount)
{
return 0.0f;
}
const int32 PlaneCells = Grid.NumX * Grid.NumY;
constexpr int32 Infinity = INT32_MAX / 4;
TArray<int32> Distances;
Distances.SetNumUninitialized(PlaneCells);
int32 MinimumDistance = Infinity;
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
{
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 PlaneIndex = X + Grid.NumX * Y;
Distances[PlaneIndex] = Grid.Air[Grid.Index(X, Y, Z)] == 0u
? 0 : Infinity;
}
}
auto Relax = [&](int32 Current, int32 Neighbor)
{
if (Distances[Neighbor] < Infinity)
{
Distances[Current] = FMath::Min(
Distances[Current], Distances[Neighbor] + 1);
}
};
// Two raster passes with the complete 8-neighbour stencil give a true 2D
// Chebyshev distance to solid in this horizontal slice. It is used only for the
// reported minimum; capsule occupancy above is the direct stencil check.
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Current = X + Grid.NumX * Y;
if (Grid.Air[Grid.Index(X, Y, Z)] == 0u) continue;
if (X > 0) Relax(Current, Current - 1);
if (Y > 0)
{
Relax(Current, Current - Grid.NumX);
if (X > 0) Relax(Current, Current - Grid.NumX - 1);
if (X + 1 < Grid.NumX) Relax(Current, Current - Grid.NumX + 1);
}
}
}
for (int32 Y = Grid.NumY - 1; Y >= 0; --Y)
{
for (int32 X = Grid.NumX - 1; X >= 0; --X)
{
const int32 Current = X + Grid.NumX * Y;
if (Grid.Air[Grid.Index(X, Y, Z)] == 0u) continue;
if (X + 1 < Grid.NumX) Relax(Current, Current + 1);
if (Y + 1 < Grid.NumY)
{
Relax(Current, Current + Grid.NumX);
if (X > 0) Relax(Current, Current + Grid.NumX - 1);
if (X + 1 < Grid.NumX) Relax(Current, Current + Grid.NumX + 1);
}
}
}
for (int32 Y = 0; Y < Grid.NumY; ++Y)
{
for (int32 X = 0; X < Grid.NumX; ++X)
{
const int32 Cell = Grid.Index(X, Y, Z);
if (PlayerFit[Cell] != 0u)
{
MinimumDistance = FMath::Min(
MinimumDistance, Distances[X + Grid.NumX * Y]);
}
}
}
}
return MinimumDistance == Infinity
? 0.0f : static_cast<float>(MinimumDistance);
}
void BuildPlayerFitMetrics(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& InOutMetrics,
TArray<uint8>* OutPlayerFitMask = nullptr)
{
InOutMetrics.bPlayerFitResolved = false;
InOutMetrics.PlayerFitRefusalReason.Reset();
InOutMetrics.NumPlayerFitCells = 0;
InOutMetrics.PlayerFitFraction = 0.0f;
InOutMetrics.NumTraversableComponents = 0;
InOutMetrics.LargestTraversableComponentCells = 0;
InOutMetrics.TraversableComponentShare = 0.0f;
InOutMetrics.MinimumPlayerClearanceVoxels = 0.0f;
TArray<uint8> PlayerFit;
if (!BuildPlayerFitMask(
Grid, Settings, PlayerFit, InOutMetrics.NumPlayerFitCells,
InOutMetrics.PlayerFitRefusalReason))
{
if (OutPlayerFitMask != nullptr)
{
OutPlayerFitMask->Reset();
}
return;
}
InOutMetrics.bPlayerFitResolved = true;
InOutMetrics.PlayerFitFraction = InOutMetrics.NumAir > 0
? static_cast<float>(static_cast<double>(InOutMetrics.NumPlayerFitCells)
/ static_cast<double>(InOutMetrics.NumAir))
: 0.0f;
TArray<int32> Components;
int32 NumComponents = 0;
int64 LargestComponentCells = 0;
int32 LargestComponentLowestCell = INDEX_NONE;
int32 NumComponentsAtLeast1Pct = 0;
FloodFillAir(
Grid,
Components,
NumComponents,
InOutMetrics.NumPlayerFitCells,
LargestComponentCells,
LargestComponentLowestCell,
NumComponentsAtLeast1Pct,
nullptr,
&PlayerFit);
InOutMetrics.NumTraversableComponents = NumComponents;
InOutMetrics.LargestTraversableComponentCells = LargestComponentCells;
InOutMetrics.TraversableComponentShare = InOutMetrics.NumPlayerFitCells > 0
? static_cast<float>(static_cast<double>(LargestComponentCells)
/ static_cast<double>(InOutMetrics.NumPlayerFitCells))
: 0.0f;
InOutMetrics.MinimumPlayerClearanceVoxels = MinimumPlayerClearanceChebyshev(
Grid, PlayerFit);
if (OutPlayerFitMask != nullptr)
{
*OutPlayerFitMask = MoveTemp(PlayerFit);
}
}
float MedianFloat(TArray<float>& Values)
{
if (Values.Num() == 0) return 0.0f;
@@ -591,7 +933,8 @@ namespace VoxelStrateMeasurePrivate
void BuildMetricsFromGrid(
const FSampleGrid& Grid,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics& InOutMetrics)
FVoxelStrateMetrics& InOutMetrics,
TArray<uint8>* OutPlayerFitMask = nullptr)
{
TArray<int32> Components;
int32 NumComponents = 0;
@@ -775,6 +1118,11 @@ namespace VoxelStrateMeasurePrivate
}
InOutMetrics.MedianFeatureScale = MedianFloat(FeatureScales);
}
// This is intentionally a separate fine-resolution pass. The legacy metrics above may
// be useful at SampleStep=4/8, but they cannot answer whether the confirmed player capsule
// fits. A coarse grid therefore receives an explicit refusal instead of an estimate.
BuildPlayerFitMetrics(Grid, Settings, InOutMetrics, OutPlayerFitMask);
}
bool FindCellForPoint(const FSampleGrid& Grid, const FVector& Point, int32& OutCell)
@@ -793,14 +1141,16 @@ namespace VoxelStrateMeasurePrivate
{
OutOfWindow,
Air,
Solid
Solid,
NotPlayerFit
};
EEndpointCellResult ResolveEndpointCell(
const FSampleGrid& Grid,
const FVector& Point,
int32& OutCell,
bool& bOutSnapped)
bool& bOutSnapped,
const TArray<uint8>* EligibilityMask = nullptr)
{
bOutSnapped = false;
int32 OriginalCell = -1;
@@ -808,7 +1158,12 @@ namespace VoxelStrateMeasurePrivate
{
return EEndpointCellResult::OutOfWindow;
}
if (Grid.Air[OriginalCell] != 0u)
const auto IsEligible = [&, EligibilityMask](int32 Cell)
{
return Grid.Air[Cell] != 0u
&& (EligibilityMask == nullptr || (*EligibilityMask)[Cell] != 0u);
};
if (IsEligible(OriginalCell))
{
OutCell = OriginalCell;
return EEndpointCellResult::Air;
@@ -848,7 +1203,7 @@ namespace VoxelStrateMeasurePrivate
}
const int32 Candidate = Grid.Index(X, Y, Z);
if (Grid.Air[Candidate] == 0u)
if (!IsEligible(Candidate))
{
continue;
}
@@ -868,7 +1223,43 @@ namespace VoxelStrateMeasurePrivate
if (BestCell == INDEX_NONE)
{
return EEndpointCellResult::Solid;
if (EligibilityMask != nullptr)
{
// Passage mouth points are authored on the centreline of a carved connector, not
// necessarily on the floor-anchor lattice used by PlayerFit. Resolve them to the
// nearest player-fitting floor cell over the bounded fine grid, rather than
// declaring an in-air mouth unreachable just because its floor is several cells
// below. The endpoint is reported as snapped, and the route re-check below still
// validates the endpoint-to-anchor segment against density.
for (int32 Candidate = 0; Candidate < Grid.CellCount; ++Candidate)
{
if ((*EligibilityMask)[Candidate] == 0u)
{
continue;
}
int32 CandidateX = 0;
int32 CandidateY = 0;
int32 CandidateZ = 0;
DecodeIndex(Grid, Candidate, CandidateX, CandidateY, CandidateZ);
const float DistanceSquared = FVector::DistSquared(
Point, Grid.CellCenter(CandidateX, CandidateY, CandidateZ));
if (DistanceSquared < BestDistanceSquared
|| (DistanceSquared == BestDistanceSquared
&& (BestCell == INDEX_NONE || Candidate < BestCell)))
{
BestDistanceSquared = DistanceSquared;
BestCell = Candidate;
}
}
}
}
if (BestCell == INDEX_NONE)
{
return EligibilityMask != nullptr
? EEndpointCellResult::NotPlayerFit
: EEndpointCellResult::Solid;
}
OutCell = BestCell;
@@ -943,10 +1334,49 @@ namespace VoxelStrateMeasurePrivate
const FVector& A,
const FVector& B,
const TArray<int32>& Path,
FCoarseEdge& OutFailedEdge)
FCoarseEdge& OutFailedEdge,
const TArray<uint8>* EligibilityMask = nullptr)
{
OutFailedEdge = FCoarseEdge();
if (EligibilityMask != nullptr)
{
// A player-fit route is deliberately a graph over floor anchors, not a second air
// route with a coarse-cell illusion. The mask was built at SampleStep=1 and every
// edge below is between adjacent player-fitting cells.
for (int32 PathIndex = 0; PathIndex < Path.Num(); ++PathIndex)
{
if (!EligibilityMask->IsValidIndex(Path[PathIndex])
|| (*EligibilityMask)[Path[PathIndex]] == 0u)
{
if (PathIndex > 0)
{
OutFailedEdge = MakeCoarseEdge(
Path[PathIndex - 1], Path[PathIndex]);
}
return false;
}
}
int32 StartX = 0;
int32 StartY = 0;
int32 StartZ = 0;
DecodeIndex(Grid, Path[0], StartX, StartY, StartZ);
int32 EndX = 0;
int32 EndY = 0;
int32 EndZ = 0;
DecodeIndex(Grid, Path.Last(), EndX, EndY, EndZ);
if (!FullResolutionAirOnSegment(
Generator, Sampler, A, Grid.CellCenter(StartX, StartY, StartZ))
|| !FullResolutionAirOnSegment(
Generator, Sampler, Grid.CellCenter(EndX, EndY, EndZ), B))
{
// Endpoint approaches have no graph edge to exclude. The player-fit route is
// therefore unknown if either mouth-to-anchor segment contains solid density.
return false;
}
return true;
}
int32 StartX = 0;
int32 StartY = 0;
int32 StartZ = 0;
@@ -998,9 +1428,15 @@ namespace VoxelStrateMeasurePrivate
int32 Start,
int32 Goal,
const TArray<FCoarseEdge>& BlockedEdges,
TArray<int32>& OutPath)
TArray<int32>& OutPath,
const TArray<uint8>* EligibilityMask = nullptr)
{
if (Grid.Air[Start] == 0u || Grid.Air[Goal] == 0u)
const auto IsEligible = [&, EligibilityMask](int32 Cell)
{
return Grid.Air[Cell] != 0u
&& (EligibilityMask == nullptr || (*EligibilityMask)[Cell] != 0u);
};
if (!IsEligible(Start) || !IsEligible(Goal))
{
return false;
}
@@ -1029,7 +1465,7 @@ namespace VoxelStrateMeasurePrivate
auto Visit = [&](int32 Neighbor)
{
if (Grid.Air[Neighbor] != 0u
if (IsEligible(Neighbor)
&& Parent[Neighbor] == -2
&& !IsCoarseEdgeBlocked(BlockedEdges, Current, Neighbor))
{
@@ -1084,7 +1520,8 @@ namespace VoxelStrateMeasurePrivate
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
int32& OutNumRouteRetries,
int32 MaxRouteRetries)
int32 MaxRouteRetries,
const TArray<uint8>* EligibilityMask = nullptr)
{
OutStart = -1;
OutGoal = -1;
@@ -1093,9 +1530,9 @@ namespace VoxelStrateMeasurePrivate
OutNumRouteRetries = 0;
const EEndpointCellResult StartCell = ResolveEndpointCell(
Grid, AVoxel, OutStart, bOutStartSnapped);
Grid, AVoxel, OutStart, bOutStartSnapped, EligibilityMask);
const EEndpointCellResult GoalCell = ResolveEndpointCell(
Grid, BVoxel, OutGoal, bOutGoalSnapped);
Grid, BVoxel, OutGoal, bOutGoalSnapped, EligibilityMask);
if (StartCell == EEndpointCellResult::OutOfWindow
|| GoalCell == EEndpointCellResult::OutOfWindow)
{
@@ -1105,16 +1542,25 @@ namespace VoxelStrateMeasurePrivate
{
return EVoxelConnectivityResult::StartCellSolid;
}
if (StartCell == EEndpointCellResult::NotPlayerFit)
{
return EVoxelConnectivityResult::StartCellNotPlayerFit;
}
if (GoalCell == EEndpointCellResult::Solid)
{
return EVoxelConnectivityResult::GoalCellSolid;
}
if (GoalCell == EEndpointCellResult::NotPlayerFit)
{
return EVoxelConnectivityResult::GoalCellNotPlayerFit;
}
TArray<FCoarseEdge> BlockedEdges;
TArray<int32> Path;
for (;;)
{
if (!FindCoarsePath(Grid, OutStart, OutGoal, BlockedEdges, Path))
if (!FindCoarsePath(
Grid, OutStart, OutGoal, BlockedEdges, Path, EligibilityMask))
{
// This is a negative verdict only after all edges excluded by earlier
// full-resolution failures leave no coarse route at this resolution.
@@ -1125,7 +1571,9 @@ namespace VoxelStrateMeasurePrivate
// spacing in the original voxel field. A single solid sample refutes only this
// route, so exclude its precise coarse edge and search again.
FCoarseEdge FailedEdge;
if (FullResolutionPathIsAir(Generator, Sampler, Grid, AVoxel, BVoxel, Path, FailedEdge))
if (FullResolutionPathIsAir(
Generator, Sampler, Grid, AVoxel, BVoxel, Path, FailedEdge,
EligibilityMask))
{
return EVoxelConnectivityResult::Connected;
}
@@ -1193,10 +1641,13 @@ namespace VoxelStrateMeasurePrivate
bool bStartSnapped,
bool bGoalSnapped,
int32 NumRouteRetries,
FVoxelConnectivityDiagnostics& OutDiagnostics)
FVoxelConnectivityDiagnostics& OutDiagnostics,
const TArray<uint8>* EligibilityMask = nullptr)
{
OutDiagnostics = FVoxelConnectivityDiagnostics();
OutDiagnostics.Result = ConnectivityResult;
OutDiagnostics.bPlayerFitRestricted = EligibilityMask != nullptr;
OutDiagnostics.bPlayerFitResolved = EligibilityMask != nullptr;
OutDiagnostics.bStartSnapped = bStartSnapped;
OutDiagnostics.bGoalSnapped = bGoalSnapped;
OutDiagnostics.NumRouteRetries = NumRouteRetries;
@@ -1213,9 +1664,11 @@ namespace VoxelStrateMeasurePrivate
int32 LargestComponentLowestCell = INDEX_NONE;
int32 NumComponentsAtLeast1Pct = 0;
int64 NumAir = 0;
for (const uint8 bAir : Grid.Air)
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
{
NumAir += bAir != 0u ? 1 : 0;
NumAir += Grid.Air[Index] != 0u
&& (EligibilityMask == nullptr || (*EligibilityMask)[Index] != 0u)
? 1 : 0;
}
FloodFillAir(
Grid,
@@ -1225,7 +1678,8 @@ namespace VoxelStrateMeasurePrivate
LargestComponentCells,
LargestComponentLowestCell,
NumComponentsAtLeast1Pct,
nullptr);
nullptr,
EligibilityMask);
const int32 StartComponent = Components[Start];
const int32 GoalComponent = Components[Goal];
@@ -1285,11 +1739,30 @@ namespace VoxelStrateMeasurePrivate
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
int32& OutNumRouteRetries,
FVoxelConnectivityDiagnostics* OutDiagnostics)
FVoxelConnectivityDiagnostics* OutDiagnostics,
bool bPlayerFitRestricted = false,
FVoxelStrateMetrics* OutPlayerMetrics = nullptr)
{
bOutStartSnapped = false;
bOutGoalSnapped = false;
OutNumRouteRetries = 0;
if (OutPlayerMetrics != nullptr)
{
*OutPlayerMetrics = FVoxelStrateMetrics();
}
auto RefusePlayerFit = [&](const FString& Reason)
{
if (OutDiagnostics != nullptr)
{
*OutDiagnostics = FVoxelConnectivityDiagnostics();
OutDiagnostics->bPlayerFitRestricted = bPlayerFitRestricted;
OutDiagnostics->RefusalReason = Reason;
}
if (OutPlayerMetrics != nullptr)
{
OutPlayerMetrics->PlayerFitRefusalReason = Reason;
}
};
if (!FMath::IsFinite(AVoxel.X) || !FMath::IsFinite(AVoxel.Y)
|| !FMath::IsFinite(AVoxel.Z) || !FMath::IsFinite(BVoxel.X)
|| !FMath::IsFinite(BVoxel.Y) || !FMath::IsFinite(BVoxel.Z))
@@ -1297,6 +1770,7 @@ namespace VoxelStrateMeasurePrivate
if (OutDiagnostics != nullptr)
{
*OutDiagnostics = FVoxelConnectivityDiagnostics();
OutDiagnostics->bPlayerFitRestricted = bPlayerFitRestricted;
}
return EVoxelConnectivityResult::OutOfWindow;
}
@@ -1305,10 +1779,19 @@ namespace VoxelStrateMeasurePrivate
if (OutDiagnostics != nullptr)
{
*OutDiagnostics = FVoxelConnectivityDiagnostics();
OutDiagnostics->bPlayerFitRestricted = bPlayerFitRestricted;
}
return EVoxelConnectivityResult::OutOfWindow;
}
if (bPlayerFitRestricted && Settings.SampleStep != 1)
{
RefusePlayerFit(FString::Printf(
TEXT("Player-fit connectivity refused: SampleStep=%d; exact capsule fit requires SampleStep=1."),
Settings.SampleStep));
return EVoxelConnectivityResult::OutOfWindow;
}
FSampleGrid Grid;
FString RefusalReason;
if (!BuildSampleGrid(Generator, Manager, Sampler, StrateIndex,
@@ -1319,10 +1802,58 @@ namespace VoxelStrateMeasurePrivate
if (OutDiagnostics != nullptr)
{
*OutDiagnostics = FVoxelConnectivityDiagnostics();
OutDiagnostics->bPlayerFitRestricted = bPlayerFitRestricted;
OutDiagnostics->RefusalReason = RefusalReason;
}
if (OutPlayerMetrics != nullptr)
{
OutPlayerMetrics->PlayerFitRefusalReason = RefusalReason;
}
return EVoxelConnectivityResult::OutOfWindow;
}
TArray<uint8> PlayerFitMask;
FVoxelStrateMetrics PlayerMetrics;
if (bPlayerFitRestricted)
{
// A player-fit query needs only the fine-grid air count plus the direct capsule
// stencil. Do not pay for the legacy all-air flood fill, walkable-column scan, or
// Manhattan feature transform here: those values belong to the separate legacy
// measurement pass, and recomputing them for every fine candidate made the gate
// needlessly expensive without changing its answer.
PlayerMetrics.ResolvedMarginVoxels = Grid.ResolvedMarginVoxels;
PlayerMetrics.SampledMinZ = Grid.SampledMinZ;
PlayerMetrics.SampledMaxZ = Grid.SampledMaxZ;
PlayerMetrics.SampledNumX = Grid.NumX;
PlayerMetrics.SampledNumY = Grid.NumY;
PlayerMetrics.SampledNumZ = Grid.NumZ;
PlayerMetrics.SampledMinX = Grid.MinX;
PlayerMetrics.SampledMaxX = Grid.MaxX;
PlayerMetrics.SampledMinY = Grid.MinY;
PlayerMetrics.SampledMaxY = Grid.MaxY;
PlayerMetrics.NumSampled = Grid.CellCount;
for (const uint8 bAir : Grid.Air)
{
PlayerMetrics.NumAir += bAir != 0u ? 1 : 0;
}
PlayerMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - PlayerMetrics.NumAir;
PlayerMetrics.AirFraction = PlayerMetrics.NumSampled > 0
? static_cast<float>(static_cast<double>(PlayerMetrics.NumAir)
/ static_cast<double>(PlayerMetrics.NumSampled))
: 0.0f;
BuildPlayerFitMetrics(Grid, Settings, PlayerMetrics, &PlayerFitMask);
PlayerMetrics.bValid = PlayerMetrics.NumSampled > 0;
if (OutPlayerMetrics != nullptr)
{
*OutPlayerMetrics = PlayerMetrics;
}
if (!PlayerMetrics.bPlayerFitResolved)
{
RefusePlayerFit(PlayerMetrics.PlayerFitRefusalReason);
return EVoxelConnectivityResult::OutOfWindow;
}
}
int32 Start = -1;
int32 Goal = -1;
const EVoxelConnectivityResult Result = EvaluateConnectivityOnGrid(
@@ -1336,7 +1867,8 @@ namespace VoxelStrateMeasurePrivate
bOutStartSnapped,
bOutGoalSnapped,
OutNumRouteRetries,
Settings.MaxRouteRetries);
Settings.MaxRouteRetries,
bPlayerFitRestricted ? &PlayerFitMask : nullptr);
if (OutDiagnostics != nullptr)
{
PopulateConnectivityDiagnostics(
@@ -1347,7 +1879,16 @@ namespace VoxelStrateMeasurePrivate
bOutStartSnapped,
bOutGoalSnapped,
OutNumRouteRetries,
*OutDiagnostics);
*OutDiagnostics,
bPlayerFitRestricted ? &PlayerFitMask : nullptr);
if (bPlayerFitRestricted &&
(Result == EVoxelConnectivityResult::StartCellNotPlayerFit
|| Result == EVoxelConnectivityResult::GoalCellNotPlayerFit))
{
OutDiagnostics->RefusalReason = Result == EVoxelConnectivityResult::StartCellNotPlayerFit
? TEXT("arrival endpoint is not a player-fitting floor cell")
: TEXT("departure endpoint is not a player-fitting floor cell");
}
}
return Result;
}
@@ -1582,4 +2123,44 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
return Result;
}
EVoxelConnectivityResult VF_ArePlayerFitConnectedWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics* OutDiagnostics)
{
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity(
nullptr, nullptr, &Sampler, INDEX_NONE,
StrateBottomWorldZ, StrateTopWorldZ, BoundarySealThickness, true,
AVoxel, BVoxel, Settings, bOutStartSnapped, bOutGoalSnapped,
NumRouteRetries, OutDiagnostics, true, nullptr);
}
FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics* OutPlayerMetrics)
{
FVoxelConnectivityDiagnostics Result;
int32 NumRouteRetries = 0;
VoxelStrateMeasurePrivate::QueryConnectivity(
nullptr, nullptr, &Sampler, INDEX_NONE,
StrateBottomWorldZ, StrateTopWorldZ, BoundarySealThickness, true,
AVoxel, BVoxel, Settings, Result.bStartSnapped, Result.bGoalSnapped,
NumRouteRetries, &Result, true, OutPlayerMetrics);
return Result;
}
#endif // WITH_EDITOR — measurement never ships
@@ -716,6 +716,15 @@ bool VF_WriteStratePreviewCandidate(
OutCandidate.LargestWalkableSurfaceShare = Metrics.LargestWalkableSurfaceShare;
OutCandidate.MedianFeatureScale = Metrics.MedianFeatureScale;
OutCandidate.MedianVerticalClearance = Metrics.MedianVerticalClearance;
OutCandidate.bPlayerFitResolved = Metrics.bPlayerFitResolved;
OutCandidate.PlayerFitRefusalReason = Metrics.PlayerFitRefusalReason;
OutCandidate.NumPlayerFitCells = Metrics.NumPlayerFitCells;
OutCandidate.PlayerFitFraction = Metrics.PlayerFitFraction;
OutCandidate.NumTraversableComponents = Metrics.NumTraversableComponents;
OutCandidate.LargestTraversableComponentCells =
Metrics.LargestTraversableComponentCells;
OutCandidate.TraversableComponentShare = Metrics.TraversableComponentShare;
OutCandidate.MinimumPlayerClearanceVoxels = Metrics.MinimumPlayerClearanceVoxels;
OutCandidate.DistanceFromCorpusCentroid = FMath::IsFinite(DistanceFromCorpusCentroid)
? DistanceFromCorpusCentroid : 0.0;
@@ -1085,10 +1094,11 @@ bool VF_WriteStratePreviewIndex(
"walkable cell / sampled XY footprint</em>. Largest connected surface is the largest "
"four-neighbour component of those projected columns / all walkable columns. It is "
"not a 3D traversal proof; the arrival → departure route check is separate.</p>\n"
"<p class=\"note\"><strong>Part A measurement settings:</strong> step=4 voxels, "
"HeadroomCells=2, XY radius=256 voxels, MaxCells=8,000,000. Each summary row lists "
"the exact target, derived interior margin, Z range, XY range, and grid for every "
"hard-gate survivor; each card repeats its selected window.</p>\n"
"<p class=\"note\"><strong>Part A measurement settings:</strong> legacy fields use "
"step=4 voxels, HeadroomCells=2, XY radius=256 voxels, MaxCells=8,000,000. The "
"player-fit gate uses a separate step-1 fine ROI and refuses to answer at any coarser "
"step. Each summary row lists the exact target, derived interior margin, Z range, XY "
"range, and grid for every hard-gate survivor; each card repeats its selected window.</p>\n"
"<p class=\"note\"><strong>Reality check:</strong> real caves mix long, narrow passages "
"with rooms, branches, multiple levels, and occasional vertical shafts; there is no "
"universal target floor-to-volume ratio. The quantitative table below is therefore a "
@@ -1103,20 +1113,23 @@ bool VF_WriteStratePreviewIndex(
"<table class=\"summary\"><caption>Part A — survivor distributions; every value uses the exact window printed in its row</caption>\n"
"<tr><th>archetype</th><th>eligible single-region rolls / hard-gate survivors</th><th>walkable fraction<br>(median; min–max)</th>"
"<th>floor area fraction<br>(median; min–max)</th><th>median clearance over walkable cells<br>(median; min–max voxels)</th>"
"<th>largest connected surface<br>(median; min–max)</th><th>window</th></tr>\n");
"<th>largest connected surface<br>(median; min–max)</th><th>player-fit fraction<br>(selected fine ROI)</th>"
"<th>traversable component share<br>(selected fine ROI)</th><th>window</th></tr>\n");
for (const FVoxelStratePreviewArchetypeSummary& Summary : *ArchetypeSummaries)
{
const FString SummaryWindow = !Summary.WindowSummary.IsEmpty()
? Summary.WindowSummary
: Summary.Window.IsValid() ? Summary.Window.Describe() : TEXT("unavailable");
Html += FString::Printf(
TEXT("<tr><th>%s</th><td>%d / %d</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td class=\"summary-window\">%s</td></tr>\n"),
TEXT("<tr><th>%s</th><td>%d / %d</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td class=\"summary-window\">%s</td></tr>\n"),
*VF_HtmlEscape(Summary.ArchetypeName), Summary.EvaluatedCandidates,
Summary.HardGateSurvivors,
*VF_HtmlEscape(Summary.WalkableFractionSummary),
*VF_HtmlEscape(Summary.WalkableFloorAreaSummary),
*VF_HtmlEscape(Summary.MedianVerticalClearanceSummary),
*VF_HtmlEscape(Summary.LargestWalkableSurfaceSummary),
*VF_HtmlEscape(Summary.PlayerFitFractionSummary),
*VF_HtmlEscape(Summary.TraversableComponentShareSummary),
*VF_HtmlEscape(SummaryWindow));
}
Html += TEXT("</table>\n");
@@ -1142,10 +1155,11 @@ bool VF_WriteStratePreviewIndex(
"<span><i class=\"swatch\" style=\"background:#dae1e7\"></i>air</span>"
"<span><i class=\"swatch\" style=\"background:#f5a636\"></i>walkable cell</span>"
"<span><i class=\"swatch\" style=\"background:#4de2be\"></i>density=0 contour</span></div>\n"
"<p class=\"note\"><strong>Fine ROI — largest open space:</strong> the separate step-1 pass "
"is requested only for selected candidates (this run selects coarse survivors). Its XY "
"window is centred on the deterministic <code>LargestComponentPoint</code> from the coarse "
"air flood fill, not on the strate midpoint or the world origin. Its exact window is printed "
"<p class=\"note\"><strong>Fine ROI — player-fit audit:</strong> the separate step-1 pass "
"is used by the player-fit gate for every candidate that reaches the legacy screen, and "
"is rendered for the selected candidate. The caller supplies its bounded XY window; the "
"showcase fits that window to the deterministic arrival/departure mouth pair so the gate "
"cannot pass or fail because its endpoints were cropped out. Its exact window is printed "
"on the card. If its cell cap is exceeded, no fine allocation is made and the refusal is "
"shown on that card.</p>\n"
"<p class=\"note\">Ordering: survivors first, then each group by descending distance from "
@@ -1271,7 +1285,7 @@ bool VF_WriteStratePreviewIndex(
Html += FString::Printf(TEXT("<div class=\"no-image\">No coarse image: %s</div>\n"),
*VF_HtmlEscape(Candidate.RenderFailureReason));
}
Html += TEXT("</div>\n<div class=\"fine-section\"><h3>FINE ROI — largest open space; filled cells and density=0 contour</h3>\n");
Html += TEXT("</div>\n<div class=\"fine-section\"><h3>FINE ROI — player-fit audit; filled cells and density=0 contour</h3>\n");
if (!Candidate.bFineRequested)
{
Html += TEXT("<p class=\"not-requested\">Fine ROI not requested for this candidate; it did not pass the coarse survivor screen.</p>\n");
@@ -1294,7 +1308,7 @@ bool VF_WriteStratePreviewIndex(
{
const FString FineWindowDescription = Candidate.FineWindow.Describe();
Html += FString::Printf(
TEXT("<p class=\"window\">Largest-open-space fine ROI; exact window: %s; slice Y=%d voxels.</p>\n"
TEXT("<p class=\"window\">Player-fit fine ROI; exact window: %s; slice Y=%d voxels.</p>\n"
"<div class=\"pair\"><figure><img src=\"%s\" alt=\"candidate %d fine XZ filled cells\"><figcaption>"
"FINE ROI · filled cells · step=%d</figcaption></figure>\n"),
*VF_HtmlEscape(FineWindowDescription), Candidate.FineVerticalSliceWorldY,
@@ -1316,7 +1330,7 @@ bool VF_WriteStratePreviewIndex(
}
Html += FString::Printf(
TEXT("<p class=\"window\">Largest-open-space fine ROI; exact window: %s; selected Z=%d voxels.</p>\n"
TEXT("<p class=\"window\">Player-fit fine ROI; exact window: %s; selected Z=%d voxels.</p>\n"
"<div class=\"pair\"><figure><img src=\"%s\" alt=\"candidate %d fine XY filled cells\"><figcaption>"
"FINE ROI · filled cells · step=%d</figcaption></figure>\n"),
*VF_HtmlEscape(FineWindowDescription), Candidate.FinePlanSliceWorldZ,
@@ -1349,6 +1363,12 @@ bool VF_WriteStratePreviewIndex(
"<tr><th>walkable surface components</th><td>%s</td></tr>\n"
"<tr><th>feature scale (voxels)</th><td>%s</td></tr>\n"
"<tr><th>vertical clearance (voxels)</th><td>%s</td></tr>\n"
"<tr><th>player-fit resolved</th><td>%s</td></tr>\n"
"<tr><th>player-fit fraction (fine ROI)</th><td>%s</td></tr>\n"
"<tr><th>player-fitting floor cells</th><td>%s</td></tr>\n"
"<tr><th>traversable component share (fine ROI)</th><td>%s</td></tr>\n"
"<tr><th>traversable components</th><td>%s</td></tr>\n"
"<tr><th>minimum player clearance (voxels)</th><td>%s</td></tr>\n"
"<tr><th>arrival → departure</th><td class=\"verdict\">%s</td></tr>\n"
"<tr><th>distance from corpus centroid (roll-space)</th><td>%.6f</td></tr>\n</table>\n</section>\n"),
*VF_FormatMetric(Candidate.bMetricsValid, Candidate.AirFraction),
@@ -1360,6 +1380,12 @@ bool VF_WriteStratePreviewIndex(
*VF_FormatIntegerMetric(Candidate.bMetricsValid, Candidate.NumWalkableSurfaceComponents),
*VF_FormatMetric(Candidate.bMetricsValid, Candidate.MedianFeatureScale),
*VF_FormatIntegerMetric(Candidate.bMetricsValid, Candidate.MedianVerticalClearance),
Candidate.bPlayerFitResolved ? TEXT("yes") : TEXT("no"),
*VF_FormatMetric(Candidate.bPlayerFitResolved, Candidate.PlayerFitFraction),
*VF_FormatInteger64Metric(Candidate.bPlayerFitResolved, Candidate.NumPlayerFitCells),
*VF_FormatMetric(Candidate.bPlayerFitResolved, Candidate.TraversableComponentShare),
*VF_FormatIntegerMetric(Candidate.bPlayerFitResolved, Candidate.NumTraversableComponents),
*VF_FormatMetric(Candidate.bPlayerFitResolved, Candidate.MinimumPlayerClearanceVoxels),
*VF_HtmlEscape(Candidate.ArrivalDepartureVerdict),
Candidate.DistanceFromCorpusCentroid);
}
@@ -93,6 +93,9 @@ struct VOXELFORGE_API FVoxelSeasonCompositionSettings
// Coarse measurement is deliberately bounded. Tests may reduce the window; production may
// spend the overnight budget on a finer pass after the selector has been judged.
FVoxelStrateMeasureSettings MeasureSettings;
// Player-fit is a separate, explicitly fine pass. SampleStep values other than one are
// refused by the metric, never interpreted as an approximate capsule answer.
FVoxelStrateMeasureSettings PlayerFitMeasureSettings;
FVoxelSeasonSelectionPolicy SelectionPolicy;
// Explicit composed fixed slots take precedence. When empty, the composer adapts the
@@ -119,6 +122,13 @@ struct VOXELFORGE_API FVoxelSeasonCompositionSettings
MeasureSettings.MaxRouteRetries = 16;
MeasureSettings.HeadroomCells = 2;
MeasureSettings.InteriorMarginVoxels = -1;
PlayerFitMeasureSettings.SampleStep = 1;
PlayerFitMeasureSettings.RadiusInVoxels = 64;
PlayerFitMeasureSettings.MaxCells = 2000000;
PlayerFitMeasureSettings.MaxRouteRetries = 16;
PlayerFitMeasureSettings.HeadroomCells = 2;
PlayerFitMeasureSettings.InteriorMarginVoxels = -1;
}
};
@@ -200,6 +200,15 @@ struct VOXELFORGE_API FVoxelStrateMeasuredMetrics
float MedianFeatureScale = 0.0f;
int32 MedianVerticalClearance = 0;
bool bPlayerFitResolved = false;
FString PlayerFitRefusalReason;
int64 NumPlayerFitCells = 0;
float PlayerFitFraction = 0.0f;
int32 NumTraversableComponents = 0;
int64 LargestTraversableComponentCells = 0;
float TraversableComponentShare = 0.0f;
float MinimumPlayerClearanceVoxels = 0.0f;
int32 ResolvedMarginVoxels = 0;
int32 SampledMinZ = 0;
int32 SampledMaxZ = 0;
+73 -1
View File
@@ -7,6 +7,24 @@
class UVoxelGenerator;
class UVoxelStrateManager;
/**
* The confirmed base ACharacter capsule in voxel-space defaults.
*
* 25 cm/voxel is the authored world scale: radius = 34 cm / 25 = 1.36 voxels and
* half-height = 88 cm / 25 = 3.52 voxels, hence a 176 cm / 25 = 7.04 voxel total height.
* The measure settings copy these values so a future character can change the query without
* changing generation.
*/
struct VOXELFORGE_API FVoxelPlayerCapsuleConstants
{
static constexpr float VoxelSizeCentimeters = 25.0f;
static constexpr float RadiusCentimeters = 34.0f;
static constexpr float HalfHeightCentimeters = 88.0f;
static constexpr float RadiusVoxels = RadiusCentimeters / VoxelSizeCentimeters;
static constexpr float HalfHeightVoxels = HalfHeightCentimeters / VoxelSizeCentimeters;
static constexpr float HeightVoxels = 2.0f * HalfHeightVoxels;
};
/** Read-only density source used by the offline Tier 2 pass for a materialised custom stack. */
class VOXELFORGE_API IVoxelStrateDensitySampler
{
@@ -23,7 +41,10 @@ enum class EVoxelConnectivityResult : uint8
StartCellSolid,
GoalCellSolid,
OutOfWindow,
CoarseLiedBudgetExhausted // Unknown: retries are exhausted, or the refutation has no coarse edge to exclude.
CoarseLiedBudgetExhausted, // Unknown: retries are exhausted, or the refutation has no coarse edge to exclude.
// Appended to preserve the underlying values of the legacy result codes above.
StartCellNotPlayerFit,
GoalCellNotPlayerFit
};
/** Settings for the deterministic coarse grid used by the strate measurement pass. */
@@ -42,6 +63,12 @@ struct VOXELFORGE_API FVoxelStrateMeasureSettings
int32 MaxRouteRetries = 16; // Alternate coarse routes to full-resolution-check after the first route.
int32 HeadroomCells = 2; // Air cells above a floor cell required for walkable.
int32 InteriorMarginVoxels = -1; // <0 = derive from BoundarySealThickness (2x, clamped); explicit 0 includes the seal.
// Base ACharacter defaults at 25 cm/voxel: 34 cm radius / 25 = 1.36 voxels and
// 88 cm half-height / 25 = 3.52 voxels (7.04 voxels total). These are parameters, not
// generation values, so another playable capsule can be measured without a code change.
float PlayerCapsuleRadiusVoxels = FVoxelPlayerCapsuleConstants::RadiusVoxels;
float PlayerCapsuleHalfHeightVoxels = FVoxelPlayerCapsuleConstants::HalfHeightVoxels;
};
/**
@@ -135,6 +162,20 @@ struct VOXELFORGE_API FVoxelStrateMetrics
float MedianFeatureScale = 0.f;
int32 MedianVerticalClearance = 0;
// Player-fit metrics are answered only by a one-voxel grid. A coarse grid refuses to infer
// clearance because SampleStep=4 cannot resolve a 1.36-voxel radius capsule.
bool bPlayerFitResolved = false;
FString PlayerFitRefusalReason;
int64 NumPlayerFitCells = 0;
float PlayerFitFraction = 0.0f; // Player-fitting floor anchors / all air cells.
int32 NumTraversableComponents = 0;
int64 LargestTraversableComponentCells = 0;
float TraversableComponentShare = 0.0f; // Largest player-fit component / player-fit cells.
// Minimum horizontal Chebyshev clearance, in voxels, among player-fit floor anchors. This
// is a diagnostic only; player occupancy itself uses a direct capsule stencil, never the old
// Manhattan distance transform.
float MinimumPlayerClearanceVoxels = 0.0f;
// The exact vertical window used by the measurement: [SampledMinZ, SampledMaxZ).
int32 ResolvedMarginVoxels = 0;
int32 SampledMinZ = 0;
@@ -170,6 +211,9 @@ struct VOXELFORGE_API FVoxelConnectivityDiagnostics
{
bool bValid = false;
EVoxelConnectivityResult Result = EVoxelConnectivityResult::OutOfWindow;
bool bPlayerFitRestricted = false;
bool bPlayerFitResolved = false;
FString RefusalReason;
bool bStartSnapped = false;
bool bGoalSnapped = false;
int32 NumRouteRetries = 0; // Alternate routes full-resolution-checked after the first route.
@@ -300,3 +344,31 @@ VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings);
/**
* Player-fit connectivity over a custom stack. This is deliberately a separate API from the
* legacy air route: it refuses unless Settings.SampleStep == 1, resolves endpoints against the
* direct capsule-occupancy/floor mask, and flood-fills/routes only through player-fit cells.
*/
VOXELFORGE_API EVoxelConnectivityResult VF_ArePlayerFitConnectedWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics* OutDiagnostics = nullptr);
/** Full diagnostics variant for the player-fit custom-stack route. */
VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
FVoxelStrateMetrics* OutPlayerMetrics = nullptr);
@@ -124,6 +124,14 @@ struct VOXELFORGE_API FVoxelStratePreviewCandidate
float LargestWalkableSurfaceShare = 0.0f;
float MedianFeatureScale = 0.0f;
int32 MedianVerticalClearance = 0;
bool bPlayerFitResolved = false;
FString PlayerFitRefusalReason;
int64 NumPlayerFitCells = 0;
float PlayerFitFraction = 0.0f;
int32 NumTraversableComponents = 0;
int64 LargestTraversableComponentCells = 0;
float TraversableComponentShare = 0.0f;
float MinimumPlayerClearanceVoxels = 0.0f;
double DistanceFromCorpusCentroid = 0.0;
int32 VerticalSliceCellY = INDEX_NONE;
@@ -160,6 +168,8 @@ struct VOXELFORGE_API FVoxelStratePreviewArchetypeSummary
FString WalkableFloorAreaSummary;
FString MedianVerticalClearanceSummary;
FString LargestWalkableSurfaceSummary;
FString PlayerFitFractionSummary;
FString TraversableComponentShareSummary;
};
/** Copy the exact measurement-window metadata needed by the index page. */