feat(composer): the parameter roll -- infrastructure real, corpus starved
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
This commit is contained in:
1 parent
8e346a3054
commit
358bd415a9
5 files changed
+1649
-2
No files matched your search
@@ -0,0 +1,931 @@
|
||||
// Offline strate corpus and parameter roll.
|
||||
|
||||
#include "VoxelStrateComposer.h"
|
||||
|
||||
#include "Math/RandomStream.h"
|
||||
#include "Misc/Crc.h"
|
||||
#include "UObject/FieldIterator.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float GJitterFraction = 0.15f;
|
||||
|
||||
template <typename T>
|
||||
EVoxelStrateFieldKind VF_FieldKindFor()
|
||||
{
|
||||
using ValueType = typename std::remove_cv<
|
||||
typename std::remove_reference<T>::type>::type;
|
||||
|
||||
if (std::is_same<ValueType, bool>::value)
|
||||
{
|
||||
return EVoxelStrateFieldKind::Boolean;
|
||||
}
|
||||
if (std::is_enum<ValueType>::value)
|
||||
{
|
||||
return EVoxelStrateFieldKind::Enum;
|
||||
}
|
||||
if (std::is_integral<ValueType>::value)
|
||||
{
|
||||
return EVoxelStrateFieldKind::Integer;
|
||||
}
|
||||
return EVoxelStrateFieldKind::Continuous;
|
||||
}
|
||||
|
||||
const FVoxelStrateFieldExclusion* VF_FindExclusion(const FString& FieldName)
|
||||
{
|
||||
for (const FVoxelStrateFieldExclusion& Exclusion : FVoxelStrateCorpus::GetNonTunableFields())
|
||||
{
|
||||
if (Exclusion.FieldName == FieldName)
|
||||
{
|
||||
return &Exclusion;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void VF_AddExclusion(TArray<FVoxelStrateFieldExclusion>& Out,
|
||||
const TCHAR* FieldName, const TCHAR* Reason)
|
||||
{
|
||||
FVoxelStrateFieldExclusion& Entry = Out.AddDefaulted_GetRef();
|
||||
Entry.FieldName = FieldName;
|
||||
Entry.Reason = Reason;
|
||||
}
|
||||
|
||||
void VF_CollectGenerationProperties(TMap<FString, FProperty*>& OutProperties)
|
||||
{
|
||||
OutProperties.Reset();
|
||||
UStruct* Struct = FStrateGenerationParams::StaticStruct();
|
||||
for (TFieldIterator<FProperty> It(Struct, EFieldIteratorFlags::IncludeSuper); It; ++It)
|
||||
{
|
||||
FProperty* Property = *It;
|
||||
if (Property != nullptr)
|
||||
{
|
||||
OutProperties.Add(Property->GetName(), Property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VF_AddPropertyMetadata(FVoxelStrateFieldSpread& Spread, const FProperty* Property)
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
if (Property == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FString ClampMinText = Property->GetMetaData(TEXT("ClampMin"));
|
||||
const FString ClampMaxText = Property->GetMetaData(TEXT("ClampMax"));
|
||||
|
||||
if (!ClampMinText.IsEmpty())
|
||||
{
|
||||
const double Value = FCString::Atod(*ClampMinText);
|
||||
if (FMath::IsFinite(Value))
|
||||
{
|
||||
Spread.bHasClampMin = true;
|
||||
Spread.ClampMin = Value;
|
||||
}
|
||||
}
|
||||
if (!ClampMaxText.IsEmpty())
|
||||
{
|
||||
const double Value = FCString::Atod(*ClampMaxText);
|
||||
if (FMath::IsFinite(Value))
|
||||
{
|
||||
Spread.bHasClampMax = true;
|
||||
Spread.ClampMax = Value;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// UPROPERTY metadata is not a cooked-runtime contract. The composer is an offline tool;
|
||||
// runtime/cook-time clamp data must be baked by a future commandlet.
|
||||
(void)Spread;
|
||||
(void)Property;
|
||||
#endif
|
||||
}
|
||||
|
||||
void VF_AddSpreadDescriptor(TArray<FVoxelStrateFieldSpread>& OutSpreads,
|
||||
TMap<FString, int32>& OutIndices,
|
||||
const TMap<FString, FProperty*>& Properties,
|
||||
const TCHAR* Name, EVoxelStrateFieldKind Kind)
|
||||
{
|
||||
FVoxelStrateFieldSpread& Spread = OutSpreads.AddDefaulted_GetRef();
|
||||
Spread.FieldName = Name;
|
||||
Spread.Kind = Kind;
|
||||
Spread.bExcluded = VF_FindExclusion(Spread.FieldName) != nullptr;
|
||||
|
||||
if (const FProperty* const* Property = Properties.Find(Spread.FieldName))
|
||||
{
|
||||
Spread.bReflected = (*Property != nullptr);
|
||||
VF_AddPropertyMetadata(Spread, *Property);
|
||||
}
|
||||
|
||||
OutIndices.Add(Spread.FieldName, OutSpreads.Num() - 1);
|
||||
}
|
||||
|
||||
void VF_AccumulateValue(const TCHAR* Name, double Value,
|
||||
const TMap<FString, int32>& Indices,
|
||||
TArray<TArray<double>>& Values)
|
||||
{
|
||||
const int32* Index = Indices.Find(FString(Name));
|
||||
if (Index != nullptr && Values.IsValidIndex(*Index) && FMath::IsFinite(Value))
|
||||
{
|
||||
Values[*Index].Add(Value);
|
||||
}
|
||||
}
|
||||
|
||||
void VF_ResetNonTunableFields(FStrateGenerationParams& Params)
|
||||
{
|
||||
// These fields are deliberately not inherited from a parent, jittered, or clamped. The
|
||||
// first group is terrain-op transport populated per room by UVoxelTerrainOpDefinition;
|
||||
// the last two are manager-owned runtime Z bounds.
|
||||
Params.TerraceStepHeight = 0.0f;
|
||||
Params.TerraceHardness = 0.0f;
|
||||
Params.TerraceNoiseDisplacement = 0.0f;
|
||||
Params.LayerLineSpacing = 0.0f;
|
||||
Params.LayerLineDepth = 0.0f;
|
||||
Params.OverhangStrength = 0.0f;
|
||||
Params.OverhangDepth = 0.0f;
|
||||
Params.OverhangFrequency = 0.0f;
|
||||
Params.RibbingSpacing = 0.0f;
|
||||
Params.RibbingDepth = 0.0f;
|
||||
Params.CliffStrength = 0.0f;
|
||||
Params.ScallopStrength = 0.0f;
|
||||
Params.ScallopFrequency = 0.0f;
|
||||
Params.ArchDensity = 0.0f;
|
||||
Params.ArchMinRadius = 0.0f;
|
||||
Params.ArchMaxRadius = 0.0f;
|
||||
Params.ColumnDensity = 0.0f;
|
||||
Params.ColumnMinRadius = 0.0f;
|
||||
Params.ColumnMaxRadius = 0.0f;
|
||||
Params.PitDensity = 0.0f;
|
||||
Params.PitMinRadius = 0.0f;
|
||||
Params.PitMaxRadius = 0.0f;
|
||||
Params.PitDepth = 0.0f;
|
||||
Params.ChimneyDensity = 0.0f;
|
||||
Params.ChimneyMinRadius = 0.0f;
|
||||
Params.ChimneyMaxRadius = 0.0f;
|
||||
Params.ChimneyHeight = 0.0f;
|
||||
Params.DomeDensity = 0.0f;
|
||||
Params.DomeMinRadius = 0.0f;
|
||||
Params.DomeMaxRadius = 0.0f;
|
||||
Params.DomeHeightRatio = 0.0f;
|
||||
Params.PinchDensity = 0.0f;
|
||||
Params.PinchStrength = 0.0f;
|
||||
Params.PinchLength = 0.0f;
|
||||
Params.StrateTopWorldZ = 0.0f;
|
||||
Params.StrateBottomWorldZ = 0.0f;
|
||||
}
|
||||
|
||||
void VF_JitterFloat(float& Value, const FVoxelStrateFieldSpread* Spread, FRandomStream& Rng)
|
||||
{
|
||||
if (Spread == nullptr || Spread->bExcluded || Spread->Kind != EVoxelStrateFieldKind::Continuous)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float CorpusRange = (float)FMath::Max(0.0, Spread->Max - Spread->Min);
|
||||
if (CorpusRange > 0.0f)
|
||||
{
|
||||
Value += (Rng.FRand() * 2.0f - 1.0f) * GJitterFraction * CorpusRange;
|
||||
}
|
||||
}
|
||||
|
||||
void VF_ClampFloat(float& Value, const FVoxelStrateFieldSpread* Spread)
|
||||
{
|
||||
if (Spread == nullptr || Spread->bExcluded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Spread->bHasClampMin)
|
||||
{
|
||||
Value = FMath::Max(Value, (float)Spread->ClampMin);
|
||||
}
|
||||
if (Spread->bHasClampMax)
|
||||
{
|
||||
Value = FMath::Min(Value, (float)Spread->ClampMax);
|
||||
}
|
||||
}
|
||||
|
||||
void VF_ClampInt(int32& Value, const FVoxelStrateFieldSpread* Spread)
|
||||
{
|
||||
if (Spread == nullptr || Spread->bExcluded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Spread->bHasClampMin)
|
||||
{
|
||||
Value = FMath::Max(Value, FMath::CeilToInt((float)Spread->ClampMin));
|
||||
}
|
||||
if (Spread->bHasClampMax)
|
||||
{
|
||||
Value = FMath::Min(Value, FMath::FloorToInt((float)Spread->ClampMax));
|
||||
}
|
||||
}
|
||||
|
||||
void VF_RepairOrderedPair(float& MinValue, float& MaxValue)
|
||||
{
|
||||
// Convex blending preserves these relations, but independent jitter is allowed to cross
|
||||
// one. Repairing the pair after the jitter keeps the relation without inventing a new
|
||||
// range or touching generation code.
|
||||
if (MinValue > MaxValue)
|
||||
{
|
||||
Swap(MinValue, MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
uint32 VF_Avalanche(uint32 Value)
|
||||
{
|
||||
Value ^= Value >> 16;
|
||||
Value *= 0x7feb352dU;
|
||||
Value ^= Value >> 15;
|
||||
Value *= 0x846ca68bU;
|
||||
Value ^= Value >> 16;
|
||||
return Value;
|
||||
}
|
||||
|
||||
uint32 VF_RollSeed(uint32 CorpusHash, int32 Seed, int32 Index)
|
||||
{
|
||||
uint32 Value = CorpusHash ^ 0x9e3779b9U;
|
||||
Value = VF_Avalanche(Value ^ (uint32)Seed);
|
||||
Value = VF_Avalanche(Value ^ ((uint32)Index + 0x85ebca6bU));
|
||||
return Value;
|
||||
}
|
||||
|
||||
struct FParentGroup
|
||||
{
|
||||
ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork;
|
||||
TArray<int32> EntryIndices;
|
||||
};
|
||||
|
||||
int32 VF_WeightedPick(const TArray<int32>& Candidates,
|
||||
const TArray<FVoxelStrateCorpusEntry>& Entries,
|
||||
FRandomStream& Rng)
|
||||
{
|
||||
if (Candidates.Num() == 0)
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
float TotalWeight = 0.0f;
|
||||
for (const int32 Candidate : Candidates)
|
||||
{
|
||||
if (Entries.IsValidIndex(Candidate))
|
||||
{
|
||||
TotalWeight += FMath::Max(0.0f, Entries[Candidate].Weight);
|
||||
}
|
||||
}
|
||||
|
||||
if (TotalWeight <= 0.0f)
|
||||
{
|
||||
return Candidates[Rng.RandRange(0, Candidates.Num() - 1)];
|
||||
}
|
||||
|
||||
float Cursor = Rng.FRand() * TotalWeight;
|
||||
for (const int32 Candidate : Candidates)
|
||||
{
|
||||
if (!Entries.IsValidIndex(Candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Cursor -= FMath::Max(0.0f, Entries[Candidate].Weight);
|
||||
if (Cursor <= 0.0f)
|
||||
{
|
||||
return Candidate;
|
||||
}
|
||||
}
|
||||
return Candidates.Last();
|
||||
}
|
||||
|
||||
FVoxelStrateRollInfo VF_RollInternal(const FVoxelStrateCorpus& Corpus,
|
||||
int32 Seed, int32 Index)
|
||||
{
|
||||
FVoxelStrateRollInfo Result;
|
||||
if (!Corpus.IsValid())
|
||||
{
|
||||
Result.FailureReason = Corpus.IsSchemaValid()
|
||||
? TEXT("The corpus is empty.")
|
||||
: Corpus.GetSchemaError();
|
||||
return Result;
|
||||
}
|
||||
|
||||
const TArray<FVoxelStrateCorpusEntry>& Entries = Corpus.GetEntries();
|
||||
TArray<FParentGroup> Groups;
|
||||
for (int32 EntryIndex = 0; EntryIndex < Entries.Num(); ++EntryIndex)
|
||||
{
|
||||
int32 GroupIndex = INDEX_NONE;
|
||||
for (int32 CandidateGroup = 0; CandidateGroup < Groups.Num(); ++CandidateGroup)
|
||||
{
|
||||
if (Groups[CandidateGroup].Archetype == Entries[EntryIndex].Archetype)
|
||||
{
|
||||
GroupIndex = CandidateGroup;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (GroupIndex == INDEX_NONE)
|
||||
{
|
||||
FParentGroup& Group = Groups.AddDefaulted_GetRef();
|
||||
Group.Archetype = Entries[EntryIndex].Archetype;
|
||||
GroupIndex = Groups.Num() - 1;
|
||||
}
|
||||
Groups[GroupIndex].EntryIndices.Add(EntryIndex);
|
||||
}
|
||||
|
||||
if (Groups.Num() == 0)
|
||||
{
|
||||
Result.FailureReason = TEXT("The corpus contains no usable archetype group.");
|
||||
return Result;
|
||||
}
|
||||
|
||||
FRandomStream Rng((int32)VF_RollSeed(Corpus.GetContentsHash(), Seed, Index));
|
||||
|
||||
float TotalGroupWeight = 0.0f;
|
||||
for (const FParentGroup& Group : Groups)
|
||||
{
|
||||
for (const int32 EntryIndex : Group.EntryIndices)
|
||||
{
|
||||
TotalGroupWeight += FMath::Max(0.0f, Entries[EntryIndex].Weight);
|
||||
}
|
||||
}
|
||||
|
||||
int32 GroupIndex = 0;
|
||||
if (TotalGroupWeight > 0.0f)
|
||||
{
|
||||
float Cursor = Rng.FRand() * TotalGroupWeight;
|
||||
for (int32 CandidateGroup = 0; CandidateGroup < Groups.Num(); ++CandidateGroup)
|
||||
{
|
||||
float GroupWeight = 0.0f;
|
||||
for (const int32 EntryIndex : Groups[CandidateGroup].EntryIndices)
|
||||
{
|
||||
GroupWeight += FMath::Max(0.0f, Entries[EntryIndex].Weight);
|
||||
}
|
||||
Cursor -= GroupWeight;
|
||||
if (Cursor <= 0.0f)
|
||||
{
|
||||
GroupIndex = CandidateGroup;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GroupIndex = Rng.RandRange(0, Groups.Num() - 1);
|
||||
}
|
||||
|
||||
const FParentGroup& Group = Groups[GroupIndex];
|
||||
const int32 ParentCount = Rng.RandRange(2, 3);
|
||||
TArray<int32> Available = Group.EntryIndices;
|
||||
Result.ParentEntryIndices.Reserve(ParentCount);
|
||||
Result.ParentWeights.Reserve(ParentCount);
|
||||
|
||||
for (int32 ParentSlot = 0; ParentSlot < ParentCount; ++ParentSlot)
|
||||
{
|
||||
// Once a group is exhausted, replacement is intentional. It keeps the requested
|
||||
// 2–3-parent algorithm defined for a one-entry corpus and makes that limitation
|
||||
// visible in the provenance list rather than silently inventing a second vector.
|
||||
const TArray<int32>& Candidates = Available.Num() > 0 ? Available : Group.EntryIndices;
|
||||
const int32 ParentIndex = VF_WeightedPick(Candidates, Entries, Rng);
|
||||
if (ParentIndex == INDEX_NONE)
|
||||
{
|
||||
Result.FailureReason = TEXT("Parent selection failed for a non-empty group.");
|
||||
return Result;
|
||||
}
|
||||
Result.ParentEntryIndices.Add(ParentIndex);
|
||||
Result.ParentWeights.Add(0.1f + Rng.FRand());
|
||||
Available.Remove(ParentIndex);
|
||||
}
|
||||
|
||||
float WeightSum = 0.0f;
|
||||
for (const float Weight : Result.ParentWeights)
|
||||
{
|
||||
WeightSum += Weight;
|
||||
}
|
||||
if (!(WeightSum > 0.0f))
|
||||
{
|
||||
Result.FailureReason = TEXT("Parent blend weights were not positive.");
|
||||
return Result;
|
||||
}
|
||||
for (float& Weight : Result.ParentWeights)
|
||||
{
|
||||
Weight /= WeightSum;
|
||||
}
|
||||
|
||||
Result.DominantParentPosition = 0;
|
||||
for (int32 ParentSlot = 1; ParentSlot < Result.ParentWeights.Num(); ++ParentSlot)
|
||||
{
|
||||
if (Result.ParentWeights[ParentSlot] > Result.ParentWeights[Result.DominantParentPosition])
|
||||
{
|
||||
Result.DominantParentPosition = ParentSlot;
|
||||
}
|
||||
}
|
||||
|
||||
FStrateGenerationParams Params = Entries[Result.ParentEntryIndices[0]].Params;
|
||||
float AccumulatedWeight = Result.ParentWeights[0];
|
||||
for (int32 ParentSlot = 1; ParentSlot < Result.ParentEntryIndices.Num(); ++ParentSlot)
|
||||
{
|
||||
const float ThisWeight = Result.ParentWeights[ParentSlot];
|
||||
const float Alpha = ThisWeight / (AccumulatedWeight + ThisWeight);
|
||||
Params = FStrateGenerationParams::Lerp(
|
||||
Params, Entries[Result.ParentEntryIndices[ParentSlot]].Params, Alpha);
|
||||
AccumulatedWeight += ThisWeight;
|
||||
}
|
||||
|
||||
// Discrete values are not jittered. Lerp already snaps int/enum fields at 0.5; bools use
|
||||
// the explicit dominant-parent policy because a boolean has no meaningful linear blend.
|
||||
const int32 DominantParent = Result.ParentEntryIndices[Result.DominantParentPosition];
|
||||
Params.bTunnelsFlowTowardOrigin = Entries[DominantParent].Params.bTunnelsFlowTowardOrigin;
|
||||
|
||||
#define VF_COMPOSER_JITTER_LERPF(Name) \
|
||||
VF_JitterFloat(Params.Name, Corpus.FindSpread(TEXT(#Name)), Rng);
|
||||
#define VF_COMPOSER_JITTER_SNAP(Name)
|
||||
VF_STRATE_PARAM_FIELDS(VF_COMPOSER_JITTER_LERPF, VF_COMPOSER_JITTER_SNAP)
|
||||
#undef VF_COMPOSER_JITTER_LERPF
|
||||
#undef VF_COMPOSER_JITTER_SNAP
|
||||
|
||||
// Clamp metadata is consulted only after blend+jitter. It is a hard editor-build safety
|
||||
// net, not a distribution range.
|
||||
#define VF_COMPOSER_CLAMP_LERPF(Name) \
|
||||
VF_ClampFloat(Params.Name, Corpus.FindSpread(TEXT(#Name)));
|
||||
#define VF_COMPOSER_CLAMP_INT(Name) \
|
||||
VF_ClampInt(Params.Name, Corpus.FindSpread(TEXT(#Name)));
|
||||
#define VF_COMPOSER_CLAMP_SNAP_OriginRoomMaxConnections() \
|
||||
VF_ClampInt(Params.OriginRoomMaxConnections, Corpus.FindSpread(TEXT("OriginRoomMaxConnections")));
|
||||
#define VF_COMPOSER_CLAMP_SNAP_bTunnelsFlowTowardOrigin()
|
||||
#define VF_COMPOSER_CLAMP_SNAP_RoughnessNoiseType()
|
||||
#define VF_COMPOSER_CLAMP_SNAP(Name) VF_COMPOSER_CLAMP_SNAP_##Name()
|
||||
/* The SNAP expansion is dispatched field-by-field because the list contains one int,
|
||||
* one bool, and one enum. */
|
||||
VF_STRATE_PARAM_FIELDS(VF_COMPOSER_CLAMP_LERPF, VF_COMPOSER_CLAMP_SNAP)
|
||||
#undef VF_COMPOSER_CLAMP_LERPF
|
||||
#undef VF_COMPOSER_CLAMP_INT
|
||||
#undef VF_COMPOSER_CLAMP_SNAP
|
||||
#undef VF_COMPOSER_CLAMP_SNAP_OriginRoomMaxConnections
|
||||
#undef VF_COMPOSER_CLAMP_SNAP_bTunnelsFlowTowardOrigin
|
||||
#undef VF_COMPOSER_CLAMP_SNAP_RoughnessNoiseType
|
||||
|
||||
VF_RepairOrderedPair(Params.MinRoomRadius, Params.MaxRoomRadius);
|
||||
VF_RepairOrderedPair(Params.RoomFloorCutMin, Params.RoomFloorCutMax);
|
||||
VF_RepairOrderedPair(Params.TunnelMinRadius, Params.TunnelMaxRadius);
|
||||
VF_RepairOrderedPair(Params.ArchMinRadius, Params.ArchMaxRadius);
|
||||
VF_RepairOrderedPair(Params.ColumnMinRadius, Params.ColumnMaxRadius);
|
||||
VF_RepairOrderedPair(Params.PitMinRadius, Params.PitMaxRadius);
|
||||
VF_RepairOrderedPair(Params.ChimneyMinRadius, Params.ChimneyMaxRadius);
|
||||
VF_RepairOrderedPair(Params.DomeMinRadius, Params.DomeMaxRadius);
|
||||
|
||||
VF_ResetNonTunableFields(Params);
|
||||
|
||||
Result.Params = Params;
|
||||
Result.Archetype = Group.Archetype;
|
||||
Result.bValid = true;
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
|
||||
const TArray<FVoxelStrateFieldExclusion>& FVoxelStrateCorpus::GetNonTunableFields()
|
||||
{
|
||||
static const TArray<FVoxelStrateFieldExclusion> Exclusions = []
|
||||
{
|
||||
TArray<FVoxelStrateFieldExclusion> Result;
|
||||
|
||||
const TCHAR* TerrainOpReason = TEXT(
|
||||
"Internal terrain-op transport: populated per room by UVoxelTerrainOpDefinition, not an authored GenerationParams tunable.");
|
||||
VF_AddExclusion(Result, TEXT("TerraceStepHeight"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("TerraceHardness"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("TerraceNoiseDisplacement"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("LayerLineSpacing"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("LayerLineDepth"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("OverhangStrength"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("OverhangDepth"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("OverhangFrequency"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("RibbingSpacing"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("RibbingDepth"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("CliffStrength"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ScallopStrength"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ScallopFrequency"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ArchDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ArchMinRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ArchMaxRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ColumnDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ColumnMinRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ColumnMaxRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitMinRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitMaxRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PitDepth"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyMinRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyMaxRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("ChimneyHeight"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("DomeDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("DomeMinRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("DomeMaxRadius"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("DomeHeightRatio"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PinchDensity"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PinchStrength"), TerrainOpReason);
|
||||
VF_AddExclusion(Result, TEXT("PinchLength"), TerrainOpReason);
|
||||
|
||||
VF_AddExclusion(Result, TEXT("StrateTopWorldZ"), TEXT(
|
||||
"Runtime Z bound supplied by UVoxelStrateManager for the active layout slot; not a tunable."));
|
||||
VF_AddExclusion(Result, TEXT("StrateBottomWorldZ"), TEXT(
|
||||
"Runtime Z bound supplied by UVoxelStrateManager for the active layout slot; not a tunable."));
|
||||
return Result;
|
||||
}();
|
||||
return Exclusions;
|
||||
}
|
||||
|
||||
void FVoxelStrateCorpus::Reset()
|
||||
{
|
||||
Entries.Reset();
|
||||
FieldSpreads.Reset();
|
||||
SkippedDefinitions.Reset();
|
||||
SchemaError.Reset();
|
||||
bSchemaValid = true;
|
||||
bClampMetadataAvailableAtBuild = false;
|
||||
}
|
||||
|
||||
void FVoxelStrateCorpus::RebuildSpreads()
|
||||
{
|
||||
FieldSpreads.Reset();
|
||||
SchemaError.Reset();
|
||||
bSchemaValid = true;
|
||||
|
||||
TMap<FString, FProperty*> Properties;
|
||||
VF_CollectGenerationProperties(Properties);
|
||||
|
||||
TArray<FString> ExpectedNames;
|
||||
TSet<FString> ExpectedNameSet;
|
||||
TMap<FString, int32> Indices;
|
||||
|
||||
#define VF_COMPOSER_ADD_LERPF(Name) \
|
||||
ExpectedNames.Add(TEXT(#Name)); \
|
||||
ExpectedNameSet.Add(TEXT(#Name)); \
|
||||
VF_AddSpreadDescriptor(FieldSpreads, Indices, Properties, TEXT(#Name), EVoxelStrateFieldKind::Continuous);
|
||||
#define VF_COMPOSER_ADD_SNAP(Name) \
|
||||
ExpectedNames.Add(TEXT(#Name)); \
|
||||
ExpectedNameSet.Add(TEXT(#Name)); \
|
||||
VF_AddSpreadDescriptor(FieldSpreads, Indices, Properties, TEXT(#Name), \
|
||||
VF_FieldKindFor<decltype(std::declval<FStrateGenerationParams>().Name)>());
|
||||
VF_STRATE_PARAM_FIELDS(VF_COMPOSER_ADD_LERPF, VF_COMPOSER_ADD_SNAP)
|
||||
#undef VF_COMPOSER_ADD_LERPF
|
||||
#undef VF_COMPOSER_ADD_SNAP
|
||||
|
||||
for (const TPair<FString, FProperty*>& Pair : Properties)
|
||||
{
|
||||
if (!ExpectedNameSet.Contains(Pair.Key))
|
||||
{
|
||||
bSchemaValid = false;
|
||||
SchemaError += FString::Printf(
|
||||
TEXT("Reflected FStrateGenerationParams field '%s' is absent from VF_STRATE_PARAM_FIELDS. "),
|
||||
*Pair.Key);
|
||||
}
|
||||
}
|
||||
for (const FVoxelStrateFieldExclusion& Exclusion : GetNonTunableFields())
|
||||
{
|
||||
if (!ExpectedNameSet.Contains(Exclusion.FieldName))
|
||||
{
|
||||
bSchemaValid = false;
|
||||
SchemaError += FString::Printf(
|
||||
TEXT("Explicit exclusion '%s' is absent from VF_STRATE_PARAM_FIELDS. "),
|
||||
*Exclusion.FieldName);
|
||||
}
|
||||
}
|
||||
for (const FString& ExpectedName : ExpectedNames)
|
||||
{
|
||||
const FVoxelStrateFieldSpread* Spread = FindSpread(ExpectedName);
|
||||
if (Spread != nullptr && !Spread->bReflected && !Spread->bExcluded)
|
||||
{
|
||||
bSchemaValid = false;
|
||||
SchemaError += FString::Printf(
|
||||
TEXT("Field '%s' is neither reflected nor explicitly excluded. "), *ExpectedName);
|
||||
}
|
||||
}
|
||||
|
||||
TArray<TArray<double>> Values;
|
||||
Values.SetNum(FieldSpreads.Num());
|
||||
for (const FVoxelStrateCorpusEntry& Entry : Entries)
|
||||
{
|
||||
#define VF_COMPOSER_ACCUM_LERPF(Name) \
|
||||
VF_AccumulateValue(TEXT(#Name), static_cast<double>(Entry.Params.Name), Indices, Values);
|
||||
#define VF_COMPOSER_ACCUM_SNAP(Name) \
|
||||
VF_AccumulateValue(TEXT(#Name), static_cast<double>(Entry.Params.Name), Indices, Values);
|
||||
VF_STRATE_PARAM_FIELDS(VF_COMPOSER_ACCUM_LERPF, VF_COMPOSER_ACCUM_SNAP)
|
||||
#undef VF_COMPOSER_ACCUM_LERPF
|
||||
#undef VF_COMPOSER_ACCUM_SNAP
|
||||
}
|
||||
|
||||
for (int32 FieldIndex = 0; FieldIndex < FieldSpreads.Num(); ++FieldIndex)
|
||||
{
|
||||
FVoxelStrateFieldSpread& Spread = FieldSpreads[FieldIndex];
|
||||
const TArray<double>& Samples = Values[FieldIndex];
|
||||
Spread.SampleCount = Samples.Num();
|
||||
if (Samples.Num() == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Spread.Min = Samples[0];
|
||||
Spread.Max = Samples[0];
|
||||
double Sum = 0.0;
|
||||
for (const double Sample : Samples)
|
||||
{
|
||||
Spread.Min = FMath::Min(Spread.Min, Sample);
|
||||
Spread.Max = FMath::Max(Spread.Max, Sample);
|
||||
Sum += Sample;
|
||||
}
|
||||
Spread.Mean = Sum / (double)Samples.Num();
|
||||
|
||||
double Variance = 0.0;
|
||||
for (const double Sample : Samples)
|
||||
{
|
||||
const double Delta = Sample - Spread.Mean;
|
||||
Variance += Delta * Delta;
|
||||
}
|
||||
Spread.StdDev = FMath::Sqrt(FMath::Max(0.0, Variance / (double)Samples.Num()));
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
bClampMetadataAvailableAtBuild = true;
|
||||
#else
|
||||
bClampMetadataAvailableAtBuild = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool FVoxelStrateCorpus::AddEntry(const FString& SourcePath, const FString& SourceName,
|
||||
ECaveGeneratorType Archetype,
|
||||
const FStrateGenerationParams& Params, float Weight)
|
||||
{
|
||||
if (Archetype != ECaveGeneratorType::TunnelNetwork
|
||||
&& Archetype != ECaveGeneratorType::Underwater)
|
||||
{
|
||||
SkippedDefinitions.Add(FString::Printf(
|
||||
TEXT("%s skipped: archetype %s does not author FStrateGenerationParams."),
|
||||
*SourcePath, VF_GetStrateArchetypeName(Archetype)));
|
||||
return false;
|
||||
}
|
||||
if (!FMath::IsFinite(Weight) || Weight <= 0.0f)
|
||||
{
|
||||
SkippedDefinitions.Add(FString::Printf(
|
||||
TEXT("%s skipped: corpus weight %.9g is not positive and finite."),
|
||||
*SourcePath, Weight));
|
||||
return false;
|
||||
}
|
||||
|
||||
FVoxelStrateCorpusEntry& Entry = Entries.AddDefaulted_GetRef();
|
||||
Entry.SourcePath = SourcePath;
|
||||
Entry.SourceName = SourceName.IsEmpty() ? SourcePath : SourceName;
|
||||
Entry.Archetype = Archetype;
|
||||
Entry.Params = Params;
|
||||
Entry.Weight = Weight;
|
||||
|
||||
Entries.Sort([](const FVoxelStrateCorpusEntry& A, const FVoxelStrateCorpusEntry& B)
|
||||
{
|
||||
if (A.SourcePath != B.SourcePath) { return A.SourcePath < B.SourcePath; }
|
||||
if ((uint8)A.Archetype != (uint8)B.Archetype)
|
||||
{
|
||||
return (uint8)A.Archetype < (uint8)B.Archetype;
|
||||
}
|
||||
return A.SourceName < B.SourceName;
|
||||
});
|
||||
RebuildSpreads();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FVoxelStrateCorpus::LoadFromDefinitions(
|
||||
const TArray<UVoxelStrateDefinition*>& Definitions, FString& OutReport)
|
||||
{
|
||||
Reset();
|
||||
|
||||
TArray<UVoxelStrateDefinition*> SortedDefinitions;
|
||||
for (UVoxelStrateDefinition* Definition : Definitions)
|
||||
{
|
||||
if (Definition != nullptr)
|
||||
{
|
||||
SortedDefinitions.Add(Definition);
|
||||
}
|
||||
}
|
||||
SortedDefinitions.Sort([](const UVoxelStrateDefinition& A, const UVoxelStrateDefinition& B)
|
||||
{
|
||||
return A.GetPathName() < B.GetPathName();
|
||||
});
|
||||
|
||||
TSet<FString> SeenPaths;
|
||||
for (UVoxelStrateDefinition* Definition : SortedDefinitions)
|
||||
{
|
||||
const FString SourcePath = Definition->GetPathName();
|
||||
if (SeenPaths.Contains(SourcePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
SeenPaths.Add(SourcePath);
|
||||
|
||||
const FString SourceName = Definition->StrateName.ToString().IsEmpty()
|
||||
? Definition->GetName()
|
||||
: Definition->StrateName.ToString();
|
||||
|
||||
if (Definition->GeneratorType != ECaveGeneratorType::TunnelNetwork
|
||||
&& Definition->GeneratorType != ECaveGeneratorType::Underwater)
|
||||
{
|
||||
SkippedDefinitions.Add(FString::Printf(
|
||||
TEXT("%s (%s) skipped: %s uses a different authored parameter struct; its ")
|
||||
TEXT("GenerationParams default is not corpus data."),
|
||||
*SourcePath, *SourceName,
|
||||
VF_GetStrateArchetypeName(Definition->GeneratorType)));
|
||||
continue;
|
||||
}
|
||||
|
||||
FVoxelStrateCorpusEntry& Entry = Entries.AddDefaulted_GetRef();
|
||||
Entry.SourcePath = SourcePath;
|
||||
Entry.SourceName = SourceName;
|
||||
Entry.Archetype = Definition->GeneratorType;
|
||||
Entry.Params = Definition->GenerationParams;
|
||||
Entry.Weight = 1.0f; // There is no authoring weight on UVoxelStrateDefinition.
|
||||
}
|
||||
|
||||
Entries.Sort([](const FVoxelStrateCorpusEntry& A, const FVoxelStrateCorpusEntry& B)
|
||||
{
|
||||
if (A.SourcePath != B.SourcePath) { return A.SourcePath < B.SourcePath; }
|
||||
return (uint8)A.Archetype < (uint8)B.Archetype;
|
||||
});
|
||||
RebuildSpreads();
|
||||
|
||||
OutReport = FString::Printf(
|
||||
TEXT("Corpus definitions: %d resolved usable entries, %d skipped, %d fields measured. "),
|
||||
Entries.Num(), SkippedDefinitions.Num(), FieldSpreads.Num());
|
||||
for (const FString& Skipped : SkippedDefinitions)
|
||||
{
|
||||
OutReport += Skipped;
|
||||
OutReport += TEXT(" ");
|
||||
}
|
||||
if (!SchemaError.IsEmpty())
|
||||
{
|
||||
OutReport += TEXT("Schema error: ");
|
||||
OutReport += SchemaError;
|
||||
}
|
||||
return IsValid();
|
||||
}
|
||||
|
||||
bool FVoxelStrateCorpus::LoadFromSettings(const UVoxelSettings* Settings, FString& OutReport)
|
||||
{
|
||||
Reset();
|
||||
if (Settings == nullptr)
|
||||
{
|
||||
OutReport = TEXT("Cannot load strate corpus: settings is null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<TSoftObjectPtr<UVoxelStrateDefinition>> References;
|
||||
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Pair : Settings->FixedStrates)
|
||||
{
|
||||
References.Add(Pair.Value);
|
||||
}
|
||||
References.Append(Settings->StratePool);
|
||||
References.Sort([](const TSoftObjectPtr<UVoxelStrateDefinition>& A,
|
||||
const TSoftObjectPtr<UVoxelStrateDefinition>& B)
|
||||
{
|
||||
return A.ToSoftObjectPath().ToString() < B.ToSoftObjectPath().ToString();
|
||||
});
|
||||
|
||||
TArray<UVoxelStrateDefinition*> Definitions;
|
||||
TSet<FString> SeenPaths;
|
||||
FString ResolutionNotes;
|
||||
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Reference : References)
|
||||
{
|
||||
const FString Path = Reference.ToSoftObjectPath().ToString();
|
||||
if (Path.IsEmpty() || SeenPaths.Contains(Path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
SeenPaths.Add(Path);
|
||||
|
||||
TSoftObjectPtr<UVoxelStrateDefinition> LoadReference = Reference;
|
||||
UVoxelStrateDefinition* Definition = LoadReference.LoadSynchronous();
|
||||
if (Definition == nullptr)
|
||||
{
|
||||
ResolutionNotes += FString::Printf(TEXT("%s could not be resolved. "), *Path);
|
||||
continue;
|
||||
}
|
||||
Definitions.Add(Definition);
|
||||
}
|
||||
|
||||
FString DefinitionReport;
|
||||
const bool bLoaded = LoadFromDefinitions(Definitions, DefinitionReport);
|
||||
OutReport = FString::Printf(
|
||||
TEXT("Settings references: %d unique soft paths, %d resolved. %s%s"),
|
||||
SeenPaths.Num(), Definitions.Num(), *ResolutionNotes, *DefinitionReport);
|
||||
return bLoaded;
|
||||
}
|
||||
|
||||
const FVoxelStrateFieldSpread* FVoxelStrateCorpus::FindSpread(const FString& FieldName) const
|
||||
{
|
||||
for (const FVoxelStrateFieldSpread& Spread : FieldSpreads)
|
||||
{
|
||||
if (Spread.FieldName == FieldName)
|
||||
{
|
||||
return &Spread;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32 FVoxelStrateCorpus::GetContentsHash() const
|
||||
{
|
||||
uint32 Hash = 2166136261U;
|
||||
|
||||
auto HashBytes = [&Hash](const void* Data, SIZE_T Size)
|
||||
{
|
||||
const uint8* Bytes = static_cast<const uint8*>(Data);
|
||||
for (SIZE_T ByteIndex = 0; ByteIndex < Size; ++ByteIndex)
|
||||
{
|
||||
Hash ^= (uint32)Bytes[ByteIndex];
|
||||
Hash *= 16777619U;
|
||||
}
|
||||
};
|
||||
auto HashString = [&HashBytes](const FString& String)
|
||||
{
|
||||
for (int32 CharIndex = 0; CharIndex < String.Len(); ++CharIndex)
|
||||
{
|
||||
const uint32 Code = (uint32)String[CharIndex];
|
||||
HashBytes(&Code, sizeof(Code));
|
||||
}
|
||||
const uint32 Terminator = 0U;
|
||||
HashBytes(&Terminator, sizeof(Terminator));
|
||||
};
|
||||
|
||||
for (const FVoxelStrateCorpusEntry& Entry : Entries)
|
||||
{
|
||||
HashString(Entry.SourcePath);
|
||||
HashString(Entry.SourceName);
|
||||
const uint8 Archetype = (uint8)Entry.Archetype;
|
||||
HashBytes(&Archetype, sizeof(Archetype));
|
||||
HashBytes(&Entry.Weight, sizeof(Entry.Weight));
|
||||
|
||||
#define VF_COMPOSER_HASH_LERPF(Name) \
|
||||
HashBytes(&Entry.Params.Name, sizeof(Entry.Params.Name));
|
||||
#define VF_COMPOSER_HASH_SNAP(Name) \
|
||||
HashBytes(&Entry.Params.Name, sizeof(Entry.Params.Name));
|
||||
VF_STRATE_PARAM_FIELDS(VF_COMPOSER_HASH_LERPF, VF_COMPOSER_HASH_SNAP)
|
||||
#undef VF_COMPOSER_HASH_LERPF
|
||||
#undef VF_COMPOSER_HASH_SNAP
|
||||
}
|
||||
return Hash;
|
||||
}
|
||||
|
||||
const TCHAR* VF_GetStrateArchetypeName(ECaveGeneratorType Archetype)
|
||||
{
|
||||
switch (Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork: return TEXT("TunnelNetwork");
|
||||
case ECaveGeneratorType::FlatPlain: return TEXT("FlatPlain");
|
||||
case ECaveGeneratorType::CrystalChamber: return TEXT("CrystalChamber");
|
||||
case ECaveGeneratorType::Maze: return TEXT("Maze");
|
||||
case ECaveGeneratorType::SurfaceWorld: return TEXT("SurfaceWorld");
|
||||
case ECaveGeneratorType::VerticalShafts: return TEXT("VerticalShafts");
|
||||
case ECaveGeneratorType::FloatingIslands: return TEXT("FloatingIslands");
|
||||
case ECaveGeneratorType::Underwater: return TEXT("Underwater");
|
||||
}
|
||||
return TEXT("Unknown");
|
||||
}
|
||||
|
||||
bool VF_AreStrateParamsBitIdentical(const FStrateGenerationParams& A,
|
||||
const FStrateGenerationParams& B)
|
||||
{
|
||||
#define VF_COMPOSER_COMPARE_LERPF(Name) \
|
||||
if (FMemory::Memcmp(&A.Name, &B.Name, sizeof(A.Name)) != 0) { return false; }
|
||||
#define VF_COMPOSER_COMPARE_SNAP(Name) \
|
||||
if (A.Name != B.Name) { return false; }
|
||||
VF_STRATE_PARAM_FIELDS(VF_COMPOSER_COMPARE_LERPF, VF_COMPOSER_COMPARE_SNAP)
|
||||
#undef VF_COMPOSER_COMPARE_LERPF
|
||||
#undef VF_COMPOSER_COMPARE_SNAP
|
||||
return true;
|
||||
}
|
||||
|
||||
FVoxelStrateRollInfo VF_RollStrateParamsDetailed(const FVoxelStrateCorpus& Corpus,
|
||||
int32 Seed, int32 Index)
|
||||
{
|
||||
const FVoxelStrateRollInfo Result = VF_RollInternal(Corpus, Seed, Index);
|
||||
|
||||
#if DO_CHECK
|
||||
if (Result.bValid)
|
||||
{
|
||||
const FVoxelStrateRollInfo Repeat = VF_RollInternal(Corpus, Seed, Index);
|
||||
checkf(Repeat.bValid && Repeat.Archetype == Result.Archetype
|
||||
&& VF_AreStrateParamsBitIdentical(Repeat.Params, Result.Params)
|
||||
&& Repeat.ParentEntryIndices == Result.ParentEntryIndices
|
||||
&& Repeat.ParentWeights == Result.ParentWeights,
|
||||
TEXT("VF_RollStrateParams lost determinism for the same corpus/seed/index."));
|
||||
}
|
||||
#endif
|
||||
return Result;
|
||||
}
|
||||
|
||||
FStrateGenerationParams VF_RollStrateParams(const FVoxelStrateCorpus& Corpus,
|
||||
int32 Seed, int32 Index)
|
||||
{
|
||||
return VF_RollStrateParamsDetailed(Corpus, Seed, Index).Params;
|
||||
}
|
||||
Reference in new issue
Block a user