Files
VoxelForge/Source/VoxelForge/Private/VoxelSeasonManifest.cpp
T
Fr0zkaandClaude Opus 5 faa62d419e feat(runtime): the composed world can BE the game world
Jahni: "can you not implement those settings? as in, have them be live in
game?" This is that. The composer stops being a lab tool.

THE ARCHITECTURAL LINE, which is the whole design (COMPOSER-NOTES §3.1):
COMPOSITION stays editor-only, EVALUATION becomes runtime. Rolling,
measuring, preview, selection, promotion, the corpus -- none of it reaches
a player's machine. What crosses into the runtime is exactly two things:
reading a season manifest, and materialising a recipe into an op stack.

  UVoxelSeasonAsset      a cookable UPrimaryDataAsset holding the manifest,
                         so a season ships in a packaged game. The JSON
                         stays the reviewable, git-diffable artefact.
  UVoxelSettings::Season  optional. Unset => behaviour is exactly as today.
  GetEffective*()         season seed / spine radius / world radius / season
                         number override the authored values when assigned.
                         The test deliberately sets DIFFERENT authored
                         values, so "the season is authoritative" is proved
                         rather than coincidental.

⭐ MULTIPLAYER DETERMINISM, PROVED: two independent runtime managers built
the same season and compared 393,216 density samples BIT-IDENTICALLY. This
is the property the whole multiplayer model rests on -- replicate seed +
layout + diff, never geometry.

⭐ AND A STALE SEASON IS DETECTABLE, NOT SILENT. Initialize() now returns
bool and an assigned-but-invalid season NEVER falls back to the authored
pool. Falling back would hand a client a world that looks fine and is not
the season -- silent divergence, the worst failure mode in a shared world.
GetCurrentSeasonContentHash() is exposed BlueprintPure to compare at join.

HasComposerRecipeOverride() is gone. It force-classified every custom recipe
as Mixed, which was correct while box verdicts were only proved for
hand-written stacks -- but 8e346a3 made EffectOverBox correct in ISOLATION,
proved on a stack no builder produces. Keeping the force would have cost
tile skipping on every composed world forever.

VF_RollStrateRegionCount moved behind WITH_EDITOR while the partition-seed
helpers stayed runtime: rolling out, evaluation in. Lateral regions remain
gated off.

⚠️ Sol hit its usage limit mid-task, so the edits arrived unvalidated. I
built and ran them myself rather than assume -- and they did not compile:
VoxelForgeComposerSeasonTest.cpp was missing UObject/Package.h, which every
other test picks up transitively through the shared fixture it does not use.
One include; every NewObject/GetTransientPackage in the file was failing.

Editor build clean. SHIPPING BUILD SUCCEEDS -- the invariant that proves no
composer roller is linked into a packaged game. 30 succeeded / 0 failed.

⚠️ UNVERIFIED: whether it looks right on screen. Nothing headless can tell
us that; it needs Jahni to assign a season asset and press play.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
2026-09-05 08:37:01 +02:00

3145 lines
137 KiB
C++

