Windows MSVC catch-up after abyss L15-L61 (no Unreal there): 64-bit clipmap shift (C4334), qualified VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel, UHT metadata and duplicate-signature fixes. Optional CoreGeometry section on UVoxelStrateDefinition (native caves, lattice, profiled heightfields, named stamps, rooms/tunnels; off/empty by default), resolved after passage planning. VoxelForgeAssetVersion 2 converts strate height/transition/gap to metres on load (exact, 0 m change on the owner's 5 assets; packages never saved). Gates pass (SurfaceWorld, FloatingIslands, Core-off, M2, W1/W8/W16). KNOWN REGRESSION vs L14 (2026-10-20, same command, same PC): full set 3.9 -> 5.37 s, worker-seconds 15.8 -> 30, LOD0 1.11 -> 1.69 s, ring 0.50 -> 0.80 s; not yet attributed (Core L15-L61 vs Unreal side). Report: Standalone/Research/WorldAssemblerM3/Unreal1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
261 lines
9.1 KiB
C++
261 lines
9.1 KiB
C++
#include "VoxelSettings.h"
|
|
|
|
#include "VoxelForgeAssetVersion.h"
|
|
#include "VoxelStrateDefinition.h"
|
|
#include "VoxelSeasonAsset.h"
|
|
|
|
#if WITH_EDITOR
|
|
#include "Misc/FileHelper.h"
|
|
#endif
|
|
|
|
#include <VoxelForgeCore/VoxelForgeCore.h>
|
|
#include <string>
|
|
|
|
void UVoxelSettings::Serialize(FArchive& Ar)
|
|
{
|
|
Ar.UsingCustomVersion(VoxelForgeAssetVersion::GUID);
|
|
Super::Serialize(Ar);
|
|
if (Ar.IsLoading() && Ar.CustomVer(VoxelForgeAssetVersion::GUID)
|
|
< VoxelForgeAssetVersion::MetreAuthoringV1)
|
|
{
|
|
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
|
|
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
|
|
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
|
|
* LayoutChunkMeters;
|
|
UE_LOG(LogTemp, Display,
|
|
TEXT("[VoxelForgeAssetUpgrade] type=Settings asset=%s gap_chunks=%d gap_m=%.6f layout_chunk_m=%.6f"),
|
|
*GetPathName(), InterStrateGapChunks, InterStrateGapMeters, LayoutChunkMeters);
|
|
}
|
|
}
|
|
|
|
void UVoxelSettings::PostLoad()
|
|
{
|
|
Super::PostLoad();
|
|
if (GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
|
>= VoxelForgeAssetVersion::MetreAuthoringV1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Resolve the legacy count against this settings asset's saved physical layout width.
|
|
// Convertit les anciens comptes avec la largeur physique sauvegardee dans ces reglages.
|
|
const double LayoutChunkMeters = FMath::IsFinite(StrateLayoutChunkSizeMeters)
|
|
? FMath::Max(0.001, static_cast<double>(StrateLayoutChunkSizeMeters)) : 8.0;
|
|
InterStrateGapMeters = static_cast<double>(FMath::Max(0, InterStrateGapChunks))
|
|
* LayoutChunkMeters;
|
|
|
|
TSet<UVoxelStrateDefinition*> Migrated;
|
|
const auto MigrateStrate = [this, LayoutChunkMeters, &Migrated](
|
|
const TSoftObjectPtr<UVoxelStrateDefinition>& SoftDefinition)
|
|
{
|
|
UVoxelStrateDefinition* Definition = SoftDefinition.LoadSynchronous();
|
|
if (Definition == nullptr || Migrated.Contains(Definition)
|
|
|| Definition->GetLinkerCustomVersion(VoxelForgeAssetVersion::GUID)
|
|
>= VoxelForgeAssetVersion::MetreAuthoringV1)
|
|
{
|
|
return;
|
|
}
|
|
Migrated.Add(Definition);
|
|
const int32 OldHeightChunks = Definition->StrateHeightInChunks;
|
|
const int32 OldTransitionChunks = Definition->TransitionBlendChunks;
|
|
Definition->StrateHeightMeters = static_cast<double>(FMath::Max(1, OldHeightChunks))
|
|
* LayoutChunkMeters;
|
|
Definition->TransitionWidthMeters = static_cast<double>(FMath::Max(0, OldTransitionChunks))
|
|
* LayoutChunkMeters;
|
|
Definition->MarkPackageDirty();
|
|
UE_LOG(LogTemp, Display,
|
|
TEXT("[VoxelForgeAssetUpgrade] type=Strate asset=%s height_chunks=%d height_m=%.6f transition_chunks=%d transition_m=%.6f effective_layout_chunk_m=%.6f"),
|
|
*Definition->GetPathName(), OldHeightChunks, Definition->StrateHeightMeters,
|
|
OldTransitionChunks, Definition->TransitionWidthMeters, LayoutChunkMeters);
|
|
};
|
|
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Definition : StratePool)
|
|
{
|
|
MigrateStrate(Definition);
|
|
}
|
|
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Definition : FixedStrates)
|
|
{
|
|
MigrateStrate(Definition.Value);
|
|
}
|
|
MarkPackageDirty();
|
|
}
|
|
|
|
int32 UVoxelSettings::GetEffectiveWorldSeed() const
|
|
{
|
|
if (!Season.IsNull())
|
|
{
|
|
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
|
|
{
|
|
return Asset->GetSeasonSeed();
|
|
}
|
|
}
|
|
return Seed;
|
|
}
|
|
|
|
float UVoxelSettings::GetEffectiveOriginSpineRadius() const
|
|
{
|
|
if (!Season.IsNull())
|
|
{
|
|
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
|
|
{
|
|
return Asset->GetOriginSpineRadius();
|
|
}
|
|
}
|
|
return OriginSpineRadius;
|
|
}
|
|
|
|
float UVoxelSettings::GetEffectiveWorldRadiusVoxels() const
|
|
{
|
|
if (!Season.IsNull())
|
|
{
|
|
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
|
|
{
|
|
return Asset->GetWorldRadiusVoxels();
|
|
}
|
|
}
|
|
return WorldRadiusVoxels;
|
|
}
|
|
|
|
int32 UVoxelSettings::GetEffectiveSeasonNumber() const
|
|
{
|
|
if (!Season.IsNull())
|
|
{
|
|
if (const UVoxelSeasonAsset* Asset = Season.LoadSynchronous())
|
|
{
|
|
return Asset->GetSeasonNumber();
|
|
}
|
|
}
|
|
return CurrentSeason;
|
|
}
|
|
|
|
bool UVoxelSettings::SetWorldAssemblyManifestJson(const FString& InJson, FString& OutReport)
|
|
{
|
|
OutReport.Reset();
|
|
if (InJson.IsEmpty())
|
|
{
|
|
OutReport = TEXT("world assembly manifest JSON is empty");
|
|
return false;
|
|
}
|
|
|
|
FTCHARToUTF8 Utf8Json(*InJson);
|
|
std::string CoreError;
|
|
const std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest =
|
|
VoxelForgeCore::WorldAssemblyManifest::FromJson(
|
|
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
|
|
if (!Manifest)
|
|
{
|
|
OutReport = FString::Printf(TEXT("world assembly manifest is invalid: %s"),
|
|
UTF8_TO_TCHAR(CoreError.c_str()));
|
|
return false;
|
|
}
|
|
if (Manifest->recipe.seed != static_cast<uint32>(GetEffectiveWorldSeed()))
|
|
{
|
|
OutReport = FString::Printf(
|
|
TEXT("world assembly seed %u does not match the effective settings seed %d"),
|
|
Manifest->recipe.seed, GetEffectiveWorldSeed());
|
|
return false;
|
|
}
|
|
|
|
const std::string CanonicalJson = Manifest->ToJson();
|
|
if (CanonicalJson.empty())
|
|
{
|
|
OutReport = TEXT("world assembly manifest could not be canonicalized");
|
|
return false;
|
|
}
|
|
Modify();
|
|
WorldAssemblyManifestJson = UTF8_TO_TCHAR(CanonicalJson.c_str());
|
|
WorldAssemblyManifestHash = FString::Printf(TEXT("%016llx"),
|
|
static_cast<unsigned long long>(Manifest->contentHash));
|
|
bUseWorldAssemblyManifestOverride = true;
|
|
MarkPackageDirty();
|
|
OutReport = FString::Printf(
|
|
TEXT("imported native world assembly %s (%d SurfaceWorld plans, %d floating-island plans)"),
|
|
*WorldAssemblyManifestHash, static_cast<int32>(Manifest->plan.strates.size()),
|
|
static_cast<int32>(Manifest->floatingIslands.size()));
|
|
return true;
|
|
}
|
|
|
|
bool UVoxelSettings::GetWorldAssemblyManifestJson(
|
|
FString& OutCanonicalJson, FString& OutContentHash, FString& OutError) const
|
|
{
|
|
OutCanonicalJson.Reset();
|
|
OutContentHash.Reset();
|
|
OutError.Reset();
|
|
if (!bUseWorldAssemblyManifestOverride)
|
|
{
|
|
return true;
|
|
}
|
|
if (WorldAssemblyManifestJson.IsEmpty())
|
|
{
|
|
if (bUseWorldAssemblyManifestOverride)
|
|
{
|
|
OutError = TEXT("world assembly override is enabled but its manifest JSON is empty");
|
|
return false;
|
|
}
|
|
if (!WorldAssemblyManifestHash.IsEmpty())
|
|
{
|
|
OutError = TEXT("world assembly hash is set but its manifest JSON is empty");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
FTCHARToUTF8 Utf8Json(*WorldAssemblyManifestJson);
|
|
std::string CoreError;
|
|
const std::optional<VoxelForgeCore::WorldAssemblyManifest> Manifest =
|
|
VoxelForgeCore::WorldAssemblyManifest::FromJson(
|
|
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
|
|
if (!Manifest)
|
|
{
|
|
OutError = FString::Printf(TEXT("cooked world assembly manifest is invalid: %s"),
|
|
UTF8_TO_TCHAR(CoreError.c_str()));
|
|
return false;
|
|
}
|
|
if (Manifest->recipe.seed != static_cast<uint32>(GetEffectiveWorldSeed()))
|
|
{
|
|
OutError = TEXT("cooked world assembly seed no longer matches the effective settings seed");
|
|
return false;
|
|
}
|
|
const FString ComputedHash = FString::Printf(TEXT("%016llx"),
|
|
static_cast<unsigned long long>(Manifest->contentHash));
|
|
if (!ComputedHash.Equals(WorldAssemblyManifestHash, ESearchCase::IgnoreCase))
|
|
{
|
|
OutError = FString::Printf(
|
|
TEXT("cooked world assembly hash mismatch (stored %s, computed %s)"),
|
|
*WorldAssemblyManifestHash, *ComputedHash);
|
|
return false;
|
|
}
|
|
|
|
const std::string CanonicalJson = Manifest->ToJson();
|
|
if (CanonicalJson.empty())
|
|
{
|
|
OutError = TEXT("cooked world assembly manifest could not be canonicalized");
|
|
return false;
|
|
}
|
|
OutCanonicalJson = UTF8_TO_TCHAR(CanonicalJson.c_str());
|
|
OutContentHash = ComputedHash;
|
|
return true;
|
|
}
|
|
|
|
void UVoxelSettings::ImportWorldAssemblyManifestJson()
|
|
{
|
|
#if WITH_EDITOR
|
|
FString JsonText;
|
|
FString Report;
|
|
if (SourceWorldAssemblyManifestJson.FilePath.IsEmpty()
|
|
|| !FFileHelper::LoadFileToString(JsonText, *SourceWorldAssemblyManifestJson.FilePath))
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("[VoxelSettings] Could not read world assembly manifest '%s'."),
|
|
*SourceWorldAssemblyManifestJson.FilePath);
|
|
return;
|
|
}
|
|
if (!SetWorldAssemblyManifestJson(JsonText, Report))
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("[VoxelSettings] World assembly import failed: %s"), *Report);
|
|
return;
|
|
}
|
|
UE_LOG(LogTemp, Log, TEXT("[VoxelSettings] %s"), *Report);
|
|
#else
|
|
UE_LOG(LogTemp, Error, TEXT("[VoxelSettings] World assembly JSON import is editor-only."));
|
|
#endif
|
|
}
|