feat(composer): PROMOTION -- the corpus grows itself, and it did not collapse
Tier 5. §3.3's answer to Jahni's "I'd like the system to not rely at all on
the handmade ones": promotion does not REMOVE the authored seed, it dilutes
it. Every invented strate that measures well rejoins the corpus.
Five simulated seasons, 24 candidates each:
season corpus project/default/promoted survival spread
1 15 4/8/3 20.8% 0.869
2 20 4/8/8 54.2% 0.849
3 20 4/8/8 54.2% 0.849
4 21 4/8/9 45.8% 0.916
5 21 4/8/9 50.0% 0.916
Corpus 12 -> 21, of which 9 are discovered. Survival roughly doubles.
⭐ AND THE COLLAPSE RISK DID NOT MATERIALISE, which was the thing worth
measuring rather than assuming. Promotion selects survivors, and blending
survivors repeatedly can converge the corpus onto a narrow safe region --
the composer would get better at passing and worse at surprising. Spread
went 0.869 -> 0.916. It dipped and plateaued mid-run, but did not narrow.
Recorded honestly: seasons 3 and 5 promoted ZERO. The near-duplicate gate
(normalised metric distance < 0.20 over air, largest share, walkable,
feature scale, clearance) is doing its job, but it also means the corpus
saturates at this scale. Five seasons of 24 demonstrates the MECHANISM, not
§3.3's "season 5 blends 200 discovered ones" -- that needs the overnight
budget, not a test.
Safety: a promoted record is RE-MEASURED on load, never trusted. Verified by
corrupting a stored metric -- the loader discarded it and re-derived the
geometry. A stale or tampered record therefore cannot introduce an
unescapable strate into a future season, which is the one failure that would
matter. Provenance (project/default/promoted) is tracked and logged.
Codex's recommendation, and I agree: keep a small explicit OUTLIER QUOTA
among candidates that pass every hard gate. This run showed no collapse, but
survivor-only promotion keeps the long-term risk, and a quota costs nothing.
PART A: the fine ROI now centres on LargestComponentPoint instead of (0,0).
Blank fine renders 6/42 -> 3/42. The label now reads "largest open space",
not "strate centre" -- the image moved, so the caption had to.
Build clean, 27 succeeded / 0 failed, verified by me. WorldRadiusVoxels=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
43eaf1ef0e
commit
0b3ec18b43
10 files changed
+2614
-53
No files matched your search
@@ -620,30 +620,26 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
VF_GetProjectAssetPaths(UVoxelStrateDefinition::StaticClass());
|
||||
const TArray<FString> ProjectTerrainOpAssets =
|
||||
VF_GetProjectAssetPaths(UVoxelTerrainOpDefinition::StaticClass());
|
||||
int32 NumProjectCorpusEntries = 0;
|
||||
int32 NumDefaultCorpusEntries = 0;
|
||||
const int32 NumProjectCorpusEntries = Corpus.NumForProvenance(
|
||||
EVoxelStrateCorpusProvenance::Project);
|
||||
const int32 NumDefaultCorpusEntries = Corpus.NumForProvenance(
|
||||
EVoxelStrateCorpusProvenance::Default);
|
||||
const int32 NumPromotedCorpusEntries = Corpus.NumForProvenance(
|
||||
EVoxelStrateCorpusProvenance::Promoted);
|
||||
FString CorpusMembership = TEXT("CORPUS MEMBERSHIP\n");
|
||||
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
|
||||
{
|
||||
if (VF_IsComposerDefault(Entry))
|
||||
{
|
||||
++NumDefaultCorpusEntries;
|
||||
}
|
||||
else
|
||||
{
|
||||
++NumProjectCorpusEntries;
|
||||
}
|
||||
CorpusMembership += FString::Printf(TEXT("%s | %s | %s\n"),
|
||||
VF_IsComposerDefault(Entry) ? TEXT("default") : TEXT("project"),
|
||||
VF_GetStrateCorpusProvenanceName(Entry.Provenance),
|
||||
VF_GetStrateArchetypeName(Entry.Archetype),
|
||||
*Entry.SourcePath);
|
||||
}
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Corpus membership: %d project strate assets discovered, %d project vectors loaded, "
|
||||
"%d defaults, %d total members. Terrain-op Asset Registry count (not rolled in this "
|
||||
"%d defaults, %d promoted, %d total members. Terrain-op Asset Registry count (not rolled in this "
|
||||
"task; live fields use header defaults instead): %d."),
|
||||
ProjectStrateAssets.Num(), NumProjectCorpusEntries, NumDefaultCorpusEntries, Corpus.Num(),
|
||||
ProjectTerrainOpAssets.Num()));
|
||||
ProjectStrateAssets.Num(), NumProjectCorpusEntries, NumDefaultCorpusEntries,
|
||||
NumPromotedCorpusEntries, Corpus.Num(), ProjectTerrainOpAssets.Num()));
|
||||
AddInfo(CorpusMembership);
|
||||
for (const ECaveGeneratorType Archetype : {
|
||||
ECaveGeneratorType::TunnelNetwork,
|
||||
@@ -712,11 +708,11 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
FString SpreadTable = TEXT(
|
||||
"CORPUS SPREAD (population stddev; spread jitter = ±15% of max-min; near-zero spread "
|
||||
"uses ±25% of field magnitude with a one-unit floor; clamps are safety only)\n"
|
||||
"archetype | struct | field | kind | excluded | reflected | authored | op-defaults | samples | min | max | mean | stddev | clamp\n");
|
||||
"archetype | struct | field | kind | excluded | reflected | authored | promoted | op-defaults | samples | min | max | mean | stddev | clamp\n");
|
||||
for (const FVoxelStrateFieldSpread& Spread : Corpus.GetFieldSpreads())
|
||||
{
|
||||
SpreadTable += FString::Printf(
|
||||
TEXT("%s | %s | %s | %s | %s | %s | %d | %d | %d | %s | %s | %s | %s | %s\n"),
|
||||
TEXT("%s | %s | %s | %s | %s | %s | %d | %d | %d | %d | %s | %s | %s | %s | %s\n"),
|
||||
VF_GetStrateArchetypeName(Spread.Archetype),
|
||||
*Spread.ParamStructName,
|
||||
*Spread.FieldName,
|
||||
@@ -724,6 +720,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
Spread.bExcluded ? TEXT("yes") : TEXT("no"),
|
||||
Spread.bReflected ? TEXT("yes") : TEXT("no"),
|
||||
Spread.AuthoredSampleCount,
|
||||
Spread.PromotedSampleCount,
|
||||
Spread.DefaultSeedCount,
|
||||
Spread.SampleCount,
|
||||
*VF_FormatSpreadValue(Spread.Min),
|
||||
|
||||
@@ -0,0 +1,714 @@
|
||||
// Tier 5 — promotion, corpus persistence, re-verification, and compounding simulation.
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
#include "Math/RandomStream.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int32 NumSimulatedSeasons = 5;
|
||||
constexpr int32 CandidatesPerSeason = 24;
|
||||
constexpr int32 PromotionCapPerSeason = 6;
|
||||
constexpr float LargestComponentSurvivalThreshold = 0.50f;
|
||||
constexpr int32 SimulationSampleStep = 8;
|
||||
constexpr int32 SimulationRadius = 256;
|
||||
|
||||
const TCHAR* VF_ConnectivityName(EVoxelConnectivityResult Result)
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case EVoxelConnectivityResult::Connected: return TEXT("Connected");
|
||||
case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnected");
|
||||
case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid");
|
||||
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid");
|
||||
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow");
|
||||
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudget");
|
||||
}
|
||||
return TEXT("Unknown");
|
||||
}
|
||||
|
||||
void VF_SetAllRuntimeBounds(FVoxelStrateArchetypeParams& Params,
|
||||
float TopWorldZ, float BottomWorldZ)
|
||||
{
|
||||
Params.TunnelNetworkParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.TunnelNetworkParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.SlabParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.SlabParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.MazeParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.MazeParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.SurfaceParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.SurfaceParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.VerticalShaftParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.VerticalShaftParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.FloatingIslandParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.FloatingIslandParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
}
|
||||
|
||||
float VF_GetBoundarySeal(const FVoxelStrateArchetypeParams& Params,
|
||||
ECaveGeneratorType Archetype)
|
||||
{
|
||||
switch (Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater:
|
||||
return Params.TunnelNetworkParams.BoundarySealThickness;
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber:
|
||||
return Params.SlabParams.BoundarySealThickness;
|
||||
case ECaveGeneratorType::Maze:
|
||||
return Params.MazeParams.BoundarySealThickness;
|
||||
case ECaveGeneratorType::SurfaceWorld:
|
||||
return Params.SurfaceParams.BoundarySealThickness;
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
return Params.VerticalShaftParams.BoundarySealThickness;
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
return Params.FloatingIslandParams.BoundarySealThickness;
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
ECaveGeneratorType VF_ArchetypeForRecipe(const FVoxelOpStackRecipe& Recipe)
|
||||
{
|
||||
switch (Recipe.StructuralParamBlock)
|
||||
{
|
||||
case EVoxelStrateParamBlock::TunnelNetwork: return ECaveGeneratorType::TunnelNetwork;
|
||||
case EVoxelStrateParamBlock::Slab: return ECaveGeneratorType::FlatPlain;
|
||||
case EVoxelStrateParamBlock::Maze: return ECaveGeneratorType::Maze;
|
||||
case EVoxelStrateParamBlock::Surface: return ECaveGeneratorType::SurfaceWorld;
|
||||
case EVoxelStrateParamBlock::VerticalShaft: return ECaveGeneratorType::VerticalShafts;
|
||||
case EVoxelStrateParamBlock::FloatingIsland: return ECaveGeneratorType::FloatingIslands;
|
||||
default: return ECaveGeneratorType::TunnelNetwork;
|
||||
}
|
||||
}
|
||||
|
||||
class FStackDensitySampler final : public IVoxelStrateDensitySampler
|
||||
{
|
||||
public:
|
||||
explicit FStackDensitySampler(const FVoxelOpStack& InStack) : Stack(InStack) {}
|
||||
|
||||
float SampleDensity(float WorldX, float WorldY, float WorldZ) const override
|
||||
{
|
||||
return Stack.EvalMC(WorldX, WorldY, WorldZ);
|
||||
}
|
||||
|
||||
private:
|
||||
const FVoxelOpStack& Stack;
|
||||
};
|
||||
|
||||
class FPromotionVerifier final : public IVoxelStratePromotionVerifier
|
||||
{
|
||||
public:
|
||||
FPromotionVerifier(const VoxelForgeTest::FTestWorld& InWorld,
|
||||
int32 InTopVoxelZ,
|
||||
int32 InBottomVoxelZ,
|
||||
const FVector& InArrivalPoint,
|
||||
const FVector& InDeparturePoint,
|
||||
const FVoxelStrateMeasureSettings& InMeasureSettings,
|
||||
float InLargestThreshold)
|
||||
: World(InWorld)
|
||||
, TopVoxelZ(InTopVoxelZ)
|
||||
, BottomVoxelZ(InBottomVoxelZ)
|
||||
, ArrivalPoint(InArrivalPoint)
|
||||
, DeparturePoint(InDeparturePoint)
|
||||
, MeasureSettings(InMeasureSettings)
|
||||
, LargestThreshold(InLargestThreshold)
|
||||
{
|
||||
}
|
||||
|
||||
bool Verify(const FVoxelStratePromotableRecord& Record,
|
||||
FVoxelStratePromotionVerification& OutVerification) const override
|
||||
{
|
||||
++NumVerifications;
|
||||
OutVerification = FVoxelStratePromotionVerification();
|
||||
|
||||
if (Record.Archetype != VF_ArchetypeForRecipe(Record.Recipe))
|
||||
{
|
||||
OutVerification.FailureReason = TEXT("record archetype does not match recipe post block");
|
||||
return false;
|
||||
}
|
||||
|
||||
FVoxelStrateArchetypeParams Params = Record.Params;
|
||||
VF_SetAllRuntimeBounds(Params, static_cast<float>(TopVoxelZ) + 1.0f,
|
||||
static_cast<float>(BottomVoxelZ));
|
||||
|
||||
FVoxelOpStack Stack;
|
||||
FVoxelOpContext Context;
|
||||
FString BuildError;
|
||||
if (!VF_BuildStackFromRecipe(
|
||||
Record.Recipe, Params, Record.Seed, 14.0f,
|
||||
World.StrateManager.Get(), Stack, Context, &BuildError))
|
||||
{
|
||||
OutVerification.FailureReason = FString::Printf(
|
||||
TEXT("recipe rebuild failed: %s"), *BuildError);
|
||||
return false;
|
||||
}
|
||||
if (Context.WorldRadiusVoxels != 0.0f)
|
||||
{
|
||||
OutVerification.FailureReason = TEXT("re-verified recipe changed WorldRadiusVoxels");
|
||||
return false;
|
||||
}
|
||||
|
||||
Stack.PrepareChunk(Context);
|
||||
FStackDensitySampler Sampler(Stack);
|
||||
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||
MeasureSettings, nullptr);
|
||||
OutVerification.MeasuredMetrics = VF_SummarizeStrateMetrics(Metrics);
|
||||
OutVerification.bPassedNonVacuous = Metrics.bValid
|
||||
&& Metrics.NumSampled > 0 && Metrics.NumAir > 0 && Metrics.NumSolid > 0;
|
||||
OutVerification.bPassedLargestComponent = OutVerification.bPassedNonVacuous
|
||||
&& Metrics.LargestComponentShare >= LargestThreshold;
|
||||
|
||||
const FVoxelConnectivityDiagnostics Law = VF_DiagnoseConnectivityWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||
ArrivalPoint, DeparturePoint, MeasureSettings);
|
||||
OutVerification.bPassedPrimordialLaw = Law.bValid
|
||||
&& Law.Result == EVoxelConnectivityResult::Connected
|
||||
&& !Law.bStartSnapped && !Law.bGoalSnapped;
|
||||
|
||||
if (!OutVerification.PassedAllGates())
|
||||
{
|
||||
OutVerification.FailureReason = FString::Printf(
|
||||
TEXT("fresh gates failed: non-vacuous=%s largest=%s law=%s"),
|
||||
OutVerification.bPassedNonVacuous ? TEXT("yes") : TEXT("no"),
|
||||
OutVerification.bPassedLargestComponent ? TEXT("yes") : TEXT("no"),
|
||||
VF_ConnectivityName(Law.Result));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MeasureCorpusEntry(const FVoxelStrateCorpusEntry& Entry,
|
||||
FVoxelStrateMeasuredMetrics& OutMetrics) const override;
|
||||
|
||||
mutable int32 NumVerifications = 0;
|
||||
|
||||
private:
|
||||
const VoxelForgeTest::FTestWorld& World;
|
||||
int32 TopVoxelZ = 0;
|
||||
int32 BottomVoxelZ = 0;
|
||||
FVector ArrivalPoint = FVector::ZeroVector;
|
||||
FVector DeparturePoint = FVector::ZeroVector;
|
||||
FVoxelStrateMeasureSettings MeasureSettings;
|
||||
float LargestThreshold = 0.50f;
|
||||
};
|
||||
|
||||
bool VF_MeasureNativeCorpusEntry(
|
||||
const VoxelForgeTest::FTestWorld& World,
|
||||
const FVoxelStrateCorpusEntry& Entry,
|
||||
int32 TopVoxelZ,
|
||||
int32 BottomVoxelZ,
|
||||
const FVoxelStrateMeasureSettings& Settings,
|
||||
FVoxelStrateMeasuredMetrics& OutMetrics)
|
||||
{
|
||||
FVoxelStrateArchetypeParams Params = Entry.ArchetypeParams;
|
||||
VF_SetAllRuntimeBounds(Params, static_cast<float>(TopVoxelZ) + 1.0f,
|
||||
static_cast<float>(BottomVoxelZ));
|
||||
|
||||
FVoxelOpStack Stack;
|
||||
FVoxelOpContext Context;
|
||||
if (!VF_BuildNativeStrateStackForCandidate(
|
||||
Entry.Archetype, Params, 1337, 14.0f, 0.0f,
|
||||
VF_GetBoundarySeal(Params, Entry.Archetype),
|
||||
World.StrateManager.Get(), Stack, Context)
|
||||
|| Context.WorldRadiusVoxels != 0.0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Stack.PrepareChunk(Context);
|
||||
FStackDensitySampler Sampler(Stack);
|
||||
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||
Settings, nullptr);
|
||||
OutMetrics = VF_SummarizeStrateMetrics(Metrics);
|
||||
return Metrics.bValid;
|
||||
}
|
||||
|
||||
bool FPromotionVerifier::MeasureCorpusEntry(
|
||||
const FVoxelStrateCorpusEntry& Entry,
|
||||
FVoxelStrateMeasuredMetrics& OutMetrics) const
|
||||
{
|
||||
return VF_MeasureNativeCorpusEntry(
|
||||
World, Entry, TopVoxelZ, BottomVoxelZ, MeasureSettings, OutMetrics);
|
||||
}
|
||||
|
||||
FVoxelStratePromotableRecord VF_EvaluateCandidate(
|
||||
const VoxelForgeTest::FTestWorld& World,
|
||||
const FVoxelStrateComposerCandidate& Candidate,
|
||||
int32 Season,
|
||||
uint32 CorpusHash,
|
||||
int32 TopVoxelZ,
|
||||
int32 BottomVoxelZ,
|
||||
const FVector& ArrivalPoint,
|
||||
const FVector& DeparturePoint,
|
||||
const FVoxelStrateMeasureSettings& Settings,
|
||||
float LargestThreshold,
|
||||
bool& bOutSurvivor)
|
||||
{
|
||||
bOutSurvivor = false;
|
||||
FVoxelStratePromotableRecord Record = VF_MakeStratePromotableRecord(
|
||||
Candidate, FVoxelStrateMetrics(), Season, CorpusHash, false, false, false);
|
||||
if (!Candidate.bValid)
|
||||
{
|
||||
return Record;
|
||||
}
|
||||
|
||||
FVoxelStrateArchetypeParams Params = Candidate.ArchetypeParams;
|
||||
VF_SetAllRuntimeBounds(Params, static_cast<float>(TopVoxelZ) + 1.0f,
|
||||
static_cast<float>(BottomVoxelZ));
|
||||
Record.Params = Params;
|
||||
|
||||
FVoxelOpStack Stack;
|
||||
FVoxelOpContext Context;
|
||||
FString BuildError;
|
||||
if (!VF_BuildStackFromRecipe(
|
||||
Candidate.Recipe, Params, Candidate.Seed, 14.0f,
|
||||
World.StrateManager.Get(), Stack, Context, &BuildError)
|
||||
|| Context.WorldRadiusVoxels != 0.0f)
|
||||
{
|
||||
return Record;
|
||||
}
|
||||
Stack.PrepareChunk(Context);
|
||||
FStackDensitySampler Sampler(Stack);
|
||||
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||
Settings, nullptr);
|
||||
const bool bNonVacuous = Metrics.bValid
|
||||
&& Metrics.NumSampled > 0 && Metrics.NumAir > 0 && Metrics.NumSolid > 0;
|
||||
const bool bLargestEnough = bNonVacuous
|
||||
&& Metrics.LargestComponentShare >= LargestThreshold;
|
||||
const FVoxelConnectivityDiagnostics Law = VF_DiagnoseConnectivityWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1, Context.EdgeSealThickness,
|
||||
ArrivalPoint, DeparturePoint, Settings);
|
||||
const bool bLawPass = Law.bValid
|
||||
&& Law.Result == EVoxelConnectivityResult::Connected
|
||||
&& !Law.bStartSnapped && !Law.bGoalSnapped;
|
||||
|
||||
Record = VF_MakeStratePromotableRecord(
|
||||
Candidate, Metrics, Season, CorpusHash,
|
||||
bNonVacuous, bLargestEnough, bLawPass);
|
||||
Record.Params = Params;
|
||||
bOutSurvivor = bNonVacuous && bLargestEnough && bLawPass;
|
||||
return Record;
|
||||
}
|
||||
|
||||
double VF_MeanMeasuredSpread(const FVoxelStrateCorpus& Corpus,
|
||||
const TArray<FVoxelStratePromotableRecord>& NewRecords,
|
||||
const FVoxelStratePromotionPolicy& Policy)
|
||||
{
|
||||
TArray<FVoxelStrateMeasuredMetrics> Metrics;
|
||||
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
|
||||
{
|
||||
if (Entry.bHasMeasuredMetrics && Entry.MeasuredMetrics.IsUsable())
|
||||
{
|
||||
Metrics.Add(Entry.MeasuredMetrics);
|
||||
}
|
||||
}
|
||||
for (const FVoxelStratePromotableRecord& Record : NewRecords)
|
||||
{
|
||||
if (Record.MeasuredMetrics.IsUsable())
|
||||
{
|
||||
Metrics.Add(Record.MeasuredMetrics);
|
||||
}
|
||||
}
|
||||
if (Metrics.Num() < 2)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double Sum = 0.0;
|
||||
int64 PairCount = 0;
|
||||
for (int32 A = 0; A < Metrics.Num(); ++A)
|
||||
{
|
||||
for (int32 B = A + 1; B < Metrics.Num(); ++B)
|
||||
{
|
||||
const double Distance = VF_NormalizedMeasuredMetricDistance(
|
||||
Metrics[A], Metrics[B], Policy);
|
||||
if (FMath::IsFinite(Distance) && Distance < TNumericLimits<double>::Max())
|
||||
{
|
||||
Sum += Distance;
|
||||
++PairCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return PairCount > 0 ? Sum / static_cast<double>(PairCount) : 0.0;
|
||||
}
|
||||
|
||||
bool VF_PromotionBatchesIdentical(
|
||||
const FVoxelStratePromotionBatchResult& A,
|
||||
const FVoxelStratePromotionBatchResult& B)
|
||||
{
|
||||
if (A.Promoted.Num() != B.Promoted.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int32 Index = 0; Index < A.Promoted.Num(); ++Index)
|
||||
{
|
||||
const FVoxelStratePromotableRecord& Left = A.Promoted[Index];
|
||||
const FVoxelStratePromotableRecord& Right = B.Promoted[Index];
|
||||
if (Left.RecordId != Right.RecordId
|
||||
|| Left.Archetype != Right.Archetype
|
||||
|| !VF_AreStrateStructureRecipesIdentical(Left.Recipe, Right.Recipe)
|
||||
|| !VF_AreStrateArchetypeParamsBitIdentical(
|
||||
Left.Params, Right.Params, Left.Archetype)
|
||||
|| Left.MeasuredMetrics.AirFraction != Right.MeasuredMetrics.AirFraction
|
||||
|| Left.MeasuredMetrics.LargestComponentShare
|
||||
!= Right.MeasuredMetrics.LargestComponentShare)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeComposerPromotionTest,
|
||||
"VoxelForge.Composer.Promotion",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeComposerPromotionTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace VoxelForgeTest;
|
||||
(void)Parameters;
|
||||
const double TestStartSeconds = FPlatformTime::Seconds();
|
||||
|
||||
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
|
||||
nullptr, TEXT("/Game/VoxelForge/DA_Settings.DA_Settings"));
|
||||
if (AuthoredSettings == nullptr)
|
||||
{
|
||||
AddError(TEXT("Could not load authored settings; promotion simulation did not run."));
|
||||
return false;
|
||||
}
|
||||
|
||||
FTestWorld World;
|
||||
World.Build(1337, 2, true);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
TestTrue(TEXT("promotion fixture keeps WorldRadiusVoxels at 0"),
|
||||
World.Settings->WorldRadiusVoxels == 0.0f);
|
||||
|
||||
constexpr int32 CandidateStrateIndex = FTestWorld::SlotFlatPlain;
|
||||
int32 TopVoxelZ = 0;
|
||||
int32 BottomVoxelZ = 0;
|
||||
if (!World.GetSlotVoxelZRange(CandidateStrateIndex, TopVoxelZ, BottomVoxelZ))
|
||||
{
|
||||
AddError(TEXT("promotion fixture has no candidate Z range"));
|
||||
return false;
|
||||
}
|
||||
|
||||
FVector ArrivalPoint = FVector::ZeroVector;
|
||||
FVector DeparturePoint = FVector::ZeroVector;
|
||||
int32 ArrivalCount = 0;
|
||||
int32 DepartureCount = 0;
|
||||
for (const FVoxelPassage& Passage : World.StrateManager->GetPassages())
|
||||
{
|
||||
if (Passage.LowerStrateIndex == CandidateStrateIndex
|
||||
&& Passage.UpperStrateIndex + 1 == CandidateStrateIndex)
|
||||
{
|
||||
ArrivalPoint = Passage.LowerPoint;
|
||||
++ArrivalCount;
|
||||
}
|
||||
if (Passage.UpperStrateIndex == CandidateStrateIndex
|
||||
&& Passage.LowerStrateIndex == CandidateStrateIndex + 1)
|
||||
{
|
||||
DeparturePoint = Passage.UpperPoint;
|
||||
++DepartureCount;
|
||||
}
|
||||
}
|
||||
TestEqual(TEXT("promotion fixture has exactly one arrival mouth"), ArrivalCount, 1);
|
||||
TestEqual(TEXT("promotion fixture has exactly one departure mouth"), DepartureCount, 1);
|
||||
if (ArrivalCount != 1 || DepartureCount != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FVoxelStrateMeasureSettings MeasureSettings;
|
||||
MeasureSettings.SampleStep = SimulationSampleStep;
|
||||
MeasureSettings.RadiusInVoxels = SimulationRadius;
|
||||
MeasureSettings.CenterXY = FVector2D::ZeroVector;
|
||||
MeasureSettings.MaxCells = 8000000;
|
||||
MeasureSettings.MaxRouteRetries = 16;
|
||||
MeasureSettings.HeadroomCells = 2;
|
||||
// Use one fixed window for both authored/default members and candidates. Deriving a margin
|
||||
// from each member's boundary seal can make a small authored strate have no measurable
|
||||
// interior, which would make the duplicate metric space member-dependent.
|
||||
MeasureSettings.InteriorMarginVoxels = 0;
|
||||
|
||||
FPromotionVerifier Verifier(
|
||||
World, TopVoxelZ, BottomVoxelZ, ArrivalPoint, DeparturePoint,
|
||||
MeasureSettings, LargestComponentSurvivalThreshold);
|
||||
|
||||
// The fixed path is keyed by the authored corpus hash and simulation constants. Starting with
|
||||
// an empty JSON object makes reruns independent of an older test artifact without deleting it.
|
||||
const FString SimulationDirectory = FPaths::ProjectSavedDir() / TEXT("ComposerPromotion")
|
||||
/ FString::Printf(TEXT("seed_%d_step_%d_radius_%d"),
|
||||
AuthoredSettings->Seed, SimulationSampleStep, SimulationRadius);
|
||||
const FString PromotedStorePath = SimulationDirectory / TEXT("promoted_strates.json");
|
||||
const FString CorruptStorePath = SimulationDirectory / TEXT("corrupt_promoted_strates.json");
|
||||
FString StoreReport;
|
||||
TestTrue(TEXT("promotion simulation can initialise its empty JSON store"),
|
||||
VF_SaveStratePromotedRecords(
|
||||
PromotedStorePath, TArray<FVoxelStratePromotableRecord>(), StoreReport));
|
||||
AddInfo(StoreReport);
|
||||
|
||||
FVoxelStrateCorpus BaseCorpus;
|
||||
FString BaseReport;
|
||||
TestTrue(TEXT("promotion simulation loads its base project/default corpus"),
|
||||
BaseCorpus.LoadFromAssetRegistry(BaseReport, PromotedStorePath, &Verifier));
|
||||
AddInfo(BaseReport);
|
||||
if (!BaseCorpus.IsValid())
|
||||
{
|
||||
AddError(TEXT("base corpus was invalid; promotion simulation did not run."));
|
||||
return false;
|
||||
}
|
||||
|
||||
TMap<FString, FVoxelStrateMeasuredMetrics> BaseMetrics;
|
||||
int32 NumVacuousBaseMetrics = 0;
|
||||
for (const FVoxelStrateCorpusEntry& Entry : BaseCorpus.GetEntries())
|
||||
{
|
||||
if (Entry.Provenance == EVoxelStrateCorpusProvenance::Promoted)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
TestTrue(FString::Printf(TEXT("base member %s is measured before policy selection"),
|
||||
*Entry.SourcePath),
|
||||
Entry.bHasMeasuredMetrics && Entry.MeasuredMetrics.bValid);
|
||||
if (Entry.bHasMeasuredMetrics && !Entry.MeasuredMetrics.IsUsable())
|
||||
{
|
||||
++NumVacuousBaseMetrics;
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("base member %s measured but vacuous in the fixed promotion window: "
|
||||
"air=%lld solid=%lld components=%d"),
|
||||
*Entry.SourcePath, Entry.MeasuredMetrics.NumAir,
|
||||
Entry.MeasuredMetrics.NumSolid, Entry.MeasuredMetrics.NumAirComponents));
|
||||
}
|
||||
if (Entry.bHasMeasuredMetrics && Entry.MeasuredMetrics.IsUsable())
|
||||
{
|
||||
BaseMetrics.Add(Entry.SourcePath, Entry.MeasuredMetrics);
|
||||
}
|
||||
}
|
||||
TestTrue(TEXT("base project/default members have measured metrics for spread"),
|
||||
BaseMetrics.Num() > 0);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Base metric audit for duplicate policy: %d usable members, %d measured-but-vacuous "
|
||||
"members; vacuous members use exact parameter identity as the conservative fallback."),
|
||||
BaseMetrics.Num(), NumVacuousBaseMetrics));
|
||||
|
||||
FVoxelStratePromotionPolicy Policy;
|
||||
Policy.MinimumNormalizedMeasuredMetricDistance = 0.20;
|
||||
Policy.MaxPromotionsPerSeason = PromotionCapPerSeason;
|
||||
Policy.FeatureScaleNormalizationVoxels = 256.0f;
|
||||
Policy.ClearanceNormalizationVoxels = 64.0f;
|
||||
|
||||
TArray<FVoxelStratePromotableRecord> AllPromoted;
|
||||
FString SeasonTable = TEXT(
|
||||
"season | input corpus | corpus after | project/default/promoted | survivors/candidates "
|
||||
"| survival | measured spread | promoted | near-dup/hash/cap\n");
|
||||
double FirstSpread = -1.0;
|
||||
double LastSpread = -1.0;
|
||||
int32 NumSeasonLoadsWithPromotions = 0;
|
||||
|
||||
for (int32 Season = 1; Season <= NumSimulatedSeasons; ++Season)
|
||||
{
|
||||
FVoxelStrateCorpus Corpus;
|
||||
FString CorpusReport;
|
||||
const bool bLoaded = Corpus.LoadFromAssetRegistry(
|
||||
CorpusReport, PromotedStorePath, &Verifier);
|
||||
AddInfo(FString::Printf(TEXT("Season %d corpus load: %s"), Season, *CorpusReport));
|
||||
TestTrue(FString::Printf(TEXT("season %d reloads the cumulative corpus safely"), Season),
|
||||
bLoaded && Corpus.IsValid());
|
||||
if (!bLoaded || !Corpus.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const TPair<FString, FVoxelStrateMeasuredMetrics>& Pair : BaseMetrics)
|
||||
{
|
||||
TestTrue(FString::Printf(TEXT("season %d restores base metric %s"),
|
||||
Season, *Pair.Key),
|
||||
Corpus.SetMeasuredMetrics(Pair.Key, Pair.Value));
|
||||
}
|
||||
|
||||
if (AllPromoted.Num() > 0)
|
||||
{
|
||||
++NumSeasonLoadsWithPromotions;
|
||||
TestTrue(FString::Printf(TEXT("season %d re-verifies every stored promotion"), Season),
|
||||
Verifier.NumVerifications >= AllPromoted.Num());
|
||||
}
|
||||
|
||||
const uint32 InputCorpusHash = Corpus.GetContentsHash();
|
||||
const uint32 SeasonSeedBits = static_cast<uint32>(AuthoredSettings->Seed)
|
||||
^ (static_cast<uint32>(Season) * 0x9E3779B9u);
|
||||
const int32 SeasonSeed = static_cast<int32>(SeasonSeedBits);
|
||||
TArray<FVoxelStratePromotableRecord> CandidateRecords;
|
||||
CandidateRecords.Reserve(CandidatesPerSeason);
|
||||
int32 NumSurvivors = 0;
|
||||
for (int32 CandidateIndex = 0;
|
||||
CandidateIndex < CandidatesPerSeason;
|
||||
++CandidateIndex)
|
||||
{
|
||||
const FVoxelStrateComposerCandidate Candidate = VF_RollStrateCandidate(
|
||||
Corpus, SeasonSeed, CandidateIndex, true);
|
||||
bool bSurvivor = false;
|
||||
CandidateRecords.Add(VF_EvaluateCandidate(
|
||||
World, Candidate, Season, InputCorpusHash, TopVoxelZ, BottomVoxelZ,
|
||||
ArrivalPoint, DeparturePoint, MeasureSettings,
|
||||
LargestComponentSurvivalThreshold, bSurvivor));
|
||||
if (bSurvivor)
|
||||
{
|
||||
++NumSurvivors;
|
||||
}
|
||||
}
|
||||
|
||||
const FVoxelStratePromotionBatchResult Batch = VF_SelectStratePromotions(
|
||||
Corpus, CandidateRecords, Policy);
|
||||
const FVoxelStratePromotionBatchResult RepeatBatch = VF_SelectStratePromotions(
|
||||
Corpus, CandidateRecords, Policy);
|
||||
TestTrue(FString::Printf(TEXT("season %d promotion selection is deterministic"), Season),
|
||||
VF_PromotionBatchesIdentical(Batch, RepeatBatch));
|
||||
TestTrue(FString::Printf(TEXT("season %d uses a valid explicit promotion policy"), Season),
|
||||
Batch.bPolicyValid);
|
||||
TestTrue(FString::Printf(TEXT("season %d respects its promotion cap"), Season),
|
||||
Batch.Promoted.Num() <= PromotionCapPerSeason);
|
||||
|
||||
const double Spread = VF_MeanMeasuredSpread(Corpus, Batch.Promoted, Policy);
|
||||
if (FirstSpread < 0.0) { FirstSpread = Spread; }
|
||||
LastSpread = Spread;
|
||||
|
||||
const int32 ProjectCount = Corpus.NumForProvenance(EVoxelStrateCorpusProvenance::Project);
|
||||
const int32 DefaultCount = Corpus.NumForProvenance(EVoxelStrateCorpusProvenance::Default);
|
||||
const int32 ExistingPromotedCount = Corpus.NumForProvenance(
|
||||
EVoxelStrateCorpusProvenance::Promoted);
|
||||
const int32 CorpusAfter = Corpus.Num() + Batch.Promoted.Num();
|
||||
const int32 PromotedAfter = ExistingPromotedCount + Batch.Promoted.Num();
|
||||
const float SurvivalRate = CandidateRecords.Num() > 0
|
||||
? static_cast<float>(NumSurvivors) / static_cast<float>(CandidateRecords.Num())
|
||||
: 0.0f;
|
||||
SeasonTable += FString::Printf(
|
||||
TEXT("%d | %08x | %d | %d/%d/%d | %d/%d | %.1f%% | %.6f | %d | %d/%d/%d\n"),
|
||||
Season, InputCorpusHash, CorpusAfter, ProjectCount, DefaultCount, PromotedAfter,
|
||||
NumSurvivors, CandidateRecords.Num(), 100.0f * SurvivalRate, Spread,
|
||||
Batch.Promoted.Num(), Batch.NumNearDuplicateRejected,
|
||||
Batch.NumCorpusHashRejected, Batch.NumCapRejected);
|
||||
|
||||
FVoxelStrateSeasonManifest Manifest;
|
||||
Manifest.Season = Season;
|
||||
Manifest.Seed = SeasonSeed;
|
||||
Manifest.InputCorpusHash = InputCorpusHash;
|
||||
Manifest.CandidateCount = CandidateRecords.Num();
|
||||
Manifest.SurvivorCount = NumSurvivors;
|
||||
Manifest.PromotedCount = Batch.Promoted.Num();
|
||||
Manifest.CorpusSize = CorpusAfter;
|
||||
Manifest.ProjectCount = ProjectCount;
|
||||
Manifest.DefaultCount = DefaultCount;
|
||||
Manifest.PromotedCorpusCount = PromotedAfter;
|
||||
Manifest.SurvivalRate = SurvivalRate;
|
||||
Manifest.MeasuredSpread = Spread;
|
||||
FString ManifestReport;
|
||||
const FString ManifestPath = SimulationDirectory
|
||||
/ FString::Printf(TEXT("season_%02d_manifest.json"), Season);
|
||||
TestTrue(FString::Printf(TEXT("season %d manifest is written beside the store"), Season),
|
||||
VF_SaveStrateSeasonManifest(ManifestPath, Manifest, ManifestReport));
|
||||
AddInfo(ManifestReport);
|
||||
|
||||
AllPromoted.Append(Batch.Promoted);
|
||||
TestTrue(FString::Printf(TEXT("season %d cumulative promoted store is written"), Season),
|
||||
VF_SaveStratePromotedRecords(PromotedStorePath, AllPromoted, StoreReport));
|
||||
}
|
||||
|
||||
AddInfo(SeasonTable);
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Promotion policy: normalized measured-metric distance < %.2f rejects a near duplicate; "
|
||||
"dimensions=(air, largest share, walkable, feature/%.0f voxels, clearance/%.0f voxels); "
|
||||
"cap=%d per season; records=%d; reloads with promotions=%d."),
|
||||
Policy.MinimumNormalizedMeasuredMetricDistance,
|
||||
Policy.FeatureScaleNormalizationVoxels, Policy.ClearanceNormalizationVoxels,
|
||||
Policy.MaxPromotionsPerSeason, AllPromoted.Num(), NumSeasonLoadsWithPromotions));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Measured spread is mean pairwise distance in that normalized five-dimensional space: "
|
||||
"season 1=%.6f, season 5=%.6f, delta=%+.6f."),
|
||||
FirstSpread, LastSpread, LastSpread - FirstSpread));
|
||||
|
||||
TestEqual(TEXT("five simulated seasons completed"), NumSeasonLoadsWithPromotions + 1,
|
||||
NumSimulatedSeasons);
|
||||
TestTrue(TEXT("promotion compounding produced at least one promoted member"),
|
||||
AllPromoted.Num() > 0);
|
||||
TestEqual(TEXT("promotion cap was never exceeded in cumulative output"),
|
||||
FMath::Min(AllPromoted.Num(), NumSimulatedSeasons * PromotionCapPerSeason),
|
||||
AllPromoted.Num());
|
||||
TestTrue(TEXT("promotion corpus contains project members"),
|
||||
BaseCorpus.NumForProvenance(EVoxelStrateCorpusProvenance::Project) > 0);
|
||||
TestTrue(TEXT("promotion corpus contains default members"),
|
||||
BaseCorpus.NumForProvenance(EVoxelStrateCorpusProvenance::Default) > 0);
|
||||
|
||||
// Deliberately corrupt only the stored metric. The loader must still admit the record only
|
||||
// after rebuilding it, and the corpus entry must expose the fresh value rather than the JSON.
|
||||
if (AllPromoted.Num() > 0)
|
||||
{
|
||||
FVoxelStratePromotableRecord CorruptRecord = AllPromoted[0];
|
||||
const float OriginalAir = CorruptRecord.MeasuredMetrics.AirFraction;
|
||||
CorruptRecord.MeasuredMetrics.AirFraction = OriginalAir > 0.5f ? 0.01f : 0.99f;
|
||||
TArray<FVoxelStratePromotableRecord> CorruptRecords;
|
||||
CorruptRecords.Add(CorruptRecord);
|
||||
FString CorruptReport;
|
||||
TestTrue(TEXT("corrupt-metric promotion store can be written for the safety test"),
|
||||
VF_SaveStratePromotedRecords(CorruptStorePath, CorruptRecords, CorruptReport));
|
||||
|
||||
FVoxelStrateCorpus CorruptCorpus;
|
||||
FString CorruptLoadReport;
|
||||
TestTrue(TEXT("corrupt stored metric is reverified before corpus admission"),
|
||||
CorruptCorpus.LoadFromAssetRegistry(
|
||||
CorruptLoadReport, CorruptStorePath, &Verifier));
|
||||
AddInfo(CorruptLoadReport);
|
||||
TestTrue(TEXT("corrupt stored metric was detected and replaced"),
|
||||
CorruptLoadReport.Contains(TEXT("stored metrics replaced for 1 records")));
|
||||
|
||||
FVoxelStratePromotionVerification ExpectedVerification;
|
||||
TestTrue(TEXT("safety verifier can independently rebuild the corrupted record"),
|
||||
Verifier.Verify(CorruptRecord, ExpectedVerification));
|
||||
bool bFoundReverifiedEntry = false;
|
||||
for (const FVoxelStrateCorpusEntry& Entry : CorruptCorpus.GetEntries())
|
||||
{
|
||||
if (Entry.Provenance == EVoxelStrateCorpusProvenance::Promoted
|
||||
&& Entry.PromotionRecordId == CorruptRecord.RecordId)
|
||||
{
|
||||
bFoundReverifiedEntry = true;
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("stale metric audit %s: stored/corpus air=%.9g, fresh air=%.9g, "
|
||||
"corrupted JSON air=%.9g"),
|
||||
*CorruptRecord.RecordId,
|
||||
Entry.MeasuredMetrics.AirFraction,
|
||||
ExpectedVerification.MeasuredMetrics.AirFraction,
|
||||
CorruptRecord.MeasuredMetrics.AirFraction));
|
||||
TestTrue(TEXT("loaded promoted metric equals fresh measurement, not stale JSON"),
|
||||
FMath::IsNearlyEqual(
|
||||
Entry.MeasuredMetrics.AirFraction,
|
||||
ExpectedVerification.MeasuredMetrics.AirFraction));
|
||||
}
|
||||
}
|
||||
TestTrue(TEXT("corrupt safety test found its promoted corpus entry"),
|
||||
bFoundReverifiedEntry);
|
||||
}
|
||||
|
||||
const double TestSeconds = FPlatformTime::Seconds() - TestStartSeconds;
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Promotion total runtime %.3fs; WorldRadiusVoxels=0; JSON store=%s."),
|
||||
TestSeconds, *PromotedStorePath));
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR
|
||||
@@ -344,6 +344,10 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
int32 NumFineRendered = 0;
|
||||
int32 NumFineContourRendered = 0;
|
||||
int32 NumFineRefused = 0;
|
||||
int32 NumFinePlanBlankBeforeCentering = 0;
|
||||
int32 NumFinePlanBlankAfterCentering = 0;
|
||||
int32 NumFineCardBlankBeforeCentering = 0;
|
||||
int32 NumFineCardBlankAfterCentering = 0;
|
||||
int64 TotalFineCells = 0;
|
||||
int64 PeakFineCaptureBytes = 0;
|
||||
int64 PeakFineRasterBytes = 0;
|
||||
@@ -625,11 +629,50 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
if (bCandidateSurvivor)
|
||||
{
|
||||
++NumFineRequested;
|
||||
|
||||
// Render the old origin-centred pass into the same filenames first. The temporary
|
||||
// record gives an exact before count from the renderer's content-variation check; the
|
||||
// corrected capture below overwrites those files. The two grids are sequential, so
|
||||
// peak retained memory remains one fine capture.
|
||||
FVoxelStrateSampleGrid OriginFineGrid;
|
||||
const double OriginFineMeasureStartSeconds = FPlatformTime::Seconds();
|
||||
const FVoxelStrateMetrics OriginFineMetrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||
Context.EdgeSealThickness, FineMeasureSettings, &OriginFineGrid);
|
||||
FineMeasureSeconds += FPlatformTime::Seconds() - OriginFineMeasureStartSeconds;
|
||||
FVoxelStratePreviewCandidate OriginFinePreview;
|
||||
FString OriginFinePreviewError;
|
||||
const double OriginFineRenderStartSeconds = FPlatformTime::Seconds();
|
||||
const bool bOriginFineWritten = VF_WriteStratePreviewFineCandidate(
|
||||
PreviewDirectory, CandidateIndex, OriginFineGrid,
|
||||
FineMeasureSettings.HeadroomCells,
|
||||
OriginFineMetrics.bValid ? FString() : OriginFineMetrics.RefusalReason,
|
||||
OriginFinePreview, OriginFinePreviewError);
|
||||
FineRenderSeconds += FPlatformTime::Seconds() - OriginFineRenderStartSeconds;
|
||||
TestTrue(FString::Printf(TEXT("candidate %d origin baseline fine render records"),
|
||||
CandidateIndex),
|
||||
bOriginFineWritten);
|
||||
if (OriginFinePreview.bFineRendered && !OriginFinePreview.bFinePlanContentVaries)
|
||||
{
|
||||
++NumFinePlanBlankBeforeCentering;
|
||||
}
|
||||
if (OriginFinePreview.bFineRendered && OriginFinePreview.bFineBlank)
|
||||
{
|
||||
++NumFineCardBlankBeforeCentering;
|
||||
}
|
||||
|
||||
FVoxelStrateMeasureSettings CenteredFineMeasureSettings = FineMeasureSettings;
|
||||
if (Metrics.bValid && FMath::IsFinite(Metrics.LargestComponentPoint.X)
|
||||
&& FMath::IsFinite(Metrics.LargestComponentPoint.Y))
|
||||
{
|
||||
CenteredFineMeasureSettings.CenterXY = FVector2D(
|
||||
Metrics.LargestComponentPoint.X, Metrics.LargestComponentPoint.Y);
|
||||
}
|
||||
FVoxelStrateSampleGrid FineGrid;
|
||||
const double FineMeasureStartSeconds = FPlatformTime::Seconds();
|
||||
const FVoxelStrateMetrics FineMetrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||
Context.EdgeSealThickness, FineMeasureSettings, &FineGrid);
|
||||
Context.EdgeSealThickness, CenteredFineMeasureSettings, &FineGrid);
|
||||
FineMeasureSeconds += FPlatformTime::Seconds() - FineMeasureStartSeconds;
|
||||
|
||||
const FString FineFailureReason = FineMetrics.bValid
|
||||
@@ -645,6 +688,14 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
TestTrue(FString::Printf(TEXT("candidate %d fine preview records without a second renderer sample"),
|
||||
CandidateIndex),
|
||||
bFineWritten);
|
||||
if (PreviewCandidate.bFineRendered && !PreviewCandidate.bFinePlanContentVaries)
|
||||
{
|
||||
++NumFinePlanBlankAfterCentering;
|
||||
}
|
||||
if (PreviewCandidate.bFineRendered && PreviewCandidate.bFineBlank)
|
||||
{
|
||||
++NumFineCardBlankAfterCentering;
|
||||
}
|
||||
TestTrue(FString::Printf(TEXT("candidate %d fine capture keeps scalar density"),
|
||||
CandidateIndex),
|
||||
!FineMetrics.bValid || FineGrid.HasScalarDensity());
|
||||
@@ -768,11 +819,15 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
FirstBoxViolation.IsEmpty() ? TEXT("No violations.") : *FirstBoxViolation));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Preview cost: coarse step=%d 64-card pass render %.3fs; fine ROI requested=%d "
|
||||
"(survivors=%d), rendered=%d, contour pairs=%d, refused=%d; fine sample %.3fs + "
|
||||
"(survivors=%d), rendered=%d, contour pairs=%d, refused=%d; "
|
||||
"blank plan origin=%d, blank plan largest-open-space=%d; blank card origin=%d, "
|
||||
"blank card largest-open-space=%d; fine sample %.3fs + "
|
||||
"render %.3fs; fine cells sampled=%lld; peak fine Air+Density capture=%lld bytes; "
|
||||
"peak single RGBA raster=%lld bytes; ROI step=%d radius=%d MaxCells=%d."),
|
||||
MeasureSettings.SampleStep, CoarsePreviewSeconds,
|
||||
NumFineRequested, NumSurvivors, NumFineRendered, NumFineContourRendered, NumFineRefused,
|
||||
NumFinePlanBlankBeforeCentering, NumFinePlanBlankAfterCentering,
|
||||
NumFineCardBlankBeforeCentering, NumFineCardBlankAfterCentering,
|
||||
FineMeasureSeconds, FineRenderSeconds, TotalFineCells,
|
||||
PeakFineCaptureBytes, PeakFineRasterBytes,
|
||||
FinePreviewSettings.SampleStep, FinePreviewSettings.RadiusInVoxels,
|
||||
@@ -815,7 +870,8 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
"vertical=%dx%d, plan=%dx%d, "
|
||||
"plan Z chosen from each candidate's measured layer with the highest solid/air "
|
||||
"boundary count (first candidate Z=%d); index=%s; first-candidate window=%s; "
|
||||
"fine previews are linked on survivor cards; image cap=512 px including 20 px "
|
||||
"fine previews are linked on survivor cards and centred on each largest open "
|
||||
"space point (never the strate midpoint); image cap=512 px including 20 px "
|
||||
"scale footer."),
|
||||
PreviewCandidates.Num(),
|
||||
NumRenderedPairs,
|
||||
@@ -840,6 +896,15 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
|
||||
NumFineRequested, NumSurvivors);
|
||||
TestEqual(TEXT("fine preview outcomes account for every requested survivor"),
|
||||
NumFineRendered + NumFineRefused, NumFineRequested);
|
||||
AddInfo(FString::Printf(TEXT(
|
||||
"Fine blank baseline: plan projection %d/%d origin-centred versus %d/%d centred on "
|
||||
"LargestComponentPoint; complete two-projection cards %d/%d versus %d/%d. A blank "
|
||||
"projection/card means its filled-cell pixels were uniform (scale footer excluded), "
|
||||
"not that the measurement was refused."),
|
||||
NumFinePlanBlankBeforeCentering, NumFineRequested,
|
||||
NumFinePlanBlankAfterCentering, NumFineRequested,
|
||||
NumFineCardBlankBeforeCentering, NumFineRequested,
|
||||
NumFineCardBlankAfterCentering, NumFineRequested));
|
||||
TestEqual(TEXT("every rendered fine preview has a scalar contour pair"),
|
||||
NumFineContourRendered, NumFineRendered);
|
||||
TestTrue(TEXT("structure roller emits 64 distinct structural recipes"),
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -177,6 +177,28 @@ namespace
|
||||
return bAnyWalkable ? GWalkableColor : bAnyAir ? GAirColor : GSolidColor;
|
||||
}
|
||||
|
||||
bool VF_HasContentVariation(const TArray<FColor>& Pixels,
|
||||
int32 Width,
|
||||
int32 ContentHeight)
|
||||
{
|
||||
if (Width <= 0 || ContentHeight <= 0 || Pixels.Num() < Width * ContentHeight)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const FColor First = Pixels[0];
|
||||
for (int32 Y = 0; Y < ContentHeight; ++Y)
|
||||
{
|
||||
for (int32 X = 0; X < Width; ++X)
|
||||
{
|
||||
if (Pixels[Y * Width + X] != First)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FPlanSliceChoice VF_ChoosePlanSlice(const FVoxelStrateSampleGrid& Grid,
|
||||
int32 HeadroomCells)
|
||||
{
|
||||
@@ -864,6 +886,8 @@ bool VF_WriteStratePreviewFineCandidate(
|
||||
CandidateIndex);
|
||||
return false;
|
||||
}
|
||||
InOutCandidate.bFineVerticalContentVaries = VF_HasContentVariation(
|
||||
Pixels, Width, Height - GPreviewScaleBarFooterPixels);
|
||||
|
||||
const bool bHasContour = FineGrid.HasScalarDensity();
|
||||
if (bHasContour)
|
||||
@@ -896,6 +920,8 @@ bool VF_WriteStratePreviewFineCandidate(
|
||||
CandidateIndex);
|
||||
return false;
|
||||
}
|
||||
InOutCandidate.bFinePlanContentVaries = VF_HasContentVariation(
|
||||
Pixels, Width, Height - GPreviewScaleBarFooterPixels);
|
||||
if (bHasContour)
|
||||
{
|
||||
int32 ContourWidth = 0;
|
||||
@@ -914,6 +940,8 @@ bool VF_WriteStratePreviewFineCandidate(
|
||||
|
||||
InOutCandidate.bFineRendered = true;
|
||||
InOutCandidate.bFineContourRendered = bHasContour;
|
||||
InOutCandidate.bFineBlank = !InOutCandidate.bFineVerticalContentVaries
|
||||
&& !InOutCandidate.bFinePlanContentVaries;
|
||||
InOutCandidate.FinePlanSliceCellZ = Plan.CellZ;
|
||||
InOutCandidate.FinePlanSliceWorldZ = VF_CellWorldCoordinate(
|
||||
FineGrid.MinZ, FineGrid.MaxZ, FineGrid.SampleStep, Plan.CellZ);
|
||||
@@ -1021,8 +1049,10 @@ 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:</strong> the separate step-1 pass is requested only "
|
||||
"for selected candidates (this run selects coarse survivors). Its exact window is printed "
|
||||
"<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 "
|
||||
"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 "
|
||||
@@ -1115,7 +1145,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 — filled cells and density=0 contour</h3>\n");
|
||||
Html += TEXT("</div>\n<div class=\"fine-section\"><h3>FINE ROI — largest open space; 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");
|
||||
@@ -1132,7 +1162,7 @@ bool VF_WriteStratePreviewIndex(
|
||||
{
|
||||
const FString FineWindowDescription = Candidate.FineWindow.Describe();
|
||||
Html += FString::Printf(
|
||||
TEXT("<p class=\"window\">Exact fine ROI window: %s; slice Y=%d voxels.</p>\n"
|
||||
TEXT("<p class=\"window\">Largest-open-space 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,
|
||||
@@ -1154,7 +1184,7 @@ bool VF_WriteStratePreviewIndex(
|
||||
}
|
||||
|
||||
Html += FString::Printf(
|
||||
TEXT("<p class=\"window\">Exact fine ROI window: %s; selected Z=%d voxels.</p>\n"
|
||||
TEXT("<p class=\"window\">Largest-open-space 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,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "CoreMinimal.h"
|
||||
#include "VoxelStrateTypes.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
|
||||
#include "VoxelStrateComposer.generated.h"
|
||||
|
||||
@@ -142,6 +143,7 @@ struct VOXELFORGE_API FVoxelStrateFieldSpread
|
||||
// Actual authored corpus samples and synthetic per-operation default samples are kept
|
||||
// separate in the report. SampleCount is their effective total used for the statistics.
|
||||
int32 AuthoredSampleCount = 0;
|
||||
int32 PromotedSampleCount = 0;
|
||||
int32 DefaultSeedCount = 0;
|
||||
int32 SampleCount = 0;
|
||||
double Min = 0.0;
|
||||
@@ -157,6 +159,54 @@ struct VOXELFORGE_API FVoxelStrateFieldSpread
|
||||
double ClampMax = 0.0;
|
||||
};
|
||||
|
||||
/** Where a member of the offline corpus came from. */
|
||||
enum class EVoxelStrateCorpusProvenance : uint8
|
||||
{
|
||||
Project,
|
||||
Default,
|
||||
Promoted,
|
||||
};
|
||||
|
||||
/** The finite, diffable metric summary stored with a promoted strate. */
|
||||
struct VOXELFORGE_API FVoxelStrateMeasuredMetrics
|
||||
{
|
||||
bool bValid = false;
|
||||
|
||||
int64 NumSampled = 0;
|
||||
int64 NumAir = 0;
|
||||
int64 NumSolid = 0;
|
||||
float AirFraction = 0.0f;
|
||||
int32 NumAirComponents = 0;
|
||||
float LargestComponentShare = 0.0f;
|
||||
FVector LargestComponentPoint = FVector::ZeroVector;
|
||||
int64 LargestComponentCells = 0;
|
||||
int32 NumComponentsAtLeast1Pct = 0;
|
||||
float WalkableFraction = 0.0f;
|
||||
float MedianFeatureScale = 0.0f;
|
||||
int32 MedianVerticalClearance = 0;
|
||||
|
||||
int32 ResolvedMarginVoxels = 0;
|
||||
int32 SampledMinZ = 0;
|
||||
int32 SampledMaxZ = 0;
|
||||
int32 SampledNumX = 0;
|
||||
int32 SampledNumY = 0;
|
||||
int32 SampledNumZ = 0;
|
||||
float SampledMinX = 0.0f;
|
||||
float SampledMaxX = 0.0f;
|
||||
float SampledMinY = 0.0f;
|
||||
float SampledMaxY = 0.0f;
|
||||
|
||||
// This is the component-size audit trail, not a retained density grid. It is bounded by the
|
||||
// number of discovered air components and is useful when reviewing a promoted record.
|
||||
TArray<int64> AirComponentCells;
|
||||
|
||||
bool IsUsable() const;
|
||||
};
|
||||
|
||||
/** Copy the measured facts that are safe and useful to persist; no sampler or grid is retained. */
|
||||
VOXELFORGE_API FVoxelStrateMeasuredMetrics VF_SummarizeStrateMetrics(
|
||||
const FVoxelStrateMetrics& Metrics);
|
||||
|
||||
/** Native parameter storage for every currently supported strate archetype. */
|
||||
struct VOXELFORGE_API FVoxelStrateArchetypeParams
|
||||
{
|
||||
@@ -187,6 +237,129 @@ struct VOXELFORGE_API FVoxelStrateCorpusEntry
|
||||
// use 1.0. The field remains explicit so a future offline corpus can assign weights without
|
||||
// changing the roll algorithm.
|
||||
float Weight = 1.0f;
|
||||
|
||||
EVoxelStrateCorpusProvenance Provenance = EVoxelStrateCorpusProvenance::Project;
|
||||
|
||||
// Promoted entries retain their recipe and re-measured facts so the next policy decision can
|
||||
// compare measured geometry, not only parameter-space distance. Project/default entries leave
|
||||
// these fields unset unless an offline simulation explicitly measures them.
|
||||
bool bHasRecipe = false;
|
||||
FVoxelOpStackRecipe Recipe;
|
||||
bool bHasMeasuredMetrics = false;
|
||||
FVoxelStrateMeasuredMetrics MeasuredMetrics;
|
||||
|
||||
FString PromotionRecordId;
|
||||
int32 PromotionSeason = INDEX_NONE;
|
||||
int32 PromotionSeed = 0;
|
||||
int32 PromotionCandidateIndex = INDEX_NONE;
|
||||
uint32 PromotionInputCorpusHash = 0;
|
||||
};
|
||||
|
||||
/** A candidate record that is eligible to be considered for corpus promotion. */
|
||||
struct VOXELFORGE_API FVoxelStratePromotableRecord
|
||||
{
|
||||
FString RecordId;
|
||||
int32 Season = 0;
|
||||
int32 Seed = 0;
|
||||
int32 CandidateIndex = INDEX_NONE;
|
||||
uint32 InputCorpusHash = 0;
|
||||
|
||||
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
|
||||
FVoxelStrateArchetypeParams Params;
|
||||
FVoxelOpStackRecipe Recipe;
|
||||
FVoxelStrateMeasuredMetrics MeasuredMetrics;
|
||||
|
||||
// These are recorded evidence for review. The loader deliberately ignores the stored metric
|
||||
// and gate values and asks its verifier to rebuild/re-measure the recipe instead.
|
||||
bool bPassedNonVacuous = false;
|
||||
bool bPassedLargestComponent = false;
|
||||
bool bPassedPrimordialLaw = false;
|
||||
};
|
||||
|
||||
/** Fresh validation output used by the safe promoted-record loader. */
|
||||
struct VOXELFORGE_API FVoxelStratePromotionVerification
|
||||
{
|
||||
bool bPassedNonVacuous = false;
|
||||
bool bPassedLargestComponent = false;
|
||||
bool bPassedPrimordialLaw = false;
|
||||
FVoxelStrateMeasuredMetrics MeasuredMetrics;
|
||||
FString FailureReason;
|
||||
|
||||
bool PassedAllGates() const
|
||||
{
|
||||
return bPassedNonVacuous && bPassedLargestComponent && bPassedPrimordialLaw
|
||||
&& MeasuredMetrics.IsUsable();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* World-specific re-measurement seam for promoted records.
|
||||
*
|
||||
* A corpus store cannot know the active fixture, mouths, or validation window. Its caller must
|
||||
* provide this verifier; a null verifier means promoted records are skipped rather than trusted.
|
||||
*/
|
||||
class VOXELFORGE_API IVoxelStratePromotionVerifier
|
||||
{
|
||||
public:
|
||||
virtual ~IVoxelStratePromotionVerifier() = default;
|
||||
virtual bool Verify(const FVoxelStratePromotableRecord& Record,
|
||||
FVoxelStratePromotionVerification& OutVerification) const = 0;
|
||||
|
||||
// Optional companion audit for project/default members. A policy pass can then compare a
|
||||
// candidate against the measured corpus rather than falling back to parameter identity. A
|
||||
// world-specific verifier may return false when that fixture cannot measure the member.
|
||||
virtual bool MeasureCorpusEntry(const FVoxelStrateCorpusEntry& Entry,
|
||||
FVoxelStrateMeasuredMetrics& OutMetrics) const
|
||||
{
|
||||
(void)Entry;
|
||||
(void)OutMetrics;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/** Explicit, provisional Tier 5 policy. The owner can change these after seeing the preview. */
|
||||
struct VOXELFORGE_API FVoxelStratePromotionPolicy
|
||||
{
|
||||
// Five normalized measured dimensions are compared with Euclidean distance. 0.20 is large
|
||||
// enough to reject close copies while still admitting genuinely different geometry; it is a
|
||||
// provisional review threshold, not a claim that this is the owner's final definition of good.
|
||||
double MinimumNormalizedMeasuredMetricDistance = 0.20;
|
||||
int32 MaxPromotionsPerSeason = 6;
|
||||
|
||||
// Physical normalization for the two unbounded-in-principle metric dimensions. Air fraction,
|
||||
// largest share, and walkable fraction already live in [0,1].
|
||||
float FeatureScaleNormalizationVoxels = 256.0f;
|
||||
float ClearanceNormalizationVoxels = 64.0f;
|
||||
};
|
||||
|
||||
/** Counters and accepted records from one deterministic policy pass. */
|
||||
struct VOXELFORGE_API FVoxelStratePromotionBatchResult
|
||||
{
|
||||
bool bPolicyValid = false;
|
||||
TArray<FVoxelStratePromotableRecord> Promoted;
|
||||
int32 NumCandidates = 0;
|
||||
int32 NumValidationRejected = 0;
|
||||
int32 NumCorpusHashRejected = 0;
|
||||
int32 NumNearDuplicateRejected = 0;
|
||||
int32 NumCapRejected = 0;
|
||||
double MinimumAcceptedDistance = 0.0;
|
||||
};
|
||||
|
||||
/** One line of the diffable season manifest written beside the cumulative record store. */
|
||||
struct VOXELFORGE_API FVoxelStrateSeasonManifest
|
||||
{
|
||||
int32 Season = 0;
|
||||
int32 Seed = 0;
|
||||
uint32 InputCorpusHash = 0;
|
||||
int32 CandidateCount = 0;
|
||||
int32 SurvivorCount = 0;
|
||||
int32 PromotedCount = 0;
|
||||
int32 CorpusSize = 0;
|
||||
int32 ProjectCount = 0;
|
||||
int32 DefaultCount = 0;
|
||||
int32 PromotedCorpusCount = 0;
|
||||
float SurvivalRate = 0.0f;
|
||||
double MeasuredSpread = 0.0;
|
||||
};
|
||||
|
||||
/** Result of one deterministic roll, including provenance for the offline report. */
|
||||
@@ -243,15 +416,20 @@ struct VOXELFORGE_API FVoxelStrateComposerCandidate
|
||||
*
|
||||
* `LoadFromAssetRegistry` enumerates every project UVoxelStrateDefinition through the Asset
|
||||
* Registry, explicitly excluding Saved/Autosaves and Saved/Cooked copies. It also adds one
|
||||
* known-good default vector for every archetype/family in VoxelStrateTypes.h. Parents are selected
|
||||
* only inside the exact archetype group; no cross-archetype blend is attempted.
|
||||
* known-good default vector for every archetype/family in VoxelStrateTypes.h, then optionally
|
||||
* loads the cumulative promoted-record JSON store. Promoted records enter only after the caller's
|
||||
* world-specific verifier rebuilds and remeasures them; with no verifier they are skipped safely.
|
||||
* Parents are selected only inside the exact archetype group; no cross-archetype blend is attempted.
|
||||
*/
|
||||
class VOXELFORGE_API FVoxelStrateCorpus
|
||||
{
|
||||
public:
|
||||
void Reset();
|
||||
|
||||
bool LoadFromAssetRegistry(FString& OutReport);
|
||||
bool LoadFromAssetRegistry(
|
||||
FString& OutReport,
|
||||
const FString& PromotedStorePath = FString(),
|
||||
const IVoxelStratePromotionVerifier* PromotionVerifier = nullptr);
|
||||
|
||||
/**
|
||||
* Compatibility alias retained for callers compiled against the first Tier 4a pass. The
|
||||
@@ -268,6 +446,15 @@ public:
|
||||
ECaveGeneratorType Archetype, const FVoxelStrateArchetypeParams& Params,
|
||||
float Weight = 1.0f);
|
||||
|
||||
/** Load cumulative promoted records, re-verifying every record before it enters Entries. */
|
||||
bool LoadPromotedRecords(const FString& StorePath,
|
||||
const IVoxelStratePromotionVerifier* PromotionVerifier,
|
||||
FString& OutReport);
|
||||
|
||||
/** Attach an offline measurement to an existing member without changing corpus identity. */
|
||||
bool SetMeasuredMetrics(const FString& SourcePath,
|
||||
const FVoxelStrateMeasuredMetrics& Metrics);
|
||||
|
||||
bool IsValid() const { return bSchemaValid && Entries.Num() > 0; }
|
||||
bool IsSchemaValid() const { return bSchemaValid; }
|
||||
const FString& GetSchemaError() const { return SchemaError; }
|
||||
@@ -278,6 +465,7 @@ public:
|
||||
const TArray<FString>& GetSkippedDefinitions() const { return SkippedDefinitions; }
|
||||
|
||||
int32 NumForArchetype(ECaveGeneratorType Archetype) const;
|
||||
int32 NumForProvenance(EVoxelStrateCorpusProvenance Provenance) const;
|
||||
|
||||
const FVoxelStrateFieldSpread* FindSpread(
|
||||
ECaveGeneratorType Archetype, const FString& FieldName) const;
|
||||
@@ -296,6 +484,18 @@ public:
|
||||
bool AreClampMetadataAvailableAtRuntime() const { return false; }
|
||||
|
||||
private:
|
||||
bool AddEntryInternal(const FString& SourcePath, const FString& SourceName,
|
||||
ECaveGeneratorType Archetype,
|
||||
const FVoxelStrateArchetypeParams& Params,
|
||||
float Weight, EVoxelStrateCorpusProvenance Provenance,
|
||||
const FVoxelOpStackRecipe* Recipe,
|
||||
const FVoxelStrateMeasuredMetrics* Metrics,
|
||||
const FString& PromotionRecordId,
|
||||
int32 PromotionSeason,
|
||||
int32 PromotionSeed,
|
||||
int32 PromotionCandidateIndex,
|
||||
uint32 PromotionInputCorpusHash,
|
||||
bool bRebuild);
|
||||
void RebuildSpreads();
|
||||
|
||||
TArray<FVoxelStrateCorpusEntry> Entries;
|
||||
@@ -308,6 +508,8 @@ private:
|
||||
|
||||
/** Human-readable name used by the offline report. */
|
||||
VOXELFORGE_API const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archetype);
|
||||
VOXELFORGE_API const TCHAR* VF_GetStrateCorpusProvenanceName(
|
||||
EVoxelStrateCorpusProvenance Provenance);
|
||||
|
||||
/** Full provenance result. Pure with respect to the corpus: no global or retained RNG state. */
|
||||
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailed(
|
||||
@@ -349,6 +551,30 @@ VOXELFORGE_API bool VF_ValidateStrateCorpusFreeConstraints(
|
||||
float StrateHeightInVoxels,
|
||||
FString& OutViolation);
|
||||
|
||||
/** Normalized Euclidean distance in the five policy metric dimensions. */
|
||||
VOXELFORGE_API double VF_NormalizedMeasuredMetricDistance(
|
||||
const FVoxelStrateMeasuredMetrics& A,
|
||||
const FVoxelStrateMeasuredMetrics& B,
|
||||
const FVoxelStratePromotionPolicy& Policy);
|
||||
|
||||
/** Select deterministic, validated, varied records for one season. */
|
||||
VOXELFORGE_API FVoxelStratePromotionBatchResult VF_SelectStratePromotions(
|
||||
const FVoxelStrateCorpus& Corpus,
|
||||
const TArray<FVoxelStratePromotableRecord>& Candidates,
|
||||
const FVoxelStratePromotionPolicy& Policy);
|
||||
|
||||
/** Write/readable cumulative JSON store; records are sorted by RecordId before writing. */
|
||||
VOXELFORGE_API bool VF_SaveStratePromotedRecords(
|
||||
const FString& StorePath,
|
||||
const TArray<FVoxelStratePromotableRecord>& Records,
|
||||
FString& OutReport);
|
||||
|
||||
/** Write the matching season summary beside the promoted-record store. */
|
||||
VOXELFORGE_API bool VF_SaveStrateSeasonManifest(
|
||||
const FString& ManifestPath,
|
||||
const FVoxelStrateSeasonManifest& Manifest,
|
||||
FString& OutReport);
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Roll one candidate for the editor walk-through path. Parameter rolls call
|
||||
@@ -373,6 +599,16 @@ VOXELFORGE_API bool VF_BuildNativeStrateStackForCandidate(
|
||||
const UVoxelStrateManager* StrateManager,
|
||||
FVoxelOpStack& OutStack,
|
||||
FVoxelOpContext& OutContext);
|
||||
|
||||
/** Build the persisted record from the exact editor candidate and its just-measured gate facts. */
|
||||
VOXELFORGE_API FVoxelStratePromotableRecord VF_MakeStratePromotableRecord(
|
||||
const FVoxelStrateComposerCandidate& Candidate,
|
||||
const FVoxelStrateMetrics& Metrics,
|
||||
int32 Season,
|
||||
uint32 InputCorpusHash,
|
||||
bool bPassedNonVacuous,
|
||||
bool bPassedLargestComponent,
|
||||
bool bPassedPrimordialLaw);
|
||||
#endif
|
||||
|
||||
/** Stable compact representation and equality/hash helpers for manifests and reports. */
|
||||
|
||||
@@ -39,7 +39,8 @@ struct VOXELFORGE_API FVoxelStrateFinePreviewSettings
|
||||
// The default is deliberately one voxel: the pass exists to expose detail that step 4 cannot.
|
||||
int32 SampleStep = 1;
|
||||
// radius=64 gives a 128x128 XY ROI and stays below the default two-million-cell cap for the
|
||||
// four-chunk fixture while retaining the same origin-centred frame as the coarse sweep.
|
||||
// four-chunk fixture. The caller may re-centre it on the deterministic largest open space
|
||||
// point found by the coarse air flood fill; it is not inherently the strate midpoint.
|
||||
int32 RadiusInVoxels = 64;
|
||||
int32 MaxCells = 2000000;
|
||||
FVector2D CenterXY = FVector2D::ZeroVector;
|
||||
@@ -96,6 +97,9 @@ struct VOXELFORGE_API FVoxelStratePreviewCandidate
|
||||
bool bFineRequested = false;
|
||||
bool bFineRendered = false;
|
||||
bool bFineContourRendered = false;
|
||||
bool bFineVerticalContentVaries = false;
|
||||
bool bFinePlanContentVaries = false;
|
||||
bool bFineBlank = false;
|
||||
|
||||
float AirFraction = 0.0f;
|
||||
float LargestComponentShare = 0.0f;
|
||||
|
||||
@@ -79,6 +79,7 @@ public class VoxelForge : ModuleRules
|
||||
// Private dependencies - only used in our .cpp files, not exposed in headers
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Json", // Diffable Tier 5 promoted-record and season-manifest stores
|
||||
"ImageWrapper", // PNG encode for the biome-map preview bake (BakeBiomePreview)
|
||||
"RHI", // Texture3D create + RHIUpdateTexture3D for the density volume (mini-sun shadows)
|
||||
"RenderCore", // ENQUEUE_RENDER_COMMAND for the volume upload
|
||||
|
||||
Reference in new issue
Block a user