wip(assembler): M3 round 4 -- plans built inside Unreal from the owner's strates; no import needed
SurfaceWorld and FloatingIslands plans generate at startup from the resolved seed, strate types and heights (hashed, cached). Imported JSON is an opt-in override (off by default); an invalid override shows an on-screen notice and falls back to legacy. BuildHost on staged owner assets: SurfaceWorld cross-system checks, islands at owner height (route, collision, anchors, BP edits), seed 137501, W1/W8/W16, Core-off legacy. XY-column cache verified (hashes kept); island LOD0 p95 91 ms vs 133 ms sprint deadline. Not exercised: season-seed path (no Season in staged settings), rendered notice pixels. 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
48fa028086
commit
111cf3d262
21 files changed
+1934
-391
No files matched your search
@@ -152,6 +152,7 @@ public:
|
||||
std::atomic<bool> bShuttingDown{false};
|
||||
std::atomic<uint64> NextEditSequence{1};
|
||||
int32 FullResolutionLodCount = 0;
|
||||
FString InitialFailureReason;
|
||||
};
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -2456,12 +2457,13 @@ void AVoxelWorld::BeginPlay()
|
||||
DiffLayer->SetBudget(Settings->MaxModifications, Settings->MaxBrushRadius, Settings->MaxTotalVolume);
|
||||
Generator->SetDiffLayer(DiffLayer);
|
||||
|
||||
if (bUseVoxelForgeCore && !InitializeVoxelForgeCoreWorld())
|
||||
if (bUseVoxelForgeCore)
|
||||
{
|
||||
// A failed opt-in must stay visibly failed. Falling back to the legacy route here would
|
||||
// make the switch appear to work while silently changing the generated world.
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeCoreRuntime] Core mode is enabled but initialization did not complete."));
|
||||
FString CoreFailureReason;
|
||||
if (!InitializeVoxelForgeCoreWorld(CoreFailureReason))
|
||||
{
|
||||
FallBackToLegacyAfterCoreInitializationFailure(CoreFailureReason);
|
||||
}
|
||||
}
|
||||
|
||||
if (bTestCeilingViewEnabled && IsParameterizedResearchFieldActive())
|
||||
@@ -6596,6 +6598,12 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_Tick); // game-thread streaming orchestration breakdown
|
||||
if (bUseVoxelForgeCore && VoxelForgeCoreState.IsValid()
|
||||
&& VoxelForgeCoreState->bInitialFailed.load(std::memory_order_acquire))
|
||||
{
|
||||
FallBackToLegacyAfterCoreInitializationFailure(
|
||||
VoxelForgeCoreState->InitialFailureReason);
|
||||
}
|
||||
#if WITH_EDITOR
|
||||
if (bTestResearchProfileSwitchConfigured
|
||||
&& !bTestResearchProfileSwitchApplied
|
||||
@@ -7217,6 +7225,14 @@ FVector AVoxelWorld::GetVoxelForgeCoreDefaultCenterWorld() const
|
||||
return GetActorTransform().TransformPosition(CoreFallbackCenterLocalCm);
|
||||
}
|
||||
|
||||
bool AVoxelWorld::GetVoxelForgeCoreWorldAssembly(
|
||||
FString& OutCanonicalJson, FString& OutContentHash) const
|
||||
{
|
||||
OutCanonicalJson = VoxelForgeCoreWorldAssemblyJson;
|
||||
OutContentHash = VoxelForgeCoreWorldAssemblyHash;
|
||||
return bUseVoxelForgeCore && !OutCanonicalJson.IsEmpty() && !OutContentHash.IsEmpty();
|
||||
}
|
||||
|
||||
bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const
|
||||
{
|
||||
OutDensity = 0.0f;
|
||||
@@ -7230,13 +7246,15 @@ bool AVoxelWorld::QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& Out
|
||||
return FMath::IsFinite(OutDensity);
|
||||
}
|
||||
|
||||
bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
|
||||
bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
|
||||
{
|
||||
OutFailureReason.Reset();
|
||||
VoxelForgeCoreWorldAssemblyJson.Reset();
|
||||
VoxelForgeCoreWorldAssemblyHash.Reset();
|
||||
if (!bUseVoxelForgeCore) return true;
|
||||
if (!Settings || !StrateManager)
|
||||
{
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeCoreRuntime] initialization failed: Settings or resolved StrateManager is missing."));
|
||||
OutFailureReason = TEXT("Settings or the resolved strate layout is missing.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7247,7 +7265,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
Settings, StrateManager, ActiveCoreClipRadius, ActiveCoreMaxClipLevel, Json, ExportError))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] snapshot export failed: %s"), *ExportError);
|
||||
OutFailureReason = FString::Printf(TEXT("snapshot export failed: %s"), *ExportError);
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason);
|
||||
return false;
|
||||
}
|
||||
FTCHARToUTF8 Utf8Json(*Json);
|
||||
@@ -7257,12 +7276,22 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
|
||||
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())), CoreError);
|
||||
if (!Snapshot)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] schema-v1 resolve failed: %s"),
|
||||
OutFailureReason = FString::Printf(TEXT("schema-v1 snapshot resolve failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] %s"), *OutFailureReason);
|
||||
return false;
|
||||
}
|
||||
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
|
||||
{
|
||||
const std::string CanonicalAssembly = Assembly->ToJson();
|
||||
if (CanonicalAssembly.empty())
|
||||
{
|
||||
OutFailureReason = TEXT("the generated world assembly could not be serialized canonically.");
|
||||
return false;
|
||||
}
|
||||
VoxelForgeCoreWorldAssemblyJson = UTF8_TO_TCHAR(CanonicalAssembly.c_str());
|
||||
VoxelForgeCoreWorldAssemblyHash = FString::Printf(TEXT("%016llx"),
|
||||
static_cast<unsigned long long>(Assembly->contentHash));
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreRuntime] world_assembly_hash=%016llx recipe_hash=%016llx plan_hash=%016llx surface_strates=%d island_strates=%d"),
|
||||
static_cast<unsigned long long>(Assembly->contentHash),
|
||||
@@ -7294,7 +7323,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
|
||||
{CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z});
|
||||
if (InitialDesired.empty())
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] Core returned an empty initial clipmap."));
|
||||
OutFailureReason = TEXT("Core produced an empty initial clipmap.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7327,7 +7356,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
|
||||
}
|
||||
if (FineMin.x == IntMax || ClipMin.x == IntMax)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] Core initial clipmap bounds are invalid."));
|
||||
OutFailureReason = TEXT("Core produced invalid initial clipmap bounds.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7373,6 +7402,8 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
|
||||
}
|
||||
if (!bCompiled)
|
||||
{
|
||||
State->InitialFailureReason = FString::Printf(
|
||||
TEXT("Core shape compilation failed: %s"), UTF8_TO_TCHAR(Error.c_str()));
|
||||
State->bInitialFailed.store(true, std::memory_order_release);
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreRuntime] shape compile failed: %s"),
|
||||
UTF8_TO_TCHAR(Error.c_str()));
|
||||
@@ -7394,6 +7425,49 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld()
|
||||
return true;
|
||||
}
|
||||
|
||||
void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString& FailureReason)
|
||||
{
|
||||
if (!bUseVoxelForgeCore) return;
|
||||
const FString Reason = FailureReason.IsEmpty()
|
||||
? TEXT("Core initialization failed without a diagnostic reason.") : FailureReason;
|
||||
if (VoxelForgeCoreState.IsValid())
|
||||
{
|
||||
VoxelForgeCoreState->bShuttingDown.store(true, std::memory_order_release);
|
||||
VoxelForgeCoreState.Reset();
|
||||
}
|
||||
VoxelForgeCoreWorldAssemblyJson.Reset();
|
||||
VoxelForgeCoreWorldAssemblyHash.Reset();
|
||||
bUseVoxelForgeCore = false;
|
||||
CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||
if (Mesher != nullptr)
|
||||
{
|
||||
Mesher->SetGeometryVoxelSizeCm(VOXEL_SIZE);
|
||||
}
|
||||
if (ContentManager != nullptr && Settings != nullptr)
|
||||
{
|
||||
ContentManager->Initialize(this, StrateManager, Generator, Settings,
|
||||
Settings->GetEffectiveWorldSeed());
|
||||
ContentManager->ClearAll();
|
||||
}
|
||||
if (AtmosphereManager != nullptr)
|
||||
{
|
||||
AtmosphereManager->Reset();
|
||||
}
|
||||
RegenerateAllChunks();
|
||||
|
||||
UE_LOG(LogTemp, Error,
|
||||
TEXT("[VoxelForgeCoreRuntime] Core initialization failed; falling back to the legacy generator. reason=%s"),
|
||||
*Reason);
|
||||
if (GEngine != nullptr)
|
||||
{
|
||||
const FString ScreenMessage = FString::Printf(
|
||||
TEXT("VoxelForge Core could not start: %s Legacy voxel generation is active."), *Reason);
|
||||
GEngine->AddOnScreenDebugMessage(INDEX_NONE, 20.0f, FColor::Red, ScreenMessage);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreRuntime] fallback_notice=on_screen seconds=20 reason=%s"), *Reason);
|
||||
}
|
||||
}
|
||||
|
||||
bool AVoxelWorld::TryGetPlayerPosition(FVector& OutPosition, APawn** OutPawn) const
|
||||
{
|
||||
OutPosition = FVector::ZeroVector;
|
||||
@@ -13293,12 +13367,43 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
||||
return SeasonHash;
|
||||
}
|
||||
|
||||
FString AssemblyJson;
|
||||
FString AssemblyHash;
|
||||
FString AssemblyError;
|
||||
if (!Settings->GetWorldAssemblyManifestJson(AssemblyJson, AssemblyHash, AssemblyError))
|
||||
FString AssemblyHash = VoxelForgeCoreWorldAssemblyHash;
|
||||
if (AssemblyHash.IsEmpty())
|
||||
{
|
||||
return FString();
|
||||
FString AssemblyJson;
|
||||
FString AssemblyError;
|
||||
if (!Settings->GetWorldAssemblyManifestJson(AssemblyJson, AssemblyHash, AssemblyError))
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
if (AssemblyHash.IsEmpty() && StrateManager != nullptr)
|
||||
{
|
||||
// Multiplayer/content-hash callers can ask before BeginPlay has cached the startup
|
||||
// artifact. Resolve the same deterministic plan from the already-resolved settings
|
||||
// and layout instead of temporarily returning an identity without that plan.
|
||||
FString SnapshotJson;
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(Settings, StrateManager,
|
||||
FMath::Clamp(CoreClipRadius, 1, 64),
|
||||
FMath::Clamp(CoreMaxClipLevel, 0, 8), SnapshotJson, AssemblyError))
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
FTCHARToUTF8 Utf8Snapshot(*SnapshotJson);
|
||||
std::string CoreError;
|
||||
const std::optional<VoxelForgeCore::ResolvedWorldSnapshot> Snapshot =
|
||||
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
|
||||
std::string_view(Utf8Snapshot.Get(),
|
||||
static_cast<std::size_t>(Utf8Snapshot.Length())), CoreError);
|
||||
if (!Snapshot)
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
if (const VoxelForgeCore::WorldAssemblyManifest* Assembly = Snapshot->worldAssembly())
|
||||
{
|
||||
AssemblyHash = FString::Printf(TEXT("%016llx"),
|
||||
static_cast<unsigned long long>(Assembly->contentHash));
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool bHybridMesher = CoreMesherRecipe != EVoxelForgeCoreMesherRecipe::MarchingCubesV1;
|
||||
if (AssemblyHash.IsEmpty() && !bHybridMesher)
|
||||
|
||||
Reference in new issue
Block a user