wip(assembler): M1 round 5 -- native SurfaceWorld manifest delivered into the Unreal Core route
Manifest imported into UVoxelSettings (hash 8933693d9560ba9f), folded into the season content hash only when Core is on. BuildHost: W1/W8/W16 stable, 2553 surface tiles match standalone, 432 LOD transitions, collision on all 36 expected tiles. Matched timing 96.8 s native vs 197.3 s bridge (2.04x). Collision support tiles wait for Core region admission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
2d0cec90e4
commit
f67b519e75
11 files changed
+1002
-45
No files matched your search
@@ -2,9 +2,12 @@
|
||||
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Policies/CondensedJsonPrintPolicy.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
#include "UObject/TextProperty.h"
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include "VoxelSeasonAsset.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
@@ -271,6 +274,95 @@ void WriteCoreJsonValue(FCoreJsonWriter& Writer, const TCHAR* Identifier,
|
||||
Writer.WriteNull();
|
||||
}
|
||||
}
|
||||
|
||||
bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
const UVoxelStrateManager* StrateManager,
|
||||
const TSharedRef<FJsonObject>& Root,
|
||||
FString& OutError)
|
||||
{
|
||||
FString ManifestJson;
|
||||
FString ManifestHash;
|
||||
if (!Settings->GetWorldAssemblyManifestJson(ManifestJson, ManifestHash, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ManifestJson.IsEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
FTCHARToUTF8 Utf8Json(*ManifestJson);
|
||||
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("Core world assembly validation failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (Manifest->recipe.seed != static_cast<uint32>(StrateManager->GetWorldSeed()))
|
||||
{
|
||||
OutError = TEXT("Core world assembly recipe seed does not match the resolved strate layout.");
|
||||
return false;
|
||||
}
|
||||
|
||||
int32 SurfaceSlotCount = 0;
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
if (Slot.Definition != nullptr
|
||||
&& Slot.Definition->GeneratorType == ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
++SurfaceSlotCount;
|
||||
}
|
||||
}
|
||||
if (SurfaceSlotCount == 0
|
||||
|| Manifest->plan.strates.size() != static_cast<std::size_t>(SurfaceSlotCount))
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core world assembly has %d plans for %d resolved SurfaceWorld strates."),
|
||||
static_cast<int32>(Manifest->plan.strates.size()), SurfaceSlotCount);
|
||||
return false;
|
||||
}
|
||||
|
||||
TSet<int32> MatchedStrateIndices;
|
||||
for (const VoxelForgeCore::WorldAssemblyStratumPlanV1& Plan : Manifest->plan.strates)
|
||||
{
|
||||
const FStrateSlot* MatchingSlot = StrateManager->GetLayout().FindByPredicate(
|
||||
[&Plan](const FStrateSlot& Slot) { return Slot.StrateIndex == Plan.strateIndex; });
|
||||
if (MatchingSlot == nullptr || MatchingSlot->Definition == nullptr
|
||||
|| MatchingSlot->Definition->GeneratorType != ECaveGeneratorType::SurfaceWorld
|
||||
|| MatchingSlot->TopChunkZ != Plan.topChunkZ
|
||||
|| MatchingSlot->BottomChunkZ != Plan.bottomChunkZ
|
||||
|| MatchedStrateIndices.Contains(Plan.strateIndex))
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core world assembly plan for strate %d does not exactly match a unique resolved SurfaceWorld slot."),
|
||||
Plan.strateIndex);
|
||||
return false;
|
||||
}
|
||||
MatchedStrateIndices.Add(Plan.strateIndex);
|
||||
}
|
||||
|
||||
const std::string CanonicalJson = Manifest->ToJson();
|
||||
if (CanonicalJson.empty())
|
||||
{
|
||||
OutError = TEXT("Core world assembly manifest could not be canonicalized for the snapshot.");
|
||||
return false;
|
||||
}
|
||||
const FString CanonicalText = UTF8_TO_TCHAR(CanonicalJson.c_str());
|
||||
TSharedPtr<FJsonObject> ManifestObject;
|
||||
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(CanonicalText);
|
||||
if (!FJsonSerializer::Deserialize(Reader, ManifestObject) || !ManifestObject.IsValid())
|
||||
{
|
||||
OutError = TEXT("Core world assembly canonical JSON could not be embedded in the snapshot.");
|
||||
return false;
|
||||
}
|
||||
Root->SetObjectField(TEXT("world_assembly"), ManifestObject.ToSharedRef());
|
||||
Root->SetStringField(TEXT("world_assembly_content_hash"), ManifestHash);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
@@ -380,6 +472,10 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
Passages.Add(MakeShared<FJsonValueObject>(CorePassageJson(Passage)));
|
||||
}
|
||||
Root->SetArrayField(TEXT("passages"), MoveTemp(Passages));
|
||||
if (!AttachValidatedWorldAssembly(Settings, StrateManager, Root, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Root->SetArrayField(TEXT("density_samples"), {});
|
||||
TSharedRef<FJsonObject> ViewerStart = MakeShared<FJsonObject>();
|
||||
ViewerStart->SetBoolField(TEXT("found_open_space"), false);
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
#include "VoxelSeasonAsset.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "Misc/FileHelper.h"
|
||||
#endif
|
||||
|
||||
#include <VoxelForgeCore/VoxelForgeCore.h>
|
||||
#include <string>
|
||||
|
||||
int32 UVoxelSettings::GetEffectiveWorldSeed() const
|
||||
{
|
||||
if (!Season.IsNull())
|
||||
@@ -49,3 +56,123 @@ int32 UVoxelSettings::GetEffectiveSeasonNumber() const
|
||||
}
|
||||
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));
|
||||
MarkPackageDirty();
|
||||
OutReport = FString::Printf(TEXT("imported native world assembly %s (%d stratum plans)"),
|
||||
*WorldAssemblyManifestHash, static_cast<int32>(Manifest->plan.strates.size()));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UVoxelSettings::GetWorldAssemblyManifestJson(
|
||||
FString& OutCanonicalJson, FString& OutContentHash, FString& OutError) const
|
||||
{
|
||||
OutCanonicalJson.Reset();
|
||||
OutContentHash.Reset();
|
||||
OutError.Reset();
|
||||
if (WorldAssemblyManifestJson.IsEmpty())
|
||||
{
|
||||
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
|
||||
}
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "Engine/World.h"
|
||||
#include "Misc/IQueuedWork.h"
|
||||
#include "Misc/QueuedThreadPool.h"
|
||||
#include "Misc/SecureHash.h"
|
||||
#include "RealtimeMeshCollisionLibrary.h"
|
||||
#include "Camera/PlayerCameraManager.h"
|
||||
#include "Async/Async.h"
|
||||
@@ -7259,6 +7260,15 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreRuntime] world_assembly_hash=%016llx recipe_hash=%016llx plan_hash=%016llx strates=%d"),
|
||||
static_cast<unsigned long long>(Assembly->contentHash),
|
||||
static_cast<unsigned long long>(Assembly->recipe.contentHash),
|
||||
static_cast<unsigned long long>(Assembly->plan.contentHash),
|
||||
static_cast<int32>(Assembly->plan.strates.size()));
|
||||
}
|
||||
|
||||
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State =
|
||||
MakeShared<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe>();
|
||||
@@ -7786,6 +7796,18 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
|
||||
}
|
||||
bForceDesiredRebuild |= bNeedsDesiredRebuild;
|
||||
|
||||
// Core surface tiles must enter through the desired-set region compile before collision work.
|
||||
// Les tuiles de surface Core doivent passer par la compilation de région avant la collision.
|
||||
if (bUseVoxelForgeCore && !bDesiredAtRequest)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCollisionGate] support_request_deferred tile=(%d,%d,%d) "
|
||||
"reason=core_region_admission desired=0"),
|
||||
SupportTile.Coord.X, SupportTile.Coord.Y, SupportTile.Coord.Z);
|
||||
bPawnGateSupportRequestReported = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Preserve the established critical floor ordering when it already belongs to this desired
|
||||
// epoch. Before the first build there is no array to promote into; LoadTile below still uses
|
||||
// the same CollisionCritical work class and priority queue.
|
||||
@@ -13222,7 +13244,47 @@ int32 AVoxelWorld::GetCurrentSeason() const
|
||||
|
||||
FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
{
|
||||
return StrateManager ? StrateManager->GetSeasonContentHash() : FString();
|
||||
const FString SeasonHash = StrateManager ? StrateManager->GetSeasonContentHash() : FString();
|
||||
// Only active Core generation includes the optional native surface manifest in world identity.
|
||||
// Seul le Core actif inclut le manifeste de surface natif dans l'identite du monde.
|
||||
if (!Settings || !bUseVoxelForgeCore)
|
||||
{
|
||||
return SeasonHash;
|
||||
}
|
||||
|
||||
FString AssemblyJson;
|
||||
FString AssemblyHash;
|
||||
FString AssemblyError;
|
||||
if (!Settings->GetWorldAssemblyManifestJson(AssemblyJson, AssemblyHash, AssemblyError))
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
if (AssemblyHash.IsEmpty())
|
||||
{
|
||||
return SeasonHash;
|
||||
}
|
||||
|
||||
FString Payload = SeasonHash;
|
||||
if (Payload.IsEmpty())
|
||||
{
|
||||
Payload = FString::Printf(TEXT("seed=%d"), StrateManager ? StrateManager->GetWorldSeed()
|
||||
: Settings->GetEffectiveWorldSeed());
|
||||
if (StrateManager)
|
||||
{
|
||||
for (const FStrateSlot& Slot : StrateManager->GetLayout())
|
||||
{
|
||||
Payload += FString::Printf(TEXT("|strate=%d,%d,%d,%s"), Slot.StrateIndex,
|
||||
Slot.TopChunkZ, Slot.BottomChunkZ,
|
||||
Slot.Definition ? *Slot.Definition->GetPathName() : TEXT("missing"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Payload += TEXT("|world_assembly=");
|
||||
Payload += AssemblyHash;
|
||||
FTCHARToUTF8 Utf8Payload(*Payload);
|
||||
uint8 Digest[FSHA1::DigestSize];
|
||||
FSHA1::HashBuffer(Utf8Payload.Get(), Utf8Payload.Length(), Digest);
|
||||
return BytesToHex(Digest, FSHA1::DigestSize);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
|
||||
Reference in new issue
Block a user