feat(composer): Tier 4c -- a season that can actually ship, with a veto page

The composer could invent, measure and show strates. What did not exist was
an ARTEFACT a season could ship. Now it does.

VF_ComposeSeason: generate N candidates -> measure -> reject failures ->
select a descent sequence -> emit a JSON manifest + a review page.

⭐ THE PROPERTY THAT MATTERS: round-trip bit-identity. The manifest was
loaded, every selected stack rebuilt, and 393,216 density samples compared
BIT-IDENTICALLY. Re-composing the same seed yields byte-identical JSON.
A manifest that cannot reproduce its own world is worthless, and that is
the one thing that could not be assumed. Floats serialise as readable
values PLUS exact IEEE bits, so a season survives a text round trip.

Test season: 24 generated, 19 survived the hard gates, 6 selected.
18 not selected -- 4 vacuous, 1 primordial-law failure, 13 policy
non-selections. 4 grounded, 2 outliers. EVERY selected slot passes the
primordial law, asserted per slot rather than sampled.

Selection policy is explicit, named and PROVISIONAL, because Jahni has not
defined "good" yet and said so: "it'll be easier once i'm able to see the
strates candidates." So no quality score was invented and presented as
settled. The knobs are 75/25 grounded-to-outlier (outlier axis = distance
from corpus centroid), adjacent-similarity threshold .20 with penalty 1.0,
new-archetype bonus .10, new-recipe bonus .05, boss cadence every 5th
non-terminal slot. All of them are his to turn.

The veto is a review page in DESCENT ORDER -- the season read top to bottom
the way it is played, not the survivor/outlier ordering the candidate sheet
uses. Reuses VoxelStratePreview rather than a second renderer.

  Saved/VoxelForge/SeasonTest/season_0000_seed_9320/season_manifest.json
  .../review/index.html

⚠️ FIVE GAPS CODEX RAISED, recorded not buried:
1. §4's Tier A is strates PLUS passages; only the strate spine is persisted.
   Manager-generated passage geometry and content placement are not.
2. Authored fixed strates keep scalar vectors + asset paths, not every
   asset-level passage/biome/disturbance/content field.
3. Boss slots are MARKERS, not arena or content guarantees.
4. WorldRadiusVoxels == 0 is enforced as specced but contradicts a
   production bounded world -- Jahni's call, still unset by design.
5. Bit identity is proven for the same build and platform. A frozen season
   spanning engine versions needs an explicit schema contract. Real risk,
   since seasons freeze generation for months.

Build clean, 28 succeeded / 0 failed, verified by me.

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 02:55:50 +02:00
1 parent 0b3ec18b43
commit 42e1f0ec8c
8 files changed
+3351 -5

No files matched your search

