fix(passages): aim the UPPER mouth too -- Tier 1 was only half built

VF_SuggestLandingPoint was called exactly once per passage, for the LOWER
mouth. The upper mouth fell out of the control-point chain with no landing
query at all: Tier 1 fixed where a player ARRIVES and never touched where
they DEPART. That predicted the measurement exactly -- departure->spine was
0 connected across all 16 seeds.

The ring proxy could never have caught this. It checks each mouth for open
space individually, and an unaimed mouth still has open space around it,
because the passage tube carved some. It took a connectivity measurement.

Each mouth now queries ITS OWN strate independently, which is what keeps
the layout a set rather than a sequence -- passage i never reads passage
i-1, so LayoutOrderIndependence stays green. Seed contracts kept unmixed;
SurfaceWorld still refuses; a declined query still preserves today's
placement rather than degrading it.

  departure->spine   0 -> 9 connected of 16
  arrival->departure 4 -> 7 of 16
  aiming coverage    upper 6/7 and lower 6/7 mouths now aimed at
                     generator-confirmed open space

⚠️ ACCEPTANCE STILL FAILS at 7/16, and the breakdown says why it is not
what it looks like:

  arrival->departure: CONNECTED 7, NOT_CONNECTED 2, COARSE_LIED 7

Only TWO seeds are genuinely disconnected. Seven are COARSE_LIED -- the
coarse BFS found a route and the full-resolution walk hit solid on it.
VF_AreConnected checks that ONE route and gives up, so a blocked candidate
is reported as failure even when another route exists. That is a false
NEGATIVE in the verifier, and it means this acceptance number probably
understates the world. Next task fixes the measurement, not the world.

Codex was killed mid-flight by a session teardown, so the edits arrived
unvalidated; I built and ran the suite myself rather than assume.
Build clean, 20 succeeded / 0 warnings / 0 failed, verified in VoxelM.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-03 15:14:25 +02:00
1 parent 7a20eeed0d
commit 9d5633ad54
2 files changed
+121 -45

No files matched your search

