feat(strate): passages aim at open space -- for half the archetypes, measurably
Tier 1. GeneratePassages placed every passage at a random angle, distance and reach, never consulting the destination strate. The carved tube is air by construction, so nothing looked wrong -- but whether the lower mouth joined the destination's connected cave space was luck. A player could descend and arrive in a sealed pocket. The primordial law was a hope. VF_SuggestOpenPointZ is a pure free function of (archetype, params, seed, XY): no operator stack, no StrateManager, no cache. That is deliberate. This runs inside Initialize, where the layout is half-built and LayoutVersion is in flight, so anything holding a manager pointer there is a re-entrancy trap. Coverage is four archetypes of eight, and the number is the deliverable: 7 passages, 3 aimed at generator-confirmed open space with 48/48 ring samples air, 4 falling back. The hole is not closed. It is measured, where this morning it was invisible. Two corrections from Codex, both mine and both would have shipped confident nonsense: room placement is seeded by MakeStrateSeed, not the passage salt, so my version would have queried a cave graph that does not exist; and slabs need the world seed because that is what GetSlabDensity uses. And one of mine caught in review: the first test sampled GetDensityAt at the mouth and asserted air, which is true by construction because the passage carve is a structural-post invariant. It could not fail. Replaced with a 16-point ring outside the carve radius requiring 8/16 air -- a proxy for connectivity, not a proof. The proof is a flood fill, which is Tier 2. Logging: my first spec asked for a per-passage warning on fallback. It fired 572 times and turned 14 clean tests yellow, which is precisely the anti-pattern OPSTACK-HANDOFF.md warns about -- a warning that always means "this is fine" teaches people to ignore warnings. Now one sorted Log line per layout build. Built clean, suite green: 17 succeeded, 0 with warnings, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPUuCtn5zTLui6WybLbkNb
This commit is contained in:
1 parent
10a8bbc677
commit
1ab8c0cae4
7 files changed
+806
-4
No files matched your search
@@ -266,6 +266,19 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
constexpr uint32 PassageSaltPhase = 0xA1100009u;
|
||||
constexpr uint32 PassageSaltBendFreq = 0xA110000Au;
|
||||
|
||||
int32 TotalPassages = 0;
|
||||
int32 NumAimedAtOpenPoint = 0;
|
||||
TSet<FString> NoQueryArchetypes;
|
||||
|
||||
const auto ArchetypeName = [](ECaveGeneratorType Archetype)
|
||||
{
|
||||
if (const UEnum* ArchetypeEnum = StaticEnum<ECaveGeneratorType>())
|
||||
{
|
||||
return ArchetypeEnum->GetNameStringByValue(static_cast<int64>(Archetype));
|
||||
}
|
||||
return FString::Printf(TEXT("Value_%d"), static_cast<int32>(Archetype));
|
||||
};
|
||||
|
||||
//=========================================================================
|
||||
// INTER-STRATE PASSAGES: tunnels connecting consecutive strates.
|
||||
// Each passage is randomly assigned one of 5 types, which determines
|
||||
@@ -317,13 +330,92 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
const float PX = FMath::Cos(Angle) * Distance;
|
||||
const float PY = FMath::Sin(Angle) * Distance;
|
||||
|
||||
++TotalPassages;
|
||||
|
||||
// Ask the destination archetype for a source-level open Z at this XY. This is pure
|
||||
// and safe during Initialize: it does not construct an operator stack or call back
|
||||
// into the manager. Cave room graphs use the same strate seed as BuildChunkCache;
|
||||
// slab fields use the world seed consumed by GetSlabDensity.
|
||||
float SuggestedLowerZ = 0.0f;
|
||||
bool bAimedAtOpenPoint = false;
|
||||
const UVoxelStrateDefinition* LowerDef = Lower.Definition;
|
||||
if (LowerDef)
|
||||
{
|
||||
const bool bUsesRoomSeed =
|
||||
LowerDef->GeneratorType == ECaveGeneratorType::TunnelNetwork
|
||||
|| LowerDef->GeneratorType == ECaveGeneratorType::Underwater;
|
||||
const int32 LowerQuerySeed = bUsesRoomSeed
|
||||
? static_cast<int32>(VoxelCaveMorphology::MakeStrateSeed(
|
||||
static_cast<uint32>(CachedSeed), Lower.StrateIndex))
|
||||
: CachedSeed;
|
||||
|
||||
bAimedAtOpenPoint = VF_SuggestOpenPointZ(
|
||||
LowerDef->GeneratorType,
|
||||
LowerDef->GenerationParams,
|
||||
LowerDef->SlabParams,
|
||||
LowerQuerySeed,
|
||||
LowerTopZ,
|
||||
(float)(Lower.BottomChunkZ) * CHUNK_SIZE,
|
||||
PX,
|
||||
PY,
|
||||
SuggestedLowerZ);
|
||||
|
||||
if (bAimedAtOpenPoint)
|
||||
{
|
||||
++NumAimedAtOpenPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
NoQueryArchetypes.Add(ArchetypeName(LowerDef->GeneratorType));
|
||||
}
|
||||
}
|
||||
|
||||
// LENGTH: reach into each strate, capped to the interior.
|
||||
const float UpperReach = FMath::Min(
|
||||
PassageRandomRange(Cfg.ReachMin, Cfg.ReachMax, PassageSaltUpperReach), UpperMax);
|
||||
const float LowerReach = FMath::Min(
|
||||
PassageRandomRange(Cfg.ReachMin, Cfg.ReachMax, PassageSaltLowerReach), LowerMax);
|
||||
const float TopZ = UpperBottomZ + UpperReach;
|
||||
const float BottomZ = LowerTopZ - LowerReach;
|
||||
float BottomZ = LowerTopZ - LowerReach;
|
||||
|
||||
if (bAimedAtOpenPoint)
|
||||
{
|
||||
float LowerSealThickness = 0.0f;
|
||||
switch (LowerDef->GeneratorType)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater:
|
||||
LowerSealThickness = LowerDef->GenerationParams.BoundarySealThickness;
|
||||
break;
|
||||
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber:
|
||||
LowerSealThickness = LowerDef->SlabParams.BoundarySealThickness;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const float LowerBottomZ = (float)(Lower.BottomChunkZ) * CHUNK_SIZE;
|
||||
const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
|
||||
const float InnerTopZ = LowerTopZ - LowerSealThickness;
|
||||
if (FMath::IsFinite(SuggestedLowerZ)
|
||||
&& FMath::IsFinite(InnerBottomZ)
|
||||
&& FMath::IsFinite(InnerTopZ)
|
||||
&& InnerBottomZ < InnerTopZ)
|
||||
{
|
||||
// VF_SuggestOpenPointZ already guarantees a strict interior answer. Keep a
|
||||
// tiny margin in the final clamp so a future source query cannot land on a
|
||||
// seal boundary through rounding.
|
||||
const float StrictMargin = FMath::Min(
|
||||
KINDA_SMALL_NUMBER, (InnerTopZ - InnerBottomZ) * 0.25f);
|
||||
BottomZ = FMath::Clamp(
|
||||
SuggestedLowerZ,
|
||||
InnerBottomZ + StrictMargin,
|
||||
InnerTopZ - StrictMargin);
|
||||
}
|
||||
}
|
||||
|
||||
const int32 Segments = FMath::Clamp(Cfg.Segments, 1, 48);
|
||||
Passage.ControlPoints.Reset();
|
||||
@@ -422,6 +514,25 @@ void UVoxelStrateManager::GeneratePassages()
|
||||
}
|
||||
}
|
||||
|
||||
TArray<FString> SortedNoQueryArchetypes;
|
||||
for (const FString& Archetype : NoQueryArchetypes)
|
||||
{
|
||||
SortedNoQueryArchetypes.Add(Archetype);
|
||||
}
|
||||
SortedNoQueryArchetypes.Sort();
|
||||
FString NoQueryList = TEXT("none");
|
||||
if (SortedNoQueryArchetypes.Num() > 0)
|
||||
{
|
||||
NoQueryList = FString::Join(SortedNoQueryArchetypes, TEXT(", "));
|
||||
}
|
||||
|
||||
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)."),
|
||||
NumAimedAtOpenPoint,
|
||||
TotalPassages,
|
||||
TotalPassages - NumAimedAtOpenPoint,
|
||||
*NoQueryList);
|
||||
|
||||
//=========================================================================
|
||||
// SURFACE ENTRY SHAFT — the one auto-opened (0,0) connection.
|
||||
// A straight vertical shaft at (0,0) piercing the TOP seal of the topmost
|
||||
|
||||
Reference in new issue
Block a user