@@ -0,0 +1,208 @@
// Tier 4c — offline season composition, manifest determinism, and exact rebuild.
#if WITH_DEV_AUTOMATION_TESTS && WITH_EDITOR
#include "Misc/AutomationTest.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "VoxelDensityOpStack.h"
#include "VoxelSeasonManifest.h"
#include "VoxelStrateComposer.h"
namespace
{
bool SameDensityBits(float A, float B)
{
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgeComposerSeasonTest,
"VoxelForge.Composer.Season",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FVoxelForgeComposerSeasonTest::RunTest(const FString& Parameters)
{
(void)Parameters;
FVoxelStrateCorpus Corpus;
FString CorpusReport;
TestTrue(TEXT("season test loads a valid corpus"),
Corpus.LoadFromAssetRegistry(CorpusReport) && Corpus.IsValid());
if (!Corpus.IsValid())
{
AddError(CorpusReport);
return false;
}
FVoxelSeasonCompositionSettings Settings;
Settings.Corpus = &Corpus;
Settings.CandidateCount = 24;
Settings.SelectedStrateCount = 6;
Settings.StrateHeightInChunks = 4;
Settings.InterStrateGapChunks = 0;
Settings.OriginSpineRadius = 14.0f;
Settings.WorldRadiusVoxels = 0.0f;
Settings.OutputDirectory = FPaths::Combine(
FPaths::ProjectSavedDir(), TEXT("VoxelForge"), TEXT("SeasonTest"));
Settings.bWriteArtifacts = true;
Settings.bWriteReviewPage = true;
Settings.bIncludeCandidateAudit = true;
const int32 SeasonSeed = 0x2468;
const FVoxelSeasonManifest Manifest = VF_ComposeSeason(SeasonSeed, Settings);
TestTrue(TEXT("composition produces a usable season manifest"), Manifest.IsUsable());
if (!Manifest.IsUsable())
{
AddError(Manifest.Error);
return false;
}
TestEqual(TEXT("candidate count is the configured number of attempts"),
Manifest.CandidateCount, Settings.CandidateCount);
TestEqual(TEXT("selected count is the configured spine length"),
Manifest.SelectedCount, Settings.SelectedStrateCount);
TestEqual(TEXT("candidate audit contains every attempt"),
Manifest.CandidateAudit.Num(), Settings.CandidateCount);
TestEqual(TEXT("WorldRadiusVoxels remains zero in the artifact"),
Manifest.WorldRadiusVoxels, 0.0f);
TestEqual(TEXT("selected grounded/outlier counts add to generated slots"),
Manifest.GroundedSelectedCount + Manifest.OutlierSelectedCount,
Manifest.SelectedCount - Manifest.FixedSelectedCount);
for (int32 Index = 0; Index < Manifest.Strates.Num(); ++Index)
{
const FVoxelSeasonStrate& Strate = Manifest.Strates[Index];
TestEqual(FString::Printf(TEXT("slot %d keeps descent order"), Index),
Strate.DepthIndex, Index);
TestTrue(FString::Printf(TEXT("slot %d passed non-vacuous gate"), Index),
Strate.bPassedNonVacuous);
TestTrue(FString::Printf(TEXT("slot %d passed largest-component gate"), Index),
Strate.bPassedLargestComponent);
TestTrue(FString::Printf(TEXT("slot %d passed primordial law"), Index),
Strate.bPassedPrimordialLaw
&& Strate.PrimordialLawResult == EVoxelConnectivityResult::Connected);
TestTrue(FString::Printf(TEXT("slot %d has zero world radius"), Index),
Manifest.WorldRadiusVoxels == 0.0f);
}
TestTrue(TEXT("season manifest JSON was emitted"), !Manifest.SerializedJson.IsEmpty());
TestTrue(TEXT("season manifest file was emitted"), FPaths::FileExists(Manifest.ManifestPath));
TestTrue(TEXT("selected-season review page was emitted"),
FPaths::FileExists(Manifest.ReviewPagePath));
FString ReviewHtml;
if (FFileHelper::LoadFileToString(ReviewHtml, *Manifest.ReviewPagePath))
{
TestTrue(TEXT("review page explains why each selected slot was chosen"),
ReviewHtml.Contains(TEXT("Why selected:")));
TestTrue(TEXT("review page marks boss slots"), ReviewHtml.Contains(TEXT("Boss slot:")));
}
else
{
AddError(FString::Printf(TEXT("could not read review page %s"), *Manifest.ReviewPagePath));
}
// Compose again with the same inputs. SerializedJson excludes derived filesystem paths, so
// this is the byte-level determinism assertion for the portable artifact itself.
const FVoxelSeasonManifest RepeatManifest = VF_ComposeSeason(SeasonSeed, Settings);
TestTrue(TEXT("repeat composition is usable"), RepeatManifest.IsUsable());
TestEqual(TEXT("same season seed produces byte-identical JSON"),
RepeatManifest.SerializedJson, Manifest.SerializedJson);
FVoxelSeasonManifest LoadedManifest;
FString LoadReport;
TestTrue(TEXT("manifest loads from its JSON file"),
VF_LoadVoxelSeasonManifest(Manifest.ManifestPath, LoadedManifest, LoadReport));
if (!LoadedManifest.IsUsable())
{
AddError(LoadReport);
return false;
}
TestEqual(TEXT("loaded manifest has the same number of selected strates"),
LoadedManifest.Strates.Num(), Manifest.Strates.Num());
int64 ComparedDensitySamples = 0;
bool bAllDensityBitsIdentical = true;
for (int32 Index = 0; Index < Manifest.Strates.Num(); ++Index)
{
const FVoxelSeasonStrate& OriginalStrate = Manifest.Strates[Index];
const FVoxelSeasonStrate& LoadedStrate = LoadedManifest.Strates[Index];
TestTrue(FString::Printf(TEXT("slot %d parameter vector survives JSON bit-identically"), Index),
VF_AreStrateArchetypeParamsBitIdentical(
OriginalStrate.Params, LoadedStrate.Params, OriginalStrate.Archetype));
TestTrue(FString::Printf(TEXT("slot %d recipe survives JSON identically"), Index),
VF_AreStrateStructureRecipesIdentical(
OriginalStrate.Recipe, LoadedStrate.Recipe));
FVoxelOpStack OriginalStack;
FVoxelOpContext OriginalContext;
FVoxelOpStack LoadedStack;
FVoxelOpContext LoadedContext;
FString BuildError;
TestTrue(FString::Printf(TEXT("slot %d original stack rebuilds"), Index),
VF_RebuildVoxelSeasonStrate(
OriginalStrate, Manifest.OriginSpineRadius, nullptr,
OriginalStack, OriginalContext, &BuildError));
TestTrue(FString::Printf(TEXT("slot %d loaded stack rebuilds"), Index),
VF_RebuildVoxelSeasonStrate(
LoadedStrate, LoadedManifest.OriginSpineRadius, nullptr,
LoadedStack, LoadedContext, &BuildError));
if (OriginalStack.Num() == 0 || LoadedStack.Num() == 0)
{
bAllDensityBitsIdentical = false;
continue;
}
TestEqual(FString::Printf(TEXT("slot %d original/rebuilt stack operation count"), Index),
LoadedStack.Num(), OriginalStack.Num());
TestEqual(FString::Printf(TEXT("slot %d original context radius"), Index),
OriginalContext.WorldRadiusVoxels, 0.0f);
TestEqual(FString::Printf(TEXT("slot %d loaded context radius"), Index),
LoadedContext.WorldRadiusVoxels, 0.0f);
OriginalStack.PrepareChunk(OriginalContext);
LoadedStack.PrepareChunk(LoadedContext);
// Compare the complete bounded probe lattice for each selected field at two-voxel
// spacing, not a handful of favorable points. Every sampled scalar must retain its
// exact IEEE-754 bits after JSON load and stack reconstruction.
constexpr int32 ProbeHalfExtent = 32;
constexpr int32 ProbeStep = 2;
for (int32 Z = OriginalStrate.BottomWorldZ;
Z < OriginalStrate.TopWorldZ && bAllDensityBitsIdentical;
Z += ProbeStep)
{
for (int32 Y = -ProbeHalfExtent;
Y < ProbeHalfExtent && bAllDensityBitsIdentical;
Y += ProbeStep)
{
for (int32 X = -ProbeHalfExtent;
X < ProbeHalfExtent;
X += ProbeStep)
{
const float OriginalDensity = OriginalStack.EvalMC(
static_cast<float>(X), static_cast<float>(Y), static_cast<float>(Z));
const float LoadedDensity = LoadedStack.EvalMC(
static_cast<float>(X), static_cast<float>(Y), static_cast<float>(Z));
++ComparedDensitySamples;
if (!SameDensityBits(OriginalDensity, LoadedDensity))
{
bAllDensityBitsIdentical = false;
AddError(FString::Printf(
TEXT("slot %d density mismatch at (%d,%d,%d): 0x%08x vs 0x%08x"),
Index, X, Y, Z,
*reinterpret_cast<const uint32*>(&OriginalDensity),
*reinterpret_cast<const uint32*>(&LoadedDensity)));
break;
}
}
}
}
}
AddInfo(FString::Printf(TEXT("round-trip density probe compared %lld bit-identical samples"),
ComparedDensitySamples));
TestTrue(TEXT("manifest round-trip density field is bit-identical"),
bAllDensityBitsIdentical);
return true;
}
#endif
File diff suppressed because it is too large. Load diff
@@ -979,6 +979,18 @@ bool VF_WriteStratePreviewIndex(
SortedCandidates.Sort([](const FVoxelStratePreviewCandidate& A,
const FVoxelStratePreviewCandidate& B)
{
if (A.bSeasonOrder != B.bSeasonOrder)
{
return A.bSeasonOrder;
}
if (A.bSeasonOrder && B.bSeasonOrder)
{
if (A.DepthIndex != B.DepthIndex)
{
return A.DepthIndex < B.DepthIndex;
}
return A.CandidateIndex < B.CandidateIndex;
}
if (A.bRejected != B.bRejected)
{
return !A.bRejected;
@@ -1071,11 +1083,23 @@ bool VF_WriteStratePreviewIndex(
const FString CardClass = Candidate.bRejected ? TEXT("candidate rejected") : TEXT("candidate");
const FString MetricWindow = Candidate.Window.IsValid()
? Candidate.Window.Describe() : WindowDescription;
const int32 DisplayIndex = Candidate.bSeasonOrder && Candidate.DepthIndex != INDEX_NONE
? Candidate.DepthIndex : Candidate.CandidateIndex;
Html += FString::Printf(
TEXT("<section class=\"%s\"><h2>#%d <span class=\"badge %s\">%s</span></h2>\n"),
*CardClass, Candidate.CandidateIndex, *StatusClass, *Status);
*CardClass, DisplayIndex, *StatusClass, *Status);
Html += FString::Printf(TEXT("<div class=\"recipe\"><code>%s</code></div>\n"),
*VF_HtmlEscape(Candidate.RecipeString));
if (Candidate.bSeasonOrder)
{
Html += FString::Printf(
TEXT("<p class=\"reason\" style=\"color:#d7e3f4\"><strong>Why selected:</strong> %s"
"<br><strong>Boss slot:</strong> %s</p>\n"),
*VF_HtmlEscape(Candidate.SelectionReason.IsEmpty()
? TEXT("selected by the provisional season policy")
: Candidate.SelectionReason),
Candidate.bBossSlot ? TEXT("yes") : TEXT("no"));
}
if (Candidate.bRejected)
{
Html += FString::Printf(TEXT("<p class=\"reason\"><strong>Why rejected:</strong> %s</p>\n"),
@@ -0,0 +1,269 @@
// Offline Tier 4c season composition and the diffable Tier A manifest.
//
// This header deliberately describes an editor/build-box artifact. The normal runtime path still
// receives its existing settings and builds density exactly as before; no runtime code consumes a
// season manifest as a hidden generation fallback.
#pragma once
#include "CoreMinimal.h"
#include "VoxelStrateComposer.h"
#include "VoxelStrateMeasure.h"
class UVoxelSettings;
/** Why the provisional season selector put one surviving candidate in a slot. */
enum class EVoxelSeasonSelectionReason : uint8
{
Grounded,
Outlier,
Fixed,
};
/**
* Explicit, reviewable selection knobs for one season.
*
* None of these values is a settled definition of quality. They are the first policy Jahni can
* change after walking the review page. Hard validation remains separate from this policy.
*/
struct VOXELFORGE_API FVoxelSeasonSelectionPolicy
{
// Placeholder mix only: ordinary strates give an outlier contrast. Revisit after the first
// season review; this is not a learned or final quality score.
float GroundedFraction = 0.75f;
float OutlierFraction = 0.25f;
// Below this measured distance two neighbours look too similar. The selector subtracts a
// penalty proportional to the gap; provisional because the useful contrast is visual.
double AdjacentSimilarityDistanceThreshold = 0.20;
double AdjacentSimilarityPenaltyWeight = 1.0;
// Rewards an unseen archetype/recipe in the already chosen prefix. These are variety
// tie-breakers, not claims that one archetype or recipe is better. Provisional review knobs.
double NewArchetypeBonus = 0.10;
double NewRecipeBonus = 0.05;
// GDD §6 says roughly every five levels, and a boss is intermediary rather than terminal.
// This interval is therefore an explicit provisional layout knob, not a quality score.
int32 BossSlotInterval = 5;
// Tier 2 hard gate: a surviving air component must retain at least this share. Provisional
// only in its threshold; the existence of the gate is non-negotiable for a shippable season.
float MinimumLargestComponentShare = 0.50f;
// Normalizers for the two unbounded measured dimensions in the adjacency distance. Fractions
// already live in [0,1]. These physical scales are provisional and visible in the manifest.
float FeatureScaleNormalizationVoxels = 256.0f;
float ClearanceNormalizationVoxels = 64.0f;
};
/** A caller-supplied fixed slot for tests/tools that already have a complete manifest record. */
struct VOXELFORGE_API FVoxelSeasonFixedStrate
{
int32 Seed = 0;
int32 HeightInChunks = 4;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelStrateArchetypeParams Params;
FVoxelOpStackRecipe Recipe;
bool bUsesRecipe = true;
FString SourceDefinitionPath;
};
/** Settings for the offline build-box pass. No member is consulted by normal runtime generation. */
struct VOXELFORGE_API FVoxelSeasonCompositionSettings
{
// Optional live-world settings. TotalStrates, spine/gap values, and FixedStrates are read
// from it when present. The pointer is borrowed for the duration of VF_ComposeSeason only.
const UVoxelSettings* WorldSettings = nullptr;
// Optional preloaded corpus. With nullptr the composer loads the project asset corpus on the
// build box, exactly like the existing offline composer tests.
const FVoxelStrateCorpus* Corpus = nullptr;
// 24 gives a useful minute-scale automation pass at the default coarse measurement window.
// Production can raise this to the overnight budget without changing the policy.
int32 CandidateCount = 24;
int32 SelectedStrateCount = 10;
// Generated candidates use one stable height. Fixed authored slots may provide their own
// height. World layout bounds are written into every selected parameter family below.
int32 StrateHeightInChunks = 4;
int32 InterStrateGapChunks = 0;
float OriginSpineRadius = 14.0f;
float WorldRadiusVoxels = 0.0f;
// 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;
FVoxelSeasonSelectionPolicy SelectionPolicy;
// Explicit composed fixed slots take precedence. When empty, the composer adapts the
// existing UVoxelSettings::FixedStrates map, preserving its absolute slot indices.
TMap<int32, FVoxelSeasonFixedStrate> FixedStrates;
// Empty means ProjectSavedDir()/VoxelForge/Seasons. Files are never written to the runtime
// content directory by this API.
FString OutputDirectory;
bool bWriteArtifacts = true;
bool bWriteReviewPage = true;
bool bIncludeCandidateAudit = true;
// Informational only; when WorldSettings is present CurrentSeason supplies the default.
int32 SeasonNumber = 0;
FVoxelSeasonCompositionSettings()
{
// The existing Tier 4b test uses step 4. Season composition defaults to step 8 so the
// default automation artifact is measured in minutes, while MaxCells remains bounded.
MeasureSettings.SampleStep = 8;
MeasureSettings.RadiusInVoxels = 256;
MeasureSettings.MaxCells = 8000000;
MeasureSettings.MaxRouteRetries = 16;
MeasureSettings.HeadroomCells = 2;
MeasureSettings.InteriorMarginVoxels = -1;
}
};
// Friendly aliases for callers that use the task's shorter naming.
using FVoxelSeasonComposerSettings = FVoxelSeasonCompositionSettings;
using FVoxelSeasonComposeSettings = FVoxelSeasonCompositionSettings;
/** Compact audit row; the full vectors live only for selected strates. */
struct VOXELFORGE_API FVoxelSeasonCandidateAudit
{
int32 CandidateIndex = INDEX_NONE;
int32 Seed = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
uint32 RecipeHash = 0;
double DistanceFromCorpusCentroid = 0.0;
bool bPassedHardGates = false;
bool bSelected = false;
FString RejectionReason;
};
/** Count of one explicit hard-gate or final-placement rejection reason. */
struct VOXELFORGE_API FVoxelSeasonRejectionCount
{
FString Reason;
int32 Count = 0;
};
/** One ordered Tier A spine entry. */
struct VOXELFORGE_API FVoxelSeasonStrate
{
// DepthIndex is the absolute world-layout slot. CandidateIndex is the offline attempt id.
int32 DepthIndex = INDEX_NONE;
int32 CandidateIndex = INDEX_NONE;
int32 Seed = 0;
int32 HeightInChunks = 0;
int32 TopWorldZ = 0;
int32 BottomWorldZ = 0;
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
FVoxelOpStackRecipe Recipe;
FVoxelStrateArchetypeParams Params;
FVoxelStrateMeasuredMetrics Metrics;
double DistanceFromCorpusCentroid = 0.0;
FString SelectionReason;
FString SourceDefinitionPath;
bool bGrounded = false;
bool bOutlier = false;
bool bBossSlot = false;
bool bFixed = false;
bool bUsesRecipe = true;
bool bPassedNonVacuous = false;
bool bPassedLargestComponent = false;
bool bPassedPrimordialLaw = false;
EVoxelConnectivityResult PrimordialLawResult = EVoxelConnectivityResult::OutOfWindow;
};
/**
* The stored Tier A season artifact.
*
* Tier B is intentionally absent: its hash-lattice field remains an arithmetic/enumeration API,
* not a second stored table. Strates is ordered from the surface down and is the only finite spine
* persisted by this task.
*/
struct VOXELFORGE_API FVoxelSeasonManifest
{
static constexpr int32 CurrentSchemaVersion = 1;
bool bValid = false;
int32 SchemaVersion = CurrentSchemaVersion;
FString Error;
int32 Season = 0;
int32 Seed = 0;
uint32 InputCorpusHash = 0;
int32 CandidateCount = 0;
int32 SurvivorCount = 0;
int32 SelectedCount = 0;
int32 RejectedCount = 0;
int32 GroundedSelectedCount = 0;
int32 OutlierSelectedCount = 0;
int32 FixedSelectedCount = 0;
int32 TotalStrates = 0;
int32 InterStrateGapChunks = 0;
float OriginSpineRadius = 0.0f;
float WorldRadiusVoxels = 0.0f;
FVoxelSeasonSelectionPolicy SelectionPolicy;
FString PolicyNote;
TArray<FVoxelSeasonStrate> Strates;
TArray<FVoxelSeasonCandidateAudit> CandidateAudit;
TArray<FVoxelSeasonRejectionCount> RejectionCounts;
// Derived artifact locations; intentionally not serialized into the portable JSON.
FString ManifestPath;
FString ReviewPagePath;
FString SerializedJson;
bool IsUsable() const
{
return bValid && Error.IsEmpty() && WorldRadiusVoxels == 0.0f
&& Strates.Num() == SelectedCount && SelectedCount > 0;
}
};
/** Human-readable enum names used by JSON and the review page. */
VOXELFORGE_API const TCHAR* VF_GetVoxelSeasonSelectionReasonName(
EVoxelSeasonSelectionReason Reason);
/** Compose, validate, select, serialize, and (by default) write one season. Editor/build-box only. */
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
int32 SeasonSeed, const FVoxelSeasonCompositionSettings& Settings);
/** Convenience overload that adapts the existing runtime settings asset. */
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
int32 SeasonSeed, const UVoxelSettings* WorldSettings);
/** Serialize/read the complete Tier A manifest as stable pretty JSON. */
VOXELFORGE_API FString VF_SerializeVoxelSeasonManifest(
const FVoxelSeasonManifest& Manifest);
VOXELFORGE_API bool VF_SaveVoxelSeasonManifest(
const FString& ManifestPath, const FVoxelSeasonManifest& Manifest, FString& OutReport);
VOXELFORGE_API bool VF_LoadVoxelSeasonManifest(
const FString& ManifestPath, FVoxelSeasonManifest& OutManifest, FString& OutReport);
/** Rebuild exactly the stack represented by one manifest entry. */
VOXELFORGE_API bool VF_BuildSeasonStrateStack(
const FVoxelSeasonStrate& Strate,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
/** Verbose alias for tests/tools that want the round-trip operation named explicitly. */
VOXELFORGE_API bool VF_RebuildVoxelSeasonStrate(
const FVoxelSeasonStrate& Strate,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError = nullptr);
@@ -18,6 +18,16 @@ class UVoxelSettings;
class UVoxelStrateDefinition;
class UVoxelStrateManager;
#if WITH_EDITOR
// The complete definitions live in VoxelSeasonManifest.h. Keeping this forward declaration here
// makes the Tier 4c entry point discoverable from the composer header without creating a circular
// include (the manifest header includes this one for the existing vector/recipe types).
struct FVoxelSeasonManifest;
struct FVoxelSeasonCompositionSettings;
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
int32 SeasonSeed, const FVoxelSeasonCompositionSettings& Settings);
#endif
/** The one-bit identity choice at the root of a rolled structure. */
UENUM(BlueprintType)
enum class EVoxelStrateRootPolarity : uint8
@@ -71,6 +71,12 @@ struct VOXELFORGE_API FVoxelStrateFinePreviewSettings
struct VOXELFORGE_API FVoxelStratePreviewCandidate
{
int32 CandidateIndex = INDEX_NONE;
// Season pages deliberately opt into descent order. The ordinary candidate contact sheet
// keeps its survivor/outlier ordering; a season review must read top-to-bottom like play.
bool bSeasonOrder = false;
int32 DepthIndex = INDEX_NONE;
FString SelectionReason;
bool bBossSlot = false;
FVoxelStratePreviewWindow Window;
FString RecipeString;
FString ArrivalDepartureVerdict;