Tier 4a. The first piece of the composer that INVENTS rather than fixes. The design, from COMPOSER-NOTES §3.3: do NOT roll 73 parameters independently. The coherent region of a 73-dimensional space is a vanishingly thin sheet and uniform sampling never lands on it -- and independent rolling destroys the CORRELATIONS (room spacing <-> room radius <-> tunnel length) that make a vector coherent. So blend known-good vectors through the existing FStrateGenerationParams::Lerp instead: constraint satisfaction is free, because a convex combination preserves every relation holding in both parents. Min <= Max stays true because it was never broken. Built: FVoxelStrateCorpus (loads authored vectors, computes per-field spread), VF_RollStrateParams (2-3 weighted parents, convex blend, jitter by +-15% of each field's MEASURED corpus range -- not by the clamps, which are "don't type nonsense" limits, not "this makes a good world" limits), and VoxelForge.Composer.ParameterRoll. Determinism asserted by rolling twice and comparing every field. Exclusion list found by reflection, not assumption: StrateTopWorldZ and StrateBottomWorldZ (manager-owned runtime bounds) plus 34 terrain-op transport fields (TerraceStepHeight..PinchLength) that are copied in from room operators rather than being strate tunables. Bools inherit from the dominant parent; ints/enums use Lerp's existing snap. ⚠️ THE RESULT IS VACUOUS AND SAYS SO. 64/64 candidates "survived" -- but the corpus loaded ONE vector (DA_Strate3), so all 64 are bit-identical. 100% survival of a single point is not evidence of anything. The box- verdict check is vacuous too: 2,560 boxes, 0 proved, 0 voxels brute-forced, so §6.2 is NOT closed. The test emits a warning saying exactly this rather than reporting a clean pass, and no workaround was added to hide it. Four strate assets exist (DA_Strate1..4); the loader found one. Fixing the corpus is the next task -- with one parent there is nothing to blend, and every number above is a placeholder. Also owed: clamp metadata is editor-only, so the safety net needs a cook-time baked table before this ships. Nothing calls the composer from any runtime path (§3.1: offline, once per season, on a build box). Build clean, 23 tests / 0 failed (1 carries the deliberate vacuity warning), verified in VoxelM.log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
152 lines
5.6 KiB
C++
152 lines
5.6 KiB
C++
// Offline strate corpus and parameter composer.
|
|
//
|
|
// This is deliberately not part of the runtime generation path. The composer is a build-box
|
|
// tool/test API: it loads authored vectors, measures their spread, and makes deterministic
|
|
// candidates by blending those vectors before applying a small spread-relative jitter.
|
|
|
|
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "VoxelStrateTypes.h"
|
|
|
|
class UVoxelSettings;
|
|
class UVoxelStrateDefinition;
|
|
|
|
/** The kind of value represented by one entry of VF_STRATE_PARAM_FIELDS. */
|
|
enum class EVoxelStrateFieldKind : uint8
|
|
{
|
|
Continuous,
|
|
Integer,
|
|
Boolean,
|
|
Enum,
|
|
};
|
|
|
|
/** A field deliberately kept out of the parameter roll. */
|
|
struct VOXELFORGE_API FVoxelStrateFieldExclusion
|
|
{
|
|
FString FieldName;
|
|
FString Reason;
|
|
};
|
|
|
|
/** Measured statistics for one field across the currently loaded corpus. */
|
|
struct VOXELFORGE_API FVoxelStrateFieldSpread
|
|
{
|
|
FString FieldName;
|
|
EVoxelStrateFieldKind Kind = EVoxelStrateFieldKind::Continuous;
|
|
bool bExcluded = false;
|
|
bool bReflected = false;
|
|
|
|
int32 SampleCount = 0;
|
|
double Min = 0.0;
|
|
double Max = 0.0;
|
|
double Mean = 0.0;
|
|
double StdDev = 0.0;
|
|
|
|
// These are copied from UPROPERTY ClampMin/ClampMax when reflection metadata is available in
|
|
// the current build. They are safety limits, never the source of a roll range.
|
|
bool bHasClampMin = false;
|
|
bool bHasClampMax = false;
|
|
double ClampMin = 0.0;
|
|
double ClampMax = 0.0;
|
|
};
|
|
|
|
/** One known-good authored vector and the archetype that gives it meaning. */
|
|
struct VOXELFORGE_API FVoxelStrateCorpusEntry
|
|
{
|
|
FString SourcePath;
|
|
FString SourceName;
|
|
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
|
|
FStrateGenerationParams Params;
|
|
|
|
// The current asset format has no corpus weight field. Loaded hand-authored entries therefore
|
|
// use 1.0. The field remains explicit so a future offline corpus can assign weights without
|
|
// changing the roll algorithm.
|
|
float Weight = 1.0f;
|
|
};
|
|
|
|
/** Result of one deterministic roll, including provenance for the offline report. */
|
|
struct VOXELFORGE_API FVoxelStrateRollInfo
|
|
{
|
|
bool bValid = false;
|
|
FString FailureReason;
|
|
|
|
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
|
|
FStrateGenerationParams Params;
|
|
|
|
TArray<int32> ParentEntryIndices;
|
|
TArray<float> ParentWeights;
|
|
int32 DominantParentPosition = INDEX_NONE;
|
|
};
|
|
|
|
/**
|
|
* Known-good strate vectors plus their measured field spread.
|
|
*
|
|
* `LoadFromSettings` walks both the pool and fixed-strate references, resolves each definition,
|
|
* de-duplicates it by asset path, and admits only TunnelNetwork/Underwater definitions because
|
|
* those are the two archetypes whose authored vector is `GenerationParams`. Other archetypes have
|
|
* different parameter structs; treating their default `GenerationParams` as authored data would
|
|
* manufacture a false corpus.
|
|
*/
|
|
class VOXELFORGE_API FVoxelStrateCorpus
|
|
{
|
|
public:
|
|
void Reset();
|
|
|
|
bool LoadFromSettings(const UVoxelSettings* Settings, FString& OutReport);
|
|
bool LoadFromDefinitions(const TArray<UVoxelStrateDefinition*>& Definitions, FString& OutReport);
|
|
|
|
/** Add an already-resolved vector, primarily for offline tools and focused tests. */
|
|
bool AddEntry(const FString& SourcePath, const FString& SourceName,
|
|
ECaveGeneratorType Archetype, const FStrateGenerationParams& Params,
|
|
float Weight = 1.0f);
|
|
|
|
bool IsValid() const { return bSchemaValid && Entries.Num() > 0; }
|
|
bool IsSchemaValid() const { return bSchemaValid; }
|
|
const FString& GetSchemaError() const { return SchemaError; }
|
|
|
|
int32 Num() const { return Entries.Num(); }
|
|
const TArray<FVoxelStrateCorpusEntry>& GetEntries() const { return Entries; }
|
|
const TArray<FVoxelStrateFieldSpread>& GetFieldSpreads() const { return FieldSpreads; }
|
|
const TArray<FString>& GetSkippedDefinitions() const { return SkippedDefinitions; }
|
|
|
|
const FVoxelStrateFieldSpread* FindSpread(const FString& FieldName) const;
|
|
|
|
/** Stable hash of source paths, archetypes, weights, and every listed scalar field. */
|
|
uint32 GetContentsHash() const;
|
|
|
|
/** Explicit exclusion list discovered from the struct reflection/use audit. */
|
|
static const TArray<FVoxelStrateFieldExclusion>& GetNonTunableFields();
|
|
|
|
/** True in an editor build where UPROPERTY metadata was available while spreads were built. */
|
|
bool AreClampMetadataAvailableAtBuild() const { return bClampMetadataAvailableAtBuild; }
|
|
|
|
/** False by policy: metadata is not promised to survive into a cooked runtime. */
|
|
bool AreClampMetadataAvailableAtRuntime() const { return false; }
|
|
|
|
private:
|
|
void RebuildSpreads();
|
|
|
|
TArray<FVoxelStrateCorpusEntry> Entries;
|
|
TArray<FVoxelStrateFieldSpread> FieldSpreads;
|
|
TArray<FString> SkippedDefinitions;
|
|
FString SchemaError;
|
|
bool bSchemaValid = true;
|
|
bool bClampMetadataAvailableAtBuild = false;
|
|
};
|
|
|
|
/** Human-readable name used by the offline report. */
|
|
VOXELFORGE_API const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archetype);
|
|
|
|
/** Full provenance result. Pure with respect to the corpus: no global or retained RNG state. */
|
|
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailed(
|
|
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index);
|
|
|
|
/** The compact API requested by the composer design. */
|
|
VOXELFORGE_API FStrateGenerationParams VF_RollStrateParams(
|
|
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index);
|
|
|
|
/** Bitwise comparison over exactly the fields in FStrateGenerationParams::Lerp. */
|
|
VOXELFORGE_API bool VF_AreStrateParamsBitIdentical(
|
|
const FStrateGenerationParams& A, const FStrateGenerationParams& B);
|
|
|