+116 -45
View File
@@ -267,7 +267,8 @@ void UVoxelStrateManager::GeneratePassages()
constexpr uint32 PassageSaltBendFreq = 0xA110000Au; constexpr uint32 PassageSaltBendFreq = 0xA110000Au;
int32 TotalPassages = 0; int32 TotalPassages = 0;
int32 NumAimedAtOpenPoint = 0; int32 NumAimedAtUpperOpenPoint = 0;
int32 NumAimedAtLowerOpenPoint = 0;
TSet<FString> NoQueryArchetypes; TSet<FString> NoQueryArchetypes;
const auto ArchetypeName = [](ECaveGeneratorType Archetype) const auto ArchetypeName = [](ECaveGeneratorType Archetype)
@@ -323,6 +324,32 @@ void UVoxelStrateManager::GeneratePassages()
} }
}; };
const auto BoundarySealThicknessFor = [](const UVoxelStrateDefinition& Definition) -> float
{
switch (Definition.GeneratorType)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return Definition.GenerationParams.BoundarySealThickness;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
return Definition.SlabParams.BoundarySealThickness;
case ECaveGeneratorType::Maze:
return Definition.MazeParams.BoundarySealThickness;
case ECaveGeneratorType::VerticalShafts:
return Definition.VerticalShaftParams.BoundarySealThickness;
case ECaveGeneratorType::FloatingIslands:
return Definition.FloatingIslandParams.BoundarySealThickness;
default:
return 0.0f;
}
};
//========================================================================= //=========================================================================
// INTER-STRATE PASSAGES: tunnels connecting consecutive strates. // INTER-STRATE PASSAGES: tunnels connecting consecutive strates.
// Each passage is randomly assigned one of 5 types, which determines // Each passage is randomly assigned one of 5 types, which determines
@@ -340,6 +367,7 @@ void UVoxelStrateManager::GeneratePassages()
const FStratePassageConfig& Cfg = UpperDef->PassageConfig; const FStratePassageConfig& Cfg = UpperDef->PassageConfig;
// Upper strate floor and lower strate ceiling (differ when there's a bedrock gap). // Upper strate floor and lower strate ceiling (differ when there's a bedrock gap).
const float UpperTopZ = (float)(Upper.TopChunkZ + 1) * CHUNK_SIZE;
const float UpperBottomZ = (float)(Upper.BottomChunkZ) * CHUNK_SIZE; const float UpperBottomZ = (float)(Upper.BottomChunkZ) * CHUNK_SIZE;
const float LowerTopZ = (float)(Lower.TopChunkZ + 1) * CHUNK_SIZE; const float LowerTopZ = (float)(Lower.TopChunkZ + 1) * CHUNK_SIZE;
const float UpperMax = (float)Upper.HeightInChunks * CHUNK_SIZE * 0.9f; const float UpperMax = (float)Upper.HeightInChunks * CHUNK_SIZE * 0.9f;
@@ -376,12 +404,51 @@ void UVoxelStrateManager::GeneratePassages()
++TotalPassages; ++TotalPassages;
// Ask the destination archetype for a source-level landing point. This is pure and // Ask each mouth's own strate for a source-level landing point. These queries are pure
// safe during Initialize: it does not construct an operator stack or call back into // and safe during Initialize: they do not construct an operator stack or call back
// the manager. Cave room graphs use the strate seed from MakeStrateSeed; slab, maze, // into the manager. Cave room graphs use the queried strate's seed from
// shaft, and island fields use the world seed consumed by their source. // MakeStrateSeed; slab, maze, shaft, and island fields use the world seed consumed by
// their source. The two queries are deliberately independent: a passage never reads
// another passage's endpoint or any live layout state.
FVector SuggestedUpperPoint = FVector::ZeroVector;
bool bAimedUpperAtOpenPoint = false;
if (UpperDef)
{
const bool bUsesRoomSeed =
UpperDef->GeneratorType == ECaveGeneratorType::TunnelNetwork
|| UpperDef->GeneratorType == ECaveGeneratorType::Underwater;
const int32 UpperQuerySeed = bUsesRoomSeed
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
static_cast<uint32>(CachedSeed), Upper.StrateIndex))
: CachedSeed;
bAimedUpperAtOpenPoint = VF_SuggestLandingPoint(
UpperDef->GeneratorType,
UpperDef->GenerationParams,
UpperDef->SlabParams,
UpperDef->MazeParams,
UpperDef->VerticalShaftParams,
UpperDef->FloatingIslandParams,
UpperQuerySeed,
UpperTopZ,
UpperBottomZ,
PX,
PY,
MaxLateralSnapFor(*UpperDef),
SuggestedUpperPoint);
if (bAimedUpperAtOpenPoint)
{
++NumAimedAtUpperOpenPoint;
}
else
{
NoQueryArchetypes.Add(ArchetypeName(UpperDef->GeneratorType));
}
}
FVector SuggestedLowerPoint = FVector::ZeroVector; FVector SuggestedLowerPoint = FVector::ZeroVector;
bool bAimedAtOpenPoint = false; bool bAimedLowerAtOpenPoint = false;
const UVoxelStrateDefinition* LowerDef = Lower.Definition; const UVoxelStrateDefinition* LowerDef = Lower.Definition;
if (LowerDef) if (LowerDef)
{ {
@@ -394,7 +461,7 @@ void UVoxelStrateManager::GeneratePassages()
: CachedSeed; : CachedSeed;
const float MaxLateralSnap = MaxLateralSnapFor(*LowerDef); const float MaxLateralSnap = MaxLateralSnapFor(*LowerDef);
bAimedAtOpenPoint = VF_SuggestLandingPoint( bAimedLowerAtOpenPoint = VF_SuggestLandingPoint(
LowerDef->GeneratorType, LowerDef->GeneratorType,
LowerDef->GenerationParams, LowerDef->GenerationParams,
LowerDef->SlabParams, LowerDef->SlabParams,
@@ -409,9 +476,9 @@ void UVoxelStrateManager::GeneratePassages()
MaxLateralSnap, MaxLateralSnap,
SuggestedLowerPoint); SuggestedLowerPoint);
if (bAimedAtOpenPoint) if (bAimedLowerAtOpenPoint)
{ {
++NumAimedAtOpenPoint; ++NumAimedAtLowerOpenPoint;
} }
else else
{ {
@@ -424,43 +491,44 @@ void UVoxelStrateManager::GeneratePassages()
PassageRandomRange(Cfg.ReachMin, Cfg.ReachMax, PassageSaltUpperReach), UpperMax); PassageRandomRange(Cfg.ReachMin, Cfg.ReachMax, PassageSaltUpperReach), UpperMax);
const float LowerReach = FMath::Min( const float LowerReach = FMath::Min(
PassageRandomRange(Cfg.ReachMin, Cfg.ReachMax, PassageSaltLowerReach), LowerMax); PassageRandomRange(Cfg.ReachMin, Cfg.ReachMax, PassageSaltLowerReach), LowerMax);
const float TopZ = UpperBottomZ + UpperReach; float TopZ = UpperBottomZ + UpperReach;
float BottomZ = LowerTopZ - LowerReach; float BottomZ = LowerTopZ - LowerReach;
float UpperX = PX;
float UpperY = PY;
float LowerX = PX; float LowerX = PX;
float LowerY = PY; float LowerY = PY;
Passage.RequestedUpperPoint = FVector(PX, PY, TopZ);
Passage.RequestedLowerPoint = FVector(PX, PY, BottomZ); Passage.RequestedLowerPoint = FVector(PX, PY, BottomZ);
if (bAimedAtOpenPoint) if (bAimedUpperAtOpenPoint)
{ {
float LowerSealThickness = 0.0f; const float UpperSealThickness = BoundarySealThicknessFor(*UpperDef);
switch (LowerDef->GeneratorType) const float InnerBottomZ = UpperBottomZ + UpperSealThickness;
const float InnerTopZ = UpperTopZ - UpperSealThickness;
if (FMath::IsFinite(SuggestedUpperPoint.X)
&& FMath::IsFinite(SuggestedUpperPoint.Y)
&& FMath::IsFinite(SuggestedUpperPoint.Z)
&& FMath::IsFinite(InnerBottomZ)
&& FMath::IsFinite(InnerTopZ)
&& InnerBottomZ < InnerTopZ)
{ {
case ECaveGeneratorType::TunnelNetwork: // VF_SuggestLandingPoint already guarantees a strict interior answer. Keep a
case ECaveGeneratorType::Underwater: // tiny margin in the final clamp so a future source query cannot land on a
LowerSealThickness = LowerDef->GenerationParams.BoundarySealThickness; // seal boundary through rounding.
break; UpperX = SuggestedUpperPoint.X;
UpperY = SuggestedUpperPoint.Y;
case ECaveGeneratorType::FlatPlain: const float StrictMargin = FMath::Min(
case ECaveGeneratorType::CrystalChamber: KINDA_SMALL_NUMBER, (InnerTopZ - InnerBottomZ) * 0.25f);
LowerSealThickness = LowerDef->SlabParams.BoundarySealThickness; TopZ = FMath::Clamp(
break; SuggestedUpperPoint.Z,
InnerBottomZ + StrictMargin,
case ECaveGeneratorType::Maze: InnerTopZ - StrictMargin);
LowerSealThickness = LowerDef->MazeParams.BoundarySealThickness; }
break;
case ECaveGeneratorType::VerticalShafts:
LowerSealThickness = LowerDef->VerticalShaftParams.BoundarySealThickness;
break;
case ECaveGeneratorType::FloatingIslands:
LowerSealThickness = LowerDef->FloatingIslandParams.BoundarySealThickness;
break;
default:
break;
} }
if (bAimedLowerAtOpenPoint)
{
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
const float LowerBottomZ = (float)(Lower.BottomChunkZ) * CHUNK_SIZE; const float LowerBottomZ = (float)(Lower.BottomChunkZ) * CHUNK_SIZE;
const float InnerBottomZ = LowerBottomZ + LowerSealThickness; const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
const float InnerTopZ = LowerTopZ - LowerSealThickness; const float InnerTopZ = LowerTopZ - LowerSealThickness;
@@ -558,15 +626,15 @@ void UVoxelStrateManager::GeneratePassages()
Z += NZ * VOXEL_NOISE_SCALE * Cfg.VerticalWobble * Env; Z += NZ * VOXEL_NOISE_SCALE * Cfg.VerticalWobble * Env;
} }
const float BaseX = FMath::Lerp(PX, LowerX, T); const float BaseX = FMath::Lerp(UpperX, LowerX, T);
const float BaseY = FMath::Lerp(PY, LowerY, T); const float BaseY = FMath::Lerp(UpperY, LowerY, T);
FVector ControlPoint(BaseX + OX, BaseY + OY, Z); FVector ControlPoint(BaseX + OX, BaseY + OY, Z);
// The style envelope is mathematically zero at a mouth, but evaluating sin(PI) // The style envelope is mathematically zero at a mouth, but evaluating sin(PI)
// leaves a tiny float residue. Pin both endpoints so the full query result is the // leaves a tiny float residue. Pin both endpoints so the full query result is the
// actual lower mouth, including a lateral snap, and never merely an approximation. // actual aimed mouth, including any lateral snap, and never merely an approximation.
if (s == 0) if (s == 0)
{ {
ControlPoint = FVector(PX, PY, TopZ); ControlPoint = FVector(UpperX, UpperY, TopZ);
} }
else if (s == Segments) else if (s == Segments)
{ {
@@ -587,7 +655,7 @@ void UVoxelStrateManager::GeneratePassages()
Passage.Radius = FMath::Max(Passage.Radius, FMath::Max(Cfg.MouthRadius, Cfg.MidRadius)); Passage.Radius = FMath::Max(Passage.Radius, FMath::Max(Cfg.MouthRadius, Cfg.MidRadius));
// Bounding sphere over all control points (+ widest radius + blend) for culling. This // Bounding sphere over all control points (+ widest radius + blend) for culling. This
// remains conservative when the lower mouth snaps laterally: every slanted segment is // remains conservative when either mouth snaps laterally: every slanted segment is
// between two control points, and a segment lies inside the sphere containing both // between two control points, and a segment lies inside the sphere containing both
// endpoints. Under-sizing this sphere would make EvaluateModifierSDF cull a real // endpoints. Under-sizing this sphere would make EvaluateModifierSDF cull a real
// passage and leave a sealed pocket. // passage and leave a sealed pocket.
@@ -621,10 +689,13 @@ void UVoxelStrateManager::GeneratePassages()
} }
UE_LOG(LogTemp, Log, UE_LOG(LogTemp, Log,
TEXT("[StrateManager] Passage landing sites: %d/%d aimed at generator-confirmed open space, %d fell back to random reach (archetypes with no query: %s)."), TEXT("[StrateManager] Passage landing sites: upper %d/%d and lower %d/%d aimed at generator-confirmed open space, %d/%d mouth queries fell back to random reach (archetypes with no query: %s)."),
NumAimedAtOpenPoint, NumAimedAtUpperOpenPoint,
TotalPassages, TotalPassages,
TotalPassages - NumAimedAtOpenPoint, NumAimedAtLowerOpenPoint,
TotalPassages,
(TotalPassages * 2) - NumAimedAtUpperOpenPoint - NumAimedAtLowerOpenPoint,
TotalPassages * 2,
*NoQueryList); *NoQueryList);
//========================================================================= //=========================================================================
@@ -48,6 +48,11 @@ struct FVoxelPassage
FVector UpperPoint = FVector::ZeroVector; // Entry in upper strate FVector UpperPoint = FVector::ZeroVector; // Entry in upper strate
FVector LowerPoint = FVector::ZeroVector; // Exit in lower strate FVector LowerPoint = FVector::ZeroVector; // Exit in lower strate
// The deterministic upper-mouth request before source landing-site adjustment. Kept for
// diagnostics/tests so the bounded lateral snap can be measured against the original spine-
// relative placement; not used by the SDF evaluator.
FVector RequestedUpperPoint = FVector::ZeroVector;
// The deterministic lower-mouth request before destination landing-site adjustment. Kept for // The deterministic lower-mouth request before destination landing-site adjustment. Kept for
// diagnostics/tests so the bounded lateral snap can be measured against the original spine- // diagnostics/tests so the bounded lateral snap can be measured against the original spine-
// relative placement; not used by the SDF evaluator. // relative placement; not used by the SDF evaluator.