// Offline Tier 4c season composition, manifest persistence, and review artifact.
#include "VoxelSeasonManifest.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "HAL/FileManager.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/SecureHash.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
#include "UObject/EnumProperty.h"
#include "UObject/FieldIterator.h"
#include "UObject/UnrealType.h"
#include "VoxelDensityOpStack.h"
#include "VoxelSettings.h"
#include "VoxelStrateDefinition.h"
#include "VoxelStrateManager.h"
#include "VoxelStratePreview.h"
#include "VoxelCaveMorphology.h"
#include <limits>
namespace VoxelSeasonManifestPrivate
{
enum class EScalarKind : uint8
{
Float,
Integer,
Boolean,
};
struct FScalarValue
{
EScalarKind Kind = EScalarKind::Float;
double Number = 0.0;
bool Boolean = false;
};
bool IsTunnelArchetype(ECaveGeneratorType Archetype)
{
return Archetype == ECaveGeneratorType::TunnelNetwork
|| Archetype == ECaveGeneratorType::Underwater;
}
bool IsSupportedArchetype(ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
case ECaveGeneratorType::Maze:
case ECaveGeneratorType::SurfaceWorld:
case ECaveGeneratorType::VerticalShafts:
case ECaveGeneratorType::FloatingIslands:
case ECaveGeneratorType::Underwater:
return true;
default:
return false;
}
}
UStruct* ParamStruct(ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return FStrateGenerationParams::StaticStruct();
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
return FSlabGenerationParams::StaticStruct();
case ECaveGeneratorType::Maze:
return FMazeGenerationParams::StaticStruct();
case ECaveGeneratorType::SurfaceWorld:
return FSurfaceGenerationParams::StaticStruct();
case ECaveGeneratorType::VerticalShafts:
return FVerticalShaftParams::StaticStruct();
case ECaveGeneratorType::FloatingIslands:
return FFloatingIslandParams::StaticStruct();
default:
return nullptr;
}
}
const void* ParamMemory(const FVoxelStrateArchetypeParams& Params,
ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return &Params.TunnelNetworkParams;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
return &Params.SlabParams;
case ECaveGeneratorType::Maze:
return &Params.MazeParams;
case ECaveGeneratorType::SurfaceWorld:
return &Params.SurfaceParams;
case ECaveGeneratorType::VerticalShafts:
return &Params.VerticalShaftParams;
case ECaveGeneratorType::FloatingIslands:
return &Params.FloatingIslandParams;
default:
return nullptr;
}
}
void* ParamMemory(FVoxelStrateArchetypeParams& Params,
ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return &Params.TunnelNetworkParams;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
return &Params.SlabParams;
case ECaveGeneratorType::Maze:
return &Params.MazeParams;
case ECaveGeneratorType::SurfaceWorld:
return &Params.SurfaceParams;
case ECaveGeneratorType::VerticalShafts:
return &Params.VerticalShaftParams;
case ECaveGeneratorType::FloatingIslands:
return &Params.FloatingIslandParams;
default:
return nullptr;
}
}
template <typename T>
double NativeNumber(T Value)
{
if constexpr (std::is_enum<T>::value)
{
using Underlying = typename std::underlying_type<T>::type;
return static_cast<double>(static_cast<Underlying>(Value));
}
else
{
return static_cast<double>(Value);
}
}
template <typename T>
EScalarKind NativeKind()
{
using ValueType = typename std::remove_cv<
typename std::remove_reference<T>::type>::type;
if (std::is_same<ValueType, bool>::value)
{
return EScalarKind::Boolean;
}
if (std::is_integral<ValueType>::value || std::is_enum<ValueType>::value)
{
return EScalarKind::Integer;
}
return EScalarKind::Float;
}
template <typename T>
typename std::enable_if<!std::is_enum<T>::value, void>::type
AssignNative(T& Target, double Value)
{
Target = static_cast<T>(Value);
}
template <typename T>
typename std::enable_if<std::is_enum<T>::value, void>::type
AssignNative(T& Target, double Value)
{
using Underlying = typename std::underlying_type<T>::type;
Target = static_cast<T>(static_cast<Underlying>(FMath::RoundToInt(Value)));
}
template <typename T>
void AssignNativeScalar(T& Target, const FScalarValue& Value)
{
using ValueType = typename std::remove_cv<T>::type;
if constexpr (std::is_same<ValueType, bool>::value)
{
Target = Value.Boolean;
}
else
{
AssignNative(Target, Value.Number);
}
}
template <typename T>
bool NativeBoolValue(T Value)
{
using ValueType = typename std::remove_cv<T>::type;
if constexpr (std::is_same<ValueType, bool>::value)
{
return Value;
}
else
{
return NativeNumber(Value) != 0.0;
}
}
bool IsScalarProperty(const FProperty* Property)
{
return CastField<FBoolProperty>(Property) != nullptr
|| CastField<FNumericProperty>(Property) != nullptr
|| CastField<FEnumProperty>(Property) != nullptr;
}
bool ReadProperty(const FProperty* Property, const void* Memory, FScalarValue& OutValue)
{
if (Property == nullptr || Memory == nullptr || !IsScalarProperty(Property))
{
return false;
}
const void* ValuePtr = Property->ContainerPtrToValuePtr<void>(Memory);
if (const FBoolProperty* Bool = CastField<FBoolProperty>(Property))
{
OutValue.Kind = EScalarKind::Boolean;
OutValue.Boolean = Bool->GetPropertyValue(ValuePtr);
OutValue.Number = OutValue.Boolean ? 1.0 : 0.0;
return true;
}
const FNumericProperty* Numeric = CastField<FNumericProperty>(Property);
const FNumericProperty* Underlying = Numeric;
if (const FEnumProperty* Enum = CastField<FEnumProperty>(Property))
{
Underlying = Enum->GetUnderlyingProperty();
}
if (Underlying == nullptr)
{
return false;
}
OutValue.Kind = Underlying->IsFloatingPoint()
? EScalarKind::Float : EScalarKind::Integer;
OutValue.Number = Underlying->IsFloatingPoint()
? Underlying->GetFloatingPointPropertyValue(ValuePtr)
: static_cast<double>(Underlying->GetSignedIntPropertyValue(ValuePtr));
return FMath::IsFinite(OutValue.Number);
}
bool WriteProperty(const FProperty* Property, void* Memory, const FScalarValue& Value)
{
if (Property == nullptr || Memory == nullptr || !IsScalarProperty(Property))
{
return false;
}
void* ValuePtr = Property->ContainerPtrToValuePtr<void>(Memory);
if (const FBoolProperty* Bool = CastField<FBoolProperty>(Property))
{
if (Value.Kind != EScalarKind::Boolean)
{
return false;
}
Bool->SetPropertyValue(ValuePtr, Value.Boolean);
return true;
}
const FNumericProperty* Numeric = CastField<FNumericProperty>(Property);
const FNumericProperty* Underlying = Numeric;
if (const FEnumProperty* Enum = CastField<FEnumProperty>(Property))
{
Underlying = Enum->GetUnderlyingProperty();
}
if (Underlying == nullptr || !FMath::IsFinite(Value.Number))
{
return false;
}
if (Underlying->IsFloatingPoint())
{
if (Value.Kind != EScalarKind::Float)
{
return false;
}
Underlying->SetFloatingPointPropertyValue(ValuePtr, Value.Number);
}
else
{
if (Value.Kind != EScalarKind::Integer
|| FMath::FloorToDouble(Value.Number) != Value.Number)
{
return false;
}
Underlying->SetIntPropertyValue(ValuePtr, static_cast<int64>(Value.Number));
}
return true;
}
bool ReadTunnelField(const FStrateGenerationParams& Params,
const FString& FieldName, FScalarValue& OutValue)
{
#define VF_SEASON_READ_TUNNEL(Name) \
if (FieldName == TEXT(#Name)) \
{ \
OutValue.Kind = NativeKind<decltype(Params.Name)>(); \
OutValue.Number = NativeNumber(Params.Name); \
OutValue.Boolean = NativeBoolValue(Params.Name); \
return FMath::IsFinite(OutValue.Number); \
}
VF_STRATE_PARAM_FIELDS(VF_SEASON_READ_TUNNEL, VF_SEASON_READ_TUNNEL)
#undef VF_SEASON_READ_TUNNEL
return false;
}
bool WriteTunnelField(FStrateGenerationParams& Params,
const FString& FieldName, const FScalarValue& Value)
{
#define VF_SEASON_WRITE_TUNNEL(Name) \
if (FieldName == TEXT(#Name)) \
{ \
if (Value.Kind != NativeKind<decltype(Params.Name)>()) return false; \
AssignNativeScalar(Params.Name, Value); \
return true; \
}
VF_STRATE_PARAM_FIELDS(VF_SEASON_WRITE_TUNNEL, VF_SEASON_WRITE_TUNNEL)
#undef VF_SEASON_WRITE_TUNNEL
return false;
}
bool ReadRuntimeField(const void* Memory, ECaveGeneratorType Archetype,
const FString& FieldName, FScalarValue& OutValue)
{
if (Memory == nullptr || (FieldName != TEXT("StrateTopWorldZ")
&& FieldName != TEXT("StrateBottomWorldZ")))
{
return false;
}
const bool bTop = FieldName == TEXT("StrateTopWorldZ");
float Number = 0.0f;
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
Number = bTop ? static_cast<const FStrateGenerationParams*>(Memory)->StrateTopWorldZ
: static_cast<const FStrateGenerationParams*>(Memory)->StrateBottomWorldZ;
break;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
Number = bTop ? static_cast<const FSlabGenerationParams*>(Memory)->StrateTopWorldZ
: static_cast<const FSlabGenerationParams*>(Memory)->StrateBottomWorldZ;
break;
case ECaveGeneratorType::Maze:
Number = bTop ? static_cast<const FMazeGenerationParams*>(Memory)->StrateTopWorldZ
: static_cast<const FMazeGenerationParams*>(Memory)->StrateBottomWorldZ;
break;
case ECaveGeneratorType::SurfaceWorld:
Number = bTop ? static_cast<const FSurfaceGenerationParams*>(Memory)->StrateTopWorldZ
: static_cast<const FSurfaceGenerationParams*>(Memory)->StrateBottomWorldZ;
break;
case ECaveGeneratorType::VerticalShafts:
Number = bTop ? static_cast<const FVerticalShaftParams*>(Memory)->StrateTopWorldZ
: static_cast<const FVerticalShaftParams*>(Memory)->StrateBottomWorldZ;
break;
case ECaveGeneratorType::FloatingIslands:
Number = bTop ? static_cast<const FFloatingIslandParams*>(Memory)->StrateTopWorldZ
: static_cast<const FFloatingIslandParams*>(Memory)->StrateBottomWorldZ;
break;
default:
return false;
}
OutValue.Kind = EScalarKind::Float;
OutValue.Number = Number;
return FMath::IsFinite(Number);
}
bool WriteRuntimeField(void* Memory, ECaveGeneratorType Archetype,
const FString& FieldName, const FScalarValue& Value)
{
if (Memory == nullptr || Value.Kind != EScalarKind::Float
|| !FMath::IsFinite(Value.Number)
|| (FieldName != TEXT("StrateTopWorldZ")
&& FieldName != TEXT("StrateBottomWorldZ")))
{
return false;
}
const bool bTop = FieldName == TEXT("StrateTopWorldZ");
const float Number = static_cast<float>(Value.Number);
if (!FMath::IsFinite(Number))
{
return false;
}
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
if (bTop) static_cast<FStrateGenerationParams*>(Memory)->StrateTopWorldZ = Number;
else static_cast<FStrateGenerationParams*>(Memory)->StrateBottomWorldZ = Number;
return true;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
if (bTop) static_cast<FSlabGenerationParams*>(Memory)->StrateTopWorldZ = Number;
else static_cast<FSlabGenerationParams*>(Memory)->StrateBottomWorldZ = Number;
return true;
case ECaveGeneratorType::Maze:
if (bTop) static_cast<FMazeGenerationParams*>(Memory)->StrateTopWorldZ = Number;
else static_cast<FMazeGenerationParams*>(Memory)->StrateBottomWorldZ = Number;
return true;
case ECaveGeneratorType::SurfaceWorld:
if (bTop) static_cast<FSurfaceGenerationParams*>(Memory)->StrateTopWorldZ = Number;
else static_cast<FSurfaceGenerationParams*>(Memory)->StrateBottomWorldZ = Number;
return true;
case ECaveGeneratorType::VerticalShafts:
if (bTop) static_cast<FVerticalShaftParams*>(Memory)->StrateTopWorldZ = Number;
else static_cast<FVerticalShaftParams*>(Memory)->StrateBottomWorldZ = Number;
return true;
case ECaveGeneratorType::FloatingIslands:
if (bTop) static_cast<FFloatingIslandParams*>(Memory)->StrateTopWorldZ = Number;
else static_cast<FFloatingIslandParams*>(Memory)->StrateBottomWorldZ = Number;
return true;
default:
return false;
}
}
bool ReadNamedField(const void* Memory, ECaveGeneratorType Archetype,
const FString& FieldName, FScalarValue& OutValue)
{
if (FieldName == TEXT("StrateTopWorldZ") || FieldName == TEXT("StrateBottomWorldZ"))
{
return ReadRuntimeField(Memory, Archetype, FieldName, OutValue);
}
if (Memory == nullptr || !IsSupportedArchetype(Archetype))
{
return false;
}
if (IsTunnelArchetype(Archetype)
&& ReadTunnelField(*static_cast<const FStrateGenerationParams*>(Memory),
FieldName, OutValue))
{
return true;
}
UStruct* Struct = ParamStruct(Archetype);
for (TFieldIterator<FProperty> It(Struct, EFieldIteratorFlags::IncludeSuper); It; ++It)
{
if ((*It)->GetName() == FieldName)
{
return ReadProperty(*It, Memory, OutValue);
}
}
return false;
}
bool WriteNamedField(void* Memory, ECaveGeneratorType Archetype,
const FString& FieldName, const FScalarValue& Value)
{
if (FieldName == TEXT("StrateTopWorldZ") || FieldName == TEXT("StrateBottomWorldZ"))
{
return WriteRuntimeField(Memory, Archetype, FieldName, Value);
}
if (Memory == nullptr || !IsSupportedArchetype(Archetype))
{
return false;
}
if (IsTunnelArchetype(Archetype)
&& WriteTunnelField(*static_cast<FStrateGenerationParams*>(Memory),
FieldName, Value))
{
return true;
}
UStruct* Struct = ParamStruct(Archetype);
for (TFieldIterator<FProperty> It(Struct, EFieldIteratorFlags::IncludeSuper); It; ++It)
{
if ((*It)->GetName() == FieldName)
{
return WriteProperty(*It, Memory, Value);
}
}
return false;
}
TArray<FString> FieldNames(ECaveGeneratorType Archetype)
{
TArray<FString> Result;
if (IsTunnelArchetype(Archetype))
{
#define VF_SEASON_ADD_TUNNEL(Name) Result.AddUnique(TEXT(#Name));
VF_STRATE_PARAM_FIELDS(VF_SEASON_ADD_TUNNEL, VF_SEASON_ADD_TUNNEL)
#undef VF_SEASON_ADD_TUNNEL
}
else if (UStruct* Struct = ParamStruct(Archetype))
{
for (TFieldIterator<FProperty> It(Struct, EFieldIteratorFlags::IncludeSuper); It; ++It)
{
if (IsScalarProperty(*It))
{
Result.AddUnique((*It)->GetName());
}
}
}
Result.Add(TEXT("StrateTopWorldZ"));
Result.Add(TEXT("StrateBottomWorldZ"));
Result.Sort([](const FString& A, const FString& B) { return A < B; });
return Result;
}
FString FloatBits(float Value)
{
uint32 Bits = 0;
FMemory::Memcpy(&Bits, &Value, sizeof(Bits));
return FString::Printf(TEXT("0x%08X"), Bits);
}
bool ParseFloatBits(const FString& Text, float& OutValue)
{
FString Digits = Text;
if (Digits.StartsWith(TEXT("0x"), ESearchCase::IgnoreCase))
{
Digits.RightChopInline(2);
}
if (Digits.IsEmpty() || Digits.Len() > 8)
{
return false;
}
uint32 Bits = 0;
for (const TCHAR Ch : Digits)
{
uint32 Nibble = 0;
if (Ch >= TEXT('0') && Ch <= TEXT('9')) Nibble = static_cast<uint32>(Ch - TEXT('0'));
else if (Ch >= TEXT('a') && Ch <= TEXT('f')) Nibble = static_cast<uint32>(Ch - TEXT('a') + 10);
else if (Ch >= TEXT('A') && Ch <= TEXT('F')) Nibble = static_cast<uint32>(Ch - TEXT('A') + 10);
else return false;
Bits = (Bits << 4) | Nibble;
}
FMemory::Memcpy(&OutValue, &Bits, sizeof(OutValue));
return FMath::IsFinite(OutValue);
}
TSharedPtr<FJsonObject> SerializeScalar(const FScalarValue& Value)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
if (Value.Kind == EScalarKind::Boolean)
{
Object->SetStringField(TEXT("kind"), TEXT("bool"));
Object->SetBoolField(TEXT("value"), Value.Boolean);
return Object;
}
if (Value.Kind == EScalarKind::Integer)
{
Object->SetStringField(TEXT("kind"), TEXT("int"));
Object->SetNumberField(TEXT("value"), Value.Number);
return Object;
}
const float FloatValue = static_cast<float>(Value.Number);
Object->SetStringField(TEXT("kind"), TEXT("float"));
Object->SetNumberField(TEXT("value"), static_cast<double>(FloatValue));
Object->SetStringField(TEXT("bits"), FloatBits(FloatValue));
return Object;
}
bool ReadJsonNumber(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, double& OutValue)
{
if (!Object.IsValid()) return false;
const TSharedPtr<FJsonValue>* Found = Object->Values.Find(Name);
if (Found == nullptr || !Found->IsValid() || (*Found)->Type != EJson::Number)
{
return false;
}
OutValue = (*Found)->AsNumber();
return FMath::IsFinite(OutValue);
}
bool ReadJsonInt32(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, int32& OutValue)
{
double Number = 0.0;
if (!ReadJsonNumber(Object, Name, Number)
|| FMath::FloorToDouble(Number) != Number
|| Number < static_cast<double>(TNumericLimits<int32>::Lowest())
|| Number > static_cast<double>(TNumericLimits<int32>::Max()))
{
return false;
}
OutValue = static_cast<int32>(Number);
return true;
}
bool ReadJsonInt64(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, int64& OutValue)
{
double Number = 0.0;
if (!ReadJsonNumber(Object, Name, Number)
|| FMath::FloorToDouble(Number) != Number
|| Number < -9223372036854775807.0
|| Number > 9223372036854775807.0)
{
return false;
}
OutValue = static_cast<int64>(Number);
return static_cast<double>(OutValue) == Number;
}
bool ReadJsonString(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, FString& OutValue)
{
if (!Object.IsValid()) return false;
const TSharedPtr<FJsonValue>* Found = Object->Values.Find(Name);
if (Found == nullptr || !Found->IsValid() || (*Found)->Type != EJson::String)
{
return false;
}
OutValue = (*Found)->AsString();
return true;
}
bool ReadJsonBool(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, bool& OutValue)
{
if (!Object.IsValid()) return false;
const TSharedPtr<FJsonValue>* Found = Object->Values.Find(Name);
if (Found == nullptr || !Found->IsValid() || (*Found)->Type != EJson::Boolean)
{
return false;
}
OutValue = (*Found)->AsBool();
return true;
}
bool ReadJsonObject(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, TSharedPtr<FJsonObject>& OutValue)
{
if (!Object.IsValid()) return false;
const TSharedPtr<FJsonValue>* Found = Object->Values.Find(Name);
if (Found == nullptr || !Found->IsValid() || (*Found)->Type != EJson::Object)
{
return false;
}
OutValue = (*Found)->AsObject();
return OutValue.IsValid();
}
bool ReadJsonArray(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name,
const TArray<TSharedPtr<FJsonValue>>*& OutValue)
{
if (!Object.IsValid()) return false;
const TSharedPtr<FJsonValue>* Found = Object->Values.Find(Name);
if (Found == nullptr || !Found->IsValid() || (*Found)->Type != EJson::Array)
{
return false;
}
OutValue = &(*Found)->AsArray();
return true;
}
bool ReadScalar(const TSharedPtr<FJsonObject>& Object,
const FString& Name, FScalarValue& OutValue)
{
TSharedPtr<FJsonObject> Scalar;
if (!ReadJsonObject(Object, *Name, Scalar)) return false;
FString Kind;
if (!ReadJsonString(Scalar, TEXT("kind"), Kind)) return false;
if (Kind == TEXT("bool"))
{
OutValue.Kind = EScalarKind::Boolean;
return ReadJsonBool(Scalar, TEXT("value"), OutValue.Boolean);
}
if (Kind == TEXT("int"))
{
OutValue.Kind = EScalarKind::Integer;
return ReadJsonNumber(Scalar, TEXT("value"), OutValue.Number)
&& FMath::FloorToDouble(OutValue.Number) == OutValue.Number;
}
if (Kind != TEXT("float")) return false;
FString Bits;
if (!ReadJsonString(Scalar, TEXT("bits"), Bits)) return false;
float FloatValue = 0.0f;
if (!ParseFloatBits(Bits, FloatValue)) return false;
OutValue.Kind = EScalarKind::Float;
OutValue.Number = FloatValue;
return true;
}
TSharedPtr<FJsonObject> SerializeParamsForArchetype(
ECaveGeneratorType Archetype, const FVoxelStrateArchetypeParams& Params)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
const void* Memory = ParamMemory(Params, Archetype);
for (const FString& FieldName : FieldNames(Archetype))
{
FScalarValue Value;
if (ReadNamedField(Memory, Archetype, FieldName, Value))
{
Object->SetObjectField(FieldName, SerializeScalar(Value));
}
}
return Object;
}
bool DeserializeParamsForArchetype(
ECaveGeneratorType Archetype, const TSharedPtr<FJsonObject>& Object,
FVoxelStrateArchetypeParams& OutParams)
{
if (!Object.IsValid()) return false;
void* Memory = ParamMemory(OutParams, Archetype);
if (Memory == nullptr) return false;
for (const FString& FieldName : FieldNames(Archetype))
{
FScalarValue Value;
if (!ReadScalar(Object, FieldName, Value)
|| !WriteNamedField(Memory, Archetype, FieldName, Value))
{
return false;
}
}
return true;
}
TSharedPtr<FJsonObject> SerializeAllParams(const FVoxelStrateArchetypeParams& Params)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetObjectField(TEXT("tunnel_network"),
SerializeParamsForArchetype(ECaveGeneratorType::TunnelNetwork, Params));
Object->SetObjectField(TEXT("slab"),
SerializeParamsForArchetype(ECaveGeneratorType::FlatPlain, Params));
Object->SetObjectField(TEXT("maze"),
SerializeParamsForArchetype(ECaveGeneratorType::Maze, Params));
Object->SetObjectField(TEXT("surface"),
SerializeParamsForArchetype(ECaveGeneratorType::SurfaceWorld, Params));
Object->SetObjectField(TEXT("vertical_shaft"),
SerializeParamsForArchetype(ECaveGeneratorType::VerticalShafts, Params));
Object->SetObjectField(TEXT("floating_island"),
SerializeParamsForArchetype(ECaveGeneratorType::FloatingIslands, Params));
return Object;
}
bool DeserializeAllParams(const TSharedPtr<FJsonObject>& Object,
FVoxelStrateArchetypeParams& OutParams)
{
if (!Object.IsValid()) return false;
TSharedPtr<FJsonObject> Tunnel, Slab, Maze, Surface, Vertical, Floating;
if (!ReadJsonObject(Object, TEXT("tunnel_network"), Tunnel)
|| !ReadJsonObject(Object, TEXT("slab"), Slab)
|| !ReadJsonObject(Object, TEXT("maze"), Maze)
|| !ReadJsonObject(Object, TEXT("surface"), Surface)
|| !ReadJsonObject(Object, TEXT("vertical_shaft"), Vertical)
|| !ReadJsonObject(Object, TEXT("floating_island"), Floating))
{
return false;
}
OutParams = FVoxelStrateArchetypeParams();
return DeserializeParamsForArchetype(ECaveGeneratorType::TunnelNetwork, Tunnel, OutParams)
&& DeserializeParamsForArchetype(ECaveGeneratorType::FlatPlain, Slab, OutParams)
&& DeserializeParamsForArchetype(ECaveGeneratorType::Maze, Maze, OutParams)
&& DeserializeParamsForArchetype(ECaveGeneratorType::SurfaceWorld, Surface, OutParams)
&& DeserializeParamsForArchetype(ECaveGeneratorType::VerticalShafts, Vertical, OutParams)
&& DeserializeParamsForArchetype(ECaveGeneratorType::FloatingIslands, Floating, OutParams);
}
TSharedPtr<FJsonObject> SerializeRecipeEntry(const FVoxelOpRecipeEntry& Entry)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetNumberField(TEXT("op_class"), static_cast<int32>(Entry.OpClass));
Object->SetNumberField(TEXT("param_block"), static_cast<int32>(Entry.ParamBlock));
return Object;
}
bool DeserializeRecipeEntry(const TSharedPtr<FJsonObject>& Object,
FVoxelOpRecipeEntry& OutEntry)
{
int32 OpClass = 0;
int32 ParamBlock = 0;
if (!ReadJsonInt32(Object, TEXT("op_class"), OpClass)
|| !ReadJsonInt32(Object, TEXT("param_block"), ParamBlock)
|| OpClass < static_cast<int32>(EVoxelStrateOpClass::ConstantRockSource)
|| OpClass > static_cast<int32>(EVoxelStrateOpClass::DensityNoiseFillMod)
|| ParamBlock < static_cast<int32>(EVoxelStrateParamBlock::None)
|| ParamBlock > static_cast<int32>(EVoxelStrateParamBlock::FloatingIsland))
{
return false;
}
OutEntry.OpClass = static_cast<EVoxelStrateOpClass>(OpClass);
OutEntry.ParamBlock = static_cast<EVoxelStrateParamBlock>(ParamBlock);
return true;
}
TSharedPtr<FJsonObject> SerializeRecipe(const FVoxelOpStackRecipe& Recipe)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetNumberField(TEXT("root_polarity"), static_cast<int32>(Recipe.RootPolarity));
Object->SetObjectField(TEXT("root"), SerializeRecipeEntry(Recipe.Root));
Object->SetObjectField(TEXT("shape_source"), SerializeRecipeEntry(Recipe.ShapeSource));
Object->SetObjectField(TEXT("conversion"), SerializeRecipeEntry(Recipe.Conversion));
Object->SetNumberField(TEXT("structural_param_block"),
static_cast<int32>(Recipe.StructuralParamBlock));
TArray<TSharedPtr<FJsonValue>> Modifiers;
for (const FVoxelOpRecipeEntry& Entry : Recipe.Modifiers)
{
Modifiers.Add(MakeShared<FJsonValueObject>(SerializeRecipeEntry(Entry)));
}
Object->SetArrayField(TEXT("modifiers"), MoveTemp(Modifiers));
return Object;
}
bool DeserializeRecipe(const TSharedPtr<FJsonObject>& Object,
FVoxelOpStackRecipe& OutRecipe)
{
if (!Object.IsValid()) return false;
int32 RootPolarity = 0;
int32 StructuralBlock = 0;
TSharedPtr<FJsonObject> Root, Shape, Conversion;
const TArray<TSharedPtr<FJsonValue>>* Modifiers = nullptr;
if (!ReadJsonInt32(Object, TEXT("root_polarity"), RootPolarity)
|| !ReadJsonObject(Object, TEXT("root"), Root)
|| !ReadJsonObject(Object, TEXT("shape_source"), Shape)
|| !ReadJsonObject(Object, TEXT("conversion"), Conversion)
|| !ReadJsonInt32(Object, TEXT("structural_param_block"), StructuralBlock)
|| !ReadJsonArray(Object, TEXT("modifiers"), Modifiers)
|| RootPolarity < static_cast<int32>(EVoxelStrateRootPolarity::RockCarve)
|| RootPolarity > static_cast<int32>(EVoxelStrateRootPolarity::VoidFill)
|| StructuralBlock < static_cast<int32>(EVoxelStrateParamBlock::None)
|| StructuralBlock > static_cast<int32>(EVoxelStrateParamBlock::FloatingIsland)
|| Modifiers->Num() < 4 || Modifiers->Num() > 8
|| !DeserializeRecipeEntry(Root, OutRecipe.Root)
|| !DeserializeRecipeEntry(Shape, OutRecipe.ShapeSource)
|| !DeserializeRecipeEntry(Conversion, OutRecipe.Conversion))
{
return false;
}
OutRecipe.RootPolarity = static_cast<EVoxelStrateRootPolarity>(RootPolarity);
OutRecipe.StructuralParamBlock = static_cast<EVoxelStrateParamBlock>(StructuralBlock);
OutRecipe.Modifiers.Reset();
for (const TSharedPtr<FJsonValue>& Value : *Modifiers)
{
if (!Value.IsValid() || Value->Type != EJson::Object) return false;
FVoxelOpRecipeEntry& Entry = OutRecipe.Modifiers.AddDefaulted_GetRef();
if (!DeserializeRecipeEntry(Value->AsObject(), Entry)) return false;
}
return true;
}
void SetMetricFloat(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, float Value)
{
FScalarValue Scalar;
Scalar.Kind = EScalarKind::Float;
Scalar.Number = Value;
Object->SetObjectField(Name, SerializeScalar(Scalar));
}
bool ReadMetricFloat(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, float& OutValue)
{
FScalarValue Scalar;
if (!ReadScalar(Object, Name, Scalar) || Scalar.Kind != EScalarKind::Float)
{
return false;
}
OutValue = static_cast<float>(Scalar.Number);
return FMath::IsFinite(OutValue);
}
TSharedPtr<FJsonObject> SerializeMetrics(const FVoxelStrateMeasuredMetrics& Metrics)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetBoolField(TEXT("valid"), Metrics.bValid);
Object->SetNumberField(TEXT("num_sampled"), static_cast<double>(Metrics.NumSampled));
Object->SetNumberField(TEXT("num_air"), static_cast<double>(Metrics.NumAir));
Object->SetNumberField(TEXT("num_solid"), static_cast<double>(Metrics.NumSolid));
SetMetricFloat(Object, TEXT("air_fraction"), Metrics.AirFraction);
Object->SetNumberField(TEXT("num_air_components"), Metrics.NumAirComponents);
SetMetricFloat(Object, TEXT("largest_component_share"), Metrics.LargestComponentShare);
Object->SetNumberField(TEXT("largest_component_cells"),
static_cast<double>(Metrics.LargestComponentCells));
Object->SetNumberField(TEXT("num_components_at_least_1_pct"),
Metrics.NumComponentsAtLeast1Pct);
SetMetricFloat(Object, TEXT("walkable_fraction"), Metrics.WalkableFraction);
Object->SetNumberField(TEXT("walkable_floor_columns"),
static_cast<double>(Metrics.WalkableFloorColumns));
SetMetricFloat(Object, TEXT("walkable_floor_area_fraction"),
Metrics.WalkableFloorAreaFraction);
Object->SetNumberField(TEXT("num_walkable_surface_components"),
Metrics.NumWalkableSurfaceComponents);
Object->SetNumberField(TEXT("largest_walkable_surface_columns"),
static_cast<double>(Metrics.LargestWalkableSurfaceColumns));
SetMetricFloat(Object, TEXT("largest_walkable_surface_share"),
Metrics.LargestWalkableSurfaceShare);
SetMetricFloat(Object, TEXT("median_feature_scale"), Metrics.MedianFeatureScale);
Object->SetNumberField(TEXT("median_vertical_clearance"), Metrics.MedianVerticalClearance);
Object->SetNumberField(TEXT("resolved_margin_voxels"), Metrics.ResolvedMarginVoxels);
Object->SetNumberField(TEXT("sampled_min_z"), Metrics.SampledMinZ);
Object->SetNumberField(TEXT("sampled_max_z"), Metrics.SampledMaxZ);
Object->SetNumberField(TEXT("sampled_num_x"), Metrics.SampledNumX);
Object->SetNumberField(TEXT("sampled_num_y"), Metrics.SampledNumY);
Object->SetNumberField(TEXT("sampled_num_z"), Metrics.SampledNumZ);
SetMetricFloat(Object, TEXT("sampled_min_x"), Metrics.SampledMinX);
SetMetricFloat(Object, TEXT("sampled_max_x"), Metrics.SampledMaxX);
SetMetricFloat(Object, TEXT("sampled_min_y"), Metrics.SampledMinY);
SetMetricFloat(Object, TEXT("sampled_max_y"), Metrics.SampledMaxY);
TSharedPtr<FJsonObject> Point = MakeShared<FJsonObject>();
SetMetricFloat(Point, TEXT("x"), static_cast<float>(Metrics.LargestComponentPoint.X));
SetMetricFloat(Point, TEXT("y"), static_cast<float>(Metrics.LargestComponentPoint.Y));
SetMetricFloat(Point, TEXT("z"), static_cast<float>(Metrics.LargestComponentPoint.Z));
Object->SetObjectField(TEXT("largest_component_point"), Point);
TArray<TSharedPtr<FJsonValue>> Components;
for (const int64 Cells : Metrics.AirComponentCells)
{
Components.Add(MakeShared<FJsonValueNumber>(static_cast<double>(Cells)));
}
Object->SetArrayField(TEXT("air_component_cells"), MoveTemp(Components));
return Object;
}
bool DeserializeMetrics(const TSharedPtr<FJsonObject>& Object,
FVoxelStrateMeasuredMetrics& OutMetrics)
{
if (!Object.IsValid()) return false;
bool bValid = true;
bValid = ReadJsonBool(Object, TEXT("valid"), OutMetrics.bValid) && bValid;
bValid = ReadJsonInt64(Object, TEXT("num_sampled"), OutMetrics.NumSampled) && bValid;
bValid = ReadJsonInt64(Object, TEXT("num_air"), OutMetrics.NumAir) && bValid;
bValid = ReadJsonInt64(Object, TEXT("num_solid"), OutMetrics.NumSolid) && bValid;
bValid = ReadMetricFloat(Object, TEXT("air_fraction"), OutMetrics.AirFraction) && bValid;
bValid = ReadJsonInt32(Object, TEXT("num_air_components"), OutMetrics.NumAirComponents) && bValid;
bValid = ReadMetricFloat(Object, TEXT("largest_component_share"), OutMetrics.LargestComponentShare) && bValid;
bValid = ReadJsonInt64(Object, TEXT("largest_component_cells"), OutMetrics.LargestComponentCells) && bValid;
bValid = ReadJsonInt32(Object, TEXT("num_components_at_least_1_pct"), OutMetrics.NumComponentsAtLeast1Pct) && bValid;
bValid = ReadMetricFloat(Object, TEXT("walkable_fraction"), OutMetrics.WalkableFraction) && bValid;
if (Object->HasField(TEXT("walkable_floor_columns")))
{
bValid = ReadJsonInt64(Object, TEXT("walkable_floor_columns"),
OutMetrics.WalkableFloorColumns) && bValid;
bValid = ReadMetricFloat(Object, TEXT("walkable_floor_area_fraction"),
OutMetrics.WalkableFloorAreaFraction) && bValid;
bValid = ReadJsonInt32(Object, TEXT("num_walkable_surface_components"),
OutMetrics.NumWalkableSurfaceComponents) && bValid;
bValid = ReadJsonInt64(Object, TEXT("largest_walkable_surface_columns"),
OutMetrics.LargestWalkableSurfaceColumns) && bValid;
bValid = ReadMetricFloat(Object, TEXT("largest_walkable_surface_share"),
OutMetrics.LargestWalkableSurfaceShare) && bValid;
}
bValid = ReadMetricFloat(Object, TEXT("median_feature_scale"), OutMetrics.MedianFeatureScale) && bValid;
bValid = ReadJsonInt32(Object, TEXT("median_vertical_clearance"), OutMetrics.MedianVerticalClearance) && bValid;
bValid = ReadJsonInt32(Object, TEXT("resolved_margin_voxels"), OutMetrics.ResolvedMarginVoxels) && bValid;
bValid = ReadJsonInt32(Object, TEXT("sampled_min_z"), OutMetrics.SampledMinZ) && bValid;
bValid = ReadJsonInt32(Object, TEXT("sampled_max_z"), OutMetrics.SampledMaxZ) && bValid;
bValid = ReadJsonInt32(Object, TEXT("sampled_num_x"), OutMetrics.SampledNumX) && bValid;
bValid = ReadJsonInt32(Object, TEXT("sampled_num_y"), OutMetrics.SampledNumY) && bValid;
bValid = ReadJsonInt32(Object, TEXT("sampled_num_z"), OutMetrics.SampledNumZ) && bValid;
bValid = ReadMetricFloat(Object, TEXT("sampled_min_x"), OutMetrics.SampledMinX) && bValid;
bValid = ReadMetricFloat(Object, TEXT("sampled_max_x"), OutMetrics.SampledMaxX) && bValid;
bValid = ReadMetricFloat(Object, TEXT("sampled_min_y"), OutMetrics.SampledMinY) && bValid;
bValid = ReadMetricFloat(Object, TEXT("sampled_max_y"), OutMetrics.SampledMaxY) && bValid;
TSharedPtr<FJsonObject> Point;
bValid = ReadJsonObject(Object, TEXT("largest_component_point"), Point) && bValid;
if (Point.IsValid())
{
float X = 0.0f;
float Y = 0.0f;
float Z = 0.0f;
bValid = ReadMetricFloat(Point, TEXT("x"), X) && bValid;
bValid = ReadMetricFloat(Point, TEXT("y"), Y) && bValid;
bValid = ReadMetricFloat(Point, TEXT("z"), Z) && bValid;
OutMetrics.LargestComponentPoint = FVector(X, Y, Z);
}
const TArray<TSharedPtr<FJsonValue>>* Components = nullptr;
bValid = ReadJsonArray(Object, TEXT("air_component_cells"), Components) && bValid;
OutMetrics.AirComponentCells.Reset();
if (Components != nullptr)
{
for (const TSharedPtr<FJsonValue>& Value : *Components)
{
if (!Value.IsValid() || Value->Type != EJson::Number)
{
bValid = false;
continue;
}
const double Number = Value->AsNumber();
const int64 Cells = static_cast<int64>(Number);
if (!FMath::IsFinite(Number) || Number < 0.0
|| FMath::FloorToDouble(Number) != Number
|| static_cast<double>(Cells) != Number)
{
bValid = false;
continue;
}
OutMetrics.AirComponentCells.Add(Cells);
}
}
return bValid;
}
const TCHAR* ConnectivityName(EVoxelConnectivityResult Result)
{
switch (Result)
{
case EVoxelConnectivityResult::Connected: return TEXT("Connected");
case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnectedAtThisResolution");
case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid");
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid");
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow");
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudgetExhausted");
}
return TEXT("Unknown");
}
bool ParseConnectivityName(const FString& Name, EVoxelConnectivityResult& OutResult)
{
const EVoxelConnectivityResult Values[] = {
EVoxelConnectivityResult::Connected,
EVoxelConnectivityResult::NotConnectedAtThisResolution,
EVoxelConnectivityResult::StartCellSolid,
EVoxelConnectivityResult::GoalCellSolid,
EVoxelConnectivityResult::OutOfWindow,
EVoxelConnectivityResult::CoarseLiedBudgetExhausted,
};
for (const EVoxelConnectivityResult Value : Values)
{
if (Name == ConnectivityName(Value))
{
OutResult = Value;
return true;
}
}
return false;
}
const TCHAR* ArchetypeName(ECaveGeneratorType Archetype)
{
return VF_GetStrateArchetypeName(Archetype);
}
bool ParseArchetype(const FString& Name, ECaveGeneratorType& OutArchetype)
{
const ECaveGeneratorType Values[] = {
ECaveGeneratorType::TunnelNetwork,
ECaveGeneratorType::FlatPlain,
ECaveGeneratorType::CrystalChamber,
ECaveGeneratorType::Maze,
ECaveGeneratorType::SurfaceWorld,
ECaveGeneratorType::VerticalShafts,
ECaveGeneratorType::FloatingIslands,
ECaveGeneratorType::Underwater,
};
for (const ECaveGeneratorType Value : Values)
{
if (Name == ArchetypeName(Value))
{
OutArchetype = Value;
return true;
}
}
return false;
}
TSharedPtr<FJsonObject> SerializePolicy(const FVoxelSeasonSelectionPolicy& Policy)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetNumberField(TEXT("grounded_fraction"), Policy.GroundedFraction);
Object->SetNumberField(TEXT("outlier_fraction"), Policy.OutlierFraction);
Object->SetNumberField(TEXT("adjacent_similarity_distance_threshold"),
Policy.AdjacentSimilarityDistanceThreshold);
Object->SetNumberField(TEXT("adjacent_similarity_penalty_weight"),
Policy.AdjacentSimilarityPenaltyWeight);
Object->SetNumberField(TEXT("new_archetype_bonus"), Policy.NewArchetypeBonus);
Object->SetNumberField(TEXT("new_recipe_bonus"), Policy.NewRecipeBonus);
Object->SetNumberField(TEXT("boss_slot_interval"), Policy.BossSlotInterval);
Object->SetNumberField(TEXT("minimum_largest_component_share"),
Policy.MinimumLargestComponentShare);
Object->SetNumberField(TEXT("feature_scale_normalization_voxels"),
Policy.FeatureScaleNormalizationVoxels);
Object->SetNumberField(TEXT("clearance_normalization_voxels"),
Policy.ClearanceNormalizationVoxels);
return Object;
}
bool ReadPolicy(const TSharedPtr<FJsonObject>& Object,
FVoxelSeasonSelectionPolicy& OutPolicy)
{
if (!Object.IsValid()) return false;
double Number = 0.0;
bool bValid = true;
if (ReadJsonNumber(Object, TEXT("grounded_fraction"), Number)) OutPolicy.GroundedFraction = static_cast<float>(Number); else bValid = false;
if (ReadJsonNumber(Object, TEXT("outlier_fraction"), Number)) OutPolicy.OutlierFraction = static_cast<float>(Number); else bValid = false;
if (ReadJsonNumber(Object, TEXT("adjacent_similarity_distance_threshold"), Number)) OutPolicy.AdjacentSimilarityDistanceThreshold = Number; else bValid = false;
if (ReadJsonNumber(Object, TEXT("adjacent_similarity_penalty_weight"), Number)) OutPolicy.AdjacentSimilarityPenaltyWeight = Number; else bValid = false;
if (ReadJsonNumber(Object, TEXT("new_archetype_bonus"), Number)) OutPolicy.NewArchetypeBonus = Number; else bValid = false;
if (ReadJsonNumber(Object, TEXT("new_recipe_bonus"), Number)) OutPolicy.NewRecipeBonus = Number; else bValid = false;
if (ReadJsonInt32(Object, TEXT("boss_slot_interval"), OutPolicy.BossSlotInterval) == false) bValid = false;
if (ReadJsonNumber(Object, TEXT("minimum_largest_component_share"), Number)) OutPolicy.MinimumLargestComponentShare = static_cast<float>(Number); else bValid = false;
if (ReadJsonNumber(Object, TEXT("feature_scale_normalization_voxels"), Number)) OutPolicy.FeatureScaleNormalizationVoxels = static_cast<float>(Number); else bValid = false;
if (ReadJsonNumber(Object, TEXT("clearance_normalization_voxels"), Number)) OutPolicy.ClearanceNormalizationVoxels = static_cast<float>(Number); else bValid = false;
return bValid;
}
void SetCommonManifestFields(const TSharedPtr<FJsonObject>& Root,
const FVoxelSeasonManifest& Manifest)
{
Root->SetStringField(TEXT("format"), TEXT("VoxelForgeSeasonManifest"));
Root->SetBoolField(TEXT("valid"), Manifest.bValid);
Root->SetNumberField(TEXT("schema_version"), Manifest.SchemaVersion);
Root->SetNumberField(TEXT("season"), Manifest.Season);
Root->SetNumberField(TEXT("seed"), Manifest.Seed);
Root->SetNumberField(TEXT("input_corpus_hash"), static_cast<double>(Manifest.InputCorpusHash));
Root->SetNumberField(TEXT("candidate_count"), Manifest.CandidateCount);
Root->SetNumberField(TEXT("survivor_count"), Manifest.SurvivorCount);
Root->SetNumberField(TEXT("selected_count"), Manifest.SelectedCount);
Root->SetNumberField(TEXT("rejected_count"), Manifest.RejectedCount);
Root->SetNumberField(TEXT("grounded_selected_count"), Manifest.GroundedSelectedCount);
Root->SetNumberField(TEXT("outlier_selected_count"), Manifest.OutlierSelectedCount);
Root->SetNumberField(TEXT("fixed_selected_count"), Manifest.FixedSelectedCount);
Root->SetNumberField(TEXT("total_strates"), Manifest.TotalStrates);
Root->SetNumberField(TEXT("inter_strate_gap_chunks"), Manifest.InterStrateGapChunks);
SetMetricFloat(Root, TEXT("origin_spine_radius"), Manifest.OriginSpineRadius);
SetMetricFloat(Root, TEXT("world_radius_voxels"), Manifest.WorldRadiusVoxels);
Root->SetObjectField(TEXT("selection_policy"), SerializePolicy(Manifest.SelectionPolicy));
Root->SetStringField(TEXT("policy_note"), Manifest.PolicyNote);
}
TSharedPtr<FJsonObject> SerializeRegionManifest(
const FVoxelStrateRegionManifest& Regions)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetBoolField(TEXT("valid"), Regions.bValid);
Object->SetStringField(TEXT("failure_reason"), Regions.FailureReason);
Object->SetNumberField(TEXT("seed"), Regions.Seed);
Object->SetNumberField(TEXT("strate_index"), Regions.StrateIndex);
Object->SetNumberField(TEXT("region_count"), Regions.RegionCount);
Object->SetNumberField(TEXT("partition_seed"),
static_cast<double>(Regions.PartitionSeed));
SetMetricFloat(Object, TEXT("lattice_cell_size"), Regions.LatticeCellSize);
SetMetricFloat(Object, TEXT("blend_width"), Regions.BlendWidth);
Object->SetBoolField(TEXT("has_global_structural_params"),
Regions.bHasGlobalStructuralParams);
Object->SetNumberField(TEXT("structural_param_block"),
static_cast<int32>(Regions.StructuralParamBlock));
SetMetricFloat(Object, TEXT("strate_top_world_z"), Regions.StrateTopWorldZ);
SetMetricFloat(Object, TEXT("strate_bottom_world_z"), Regions.StrateBottomWorldZ);
SetMetricFloat(Object, TEXT("boundary_seal_thickness"),
Regions.BoundarySealThickness);
SetMetricFloat(Object, TEXT("base_density"), Regions.BaseDensity);
TArray<TSharedPtr<FJsonValue>> RegionValues;
for (const FVoxelStrateRegion& Region : Regions.Regions)
{
TSharedPtr<FJsonObject> RegionObject = MakeShared<FJsonObject>();
RegionObject->SetNumberField(TEXT("region_index"), Region.RegionIndex);
RegionObject->SetNumberField(TEXT("seed"), Region.Seed);
RegionObject->SetStringField(TEXT("archetype"), ArchetypeName(Region.Archetype));
RegionObject->SetNumberField(TEXT("archetype_id"),
static_cast<int32>(Region.Archetype));
RegionObject->SetBoolField(TEXT("uses_recipe"), Region.bUsesRecipe);
RegionObject->SetObjectField(TEXT("recipe"), SerializeRecipe(Region.Recipe));
RegionObject->SetObjectField(TEXT("parameters"),
SerializeAllParams(Region.ArchetypeParams));
RegionValues.Add(MakeShared<FJsonValueObject>(MoveTemp(RegionObject)));
}
Object->SetArrayField(TEXT("regions"), MoveTemp(RegionValues));
return Object;
}
bool ReadJsonUInt32(const TSharedPtr<FJsonObject>& Object,
const TCHAR* Name, uint32& OutValue)
{
double Number = 0.0;
if (!ReadJsonNumber(Object, Name, Number)
|| !FMath::IsFinite(Number)
|| Number < 0.0 || Number > 4294967295.0
|| FMath::FloorToDouble(Number) != Number)
{
return false;
}
OutValue = static_cast<uint32>(Number);
return true;
}
bool DeserializeRegionManifest(const TSharedPtr<FJsonObject>& Object,
FVoxelStrateRegionManifest& OutRegions)
{
if (!Object.IsValid()) return false;
OutRegions = FVoxelStrateRegionManifest();
bool JsonValid = false;
int32 StructuralBlock = 0;
bool bFieldsValid = ReadJsonBool(Object, TEXT("valid"), JsonValid)
&& ReadJsonString(Object, TEXT("failure_reason"), OutRegions.FailureReason)
&& ReadJsonInt32(Object, TEXT("seed"), OutRegions.Seed)
&& ReadJsonInt32(Object, TEXT("strate_index"), OutRegions.StrateIndex)
&& ReadJsonInt32(Object, TEXT("region_count"), OutRegions.RegionCount)
&& ReadJsonUInt32(Object, TEXT("partition_seed"), OutRegions.PartitionSeed)
&& ReadMetricFloat(Object, TEXT("lattice_cell_size"), OutRegions.LatticeCellSize)
&& ReadMetricFloat(Object, TEXT("blend_width"), OutRegions.BlendWidth)
&& ReadJsonBool(Object, TEXT("has_global_structural_params"),
OutRegions.bHasGlobalStructuralParams)
&& ReadJsonInt32(Object, TEXT("structural_param_block"), StructuralBlock)
&& ReadMetricFloat(Object, TEXT("strate_top_world_z"), OutRegions.StrateTopWorldZ)
&& ReadMetricFloat(Object, TEXT("strate_bottom_world_z"),
OutRegions.StrateBottomWorldZ)
&& ReadMetricFloat(Object, TEXT("boundary_seal_thickness"),
OutRegions.BoundarySealThickness)
&& ReadMetricFloat(Object, TEXT("base_density"), OutRegions.BaseDensity);
if (StructuralBlock < static_cast<int32>(EVoxelStrateParamBlock::None)
|| StructuralBlock > static_cast<int32>(EVoxelStrateParamBlock::FloatingIsland))
{
bFieldsValid = false;
}
OutRegions.StructuralParamBlock = static_cast<EVoxelStrateParamBlock>(StructuralBlock);
const TArray<TSharedPtr<FJsonValue>>* RegionValues = nullptr;
bFieldsValid = ReadJsonArray(Object, TEXT("regions"), RegionValues) && bFieldsValid;
if (RegionValues == nullptr
|| OutRegions.RegionCount < 1 || OutRegions.RegionCount > 3
|| RegionValues->Num() != OutRegions.RegionCount)
{
bFieldsValid = false;
}
OutRegions.Regions.Reset();
if (RegionValues != nullptr)
{
OutRegions.Regions.Reserve(RegionValues->Num());
for (int32 Index = 0; Index < RegionValues->Num(); ++Index)
{
const TSharedPtr<FJsonValue>& Value = (*RegionValues)[Index];
if (!Value.IsValid() || Value->Type != EJson::Object)
{
bFieldsValid = false;
continue;
}
const TSharedPtr<FJsonObject> RegionObject = Value->AsObject();
FVoxelStrateRegion& Region = OutRegions.Regions.AddDefaulted_GetRef();
FString ArchetypeText;
int32 ArchetypeId = -1;
TSharedPtr<FJsonObject> Recipe, Params;
bFieldsValid = ReadJsonInt32(RegionObject, TEXT("region_index"),
Region.RegionIndex) && bFieldsValid;
bFieldsValid = ReadJsonInt32(RegionObject, TEXT("seed"), Region.Seed)
&& bFieldsValid;
bFieldsValid = ReadJsonString(RegionObject, TEXT("archetype"), ArchetypeText)
&& bFieldsValid;
bFieldsValid = ReadJsonInt32(RegionObject, TEXT("archetype_id"), ArchetypeId)
&& bFieldsValid;
bFieldsValid = ReadJsonBool(RegionObject, TEXT("uses_recipe"),
Region.bUsesRecipe) && bFieldsValid;
bFieldsValid = ReadJsonObject(RegionObject, TEXT("recipe"), Recipe)
&& bFieldsValid;
bFieldsValid = ReadJsonObject(RegionObject, TEXT("parameters"), Params)
&& bFieldsValid;
bFieldsValid = ParseArchetype(ArchetypeText, Region.Archetype)
&& bFieldsValid;
bFieldsValid = Region.RegionIndex == Index && bFieldsValid;
bFieldsValid = ArchetypeId == static_cast<int32>(Region.Archetype)
&& bFieldsValid;
if (Region.bUsesRecipe)
{
bFieldsValid = DeserializeRecipe(Recipe, Region.Recipe) && bFieldsValid;
}
bFieldsValid = DeserializeAllParams(Params, Region.ArchetypeParams)
&& bFieldsValid;
}
}
OutRegions.bValid = JsonValid;
return bFieldsValid && OutRegions.IsValid();
}
TSharedPtr<FJsonObject> SerializeStrate(const FVoxelSeasonStrate& Strate)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetNumberField(TEXT("depth_index"), Strate.DepthIndex);
Object->SetNumberField(TEXT("candidate_index"), Strate.CandidateIndex);
Object->SetNumberField(TEXT("seed"), Strate.Seed);
Object->SetNumberField(TEXT("height_in_chunks"), Strate.HeightInChunks);
Object->SetNumberField(TEXT("top_world_z"), Strate.TopWorldZ);
Object->SetNumberField(TEXT("bottom_world_z"), Strate.BottomWorldZ);
Object->SetStringField(TEXT("archetype"), ArchetypeName(Strate.Archetype));
Object->SetNumberField(TEXT("archetype_id"), static_cast<int32>(Strate.Archetype));
Object->SetBoolField(TEXT("uses_recipe"), Strate.bUsesRecipe);
Object->SetBoolField(TEXT("uses_regions"), Strate.bUsesRegions);
Object->SetObjectField(TEXT("recipe"), SerializeRecipe(Strate.Recipe));
Object->SetObjectField(TEXT("parameters"), SerializeAllParams(Strate.Params));
// Keep old/single-region artifacts compact and on the exact legacy rebuild path. A
// multi-region strate carries the complete parent manifest because that is its runtime
// identity; an invalid default Regions object must not make fixed legacy entries unloadable.
if (Strate.bUsesRegions)
{
Object->SetObjectField(TEXT("regions"), SerializeRegionManifest(Strate.Regions));
}
Object->SetObjectField(TEXT("measured_metrics"), SerializeMetrics(Strate.Metrics));
Object->SetNumberField(TEXT("distance_from_corpus_centroid"), Strate.DistanceFromCorpusCentroid);
Object->SetStringField(TEXT("selection_reason"), Strate.SelectionReason);
Object->SetStringField(TEXT("source_definition_path"), Strate.SourceDefinitionPath);
Object->SetBoolField(TEXT("grounded"), Strate.bGrounded);
Object->SetBoolField(TEXT("outlier"), Strate.bOutlier);
Object->SetBoolField(TEXT("boss_slot"), Strate.bBossSlot);
Object->SetBoolField(TEXT("fixed"), Strate.bFixed);
Object->SetBoolField(TEXT("passed_non_vacuous"), Strate.bPassedNonVacuous);
Object->SetBoolField(TEXT("passed_largest_component"), Strate.bPassedLargestComponent);
Object->SetBoolField(TEXT("passed_primordial_law"), Strate.bPassedPrimordialLaw);
Object->SetStringField(TEXT("primordial_law_result"), ConnectivityName(Strate.PrimordialLawResult));
return Object;
}
bool DeserializeStrate(const TSharedPtr<FJsonObject>& Object,
FVoxelSeasonStrate& OutStrate)
{
if (!Object.IsValid()) return false;
FString ArchetypeText, Reason, SourcePath, LawText;
int32 ArchetypeId = -1;
bool bValid = ReadJsonInt32(Object, TEXT("depth_index"), OutStrate.DepthIndex)
&& ReadJsonInt32(Object, TEXT("candidate_index"), OutStrate.CandidateIndex)
&& ReadJsonInt32(Object, TEXT("seed"), OutStrate.Seed)
&& ReadJsonInt32(Object, TEXT("height_in_chunks"), OutStrate.HeightInChunks)
&& ReadJsonInt32(Object, TEXT("top_world_z"), OutStrate.TopWorldZ)
&& ReadJsonInt32(Object, TEXT("bottom_world_z"), OutStrate.BottomWorldZ)
&& ReadJsonString(Object, TEXT("archetype"), ArchetypeText)
&& ReadJsonInt32(Object, TEXT("archetype_id"), ArchetypeId)
&& ReadJsonBool(Object, TEXT("uses_recipe"), OutStrate.bUsesRecipe)
&& ReadJsonString(Object, TEXT("selection_reason"), Reason)
&& ReadJsonString(Object, TEXT("source_definition_path"), SourcePath)
&& ReadJsonBool(Object, TEXT("grounded"), OutStrate.bGrounded)
&& ReadJsonBool(Object, TEXT("outlier"), OutStrate.bOutlier)
&& ReadJsonBool(Object, TEXT("boss_slot"), OutStrate.bBossSlot)
&& ReadJsonBool(Object, TEXT("fixed"), OutStrate.bFixed)
&& ReadJsonBool(Object, TEXT("passed_non_vacuous"), OutStrate.bPassedNonVacuous)
&& ReadJsonBool(Object, TEXT("passed_largest_component"), OutStrate.bPassedLargestComponent)
&& ReadJsonBool(Object, TEXT("passed_primordial_law"), OutStrate.bPassedPrimordialLaw)
&& ReadJsonString(Object, TEXT("primordial_law_result"), LawText);
double Distance = 0.0;
bValid = ReadJsonNumber(Object, TEXT("distance_from_corpus_centroid"), Distance) && bValid;
OutStrate.DistanceFromCorpusCentroid = Distance;
OutStrate.SelectionReason = Reason;
OutStrate.SourceDefinitionPath = SourcePath;
TSharedPtr<FJsonObject> Recipe, Params, Metrics, RegionsObject;
bValid = ReadJsonObject(Object, TEXT("recipe"), Recipe) && bValid;
bValid = ReadJsonObject(Object, TEXT("parameters"), Params) && bValid;
bValid = ReadJsonObject(Object, TEXT("measured_metrics"), Metrics) && bValid;
const TSharedPtr<FJsonValue>* RegionsValue = Object->Values.Find(TEXT("regions"));
if (RegionsValue != nullptr)
{
bValid = ReadJsonObject(Object, TEXT("regions"), RegionsObject) && bValid;
if (RegionsObject.IsValid())
{
bValid = DeserializeRegionManifest(RegionsObject, OutStrate.Regions)
&& bValid;
}
}
const TSharedPtr<FJsonValue>* UsesRegionsValue =
Object->Values.Find(TEXT("uses_regions"));
if (UsesRegionsValue != nullptr)
{
bValid = ReadJsonBool(Object, TEXT("uses_regions"), OutStrate.bUsesRegions)
&& bValid;
}
else
{
// Schema-1 manifests predating lateral regions simply take the old path. A region
// object is optional for backward compatibility, and never implicitly changes that
// path without the explicit opt-in bit.
OutStrate.bUsesRegions = false;
}
bValid = ParseArchetype(ArchetypeText, OutStrate.Archetype) && bValid;
bValid = ArchetypeId == static_cast<int32>(OutStrate.Archetype) && bValid;
bValid = ParseConnectivityName(LawText, OutStrate.PrimordialLawResult) && bValid;
if (OutStrate.bUsesRecipe)
{
bValid = DeserializeRecipe(Recipe, OutStrate.Recipe) && bValid;
}
bValid = DeserializeAllParams(Params, OutStrate.Params) && bValid;
bValid = DeserializeMetrics(Metrics, OutStrate.Metrics) && bValid;
if (OutStrate.bUsesRegions
&& (!OutStrate.Regions.IsValid() || OutStrate.Regions.RegionCount <= 1))
{
bValid = false;
}
return bValid;
}
bool SaveJson(const FString& FilePath, const TSharedPtr<FJsonObject>& Root,
FString& OutReport)
{
const FString Directory = FPaths::GetPath(FilePath);
if (!Directory.IsEmpty() && !IFileManager::Get().MakeDirectory(*Directory, true))
{
OutReport = FString::Printf(TEXT("could not create manifest directory %s"), *Directory);
return false;
}
FString JsonText;
TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer =
TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&JsonText);
if (!FJsonSerializer::Serialize(Root.ToSharedRef(), Writer) || !Writer->Close()
|| !FFileHelper::SaveStringToFile(JsonText, *FilePath))
{
OutReport = FString::Printf(TEXT("could not write season manifest %s"), *FilePath);
return false;
}
return true;
}
bool LoadJson(const FString& FilePath, TSharedPtr<FJsonObject>& OutRoot,
FString& OutReport, FString* OutJsonText = nullptr)
{
FString JsonText;
if (!FFileHelper::LoadFileToString(JsonText, *FilePath))
{
OutReport = FString::Printf(TEXT("could not read season manifest %s"), *FilePath);
return false;
}
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonText);
if (!FJsonSerializer::Deserialize(Reader, OutRoot) || !OutRoot.IsValid())
{
OutReport = FString::Printf(TEXT("invalid JSON in season manifest %s"), *FilePath);
return false;
}
if (OutJsonText != nullptr)
{
*OutJsonText = MoveTemp(JsonText);
}
return true;
}
}
const TCHAR* VF_GetVoxelSeasonSelectionReasonName(EVoxelSeasonSelectionReason Reason)
{
switch (Reason)
{
case EVoxelSeasonSelectionReason::Grounded: return TEXT("grounded");
case EVoxelSeasonSelectionReason::Outlier: return TEXT("outlier");
case EVoxelSeasonSelectionReason::Fixed: return TEXT("fixed");
}
return TEXT("unknown");
}
namespace
{
FString SerializeVoxelSeasonManifestInternal(
const FVoxelSeasonManifest& Manifest, bool bIncludeContentHash)
{
using namespace VoxelSeasonManifestPrivate;
TSharedPtr<FJsonObject> Root = MakeShared<FJsonObject>();
SetCommonManifestFields(Root, Manifest);
TArray<TSharedPtr<FJsonValue>> Strates;
for (const FVoxelSeasonStrate& Strate : Manifest.Strates)
{
Strates.Add(MakeShared<FJsonValueObject>(SerializeStrate(Strate)));
}
Root->SetArrayField(TEXT("strates"), MoveTemp(Strates));
TArray<TSharedPtr<FJsonValue>> Rejections;
for (const FVoxelSeasonRejectionCount& Rejection : Manifest.RejectionCounts)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetStringField(TEXT("reason"), Rejection.Reason);
Object->SetNumberField(TEXT("count"), Rejection.Count);
Rejections.Add(MakeShared<FJsonValueObject>(Object));
}
Root->SetArrayField(TEXT("rejection_counts"), MoveTemp(Rejections));
if (!Manifest.CandidateAudit.IsEmpty())
{
TArray<TSharedPtr<FJsonValue>> Audit;
for (const FVoxelSeasonCandidateAudit& Row : Manifest.CandidateAudit)
{
TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
Object->SetNumberField(TEXT("candidate_index"), Row.CandidateIndex);
Object->SetNumberField(TEXT("seed"), Row.Seed);
Object->SetStringField(TEXT("archetype"), ArchetypeName(Row.Archetype));
Object->SetNumberField(TEXT("recipe_hash"), static_cast<double>(Row.RecipeHash));
Object->SetNumberField(TEXT("distance_from_corpus_centroid"), Row.DistanceFromCorpusCentroid);
Object->SetBoolField(TEXT("passed_hard_gates"), Row.bPassedHardGates);
Object->SetBoolField(TEXT("selected"), Row.bSelected);
Object->SetStringField(TEXT("rejection_reason"), Row.RejectionReason);
Audit.Add(MakeShared<FJsonValueObject>(Object));
}
Root->SetArrayField(TEXT("candidate_audit"), MoveTemp(Audit));
}
if (bIncludeContentHash)
{
const FString Payload = SerializeVoxelSeasonManifestInternal(Manifest, false);
if (Payload.IsEmpty()) return FString();
FTCHARToUTF8 Utf8(*Payload);
uint8 Digest[FSHA1::DigestSize];
FSHA1::HashBuffer(Utf8.Get(), Utf8.Length(), Digest);
Root->SetStringField(TEXT("content_hash"), BytesToHex(Digest, FSHA1::DigestSize));
}
FString JsonText;
TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer =
TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&JsonText);
if (!FJsonSerializer::Serialize(Root.ToSharedRef(), Writer) || !Writer->Close())
{
return FString();
}
return JsonText;
}
}
FString VF_ComputeVoxelSeasonManifestContentHash(const FVoxelSeasonManifest& Manifest)
{
const FString Payload = SerializeVoxelSeasonManifestInternal(Manifest, false);
if (Payload.IsEmpty()) return FString();
FTCHARToUTF8 Utf8(*Payload);
uint8 Digest[FSHA1::DigestSize];
FSHA1::HashBuffer(Utf8.Get(), Utf8.Length(), Digest);
return BytesToHex(Digest, FSHA1::DigestSize);
}
FString VF_SerializeVoxelSeasonManifest(const FVoxelSeasonManifest& Manifest)
{
return SerializeVoxelSeasonManifestInternal(Manifest, true);
}
bool VF_SaveVoxelSeasonManifest(const FString& ManifestPath,
const FVoxelSeasonManifest& Manifest,
FString& OutReport)
{
if (ManifestPath.IsEmpty())
{
OutReport = TEXT("season manifest path is empty");
return false;
}
const FString JsonText = VF_SerializeVoxelSeasonManifest(Manifest);
if (JsonText.IsEmpty())
{
OutReport = TEXT("could not serialize season manifest");
return false;
}
const FString Directory = FPaths::GetPath(ManifestPath);
if (!Directory.IsEmpty() && !IFileManager::Get().MakeDirectory(*Directory, true))
{
OutReport = FString::Printf(TEXT("could not create manifest directory %s"), *Directory);
return false;
}
if (!FFileHelper::SaveStringToFile(JsonText, *ManifestPath,
FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
OutReport = FString::Printf(TEXT("could not write season manifest %s"), *ManifestPath);
return false;
}
OutReport = FString::Printf(TEXT("saved season manifest to %s"), *ManifestPath);
return true;
}
bool VF_DeserializeVoxelSeasonManifest(const FString& ManifestJson,
FVoxelSeasonManifest& OutManifest,
FString& OutReport)
{
using namespace VoxelSeasonManifestPrivate;
OutManifest = FVoxelSeasonManifest();
OutReport.Reset();
if (ManifestJson.IsEmpty())
{
OutReport = TEXT("season manifest JSON is empty");
OutManifest.Error = OutReport;
return false;
}
TSharedPtr<FJsonObject> Root;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ManifestJson);
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
{
OutReport = TEXT("season manifest JSON is malformed");
OutManifest.Error = OutReport;
return false;
}
FString Format;
int32 SchemaVersion = 0;
bool bValid = false;
if (!ReadJsonString(Root, TEXT("format"), Format)
|| Format != TEXT("VoxelForgeSeasonManifest")
|| !ReadJsonBool(Root, TEXT("valid"), bValid)
|| !ReadJsonInt32(Root, TEXT("schema_version"), SchemaVersion))
{
OutReport = TEXT("season manifest header is missing or malformed");
OutManifest.Error = OutReport;
return false;
}
if (SchemaVersion != FVoxelSeasonManifest::CurrentSchemaVersion)
{
OutReport = FString::Printf(TEXT("unsupported season manifest schema %d (expected %d)"),
SchemaVersion, FVoxelSeasonManifest::CurrentSchemaVersion);
OutManifest.Error = OutReport;
return false;
}
bool bFieldsValid = true;
OutManifest.bValid = bValid;
OutManifest.SchemaVersion = SchemaVersion;
bFieldsValid = ReadJsonString(Root, TEXT("content_hash"), OutManifest.ContentHash)
&& bFieldsValid;
bFieldsValid = ReadJsonInt32(Root, TEXT("season"), OutManifest.Season) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Root, TEXT("seed"), OutManifest.Seed) && bFieldsValid;
double CorpusHash = 0.0;
bFieldsValid = ReadJsonNumber(Root, TEXT("input_corpus_hash"), CorpusHash) && bFieldsValid;
if (CorpusHash < 0.0 || CorpusHash > 4294967295.0
|| FMath::FloorToDouble(CorpusHash) != CorpusHash)
{
bFieldsValid = false;
}
OutManifest.InputCorpusHash = static_cast<uint32>(CorpusHash);
bFieldsValid = ReadJsonInt32(Root, TEXT("candidate_count"), OutManifest.CandidateCount) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Root, TEXT("survivor_count"), OutManifest.SurvivorCount) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Root, TEXT("selected_count"), OutManifest.SelectedCount) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Root, TEXT("rejected_count"), OutManifest.RejectedCount) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Root, TEXT("grounded_selected_count"), OutManifest.GroundedSelectedCount) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Root, TEXT("outlier_selected_count"), OutManifest.OutlierSelectedCount) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Root, TEXT("fixed_selected_count"), OutManifest.FixedSelectedCount) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Root, TEXT("total_strates"), OutManifest.TotalStrates) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Root, TEXT("inter_strate_gap_chunks"), OutManifest.InterStrateGapChunks) && bFieldsValid;
bFieldsValid = ReadMetricFloat(Root, TEXT("origin_spine_radius"), OutManifest.OriginSpineRadius) && bFieldsValid;
bFieldsValid = ReadMetricFloat(Root, TEXT("world_radius_voxels"), OutManifest.WorldRadiusVoxels) && bFieldsValid;
TSharedPtr<FJsonObject> Policy;
bFieldsValid = ReadJsonObject(Root, TEXT("selection_policy"), Policy) && bFieldsValid;
if (Policy.IsValid())
{
bFieldsValid = ReadPolicy(Policy, OutManifest.SelectionPolicy) && bFieldsValid;
}
bFieldsValid = ReadJsonString(Root, TEXT("policy_note"), OutManifest.PolicyNote) && bFieldsValid;
const TArray<TSharedPtr<FJsonValue>>* StrateValues = nullptr;
const TArray<TSharedPtr<FJsonValue>>* RejectionValues = nullptr;
bFieldsValid = ReadJsonArray(Root, TEXT("strates"), StrateValues) && bFieldsValid;
bFieldsValid = ReadJsonArray(Root, TEXT("rejection_counts"), RejectionValues) && bFieldsValid;
if (StrateValues != nullptr)
{
OutManifest.Strates.Reset();
OutManifest.Strates.Reserve(StrateValues->Num());
for (const TSharedPtr<FJsonValue>& Value : *StrateValues)
{
if (!Value.IsValid() || Value->Type != EJson::Object)
{
bFieldsValid = false;
continue;
}
FVoxelSeasonStrate& Strate = OutManifest.Strates.AddDefaulted_GetRef();
if (!DeserializeStrate(Value->AsObject(), Strate))
{
bFieldsValid = false;
}
}
}
if (RejectionValues != nullptr)
{
OutManifest.RejectionCounts.Reset();
OutManifest.RejectionCounts.Reserve(RejectionValues->Num());
for (const TSharedPtr<FJsonValue>& Value : *RejectionValues)
{
if (!Value.IsValid() || Value->Type != EJson::Object)
{
bFieldsValid = false;
continue;
}
TSharedPtr<FJsonObject> Object = Value->AsObject();
FVoxelSeasonRejectionCount& Row = OutManifest.RejectionCounts.AddDefaulted_GetRef();
bFieldsValid = ReadJsonString(Object, TEXT("reason"), Row.Reason) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Object, TEXT("count"), Row.Count) && bFieldsValid;
}
}
const TArray<TSharedPtr<FJsonValue>>* AuditValues = nullptr;
if (ReadJsonArray(Root, TEXT("candidate_audit"), AuditValues))
{
OutManifest.CandidateAudit.Reset();
OutManifest.CandidateAudit.Reserve(AuditValues->Num());
for (const TSharedPtr<FJsonValue>& Value : *AuditValues)
{
if (!Value.IsValid() || Value->Type != EJson::Object)
{
bFieldsValid = false;
continue;
}
TSharedPtr<FJsonObject> Object = Value->AsObject();
FVoxelSeasonCandidateAudit& Row = OutManifest.CandidateAudit.AddDefaulted_GetRef();
FString ArchetypeText;
double Distance = 0.0;
bFieldsValid = ReadJsonInt32(Object, TEXT("candidate_index"), Row.CandidateIndex) && bFieldsValid;
bFieldsValid = ReadJsonInt32(Object, TEXT("seed"), Row.Seed) && bFieldsValid;
bFieldsValid = ReadJsonString(Object, TEXT("archetype"), ArchetypeText) && bFieldsValid;
double RecipeHash = 0.0;
bFieldsValid = ReadJsonNumber(Object, TEXT("recipe_hash"), RecipeHash) && bFieldsValid;
bFieldsValid = RecipeHash >= 0.0 && RecipeHash <= 4294967295.0
&& FMath::FloorToDouble(RecipeHash) == RecipeHash && bFieldsValid;
Row.RecipeHash = static_cast<uint32>(RecipeHash);
bFieldsValid = ReadJsonNumber(Object, TEXT("distance_from_corpus_centroid"), Distance) && bFieldsValid;
Row.DistanceFromCorpusCentroid = Distance;
bFieldsValid = ReadJsonBool(Object, TEXT("passed_hard_gates"), Row.bPassedHardGates) && bFieldsValid;
bFieldsValid = ReadJsonBool(Object, TEXT("selected"), Row.bSelected) && bFieldsValid;
bFieldsValid = ReadJsonString(Object, TEXT("rejection_reason"), Row.RejectionReason) && bFieldsValid;
bFieldsValid = ParseArchetype(ArchetypeText, Row.Archetype) && bFieldsValid;
}
}
if (OutManifest.Strates.Num() != OutManifest.SelectedCount
|| OutManifest.SelectedCount <= 0
|| OutManifest.WorldRadiusVoxels != 0.0f)
{
bFieldsValid = false;
}
for (int32 Index = 0; Index < OutManifest.Strates.Num(); ++Index)
{
const FVoxelSeasonStrate& Strate = OutManifest.Strates[Index];
const float StoredTop = static_cast<float>(Strate.TopWorldZ);
const float StoredBottom = static_cast<float>(Strate.BottomWorldZ);
if (Strate.DepthIndex != Index || Strate.bUsesRegions || !Strate.bPassedPrimordialLaw
|| Strate.PrimordialLawResult != EVoxelConnectivityResult::Connected)
{
bFieldsValid = false;
break;
}
if (Strate.bUsesRegions
&& (!Strate.Regions.IsValid()
|| Strate.Regions.RegionCount <= 1
|| Strate.Regions.Seed != Strate.Seed
|| Strate.Regions.StrateIndex != Strate.DepthIndex
|| Strate.Regions.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Strate.Seed, Strate.DepthIndex)
|| !Strate.Regions.bHasGlobalStructuralParams
|| FMemory::Memcmp(&Strate.Regions.StrateTopWorldZ,
&StoredTop, sizeof(float)) != 0
|| FMemory::Memcmp(&Strate.Regions.StrateBottomWorldZ,
&StoredBottom, sizeof(float)) != 0)
)
{
bFieldsValid = false;
break;
}
}
if (!bFieldsValid)
{
OutReport = TEXT("season manifest contains invalid fields");
OutManifest.Error = OutReport;
OutManifest.bValid = false;
return false;
}
const FString ComputedHash = VF_ComputeVoxelSeasonManifestContentHash(OutManifest);
if (ComputedHash.IsEmpty()
|| !ComputedHash.Equals(OutManifest.ContentHash, ESearchCase::IgnoreCase))
{
OutReport = FString::Printf(
TEXT("season manifest content hash mismatch (stored %s, computed %s)"),
*OutManifest.ContentHash, *ComputedHash);
OutManifest.Error = OutReport;
OutManifest.bValid = false;
return false;
}
OutManifest.SerializedJson = ManifestJson;
OutReport = FString::Printf(TEXT("loaded season manifest hash %s"), *OutManifest.ContentHash);
return OutManifest.IsUsable();
}
bool VF_LoadVoxelSeasonManifest(const FString& ManifestPath,
FVoxelSeasonManifest& OutManifest,
FString& OutReport)
{
FString JsonText;
if (ManifestPath.IsEmpty())
{
OutReport = TEXT("season manifest path is empty");
OutManifest = FVoxelSeasonManifest();
OutManifest.Error = OutReport;
return false;
}
if (!FFileHelper::LoadFileToString(JsonText, *ManifestPath))
{
OutReport = FString::Printf(TEXT("could not read season manifest %s"), *ManifestPath);
OutManifest = FVoxelSeasonManifest();
OutManifest.Error = OutReport;
return false;
}
if (!VF_DeserializeVoxelSeasonManifest(JsonText, OutManifest, OutReport))
{
OutReport = FString::Printf(TEXT("season manifest %s: %s"), *ManifestPath, *OutReport);
OutManifest.Error = OutReport;
return false;
}
OutManifest.ManifestPath = ManifestPath;
OutReport = FString::Printf(TEXT("loaded season manifest from %s (hash %s)"),
*ManifestPath, *OutManifest.ContentHash);
return true;
}
namespace VoxelSeasonRuntimePrivate
{
float ActiveTop(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater: return Params.TunnelNetworkParams.StrateTopWorldZ;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber: return Params.SlabParams.StrateTopWorldZ;
case ECaveGeneratorType::Maze: return Params.MazeParams.StrateTopWorldZ;
case ECaveGeneratorType::SurfaceWorld: return Params.SurfaceParams.StrateTopWorldZ;
case ECaveGeneratorType::VerticalShafts: return Params.VerticalShaftParams.StrateTopWorldZ;
case ECaveGeneratorType::FloatingIslands: return Params.FloatingIslandParams.StrateTopWorldZ;
}
return 0.0f;
}
float ActiveBottom(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater: return Params.TunnelNetworkParams.StrateBottomWorldZ;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber: return Params.SlabParams.StrateBottomWorldZ;
case ECaveGeneratorType::Maze: return Params.MazeParams.StrateBottomWorldZ;
case ECaveGeneratorType::SurfaceWorld: return Params.SurfaceParams.StrateBottomWorldZ;
case ECaveGeneratorType::VerticalShafts: return Params.VerticalShaftParams.StrateBottomWorldZ;
case ECaveGeneratorType::FloatingIslands: return Params.FloatingIslandParams.StrateBottomWorldZ;
}
return 0.0f;
}
bool SameFloatBits(float A, float B)
{
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
}
}
#if WITH_EDITOR
namespace VoxelSeasonCompositionPrivate
{
float ActiveTop(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype);
float ActiveBottom(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype);
float ActiveSeal(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype);
void SetRuntimeBounds(FVoxelStrateArchetypeParams& Params, float TopWorldZ, float BottomWorldZ);
void SetRegionRuntimeBounds(FVoxelStrateRegionManifest& Regions,
int32 Seed, int32 StrateIndex,
float TopWorldZ, float BottomWorldZ);
const TCHAR* RejectionReasonForLaw(EVoxelConnectivityResult Result);
// These caps are policy/allocator guards, not gameplay limits. They keep a malformed build-box
// request from turning the candidate audit or Z-layout arithmetic into an unbounded allocation.
constexpr int32 MaxCandidateCount = 4096;
constexpr int32 MaxSelectedStrateCount = 256;
constexpr int32 MaxStrateHeightInChunks = 4096;
#if WITH_EDITOR
struct FSeasonCandidateState
{
FVoxelStrateComposerCandidate Candidate;
FVoxelStrateMeasuredMetrics Metrics;
double DistanceFromCorpusCentroid = 0.0;
bool bPassedHardGates = false;
bool bSelected = false;
bool bUnavailable = false;
bool bOutlierPool = false;
FString RejectionReason;
};
#endif
struct FSeasonEvaluation
{
bool bPassedNonVacuous = false;
bool bPassedLargestComponent = false;
bool bPassedPrimordialLaw = false;
EVoxelConnectivityResult PrimordialLawResult = EVoxelConnectivityResult::OutOfWindow;
FVoxelStrateMeasuredMetrics Metrics;
FString FailureReason;
};
class FSeasonStackSampler final : public IVoxelStrateDensitySampler
{
public:
explicit FSeasonStackSampler(const FVoxelOpStack& InStack)
: Stack(InStack)
{
}
float SampleDensity(float WorldX, float WorldY, float WorldZ) const override
{
// FVoxelOpStack::EvalMC is the established MC-facing convention: negative solid,
// positive air. No generation path is called here; this class exists only for the
// offline measurement API.
return Stack.EvalMC(WorldX, WorldY, WorldZ);
}
private:
const FVoxelOpStack& Stack;
};
bool IsFinitePolicy(const FVoxelSeasonSelectionPolicy& Policy)
{
const float FractionSum = Policy.GroundedFraction + Policy.OutlierFraction;
return FMath::IsFinite(Policy.GroundedFraction)
&& FMath::IsFinite(Policy.OutlierFraction)
&& Policy.GroundedFraction >= 0.0f
&& Policy.OutlierFraction >= 0.0f
&& FMath::IsFinite(FractionSum) && FractionSum > 0.0f
&& FMath::IsFinite(Policy.AdjacentSimilarityDistanceThreshold)
&& Policy.AdjacentSimilarityDistanceThreshold >= 0.0
&& FMath::IsFinite(Policy.AdjacentSimilarityPenaltyWeight)
&& Policy.AdjacentSimilarityPenaltyWeight >= 0.0
&& FMath::IsFinite(Policy.NewArchetypeBonus)
&& FMath::IsFinite(Policy.NewRecipeBonus)
&& Policy.BossSlotInterval >= 0
&& FMath::IsFinite(Policy.MinimumLargestComponentShare)
&& Policy.MinimumLargestComponentShare >= 0.0f
&& Policy.MinimumLargestComponentShare <= 1.0f
&& FMath::IsFinite(Policy.FeatureScaleNormalizationVoxels)
&& Policy.FeatureScaleNormalizationVoxels > 0.0f
&& FMath::IsFinite(Policy.ClearanceNormalizationVoxels)
&& Policy.ClearanceNormalizationVoxels > 0.0f;
}
bool IsValidMeasureSettings(const FVoxelStrateMeasureSettings& Settings)
{
return Settings.SampleStep > 0
&& Settings.RadiusInVoxels > 0
&& Settings.MaxCells > 0
&& Settings.MaxRouteRetries >= 0
&& Settings.HeadroomCells >= 0
&& Settings.InteriorMarginVoxels >= -1
&& FMath::IsFinite(Settings.CenterXY.X)
&& FMath::IsFinite(Settings.CenterXY.Y)
&& FMath::IsFinite(Settings.CoverMarginVoxels)
&& Settings.CoverMarginVoxels >= 0.0f;
}
bool IsBossSlot(int32 DepthIndex, int32 TotalStrates, int32 Interval)
{
// Slot zero is the surface entry, and the terminal slot is deliberately never a boss.
return Interval > 0 && DepthIndex > 0 && DepthIndex < TotalStrates - 1
&& ((DepthIndex + 1) % Interval) == 0;
}
uint32 SeasonAttemptHash(int32 SeasonSeed, int32 CandidateIndex)
{
// The attempt index is deliberately folded into the hash. Reordering candidate work on
// the build box therefore cannot accidentally turn an attempt into mutable RNG state.
uint32 Hash = VoxelHash::Mix(static_cast<uint32>(SeasonSeed) ^ 0x53454153u);
Hash = VoxelHash::Mix(Hash ^ (static_cast<uint32>(CandidateIndex) * 0x9E3779B9u));
return VoxelHash::Mix(Hash ^ 0x54494552u);
}
FString PercentageText(float Fraction)
{
return FString::Printf(TEXT("%.0f%%"), static_cast<double>(Fraction * 100.0f));
}
void AddRejection(TMap<FString, int32>& Counts, const FString& Reason)
{
const FString Key = Reason.IsEmpty() ? TEXT("unspecified") : Reason;
Counts.FindOrAdd(Key) += 1;
}
void ExportRejectionCounts(const TMap<FString, int32>& Counts,
TArray<FVoxelSeasonRejectionCount>& OutCounts)
{
TArray<FString> Reasons;
Counts.GetKeys(Reasons);
Reasons.Sort([](const FString& A, const FString& B) { return A < B; });
OutCounts.Reset();
for (const FString& Reason : Reasons)
{
FVoxelSeasonRejectionCount& Row = OutCounts.AddDefaulted_GetRef();
Row.Reason = Reason;
Row.Count = Counts.FindChecked(Reason);
}
}
void CopyDefinitionParams(const UVoxelStrateDefinition& Definition,
FVoxelSeasonFixedStrate& OutFixed)
{
OutFixed = FVoxelSeasonFixedStrate();
OutFixed.HeightInChunks = Definition.StrateHeightInChunks;
OutFixed.Archetype = Definition.GeneratorType;
OutFixed.SourceDefinitionPath = Definition.GetPathName();
OutFixed.bUsesRecipe = false;
OutFixed.Params.TunnelNetworkParams = Definition.GenerationParams;
OutFixed.Params.SlabParams = Definition.SlabParams;
OutFixed.Params.MazeParams = Definition.MazeParams;
OutFixed.Params.SurfaceParams = Definition.SurfaceParams;
OutFixed.Params.VerticalShaftParams = Definition.VerticalShaftParams;
OutFixed.Params.FloatingIslandParams = Definition.FloatingIslandParams;
}
bool ResolveFixedStrates(const FVoxelSeasonCompositionSettings& Settings,
int32 SeasonSeed,
TMap<int32, FVoxelSeasonFixedStrate>& OutFixed,
FString& OutError)
{
OutFixed = Settings.FixedStrates;
if (Settings.WorldSettings == nullptr)
{
return true;
}
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Pair
: Settings.WorldSettings->FixedStrates)
{
if (OutFixed.Contains(Pair.Key))
{
continue;
}
UVoxelStrateDefinition* Definition = Pair.Value.LoadSynchronous();
if (Definition == nullptr)
{
OutError = FString::Printf(TEXT("fixed strate slot %d could not load definition %s"),
Pair.Key, *Pair.Value.ToSoftObjectPath().ToString());
return false;
}
FVoxelSeasonFixedStrate Fixed;
CopyDefinitionParams(*Definition, Fixed);
Fixed.Seed = SeasonSeed;
if (Fixed.SourceDefinitionPath.IsEmpty())
{
Fixed.SourceDefinitionPath = Pair.Value.ToSoftObjectPath().ToString();
}
OutFixed.Add(Pair.Key, MoveTemp(Fixed));
}
return true;
}
#if WITH_EDITOR
void MakeCandidateStrate(const FVoxelStrateComposerCandidate& Candidate,
int32 CandidateIndex,
int32 HeightInChunks,
int32 TopWorldZ,
int32 BottomWorldZ,
FVoxelSeasonStrate& OutStrate)
{
OutStrate = FVoxelSeasonStrate();
OutStrate.DepthIndex = INDEX_NONE;
OutStrate.CandidateIndex = CandidateIndex;
OutStrate.Seed = Candidate.Seed;
OutStrate.HeightInChunks = HeightInChunks;
OutStrate.TopWorldZ = TopWorldZ;
OutStrate.BottomWorldZ = BottomWorldZ;
OutStrate.Archetype = Candidate.Archetype;
OutStrate.Recipe = Candidate.Recipe;
OutStrate.Params = Candidate.ArchetypeParams;
OutStrate.Regions = Candidate.Regions;
OutStrate.bUsesRecipe = true;
// Lateral composition remains a laboratory feature until its cross-seam law is perfect.
// The season artifact must therefore stay on the proved single-stack runtime contract.
OutStrate.bUsesRegions = VF_LateralRegionsAreShippable()
&& Candidate.Regions.RegionCount > 1;
SetRuntimeBounds(OutStrate.Params, static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
if (OutStrate.Regions.IsValid())
{
SetRegionRuntimeBounds(OutStrate.Regions, Candidate.Seed, CandidateIndex,
static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
}
}
#endif
void MakeFixedStrate(const FVoxelSeasonFixedStrate& Fixed,
int32 DepthIndex,
int32 TopWorldZ,
int32 BottomWorldZ,
FVoxelSeasonStrate& OutStrate)
{
OutStrate = FVoxelSeasonStrate();
OutStrate.DepthIndex = DepthIndex;
OutStrate.CandidateIndex = INDEX_NONE;
OutStrate.Seed = Fixed.Seed;
OutStrate.HeightInChunks = Fixed.HeightInChunks;
OutStrate.TopWorldZ = TopWorldZ;
OutStrate.BottomWorldZ = BottomWorldZ;
OutStrate.Archetype = Fixed.Archetype;
OutStrate.Recipe = Fixed.Recipe;
OutStrate.Params = Fixed.Params;
OutStrate.Regions = Fixed.Regions;
OutStrate.SourceDefinitionPath = Fixed.SourceDefinitionPath;
OutStrate.bFixed = true;
OutStrate.bUsesRecipe = Fixed.bUsesRecipe;
OutStrate.bUsesRegions = Fixed.bUsesRegions && Fixed.Regions.IsValid();
SetRuntimeBounds(OutStrate.Params, static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
if (OutStrate.bUsesRegions)
{
SetRegionRuntimeBounds(OutStrate.Regions, Fixed.Seed, DepthIndex,
static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
}
}
bool EvaluateStrate(const FVoxelSeasonStrate& Strate,
const FVoxelSeasonCompositionSettings& Settings,
FSeasonEvaluation& OutEvaluation,
FVoxelStrateSampleGrid* OutGrid = nullptr,
FVoxelStrateMetrics* OutRawMetrics = nullptr)
{
OutEvaluation = FSeasonEvaluation();
if (OutGrid != nullptr)
{
*OutGrid = FVoxelStrateSampleGrid();
}
if (OutRawMetrics != nullptr)
{
*OutRawMetrics = FVoxelStrateMetrics();
}
FVoxelOpStack Stack;
FVoxelOpContext Context;
FString BuildError;
bool bBuilt = false;
if (Strate.bUsesRegions)
{
bBuilt = VF_BuildStrateRegionStack(
Strate.Regions, Settings.OriginSpineRadius, nullptr,
Stack, Context, &BuildError);
}
else if (Strate.bUsesRecipe)
{
bBuilt = VF_BuildStackFromRecipe(Strate.Recipe, Strate.Params, Strate.Seed,
Settings.OriginSpineRadius, nullptr, Stack,
Context, &BuildError);
}
else
{
#if WITH_EDITOR
const float Seal = ActiveSeal(Strate.Params, Strate.Archetype);
bBuilt = VF_BuildNativeStrateStackForCandidate(
Strate.Archetype, Strate.Params, Strate.Seed, Settings.OriginSpineRadius,
0.0f, Seal, nullptr, Stack, Context);
if (!bBuilt)
{
BuildError = TEXT("native fixed strate stack could not be materialised");
}
#else
BuildError = TEXT("native fixed strate validation is editor-only");
#endif
}
if (!bBuilt)
{
OutEvaluation.FailureReason = BuildError.IsEmpty()
? TEXT("recipe/native stack build failed") : BuildError;
return false;
}
if (Context.WorldRadiusVoxels != 0.0f)
{
OutEvaluation.FailureReason = TEXT("WorldRadiusVoxels was not zero during composition");
return false;
}
const float Seal = Context.EdgeSealThickness;
if (!FMath::IsFinite(Seal) || Seal < 0.0f)
{
OutEvaluation.FailureReason = TEXT("boundary seal thickness is invalid");
return false;
}
Stack.PrepareChunk(Context);
FSeasonStackSampler Sampler(Stack);
FVoxelStrateMetrics RawMetrics = VF_MeasureStrateWithSampler(
Sampler, Strate.BottomWorldZ, Strate.TopWorldZ, Seal,
Settings.MeasureSettings, OutGrid);
if (OutRawMetrics != nullptr)
{
*OutRawMetrics = RawMetrics;
}
if (!RawMetrics.bValid)
{
OutEvaluation.FailureReason = RawMetrics.RefusalReason.IsEmpty()
? TEXT("measurement failed")
: FString::Printf(TEXT("measurement failed: %s"), *RawMetrics.RefusalReason);
return false;
}
OutEvaluation.bPassedNonVacuous = RawMetrics.NumSampled > 0
&& RawMetrics.NumAir > 0 && RawMetrics.NumSolid > 0;
if (!OutEvaluation.bPassedNonVacuous)
{
OutEvaluation.FailureReason = TEXT("vacuous");
return false;
}
OutEvaluation.bPassedLargestComponent =
RawMetrics.LargestComponentShare >= Settings.SelectionPolicy.MinimumLargestComponentShare;
if (!OutEvaluation.bPassedLargestComponent)
{
OutEvaluation.FailureReason = FString::Printf(
TEXT("fragmented (largest component %.6f < %.6f)"),
RawMetrics.LargestComponentShare,
Settings.SelectionPolicy.MinimumLargestComponentShare);
return false;
}
const float InteriorBottom = static_cast<float>(Strate.BottomWorldZ)
+ static_cast<float>(RawMetrics.ResolvedMarginVoxels);
const float InteriorTop = static_cast<float>(Strate.TopWorldZ)
- static_cast<float>(RawMetrics.ResolvedMarginVoxels);
if (!FMath::IsFinite(InteriorBottom) || !FMath::IsFinite(InteriorTop)
|| InteriorTop - InteriorBottom <= 1.0f)
{
OutEvaluation.FailureReason = TEXT("primordial law failed: no two interior endpoint cells");
return false;
}
const FVoxelConnectivityDiagnostics Law = VF_DiagnoseConnectivityWithSampler(
Sampler, Strate.BottomWorldZ, Strate.TopWorldZ, Seal,
FVector(0.0f, 0.0f, InteriorBottom + 0.5f),
FVector(0.0f, 0.0f, InteriorTop - 0.5f), Settings.MeasureSettings);
OutEvaluation.PrimordialLawResult = Law.Result;
OutEvaluation.bPassedPrimordialLaw = Law.bValid
&& Law.Result == EVoxelConnectivityResult::Connected
&& !Law.bStartSnapped && !Law.bGoalSnapped;
if (!OutEvaluation.bPassedPrimordialLaw)
{
OutEvaluation.FailureReason = FString::Printf(
TEXT("primordial law failed: %s%s"),
RejectionReasonForLaw(Law.Result),
(Law.bStartSnapped || Law.bGoalSnapped) ? TEXT(" (endpoint snapped)") : TEXT(""));
return false;
}
OutEvaluation.Metrics = VF_SummarizeStrateMetrics(RawMetrics);
if (!OutEvaluation.Metrics.IsUsable())
{
OutEvaluation.FailureReason = TEXT("measurement summary was not usable");
return false;
}
return true;
}
FVoxelStratePromotionPolicy MakeMetricDistancePolicy(
const FVoxelSeasonSelectionPolicy& Policy)
{
FVoxelStratePromotionPolicy Result;
Result.FeatureScaleNormalizationVoxels = Policy.FeatureScaleNormalizationVoxels;
Result.ClearanceNormalizationVoxels = Policy.ClearanceNormalizationVoxels;
return Result;
}
bool IsBetterScore(double Score, int32 CandidateIndex,
double BestScore, int32 BestCandidateIndex)
{
constexpr double TieEpsilon = 1.0e-12;
if (BestCandidateIndex == INDEX_NONE)
{
return true;
}
if (Score > BestScore + TieEpsilon)
{
return true;
}
return FMath::Abs(Score - BestScore) <= TieEpsilon
&& CandidateIndex < BestCandidateIndex;
}
float ActiveTop(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return Params.TunnelNetworkParams.StrateTopWorldZ;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
return Params.SlabParams.StrateTopWorldZ;
case ECaveGeneratorType::Maze:
return Params.MazeParams.StrateTopWorldZ;
case ECaveGeneratorType::SurfaceWorld:
return Params.SurfaceParams.StrateTopWorldZ;
case ECaveGeneratorType::VerticalShafts:
return Params.VerticalShaftParams.StrateTopWorldZ;
case ECaveGeneratorType::FloatingIslands:
return Params.FloatingIslandParams.StrateTopWorldZ;
default:
return 0.0f;
}
}
float ActiveBottom(const FVoxelStrateArchetypeParams& Params, ECaveGeneratorType Archetype)
{
switch (Archetype)
{
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
return Params.TunnelNetworkParams.StrateBottomWorldZ;
case ECaveGeneratorType::FlatPlain:
case ECaveGeneratorType::CrystalChamber:
return Params.SlabParams.StrateBottomWorldZ;
case ECaveGeneratorType::Maze:
return Params.MazeParams.StrateBottomWorldZ;
case ECaveGeneratorType::SurfaceWorld:
return Params.SurfaceParams.StrateBottomWorldZ;
case ECaveGeneratorType::VerticalShafts:
return Params.VerticalShaftParams.StrateBottomWorldZ;
case ECaveGeneratorType::FloatingIslands:
return Params.FloatingIslandParams.StrateBottomWorldZ;
default:
return 0.0f;
}
}
float ActiveSeal(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;
}
}
void SetRuntimeBounds(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;
}
void SetRegionRuntimeBounds(FVoxelStrateRegionManifest& Regions,
int32 Seed, int32 StrateIndex,
float TopWorldZ, float BottomWorldZ)
{
VF_RekeyStrateRegionManifest(Regions, Seed, StrateIndex);
Regions.StrateTopWorldZ = TopWorldZ;
Regions.StrateBottomWorldZ = BottomWorldZ;
Regions.bHasGlobalStructuralParams = true;
for (FVoxelStrateRegion& Region : Regions.Regions)
{
VF_SetStrateArchetypeRuntimeBounds(Region.ArchetypeParams,
TopWorldZ, BottomWorldZ);
}
}
bool SameFloatBits(float A, float B)
{
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
}
const TCHAR* RejectionReasonForLaw(EVoxelConnectivityResult Result)
{
return VoxelSeasonManifestPrivate::ConnectivityName(Result);
}
}
#endif // WITH_EDITOR — composition, measurement and review helpers never ship
namespace VoxelSeasonCompositionPrivate
{
#if WITH_EDITOR
bool WriteReviewPage(const FVoxelSeasonManifest& Manifest,
const FVoxelSeasonCompositionSettings& Settings,
const FString& ReviewDirectory,
FString& OutReviewPath,
FString& OutError);
#endif
}
#if WITH_EDITOR
FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed,
const FVoxelSeasonCompositionSettings& Settings)
{
using namespace VoxelSeasonCompositionPrivate;
FVoxelSeasonManifest Manifest;
Manifest.Season = Settings.SeasonNumber != 0
? Settings.SeasonNumber
: (Settings.WorldSettings != nullptr ? Settings.WorldSettings->CurrentSeason : 0);
Manifest.Seed = SeasonSeed;
Manifest.SchemaVersion = FVoxelSeasonManifest::CurrentSchemaVersion;
Manifest.SelectionPolicy = Settings.SelectionPolicy;
Manifest.PolicyNote = TEXT(
"PROVISIONAL POLICY: grounded/outlier ratio, adjacency threshold/weight, variety bonuses, "
"boss cadence, and metric normalizers are review knobs. This is not a settled quality score. "
"Jahni reviews the selected descent page and may veto the season before publish.");
auto Fail = [&](const FString& Reason) -> FVoxelSeasonManifest
{
Manifest.bValid = false;
Manifest.Error = Reason;
return Manifest;
};
if (Settings.WorldSettings != nullptr
&& Settings.WorldSettings->WorldRadiusVoxels != 0.0f)
{
return Fail(TEXT("WorldRadiusVoxels must remain exactly 0 for season composition."));
}
if (Settings.WorldRadiusVoxels != 0.0f)
{
return Fail(TEXT("WorldRadiusVoxels must remain exactly 0 for season composition."));
}
if (Settings.CandidateCount <= 0 || Settings.CandidateCount > MaxCandidateCount)
{
return Fail(FString::Printf(TEXT("CandidateCount must be in [1,%d]."), MaxCandidateCount));
}
if (Settings.SelectedStrateCount <= 0
|| Settings.SelectedStrateCount > MaxSelectedStrateCount)
{
return Fail(FString::Printf(TEXT("SelectedStrateCount must be in [1,%d]."),
MaxSelectedStrateCount));
}
if (Settings.StrateHeightInChunks <= 0
|| Settings.StrateHeightInChunks > MaxStrateHeightInChunks)
{
return Fail(FString::Printf(TEXT("StrateHeightInChunks must be in [1,%d]."),
MaxStrateHeightInChunks));
}
if (Settings.InterStrateGapChunks < 0
|| Settings.InterStrateGapChunks > MaxStrateHeightInChunks)
{
return Fail(TEXT("InterStrateGapChunks is outside the bounded composition range."));
}
if (!FMath::IsFinite(Settings.OriginSpineRadius) || Settings.OriginSpineRadius < 0.0f)
{
return Fail(TEXT("OriginSpineRadius must be finite and non-negative."));
}
if (!IsFinitePolicy(Settings.SelectionPolicy))
{
return Fail(TEXT("SelectionPolicy contains a non-finite or out-of-range knob."));
}
if (!IsValidMeasureSettings(Settings.MeasureSettings))
{
return Fail(TEXT("MeasureSettings are invalid for a bounded offline pass."));
}
TMap<int32, FVoxelSeasonFixedStrate> FixedStrates;
FString FixedError;
if (!ResolveFixedStrates(Settings, SeasonSeed, FixedStrates, FixedError))
{
return Fail(FixedError);
}
for (const TPair<int32, FVoxelSeasonFixedStrate>& Pair : FixedStrates)
{
if (Pair.Key < 0 || Pair.Key >= Settings.SelectedStrateCount)
{
return Fail(FString::Printf(TEXT("FixedStrates index %d is outside [0,%d)."),
Pair.Key, Settings.SelectedStrateCount));
}
if (!VoxelSeasonManifestPrivate::IsSupportedArchetype(Pair.Value.Archetype))
{
return Fail(FString::Printf(TEXT("FixedStrates index %d has an unsupported archetype."),
Pair.Key));
}
if (Pair.Value.HeightInChunks <= 0
|| Pair.Value.HeightInChunks > MaxStrateHeightInChunks)
{
return Fail(FString::Printf(TEXT("FixedStrates index %d has an invalid height."),
Pair.Key));
}
}
FVoxelStrateCorpus OwnedCorpus;
const FVoxelStrateCorpus* Corpus = Settings.Corpus;
if (Corpus == nullptr)
{
FString CorpusReport;
if (!OwnedCorpus.LoadFromAssetRegistry(CorpusReport) || !OwnedCorpus.IsValid())
{
return Fail(FString::Printf(TEXT("could not load the strate corpus: %s"),
*CorpusReport));
}
Corpus = &OwnedCorpus;
}
if (Corpus == nullptr || !Corpus->IsValid())
{
return Fail(TEXT("season composition requires a valid strate corpus."));
}
Manifest.InputCorpusHash = Corpus->GetContentsHash();
Manifest.CandidateCount = Settings.CandidateCount;
Manifest.TotalStrates = Settings.SelectedStrateCount;
Manifest.InterStrateGapChunks = Settings.InterStrateGapChunks;
Manifest.OriginSpineRadius = Settings.OriginSpineRadius;
Manifest.WorldRadiusVoxels = 0.0f;
TArray<FSeasonCandidateState> Candidates;
Candidates.Reserve(Settings.CandidateCount);
for (int32 CandidateIndex = 0; CandidateIndex < Settings.CandidateCount; ++CandidateIndex)
{
FSeasonCandidateState& State = Candidates.AddDefaulted_GetRef();
const int32 CandidateSeed = static_cast<int32>(
SeasonAttemptHash(SeasonSeed, CandidateIndex));
State.Candidate = VF_RollStrateCandidate(
*Corpus, CandidateSeed, CandidateIndex, true);
if (!State.Candidate.bValid)
{
State.RejectionReason = State.Candidate.FailureReason.IsEmpty()
? TEXT("candidate roll failed") : State.Candidate.FailureReason;
continue;
}
State.DistanceFromCorpusCentroid = VF_DistanceFromStrateCorpusCentroid(
*Corpus, State.Candidate.ArchetypeParams, State.Candidate.Archetype);
if (!FMath::IsFinite(State.DistanceFromCorpusCentroid))
{
State.DistanceFromCorpusCentroid = 0.0;
}
FVoxelSeasonStrate Probe;
const int32 ProbeTopWorldZ = 32;
const int32 ProbeBottomWorldZ = ProbeTopWorldZ
- Settings.StrateHeightInChunks * 32;
MakeCandidateStrate(State.Candidate, CandidateIndex,
Settings.StrateHeightInChunks, ProbeTopWorldZ,
ProbeBottomWorldZ, Probe);
FSeasonEvaluation Evaluation;
if (EvaluateStrate(Probe, Settings, Evaluation))
{
State.Metrics = Evaluation.Metrics;
State.bPassedHardGates = true;
}
else
{
State.RejectionReason = Evaluation.FailureReason;
}
}
TArray<int32> SurvivorIndices;
SurvivorIndices.Reserve(Candidates.Num());
for (int32 Index = 0; Index < Candidates.Num(); ++Index)
{
if (Candidates[Index].bPassedHardGates)
{
SurvivorIndices.Add(Index);
}
}
Manifest.SurvivorCount = SurvivorIndices.Num();
const int32 GeneratedSlotCount = Settings.SelectedStrateCount - FixedStrates.Num();
if (GeneratedSlotCount < 0)
{
return Fail(TEXT("FixedStrates contains more slots than the requested season."));
}
if (SurvivorIndices.Num() < GeneratedSlotCount)
{
return Fail(FString::Printf(
TEXT("only %d candidates survived the hard gates, but %d generated slots are required"),
SurvivorIndices.Num(), GeneratedSlotCount));
}
const float FractionSum = Settings.SelectionPolicy.GroundedFraction
+ Settings.SelectionPolicy.OutlierFraction;
const int32 TargetOutlierCount = FMath::Clamp(
FMath::RoundToInt(static_cast<float>(GeneratedSlotCount)
* Settings.SelectionPolicy.OutlierFraction / FractionSum),
0, GeneratedSlotCount);
SurvivorIndices.Sort([&Candidates](int32 A, int32 B)
{
const double ADistance = Candidates[A].DistanceFromCorpusCentroid;
const double BDistance = Candidates[B].DistanceFromCorpusCentroid;
if (ADistance != BDistance)
{
return ADistance > BDistance;
}
return Candidates[A].Candidate.Index < Candidates[B].Candidate.Index;
});
for (int32 Index = 0; Index < FMath::Min(TargetOutlierCount, SurvivorIndices.Num()); ++Index)
{
Candidates[SurvivorIndices[Index]].bOutlierPool = true;
}
const FVoxelStratePromotionPolicy MetricDistancePolicy =
MakeMetricDistancePolicy(Settings.SelectionPolicy);
TSet<int32> UsedArchetypes;
TSet<uint32> UsedRecipes;
FVoxelStrateMeasuredMetrics PreviousMetrics;
bool bHavePreviousMetrics = false;
int64 CurrentTopChunk = 0;
int32 GeneratedOrdinal = 0;
int32 SelectedGeneratedCount = 0;
int32 AssignedOutlierCount = 0;
int32 FixedSelectedCount = 0;
auto GetLayoutBounds = [&](int32 HeightInChunks,
int32& OutTopWorldZ, int32& OutBottomWorldZ,
int64& OutBottomChunk) -> bool
{
if (HeightInChunks <= 0) return false;
const int64 TopChunk = CurrentTopChunk;
OutBottomChunk = TopChunk - static_cast<int64>(HeightInChunks) + 1;
const int64 TopWorld = (TopChunk + 1) * 32;
const int64 BottomWorld = OutBottomChunk * 32;
if (TopWorld < static_cast<int64>(TNumericLimits<int32>::Lowest())
|| TopWorld > static_cast<int64>(TNumericLimits<int32>::Max())
|| BottomWorld < static_cast<int64>(TNumericLimits<int32>::Lowest())
|| BottomWorld > static_cast<int64>(TNumericLimits<int32>::Max()))
{
return false;
}
OutTopWorldZ = static_cast<int32>(TopWorld);
OutBottomWorldZ = static_cast<int32>(BottomWorld);
return OutTopWorldZ > OutBottomWorldZ;
};
for (int32 DepthIndex = 0; DepthIndex < Settings.SelectedStrateCount; ++DepthIndex)
{
const FVoxelSeasonFixedStrate* Fixed = FixedStrates.Find(DepthIndex);
const int32 HeightInChunks = Fixed != nullptr
? Fixed->HeightInChunks : Settings.StrateHeightInChunks;
int32 TopWorldZ = 0;
int32 BottomWorldZ = 0;
int64 BottomChunk = 0;
if (!GetLayoutBounds(HeightInChunks, TopWorldZ, BottomWorldZ, BottomChunk))
{
return Fail(FString::Printf(TEXT("season Z layout overflowed at slot %d"), DepthIndex));
}
if (Fixed != nullptr)
{
FVoxelSeasonStrate Strate;
MakeFixedStrate(*Fixed, DepthIndex, TopWorldZ, BottomWorldZ, Strate);
FSeasonEvaluation Evaluation;
if (!EvaluateStrate(Strate, Settings, Evaluation))
{
return Fail(FString::Printf(TEXT("fixed slot %d failed validation: %s"),
DepthIndex, *Evaluation.FailureReason));
}
Strate.Metrics = Evaluation.Metrics;
Strate.bPassedNonVacuous = Evaluation.bPassedNonVacuous;
Strate.bPassedLargestComponent = Evaluation.bPassedLargestComponent;
Strate.bPassedPrimordialLaw = Evaluation.bPassedPrimordialLaw;
Strate.PrimordialLawResult = Evaluation.PrimordialLawResult;
Strate.DistanceFromCorpusCentroid = VF_DistanceFromStrateCorpusCentroid(
*Corpus, Strate.Params, Strate.Archetype);
Strate.bBossSlot = IsBossSlot(DepthIndex, Settings.SelectedStrateCount,
Settings.SelectionPolicy.BossSlotInterval);
Strate.SelectionReason = TEXT("fixed: preserved from the absolute FixedStrates slot");
if (Strate.bBossSlot)
{
Strate.SelectionReason += TEXT("; boss slot at the provisional five-slot cadence");
}
Manifest.Strates.Add(MoveTemp(Strate));
++FixedSelectedCount;
UsedArchetypes.Add(static_cast<int32>(Fixed->Archetype));
if (Fixed->bUsesRecipe)
{
UsedRecipes.Add(VF_HashStrateStructureRecipe(Fixed->Recipe));
}
PreviousMetrics = Manifest.Strates.Last().Metrics;
bHavePreviousMetrics = true;
}
else
{
const int32 RemainingGeneratedSlots = GeneratedSlotCount - GeneratedOrdinal;
const int32 DesiredOutliersSoFar = FMath::RoundToInt(
static_cast<float>(GeneratedOrdinal + 1) * TargetOutlierCount
/ FMath::Max(1, GeneratedSlotCount));
const bool bWantOutlier = DesiredOutliersSoFar >
AssignedOutlierCount;
int32 BestStateIndex = INDEX_NONE;
double BestScore = -TNumericLimits<double>::Max();
bool bUsedClassFallback = false;
auto FindBest = [&](bool bRequireOutlierClass) -> bool
{
bool bFound = false;
for (int32 StateIndex = 0; StateIndex < Candidates.Num(); ++StateIndex)
{
FSeasonCandidateState& State = Candidates[StateIndex];
if (!State.bPassedHardGates || State.bSelected || State.bUnavailable
|| (bRequireOutlierClass && State.bOutlierPool != bWantOutlier))
{
continue;
}
const bool bNewArchetype = !UsedArchetypes.Contains(
static_cast<int32>(State.Candidate.Archetype));
const uint32 RecipeHash = VF_HashStrateStructureRecipe(State.Candidate.Recipe);
const bool bNewRecipe = !UsedRecipes.Contains(RecipeHash);
const bool bActualOutlier = State.bOutlierPool;
double Score = bActualOutlier
? State.DistanceFromCorpusCentroid
: -State.DistanceFromCorpusCentroid;
if (bHavePreviousMetrics && State.Metrics.IsUsable())
{
const double AdjacentDistance = VF_NormalizedMeasuredMetricDistance(
PreviousMetrics, State.Metrics, MetricDistancePolicy);
if (FMath::IsFinite(AdjacentDistance)
&& AdjacentDistance < Settings.SelectionPolicy.AdjacentSimilarityDistanceThreshold)
{
Score -= (Settings.SelectionPolicy.AdjacentSimilarityDistanceThreshold
- AdjacentDistance)
* Settings.SelectionPolicy.AdjacentSimilarityPenaltyWeight;
}
}
if (bNewArchetype)
{
Score += Settings.SelectionPolicy.NewArchetypeBonus;
}
if (bNewRecipe)
{
Score += Settings.SelectionPolicy.NewRecipeBonus;
}
if (!FMath::IsFinite(Score))
{
continue;
}
if (!bFound || IsBetterScore(Score, State.Candidate.Index,
BestScore,
Candidates[BestStateIndex].Candidate.Index))
{
BestStateIndex = StateIndex;
BestScore = Score;
bFound = true;
}
}
return bFound;
};
if (!FindBest(true))
{
bUsedClassFallback = FindBest(false);
}
if (BestStateIndex == INDEX_NONE)
{
return Fail(FString::Printf(
TEXT("no validated candidate remains for season slot %d (%d generated slots remain)"),
DepthIndex, RemainingGeneratedSlots));
}
bool bPlaced = false;
while (BestStateIndex != INDEX_NONE)
{
FSeasonCandidateState& State = Candidates[BestStateIndex];
FVoxelSeasonStrate Strate;
MakeCandidateStrate(State.Candidate, State.Candidate.Index,
Settings.StrateHeightInChunks, TopWorldZ,
BottomWorldZ, Strate);
Strate.DepthIndex = DepthIndex;
if (Strate.bUsesRegions)
{
// Candidate probes use the attempt index only to make their provisional
// measurements independent. The persisted world is keyed by its absolute
// strate slot, so rekey before the final placement gate and measurement.
SetRegionRuntimeBounds(Strate.Regions, Strate.Seed, DepthIndex,
static_cast<float>(TopWorldZ),
static_cast<float>(BottomWorldZ));
}
FSeasonEvaluation Evaluation;
if (!EvaluateStrate(Strate, Settings, Evaluation))
{
State.bUnavailable = true;
State.RejectionReason = FString::Printf(
TEXT("final placement at depth %d failed: %s"),
DepthIndex, *Evaluation.FailureReason);
BestStateIndex = INDEX_NONE;
if (!FindBest(true))
{
bUsedClassFallback = FindBest(false);
}
continue;
}
const bool bActualOutlier = State.bOutlierPool;
const bool bNewArchetype = !UsedArchetypes.Contains(
static_cast<int32>(State.Candidate.Archetype));
const uint32 RecipeHash = VF_HashStrateStructureRecipe(State.Candidate.Recipe);
const bool bNewRecipe = !UsedRecipes.Contains(RecipeHash);
Strate.Metrics = Evaluation.Metrics;
Strate.DistanceFromCorpusCentroid = State.DistanceFromCorpusCentroid;
Strate.bGrounded = !bActualOutlier;
Strate.bOutlier = bActualOutlier;
Strate.bBossSlot = IsBossSlot(DepthIndex, Settings.SelectedStrateCount,
Settings.SelectionPolicy.BossSlotInterval);
Strate.bPassedNonVacuous = Evaluation.bPassedNonVacuous;
Strate.bPassedLargestComponent = Evaluation.bPassedLargestComponent;
Strate.bPassedPrimordialLaw = Evaluation.bPassedPrimordialLaw;
Strate.PrimordialLawResult = Evaluation.PrimordialLawResult;
const FString ClassName = bActualOutlier ? TEXT("outlier") : TEXT("grounded");
const FString DistanceDirection = bActualOutlier
? TEXT("farthest surviving corpus-centroid distance")
: TEXT("nearest surviving corpus-centroid distance");
Strate.SelectionReason = FString::Printf(
TEXT("%s (%s grounded / %s outlier placeholder): %s; "
"adjacency penalty and variety tie-breakers"),
*ClassName,
*PercentageText(Settings.SelectionPolicy.GroundedFraction / FractionSum),
*PercentageText(Settings.SelectionPolicy.OutlierFraction / FractionSum),
*DistanceDirection);
if (bUsedClassFallback)
{
Strate.SelectionReason += TEXT("; requested ratio class was exhausted");
}
if (bNewArchetype || bNewRecipe)
{
Strate.SelectionReason += TEXT("; unseen ");
if (bNewArchetype) Strate.SelectionReason += TEXT("archetype");
if (bNewArchetype && bNewRecipe) Strate.SelectionReason += TEXT("/");
if (bNewRecipe) Strate.SelectionReason += TEXT("recipe");
Strate.SelectionReason += TEXT(" bonus applied");
}
if (Strate.bBossSlot)
{
Strate.SelectionReason += TEXT("; boss slot at the provisional five-slot cadence");
}
State.Metrics = Evaluation.Metrics;
State.bSelected = true;
State.RejectionReason.Reset();
Manifest.Strates.Add(MoveTemp(Strate));
UsedArchetypes.Add(static_cast<int32>(State.Candidate.Archetype));
UsedRecipes.Add(RecipeHash);
PreviousMetrics = Manifest.Strates.Last().Metrics;
bHavePreviousMetrics = true;
++GeneratedOrdinal;
++SelectedGeneratedCount;
if (bActualOutlier)
{
++AssignedOutlierCount;
}
bPlaced = true;
break;
}
if (!bPlaced)
{
return Fail(FString::Printf(TEXT("all remaining candidates failed final placement at slot %d"),
DepthIndex));
}
}
CurrentTopChunk = BottomChunk - 1 - static_cast<int64>(Settings.InterStrateGapChunks);
}
if (Manifest.Strates.Num() != Settings.SelectedStrateCount
|| SelectedGeneratedCount != GeneratedSlotCount
|| FixedSelectedCount != FixedStrates.Num())
{
return Fail(TEXT("season selector did not fill every absolute slot exactly once"));
}
TMap<FString, int32> RejectionCounts;
if (Settings.bIncludeCandidateAudit)
{
Manifest.CandidateAudit.Reserve(Candidates.Num());
}
for (const FSeasonCandidateState& State : Candidates)
{
const FString Reason = State.bSelected
? FString()
: (State.RejectionReason.IsEmpty()
? TEXT("not selected by provisional policy") : State.RejectionReason);
if (!State.bSelected)
{
AddRejection(RejectionCounts, Reason);
}
if (Settings.bIncludeCandidateAudit)
{
FVoxelSeasonCandidateAudit& Row = Manifest.CandidateAudit.AddDefaulted_GetRef();
Row.CandidateIndex = State.Candidate.Index;
Row.Seed = State.Candidate.Seed;
Row.Archetype = State.Candidate.Archetype;
Row.RecipeHash = State.Candidate.bValid
? VF_HashStrateStructureRecipe(State.Candidate.Recipe) : 0;
Row.DistanceFromCorpusCentroid = State.DistanceFromCorpusCentroid;
Row.bPassedHardGates = State.bPassedHardGates;
Row.bSelected = State.bSelected;
Row.RejectionReason = Reason;
}
}
ExportRejectionCounts(RejectionCounts, Manifest.RejectionCounts);
Manifest.SelectedCount = Manifest.Strates.Num();
Manifest.GroundedSelectedCount = 0;
Manifest.OutlierSelectedCount = 0;
Manifest.FixedSelectedCount = 0;
for (const FVoxelSeasonStrate& Strate : Manifest.Strates)
{
if (Strate.bGrounded) ++Manifest.GroundedSelectedCount;
if (Strate.bOutlier) ++Manifest.OutlierSelectedCount;
if (Strate.bFixed) ++Manifest.FixedSelectedCount;
if (!Strate.bPassedNonVacuous || !Strate.bPassedLargestComponent
|| !Strate.bPassedPrimordialLaw
|| Strate.PrimordialLawResult != EVoxelConnectivityResult::Connected)
{
return Fail(FString::Printf(TEXT("selected slot %d failed the final primordial-law assertion"),
Strate.DepthIndex));
}
}
Manifest.RejectedCount = FMath::Max(0, Manifest.CandidateCount - SelectedGeneratedCount);
if (Settings.bWriteArtifacts)
{
const FString BaseDirectory = Settings.OutputDirectory.IsEmpty()
? FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("VoxelForge"), TEXT("Seasons"))
: Settings.OutputDirectory;
const FString SeasonDirectory = FPaths::Combine(
BaseDirectory,
FString::Printf(TEXT("season_%04d_seed_%d"), Manifest.Season, Manifest.Seed));
Manifest.ManifestPath = FPaths::Combine(SeasonDirectory, TEXT("season_manifest.json"));
if (Settings.bWriteReviewPage)
{
const FString ReviewDirectory = FPaths::Combine(SeasonDirectory, TEXT("review"));
if (!WriteReviewPage(Manifest, Settings, ReviewDirectory,
Manifest.ReviewPagePath, Manifest.Error))
{
return Fail(Manifest.Error.IsEmpty()
? TEXT("could not write selected-season review page") : Manifest.Error);
}
}
}
Manifest.bValid = true;
Manifest.Error.Reset();
Manifest.ContentHash = VF_ComputeVoxelSeasonManifestContentHash(Manifest);
if (Manifest.ContentHash.IsEmpty())
{
return Fail(TEXT("could not hash composed season manifest"));
}
Manifest.SerializedJson = VF_SerializeVoxelSeasonManifest(Manifest);
if (Manifest.SerializedJson.IsEmpty())
{
return Fail(TEXT("could not serialize composed season manifest"));
}
if (Settings.bWriteArtifacts)
{
FString SaveReport;
if (!VF_SaveVoxelSeasonManifest(Manifest.ManifestPath, Manifest, SaveReport))
{
return Fail(SaveReport);
}
}
return Manifest;
}
#endif
#if WITH_EDITOR
FVoxelSeasonManifest VF_ComposeSeason(int32 SeasonSeed, const UVoxelSettings* WorldSettings)
{
FVoxelSeasonCompositionSettings Settings;
Settings.WorldSettings = WorldSettings;
if (WorldSettings != nullptr)
{
Settings.SelectedStrateCount = WorldSettings->TotalStrates;
Settings.InterStrateGapChunks = WorldSettings->InterStrateGapChunks;
Settings.OriginSpineRadius = WorldSettings->OriginSpineRadius;
Settings.WorldRadiusVoxels = WorldSettings->WorldRadiusVoxels;
Settings.SeasonNumber = WorldSettings->CurrentSeason;
}
return VF_ComposeSeason(SeasonSeed, Settings);
}
#endif
bool VF_BuildSeasonStrateStack(const FVoxelSeasonStrate& Strate,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError)
{
using namespace VoxelSeasonRuntimePrivate;
auto Fail = [&](const FString& Reason) -> bool
{
if (OutError != nullptr)
{
*OutError = Reason;
}
return false;
};
if (OutError != nullptr)
{
OutError->Reset();
}
if (!VoxelSeasonManifestPrivate::IsSupportedArchetype(Strate.Archetype)
|| Strate.HeightInChunks <= 0
|| !FMath::IsFinite(SpineRadius) || SpineRadius < 0.0f
|| Strate.TopWorldZ <= Strate.BottomWorldZ)
{
return Fail(TEXT("Manifest strate has invalid identity, height, bounds, or spine radius."));
}
const float StoredTop = static_cast<float>(Strate.TopWorldZ);
const float StoredBottom = static_cast<float>(Strate.BottomWorldZ);
if (Strate.bUsesRegions)
{
if (!Strate.Regions.IsValid()
|| Strate.Regions.RegionCount <= 1
|| Strate.Regions.Seed != Strate.Seed
|| Strate.Regions.StrateIndex != Strate.DepthIndex
|| Strate.Regions.PartitionSeed != VF_GetStrateRegionPartitionSeed(
Strate.Seed, Strate.DepthIndex)
|| !Strate.Regions.bHasGlobalStructuralParams
|| !SameFloatBits(Strate.Regions.StrateTopWorldZ, StoredTop)
|| !SameFloatBits(Strate.Regions.StrateBottomWorldZ, StoredBottom))
{
return Fail(TEXT("Manifest lateral region description is invalid or not keyed to its slot."));
}
}
if (!SameFloatBits(ActiveTop(Strate.Params, Strate.Archetype), StoredTop)
|| !SameFloatBits(ActiveBottom(Strate.Params, Strate.Archetype), StoredBottom))
{
return Fail(TEXT("Manifest bounds do not match the active parameter vector."));
}
bool bBuilt = false;
if (Strate.bUsesRegions)
{
#if WITH_EDITOR
bBuilt = VF_BuildStrateRegionStack(Strate.Regions, SpineRadius, StrateManager,
OutStack, OutContext, OutError);
#else
return Fail(TEXT("Lateral region manifests are not shippable."));
#endif
}
else if (Strate.bUsesRecipe)
{
bBuilt = VF_BuildStackFromRecipe(Strate.Recipe, Strate.Params, Strate.Seed,
SpineRadius, StrateManager, OutStack, OutContext,
OutError);
}
else
{
#if WITH_EDITOR
const float Seal = VoxelSeasonCompositionPrivate::ActiveSeal(
Strate.Params, Strate.Archetype);
bBuilt = VF_BuildNativeStrateStackForCandidate(
Strate.Archetype, Strate.Params, Strate.Seed, SpineRadius,
0.0f, Seal, StrateManager, OutStack, OutContext);
if (!bBuilt && OutError != nullptr)
{
*OutError = TEXT("Native manifest strate could not be materialised in the editor build.");
}
#else
bBuilt = false;
if (OutError != nullptr)
{
*OutError = TEXT("Native fixed-strate rebuild is editor/build-box only.");
}
#endif
}
if (!bBuilt)
{
return false;
}
if (OutContext.WorldRadiusVoxels != 0.0f)
{
return Fail(TEXT("Manifest rebuild changed the required WorldRadiusVoxels=0 invariant."));
}
return true;
}
bool VF_RebuildVoxelSeasonStrate(const FVoxelSeasonStrate& Strate,
float SpineRadius,
const UVoxelStrateManager* StrateManager,
FVoxelOpStack& OutStack,
FVoxelOpContext& OutContext,
FString* OutError)
{
return VF_BuildSeasonStrateStack(Strate, SpineRadius, StrateManager,
OutStack, OutContext, OutError);
}
#if WITH_EDITOR
namespace VoxelSeasonCompositionPrivate
{
bool WriteReviewPage(const FVoxelSeasonManifest& Manifest,
const FVoxelSeasonCompositionSettings& Settings,
const FString& ReviewDirectory,
FString& OutReviewPath,
FString& OutError)
{
OutReviewPath.Reset();
OutError.Reset();
if (!IFileManager::Get().MakeDirectory(*ReviewDirectory, true))
{
OutError = FString::Printf(TEXT("could not create season review directory %s"),
*ReviewDirectory);
return false;
}
TArray<FVoxelStratePreviewCandidate> Cards;
Cards.Reserve(Manifest.Strates.Num());
FVoxelStratePreviewWindow CommonWindow;
bool bHaveWindow = false;
for (const FVoxelSeasonStrate& Strate : Manifest.Strates)
{
FVoxelStrateSampleGrid Grid;
FVoxelStrateMetrics RawMetrics;
FSeasonEvaluation Evaluation;
if (!EvaluateStrate(Strate, Settings, Evaluation, &Grid, &RawMetrics))
{
OutError = FString::Printf(TEXT("selected slot %d failed its review re-measure: %s"),
Strate.DepthIndex, *Evaluation.FailureReason);
return false;
}
if (!Grid.IsValid())
{
OutError = FString::Printf(TEXT("selected slot %d produced no review grid"),
Strate.DepthIndex);
return false;
}
const FString RecipeString = Strate.bUsesRecipe
? VF_FormatStrateStructureRecipe(Strate.Recipe)
: FString::Printf(TEXT("native/%s"), VF_GetStrateArchetypeName(Strate.Archetype));
const FString ArrivalVerdict = FString::Printf(
TEXT("%s%s"),
VoxelSeasonManifestPrivate::ConnectivityName(Evaluation.PrimordialLawResult),
(Evaluation.bPassedPrimordialLaw ? TEXT("") : TEXT(" (failed)")));
FVoxelStratePreviewCandidate& Card = Cards.AddDefaulted_GetRef();
if (!VF_WriteStratePreviewCandidate(
ReviewDirectory, Strate.DepthIndex, RecipeString, Grid, RawMetrics,
Settings.MeasureSettings.HeadroomCells,
Strate.DistanceFromCorpusCentroid, ArrivalVerdict, false, TEXT(""),
Card, OutError))
{
return false;
}
Card.bSeasonOrder = true;
Card.DepthIndex = Strate.DepthIndex;
Card.SelectionReason = Strate.SelectionReason;
Card.bBossSlot = Strate.bBossSlot;
if (!bHaveWindow)
{
CommonWindow = Card.Window;
bHaveWindow = true;
}
}
if (!bHaveWindow || !CommonWindow.IsValid())
{
OutError = TEXT("selected season has no valid preview measurement window");
return false;
}
const FString RunTitle = FString::Printf(
TEXT("VoxelForge Season %d — seed %d — descent review"), Manifest.Season, Manifest.Seed);
if (!VF_WriteStratePreviewIndex(ReviewDirectory, RunTitle, CommonWindow,
Cards, OutReviewPath, OutError))
{
return false;
}
return true;
}
}
#endif // WITH_EDITOR — review rendering never ships