diff --git a/Source/VoxelForge/Private/VoxelContentManager.cpp b/Source/VoxelForge/Private/VoxelContentManager.cpp index 672db11..8d2600f 100644 --- a/Source/VoxelForge/Private/VoxelContentManager.cpp +++ b/Source/VoxelForge/Private/VoxelContentManager.cpp @@ -128,6 +128,110 @@ struct FCoreSurfaceSnapshot } }; +struct FCoreFloatingIslandFieldSnapshot +{ + VoxelForgeCore::FloatingIslandRecipeV1 Recipe; + VoxelForgeCore::FloatingIslandPlanV1 Plan; + TUniquePtr Field; +}; + +struct FCoreFloatingIslandSnapshot +{ + TMap> Fields; + + const FCoreFloatingIslandFieldSnapshot* FindField(int32 StrateIndex) const + { + const auto* Found = Fields.Find(StrateIndex); + return Found != nullptr && Found->IsValid() ? Found->Get() : nullptr; + } + + bool GetDensity(int32 StrateIndex, float LegacyVoxelX, float LegacyVoxelY, + float LegacyVoxelZ, float& OutDensity) const + { + OutDensity = 1.0f; + const FCoreFloatingIslandFieldSnapshot* Entry = FindField(StrateIndex); + if (Entry == nullptr || !Entry->Field.IsValid() + || !FMath::IsFinite(LegacyVoxelX) || !FMath::IsFinite(LegacyVoxelY) + || !FMath::IsFinite(LegacyVoxelZ)) + { + return false; + } + // A legacy voxel is 128 subvoxels at 12.5 cm; Core field distances are converted back to legacy units. + // Un voxel historique vaut 128 sous-voxels a 12,5 cm ; la distance Core revient en unites historiques. + constexpr double LegacyVoxelSubvoxels = 128.0; + constexpr double MaximumInput = static_cast(MAX_int64) / LegacyVoxelSubvoxels; + if (FMath::Abs(static_cast(LegacyVoxelX)) > MaximumInput + || FMath::Abs(static_cast(LegacyVoxelY)) > MaximumInput + || FMath::Abs(static_cast(LegacyVoxelZ)) > MaximumInput) + { + return false; + } + const int64 Density = Entry->Field->densitySubvoxels( + static_cast(std::llround(static_cast(LegacyVoxelX) * LegacyVoxelSubvoxels)), + static_cast(std::llround(static_cast(LegacyVoxelY) * LegacyVoxelSubvoxels)), + static_cast(std::llround(static_cast(LegacyVoxelZ) * LegacyVoxelSubvoxels))); + OutDensity = static_cast(static_cast(Density) / LegacyVoxelSubvoxels); + return FMath::IsFinite(OutDensity); + } + + bool GetTopAnchor(int32 StrateIndex, const FVector2D& LocalXYCm, + float& OutGroundLocalZCm) const + { + OutGroundLocalZCm = 0.0f; + const FCoreFloatingIslandFieldSnapshot* Entry = FindField(StrateIndex); + if (Entry == nullptr || !Entry->Field.IsValid() + || Entry->Plan.islands.empty() + || !FMath::IsFinite(LocalXYCm.X) || !FMath::IsFinite(LocalXYCm.Y)) + { + return false; + } + constexpr double SubvoxelsPerCoreVoxel = 256.0; + constexpr double CentimetersPerCoreVoxel = 25.0; + const double XSubvoxels = static_cast(LocalXYCm.X) + * SubvoxelsPerCoreVoxel / CentimetersPerCoreVoxel; + const double YSubvoxels = static_cast(LocalXYCm.Y) + * SubvoxelsPerCoreVoxel / CentimetersPerCoreVoxel; + constexpr double MaximumInput = static_cast(MAX_int64) - 1024.0; + if (FMath::Abs(XSubvoxels) > MaximumInput || FMath::Abs(YSubvoxels) > MaximumInput) + { + return false; + } + const int64 X = static_cast(std::llround(XSubvoxels)); + const int64 Y = static_cast(std::llround(YSubvoxels)); + int64 SearchTop = Entry->Plan.islands.front().topZSubvoxels; + int64 SearchBottom = Entry->Plan.islands.front().bottomZSubvoxels; + for (const auto& Island : Entry->Plan.islands) + { + SearchTop = FMath::Max(SearchTop, Island.topZSubvoxels); + SearchBottom = FMath::Min(SearchBottom, Island.bottomZSubvoxels); + } + SearchTop += 4 * 256; + SearchBottom -= 4 * 256; + constexpr int64 ScanStepSubvoxels = 128; + int64 PreviousZ = SearchTop; + int64 PreviousDensity = Entry->Field->densitySubvoxels(X, Y, PreviousZ); + if (PreviousDensity < 0) return false; + for (int64 Z = SearchTop - ScanStepSubvoxels; Z >= SearchBottom; Z -= ScanStepSubvoxels) + { + const int64 Density = Entry->Field->densitySubvoxels(X, Y, Z); + if (PreviousDensity >= 0 && Density < 0) + { + const double Denominator = static_cast(PreviousDensity - Density); + const double Alpha = Denominator > 0.0 + ? static_cast(PreviousDensity) / Denominator : 0.5; + const double SurfaceSubvoxels = static_cast(PreviousZ) + + static_cast(Z - PreviousZ) * Alpha; + OutGroundLocalZCm = static_cast(SurfaceSubvoxels + * CentimetersPerCoreVoxel / SubvoxelsPerCoreVoxel); + return FMath::IsFinite(OutGroundLocalZCm); + } + PreviousZ = Z; + PreviousDensity = Density; + } + return false; + } +}; + // Global safety cap on spawned decoration ACTORS per cell (across all entries). // HISM instances are exempt — they're batched render data, capped per entry by MaxPerChunk. static constexpr int32 GMaxDecorationActorsPerCell = 400; @@ -174,6 +278,7 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt Settings = InSettings; Seed = InSeed; NativeSurfaceSnapshot.Reset(); + NativeFloatingIslandSnapshot.Reset(); if (Settings) { @@ -196,6 +301,33 @@ void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InSt Snapshot->Plans.Add(Plan.strateIndex, Plan); } NativeSurfaceSnapshot = Snapshot; + + if (!Manifest->floatingIslands.empty()) + { + TSharedPtr IslandSnapshot = + MakeShared(); + for (const auto& IslandPlan : Manifest->floatingIslands) + { + TSharedPtr Entry = + MakeShared(); + Entry->Recipe = IslandPlan.manifest.recipe; + Entry->Plan = IslandPlan.manifest.plan; + Entry->Field = MakeUnique( + Entry->Recipe, Entry->Plan); + if (!Entry->Field->isValid()) + { + UE_LOG(LogTemp, Warning, + TEXT("[VoxelContentManager] Core floating-island field rejected strate %d."), + IslandPlan.strateIndex); + continue; + } + IslandSnapshot->Fields.Add(IslandPlan.strateIndex, Entry); + } + if (!IslandSnapshot->Fields.IsEmpty()) + { + NativeFloatingIslandSnapshot = IslandSnapshot; + } + } } else { @@ -244,6 +376,66 @@ bool UVoxelContentManager::QueryNativeSurfaceAnchors( return true; } +bool UVoxelContentManager::QueryNativeFloatingIslandAnchor( + int32 StrateIndex, const FVector2D& LocalXYCm, float& OutGroundLocalZCm) const +{ + OutGroundLocalZCm = 0.0f; + const AVoxelWorld* VoxelWorld = Cast(Owner.Get()); + if (VoxelWorld == nullptr || !VoxelWorld->bUseVoxelForgeCore + || !NativeFloatingIslandSnapshot.IsValid()) + { + return false; + } + return NativeFloatingIslandSnapshot->GetTopAnchor( + StrateIndex, LocalXYCm, OutGroundLocalZCm); +} + +bool UVoxelContentManager::QueryStrateWaterAnchor( + int32 StrateIndex, bool& bOutHasWater, float& OutWaterLocalZCm) const +{ + bOutHasWater = false; + OutWaterLocalZCm = -FLT_MAX; + const AVoxelWorld* VoxelWorld = Cast(Owner.Get()); + if (VoxelWorld == nullptr || StrateManager == nullptr) + { + return false; + } + + if (VoxelWorld->bUseVoxelForgeCore && NativeSurfaceSnapshot.IsValid()) + { + bool bHasPlan = false; + bool bEnabled = false; + float NativeWaterLocalZCm = -FLT_MAX; + if (NativeSurfaceSnapshot->GetWater( + StrateIndex, bHasPlan, bEnabled, NativeWaterLocalZCm) && bHasPlan) + { + bOutHasWater = bEnabled; + OutWaterLocalZCm = bEnabled ? NativeWaterLocalZCm : -FLT_MAX; + return true; + } + } + + for (const FStrateSlot& Slot : StrateManager->GetLayout()) + { + if (Slot.StrateIndex != StrateIndex) + { + continue; + } + const int32 MidChunkZ = Slot.BottomChunkZ + + (Slot.TopChunkZ - Slot.BottomChunkZ) / 2; + const float ManagerWaterVoxelZ = StrateManager->GetWaterLevelWorldZForChunk( + FIntVector(0, 0, MidChunkZ)); + if (ManagerWaterVoxelZ != -FLT_MAX) + { + bOutHasWater = true; + const float ManagerWaterLocalZCm = ManagerWaterVoxelZ * VOXEL_SIZE; + OutWaterLocalZCm = VoxelWorld->FromStrateManagerLocalZ(ManagerWaterLocalZCm); + } + return true; + } + return false; +} + void UVoxelContentManager::BeginDestroy() { // Backstop: stop any worker from touching us. EndPlay → NotifyShutdown should have already drained. @@ -395,6 +587,11 @@ void UVoxelContentManager::UpdateDecorations(const FVector& PlayerWorldPos) && CurrentCtx.bSurfaceWorld && CurrentCtx.NativeSurface.IsValid() && CurrentCtx.NativeSurface->FindPlan(StrateIndex) != nullptr; + CurrentCtx.NativeIslands = NativeFloatingIslandSnapshot; + CurrentCtx.bNativeFloatingIslands = VoxelWorld != nullptr && VoxelWorld->bUseVoxelForgeCore + && StrateManager->GetGeneratorTypeForChunk(RepChunk) == ECaveGeneratorType::FloatingIslands + && CurrentCtx.NativeIslands.IsValid() + && CurrentCtx.NativeIslands->FindField(StrateIndex) != nullptr; { bool bHasNativePlan = false; bool bNativeWaterEnabled = false; @@ -710,7 +907,17 @@ void UVoxelContentManager::BuildCellSpawns(const UVoxelGenerator* Gen, const FTr FChunkBiomeCache BiomeCache; const bool bHasBiomes = Ctx.BiomeCtx.IsValid(); - auto D = [&](float VX, float VY, float VZ) { return Gen->GetDensityAt(VX, VY, VZ); }; + auto D = [&](float VX, float VY, float VZ) + { + if (Ctx.bNativeFloatingIslands && Ctx.NativeIslands.IsValid()) + { + float NativeDensity = 1.0f; + if (Ctx.NativeIslands->GetDensity(Ctx.StrateIndex, VX, VY, VZ, NativeDensity)) + return NativeDensity; + return 1.0f; + } + return Gen->GetDensityAt(VX, VY, VZ); + }; auto GetSurface = [&](float VX, float VY, float& OutGround, float& OutCap) { if (Ctx.bNativeSurface && Ctx.NativeSurface.IsValid()) @@ -1338,6 +1545,21 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const { if (!Gen) return false; + const auto Density = [&](float SampleX, float SampleY, float SampleZ) + { + if (Ctx.bNativeFloatingIslands && Ctx.NativeIslands.IsValid()) + { + float NativeDensity = 1.0f; + if (Ctx.NativeIslands->GetDensity( + Ctx.StrateIndex, SampleX, SampleY, SampleZ, NativeDensity)) + { + return NativeDensity; + } + return 1.0f; + } + return Gen->GetDensityAt(SampleX, SampleY, SampleZ); + }; + if (Ctx.bSurfaceWorld) { float hC, cC; @@ -1368,7 +1590,7 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const } // Floor / Wall / Any → the terrain top. if (!(hC >= Ctx.BottomVoxelZ && hC <= Ctx.TopVoxelZ)) return false; - if (!Ctx.bNativeSurface && Gen->GetDensityAt(VX, VY, hC) > 0.5f) return false; + if (!Ctx.bNativeSurface && Density(VX, VY, hC) > 0.5f) return false; float d, hXp, hXm, hYp, hYm; GetSurface(VX + 1.0f, VY, hXp, d); GetSurface(VX - 1.0f, VY, hXm, d); @@ -1384,19 +1606,19 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const // Bounded by ColDepth like the decoration march (this runs SYNCHRONOUSLY on the game thread): // once past open air, a solid run longer than ColDepth means bedrock down to the strate floor — // stop instead of paying GetDensityAt across the whole remaining band. - float PrevD = Gen->GetDensityAt(VX, VY, Ctx.TopVoxelZ); + float PrevD = Density(VX, VY, Ctx.TopVoxelZ); bool bSeenAir = (PrevD >= 0.0f); float SolidRun = 0.0f; for (float Z = Ctx.TopVoxelZ - Step; Z >= Ctx.BottomVoxelZ; Z -= Step) { - const float Dz = Gen->GetDensityAt(VX, VY, Z); + const float Dz = Density(VX, VY, Z); if ((PrevD >= 0.0f) != (Dz >= 0.0f)) // air ↔ solid crossing { float ZLo = Z, ZHi = Z + Step, DHi = PrevD, DLo = Dz; for (int32 It = 0; It < 4; ++It) { const float ZM = 0.5f * (ZLo + ZHi); - const float DM = Gen->GetDensityAt(VX, VY, ZM); + const float DM = Density(VX, VY, ZM); if ((DM >= 0.0f) == (DHi >= 0.0f)) { ZHi = ZM; DHi = DM; } else { ZLo = ZM; DLo = DM; } } @@ -1405,9 +1627,9 @@ bool UVoxelContentManager::FindLandmarkColumn(const UVoxelGenerator* Gen, const const float ZC = ZLo + (ZHi - ZLo) * T; const FVector LocalGrad( - Gen->GetDensityAt(VX + 1.0f, VY, ZC) - Gen->GetDensityAt(VX - 1.0f, VY, ZC), - Gen->GetDensityAt(VX, VY + 1.0f, ZC) - Gen->GetDensityAt(VX, VY - 1.0f, ZC), - Gen->GetDensityAt(VX, VY, ZC + 1.0f) - Gen->GetDensityAt(VX, VY, ZC - 1.0f)); + Density(VX + 1.0f, VY, ZC) - Density(VX - 1.0f, VY, ZC), + Density(VX, VY + 1.0f, ZC) - Density(VX, VY - 1.0f, ZC), + Density(VX, VY, ZC + 1.0f) - Density(VX, VY, ZC - 1.0f)); FVector N = OwnerXf.TransformVectorNoScale(LocalGrad).GetSafeNormal(); if (N.IsNearlyZero()) N = FVector::UpVector; diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp index 391d809..08e1cf7 100644 --- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp @@ -18,6 +18,7 @@ namespace { using FCoreJsonWriter = TJsonWriter>; constexpr double CoreVoxelSizeCm = 25.0; +constexpr double OwnerVoxelSizeCm = 12.5; TSharedPtr SerializeCoreStruct( const UStruct* Type, const void* Data, TSet& ActiveObjects, @@ -309,20 +310,32 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, } int32 SurfaceSlotCount = 0; + int32 FloatingIslandSlotCount = 0; for (const FStrateSlot& Slot : StrateManager->GetLayout()) { - if (Slot.Definition != nullptr - && Slot.Definition->GeneratorType == ECaveGeneratorType::SurfaceWorld) + if (Slot.Definition == nullptr) + { + continue; + } + const ECaveGeneratorType Archetype = StrateManager->GetGeneratorTypeForChunk( + FIntVector(0, 0, Slot.TopChunkZ)); + if (Archetype == ECaveGeneratorType::SurfaceWorld) { ++SurfaceSlotCount; } + else if (Archetype == ECaveGeneratorType::FloatingIslands) + { + ++FloatingIslandSlotCount; + } } - if (SurfaceSlotCount == 0 - || Manifest->plan.strates.size() != static_cast(SurfaceSlotCount)) + if (Manifest->plan.strates.size() != static_cast(SurfaceSlotCount) + || Manifest->floatingIslands.size() != static_cast(FloatingIslandSlotCount)) { OutError = FString::Printf( - TEXT("Core world assembly has %d plans for %d resolved SurfaceWorld strates."), - static_cast(Manifest->plan.strates.size()), SurfaceSlotCount); + TEXT("Core world assembly has %d SurfaceWorld and %d floating-island plans for %d resolved SurfaceWorld and %d FloatingIslands strates."), + static_cast(Manifest->plan.strates.size()), + static_cast(Manifest->floatingIslands.size()), + SurfaceSlotCount, FloatingIslandSlotCount); return false; } @@ -332,7 +345,8 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, 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 + || StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, Plan.topChunkZ)) + != ECaveGeneratorType::SurfaceWorld || MatchingSlot->TopChunkZ != Plan.topChunkZ || MatchingSlot->BottomChunkZ != Plan.bottomChunkZ || MatchedStrateIndices.Contains(Plan.strateIndex)) @@ -345,6 +359,25 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings, MatchedStrateIndices.Add(Plan.strateIndex); } + for (const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1& Plan : Manifest->floatingIslands) + { + const FStrateSlot* MatchingSlot = StrateManager->GetLayout().FindByPredicate( + [&Plan](const FStrateSlot& Slot) { return Slot.StrateIndex == Plan.strateIndex; }); + if (MatchingSlot == nullptr || MatchingSlot->Definition == nullptr + || StrateManager->GetGeneratorTypeForChunk(FIntVector(0, 0, Plan.topChunkZ)) + != ECaveGeneratorType::FloatingIslands + || MatchingSlot->TopChunkZ != Plan.topChunkZ + || MatchingSlot->BottomChunkZ != Plan.bottomChunkZ + || MatchedStrateIndices.Contains(Plan.strateIndex)) + { + OutError = FString::Printf( + TEXT("Core floating-island plan for strate %d does not exactly match a unique resolved FloatingIslands slot."), + Plan.strateIndex); + return false; + } + MatchedStrateIndices.Add(Plan.strateIndex); + } + const std::string CanonicalJson = Manifest->ToJson(); if (CanonicalJson.empty()) { @@ -403,6 +436,9 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld( Root->SetNumberField(TEXT("schema_version"), 1); Root->SetStringField(TEXT("source"), TEXT("VoxelForge Runtime Core snapshot")); Root->SetNumberField(TEXT("voxel_size_cm"), CoreVoxelSizeCm); + Root->SetNumberField(TEXT("owner_voxel_size_cm"), OwnerVoxelSizeCm); + Root->SetNumberField(TEXT("core_voxel_size_cm"), CoreVoxelSizeCm); + Root->SetNumberField(TEXT("owner_to_core_length_scale"), OwnerVoxelSizeCm / CoreVoxelSizeCm); Root->SetNumberField(TEXT("seed"), Seed); Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry); Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius); diff --git a/Source/VoxelForge/Private/VoxelSettings.cpp b/Source/VoxelForge/Private/VoxelSettings.cpp index 1c85bcd..b6d8689 100644 --- a/Source/VoxelForge/Private/VoxelSettings.cpp +++ b/Source/VoxelForge/Private/VoxelSettings.cpp @@ -96,8 +96,10 @@ bool UVoxelSettings::SetWorldAssemblyManifestJson(const FString& InJson, FString WorldAssemblyManifestHash = FString::Printf(TEXT("%016llx"), static_cast(Manifest->contentHash)); MarkPackageDirty(); - OutReport = FString::Printf(TEXT("imported native world assembly %s (%d stratum plans)"), - *WorldAssemblyManifestHash, static_cast(Manifest->plan.strates.size())); + OutReport = FString::Printf( + TEXT("imported native world assembly %s (%d SurfaceWorld plans, %d floating-island plans)"), + *WorldAssemblyManifestHash, static_cast(Manifest->plan.strates.size()), + static_cast(Manifest->floatingIslands.size())); return true; } diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index d6daa39..5e9cbd0 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -7264,11 +7264,12 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld() 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"), + TEXT("[VoxelForgeCoreRuntime] world_assembly_hash=%016llx recipe_hash=%016llx plan_hash=%016llx surface_strates=%d island_strates=%d"), static_cast(Assembly->contentHash), static_cast(Assembly->recipe.contentHash), static_cast(Assembly->plan.contentHash), - static_cast(Assembly->plan.strates.size())); + static_cast(Assembly->plan.strates.size()), + static_cast(Assembly->floatingIslands.size())); } TSharedPtr State = @@ -13285,8 +13286,8 @@ int32 AVoxelWorld::GetCurrentSeason() const FString AVoxelWorld::GetCurrentSeasonContentHash() const { 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. + // Only active Core generation includes the optional native world assembly in world identity. + // Seul le Core actif inclut l'assemblage natif facultatif dans l'identite du monde. if (!Settings || !bUseVoxelForgeCore) { return SeasonHash; diff --git a/Source/VoxelForge/Public/VoxelAtmosphereManager.h b/Source/VoxelForge/Public/VoxelAtmosphereManager.h index 97914aa..5653218 100644 --- a/Source/VoxelForge/Public/VoxelAtmosphereManager.h +++ b/Source/VoxelForge/Public/VoxelAtmosphereManager.h @@ -34,6 +34,9 @@ public: * dominant-biome change. */ void UpdateForPlayer(const FVector& PlayerWorldPos); + /** Read the player's current atmosphere strate for isolated Core validation. / Lit la strate d'atmosphere active pour le test Core isole. */ + int32 GetCurrentStrateIndex() const { return CurrentStrateIndex; } + /** Tear down spawned layer actors + reset (season reset / shutdown). */ void Reset(); diff --git a/Source/VoxelForge/Public/VoxelContentManager.h b/Source/VoxelForge/Public/VoxelContentManager.h index b07d2ac..9899de3 100644 --- a/Source/VoxelForge/Public/VoxelContentManager.h +++ b/Source/VoxelForge/Public/VoxelContentManager.h @@ -43,6 +43,8 @@ #include "CoreMinimal.h" #include "Containers/Queue.h" + +struct FCoreFloatingIslandSnapshot; #include "VoxelTypes.h" #include "VoxelStrateTypes.h" // FStrateDecoration (resolved per dominant biome) #include "VoxelBiomeTypes.h" // FBiomeContext (per-column biome resolve on the worker) @@ -103,6 +105,16 @@ public: float& OutGroundLocalZCm, bool& bOutHasWater, float& OutWaterLocalZCm) const; + /** Find the top air-to-rock crossing in a Core FloatingIslands plan for content placement. + * / Trouve le premier contact air-roche d'un plan Core FloatingIslands pour placer le contenu. */ + bool QueryNativeFloatingIslandAnchor(int32 StrateIndex, const FVector2D& LocalXYCm, + float& OutGroundLocalZCm) const; + + /** Read the effective water datum used by this strate's content plane. + * / Lit le niveau d'eau effectif utilise par le plan de contenu de cette strate. */ + bool QueryStrateWaterAnchor(int32 StrateIndex, bool& bOutHasWater, + float& OutWaterLocalZCm) const; + //--- DECORATIONS (distance-based world grid, async march) ---------------- /** Stream decoration cells around the player: recompute the desired set on cell/strate change, * launch async march tasks (capped), and apply finished results budgeted. Call every Tick. */ @@ -254,6 +266,8 @@ private: bool bSurfaceWorld = false; // heightfield archetype → use the GetSurfaceHeightAt oracle bool bNativeSurface = false; // Core manifest is the height oracle and water datum / Le manifeste Core definit le relief et le niveau d'eau. TSharedPtr NativeSurface; + bool bNativeFloatingIslands = false; // islands use the Core volume for column snaps / les iles utilisent le volume Core pour les ancres. + TSharedPtr NativeIslands; // Strate biome field (PODs only → worker-safe). Empty ⇒ biomes disabled for this strate. The // worker resolves the dominant biome PER COLUMN (ResolveBiomeSampleAt) so decoration borders @@ -349,6 +363,7 @@ private: // Immutable Core manifest snapshot captured by decoration workers; workers never touch UObjects. // Instantane immuable du manifeste Core pour les workers de decoration; aucun worker ne touche aux UObjects. TSharedPtr NativeSurfaceSnapshot; + TSharedPtr NativeFloatingIslandSnapshot; int32 Seed = 0; diff --git a/Source/VoxelForge/Public/VoxelSettings.h b/Source/VoxelForge/Public/VoxelSettings.h index 1e9e0b2..3fd7650 100644 --- a/Source/VoxelForge/Public/VoxelSettings.h +++ b/Source/VoxelForge/Public/VoxelSettings.h @@ -373,10 +373,10 @@ public: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Voxel|Strates") TSoftObjectPtr Season; - // Optional reviewed Core surface plan. The canonical JSON stays in this cooked settings asset; - // its Core FNV digest is included in GetCurrentSeasonContentHash when present. - // Plan de surface Core facultatif; le JSON canonique est cuit avec les reglages et son hash - // Core est inclus dans GetCurrentSeasonContentHash lorsqu'il est present. + // Optional reviewed Core SurfaceWorld and FloatingIslands plans. Canonical JSON stays in this + // cooked settings asset; its Core FNV digest enters the season hash only while Core is enabled. + // Plans Core SurfaceWorld et FloatingIslands facultatifs. Le JSON canonique est cuit ici ; + // son hash FNV Core entre dans le hash de saison uniquement lorsque Core est actif. UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Voxel|Strates", meta = (MultiLine = true)) FString WorldAssemblyManifestJson; diff --git a/Source/VoxelForge/Public/VoxelStrateTypes.h b/Source/VoxelForge/Public/VoxelStrateTypes.h index 5578cf4..a7464f3 100644 --- a/Source/VoxelForge/Public/VoxelStrateTypes.h +++ b/Source/VoxelForge/Public/VoxelStrateTypes.h @@ -1741,7 +1741,8 @@ struct VOXELFORGE_API FFloatingIslandParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0", ClampMax = "1.0")) float IslandDensity = 0.5f; - // Island horizontal radius range (voxels): 12-24 m landmass diameters by default. + // Owner scale: 24–48 legacy voxels = 3–6 m radius at 12.5 cm/voxel (6–12 m diameter). + // Echelle proprietaire : 24–48 voxels legacy = rayon de 3–6 m a 12,5 cm/voxel (diametre 6–12 m). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "2.0")) float IslandMinRadius = 24.0f; @@ -1765,7 +1766,8 @@ struct VOXELFORGE_API FFloatingIslandParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0")) float SurfaceRoughness = 3.0f; - // SmoothMin blend radius for merging overlapping islands (voxels): 1.5 m by default. + // SmoothMin blend radius: 6 legacy voxels = 0.75 m at the owner's 12.5 cm scale. + // Rayon de fusion SmoothMin : 6 voxels legacy = 0,75 m a l'echelle proprietaire de 12,5 cm. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0")) float SDFBlendRadius = 6.0f; diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 8e6ff3f..de70838 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -11,7 +11,10 @@ #include "Engine/Blueprint.h" #include "Containers/StringConv.h" #include "Components/SceneComponent.h" +#include "Components/HierarchicalInstancedStaticMeshComponent.h" #include "Engine/Engine.h" +#include "Engine/Player.h" +#include "Engine/StaticMesh.h" #include "Engine/GameInstance.h" #include "Engine/World.h" #include "GameFramework/Character.h" @@ -59,6 +62,7 @@ #include "VoxelNoise.h" #include "VoxelPassageGeometry.h" #include "VoxelSettings.h" +#include "VoxelAtmosphereManager.h" #include "VoxelSeasonAsset.h" #include "VoxelStackSampler.h" #include "VoxelStrateDefinition.h" @@ -78,6 +82,7 @@ #include #include #include +#include #include #include #include @@ -798,6 +803,7 @@ struct FExploreArguments bool bCoreHybridFixturePlacement = false; bool bCoreHybridCliffOnly = false; bool bCoreHybridFixturesOnly = false; + bool bCoreFloatingIslandTest = false; bool bRender = true; bool bWalk = true; bool bExport = true; @@ -910,6 +916,9 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr int32 CoreWorldTest = 0; FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest); OutArguments.bCoreWorldTest = CoreWorldTest != 0; + int32 CoreFloatingIslandTest = 0; + FParse::Value(*Params, TEXT("coreislandtest="), CoreFloatingIslandTest); + OutArguments.bCoreFloatingIslandTest = CoreFloatingIslandTest != 0; int32 CoreHybridMesher = 0; FParse::Value(*Params, TEXT("corehybridmesher="), CoreHybridMesher); if (CoreHybridMesher < 0 || CoreHybridMesher > 2) @@ -1465,7 +1474,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr return false; } if ((OutArguments.bCoreHybridMesher || OutArguments.bCoreHybridFixturePlacement - || OutArguments.bCoreHybridCliffOnly || OutArguments.bCoreHybridFixturesOnly) + || OutArguments.bCoreHybridCliffOnly || OutArguments.bCoreHybridFixturesOnly + || OutArguments.bCoreFloatingIslandTest) && !OutArguments.bCoreWorldTest) { OutError = TEXT("Core hybrid tests require coreworldtest."); @@ -1492,6 +1502,14 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr OutError = TEXT("corehybridfixturesonly cannot be combined with the other focused hybrid modes."); return false; } + if (OutArguments.bCoreFloatingIslandTest + && (OutArguments.bCoreHybridMesher || OutArguments.bCoreHybridFixturePlacement + || OutArguments.bCoreHybridCliffOnly || OutArguments.bCoreHybridFixturesOnly + || OutArguments.CoreWorldAssemblyManifestPath.IsEmpty())) + { + OutError = TEXT("coreislandtest requires coreworldassembly and cannot be combined with hybrid test modes."); + return false; + } if (!OutArguments.CoreWorldAssemblyManifestPath.IsEmpty() && !OutArguments.bCoreWorldTest) { OutError = TEXT("coreworldassembly is accepted only by coreworldtest."); @@ -14502,8 +14520,1273 @@ bool RunHybridPluginMesherFixtures(UVoxelGenerator* Generator, bool bIncludeV2, return true; } +bool RunCoreFloatingIslandActorIntegrationTest( + const FExploreArguments& Arguments, FString& OutError) +{ + UVoxelSettings* AuthoredSettings = LoadObject( + nullptr, *Arguments.SettingsReference); + if (AuthoredSettings == nullptr) + { + OutError = FString::Printf(TEXT("Could not load Core island test settings '%s'."), + *Arguments.SettingsReference); + return false; + } + float StagedWalkSpeedCmPerSecond = 0.0f; + float StagedSprintSpeedCmPerSecond = 0.0f; + if (!ReadStagedPlayerSpeeds( + StagedWalkSpeedCmPerSecond, StagedSprintSpeedCmPerSecond, OutError)) + { + return false; + } + + const FString ManifestPath = FPaths::ConvertRelativePathToFull( + Arguments.CoreWorldAssemblyManifestPath); + if (!IsUnderVoxelForgeSaved(ManifestPath)) + { + OutError = TEXT("Core island test manifest must be staged under the plugin Saved directory."); + return false; + } + FString ManifestJson; + if (!FFileHelper::LoadFileToString(ManifestJson, *ManifestPath)) + { + OutError = FString::Printf(TEXT("Could not read staged island manifest '%s'."), *ManifestPath); + return false; + } + FTCHARToUTF8 Utf8Manifest(*ManifestJson); + std::string CoreError; + const std::optional ParsedAssembly = + VoxelForgeCore::WorldAssemblyManifest::FromJson( + std::string_view(Utf8Manifest.Get(), static_cast(Utf8Manifest.Length())), + CoreError); + if (!ParsedAssembly || ParsedAssembly->floatingIslands.size() != 1u + || !ParsedAssembly->plan.strates.empty() + || ParsedAssembly->floatingIslands.front().strateIndex != 1) + { + OutError = FString::Printf(TEXT("Island manifest must contain one strate-1 island plan and no SurfaceWorld plans: %s"), + ParsedAssembly ? TEXT("manifest shape differs") : UTF8_TO_TCHAR(CoreError.c_str())); + return false; + } + int32 EffectiveSeed = AuthoredSettings->Seed; + if (const UVoxelSeasonAsset* AuthoredSeason = AuthoredSettings->Season.LoadSynchronous()) + { + EffectiveSeed = AuthoredSeason->GetSeasonSeed(); + } + if (ParsedAssembly->recipe.seed != static_cast(EffectiveSeed)) + { + OutError = FString::Printf( + TEXT("Island manifest seed %u does not match the BuildHost settings seed %d."), + ParsedAssembly->recipe.seed, EffectiveSeed); + return false; + } + + // Resolve the current season first so this transient island fixture inherits its fixed water, + // atmosphere and authored content values without editing the cooked settings or strate assets. + // Resoudre la saison avant le fixture pour conserver l'eau et l'ambiance sans modifier les assets. + TStrongObjectPtr SourceManager(NewObject( + GetTransientPackage(), NAME_None, RF_Transient)); + if (!SourceManager.IsValid() + || !SourceManager->Initialize(AuthoredSettings, EffectiveSeed)) + { + OutError = TEXT("Core island test could not resolve the BuildHost season layout."); + return false; + } + const auto FindSourceDefinition = [&](int32 StrateIndex) -> UVoxelStrateDefinition* + { + for (const FStrateSlot& Slot : SourceManager->GetLayout()) + { + if (Slot.StrateIndex == StrateIndex) return Slot.Definition; + } + for (const TSoftObjectPtr& SoftDefinition : AuthoredSettings->StratePool) + { + if (UVoxelStrateDefinition* Definition = SoftDefinition.LoadSynchronous()) return Definition; + } + for (const TPair>& Pair : AuthoredSettings->FixedStrates) + { + if (UVoxelStrateDefinition* Definition = Pair.Value.LoadSynchronous()) return Definition; + } + return nullptr; + }; + UVoxelStrateDefinition* SourceTop = FindSourceDefinition(0); + UVoxelStrateDefinition* SourceIsland = FindSourceDefinition(1); + if (SourceTop == nullptr || SourceIsland == nullptr) + { + OutError = TEXT("Core island test needs authored source definitions for strata 0 and 1."); + return false; + } + + TStrongObjectPtr TestSettings(DuplicateObject( + AuthoredSettings, GetTransientPackage(), NAME_None)); + if (!TestSettings.IsValid()) + { + OutError = TEXT("Could not duplicate transient Core island test settings."); + return false; + } + TestSettings->Season.Reset(); + TestSettings->Seed = EffectiveSeed; + TestSettings->TotalStrates = 2; + TestSettings->InterStrateGapChunks = 0; + TestSettings->bEnableDensityVolume = false; + TestSettings->DecorationRadiusChunks = 2; + TestSettings->DecorationNearRadiusChunks = 1; + TestSettings->DecorationRegionSizeCells = 1; + TestSettings->DecorationSpacingVoxels = 16; + TestSettings->DecorationFarSpacingVoxels = 16; + TestSettings->DecorationMarchStepVoxels = 2; + TestSettings->DecorationMaxCrossingsPerColumn = 2; + TestSettings->DecorationColumnDepthVoxels = 320; + TestSettings->MaxConcurrentDecorationTasks = 2; + TestSettings->MaxDecorationCellsPerFrame = 4; + + TStrongObjectPtr TopDefinition(DuplicateObject( + SourceTop, TestSettings.Get(), NAME_None)); + TStrongObjectPtr IslandDefinition(DuplicateObject( + SourceIsland, TestSettings.Get(), NAME_None)); + if (!TopDefinition.IsValid() || !IslandDefinition.IsValid()) + { + OutError = TEXT("Could not create transient Core island test strate definitions."); + return false; + } + TopDefinition->StrateHeightInChunks = 16; + IslandDefinition->GeneratorType = ECaveGeneratorType::FloatingIslands; + IslandDefinition->StrateHeightInChunks = 64; + IslandDefinition->Landmarks.Empty(); + IslandDefinition->Biomes.Empty(); + IslandDefinition->Decorations.Empty(); + + // Keep the season's water datum when changing only the transient archetype to islands. + // Conserver le niveau d'eau de saison en ne changeant que l'archetype du fixture transitoire. + bool bFixedWaterEnabled = false; + float FixedWaterRelative = 0.0f; + for (const FStrateSlot& Slot : SourceManager->GetLayout()) + { + if (Slot.StrateIndex != 1 || Slot.Definition == nullptr) continue; + const int32 MidChunkZ = Slot.BottomChunkZ + + (Slot.TopChunkZ - Slot.BottomChunkZ) / 2; + const float WaterVoxelZ = SourceManager->GetWaterLevelWorldZForChunk( + FIntVector(0, 0, MidChunkZ)); + if (WaterVoxelZ != -FLT_MAX) + { + const float TopVoxelZ = (static_cast(Slot.TopChunkZ) + 1.0f) * CHUNK_SIZE; + const float BottomVoxelZ = static_cast(Slot.BottomChunkZ) * CHUNK_SIZE; + FixedWaterRelative = FMath::Clamp( + (WaterVoxelZ - BottomVoxelZ) / FMath::Max(TopVoxelZ - BottomVoxelZ, 1.0f), + 0.0f, 1.0f); + bFixedWaterEnabled = true; + } + break; + } + IslandDefinition->bHasWater = bFixedWaterEnabled; + IslandDefinition->GenerationParams.WaterLevelRelative = + bFixedWaterEnabled ? FixedWaterRelative : 0.0f; + IslandDefinition->SurfaceParams.WaterLevelRelative = + bFixedWaterEnabled ? FixedWaterRelative : 0.0f; + + UStaticMesh* ProbeTreeMesh = LoadObject( + nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); + if (ProbeTreeMesh == nullptr) + { + OutError = TEXT("Could not load the transient cube mesh used to verify island tree anchors."); + return false; + } + FStrateDecoration ProbeTree; + ProbeTree.Profile.InstancedMesh = ProbeTreeMesh; + ProbeTree.Profile.SurfacePlacement = ESurfaceType::Floor; + ProbeTree.Profile.bAlignToSurface = false; + ProbeTree.Profile.MinScale = 0.5f; + ProbeTree.Profile.MaxScale = 0.5f; + ProbeTree.Profile.CullDistance = 30000.0f; + ProbeTree.Profile.bCastShadow = false; + ProbeTree.StreamTier = EDecoStreamTier::Far; + ProbeTree.SpawnDensity = 1.0f; + ProbeTree.MaxPerChunk = 64; + IslandDefinition->Decorations.Add(ProbeTree); + TestSettings->FixedStrates.Add(0, TSoftObjectPtr(TopDefinition.Get())); + TestSettings->FixedStrates.Add(1, TSoftObjectPtr(IslandDefinition.Get())); + + FString ImportReport; + if (!TestSettings->SetWorldAssemblyManifestJson(ManifestJson, ImportReport)) + { + OutError = FString::Printf(TEXT("Could not import the floating-island manifest into transient settings: %s"), + *ImportReport); + return false; + } + FString CanonicalAssemblyJson; + FString ImportedAssemblyHash; + FString ImportError; + if (!TestSettings->GetWorldAssemblyManifestJson( + CanonicalAssemblyJson, ImportedAssemblyHash, ImportError)) + { + OutError = FString::Printf(TEXT("Transient island manifest failed its canonical hash check: %s"), + *ImportError); + return false; + } + + TStrongObjectPtr Manager(NewObject( + GetTransientPackage(), NAME_None, RF_Transient)); + if (!Manager.IsValid() || !Manager->Initialize(TestSettings.Get(), EffectiveSeed)) + { + OutError = TEXT("Core island test could not build the transient two-strate layout."); + return false; + } + const FStrateSlot* IslandSlot = nullptr; + for (const FStrateSlot& Slot : Manager->GetLayout()) + { + if (Slot.StrateIndex == 1) IslandSlot = &Slot; + } + if (IslandSlot == nullptr || IslandSlot->TopChunkZ != -16 + || IslandSlot->BottomChunkZ != -79 + || IslandSlot->Definition == nullptr + || IslandSlot->Definition->GeneratorType != ECaveGeneratorType::FloatingIslands) + { + OutError = FString::Printf( + TEXT("Transient island strate does not match manifest bounds [-16,-79]: actual=[%d,%d]."), + IslandSlot ? IslandSlot->TopChunkZ : MAX_int32, + IslandSlot ? IslandSlot->BottomChunkZ : MIN_int32); + return false; + } + + FString RuntimeJson; + if (!VoxelForgeCoreAdapter::SerializeResolvedWorld( + TestSettings.Get(), Manager.Get(), 3, 8, RuntimeJson, OutError)) + { + return false; + } + FTCHARToUTF8 Utf8Runtime(*RuntimeJson); + std::optional Snapshot = + VoxelForgeCore::ResolvedWorldSnapshot::FromJson( + std::string_view(Utf8Runtime.Get(), static_cast(Utf8Runtime.Length())), + CoreError); + if (!Snapshot || Snapshot->worldAssembly() == nullptr + || Snapshot->worldAssembly()->contentHash != ParsedAssembly->contentHash + || Snapshot->worldAssembly()->floatingIslands.size() != 1u) + { + OutError = FString::Printf(TEXT("Core adapter did not preserve the imported floating-island manifest: %s"), + Snapshot ? TEXT("manifest hash or plan count differs") : UTF8_TO_TCHAR(CoreError.c_str())); + return false; + } + const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1& NativeIslandPlan = + ParsedAssembly->floatingIslands.front(); + if (NativeIslandPlan.strateIndex != 1 || NativeIslandPlan.manifest.plan.islands.size() != 4u + || NativeIslandPlan.manifest.plan.route.size() < 2u) + { + OutError = TEXT("Core adapter island plan has the wrong strate, island count, or route."); + return false; + } + VoxelForgeCore::FloatingIslandFieldV1 RouteField( + NativeIslandPlan.manifest.recipe, NativeIslandPlan.manifest.plan); + const VoxelForgeCore::FloatingIslandRouteAuditResult RouteAudit = + VoxelForgeCore::AuditFloatingIslandRoute(RouteField); + const VoxelForgeCore::FloatingIslandThicknessAuditResult ThicknessAudit = + VoxelForgeCore::AuditFloatingIslandThickness(NativeIslandPlan.manifest.plan); + if (!RouteField.isValid() || !RouteAudit.passed || !ThicknessAudit.passed) + { + OutError = TEXT("Imported Core island field failed route clearance or mass-thickness validation."); + return false; + } + const VoxelForgeCore::WorldAssemblyRoutePointV1& StartRoute = + NativeIslandPlan.manifest.plan.route.front(); + constexpr double SubvoxelsPerCoreVoxel = 256.0; + constexpr double CoreVoxelSizeCm = 25.0; + const FVector CameraVoxels( + static_cast(StartRoute.positionSubvoxels[0]) / SubvoxelsPerCoreVoxel, + static_cast(StartRoute.positionSubvoxels[1]) / SubvoxelsPerCoreVoxel, + static_cast(StartRoute.positionSubvoxels[2]) / SubvoxelsPerCoreVoxel); + const FVector CameraLocalCm = CameraVoxels * CoreVoxelSizeCm; + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] manifest_import=PASS hash=%s recipe_hash=%016llx plan_hash=%016llx scale_owner_cm=12.5 scale_core_cm=25 islands=%d connectors=%d route_segments=%llu route_clearance_subvoxels=%lld water_fixed=%d water_relative=%.6f"), + *ImportedAssemblyHash, + static_cast(NativeIslandPlan.manifest.recipe.contentHash), + static_cast(NativeIslandPlan.manifest.plan.contentHash), + static_cast(NativeIslandPlan.manifest.plan.islands.size()), + static_cast(NativeIslandPlan.manifest.plan.clusterEdges.size()), + static_cast(RouteAudit.segmentsTested), + static_cast(RouteAudit.minimumClearanceSubvoxels), + bFixedWaterEnabled ? 1 : 0, FixedWaterRelative); + + // Generate the standalone reference before actor startup so two full Core worlds do not overlap. + // Generer la reference Standalone avant l'acteur pour ne pas garder deux mondes Core complets. + TMap ExpectedHashes; + TSet ExpectedDesiredSet; + std::vector Desired; + std::array ExpectedAggregates{}; + std::map TimingProbeKeys; + std::int32_t CoverageLocalTiles = 0; + std::int32_t CoverageFaces = 0; + std::int32_t TransitionFaces = 0; + std::int32_t Lod0Tiles = 0, Lod1Tiles = 0; + VoxelForgeCore::Vec3d Camera{CameraVoxels.X, CameraVoxels.Y, CameraVoxels.Z}; + VoxelForgeCore::ShapeBuildRequest Request; + { + VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot)); + Desired = ReferenceWorld.desiredTiles(Camera); + if (Desired.empty()) + { + OutError = TEXT("Standalone Core returned no tiles around the island route start."); + return false; + } + std::int32_t FineMin[3] = {MAX_int32, MAX_int32, MAX_int32}; + std::int32_t FineMax[3] = {MIN_int32, MIN_int32, MIN_int32}; + std::int32_t ClipMin[3] = {MAX_int32, MAX_int32, MAX_int32}; + std::int32_t ClipMax[3] = {MIN_int32, MIN_int32, MIN_int32}; + for (const VoxelForgeCore::TileKey& Key : Desired) + { + const std::int32_t Extent = 32 << Key.lod; + const std::int32_t Origins[3] = { + Key.coordinate.x * Extent, Key.coordinate.y * Extent, Key.coordinate.z * Extent}; + for (int32 Axis = 0; Axis < 3; ++Axis) + { + ClipMin[Axis] = FMath::Min(ClipMin[Axis], Origins[Axis]); + ClipMax[Axis] = FMath::Max(ClipMax[Axis], Origins[Axis] + Extent); + if (Key.lod < ReferenceWorld.configuration().fullResolutionLodCount) + { + FineMin[Axis] = FMath::Min(FineMin[Axis], Origins[Axis]); + FineMax[Axis] = FMath::Max(FineMax[Axis], Origins[Axis] + Extent); + } + } + if (Key.lod == 0) ++Lod0Tiles; + if (Key.lod == 1) ++Lod1Tiles; + } + if (FineMin[0] == MAX_int32 || ClipMin[0] == MAX_int32) + { + OutError = TEXT("Standalone Core island clipmap bounds are invalid."); + return false; + } + Request.minVoxel = {FineMin[0], FineMin[1], FineMin[2]}; + Request.maxVoxel = {FineMax[0], FineMax[1], FineMax[2]}; + Request.clipMinVoxel = {ClipMin[0], ClipMin[1], ClipMin[2]}; + Request.clipMaxVoxel = {ClipMax[0], ClipMax[1], ClipMax[2]}; + Request.workerLimit = 8; + Request.detail = VoxelForgeCore::DetailVariant::coherent; + Request.includeFarWorld = true; + FStandaloneCoreAdapterScheduler Scheduler; + if (!ReferenceWorld.compile(Request, Scheduler, CoreError)) + { + OutError = FString::Printf(TEXT("Standalone Core island reference compile failed: %s"), + UTF8_TO_TCHAR(CoreError.c_str())); + return false; + } + ReferenceWorld.retainTileRegions(Desired); + VoxelForgeCore::TileGenerationOptions Options; + Options.hashDensity = true; + std::vector AggregateRows; + AggregateRows.reserve(Desired.size()); + for (const VoxelForgeCore::TileKey& Key : Desired) + { + const std::optional Mesh = + ReferenceWorld.generateTile(Key, Options, CoreError); + if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash + || !Mesh->hasPayloadHash || !Mesh->hasTopologyHash) + { + OutError = FString::Printf(TEXT("Standalone Core island tile generation failed: %s"), + UTF8_TO_TCHAR(CoreError.c_str())); + return false; + } + const FVoxelTileKey RuntimeKey( + FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod); + FVoxelForgeCoreTileHashRecord Record; + Record.Density = Mesh->densityHash; + Record.Mesh = Mesh->meshHash; + Record.Payload = Mesh->payloadHash; + Record.Topology = Mesh->topologyHash; + Record.Triangles = static_cast(Mesh->indices.size() / 3u); + Record.bEmpty = Mesh->indices.empty(); + Record.bHasHashes = true; + ExpectedHashes.Add(RuntimeKey, Record); + ExpectedDesiredSet.Add(RuntimeKey); + AggregateRows.push_back({Key, + {Record.Density, Record.Mesh, Record.Payload, Record.Topology}}); + + if (Key.lod > 1u) continue; + const bool bAir = Mesh->diagnostics.tileClass == VoxelForgeCore::TileClass::allAir; + const bool bSolid = Mesh->diagnostics.tileClass == VoxelForgeCore::TileClass::allSolid; + bool bIslandBounds = false; + bool bConnectorBounds = false; + const std::int64_t TileMinimum[3] = { + static_cast(Key.coordinate.x) * (32ll << Key.lod) * 256ll, + static_cast(Key.coordinate.y) * (32ll << Key.lod) * 256ll, + static_cast(Key.coordinate.z) * (32ll << Key.lod) * 256ll}; + const std::int64_t TileMaximum[3] = { + TileMinimum[0] + (32ll << Key.lod) * 256ll, + TileMinimum[1] + (32ll << Key.lod) * 256ll, + TileMinimum[2] + (32ll << Key.lod) * 256ll}; + const auto IntersectsFeature = [&](const auto& Feature) + { + for (int32 Axis = 0; Axis < 3; ++Axis) + { + if (TileMaximum[Axis] <= Feature.influenceMinimumSubvoxels[Axis] + || TileMinimum[Axis] >= Feature.influenceMaximumSubvoxels[Axis]) return false; + } + return true; + }; + for (const auto& Island : NativeIslandPlan.manifest.plan.islands) + bIslandBounds |= IntersectsFeature(Island); + for (const auto& Connector : NativeIslandPlan.manifest.plan.clusterEdges) + bConnectorBounds |= IntersectsFeature(Connector); + const std::string LodName = "lod" + std::to_string(Key.lod) + ":"; + const std::string Kind = bAir ? "air" : bSolid ? "solid" + : (bConnectorBounds && !bIslandBounds) ? "span" + : bIslandBounds ? "island" : "mixed"; + const std::string ProbeName = LodName + Kind; + if (TimingProbeKeys.find(ProbeName) == TimingProbeKeys.end()) + TimingProbeKeys.emplace(ProbeName, Key); + if (!bAir && !bSolid + && TimingProbeKeys.find(LodName + "mixed") == TimingProbeKeys.end()) + TimingProbeKeys.emplace(LodName + "mixed", Key); + } + ExpectedAggregates = StandaloneCoreAggregateHashes(AggregateRows); + if (!AuditCoreClipmapCoverage(Desired, Camera, 128, + CoverageLocalTiles, CoverageFaces, TransitionFaces, OutError)) + { + OutError = FString::Printf(TEXT("Island standalone clipmap coverage failed: %s"), *OutError); + return false; + } + + UVoxelGenerator* LegacyGenerator = NewObject( + GetTransientPackage(), NAME_None, RF_Transient); + UVoxelMarchingCubesMesher* LegacyMesher = NewObject( + GetTransientPackage(), NAME_None, RF_Transient); + if (LegacyGenerator == nullptr || LegacyMesher == nullptr) + { + OutError = TEXT("Could not allocate the matched legacy island timing fixture."); + return false; + } + LegacyGenerator->InitializeSettings(TestSettings.Get()); + LegacyGenerator->SetStrateManager(Manager.Get()); + LegacyMesher->SetGenerator(LegacyGenerator); + LegacyMesher->SetGeometryVoxelSizeCm(static_cast(CoreVoxelSizeCm)); + const auto P95Milliseconds = [](TArray& Samples) + { + Samples.Sort(); + const int32 Index = FMath::Clamp( + FMath::CeilToInt(static_cast(Samples.Num()) * 0.95f) - 1, + 0, Samples.Num() - 1); + return Samples.IsEmpty() ? 0.0 : Samples[Index]; + }; + for (const auto& ProbePair : TimingProbeKeys) + { + const VoxelForgeCore::TileKey& Key = ProbePair.second; + TArray CoreMilliseconds; + TArray CoreClassificationMilliseconds; + TArray CoreColumnComputeMilliseconds; + TArray CoreMeshingMilliseconds; + TArray LegacyMilliseconds; + CoreMilliseconds.Reserve(5); + CoreClassificationMilliseconds.Reserve(5); + CoreColumnComputeMilliseconds.Reserve(5); + CoreMeshingMilliseconds.Reserve(5); + LegacyMilliseconds.Reserve(5); + for (int32 Repeat = 0; Repeat < 5; ++Repeat) + { + const double CoreStart = FPlatformTime::Seconds(); + const std::optional TimedCoreMesh = + ReferenceWorld.generateTile(Key, {}, CoreError); + const double CoreElapsedMs = (FPlatformTime::Seconds() - CoreStart) * 1000.0; + if (!TimedCoreMesh) + { + OutError = FString::Printf(TEXT("Core island timing sample failed: %s"), + UTF8_TO_TCHAR(CoreError.c_str())); + return false; + } + CoreMilliseconds.Add(CoreElapsedMs); + CoreClassificationMilliseconds.Add(TimedCoreMesh->diagnostics.classificationMilliseconds); + CoreColumnComputeMilliseconds.Add(TimedCoreMesh->diagnostics.columnComputeMilliseconds); + CoreMeshingMilliseconds.Add(TimedCoreMesh->diagnostics.meshingMilliseconds); + const int32 Extent = CHUNK_SIZE << Key.lod; + const FIntVector CoreOrigin( + Key.coordinate.x * Extent, Key.coordinate.y * Extent, + Key.coordinate.z * Extent); + const FIntVector LegacyOrigin = CoreOrigin * 2; + const int32 LegacyStep = 2 << Key.lod; + const double LegacyStart = FPlatformTime::Seconds(); + const FVoxelMeshData LegacyMesh = LegacyMesher->GenerateMesh( + LegacyOrigin, LegacyStep, CHUNK_SIZE); + const double LegacyElapsedMs = (FPlatformTime::Seconds() - LegacyStart) * 1000.0; + LegacyMilliseconds.Add(LegacyElapsedMs); + if (Repeat == 0) + { + const int32 CoreTriangles = static_cast(TimedCoreMesh->indices.size() / 3u); + const int32 LegacyTriangles = LegacyMesh.Triangles.Num() / 3; + const double TileTravelCm = CoreVoxelSizeCm * Extent; + const double WalkDeadlineMs = TileTravelCm / FMath::Max(StagedWalkSpeedCmPerSecond, 1.0f) * 1000.0; + const double SprintDeadlineMs = TileTravelCm / FMath::Max(StagedSprintSpeedCmPerSecond, 1.0f) * 1000.0; + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeIslandStreaming] lod=%d kind=%s key=(%d,%d,%d) core_triangles=%d legacy_triangles=%d tile_travel_cm=%.0f walk_deadline_ms=%.3f sprint_deadline_ms=%.3f"), + static_cast(Key.lod), UTF8_TO_TCHAR(ProbePair.first.c_str()), + Key.coordinate.x, Key.coordinate.y, Key.coordinate.z, + CoreTriangles, LegacyTriangles, TileTravelCm, WalkDeadlineMs, SprintDeadlineMs); + } + } + const double CoreP95 = P95Milliseconds(CoreMilliseconds); + const double CoreClassificationP95 = P95Milliseconds(CoreClassificationMilliseconds); + const double CoreColumnComputeP95 = P95Milliseconds(CoreColumnComputeMilliseconds); + const double CoreMeshingP95 = P95Milliseconds(CoreMeshingMilliseconds); + const double LegacyP95 = P95Milliseconds(LegacyMilliseconds); + const int32 Extent = CHUNK_SIZE << Key.lod; + const double TileTravelCm = CoreVoxelSizeCm * Extent; + const double WalkDeadlineMs = TileTravelCm / FMath::Max(StagedWalkSpeedCmPerSecond, 1.0f) * 1000.0; + const double SprintDeadlineMs = TileTravelCm / FMath::Max(StagedSprintSpeedCmPerSecond, 1.0f) * 1000.0; + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeIslandStreaming] timing lod=%d kind=%s repeats=5 core_p95_ms=%.3f core_classify_p95_ms=%.3f core_columns_p95_ms=%.3f core_mesh_p95_ms=%.3f legacy_p95_ms=%.3f walk_deadline_ms=%.3f sprint_deadline_ms=%.3f core_walk_lag_ms=%.3f legacy_walk_lag_ms=%.3f core_sprint_lag_ms=%.3f legacy_sprint_lag_ms=%.3f"), + static_cast(Key.lod), UTF8_TO_TCHAR(ProbePair.first.c_str()), + CoreP95, CoreClassificationP95, CoreColumnComputeP95, CoreMeshingP95, + LegacyP95, WalkDeadlineMs, SprintDeadlineMs, + FMath::Max(0.0, CoreP95 - WalkDeadlineMs), FMath::Max(0.0, LegacyP95 - WalkDeadlineMs), + FMath::Max(0.0, CoreP95 - SprintDeadlineMs), FMath::Max(0.0, LegacyP95 - SprintDeadlineMs)); + } + for (const int32 Lod : {0, 1}) + { + if (TimingProbeKeys.find("lod" + std::to_string(Lod) + ":air") == TimingProbeKeys.end() + || (TimingProbeKeys.find("lod" + std::to_string(Lod) + ":island") == TimingProbeKeys.end() + && TimingProbeKeys.find("lod" + std::to_string(Lod) + ":mixed") == TimingProbeKeys.end())) + { + OutError = FString::Printf(TEXT("Island clipmap timing did not find air and surface probes at LOD%d."), Lod); + return false; + } + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] standalone_reference=PASS tiles=%d lod0=%d lod1=%d lod_transitions=%d coverage_local=%d checked_faces=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"), + ExpectedDesiredSet.Num(), Lod0Tiles, Lod1Tiles, TransitionFaces, CoverageLocalTiles, + CoverageFaces, static_cast(ExpectedAggregates[0]), + static_cast(ExpectedAggregates[1]), + static_cast(ExpectedAggregates[2]), + static_cast(ExpectedAggregates[3])); + } + + const auto BuildWorkerHashAggregate = [&](uint32 WorkerCount, + std::array& OutHashes) -> bool + { + FTCHARToUTF8 WorkerJson(*RuntimeJson); + std::string WorkerError; + std::optional WorkerSnapshot = + VoxelForgeCore::ResolvedWorldSnapshot::FromJson( + std::string_view(WorkerJson.Get(), static_cast(WorkerJson.Length())), + WorkerError); + if (!WorkerSnapshot) + { + OutError = FString::Printf(TEXT("Core W%u island snapshot failed: %s"), + WorkerCount, UTF8_TO_TCHAR(WorkerError.c_str())); + return false; + } + VoxelForgeCore::World WorkerWorld(std::move(*WorkerSnapshot)); + VoxelForgeCore::ShapeBuildRequest WorkerRequest = Request; + WorkerRequest.workerLimit = WorkerCount; + FStandaloneCoreAdapterScheduler Scheduler; + if (!WorkerWorld.compile(WorkerRequest, Scheduler, WorkerError)) + { + OutError = FString::Printf(TEXT("Core W%u island compile failed: %s"), + WorkerCount, UTF8_TO_TCHAR(WorkerError.c_str())); + return false; + } + WorkerWorld.retainTileRegions(Desired); + VoxelForgeCore::TileGenerationOptions Options; + Options.hashDensity = true; + std::vector Rows; + Rows.reserve(Desired.size()); + for (const VoxelForgeCore::TileKey& Key : Desired) + { + const std::optional Mesh = + WorkerWorld.generateTile(Key, Options, WorkerError); + if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash + || !Mesh->hasPayloadHash || !Mesh->hasTopologyHash) + { + OutError = FString::Printf(TEXT("Core W%u island tile hash failed: %s"), + WorkerCount, UTF8_TO_TCHAR(WorkerError.c_str())); + return false; + } + Rows.push_back({Key, {Mesh->densityHash, Mesh->meshHash, + Mesh->payloadHash, Mesh->topologyHash}}); + } + OutHashes = StandaloneCoreAggregateHashes(Rows); + return true; + }; + std::array W1Hashes{}, W16Hashes{}; + if (!BuildWorkerHashAggregate(1, W1Hashes) + || !BuildWorkerHashAggregate(16, W16Hashes)) + { + return false; + } + if (W1Hashes != ExpectedAggregates || W16Hashes != ExpectedAggregates) + { + OutError = TEXT("Island clipmap hashes differ across worker limits 1, 8 and 16."); + return false; + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] worker_hash_matrix=1/8/16 stable=1 density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"), + static_cast(ExpectedAggregates[0]), + static_cast(ExpectedAggregates[1]), + static_cast(ExpectedAggregates[2]), + static_cast(ExpectedAggregates[3])); + + const UWorld::InitializationValues WorldInitialization = UWorld::InitializationValues() + .AllowAudioPlayback(false).RequiresHitProxies(false).CreatePhysicsScene(true) + .CreateNavigation(false).CreateAISystem(false).ShouldSimulatePhysics(true) + .SetDefaultGameMode(AGameModeBase::StaticClass()).SetTransactional(false); + UWorld* TestWorld = UWorld::CreateWorld(EWorldType::Game, false, + FName(TEXT("VoxelForgeCoreIslandValidationWorld")), GetTransientPackage(), + false, ERHIFeatureLevel::Num, &WorldInitialization); + if (TestWorld == nullptr) + { + OutError = TEXT("Could not create the transient BuildHost island actor world."); + return false; + } + UGameInstance* TestGameInstance = NewObject(GEngine); + FWorldContext& TestWorldContext = GEngine->CreateNewWorldContext(EWorldType::Game); + TestWorldContext.OwningGameInstance = TestGameInstance; + TestWorldContext.SetCurrentWorld(TestWorld); + TestWorld->SetGameInstance(TestGameInstance); + TestWorld->SetShouldTick(false); + TestGameInstance->Init(); + TestWorld->AddToRoot(); + struct FIslandWorldCleanup + { + UWorld* World = nullptr; + uint64 PreviousFrameCounter = GFrameCounter; + ~FIslandWorldCleanup() + { + if (World != nullptr) + { + if (World->HasBegunPlay()) + { + World->BeginTearingDown(); + World->EndPlay(EEndPlayReason::Quit); + } + if (UGameInstance* GameInstance = World->GetGameInstance()) GameInstance->Shutdown(); + if (GEngine != nullptr) GEngine->DestroyWorldContext(World); + World->DestroyWorld(false); + if (World->IsRooted()) World->RemoveFromRoot(); + } + GFrameCounter = PreviousFrameCounter; + } + } Cleanup{TestWorld, GFrameCounter}; + + if (!TestWorld->SetGameMode(FURL())) + { + OutError = TEXT("Could not create the transient GameMode for island actor validation."); + return false; + } + AGameModeBase* ValidationGameMode = TestWorld->GetAuthGameMode(); + if (ValidationGameMode == nullptr) + { + OutError = TEXT("Transient island validation GameMode was not available before startup."); + return false; + } + // Preserve the explicitly placed route pawn; GameMode's default spawn would move Core's first compile to the origin. + // Conserver le pawn de route ; le spawn par defaut du GameMode deplacerait le premier compile Core a l'origine. + ValidationGameMode->DefaultPawnClass = nullptr; + UPlayer* TestPlayer = NewObject(GEngine, NAME_None, RF_Transient); + APlayerController* PlayerController = TestWorld->SpawnActor( + APlayerController::StaticClass(), FTransform::Identity, + FActorSpawnParameters()); + if (TestPlayer == nullptr || PlayerController == nullptr) + { + OutError = TEXT("Could not spawn a BuildHost route player controller."); + return false; + } + // The world stream anchor reads only the first controller's pawn; use a transient base player + // to avoid online identity initialization in this headless commandlet. + // L'ancre de streaming lit le pawn du premier controleur ; eviter l'initialisation en ligne. + PlayerController->SetPlayer(TestPlayer); + // This transient world has not initialized actors yet, so register the controller explicitly. + // Ce monde transitoire n'a pas encore initialise ses acteurs ; enregistrer le controleur. + TestWorld->AddController(PlayerController); + APawn* PlayerPawn = TestWorld->SpawnActor(APawn::StaticClass(), + CameraLocalCm, FRotator::ZeroRotator); + if (PlayerPawn == nullptr) + { + OutError = TEXT("Could not spawn the BuildHost island route player."); + return false; + } + USceneComponent* PawnRoot = NewObject(PlayerPawn, NAME_None, RF_Transient); + if (PawnRoot == nullptr) + { + OutError = TEXT("Could not create the BuildHost island route pawn root."); + return false; + } + PlayerPawn->SetRootComponent(PawnRoot); + PawnRoot->RegisterComponent(); + PlayerPawn->SetActorLocation(CameraLocalCm, false, nullptr, ETeleportType::TeleportPhysics); + PlayerController->Possess(PlayerPawn); + APlayerController* FirstPlayerController = TestWorld->GetFirstPlayerController(); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] player_preflight first=%s manual=%s first_pawn=%s route_pawn=%s route_controller=%s first_matches=%d possess_matches=%d position_matches=%d route_position=(%.1f,%.1f,%.1f) world_netmode=%d"), + *GetNameSafe(FirstPlayerController), *GetNameSafe(PlayerController), + *GetNameSafe(FirstPlayerController ? FirstPlayerController->GetPawn() : nullptr), + *GetNameSafe(PlayerPawn), *GetNameSafe(PlayerPawn->GetController()), + FirstPlayerController == PlayerController ? 1 : 0, + PlayerController->GetPawn() == PlayerPawn ? 1 : 0, + PlayerPawn->GetActorLocation().Equals(CameraLocalCm, 0.1) ? 1 : 0, + PlayerPawn->GetActorLocation().X, PlayerPawn->GetActorLocation().Y, + PlayerPawn->GetActorLocation().Z, static_cast(TestWorld->GetNetMode())); + if (FirstPlayerController != PlayerController + || PlayerController->GetPawn() != PlayerPawn + || !PlayerPawn->GetActorLocation().Equals(CameraLocalCm, 0.1)) + { + OutError = TEXT("BuildHost route controller did not retain the route pawn before Core initialization."); + return false; + } + // Spawn and possess the route pawn before AVoxelWorld begins play. Its initial Core snapshot + // must compile the same route-centered clipmap that the Standalone reference audits. + // Creer le joueur avant le monde voxel afin que le premier clipmap Core soit centre sur la route. + AVoxelWorld* Actor = TestWorld->SpawnActorDeferred( + AVoxelWorld::StaticClass(), FTransform::Identity, nullptr, nullptr, + ESpawnActorCollisionHandlingMethod::AlwaysSpawn); + if (Actor == nullptr) + { + OutError = TEXT("Could not spawn the transient AVoxelWorld for island validation."); + return false; + } + Actor->Settings = TestSettings.Get(); + Actor->bManageAtmosphere = true; + Actor->bUseVoxelForgeCore = true; + Actor->CoreMesherRecipe = EVoxelForgeCoreMesherRecipe::MarchingCubesV1; + Actor->CoreClipRadius = 3; + Actor->CoreMaxClipLevel = 8; + Actor->bCaptureVoxelForgeCoreHashesForValidation = true; + Actor->FinishSpawning(FTransform::Identity); + TestWorld->InitializeActorsForPlay(FURL()); + TestWorld->BeginPlay(); + // GameMode may restart manually spawned controllers at BeginPlay and replace their pawn. + // Reassert the route pawn after startup so both the first Core request and later clipmap + // comparisons stay tied to the authored island route. + // Le GameMode peut remplacer le pawn au demarrage ; reprendre explicitement le pawn de route. + PlayerPawn->SetActorLocation(CameraLocalCm, false, nullptr, ETeleportType::TeleportPhysics); + PlayerController->Possess(PlayerPawn); + if (PlayerController->GetPawn() != PlayerPawn + || !PlayerPawn->GetActorLocation().Equals(CameraLocalCm, 0.1)) + { + OutError = TEXT("BuildHost GameMode did not preserve the route pawn after BeginPlay."); + return false; + } + Actor->SetActorTickEnabled(true); + FString ActorRuntimeJson; + FString ActorRuntimeError; + const bool bActorRuntimeSerialized = VoxelForgeCoreAdapter::SerializeResolvedWorld( + Actor->Settings, Actor->StrateManager, 3, 8, ActorRuntimeJson, ActorRuntimeError); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] world_initialized=1 core=1 actor_initialized=%d player=(%.1f,%.1f,%.1f) possessed=(%.1f,%.1f,%.1f) snapshot_parity=%d expected_bytes=%d actor_bytes=%d"), + Actor->IsActorInitialized() ? 1 : 0, + CameraLocalCm.X, CameraLocalCm.Y, CameraLocalCm.Z, + PlayerController->GetPawn() ? PlayerController->GetPawn()->GetActorLocation().X : 0.0, + PlayerController->GetPawn() ? PlayerController->GetPawn()->GetActorLocation().Y : 0.0, + PlayerController->GetPawn() ? PlayerController->GetPawn()->GetActorLocation().Z : 0.0, + bActorRuntimeSerialized && ActorRuntimeJson == RuntimeJson ? 1 : 0, + RuntimeJson.Len(), ActorRuntimeJson.Len()); + + const auto DesiredTilesLoaded = [](const AVoxelWorld* World) + { + if (World == nullptr || World->DesiredSorted.IsEmpty()) return false; + for (const FVoxelTileKey& Key : World->DesiredSorted) + if (!World->LoadedTiles.Contains(Key)) return false; + return true; + }; + const auto CollisionSetSettled = [](const AVoxelWorld* World) + { + if (World == nullptr) return false; + for (const FVoxelTileKey& Key : World->CollisionDesiredTiles) + if (!World->CollisionReadyTiles.Contains(Key) + && !World->CollisionNotRequiredTiles.Contains(Key)) return false; + return true; + }; + const auto TickWorldUntil = [&](double TimeoutSeconds, const TFunction& Predicate) + { + const double Deadline = FPlatformTime::Seconds() + TimeoutSeconds; + double LastProgress = FPlatformTime::Seconds(); + int32 Ticks = 0; + while (FPlatformTime::Seconds() < Deadline) + { + TestWorld->Tick(LEVELTICK_All, 0.016f); + ++GFrameCounter; + ++Ticks; + FPlatformProcess::Sleep(0.001f); + if (Predicate()) return true; + const double Now = FPlatformTime::Seconds(); + if (Now - LastProgress >= 10.0) + { + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] wait ticks=%d ready=%d desired=%d loaded=%d pending=%d cooks=%d active=%d"), + Ticks, Actor->IsVoxelForgeCoreReady() ? 1 : 0, Actor->DesiredSorted.Num(), + Actor->LoadedTiles.Num(), Actor->PendingTiles.Num(), + Actor->PendingCollisionCooks.Num(), Actor->ActiveTaskCount.load(std::memory_order_acquire)); + LastProgress = Now; + } + } + return false; + }; + if (!TickWorldUntil(600.0, [&]() + { + return Actor->IsVoxelForgeCoreReady() && Actor->bAllChunksLoaded + && Actor->PendingTiles.IsEmpty() && Actor->PendingCollisionCooks.IsEmpty() + && Actor->PendingUnload.IsEmpty() + && Actor->ActiveTaskCount.load(std::memory_order_acquire) == 0 + && DesiredTilesLoaded(Actor) && CollisionSetSettled(Actor); + })) + { + OutError = FString::Printf( + TEXT("BuildHost island clipmap did not settle: ready=%d desired=%d loaded=%d pending=%d cooks=%d active=%d."), + Actor->IsVoxelForgeCoreReady() ? 1 : 0, Actor->DesiredSorted.Num(), + Actor->LoadedTiles.Num(), Actor->PendingTiles.Num(), + Actor->PendingCollisionCooks.Num(), Actor->ActiveTaskCount.load(std::memory_order_acquire)); + return false; + } + TSet ActualDesiredSet; + TSet ActualVisualSet; + TArray RuntimeCollisionSupportKeys; + int32 RuntimeCollisionSupportTiles = 0; + for (const FVoxelTileKey& Key : Actor->DesiredSorted) ActualDesiredSet.Add(Key); + for (const FVoxelTileKey& Key : ActualDesiredSet) + { + if (!Actor->CollisionOnlyTiles.Contains(Key)) + { + ActualVisualSet.Add(Key); + continue; + } + const FVoxelForgeCoreTileHashRecord* SupportHash = Actor->VoxelForgeCoreTileHashes.Find(Key); + if (!Actor->CollisionDesiredTiles.Contains(Key) || SupportHash == nullptr + || !SupportHash->bHasHashes + || (!Actor->CollisionReadyTiles.Contains(Key) + && !Actor->CollisionNotRequiredTiles.Contains(Key))) + { + OutError = FString::Printf( + TEXT("BuildHost island collision-only support tile (%d,%d,%d) was not hashed and cooked."), + Key.Coord.X, Key.Coord.Y, Key.Coord.Z); + return false; + } + ++RuntimeCollisionSupportTiles; + RuntimeCollisionSupportKeys.Add(Key); + } + if (ActualVisualSet.Num() != ExpectedDesiredSet.Num()) + { + OutError = FString::Printf( + TEXT("BuildHost island visual clipmap has %d tiles; Standalone expected %d (runtime collision-only=%d)."), + ActualVisualSet.Num(), ExpectedDesiredSet.Num(), RuntimeCollisionSupportTiles); + return false; + } + if (!RuntimeCollisionSupportKeys.IsEmpty()) + { + FTCHARToUTF8 SupportJson(*RuntimeJson); + std::string SupportError; + std::optional SupportSnapshot = + VoxelForgeCore::ResolvedWorldSnapshot::FromJson( + std::string_view(SupportJson.Get(), static_cast(SupportJson.Length())), + SupportError); + if (!SupportSnapshot) + { + OutError = FString::Printf(TEXT("Could not parse the Standalone collision-support snapshot: %s"), + UTF8_TO_TCHAR(SupportError.c_str())); + return false; + } + VoxelForgeCore::World SupportReferenceWorld(std::move(*SupportSnapshot)); + std::vector SupportReferenceKeys; + VoxelForgeCore::ShapeBuildRequest SupportRequest; + SupportRequest.workerLimit = 8; + SupportRequest.detail = VoxelForgeCore::DetailVariant::coherent; + SupportRequest.includeFarWorld = true; + std::int32_t SupportFineMin[3] = {MAX_int32, MAX_int32, MAX_int32}; + std::int32_t SupportFineMax[3] = {MIN_int32, MIN_int32, MIN_int32}; + std::int32_t SupportClipMin[3] = {MAX_int32, MAX_int32, MAX_int32}; + std::int32_t SupportClipMax[3] = {MIN_int32, MIN_int32, MIN_int32}; + for (const FVoxelTileKey& Key : RuntimeCollisionSupportKeys) + { + const std::int32_t Extent = 32 << Key.Level; + const std::int32_t Origin[3] = { + Key.Coord.X * Extent, Key.Coord.Y * Extent, Key.Coord.Z * Extent}; + for (int32 Axis = 0; Axis < 3; ++Axis) + { + SupportClipMin[Axis] = FMath::Min(SupportClipMin[Axis], Origin[Axis]); + SupportClipMax[Axis] = FMath::Max(SupportClipMax[Axis], Origin[Axis] + Extent); + if (Key.Level < SupportReferenceWorld.configuration().fullResolutionLodCount) + { + SupportFineMin[Axis] = FMath::Min(SupportFineMin[Axis], Origin[Axis]); + SupportFineMax[Axis] = FMath::Max(SupportFineMax[Axis], Origin[Axis] + Extent); + } + } + SupportReferenceKeys.push_back({ + {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, static_cast(Key.Level)}); + } + if (SupportClipMin[0] == MAX_int32) + { + OutError = TEXT("Standalone collision-support tile bounds are invalid."); + return false; + } + if (SupportFineMin[0] == MAX_int32) + { + for (int32 Axis = 0; Axis < 3; ++Axis) + { + SupportFineMin[Axis] = SupportClipMin[Axis]; + SupportFineMax[Axis] = SupportClipMax[Axis]; + } + } + SupportRequest.minVoxel = {SupportFineMin[0], SupportFineMin[1], SupportFineMin[2]}; + SupportRequest.maxVoxel = {SupportFineMax[0], SupportFineMax[1], SupportFineMax[2]}; + SupportRequest.clipMinVoxel = {SupportClipMin[0], SupportClipMin[1], SupportClipMin[2]}; + SupportRequest.clipMaxVoxel = {SupportClipMax[0], SupportClipMax[1], SupportClipMax[2]}; + FStandaloneCoreAdapterScheduler SupportScheduler; + if (!SupportReferenceWorld.compile(SupportRequest, SupportScheduler, SupportError)) + { + OutError = FString::Printf(TEXT("Standalone collision-support compile failed: %s"), + UTF8_TO_TCHAR(SupportError.c_str())); + return false; + } + SupportReferenceWorld.retainTileRegions(SupportReferenceKeys); + VoxelForgeCore::TileGenerationOptions SupportOptions; + SupportOptions.hashDensity = true; + for (const FVoxelTileKey& Key : RuntimeCollisionSupportKeys) + { + const VoxelForgeCore::TileKey SupportKey{ + {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, static_cast(Key.Level)}; + const std::optional SupportMesh = + SupportReferenceWorld.generateTile(SupportKey, SupportOptions, SupportError); + const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key); + if (!SupportMesh || !SupportMesh->hasDensityHash || !SupportMesh->hasMeshHash + || !SupportMesh->hasPayloadHash || !SupportMesh->hasTopologyHash + || Actual == nullptr || !Actual->bHasHashes + || Actual->Density != SupportMesh->densityHash + || Actual->Mesh != SupportMesh->meshHash + || Actual->Payload != SupportMesh->payloadHash + || Actual->Topology != SupportMesh->topologyHash + || Actual->Triangles != static_cast(SupportMesh->indices.size() / 3u) + || Actual->bEmpty != SupportMesh->indices.empty()) + { + OutError = FString::Printf( + TEXT("BuildHost collision-support tile (%d,%d,%d) does not match Standalone."), + Key.Coord.X, Key.Coord.Y, Key.Coord.Z); + return false; + } + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] collision_support_hashes=PASS standalone_tiles=%d"), + RuntimeCollisionSupportKeys.Num()); + } + for (const FVoxelTileKey& Key : ExpectedDesiredSet) + { + const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key); + const FVoxelForgeCoreTileHashRecord* Expected = ExpectedHashes.Find(Key); + if (!ActualVisualSet.Contains(Key) || Actual == nullptr || Expected == nullptr + || !Actual->bHasHashes || Actual->Density != Expected->Density + || Actual->Mesh != Expected->Mesh || Actual->Payload != Expected->Payload + || Actual->Topology != Expected->Topology || Actual->Triangles != Expected->Triangles + || Actual->bEmpty != Expected->bEmpty) + { + OutError = FString::Printf( + TEXT("BuildHost island tile hash mismatch at (%d,%d,%d) LOD%d: " + "density=%016llx/%016llx mesh=%016llx/%016llx payload=%016llx/%016llx " + "topology=%016llx/%016llx triangles=%d/%d empty=%d/%d hashes=%d/%d."), + Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level, + static_cast(Actual ? Actual->Density : 0ull), + static_cast(Expected ? Expected->Density : 0ull), + static_cast(Actual ? Actual->Mesh : 0ull), + static_cast(Expected ? Expected->Mesh : 0ull), + static_cast(Actual ? Actual->Payload : 0ull), + static_cast(Expected ? Expected->Payload : 0ull), + static_cast(Actual ? Actual->Topology : 0ull), + static_cast(Expected ? Expected->Topology : 0ull), + Actual ? Actual->Triangles : -1, Expected ? Expected->Triangles : -1, + Actual && Actual->bEmpty ? 1 : 0, Expected && Expected->bEmpty ? 1 : 0, + Actual && Actual->bHasHashes ? 1 : 0, Expected && Expected->bHasHashes ? 1 : 0); + return false; + } + } + for (const FVoxelTileKey& Key : Actor->CollisionDesiredTiles) + { + const FVoxelForgeCoreTileHashRecord* Expected = ExpectedHashes.Find(Key); + if (Expected == nullptr && Actor->CollisionOnlyTiles.Contains(Key)) + { + const FVoxelForgeCoreTileHashRecord* SupportHash = Actor->VoxelForgeCoreTileHashes.Find(Key); + if (SupportHash == nullptr || !SupportHash->bHasHashes + || (!Actor->CollisionReadyTiles.Contains(Key) + && !Actor->CollisionNotRequiredTiles.Contains(Key))) + { + OutError = FString::Printf( + TEXT("BuildHost island collision-only support tile (%d,%d,%d) has no cooked collision."), + Key.Coord.X, Key.Coord.Y, Key.Coord.Z); + return false; + } + continue; + } + if (Expected == nullptr + || (Expected->bEmpty && !Actor->CollisionNotRequiredTiles.Contains(Key)) + || (!Expected->bEmpty && !Actor->CollisionReadyTiles.Contains(Key))) + { + OutError = FString::Printf(TEXT("BuildHost island collision state differs from Standalone for (%d,%d,%d)."), + Key.Coord.X, Key.Coord.Y, Key.Coord.Z); + return false; + } + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] pass phase=initial standalone_visual_tiles=%d collision_support_extra=%d collision_desired=%d collision_ready=%d collision_empty=%d standalone_hashes=1 lod0=%d lod1=%d transitions=%d"), + ExpectedDesiredSet.Num(), RuntimeCollisionSupportTiles, Actor->CollisionDesiredTiles.Num(), + Actor->CollisionReadyTiles.Num(), Actor->CollisionNotRequiredTiles.Num(), + Lod0Tiles, Lod1Tiles, TransitionFaces); + + const auto LocalPointFromSubvoxels = [](const std::int64_t Position[3]) + { + return FVector( + static_cast(Position[0]) * 25.0 / 256.0, + static_cast(Position[1]) * 25.0 / 256.0, + static_cast(Position[2]) * 25.0 / 256.0); + }; + const FVector IslandTopXY = LocalPointFromSubvoxels( + NativeIslandPlan.manifest.plan.islands.front().centerSubvoxels.data()); + const auto& Connector = NativeIslandPlan.manifest.plan.clusterEdges.front(); + const FVector SpanXY( + (static_cast(Connector.startSubvoxels[0] + Connector.endSubvoxels[0]) * 0.5) + * 25.0 / 256.0, + (static_cast(Connector.startSubvoxels[1] + Connector.endSubvoxels[1]) * 0.5) + * 25.0 / 256.0, + (static_cast(Connector.startSubvoxels[2] + Connector.endSubvoxels[2]) * 0.5) + * 25.0 / 256.0); + float IslandContentGroundCm = 0.0f; + float SpanContentGroundCm = 0.0f; + if (Actor->ContentManager == nullptr + || !Actor->ContentManager->QueryNativeFloatingIslandAnchor(1, + FVector2D(IslandTopXY.X, IslandTopXY.Y), IslandContentGroundCm) + || !Actor->ContentManager->QueryNativeFloatingIslandAnchor(1, + FVector2D(SpanXY.X, SpanXY.Y), SpanContentGroundCm)) + { + OutError = TEXT("Core island content could not resolve the island-top and connector anchors."); + return false; + } + const auto CheckTerrainAnchor = [&](const TCHAR* Label, const FVector& XY, + float ContentGroundCm, float& OutTerrainGroundWorldZ, FVector& OutGroundWorld) -> bool + { + const FVector StartLocal(XY.X, XY.Y, ContentGroundCm + 400.0f); + const FVector StartWorld = Actor->GetActorTransform().TransformPosition(StartLocal); + float TerrainGroundWorldZ = 0.0f, TerrainCeilingWorldZ = 0.0f; + if (!Actor->GetVoxelSurfaceHeightAt(StartWorld, TerrainGroundWorldZ, TerrainCeilingWorldZ)) + { + OutError = FString::Printf(TEXT("Core island %s height query found no ground from air."), Label); + return false; + } + const FVector GroundWorld = Actor->GetActorTransform().TransformPosition( + FVector(XY.X, XY.Y, ContentGroundCm)); + OutTerrainGroundWorldZ = TerrainGroundWorldZ; + OutGroundWorld = FVector(GroundWorld.X, GroundWorld.Y, TerrainGroundWorldZ); + const double DeltaCm = FMath::Abs(TerrainGroundWorldZ - GroundWorld.Z); + if (DeltaCm > 25.0) + { + OutError = FString::Printf(TEXT("Core island %s terrain/content anchors differ by %.2f cm."), + Label, DeltaCm); + return false; + } + return true; + }; + float IslandTerrainGroundWorldZ = 0.0f, SpanTerrainGroundWorldZ = 0.0f; + FVector IslandGroundWorld = FVector::ZeroVector, SpanGroundWorld = FVector::ZeroVector; + if (!CheckTerrainAnchor(TEXT("top"), IslandTopXY, IslandContentGroundCm, + IslandTerrainGroundWorldZ, IslandGroundWorld) + || !CheckTerrainAnchor(TEXT("span"), SpanXY, SpanContentGroundCm, + SpanTerrainGroundWorldZ, SpanGroundWorld)) + { + return false; + } + + const int32 IslandStrate = Actor->GetStrateAtPosition(IslandGroundWorld); + const int32 SpanStrate = Actor->GetStrateAtPosition(SpanGroundWorld); + if (IslandStrate != 1 || SpanStrate != 1 || Actor->AtmosphereManager == nullptr) + { + OutError = FString::Printf(TEXT("Island top/span resolved to wrong strate: island=%d span=%d atmosphere=%d."), + IslandStrate, SpanStrate, Actor->AtmosphereManager ? 1 : 0); + return false; + } + Actor->AtmosphereManager->UpdateForPlayer( + Actor->GetActorTransform().TransformPosition(CameraLocalCm)); + const int32 AtmosphereStrate = Actor->AtmosphereManager->GetCurrentStrateIndex(); + if (AtmosphereStrate != 1) + { + OutError = FString::Printf(TEXT("Island route selected atmosphere strate %d instead of 1."), + AtmosphereStrate); + return false; + } + bool bContentHasWater = false; + float ContentWaterLocalZCm = -FLT_MAX; + if (!Actor->ContentManager->QueryStrateWaterAnchor( + 1, bContentHasWater, ContentWaterLocalZCm)) + { + OutError = TEXT("Core island content did not return the fixed strate water datum."); + return false; + } + float ManagerTopZ = 0.0f, ManagerBottomZ = 0.0f; + const float ManagerRouteZ = Actor->ToStrateManagerLocalZ(CameraLocalCm.Z); + if (!Actor->StrateManager->GetStrateUnrealZRange( + ManagerRouteZ, ManagerTopZ, ManagerBottomZ)) + { + OutError = TEXT("Core island atmosphere check could not resolve corrected strate bounds."); + return false; + } + const float AtmosphereTopCm = Actor->FromStrateManagerLocalZ(ManagerTopZ); + const float AtmosphereBottomCm = Actor->FromStrateManagerLocalZ(ManagerBottomZ); + if (IslandContentGroundCm < AtmosphereBottomCm || IslandContentGroundCm > AtmosphereTopCm + || (bContentHasWater && (ContentWaterLocalZCm < AtmosphereBottomCm + || ContentWaterLocalZCm > AtmosphereTopCm))) + { + OutError = TEXT("Island ground or fixed water datum falls outside the resolved atmosphere strate."); + return false; + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] height_anchor_agreement=PASS island_ground_cm=%.3f island_terrain_world_z=%.3f span_ground_cm=%.3f span_terrain_world_z=%.3f corrected_strate=%d atmosphere_strate=%d water_enabled=%d water_local_cm=%.3f atmosphere_bounds_cm=(%.3f,%.3f)"), + IslandContentGroundCm, IslandTerrainGroundWorldZ, SpanContentGroundCm, + SpanTerrainGroundWorldZ, IslandStrate, AtmosphereStrate, + bContentHasWater ? 1 : 0, ContentWaterLocalZCm, AtmosphereBottomCm, AtmosphereTopCm); + + const auto CheckCapsuleContact = [&](const TCHAR* Label, const FVector& GroundWorld) -> bool + { + const FVector Start = GroundWorld + FVector(0.0, 0.0, 200.0); + const FVector End = GroundWorld + FVector(0.0, 0.0, 60.0); + const FCollisionShape Capsule = FCollisionShape::MakeCapsule(34.8f, 90.1f); + FCollisionQueryParams QueryParams( + SCENE_QUERY_STAT(VoxelForgeCoreIslandCapsuleContact), false); + FHitResult Hit; + if (!TestWorld->SweepSingleByChannel( + Hit, Start, End, FQuat::Identity, ECC_Pawn, Capsule, QueryParams) + || Hit.GetActor() != Actor) + { + OutError = FString::Printf(TEXT("BuildHost capsule did not contact the Core island %s."), Label); + return false; + } + const FVector LocalHit = Actor->GetActorTransform().InverseTransformPosition(Hit.ImpactPoint); + constexpr double CoreTileExtentCm = 32.0 * 25.0; + const FVoxelTileKey Tile(FIntVector( + FMath::FloorToInt(LocalHit.X / CoreTileExtentCm), + FMath::FloorToInt(LocalHit.Y / CoreTileExtentCm), + FMath::FloorToInt(LocalHit.Z / CoreTileExtentCm)), 0); + if (!Actor->CollisionReadyTiles.Contains(Tile) + || FVector::Dist(Hit.ImpactPoint, GroundWorld) > 100.0) + { + OutError = FString::Printf(TEXT("Core island %s capsule hit is outside its cooked support tile."), Label); + return false; + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] capsule_contact=PASS surface=%s tile=(%d,%d,%d) hit_cm=(%.1f,%.1f,%.1f) collision_ready=1"), + Label, Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, + Hit.ImpactPoint.X, Hit.ImpactPoint.Y, Hit.ImpactPoint.Z); + return true; + }; + if (!CheckCapsuleContact(TEXT("island_top"), IslandGroundWorld) + || !CheckCapsuleContact(TEXT("span"), SpanGroundWorld)) + { + return false; + } + + // Let the real async decoration pipeline place a transient tree mesh, then prove each instance + // sits on the Core surface and the neighboring air/rock signs are correct. + const bool bTreesReady = TickWorldUntil(90.0, [&]() + { + TArray Components; + Actor->GetComponents(Components); + for (const UHierarchicalInstancedStaticMeshComponent* Component : Components) + if (Component != nullptr && Component->GetStaticMesh() == ProbeTreeMesh + && Component->GetInstanceCount() > 0) return true; + return false; + }); + if (!bTreesReady) + { + OutError = TEXT("Core island test did not stream the transient tree decoration onto the route island."); + return false; + } + TArray TreeComponents; + Actor->GetComponents(TreeComponents); + int32 TreeInstances = 0; + double MaximumTreeSurfaceDensity = 0.0; + for (const UHierarchicalInstancedStaticMeshComponent* Component : TreeComponents) + { + if (Component == nullptr || Component->GetStaticMesh() != ProbeTreeMesh) continue; + for (int32 InstanceIndex = 0; InstanceIndex < Component->GetInstanceCount(); ++InstanceIndex) + { + FTransform InstanceTransform; + if (!Component->GetInstanceTransform(InstanceIndex, InstanceTransform, true)) continue; + const FVector InstanceWorld = InstanceTransform.GetLocation(); + float AtSurface = 0.0f, BelowSurface = 0.0f, AboveSurface = 0.0f; + if (!Actor->QueryVoxelForgeCoreDensityAt(InstanceWorld, AtSurface) + || !Actor->QueryVoxelForgeCoreDensityAt(InstanceWorld - FVector(0, 0, 25), BelowSurface) + || !Actor->QueryVoxelForgeCoreDensityAt(InstanceWorld + FVector(0, 0, 25), AboveSurface) + || FMath::Abs(AtSurface) > 0.75f || BelowSurface >= 0.0f || AboveSurface <= 0.0f) + { + OutError = FString::Printf(TEXT("Transient island tree %d is not anchored at an air-to-rock Core crossing."), + TreeInstances); + return false; + } + MaximumTreeSurfaceDensity = FMath::Max(MaximumTreeSurfaceDensity, + static_cast(FMath::Abs(AtSurface))); + ++TreeInstances; + } + } + if (TreeInstances == 0) + { + OutError = TEXT("Core island transient tree validation found no instances."); + return false; + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] content_anchor=PASS tree_instances=%d maximum_surface_density=%.5f no_floaters=1"), + TreeInstances, MaximumTreeSurfaceDensity); + + FVector CarveLocal(IslandTopXY.X, IslandTopXY.Y, IslandContentGroundCm - 40.0f); + const FVector CarveWorld = Actor->GetActorTransform().TransformPosition(CarveLocal); + float DensityBefore = 0.0f, OutsideBefore = 0.0f; + if (!Actor->QueryVoxelForgeCoreDensityAt(CarveWorld, DensityBefore) + || !Actor->QueryVoxelForgeCoreDensityAt(CarveWorld + FVector(90, 0, 0), OutsideBefore) + || DensityBefore >= 0.0f) + { + OutError = TEXT("Island Blueprint edit probe did not begin inside solid rock."); + return false; + } + Actor->CarveAtPosition(CarveWorld, 6.0f, 10.0f); + float CarvedDensity = 0.0f, OutsideAfterCarve = 0.0f; + if (!Actor->QueryVoxelForgeCoreDensityAt(CarveWorld, CarvedDensity) + || !Actor->QueryVoxelForgeCoreDensityAt(CarveWorld + FVector(90, 0, 0), OutsideAfterCarve) + || CarvedDensity <= DensityBefore || FMath::Abs(OutsideAfterCarve - OutsideBefore) > 1.0e-4f) + { + OutError = TEXT("Island Blueprint carve did not add air inside 75 cm or changed a point 90 cm away."); + return false; + } + Actor->FillAtPosition(CarveWorld, 6.0f, 10.0f); + float FilledDensity = 0.0f, OutsideAfterFill = 0.0f; + if (!Actor->QueryVoxelForgeCoreDensityAt(CarveWorld, FilledDensity) + || !Actor->QueryVoxelForgeCoreDensityAt(CarveWorld + FVector(90, 0, 0), OutsideAfterFill) + || FilledDensity >= CarvedDensity || FMath::Abs(OutsideAfterFill - OutsideBefore) > 1.0e-4f) + { + OutError = TEXT("Island Blueprint fill did not restore solid direction or changed a point 90 cm away."); + return false; + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] blueprint_edit=PASS carve_sign=air fill_sign=solid radius_legacy_voxels=6 radius_cm=75 strength_legacy=10 density_before=%.4f carved=%.4f filled=%.4f outside_90cm_delta=%.6f"), + DensityBefore, CarvedDensity, FilledDensity, + FMath::Abs(OutsideAfterFill - OutsideBefore)); + + const FString CoreSeasonHash = Actor->GetCurrentSeasonContentHash(); + const float LegacyDensityBeforeSwitch = Actor->Generator->GetDensityAt( + IslandTopXY.X / VOXEL_SIZE, IslandTopXY.Y / VOXEL_SIZE, + IslandContentGroundCm / VOXEL_SIZE); + Actor->bUseVoxelForgeCore = false; + float DisabledAnchorZ = 0.0f; + const bool bDisabledAnchorAvailable = Actor->ContentManager->QueryNativeFloatingIslandAnchor( + 1, FVector2D(IslandTopXY.X, IslandTopXY.Y), DisabledAnchorZ); + const FString LegacySeasonHash = Actor->GetCurrentSeasonContentHash(); + const float LegacyDensityAfterSwitch = Actor->Generator->GetDensityAt( + IslandTopXY.X / VOXEL_SIZE, IslandTopXY.Y / VOXEL_SIZE, + IslandContentGroundCm / VOXEL_SIZE); + if (bDisabledAnchorAvailable || Actor->bUseVoxelForgeCore + || FMath::Abs(LegacyDensityAfterSwitch - LegacyDensityBeforeSwitch) > 1.0e-6f + || CoreSeasonHash.IsEmpty() || CoreSeasonHash == LegacySeasonHash) + { + OutError = TEXT("Core-off island check changed legacy generation or retained Core manifest anchors/hash."); + return false; + } + Actor->bUseVoxelForgeCore = true; + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeCoreIslandTest] switch_off_legacy=PASS core_hash=%s off_hash=%s native_anchor_when_disabled=0 legacy_voxel_cm=%.1f legacy_density_before=%.6f after=%.6f"), + *CoreSeasonHash, *LegacySeasonHash, VOXEL_SIZE, + LegacyDensityBeforeSwitch, LegacyDensityAfterSwitch); + + UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreIslandTest] pass complete=1")); + return true; +} + bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FString& OutError) { + if (Arguments.bCoreFloatingIslandTest) + { + return RunCoreFloatingIslandActorIntegrationTest(Arguments, OutError); + } UVoxelSettings* AuthoredSettings = LoadObject( nullptr, *Arguments.SettingsReference); if (!AuthoredSettings) @@ -15241,6 +16524,9 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin int32 GeometryAuditL0Distance = MAX_int32; int32 GeometryAuditL1Distance = MAX_int32; int32 GeometryAuditHorizonDistance = MAX_int32; + FAssemblerGeometryAudit BestSpanGeometryAuditL0; + double BestSpanRatioL0 = -std::numeric_limits::infinity(); + int32 GeometryAuditL0CandidateCount = 0; int32 HorizonAuditLOD = 0; for (const VoxelForgeCore::TileKey& Key : SurfaceTravelDesired) { @@ -15292,6 +16578,16 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin } const double SpanRatioX = Candidate.SpanXcm / Candidate.ExpectedTileSpanCm; const double SpanRatioY = Candidate.SpanYcm / Candidate.ExpectedTileSpanCm; + if (LOD == 0) + { + ++GeometryAuditL0CandidateCount; + const double MinimumSpanRatio = FMath::Min(SpanRatioX, SpanRatioY); + if (MinimumSpanRatio > BestSpanRatioL0) + { + BestSpanRatioL0 = MinimumSpanRatio; + BestSpanGeometryAuditL0 = Candidate; + } + } if (SpanRatioX >= 0.75 && SpanRatioY >= 0.75 && SpanRatioX <= 1.05 && SpanRatioY <= 1.05) { @@ -15356,6 +16652,12 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin } if (!GeometryAuditL0.bValid || !GeometryAuditL1.bValid || !GeometryAuditHorizon.bValid) { + UE_LOG(LogTemp, Error, + TEXT("[VoxelForgeGeometryAudit] best_lod0_candidate count=%d min_span_ratio=%.6f tile=(%d,%d,%d) span_cm=(%.3f,%.3f) expected_cm=%.3f"), + GeometryAuditL0CandidateCount, BestSpanRatioL0, + BestSpanGeometryAuditL0.Tile.X, BestSpanGeometryAuditL0.Tile.Y, + BestSpanGeometryAuditL0.Tile.Z, BestSpanGeometryAuditL0.SpanXcm, + BestSpanGeometryAuditL0.SpanYcm, BestSpanGeometryAuditL0.ExpectedTileSpanCm); OutError = FString::Printf( TEXT("Native geometry audit missed full-span tiles (l0=%d l1=%d horizon_lod=%d horizon=%d)."), GeometryAuditL0.bValid ? 1 : 0, GeometryAuditL1.bValid ? 1 : 0, diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 2d8b011..6480bfa 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -278,23 +278,6 @@ namespace VoxelForgeCore std::uint64_t contentHash = 0; }; - struct VFCORE_API WorldAssemblyManifest - { - std::uint32_t schemaVersion = WorldAssemblySchemaVersion; - WorldAssemblyRecipeV1 recipe; - WorldAssemblyPlanV1 plan; - std::uint64_t contentHash = 0; - - // JSON is the reviewed season-manifest payload. Hashes use canonical ordered fields, - // little-endian integers and FNV-1a 64; textual key order and whitespace do not matter. - // Le JSON est le payload de saison valide; les entiers little-endian sont haches en FNV-1a 64, sans dependance a la mise en forme. - static std::optional FromJson( - std::string_view json, std::string& error); - bool finalize(std::string& error); - bool validate(std::string& error) const; - std::string ToJson() const; - }; - // Reusable finite 3D island features. The recipe owns fixed-scale shape ranges; the plan // serializes the seeded anchors, connected cluster graph, route and sharp-ready connectors. // Reutilisable dans toute recette : le plan porte les ancres, le graphe, la route et les plans nets. @@ -305,6 +288,12 @@ namespace VoxelForgeCore // bounded lobed roots and arched spans without changing the feature-family schema. std::uint32_t geometryVersion = 2; std::uint32_t unitsPerVoxel = static_cast(WorldAssemblySubvoxelsPerVoxel); + // Import scale is explicit: legacy owner voxels are 12.5 cm and Core voxels are 25 cm. + // L'echelle d'import est explicite : voxel owner de 12,5 cm et voxel Core de 25 cm. + std::uint32_t ownerVoxelSizeMicrons = 125000; + std::uint32_t coreVoxelSizeMicrons = 250000; + std::uint32_t ownerToCoreScaleNumerator = 1; + std::uint32_t ownerToCoreScaleDenominator = 2; std::uint32_t seed = 0; std::int64_t layoutOriginXSubvoxels = 0; std::int64_t layoutOriginYSubvoxels = 0; @@ -327,9 +316,9 @@ namespace VoxelForgeCore std::int64_t rootDepthRatioQ16 = 49152; // 0.75 radius, profile 2 only std::int64_t blendRadiusSubvoxels = 3 * WorldAssemblySubvoxelsPerVoxel; std::int64_t contourWarpSubvoxels = 6 * WorldAssemblySubvoxelsPerVoxel + 77; // 6.3 - std::int64_t roughnessSubvoxels = 3 * WorldAssemblySubvoxelsPerVoxel / 2; + std::int64_t roughnessSubvoxels = WorldAssemblySubvoxelsPerVoxel; std::int64_t boundarySealSubvoxels = 2 * WorldAssemblySubvoxelsPerVoxel; - std::int64_t bridgeHalfWidthSubvoxels = 6 * WorldAssemblySubvoxelsPerVoxel; + std::int64_t bridgeHalfWidthSubvoxels = 3 * WorldAssemblySubvoxelsPerVoxel; std::int64_t bridgeThicknessSubvoxels = 4 * WorldAssemblySubvoxelsPerVoxel; WorldAssemblyRouteConstraintsV1 routeConstraints; std::uint64_t contentHash = 0; @@ -384,6 +373,32 @@ namespace VoxelForgeCore std::string ToJson() const; }; + struct WorldAssemblyFloatingIslandPlanV1 + { + std::int32_t strateIndex = -1; + std::int32_t topChunkZ = 0; + std::int32_t bottomChunkZ = 0; + FloatingIslandManifestV1 manifest; + }; + + struct VFCORE_API WorldAssemblyManifest + { + std::uint32_t schemaVersion = WorldAssemblySchemaVersion; + WorldAssemblyRecipeV1 recipe; + WorldAssemblyPlanV1 plan; + std::vector floatingIslands; + std::uint64_t contentHash = 0; + + // JSON is the reviewed season-manifest payload. Hashes use canonical ordered fields, + // little-endian integers and FNV-1a 64; textual key order and whitespace do not matter. + // Le JSON est le payload de saison valide; les entiers little-endian sont haches en FNV-1a 64, sans dependance a la mise en forme. + static std::optional FromJson( + std::string_view json, std::string& error); + bool finalize(std::string& error); + bool validate(std::string& error) const; + std::string ToJson() const; + }; + struct FloatingIslandBoundsAuditResult { std::uint64_t boxesTested = 0; diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h index e9e339a..8783687 100644 --- a/Standalone/Core/src/FloatingIslandsInternal.h +++ b/Standalone/Core/src/FloatingIslandsInternal.h @@ -158,6 +158,10 @@ namespace floating_island_detail std::uint64_t hash = StartHash(); HashWord(hash, recipe.schemaVersion, 4); HashWord(hash, recipe.unitsPerVoxel, 4); + HashWord(hash, recipe.ownerVoxelSizeMicrons, 4); + HashWord(hash, recipe.coreVoxelSizeMicrons, 4); + HashWord(hash, recipe.ownerToCoreScaleNumerator, 4); + HashWord(hash, recipe.ownerToCoreScaleDenominator, 4); HashWord(hash, recipe.seed, 4); HashWord(hash, static_cast(recipe.layoutOriginXSubvoxels), 8); HashWord(hash, static_cast(recipe.layoutOriginYSubvoxels), 8); @@ -258,6 +262,10 @@ namespace floating_island_detail || (recipe.geometryVersion == 1 && (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0)) || recipe.unitsPerVoxel != Q + || recipe.ownerVoxelSizeMicrons != 125000 + || recipe.coreVoxelSizeMicrons != 250000 + || recipe.ownerToCoreScaleNumerator != 1 + || recipe.ownerToCoreScaleDenominator != 2 || recipe.primaryIslandCount < 3 || recipe.primaryIslandCount > 9 || recipe.layoutMaximumZSubvoxels <= recipe.layoutMinimumZSubvoxels || recipe.layoutMaximumZSubvoxels - recipe.layoutMinimumZSubvoxels > 2048 * Q @@ -339,6 +347,30 @@ namespace floating_island_detail return static_cast(recipe.contourWarpSubvoxels) * (0.50 * a + 0.30 * b + 0.20 * c); } + inline double LobePhase(const FloatingIslandRecipeV1& recipe, + const FloatingIslandAnchorV1& island) + { + return static_cast(Roll(recipe.seed, island.stableId, 0x4c4f4245ull) + & 0xffffffffull) * (2.0 * Pi / 4294967296.0); + } + + inline double LobeOffsetWithPhase(const FloatingIslandRecipeV1& recipe, + double angle,double zSubvoxels,double phase) + { + const double z = zSubvoxels / static_cast(Q); + const double a = std::sin(3.0 * angle + phase + z * 0.017); + const double b = std::sin(5.0 * angle - phase * 0.73 - z * 0.023); + const double c = std::sin(8.0 * angle + phase * 1.41 + z * 0.011); + return static_cast(recipe.contourWarpSubvoxels) * (0.50 * a + 0.30 * b + 0.20 * c); + } + + inline double LobeOffsetAtAngle(const FloatingIslandRecipeV1& recipe, + const FloatingIslandAnchorV1& island, + double angle,double zSubvoxels) + { + return LobeOffsetWithPhase(recipe,angle,zSubvoxels,LobePhase(recipe,island)); + } + inline double Roughness(const FloatingIslandRecipeV1& recipe, std::uint64_t featureId, double x, double y, double z) { @@ -382,6 +414,83 @@ namespace floating_island_detail return static_cast(island.topZSubvoxels) - drop; } + struct FIslandColumnCacheEntry + { + bool bValid=false; + std::uint32_t Seed=0; + std::uint64_t StableId=0; + std::int64_t SampleX=0,SampleY=0,CenterX=0,CenterY=0,TopZ=0,Radius=0,TopHalf=0; + std::int64_t TopFlattenQ16=0,ContourWarp=0; + double DeltaX=0.0,DeltaY=0.0,Radial=0.0,Angle=0.0,LobePhase=0.0,TopSurface=0.0; + }; + + inline const FIslandColumnCacheEntry& IslandColumnCache( + const FloatingIslandRecipeV1& recipe,const FloatingIslandAnchorV1& island, + double x,double y) + { + // Density samples arrive on integer Core lattice columns; reuse the exact column metrics + // across Z samples without changing the field value or touching shared world state. + // Les echantillons suivent les colonnes Core entieres; reutiliser les memes metriques XY. + constexpr std::size_t Ways=4; + constexpr std::size_t SetCount=2048; + static_assert((SetCount&(SetCount-1))==0); + static thread_local std::array Cache{}; + const std::int64_t SampleX=static_cast(x); + const std::int64_t SampleY=static_cast(y); + std::uint64_t IndexKey=island.stableId + ^(static_cast(SampleX)*0x9e3779b97f4a7c15ull) + ^(static_cast(SampleY)*0xc2b2ae3d27d4eb4full) + ^(static_cast(recipe.seed)<<23); + IndexKey^=IndexKey>>33; + IndexKey*=0xff51afd7ed558ccdull; + IndexKey^=IndexKey>>33; + const std::size_t Base=static_cast(IndexKey&(SetCount-1))*Ways; + FIslandColumnCacheEntry* Replacement=&Cache[Base+((IndexKey>>11)&(Ways-1))]; + for(std::size_t Way=0;Way(island.centerSubvoxels[0]); + Entry.DeltaY=y-static_cast(island.centerSubvoxels[1]); + Entry.Radial=std::hypot(Entry.DeltaX,Entry.DeltaY); + Entry.Angle=std::atan2(Entry.DeltaY,Entry.DeltaX); + Entry.LobePhase=LobePhase(recipe,island); + const double EffectiveRadius=std::max(1.0,static_cast(island.radiusSubvoxels) + +LobeOffsetWithPhase(recipe,Entry.Angle,static_cast(island.topZSubvoxels), + Entry.LobePhase)); + const double Edge=std::clamp(Entry.Radial/EffectiveRadius,0.0,1.0); + const double Drop=(1.0-Q16ToDouble(recipe.topFlattenQ16)) + *static_cast(island.topHalfSubvoxels)*2.0*Edge*Edge; + Entry.TopSurface=static_cast(island.topZSubvoxels)-Drop; + return Entry; + } + inline std::int64_t ScaledQ16(std::int64_t value, std::int64_t scaleQ16) { return (value * scaleQ16 + Q16 / 2) / Q16; } @@ -406,42 +515,104 @@ namespace floating_island_detail inline std::int32_t RootCount(const FloatingIslandRecipeV1& recipe, const FloatingIslandAnchorV1& island) { - return 3 + static_cast(Roll(recipe.seed, island.stableId, - 0x524f4f54434e54ull) & 1ull); + // Keep four separate side lobes and a full-depth central buttress to close the measured + // underside cavity without driving the outer roots to one point. + // Garder quatre lobes lateraux et un renfort central profond pour fermer la cavite mesuree. + return 5; } inline double UnitHash(std::uint64_t hash) { return static_cast(hash & 0xffffull) / 65535.0; } + struct FRootLobeParameters + { + double RootX=0.0,RootY=0.0,CenterZ=0.0; + double RadiusXSquared=1.0,RadiusYSquared=1.0,VerticalRadiusSquared=1.0; + double MinimumRadius=1.0; + }; + + struct FRootLobeCacheEntry + { + bool bValid=false; + std::uint32_t Seed=0; + std::uint64_t StableId=0; + std::int32_t RootIndex=0,GeometryVersion=0; + std::array Center{}; + std::int64_t TopZ=0,Radius=0,TaperEndQ16=0; + std::int64_t ThicknessRatioQ16=0,CrustDepthBonusQ16=0,RootDepthRatioQ16=0; + FRootLobeParameters Parameters{}; + }; + inline double RootLobeDensity(const FloatingIslandRecipeV1& recipe, const FloatingIslandAnchorV1& island, std::int32_t rootIndex, double x, double y, double z) { - constexpr std::array, 4> pattern{{ - {{-0.92, -0.12}}, {{0.28, 0.98}}, {{0.83, -0.42}}, {{-0.16, -0.94}}}}; - const std::uint64_t salt = 0x524f4f540000ull + static_cast(rootIndex); - const std::uint64_t roll = Roll(recipe.seed, island.stableId, salt); - const double radius = static_cast(island.radiusSubvoxels); - const double rootDepth = radius * Q16ToDouble(recipe.rootDepthRatioQ16); - const double crustBottom = static_cast(IslandCrustBottom(recipe, island)); - const double orbit = radius * (0.17 + 0.07 * UnitHash(roll)); - const double rootX = static_cast(island.centerSubvoxels[0]) - + pattern[static_cast(rootIndex)][0] * orbit; - const double rootY = static_cast(island.centerSubvoxels[1]) - + pattern[static_cast(rootIndex)][1] * orbit; - const double shortRootVariation = UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x4c454eull)); - const double lengthRatio = rootIndex == 0 ? 1.0 - : Q16ToDouble(island.taperEndQ16) * (0.82 + 0.18 * shortRootVariation); - const double lowerDepth = rootDepth * lengthRatio; - const double embedDepth = rootDepth * (rootIndex == 0 ? 0.30 : 0.22); - const double verticalRadius = 0.5 * (lowerDepth + embedDepth); - const double centerZ = crustBottom + 0.5 * (embedDepth - lowerDepth); - const double rx = radius * (0.15 + 0.055 * UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x5258ull))); - const double ry = radius * (0.15 + 0.065 * UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x5259ull))); - const double q = (x - rootX) * (x - rootX) / (rx * rx) - + (y - rootY) * (y - rootY) / (ry * ry) - + (z - centerZ) * (z - centerZ) / (verticalRadius * verticalRadius); - return (std::sqrt(std::max(q, 0.0)) - 1.0) * std::min({rx, ry, verticalRadius}); + // Cache derived root constants per worker. The complete immutable input key keeps this + // value-only cache safe across worlds, epochs and worker schedules. + // Mettre en cache les constantes par worker; la cle complete evite tout partage d'etat. + constexpr std::size_t CacheSize=1024; + static_assert((CacheSize&(CacheSize-1))==0); + static thread_local std::array Cache{}; + const std::uint64_t IndexKey=island.stableId^(island.stableId>>33) + ^(static_cast(recipe.seed)<<17) + ^(static_cast(static_cast(rootIndex))*0x9e3779b97f4a7c15ull); + FRootLobeCacheEntry& Entry=Cache[static_cast(IndexKey)&(CacheSize-1)]; + const bool bKeyMatches=Entry.bValid&&Entry.Seed==recipe.seed + &&Entry.StableId==island.stableId&&Entry.RootIndex==rootIndex + &&Entry.GeometryVersion==recipe.geometryVersion + &&Entry.Center==island.centerSubvoxels + &&Entry.TopZ==island.topZSubvoxels&&Entry.Radius==island.radiusSubvoxels + &&Entry.TaperEndQ16==island.taperEndQ16 + &&Entry.ThicknessRatioQ16==recipe.thicknessRatioQ16 + &&Entry.CrustDepthBonusQ16==recipe.crustDepthBonusQ16 + &&Entry.RootDepthRatioQ16==recipe.rootDepthRatioQ16; + if(!bKeyMatches) + { + constexpr std::array, 4> pattern{{ + {{-0.92, -0.12}}, {{0.28, 0.98}}, {{0.83, -0.42}}, {{-0.16, -0.94}}}}; + const std::uint64_t salt = 0x524f4f540000ull + static_cast(rootIndex); + const std::uint64_t roll = Roll(recipe.seed, island.stableId, salt); + const double radius = static_cast(island.radiusSubvoxels); + const double rootDepth = radius * Q16ToDouble(recipe.rootDepthRatioQ16); + const double crustBottom = static_cast(IslandCrustBottom(recipe, island)); + const bool centralButtress = rootIndex == 4; + const double orbit = centralButtress ? 0.0 + : radius * (0.17 + 0.07 * UnitHash(roll)); + const double rootX = static_cast(island.centerSubvoxels[0]) + + (centralButtress ? 0.0 : pattern[static_cast(rootIndex)][0] * orbit); + const double rootY = static_cast(island.centerSubvoxels[1]) + + (centralButtress ? 0.0 : pattern[static_cast(rootIndex)][1] * orbit); + const double shortRootVariation = UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x4c454eull)); + const double lengthRatio = centralButtress ? 1.0 : (rootIndex == 0 ? 1.0 + : Q16ToDouble(island.taperEndQ16) * (0.82 + 0.18 * shortRootVariation)); + const double lowerDepth = rootDepth * lengthRatio; + const double embedDepth = rootDepth * (centralButtress ? 0.22 : (rootIndex == 0 ? 0.30 : 0.22)); + const double verticalRadius = 0.5 * (lowerDepth + embedDepth); + const double centerZ = crustBottom + 0.5 * (embedDepth - lowerDepth); + const double rx = radius * (centralButtress ? 0.25 + : 0.15 + 0.055 * UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x5258ull))); + const double ry = radius * (centralButtress ? 0.25 + : 0.15 + 0.065 * UnitHash(Roll(recipe.seed, island.stableId, salt ^ 0x5259ull))); + Entry.bValid=true; + Entry.Seed=recipe.seed; + Entry.StableId=island.stableId; + Entry.RootIndex=rootIndex; + Entry.GeometryVersion=recipe.geometryVersion; + Entry.Center=island.centerSubvoxels; + Entry.TopZ=island.topZSubvoxels; + Entry.Radius=island.radiusSubvoxels; + Entry.TaperEndQ16=island.taperEndQ16; + Entry.ThicknessRatioQ16=recipe.thicknessRatioQ16; + Entry.CrustDepthBonusQ16=recipe.crustDepthBonusQ16; + Entry.RootDepthRatioQ16=recipe.rootDepthRatioQ16; + Entry.Parameters={rootX,rootY,centerZ,rx*rx,ry*ry, + verticalRadius*verticalRadius,std::min({rx,ry,verticalRadius})}; + } + const FRootLobeParameters& Parameters=Entry.Parameters; + const double q = (x - Parameters.RootX) * (x - Parameters.RootX) / Parameters.RadiusXSquared + + (y - Parameters.RootY) * (y - Parameters.RootY) / Parameters.RadiusYSquared + + (z - Parameters.CenterZ) * (z - Parameters.CenterZ) / Parameters.VerticalRadiusSquared; + return (std::sqrt(std::max(q, 0.0)) - 1.0) * Parameters.MinimumRadius; } inline double ApplySurfaceDetail(const FloatingIslandRecipeV1& recipe, @@ -482,19 +653,18 @@ namespace floating_island_detail std::max({z - top, radial - envelope, bottomZ - z}), x, y, z); } - const double dx = x - static_cast(island.centerSubvoxels[0]); - const double dy = y - static_cast(island.centerSubvoxels[1]); - const double radial = std::hypot(dx, dy); + const FIslandColumnCacheEntry& Column=IslandColumnCache(recipe,island,x,y); + const double dx=Column.DeltaX; + const double dy=Column.DeltaY; + const double radial=Column.Radial; const double topZ = static_cast(island.topZSubvoxels); const double radius = static_cast(island.radiusSubvoxels); const double effectiveRadius = std::max(0.0, - radius + LobeOffset(recipe, island, dx, dy, z)); + radius + LobeOffsetWithPhase(recipe,Column.Angle,z,Column.LobePhase)); const double edge = std::clamp(radial / std::max(effectiveRadius, 1.0), 0.0, 1.0); const double lowerSurface = static_cast(IslandCrustBottom(recipe, island)) + radius * 0.16 * edge * edge; - const double top = IslandTopSurface(recipe, island, - static_cast(island.centerSubvoxels[0]) + dx, - static_cast(island.centerSubvoxels[1]) + dy); + const double top = Column.TopSurface; const double topDistance = z - top; const double sideDistance = radial - effectiveRadius; const double undersideDistance = lowerSurface - z; @@ -774,7 +944,9 @@ namespace floating_island_detail // Land each connector well inside the broad top so the full capsule footprint remains // supported across the bridge-to-island transition, including bounded surface roughness. // Les connecteurs recouvrent le plateau pour soutenir toute la capsule au raccord. - const double landingFraction = recipe.geometryVersion >= 2 ? 0.38 : 0.62; + // Keep the narrow span exposed to open sky; a smaller fraction buries most of it in the island tops. + // Garder la passerelle etroite visible dans le vide ; une fraction plus faible l'enfouit dans les plateaux. + const double landingFraction = recipe.geometryVersion >= 2 ? 0.52 : 0.62; const double fromReach = static_cast(from.radiusSubvoxels) * landingFraction; const double toReach = static_cast(to.radiusSubvoxels) * landingFraction; const double sx = from.centerSubvoxels[0] + ux * fromReach; @@ -912,6 +1084,10 @@ std::optional FloatingIslandManifestV1::FromJson( || !floating_island_detail::ReadHash(root.Get("content_hash"), manifestHash) || !floating_island_detail::ReadU32(recipeValue.Get("schema_version"), manifest.recipe.schemaVersion) || !floating_island_detail::ReadU32(recipeValue.Get("units_per_voxel"), manifest.recipe.unitsPerVoxel) + || !floating_island_detail::ReadU32(recipeValue.Get("owner_voxel_size_microns"), manifest.recipe.ownerVoxelSizeMicrons) + || !floating_island_detail::ReadU32(recipeValue.Get("core_voxel_size_microns"), manifest.recipe.coreVoxelSizeMicrons) + || !floating_island_detail::ReadU32(recipeValue.Get("owner_to_core_scale_numerator"), manifest.recipe.ownerToCoreScaleNumerator) + || !floating_island_detail::ReadU32(recipeValue.Get("owner_to_core_scale_denominator"), manifest.recipe.ownerToCoreScaleDenominator) || !floating_island_detail::ReadU32(recipeValue.Get("seed"), manifest.recipe.seed) || !floating_island_detail::ReadInteger(recipeValue.Get("origin_x_q"), manifest.recipe.layoutOriginXSubvoxels) || !floating_island_detail::ReadInteger(recipeValue.Get("origin_y_q"), manifest.recipe.layoutOriginYSubvoxels) @@ -1026,6 +1202,10 @@ std::string FloatingIslandManifestV1::ToJson() const << floating_island_detail::HashText(value.contentHash) << "\",\"recipe\":{" << "\"schema_version\":" << recipe.schemaVersion << ",\"units_per_voxel\":" << recipe.unitsPerVoxel + << ",\"owner_voxel_size_microns\":" << recipe.ownerVoxelSizeMicrons + << ",\"core_voxel_size_microns\":" << recipe.coreVoxelSizeMicrons + << ",\"owner_to_core_scale_numerator\":" << recipe.ownerToCoreScaleNumerator + << ",\"owner_to_core_scale_denominator\":" << recipe.ownerToCoreScaleDenominator << ",\"seed\":" << recipe.seed << ",\"origin_x_q\":" << recipe.layoutOriginXSubvoxels << ",\"origin_y_q\":" << recipe.layoutOriginYSubvoxels @@ -1190,6 +1370,17 @@ std::optional PlanFloatingIslandCluster( static_cast(std::ceil(riseLimit(static_cast(i), static_cast(i + 1))))); } + if (recipe.geometryVersion >= 2) + { + // Step the outer route islands around the middle island so the connected path still reads as separate floating masses. + // Decaler les iles externes autour de l'ile centrale pour distinguer les masses tout en gardant la route continue. + constexpr std::int64_t VerticalStagger = 3 * floating_island_detail::Q; + for (std::uint32_t i = 0; i < count; ++i) + { + if (i < middle) tops[i] -= VerticalStagger; + else if (i > middle) tops[i] += VerticalStagger; + } + } for (std::uint32_t i = 0; i < count; ++i) { const std::uint64_t site = 0x100000000ull + i; diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index d1fc725..7649fa3 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -936,6 +936,12 @@ public: // edge interpolation and normals to the existing mesher. float DensityAt(float X,float Y,float Z) const { + float IslandDensity=0.0f; + if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity)) + { + ++ThreadClassificationStats().NativeFloatFieldCalls; + return ApplyEditBrushes(IslandDensity,X,Y,Z); + } float NativeDensity=0.0f; if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) { @@ -954,6 +960,13 @@ public: float DensityAtLattice(int32 X,int32 Y,int32 Z) const { + float IslandDensity=0.0f; + if(NativeFloatingIslandDensityAt(static_cast(X),static_cast(Y), + static_cast(Z),IslandDensity)) + { + ++ThreadClassificationStats().NativeLatticeFieldCalls; + return ApplyEditBrushes(IslandDensity,X,Y,Z); + } float NativeDensity=0.0f; if(NativeSurfaceDensityAt(static_cast(X),static_cast(Y), static_cast(Z),NativeDensity)) @@ -975,6 +988,12 @@ public: float DensityAtUnclamped(float X,float Y,float Z) const { + float IslandDensity=0.0f; + if(NativeFloatingIslandDensityAt(X,Y,Z,IslandDensity)) + { + ++ThreadClassificationStats().NativeUnclampedFieldCalls; + return ApplyEditBrushes(IslandDensity,X,Y,Z); + } float NativeDensity=0.0f; if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity)) { @@ -1007,11 +1026,18 @@ public: Count=std::clamp(Count,0,8); if(Count==0)return; bool bHasLegacySurfaceSample=false; + bool bHasNativeIslandSample=false; for(int32 Lane=0;Lane(XStart+Lane*XStep),static_cast(Y), - static_cast(Z)); - if(bHasLegacySurfaceSample) + { + const float SampleX=static_cast(XStart+Lane*XStep); + const float SampleY=static_cast(Y); + const float SampleZ=static_cast(Z); + bHasLegacySurfaceSample|=UsesLegacySurfaceField(SampleX,SampleY,SampleZ); + float IslandDensity=0.0f; + bHasNativeIslandSample|=NativeFloatingIslandDensityAt( + SampleX,SampleY,SampleZ,IslandDensity); + } + if(bHasLegacySurfaceSample||bHasNativeIslandSample) { for(int32 Lane=0;LaneManager) { + // Keep the delivered SurfaceWorld chunk mapping unchanged. + // Conserver le mapping de chunks SurfaceWorld deja livre. const int32 FirstChunk=FMath::FloorToInt(static_cast(BMin[2]) /static_cast(CHUNK_SIZE)); const int32 LastChunk=FMath::FloorToInt(static_cast(BMax[2]) /static_cast(CHUNK_SIZE)); for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ) { - if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)) - ==ECaveGeneratorType::SurfaceWorld) + const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk( + FIntVector(0,0,ChunkZ)); + if(Type==ECaveGeneratorType::SurfaceWorld) { bSurfaceOverlap=true; const int32 StrateIndex=Owner->Manager->GetStrateIndex( @@ -1211,12 +1245,39 @@ public: bBadPlan=true; SurfacePlan=Candidate; } - else bOtherStrateOverlap=true; + else if(Type!=ECaveGeneratorType::FloatingIslands) + bOtherStrateOverlap=true; + } + + // Floating islands use the explicit 12.5 cm owner to 25 cm Core mapping: + // one legacy 4 m chunk occupies 16 Core voxels. + // Les iles flottantes suivent l'echelle explicite proprietaire/Core: 16 voxels Core. + constexpr int32 CoreVoxelsPerIslandChunk=CHUNK_SIZE/2; + const int32 FirstIslandChunk=FMath::FloorToInt(static_cast(BMin[2]) + /static_cast(CoreVoxelsPerIslandChunk)); + const int32 LastIslandChunk=FMath::FloorToInt(static_cast(BMax[2]) + /static_cast(CoreVoxelsPerIslandChunk)); + for(int32 ChunkZ=FirstIslandChunk;ChunkZ<=LastIslandChunk;++ChunkZ) + { + const ECaveGeneratorType Type=Owner->Manager->GetGeneratorTypeForChunk( + FIntVector(0,0,ChunkZ)); + if(Type==ECaveGeneratorType::FloatingIslands) + { + bIslandOverlap=true; + const int32 StrateIndex=Owner->Manager->GetStrateIndex( + static_cast((static_cast(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE)); + const auto* Candidate=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ); + if(!Candidate||(IslandPlan&&IslandPlan!=Candidate))bBadPlan=true; + IslandPlan=Candidate; + } + else bIslandOtherStrateOverlap=true; } } const auto Finish=[&](EVoxelTileClass Verdict,bool bUnknown=false) { - FBoxClassification Result{Verdict,bSurfaceOverlap&&!bOtherStrateOverlap,bUnknown}; + const bool bNative=(bSurfaceOverlap&&!bOtherStrateOverlap) + ||(bIslandOverlap&&!bIslandOtherStrateOverlap); + FBoxClassification Result{Verdict,bNative,bUnknown}; auto& Counts=ThreadClassificationStats(); Counts.Milliseconds+=std::chrono::duration(Clock::now()-Start).count(); if(!Result.bNativeSurface||bUnknown)++Counts.UnknownBoxes; @@ -1227,7 +1288,9 @@ public: }; // A box crossing recipes cannot use one field interval; unknown composition stays Mixed. // Une boite qui traverse plusieurs recettes reste Mixed, car l'intervalle d'un seul champ ne suffit pas. - if(bBadPlan|| (bSurfaceOverlap&&bOtherStrateOverlap)) + if(bBadPlan||(bSurfaceOverlap&&(bIslandOverlap||bOtherStrateOverlap)) + ||(bIslandOverlap&&(bIslandOtherStrateOverlap + ||(!bSurfaceOverlap&&bOtherStrateOverlap)))) return Finish(EVoxelTileClass::Mixed,true); if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0] &&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1] @@ -1273,6 +1336,38 @@ public: default:return Finish(EVoxelTileClass::Mixed,true); } } + if(bIslandOverlap) + { + if(!IslandPlan)return Finish(EVoxelTileClass::Mixed,true); + constexpr std::int64_t Scale=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + constexpr std::int64_t CoreVoxelsPerLegacyChunk=CHUNK_SIZE/2; + const std::int64_t Bottom=static_cast(IslandPlan->bottomChunkZ) + *CoreVoxelsPerLegacyChunk*Scale; + const std::int64_t Top=(static_cast(IslandPlan->topChunkZ)+1) + *CoreVoxelsPerLegacyChunk*Scale; + const std::int64_t Seal=IslandPlan->manifest.recipe.boundarySealSubvoxels; + const std::int64_t BoxBottom=static_cast(BMin[2])*Scale; + const std::int64_t BoxTop=static_cast(BMax[2])*Scale; + const bool bBottomSealOverlap=BoxBottom=Bottom; + const bool bTopSealOverlap=BoxBottom<=Top&&BoxTop>Top-Seal; + if(bBottomSealOverlap) + return Finish(BoxBottom>=Bottom&&BoxTop<=Bottom+Seal + ?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed); + if(bTopSealOverlap) + return Finish(BoxBottom>=Top-Seal&&BoxTop<=Top + ?EVoxelTileClass::AllSolid:EVoxelTileClass::Mixed); + const VoxelForgeCore::FloatingIslandFieldV1 Block( + IslandPlan->manifest.recipe,IslandPlan->manifest.plan); + if(!Block.isValid())return Finish(EVoxelTileClass::Mixed,true); + const auto Interval=Block.interval(static_cast(BMin[0])*Scale, + static_cast(BMin[1])*Scale,static_cast(BMin[2])*Scale, + static_cast(BMax[0])*Scale,static_cast(BMax[1])*Scale, + static_cast(BMax[2])*Scale); + if(!Interval.known)return Finish(EVoxelTileClass::Mixed,true); + if(Interval.minimumDensitySubvoxels>=0)return Finish(EVoxelTileClass::AllAir); + if(Interval.maximumDensitySubvoxels<0)return Finish(EVoxelTileClass::AllSolid); + return Finish(EVoxelTileClass::Mixed); + } const bool bRock=EvaluateFast((BMin[0]+BMax[0])*0.5, (BMin[1]+BMax[1])*0.5,(BMin[2]+BMax[2])*0.5); return Finish(bRock?EVoxelTileClass::AllSolid:EVoxelTileClass::AllAir); @@ -1753,6 +1848,16 @@ private: return It==WorldAssembly->plan.strates.end()?nullptr:&*It; } + const VoxelForgeCore::WorldAssemblyFloatingIslandPlanV1* FindWorldAssemblyFloatingIslandStratum( + int32 StrateIndex,int32 ChunkZ) const + { + if(!WorldAssembly||StrateIndex<0)return nullptr; + const auto It=std::find_if(WorldAssembly->floatingIslands.begin(), + WorldAssembly->floatingIslands.end(),[StrateIndex,ChunkZ](const auto& Entry) + {return Entry.strateIndex==StrateIndex&&ChunkZ<=Entry.topChunkZ&&ChunkZ>=Entry.bottomChunkZ;}); + return It==WorldAssembly->floatingIslands.end()?nullptr:&*It; + } + bool NativeSurfaceDensityAt(float X,float Y,float Z,float& OutDensity) const { if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator @@ -1778,6 +1883,45 @@ private: return true; } + bool NativeFloatingIslandDensityAt(float X,float Y,float Z,float& OutDensity) const + { + if(!WorldAssembly||!Owner||!Owner->Manager||!Owner->Generator + ||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return false; + // Convert Core query Z back to the owner's legacy chunk coordinate. + // Convertir Z du Core vers la coordonnee de chunk legacy du proprietaire. + constexpr int32 CoreVoxelsPerLegacyChunk=CHUNK_SIZE/2; + const int32 ChunkZ=FMath::FloorToInt(Z/static_cast(CoreVoxelsPerLegacyChunk)); + if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ)) + !=ECaveGeneratorType::FloatingIslands)return false; + const int32 StrateIndex=Owner->Manager->GetStrateIndex( + static_cast((static_cast(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE)); + const auto* Plan=FindWorldAssemblyFloatingIslandStratum(StrateIndex,ChunkZ); + if(!Plan)return false; + constexpr double Scale=static_cast(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel); + constexpr double Maximum=1000000000000.0/Scale; + if(std::abs(static_cast(X))>Maximum||std::abs(static_cast(Y))>Maximum + ||std::abs(static_cast(Z))>Maximum)return false; + const auto ToFixed=[](float Value) + {return static_cast(std::llround(static_cast(Value) + *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));}; + const VoxelForgeCore::FloatingIslandFieldV1 Field( + Plan->manifest.recipe,Plan->manifest.plan); + if(!Field.isValid())return false; + std::int64_t Density=Field.densitySubvoxels(ToFixed(X),ToFixed(Y),ToFixed(Z)); + // A legacy 4 m chunk is 16 Core voxels at the published 12.5 cm to 25 cm scale. + // Un chunk legacy de 4 m represente 16 voxels Core avec l'echelle publiee. + const std::int64_t Bottom=static_cast(Plan->bottomChunkZ) + *CoreVoxelsPerLegacyChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const std::int64_t Top=(static_cast(Plan->topChunkZ)+1) + *CoreVoxelsPerLegacyChunk*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + const std::int64_t Seal=Plan->manifest.recipe.boundarySealSubvoxels; + const std::int64_t Zq=ToFixed(Z); + if(Zq>=Bottom&&Zq(1,Zq-Bottom)); + if(Zq<=Top&&Zq>Top-Seal)Density=std::min(Density,-std::max(1,Top-Zq)); + OutDensity=static_cast(static_cast(Density)/Scale); + return std::isfinite(OutDensity); + } + bool UsesLegacySurfaceField(float X,float Y,float Z) const { if(!Owner||!Owner->Manager||!Owner->Generator)return false; diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h index 49c13f9..ced1443 100644 --- a/Standalone/Core/src/WorldAssemblerInternal.h +++ b/Standalone/Core/src/WorldAssemblerInternal.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -403,9 +404,91 @@ namespace world_assembly_detail HashWord(hash, manifest.schemaVersion, 4); HashWord(hash, manifest.recipe.contentHash, 8); HashWord(hash, manifest.plan.contentHash, 8); + // Preserve the historical SurfaceWorld manifest digest when no island plan is attached. + if (!manifest.floatingIslands.empty()) + { + HashWord(hash, 0x464c4f415449534cull, 8); + HashWord(hash, manifest.floatingIslands.size(), 4); + for (const auto& islands : manifest.floatingIslands) + { + HashWord(hash, static_cast(islands.strateIndex), 4); + HashWord(hash, static_cast(islands.topChunkZ), 4); + HashWord(hash, static_cast(islands.bottomChunkZ), 4); + HashWord(hash, islands.manifest.contentHash, 8); + } + } return hash; } + inline void AppendJsonString(std::ostringstream& out, std::string_view value) + { + out << '"'; + for (const unsigned char ch : value) + { + switch (ch) + { + case '"': out << "\\\""; break; + case '\\': out << "\\\\"; break; + case '\b': out << "\\b"; break; + case '\f': out << "\\f"; break; + case '\n': out << "\\n"; break; + case '\r': out << "\\r"; break; + case '\t': out << "\\t"; break; + default: + if (ch < 0x20) out << "\\u00" << std::hex << std::setw(2) + << std::setfill('0') << static_cast(ch) << std::dec; + else out << static_cast(ch); + break; + } + } + out << '"'; + } + + inline void AppendJsonValue(std::ostringstream& out, const VFJson::Value& value) + { + if (std::holds_alternative(value.Data)) out << "null"; + else if (const auto* boolean = std::get_if(&value.Data)) out << (*boolean ? "true" : "false"); + else if (const auto* number = std::get_if(&value.Data)) + { + char buffer[64]; + const auto result = std::to_chars(buffer, buffer + sizeof(buffer), *number, + std::chars_format::general, std::numeric_limits::max_digits10); + if (result.ec == std::errc{}) out.write(buffer, result.ptr - buffer); + else out << '0'; + } + else if (const auto* string = std::get_if(&value.Data)) AppendJsonString(out, *string); + else if (const auto* array = std::get_if(&value.Data)) + { + out << '['; + for (std::size_t i = 0; i < array->size(); ++i) + { if (i) out << ','; AppendJsonValue(out, (*array)[i]); } + out << ']'; + } + else + { + const auto& object = std::get(value.Data); + std::vector keys; + keys.reserve(object.size()); + for (const auto& entry : object) keys.push_back(entry.first); + std::sort(keys.begin(), keys.end()); + out << '{'; + for (std::size_t i = 0; i < keys.size(); ++i) + { + if (i) out << ','; + AppendJsonString(out, keys[i]); out << ':'; + AppendJsonValue(out, object.at(std::string(keys[i]))); + } + out << '}'; + } + } + + inline std::string JsonText(const VFJson::Value& value) + { + std::ostringstream out; + AppendJsonValue(out, value); + return out.str(); + } + inline bool ParseRecipe(const VFJson::Value& value, WorldAssemblyRecipeV1& recipe, std::string& error) { @@ -566,6 +649,26 @@ namespace world_assembly_detail if (error.empty()) error = "world assembly manifest is missing a required field"; return std::nullopt; } + const VFJson::Value& islandPlans = value.Get("floating_islands"); + if (!std::holds_alternative(islandPlans.Data)) + { + if (!islandPlans.IsArray() || islandPlans.AsArray().size() > MaxStrateCount) + { error = "world assembly floating-island plans must be an array of at most sixty-four entries"; return std::nullopt; } + for (const VFJson::Value& islandValue : islandPlans.AsArray()) + { + WorldAssemblyFloatingIslandPlanV1 entry; + if (!islandValue.IsObject() + || !ReadI32(islandValue.Get("strate_index"), entry.strateIndex) + || !ReadI32(islandValue.Get("top_chunk_z"), entry.topChunkZ) + || !ReadI32(islandValue.Get("bottom_chunk_z"), entry.bottomChunkZ)) + { error = "world assembly floating-island slot is missing its resolved strate bounds"; return std::nullopt; } + const std::string nestedJson = JsonText(islandValue.Get("manifest")); + auto parsedIsland = FloatingIslandManifestV1::FromJson(nestedJson, error); + if (!parsedIsland) return std::nullopt; + entry.manifest = std::move(*parsedIsland); + manifest.floatingIslands.push_back(std::move(entry)); + } + } if (!manifest.validate(error)) return std::nullopt; return manifest; } @@ -1292,8 +1395,10 @@ bool WorldAssemblyManifest::finalize(std::string& error) if (schemaVersion != WorldAssemblySchemaVersion || plan.schemaVersion != WorldAssemblySchemaVersion || !world_assembly_detail::ValidateRecipe(recipe, error)) { if (error.empty()) error = "world assembly manifest or plan version is unsupported"; return false; } - if (plan.strates.empty() || plan.strates.size() > world_assembly_detail::MaxStrateCount) - { error = "world assembly plan must contain one to sixty-four finite strates"; return false; } + if ((plan.strates.empty() && floatingIslands.empty()) + || plan.strates.size() > world_assembly_detail::MaxStrateCount + || floatingIslands.size() > world_assembly_detail::MaxStrateCount) + { error = "world assembly must contain SurfaceWorld or floating-island plans within the sixty-four-strate limit"; return false; } std::vector strateIds; for (auto& stratum : plan.strates) { @@ -1323,6 +1428,25 @@ bool WorldAssemblyManifest::finalize(std::string& error) strateIds.push_back(stratum.strateIndex); stratum.contentHash = world_assembly_detail::HashStratum(stratum); } + std::vector islandStrateIds; + for (auto& islands : floatingIslands) + { + if (islands.strateIndex < 0 || islands.topChunkZ < islands.bottomChunkZ + || std::find(islandStrateIds.begin(), islandStrateIds.end(), islands.strateIndex) != islandStrateIds.end()) + { error = "world assembly contains invalid or duplicate floating-island strate bounds"; return false; } + if (!islands.manifest.validate(error)) return false; + if (islands.manifest.recipe.seed != recipe.seed) + { error = "floating-island seed does not match the world assembly season seed"; return false; } + constexpr std::int64_t CoreVoxelsPerLegacyChunk = 16; + const std::int64_t bottomQ = static_cast(islands.bottomChunkZ) + * CoreVoxelsPerLegacyChunk * WorldAssemblySubvoxelsPerVoxel; + const std::int64_t topQ = (static_cast(islands.topChunkZ) + 1) + * CoreVoxelsPerLegacyChunk * WorldAssemblySubvoxelsPerVoxel; + if (islands.manifest.recipe.layoutMinimumZSubvoxels < bottomQ + || islands.manifest.recipe.layoutMaximumZSubvoxels > topQ) + { error = "floating-island recipe layout escapes the resolved 12.5 cm legacy strate bounds"; return false; } + islandStrateIds.push_back(islands.strateIndex); + } recipe.contentHash = world_assembly_detail::HashRecipe(recipe); plan.contentHash = world_assembly_detail::HashPlan(plan); contentHash = world_assembly_detail::HashManifest(*this); @@ -1337,13 +1461,19 @@ bool WorldAssemblyManifest::validate(std::string& error) const const std::uint64_t expectedManifest = contentHash; std::vector expectedStrates; for (const auto& stratum : plan.strates) expectedStrates.push_back(stratum.contentHash); + std::vector expectedIslandManifests; + for (const auto& islands : floatingIslands) expectedIslandManifests.push_back(islands.manifest.contentHash); if (!copy.finalize(error)) return false; if (expectedRecipe != copy.recipe.contentHash || expectedPlan != copy.plan.contentHash - || expectedManifest != copy.contentHash || expectedStrates.size() != copy.plan.strates.size()) + || expectedManifest != copy.contentHash || expectedStrates.size() != copy.plan.strates.size() + || expectedIslandManifests.size() != copy.floatingIslands.size()) { error = "world assembly recipe, plan or manifest content hash does not match its fields"; return false; } for (std::size_t i = 0; i < expectedStrates.size(); ++i) if (expectedStrates[i] != copy.plan.strates[i].contentHash) { error = "world assembly stratum content hash does not match its fields"; return false; } + for (std::size_t i = 0; i < expectedIslandManifests.size(); ++i) + if (expectedIslandManifests[i] != copy.floatingIslands[i].manifest.contentHash) + { error = "world assembly floating-island content hash does not match its fields"; return false; } return true; } @@ -1474,7 +1604,23 @@ std::string WorldAssemblyManifest::ToJson() const } out << "]}"; } - out << "]}}"; + if (value.floatingIslands.empty()) out << "]}}"; + else + { + out << "]},\"floating_islands\":["; + for (std::size_t i = 0; i < value.floatingIslands.size(); ++i) + { + const auto& islands = value.floatingIslands[i]; + if (i) out << ','; + const std::string nested = islands.manifest.ToJson(); + if (nested.empty()) return {}; + out << "{\"strate_index\":" << islands.strateIndex + << ",\"top_chunk_z\":" << islands.topChunkZ + << ",\"bottom_chunk_z\":" << islands.bottomChunkZ + << ",\"manifest\":" << nested << '}'; + } + out << "]}"; + } return out.str(); } diff --git a/Standalone/DESIGN_WORLD_ASSEMBLER.md b/Standalone/DESIGN_WORLD_ASSEMBLER.md index 665a67e..01001ee 100644 --- a/Standalone/DESIGN_WORLD_ASSEMBLER.md +++ b/Standalone/DESIGN_WORLD_ASSEMBLER.md @@ -542,6 +542,55 @@ what the shape list alone misses: site selection, cluster connectivity, landings route. The support ranges do not promise that every roll is pretty or walkable; measurement rejects bad candidates and records which constraint failed. +#### M3 round 2 — look pass and standalone gates + +The legacy settings describe a stochastic site process, not a fixed island count: `IslandDensity=0.5` +is the chance that each hashed XY cell exists. The owner settings are spacing 112 legacy voxels, +radii 24–48 legacy voxels, thickness ratio 0.6, vertical jitter/top flatten 0.6, roughness 3, +blend 6 and boundary seal 4. The production legacy field uses 12.5 cm voxels and Core uses 25 cm, so +the round-2 Core recipe halves length-valued settings: 56 spacing, 12–24 radii, 1.5 roughness, 3 +blend, 2 seal and 6.3 contour warp. Those values preserve 14 m spacing and 3–6 m radii under the +production source scale. The legacy struct's radius comment instead assumes 25 cm, and the frozen +probe metadata reports 25 cm while production `Source` and the plugin `CLAUDE.md` report 12.5 cm. +The next round must reconcile that fixture metadata before publishing the importer contract. + +Geometry version 2 adds a broad crust and 3–4 deterministic tapering root lobes per island. Its +measured total depth is 0.75 of island diameter; the old thin profile control measures 0.30 and is +rejected by the 0.70 thickness gate. Full bounded roughness remains on cliffs and roots, while top +detail is reduced where the route needs stable footing. The three primary islands guarantee an +entrance-to-exit route; density 0.5 selected one optional side island for this seed, giving four +islands and three sharp-tag-ready arched spans. The side island is not part of the route. + +[`M3 Round 2 report`](Research/WorldAssemblerM3/Round2/REPORT.md) records the five matched views, +owner-parameter conversion, the full run ledger, and Round 3 delivery plan. The contact sheet is +[`contact_sheet.jpg`](Research/WorldAssemblerM3/Round2/results/captures/contact_sheet.jpg). Bounds +had zero misses, the route passed 254 capsule centers and 69,088 field samples, the raised-route and +thin-pad controls failed as intended, and W1/W8/W16 field hashes matched. On the same 32,768 physical +query locations, Core took 38.06 ms versus 64.64 ms for the legacy evaluator. The local sample still +contains 10.6× as many native solid points (1,380 vs 130), due to the guaranteed route backbone and +deeper root profile; this is a known local density difference, not an occupancy-parity claim. + +#### M3 round 3 — Unreal delivery plan + +1. Resolve the legacy/Core voxel-scale conflict using the production field constant and the owner + snapshot producer; record a single conversion rule in the imported manifest and its preview notes. +2. Add the floating-island recipe and plan to the existing optional `world_assembly` import pattern. + Parse through `ResolvedWorldSnapshot::FromJson`, validate recipe/plan hashes and stable feature + IDs, and reject malformed or scale-mismatched payloads before generation. +3. Prove the imported plan in a null-RHI BuildHost commandlet. Check the canonical hashes and W1/W8/W16 + queries, then compare generated tiles against the reviewed standalone field without changing the + existing Core world or SurfaceWorld hashes. +4. Carry island and sharp-tag-ready span IDs into the feature-aware mesher path. Audit same-LOD and + LOD0/LOD1 borders around the crust, roots and arch landings. +5. Cook the rendered collision, trace it on the Pawn channel, and capsule-sweep the same serialized + route. Compare collision support points to analytic Core support at the island tops and spans. +6. Hand the owner the manifest, five-view sheet, import steps, route coordinates, and collision + results. Use a duplicated test settings asset for preview only after the owner authorizes project + asset work; keep shipped assets untouched during the standalone proof. + +Water remains a separate fixed-season body and is not part of the round-2 field or the round-3 +collision claim. + ## E. Milestones that end in owner-visible artifacts These estimates are engineering rounds, not elapsed-time promises. Each milestone ends with a @@ -551,7 +600,7 @@ standalone viewer/contact sheet, data report or playable inspection the owner ca |---|---:|---|---| | M1 — sharp-mesher decision spike | 2 rounds | 25 cm contact sheet of corner, one-metre step, cliff ledge and doorway: current MC beside feature-aware output; include LOD/tile seam and collision overlays | Measure surface error, normal splits, determinism and seam behavior before building architectural recipes on top | | M2 — SurfaceWorld native vertical slice | 3 rounds | Matched-camera native surface with two biomes, a coast/beach, cliff/overhang, cap, route markers and timing beside the exact legacy bridge | No missed bound; the matched full-view workload materially beats the 23.085 s bridge; local collision/query readiness is reported separately; route candidate survives final composition | -| M3 — FloatingIslands volumetric proof | 2 rounds | Walkable island cluster with underside/cliff views, water/body overlay, tile/LOD seam views and bound counters | Stable field/feature IDs; no false interval proof; route support is visible and capsule-swept | +| M3 — FloatingIslands volumetric proof and Unreal delivery | 3 rounds | Walkable island cluster with underside/cliff views, tile/LOD seam views, imported manifest, BuildHost proof and cooked route collision | Stable field/feature IDs; no false interval proof; route support is visible and capsule-swept | | M4 — route, edit and water law | 2 rounds | Owner can inspect before/after edit views and traverse the tested route; report shows protect-or-replace transaction and unchanged seasonal water IDs | Final-field capsule clearance/support passes; edit either preserves route or atomically installs a replacement; water remains fixed | M1 goes first because the owner needs to judge crisp geometry before a slow legacy/native comparison diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md index 5155e60..09f57eb 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md @@ -1063,3 +1063,8 @@ match; the M2 mesher hash matrix was not replayed. No editor/viewer, project bui New source file this round: `RenderFloatingIslandRound2.py`. — Luna +## World assembler M3 round 3 — 2026-10-10 + +The owner's scale is settled at 12.5 cm per legacy voxel and 25 cm per Core voxel (owner-to-Core length ratio 0.5). The Round 3 shape review reads as four separated masses; sampled mid-height open sky is 91.89% of the XY bounding box. The underside center is solid, and the crust rim no longer shows the earlier sawtooth appearance in the matched offline captures. + +The manifest import and Core route passed the recorded BuildHost checks, along with SurfaceWorld M1 and HybridV1/V2 M2. Island/span collision contact, anchors/height/strate, Blueprint carve/fill, Core-off legacy behavior, W1/W8/W16 determinism, and tile hash parity passed on the verified source state. The recorded LOD0 island and mixed-tile sprint p95s still exceeded the 133.333 ms deadline by about 24 ms and 20 ms. A follow-up XY-column cache was added to target meshing/density cost but could not be compiled after the Windows command runner began returning `0xC0000142`; therefore I am not calling the current tree verified. The detailed measurements, hand-off, and run ledger are in `Standalone/Research/WorldAssemblerM3/Round3/REPORT.md`. diff --git a/Standalone/Research/WorldAssemblerM3/Round3/BuildFloatingIslandsAudit.ps1 b/Standalone/Research/WorldAssemblerM3/Round3/BuildFloatingIslandsAudit.ps1 new file mode 100644 index 0000000..820bcd3 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round3/BuildFloatingIslandsAudit.ps1 @@ -0,0 +1,118 @@ +[CmdletBinding()] +param( + [string]$ProbePath = (Join-Path $PSScriptRoot '..\..\OwnerShapeWorld\core_strate_coverage\probes\type_6_FloatingIslands.json'), + [string]$OutputDirectory = (Join-Path $PSScriptRoot 'results') +) + +$ErrorActionPreference = 'Stop' +$ScriptProcess = Get-Process -Id $PID +$ScriptProcess.PriorityClass = 'BelowNormal' +$StandaloneRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..')) +$BuildRoot = Join-Path $StandaloneRoot 'Build\VoxelForgeCore' +$CoreLibrary = Join-Path $BuildRoot 'VoxelForgeCore.lib' +$AuditSource = Join-Path $PSScriptRoot 'FloatingIslandsAudit.cpp' +$AuditExe = Join-Path $BuildRoot 'FloatingIslandsAudit.exe' +$ProbePath = [IO.Path]::GetFullPath($ProbePath) +$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) +$VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat' +if (-not (Test-Path -LiteralPath $CoreLibrary -PathType Leaf)) { throw "Core library is missing: $CoreLibrary" } +if (-not (Test-Path -LiteralPath $ProbePath -PathType Leaf)) { throw "Legacy probe is missing: $ProbePath" } +if (-not (Test-Path -LiteralPath $VcVars -PathType Leaf)) { throw "MSVC environment script is missing: $VcVars" } +[IO.Directory]::CreateDirectory($OutputDirectory) | Out-Null + +$IncludeArgs = @( + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Core\include')), + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Core\src')), + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Compat')), + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Source')), + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Source\VoxelForge\Public')), + ('/I"{0}"' -f (Join-Path $StandaloneRoot 'Source\VoxelForge\Private')) +) +$LegacyObjectNames = @( + 'UnrealPerlin.obj','StandaloneDataAssetGlue.obj','VoxelBiomeDefinition.obj','VoxelCaveMorphology.obj', + 'VoxelDensityAblation.obj','VoxelDensityOpStack.obj','VoxelDensityProfile.obj','VoxelDiffLayer.obj', + 'VoxelGenerator.obj','VoxelHeightOpStack.obj','VoxelMarchingCubesMesher.obj','VoxelSettings.obj', + 'VoxelStrateComposer.obj','VoxelStrateManager.obj','VoxelStrateMeasure.obj','VoxelTerrainOpDefinition.obj', + 'VoxelWormField.obj','GpuFarViewerLinkStubs.obj' +) +foreach ($ObjectName in $LegacyObjectNames) { + if (-not (Test-Path -LiteralPath (Join-Path $BuildRoot $ObjectName) -PathType Leaf)) { + throw "Required legacy bridge object is missing: $ObjectName; run Standalone/Build.ps1 -Target All first." + } +} + +$ResponsePath = Join-Path $BuildRoot 'floating-islands-audit.rsp' +$ResponseLines = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc','/D_CRT_SECURE_NO_WARNINGS') + + $IncludeArgs + @(('/Fe"{0}"' -f $AuditExe),('"{0}"' -f $AuditSource)) + + @($LegacyObjectNames | ForEach-Object { '"{0}"' -f $_ }) + @( + ('"{0}"' -f $CoreLibrary),'/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib' + ) +[IO.File]::WriteAllLines($ResponsePath,$ResponseLines,[Text.UTF8Encoding]::new($false)) +Push-Location $BuildRoot +try { + $CompileCommand = 'call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$ResponsePath + & $env:ComSpec /d /s /c $CompileCommand + if ($LASTEXITCODE -eq -1073741502) { throw 'Audit build exited with 0xC0000142; retry once using this same wrapper.' } + if ($LASTEXITCODE -ne 0) { throw "Floating-island audit build failed with exit code $LASTEXITCODE" } + if (-not (Test-Path -LiteralPath $AuditExe -PathType Leaf)) { throw "Audit executable was not produced: $AuditExe" } +} finally { Pop-Location } + +$RunLedger = Join-Path $OutputDirectory 'run_ledger.txt' +$RunRecords = [Collections.Generic.List[string]]::new() +$Succeeded = $false +for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) { + $StartInfo = [Diagnostics.ProcessStartInfo]::new() + $StartInfo.FileName = $AuditExe + $StartInfo.Arguments = '"{0}" "{1}"' -f $ProbePath.Replace('"','\"'),$OutputDirectory.Replace('"','\"') + $StartInfo.WorkingDirectory = $BuildRoot + $StartInfo.UseShellExecute = $false + $StartInfo.CreateNoWindow = $true + $StartInfo.RedirectStandardOutput = $true + $StartInfo.RedirectStandardError = $true + $Process = [Diagnostics.Process]::new() + $Process.StartInfo = $StartInfo + [void]$Process.Start() + try { $Process.PriorityClass = 'BelowNormal' } catch { } + $StdoutTask = $Process.StandardOutput.ReadToEndAsync() + $StderrTask = $Process.StandardError.ReadToEndAsync() + $Clock = [Diagnostics.Stopwatch]::StartNew() + [int64]$PeakPrivateBytes = 0 + $LimitFailure = $null + while (-not $Process.WaitForExit(250)) { + $Process.Refresh() + $PeakPrivateBytes = [Math]::Max($PeakPrivateBytes,$Process.PrivateMemorySize64) + if ($PeakPrivateBytes -gt 8GB) { + $LimitFailure = 'process private bytes exceeded 8 GiB' + $Process.Kill() + break + } + if ($Clock.Elapsed.TotalMinutes -ge 15.0) { + $LimitFailure = 'audit test exceeded the 15-minute time box' + $Process.Kill() + break + } + } + $Process.WaitForExit() + $Clock.Stop() + $Process.Refresh() + $PeakPrivateBytes = [Math]::Max($PeakPrivateBytes,$Process.PrivateMemorySize64) + $Stdout = $StdoutTask.GetAwaiter().GetResult() + $Stderr = $StderrTask.GetAwaiter().GetResult() + if ($Stdout) { Write-Output $Stdout.TrimEnd() } + if ($Stderr) { [Console]::Error.WriteLine($Stderr.TrimEnd()) } + $ExitCode = $Process.ExitCode + $Status = if ($LimitFailure) { $LimitFailure } elseif ($ExitCode -eq 0) { 'passed' } else { "failed exit=$ExitCode" } + $RunRecords.Add("attempt=$Attempt status=$Status elapsed_s=$([Math]::Round($Clock.Elapsed.TotalSeconds,2)) peak_private_MiB=$([Math]::Round($PeakPrivateBytes/1MB,1)) exit=$ExitCode") + if ($LimitFailure) { break } + if ($ExitCode -eq 0) { $Succeeded = $true; break } + if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) { + $RunRecords.Add('retry_reason=0xC0000142 single permitted retry') + continue + } + break +} +[IO.File]::WriteAllLines($RunLedger,$RunRecords,[Text.UTF8Encoding]::new($false)) +Get-Content -LiteralPath $RunLedger | Write-Output +if (-not $Succeeded) { throw "Floating-island audit run failed or exceeded a process limit. See $RunLedger" } +Write-Output "audit_executable=$AuditExe" +Write-Output "audit_results=$OutputDirectory" diff --git a/Standalone/Research/WorldAssemblerM3/Round3/FloatingIslandsAudit.cpp b/Standalone/Research/WorldAssemblerM3/Round3/FloatingIslandsAudit.cpp new file mode 100644 index 0000000..a317713 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round3/FloatingIslandsAudit.cpp @@ -0,0 +1,923 @@ +#include "CoreMinimal.h" +#include "OwnerWorldLoader.h" +#include "VoxelForgeCore/VoxelForgeCore.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifdef TEXT +#undef TEXT +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#ifdef TEXT +#undef TEXT +#endif +#endif + +namespace +{ + using namespace VoxelForgeCore; + constexpr std::int64_t Q = WorldAssemblySubvoxelsPerVoxel; + using Clock = std::chrono::steady_clock; + using Point = std::array; + + struct OracleAnchor + { + Point surfaceQ{}; + std::uint32_t supportScore = 0; + }; + + struct SampleStats + { + std::string hash; + double legacyMilliseconds = 0.0; + double nativeMilliseconds = 0.0; + std::uint64_t signMismatches = 0; + std::uint64_t legacySolidSamples = 0; + std::uint64_t nativeSolidSamples = 0; + std::uint64_t legacyOnlySolidSamples = 0; + std::uint64_t nativeOnlySolidSamples = 0; + double legacyChecksum = 0.0; + double nativeChecksum = 0.0; + }; + + struct Triangle + { + std::array values{}; // Three xyz vertices followed by one flat normal. + }; + + struct MeshResult + { + std::uint64_t gridSamples = 0; + std::uint64_t triangles = 0; + std::uint64_t bytes = 0; + }; + + struct ExistingHashes + { + std::string round1Manifest; + std::string surfaceWorldRound4Manifest; + }; + + struct OpenSkyAudit + { + std::array minimumXY{}; + std::array maximumXY{}; + std::int64_t midHeight = 0; + std::uint64_t openSamples = 0; + std::uint64_t totalSamples = 0; + }; + + std::uint64_t Mix(std::uint64_t value) + { + value += 0x9e3779b97f4a7c15ull; + value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ull; + value = (value ^ (value >> 27)) * 0x94d049bb133111ebull; + return value ^ (value >> 31); + } + + std::uint64_t Next(std::uint64_t& state) + { + state += 0x9e3779b97f4a7c15ull; + return Mix(state); + } + + std::int64_t VoxQ(double value) + { return static_cast(std::llround(value * static_cast(Q))); } + + std::string Hex64(std::uint64_t value) + { + std::ostringstream output; + output << std::hex << std::setfill('0') << std::setw(16) << value; + return output.str(); + } + + std::string HashValues(const std::vector& values) + { + std::uint64_t hash = 14695981039346656037ull; + for (const auto value : values) + for (int byte = 0; byte < 8; ++byte) + { + hash ^= static_cast(static_cast(value) >> (byte * 8)); + hash *= 1099511628211ull; + } + return Hex64(hash); + } + + ExistingHashes VerifyExistingManifests(const std::filesystem::path& standaloneRoot) + { + const auto read = [](const std::filesystem::path& path) + { + std::ifstream input(path, std::ios::binary); + if (!input) throw std::runtime_error("cannot read existing manifest: " + path.string()); + return std::string(std::istreambuf_iterator(input), std::istreambuf_iterator()); + }; + ExistingHashes hashes; + std::string error; + const auto round1 = WorldAssemblyManifest::FromJson(read( + standaloneRoot / "Research/WorldAssemblerM1/round1-manifest.json"), error); + if (!round1 || !round1->validate(error) + || Hex64(round1->contentHash) != "c87ae279b0e26f53") + throw std::runtime_error("existing M1 manifest hash changed or failed validation: " + error); + hashes.round1Manifest = Hex64(round1->contentHash); + error.clear(); + const auto surface = WorldAssemblyManifest::FromJson(read( + standaloneRoot / "Research/WorldAssemblerM1/round4/world_assembly.json"), error); + if (!surface || !surface->validate(error) + || Hex64(surface->contentHash) != "8933693d9560ba9f") + throw std::runtime_error("existing SurfaceWorld round-4 hash changed or failed validation: " + error); + hashes.surfaceWorldRound4Manifest = Hex64(surface->contentHash); + return hashes; + } + + float LegacyDensity(const FStandaloneOwnerWorld& owner, const Point& q) + { + constexpr double CoreToLegacyVoxelScale = 2.0; + return owner.Generator->GetDensityAt( + static_cast(CoreToLegacyVoxelScale * q[0] / static_cast(Q)), + static_cast(CoreToLegacyVoxelScale * q[1] / static_cast(Q)), + static_cast(CoreToLegacyVoxelScale * q[2] / static_cast(Q))); + } + + OracleAnchor FindLegacyAnchor(const FStandaloneOwnerWorld& owner) + { + OracleAnchor best; + const float highZ = -560.0f; + const float lowZ = -2480.0f; + constexpr float ScanStep = 8.0f; + for (int y = -448; y <= 448; y += 16) + for (int x = -448; x <= 448; x += 16) + { + float airZ = highZ; + float above = owner.Generator->GetDensityAt(static_cast(x), + static_cast(y), airZ); + if (above < 0.0f) continue; + for (float solidZ = highZ - ScanStep; solidZ >= lowZ; solidZ -= ScanStep) + { + const float density = owner.Generator->GetDensityAt(static_cast(x), + static_cast(y), solidZ); + if (density < 0.0f) + { + float outside = airZ, inside = solidZ; + for (int iteration = 0; iteration < 12; ++iteration) + { + const float middle = (outside + inside) * 0.5f; + if (owner.Generator->GetDensityAt(static_cast(x), + static_cast(y), middle) < 0.0f) inside = middle; + else outside = middle; + } + const std::int64_t topQ = VoxQ(outside); + std::uint32_t score = 0; + for (int dy = -16; dy <= 16; dy += 8) + for (int dx = -16; dx <= 16; dx += 8) + if (owner.Generator->GetDensityAt(static_cast(x + dx), + static_cast(y + dy), + static_cast((topQ - 4 * Q) / static_cast(Q))) < 0.0f) + ++score; + if (score > best.supportScore + || (score == best.supportScore + && std::abs(x) + std::abs(y) + < std::abs(best.surfaceQ[0] / Q) + std::abs(best.surfaceQ[1] / Q))) + { + best.surfaceQ = {static_cast(x) * Q, + static_cast(y) * Q, topQ}; + best.supportScore = score; + } + break; // The first air-to-solid crossing is the island top. + } + airZ = solidZ; + } + } + return best; + } + + std::vector MakeSamples(const FloatingIslandInfluenceBounds& bounds, + std::size_t count, std::uint64_t seed) + { + std::vector points(count); + std::uint64_t state = seed; + for (auto& point : points) + for (std::size_t axis = 0; axis < 3; ++axis) + { + const std::uint64_t span = static_cast( + bounds.maximumSubvoxels[axis] - bounds.minimumSubvoxels[axis] + 1); + point[axis] = bounds.minimumSubvoxels[axis] + + static_cast(Next(state) % std::max(span, 1)); + } + return points; + } + + OpenSkyAudit MeasureOpenSky(const FloatingIslandFieldV1& field) + { + OpenSkyAudit result; + const FloatingIslandInfluenceBounds bounds = field.influenceBounds(); + if (!bounds.finite) throw std::runtime_error("floating-island cluster has no finite bounds"); + result.minimumXY = {bounds.minimumSubvoxels[0], bounds.minimumSubvoxels[1]}; + result.maximumXY = {bounds.maximumSubvoxels[0], bounds.maximumSubvoxels[1]}; + result.midHeight = (bounds.minimumSubvoxels[2] + bounds.maximumSubvoxels[2]) / 2; + constexpr std::int64_t SampleStep = 4 * Q; + for (std::int64_t y = result.minimumXY[1]; y <= result.maximumXY[1]; y += SampleStep) + for (std::int64_t x = result.minimumXY[0]; x <= result.maximumXY[0]; x += SampleStep) + { + if (field.densitySubvoxels(x, y, result.midHeight) >= 0) ++result.openSamples; + ++result.totalSamples; + } + return result; + } + + void WriteIslandImportManifest(const std::filesystem::path& standaloneRoot, + const FloatingIslandManifestV1& islands, const FStrateSlot& slot, + const std::filesystem::path& outputDir) + { + const auto parentPath = standaloneRoot / "Research/WorldAssemblerM1/round4/world_assembly.json"; + std::ifstream parentInput(parentPath, std::ios::binary); + if (!parentInput) throw std::runtime_error("cannot read seed-matched parent world assembly recipe"); + const std::string parentText{std::istreambuf_iterator(parentInput), + std::istreambuf_iterator()}; + std::string error; + auto parsedParent = WorldAssemblyManifest::FromJson(parentText, error); + if (!parsedParent) throw std::runtime_error("cannot parse parent world assembly recipe: " + error); + parsedParent->plan.strates.clear(); + parsedParent->floatingIslands.clear(); + WorldAssemblyFloatingIslandPlanV1 entry; + entry.strateIndex = slot.StrateIndex; + entry.topChunkZ = slot.TopChunkZ; + entry.bottomChunkZ = slot.BottomChunkZ; + entry.manifest = islands; + parsedParent->floatingIslands.push_back(std::move(entry)); + if (!parsedParent->finalize(error)) + throw std::runtime_error("combined island import manifest is invalid: " + error); + const std::string canonical = parsedParent->ToJson(); + const auto roundTrip = WorldAssemblyManifest::FromJson(canonical, error); + if (canonical.empty() || !roundTrip || roundTrip->ToJson() != canonical) + throw std::runtime_error("combined island import manifest failed canonical round-trip: " + error); + std::ofstream nested(outputDir / "floating_island_manifest.json", std::ios::trunc); + nested << islands.ToJson() << '\n'; + if (!nested) throw std::runtime_error("cannot write nested floating-island manifest"); + std::ofstream combined(outputDir / "world_assembly_floating_islands.json", std::ios::trunc); + combined << canonical << '\n'; + if (!combined) throw std::runtime_error("cannot write combined floating-island import manifest"); + } + + SampleStats HashWithWorkers(const FloatingIslandFieldV1& field, + const FStandaloneOwnerWorld& owner, const std::vector& points, + std::uint32_t workers, bool measure) + { + std::vector values(points.size()); + std::vector threads; + threads.reserve(workers); + const auto evaluate = [&](std::uint32_t worker) + { + for (std::size_t i = worker; i < points.size(); i += workers) + values[i] = field.densitySubvoxels(points[i][0], points[i][1], points[i][2]); + }; + for (std::uint32_t worker = 0; worker < workers; ++worker) + threads.emplace_back(evaluate, worker); + for (auto& thread : threads) thread.join(); + + SampleStats stats; + stats.hash = HashValues(values); + if (!measure) return stats; + std::vector legacyValues(points.size()); + const auto beginLegacy = Clock::now(); + double legacyChecksum = 0.0; + for (std::size_t i = 0; i < points.size(); ++i) + { + const float value = LegacyDensity(owner, points[i]); + legacyValues[i] = value; + legacyChecksum += value; + } + const auto endLegacy = Clock::now(); + const auto beginNative = Clock::now(); + double nativeChecksum = 0.0; + for (const Point& point : points) + nativeChecksum += static_cast(field.densitySubvoxels( + point[0], point[1], point[2])) / static_cast(Q); + const auto endNative = Clock::now(); + for (std::size_t i = 0; i < values.size(); ++i) + { + const bool legacySolid = legacyValues[i] < 0.0f; + const bool nativeSolid = values[i] < 0; + if (legacySolid) ++stats.legacySolidSamples; + if (nativeSolid) ++stats.nativeSolidSamples; + if (legacySolid && !nativeSolid) ++stats.legacyOnlySolidSamples; + if (nativeSolid && !legacySolid) ++stats.nativeOnlySolidSamples; + if (legacySolid != nativeSolid) ++stats.signMismatches; + } + stats.legacyMilliseconds = std::chrono::duration(endLegacy - beginLegacy).count(); + stats.nativeMilliseconds = std::chrono::duration(endNative - beginNative).count(); + stats.legacyChecksum = legacyChecksum; + stats.nativeChecksum = nativeChecksum; + return stats; + } + + using D3 = std::array; + D3 Add(D3 a, D3 b) { return {a[0] + b[0], a[1] + b[1], a[2] + b[2]}; } + D3 Sub(D3 a, D3 b) { return {a[0] - b[0], a[1] - b[1], a[2] - b[2]}; } + D3 Mul(D3 a, double s) { return {a[0] * s, a[1] * s, a[2] * s}; } + double Dot(D3 a, D3 b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } + D3 Cross(D3 a, D3 b) + { return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]}; } + double Length(D3 a) { return std::sqrt(Dot(a, a)); } + D3 Normalize(D3 a) + { + const double length = Length(a); + return length > 1e-12 ? Mul(a, 1.0 / length) : D3{0.0, 0.0, 1.0}; + } + + double SampleNative(const FloatingIslandFieldV1& field, const Point& q) + { return static_cast(field.densitySubvoxels(q[0], q[1], q[2])) / static_cast(Q); } + + double SampleLegacy(const FStandaloneOwnerWorld& owner, const Point& q) + { return static_cast(LegacyDensity(owner, q)); } + + double TetGradient(const std::array& points, const std::array& values, D3& output) + { + const D3 a = Sub(points[1], points[0]); + const D3 b = Sub(points[2], points[0]); + const D3 c = Sub(points[3], points[0]); + const D3 bc = Cross(b, c), ca = Cross(c, a), ab = Cross(a, b); + const double determinant = Dot(a, bc); + if (std::abs(determinant) < 1e-12) { output = {0.0, 0.0, 1.0}; return 0.0; } + output = Mul(Add(Add(Mul(bc, values[1] - values[0]), + Mul(ca, values[2] - values[0])), Mul(ab, values[3] - values[0])), 1.0 / determinant); + return Length(output); + } + + MeshResult ExtractMesh(const std::filesystem::path& outputPath, + Point minimumQ, Point maximumQ, std::int64_t stepQ, + const std::function& density) + { + const std::size_t nx = static_cast((maximumQ[0] - minimumQ[0]) / stepQ + 1); + const std::size_t ny = static_cast((maximumQ[1] - minimumQ[1]) / stepQ + 1); + const std::size_t nz = static_cast((maximumQ[2] - minimumQ[2]) / stepQ + 1); + const std::size_t gridCount = nx * ny * nz; + if (gridCount > 4'000'000) throw std::runtime_error("mesh grid exceeds the 4M-sample cap"); + std::vector grid(gridCount); + const auto gridIndex = [=](std::size_t x, std::size_t y, std::size_t z) + { return (z * ny + y) * nx + x; }; + for (std::size_t z = 0; z < nz; ++z) + for (std::size_t y = 0; y < ny; ++y) + for (std::size_t x = 0; x < nx; ++x) + { + const Point q{minimumQ[0] + static_cast(x) * stepQ, + minimumQ[1] + static_cast(y) * stepQ, + minimumQ[2] + static_cast(z) * stepQ}; + grid[gridIndex(x, y, z)] = static_cast(density(q)); + } + + constexpr std::array, 8> Corner = {{{0,0,0},{1,0,0},{1,1,0},{0,1,0}, + {0,0,1},{1,0,1},{1,1,1},{0,1,1}}}; + constexpr std::array, 6> Tets = {{{0,5,1,6},{0,1,2,6},{0,2,3,6}, + {0,3,7,6},{0,7,4,6},{0,4,5,6}}}; + constexpr std::array, 6> TetEdges = {{{0,1},{0,2},{0,3},{1,2},{1,3},{2,3}}}; + std::vector triangles; + triangles.reserve(8192); + for (std::size_t z = 0; z + 1 < nz; ++z) + for (std::size_t y = 0; y + 1 < ny; ++y) + for (std::size_t x = 0; x + 1 < nx; ++x) + { + std::array cubePoints{}; + std::array cubeValues{}; + double low = std::numeric_limits::infinity(); + double high = -std::numeric_limits::infinity(); + for (std::size_t corner = 0; corner < Corner.size(); ++corner) + { + const auto& offset = Corner[corner]; + cubePoints[corner] = { + static_cast(minimumQ[0] + static_cast(x + offset[0]) * stepQ) / Q, + static_cast(minimumQ[1] + static_cast(y + offset[1]) * stepQ) / Q, + static_cast(minimumQ[2] + static_cast(z + offset[2]) * stepQ) / Q}; + cubeValues[corner] = grid[gridIndex(x + offset[0], y + offset[1], z + offset[2])]; + low = std::min(low, cubeValues[corner]); + high = std::max(high, cubeValues[corner]); + } + if (low >= 0.0 || high < 0.0) continue; + + for (const auto& tet : Tets) + { + std::array points{}; + std::array values{}; + for (std::size_t i = 0; i < 4; ++i) + { points[i] = cubePoints[tet[i]]; values[i] = cubeValues[tet[i]]; } + std::array polygon{}; + std::size_t polygonCount = 0; + for (const auto& edge : TetEdges) + { + const int ia = edge[0], ib = edge[1]; + if ((values[ia] < 0.0) == (values[ib] < 0.0)) continue; + const double t = values[ia] / (values[ia] - values[ib]); + const D3 point = Add(points[ia], Mul(Sub(points[ib], points[ia]), t)); + bool duplicate = false; + for (std::size_t i = 0; i < polygonCount; ++i) + if (Length(Sub(polygon[i], point)) < 1e-7) { duplicate = true; break; } + if (!duplicate && polygonCount < polygon.size()) polygon[polygonCount++] = point; + } + if (polygonCount < 3) continue; + D3 normal{}; + TetGradient(points, values, normal); + normal = Normalize(normal); + D3 center{}; + for (std::size_t i = 0; i < polygonCount; ++i) center = Add(center, polygon[i]); + center = Mul(center, 1.0 / polygonCount); + const D3 axis = std::abs(normal[2]) < 0.85 ? D3{0,0,1} : D3{0,1,0}; + const D3 right = Normalize(Cross(axis, normal)); + const D3 up = Normalize(Cross(normal, right)); + std::array, 4> ordered{}; + for (std::size_t i = 0; i < polygonCount; ++i) + { + const D3 delta = Sub(polygon[i], center); + ordered[i] = {std::atan2(Dot(delta, up), Dot(delta, right)), polygon[i]}; + } + std::sort(ordered.begin(), ordered.begin() + polygonCount, + [](const auto& a, const auto& b) { return a.first < b.first; }); + for (std::size_t i = 1; i + 1 < polygonCount; ++i) + { + D3 a = ordered[0].second, b = ordered[i].second, c = ordered[i + 1].second; + if (Dot(Cross(Sub(b, a), Sub(c, a)), normal) < 0.0) std::swap(b, c); + Triangle triangle; + for (std::size_t axisIndex = 0; axisIndex < 3; ++axisIndex) + { + triangle.values[axisIndex] = static_cast(a[axisIndex]); + triangle.values[3 + axisIndex] = static_cast(b[axisIndex]); + triangle.values[6 + axisIndex] = static_cast(c[axisIndex]); + triangle.values[9 + axisIndex] = static_cast(normal[axisIndex]); + } + triangles.push_back(triangle); + } + } + } + + std::ofstream output(outputPath, std::ios::binary | std::ios::trunc); + if (!output) throw std::runtime_error("cannot create mesh capture: " + outputPath.string()); + const std::uint32_t count = static_cast(triangles.size()); + output.write("VFMI", 4); + output.write(reinterpret_cast(&count), sizeof(count)); + for (const auto& triangle : triangles) + output.write(reinterpret_cast(triangle.values.data()), + static_cast(sizeof(triangle.values))); + output.close(); + MeshResult result; + result.gridSamples = gridCount; + result.triangles = triangles.size(); + result.bytes = std::filesystem::file_size(outputPath); + return result; + } + + void WriteJson(const std::filesystem::path& path, const FloatingIslandManifestV1& manifest, + const FloatingIslandBoundsAuditResult& bounds, const FloatingIslandRouteAuditResult& route, + const FloatingIslandRouteAuditResult& raised, const SampleStats& sample, + const FloatingIslandThicknessAuditResult& thickness, + const FloatingIslandThicknessAuditResult& thinThickness, + const OracleAnchor& anchor, const std::array& meshes, + const std::array& fieldHashes, const ExistingHashes& oldHashes, + const OpenSkyAudit& openSky) + { + const Point ownerAnchorQ = anchor.surfaceQ; + const Point coreAnchorQ{ownerAnchorQ[0] / 2, ownerAnchorQ[1] / 2, ownerAnchorQ[2] / 2}; + std::ofstream out(path, std::ios::trunc); + if (!out) throw std::runtime_error("cannot create audit result JSON"); + out << std::setprecision(12); + out << "{\n \"recipe_hash\":\"" << Hex64(manifest.recipe.contentHash) + << "\",\n \"plan_hash\":\"" << Hex64(manifest.plan.contentHash) + << "\",\n \"manifest_hash\":\"" << Hex64(manifest.contentHash) + << "\",\n \"unchanged_existing_hashes\":{\"m1_round1_manifest\":\"" + << oldHashes.round1Manifest << "\",\"surfaceworld_round4_manifest\":\"" + << oldHashes.surfaceWorldRound4Manifest << "\"}" + << ",\n \"geometry_version\":" << manifest.recipe.geometryVersion + << ",\n \"owner_voxel_size_cm\":12.5,\n \"core_voxel_size_cm\":25" + << ",\n \"owner_to_core_length_scale\":0.5" + << ",\n \"plan_islands\":" << manifest.plan.islands.size() + << ",\n \"mid_height_open_sky\":{\"sampling_step_m\":1.0,\"z_core_voxels\":" + << openSky.midHeight / static_cast(Q) + << ",\"bounds_xy_core_voxels\":[" + << openSky.minimumXY[0] / static_cast(Q) << ',' + << openSky.minimumXY[1] / static_cast(Q) << ',' + << openSky.maximumXY[0] / static_cast(Q) << ',' + << openSky.maximumXY[1] / static_cast(Q) + << "],\"open_samples\":" << openSky.openSamples + << ",\"total_samples\":" << openSky.totalSamples + << ",\"open_fraction\":" << (openSky.totalSamples + ? static_cast(openSky.openSamples) / openSky.totalSamples : 0.0) << "}" + << ",\n \"plan_connectors\":" << manifest.plan.clusterEdges.size() + << ",\n \"route_points\":" << manifest.plan.route.size() + << ",\n \"legacy_anchor_owner_voxels\":[" << ownerAnchorQ[0] / static_cast(Q) + << ',' << ownerAnchorQ[1] / static_cast(Q) << ',' + << ownerAnchorQ[2] / static_cast(Q) << ']' + << ",\n \"comparison_anchor_core_voxels\":[" << coreAnchorQ[0] / static_cast(Q) + << ',' << coreAnchorQ[1] / static_cast(Q) << ',' + << coreAnchorQ[2] / static_cast(Q) << "],\n \"legacy_anchor_plateau_samples\":" + << anchor.supportScore << ",\n \"recipe_parameters\":{\"seed\":" << manifest.recipe.seed + << ",\"primary_island_count\":" << manifest.recipe.primaryIslandCount + << ",\"density_q16\":" << manifest.recipe.islandDensityQ16 + << ",\"spacing_voxels\":" << manifest.recipe.spacingSubvoxels / static_cast(Q) + << ",\"radius_voxels\":[" << manifest.recipe.minimumRadiusSubvoxels / static_cast(Q) + << ',' << manifest.recipe.maximumRadiusSubvoxels / static_cast(Q) + << "],\"thickness_ratio_q16\":" << manifest.recipe.thicknessRatioQ16 + << ",\"vertical_jitter_q16\":" << manifest.recipe.verticalJitterQ16 + << ",\"top_flatten_q16\":" << manifest.recipe.topFlattenQ16 + << ",\"contour_warp_voxels\":" << manifest.recipe.contourWarpSubvoxels / static_cast(Q) + << ",\"crust_depth_bonus_q16\":" << manifest.recipe.crustDepthBonusQ16 + << ",\"root_depth_ratio_q16\":" << manifest.recipe.rootDepthRatioQ16 + << ",\"roughness_voxels\":" << manifest.recipe.roughnessSubvoxels / static_cast(Q) + << ",\"blend_voxels\":" << manifest.recipe.blendRadiusSubvoxels / static_cast(Q) + << ",\"bridge_half_width_voxels\":" << manifest.recipe.bridgeHalfWidthSubvoxels / static_cast(Q) + << ",\"bridge_thickness_voxels\":" << manifest.recipe.bridgeThicknessSubvoxels / static_cast(Q) + << "},\n \"stable_island_ids\":["; + for (std::size_t i = 0; i < manifest.plan.islands.size(); ++i) + { if (i) out << ','; out << '\"' << Hex64(manifest.plan.islands[i].stableId) << '\"'; } + out << "],\n \"stable_connector_ids\":["; + for (std::size_t i = 0; i < manifest.plan.clusterEdges.size(); ++i) + { if (i) out << ','; out << '\"' << Hex64(manifest.plan.clusterEdges[i].stableId) << '\"'; } + out << "],\n \"island_profiles\":["; + for (std::size_t i = 0; i < manifest.plan.islands.size(); ++i) + { + if (i) out << ','; + const auto& island = manifest.plan.islands[i]; + const double ratio = static_cast(island.topZSubvoxels - island.bottomZSubvoxels) + / (2.0 * static_cast(island.radiusSubvoxels)); + out << "{\"id\":\"" << Hex64(island.stableId) << "\",\"ordinal\":" << island.ordinal + << ",\"center_voxels\":[" << island.centerSubvoxels[0] / static_cast(Q) + << ',' << island.centerSubvoxels[1] / static_cast(Q) + << ',' << island.centerSubvoxels[2] / static_cast(Q) + << "],\"radius_voxels\":" << island.radiusSubvoxels / static_cast(Q) + << ",\"top_voxels\":" << island.topZSubvoxels / static_cast(Q) + << ",\"bottom_voxels\":" << island.bottomZSubvoxels / static_cast(Q) + << ",\"depth_to_diameter\":" << ratio << '}'; + } + out << "],\n \"bounds\":{\"passed\":" << (bounds.passed ? "true" : "false") + << ",\"negative_control_detected\":" << (bounds.negativeControlDetected ? "true" : "false") + << ",\"boxes\":" << bounds.boxesTested << ",\"samples\":" << bounds.pointSamples + << ",\"unknown\":" << bounds.unknownBounds << ",\"missed\":" << bounds.missedBounds + << ",\"island_boxes\":" << bounds.islandBoundsTested + << ",\"connector_boxes\":" << bounds.connectorBoundsTested + << ",\"void_boxes_proved_air\":" << bounds.voidBoxesProvedAir << "},\n \"thickness\":{\"passed\":" + << (thickness.passed ? "true" : "false") + << ",\"islands_tested\":" << thickness.islandsTested + << ",\"islands_below_minimum\":" << thickness.islandsBelowMinimum + << ",\"minimum_depth_to_diameter_ratio\":" << thickness.minimumDepthToDiameterRatio + << ",\"required_depth_to_diameter_ratio\":" << thickness.requiredDepthToDiameterRatio + << ",\"minimum_island_id\":\"" << Hex64(thickness.minimumIslandStableId) << "\"}" + << ",\n \"thin_pad_negative_control\":{\"detected\":" + << ((!thinThickness.passed && thinThickness.islandsBelowMinimum == thinThickness.islandsTested) + ? "true" : "false") + << ",\"failed_islands\":" << thinThickness.islandsBelowMinimum + << ",\"minimum_depth_to_diameter_ratio\":" << thinThickness.minimumDepthToDiameterRatio << "}" + << ",\n \"route\":{\"passed\":" + << (route.passed ? "true" : "false") << ",\"segments\":" << route.segmentsTested + << ",\"centers\":" << route.centerSamples << ",\"capsule_samples\":" + << route.capsuleFieldSamples << ",\"blocked\":" << route.blockedCapsuleSamples + << ",\"support_samples\":" << route.supportProbeSamples << ",\"unsupported\":" + << route.unsupportedCenterSamples << ",\"grade_failures\":" << route.gradeFailures + << ",\"minimum_clearance_subvoxels\":" << route.minimumClearanceSubvoxels + << "},\n \"raised_route_negative_control\":{\"detected\":" + << ((!raised.passed && (raised.unsupportedCenterSamples || raised.blockedCapsuleSamples)) ? "true" : "false") + << ",\"passed\":" << (raised.passed ? "true" : "false") + << ",\"unsupported\":" << raised.unsupportedCenterSamples + << ",\"blocked\":" << raised.blockedCapsuleSamples << "},\n \"determinism\":{\"W1\":\"" + << fieldHashes[0] << "\",\"W8\":\"" << fieldHashes[1] << "\",\"W16\":\"" + << fieldHashes[2] << "\",\"plan_repeat\":true,\"hash_samples\":32768},\n \"timing\":{\"same_queries\":32768,\"legacy_ms\":" + << sample.legacyMilliseconds << ",\"native_ms\":" << sample.nativeMilliseconds + << ",\"native_over_legacy\":" << (sample.nativeMilliseconds / std::max(sample.legacyMilliseconds, 1e-9)) + << ",\"sign_mismatches\":" << sample.signMismatches << ",\"sign_mismatch_percent\":" + << (100.0 * sample.signMismatches / 32768.0) + << ",\"legacy_solid_samples\":" << sample.legacySolidSamples + << ",\"native_solid_samples\":" << sample.nativeSolidSamples + << ",\"legacy_only_solid_samples\":" << sample.legacyOnlySolidSamples + << ",\"native_only_solid_samples\":" << sample.nativeOnlySolidSamples << "},\n \"mesh\":["; + constexpr std::array names{"cluster_legacy", "cluster_native", "detail_legacy", "detail_native"}; + for (std::size_t i = 0; i < meshes.size(); ++i) + { + if (i) out << ','; + out << "{\"name\":\"" << names[i] << "\",\"grid_samples\":" << meshes[i].gridSamples + << ",\"triangles\":" << meshes[i].triangles << ",\"bytes\":" << meshes[i].bytes << '}'; + } + out << "],\n \"route_points_core_voxels\":["; + for (std::size_t i = 0; i < manifest.plan.route.size(); ++i) + { + if (i) out << ','; + const auto& point = manifest.plan.route[i]; + out << '[' << point.positionSubvoxels[0] / static_cast(Q) << ',' + << point.positionSubvoxels[1] / static_cast(Q) << ',' + << point.positionSubvoxels[2] / static_cast(Q) << ']'; + } + out << "]\n}\n"; + } +} + +int main(int argc, char** argv) +{ +#if defined(_WIN32) + SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS); + SetProcessAffinityMask(GetCurrentProcess(), static_cast(0xFFFF)); +#endif + try + { + if (argc != 3) + { std::cerr << "usage: FloatingIslandsAudit \n"; return 2; } + const std::filesystem::path probePath = argv[1]; + const std::filesystem::path outputDir = argv[2]; + std::filesystem::create_directories(outputDir); + const auto standaloneRoot = probePath.parent_path().parent_path().parent_path() + .parent_path().parent_path(); + const ExistingHashes oldHashes = VerifyExistingManifests(standaloneRoot); + + FStandaloneOwnerWorld owner; + std::string error; + if (!StandaloneOwnerLoader::Load(probePath.string(), owner, error)) + throw std::runtime_error("legacy probe load failed: " + error); + const OracleAnchor anchor = FindLegacyAnchor(owner); + if (anchor.supportScore < 9) + throw std::runtime_error("legacy scan found no broad FloatingIslands top in the frozen strate range"); + const Point coreAnchorQ{anchor.surfaceQ[0] / 2, + anchor.surfaceQ[1] / 2, anchor.surfaceQ[2] / 2}; + + FloatingIslandRecipeV1 recipe; + recipe.seed = 0; + recipe.layoutOriginXSubvoxels = coreAnchorQ[0]; + recipe.layoutOriginYSubvoxels = coreAnchorQ[1]; + recipe.layoutCenterTopZSubvoxels = coreAnchorQ[2]; + recipe.geometryVersion = 2; + recipe.layoutMinimumZSubvoxels = coreAnchorQ[2] - 80 * Q; + recipe.layoutMaximumZSubvoxels = coreAnchorQ[2] + 48 * Q; + recipe.primaryIslandCount = 3; + recipe.islandDensityQ16 = 32768; + recipe.spacingSubvoxels = 56 * Q; + recipe.minimumRadiusSubvoxels = 12 * Q; + recipe.maximumRadiusSubvoxels = 24 * Q; + recipe.thicknessRatioQ16 = 39322; + recipe.verticalJitterQ16 = 39322; + recipe.topFlattenQ16 = 39322; + recipe.taperEndMinimumQ16 = 29491; + recipe.taperEndMaximumQ16 = 45875; + recipe.blendRadiusSubvoxels = 3 * Q; + recipe.contourWarpSubvoxels = VoxQ(6.3); + recipe.roughnessSubvoxels = Q; + recipe.boundarySealSubvoxels = 2 * Q; + recipe.bridgeHalfWidthSubvoxels = VoxQ(2.5); + recipe.bridgeThicknessSubvoxels = 4 * Q; + + const auto beginPlanning = Clock::now(); + auto planned = PlanFloatingIslandCluster(recipe, error); + const auto endPlanning = Clock::now(); + if (!planned) throw std::runtime_error("native island planner failed: " + error); + const std::string canonical = planned->ToJson(); + for (int repeat = 0; repeat < 2; ++repeat) + { + std::string repeatError; + const auto repeated = PlanFloatingIslandCluster(recipe, repeatError); + if (!repeated || repeated->contentHash != planned->contentHash + || repeated->plan.contentHash != planned->plan.contentHash + || repeated->ToJson() != canonical) + throw std::runtime_error("repeated native plan hash or canonical JSON changed"); + } + const auto parsed = FloatingIslandManifestV1::FromJson(canonical, error); + if (!parsed || parsed->ToJson() != canonical || !parsed->validate(error)) + throw std::runtime_error("native manifest did not validate after round-trip: " + error); + + FloatingIslandFieldV1 field(planned->recipe, planned->plan); + if (!field.isValid()) throw std::runtime_error("native field rejected the planned island graph"); + const auto& middleIsland = planned->plan.islands.at(1); + const std::int64_t crustBottom = middleIsland.topZSubvoxels + - middleIsland.radiusSubvoxels * (planned->recipe.thicknessRatioQ16 + + planned->recipe.crustDepthBonusQ16) / 65536; + std::cout << "underside_center_density_q="; + for (int sample = 0; sample <= 8; ++sample) + { + if (sample) std::cout << ','; + const std::int64_t z = crustBottom - middleIsland.radiusSubvoxels * sample / 8; + std::cout << field.densitySubvoxels(middleIsland.centerSubvoxels[0], + middleIsland.centerSubvoxels[1], z); + } + std::cout << '\n'; + const auto& ownerSlots = owner.Manager->GetLayout(); + const auto islandSlot = std::find_if(ownerSlots.begin(), ownerSlots.end(), + [](const FStrateSlot& slot) + { return slot.Definition != nullptr + && slot.Definition->GeneratorType == ECaveGeneratorType::FloatingIslands; }); + if (islandSlot == ownerSlots.end()) + throw std::runtime_error("owner snapshot has no resolved FloatingIslands strate slot"); + if (planned->recipe.seed != static_cast(owner.Seed)) + throw std::runtime_error("native island recipe seed does not match the owner season"); + const OpenSkyAudit openSky = MeasureOpenSky(field); + const auto bounds = AuditFloatingIslandBounds(field, 512); + const auto route = AuditFloatingIslandRoute(field); + const auto raisedRoute = AuditFloatingIslandRoute(field, 8 * Q); + const auto thickness = AuditFloatingIslandThickness(planned->plan); + auto thinPlan = planned->plan; + for (auto& island : thinPlan.islands) + island.bottomZSubvoxels = island.topZSubvoxels + - island.radiusSubvoxels * planned->recipe.thicknessRatioQ16 / 65536; + const auto thinThickness = AuditFloatingIslandThickness(thinPlan); + const auto influence = field.influenceBounds(); + const auto points = MakeSamples(influence, 32768, 0x4d33524f554e4431ull); + const auto w1 = HashWithWorkers(field, owner, points, 1, false); + const auto w8 = HashWithWorkers(field, owner, points, 8, false); + const auto w16 = HashWithWorkers(field, owner, points, 16, true); + const std::array hashes{w1.hash, w8.hash, w16.hash}; + if (w1.hash != w8.hash || w1.hash != w16.hash) + throw std::runtime_error("native field sample hash differs between W1/W8/W16"); + if (!bounds.passed) throw std::runtime_error("native island bounds audit or negative control failed"); + if (!thickness.passed || thinThickness.passed + || thinThickness.islandsBelowMinimum != thinThickness.islandsTested) + throw std::runtime_error("native island thickness audit or thin-pad negative control failed"); + std::cout << "route_diagnostics pass=" << route.passed << " segments=" << route.segmentsTested + << " centers=" << route.centerSamples << " capsule_samples=" << route.capsuleFieldSamples + << " blocked=" << route.blockedCapsuleSamples << " support_samples=" << route.supportProbeSamples + << " unsupported=" << route.unsupportedCenterSamples << " grade_failures=" << route.gradeFailures + << " minimum_clearance_q=" << route.minimumClearanceSubvoxels + << " first_blocked_segment=" << route.firstBlockedSegment + << " first_unsupported_segment=" << route.firstUnsupportedSegment + << " first_grade_segment=" << route.firstGradeFailureSegment + << " unsupported_center_voxels=" << route.firstUnsupportedCenterSubvoxels[0] / static_cast(Q) + << ',' << route.firstUnsupportedCenterSubvoxels[1] / static_cast(Q) + << ',' << route.firstUnsupportedCenterSubvoxels[2] / static_cast(Q) << '\n'; + if (!route.passed) + { + const auto& routePoints = planned->plan.route; + if (route.firstBlockedSegment != ~std::uint64_t{0}) + std::cout << "first_blocked_position_voxels=" + << route.firstBlockedPositionSubvoxels[0] / static_cast(Q) << ',' + << route.firstBlockedPositionSubvoxels[1] / static_cast(Q) << ',' + << route.firstBlockedPositionSubvoxels[2] / static_cast(Q) << '\n'; + const std::size_t debugBegin = route.firstUnsupportedSegment == ~std::uint64_t{0} + ? 0 : static_cast(route.firstUnsupportedSegment); + for (std::size_t i = debugBegin > 1 ? debugBegin - 1 : 0; + i < std::min(routePoints.size(), debugBegin + 3); ++i) + { + const auto& point = routePoints[i].positionSubvoxels; + std::cout << "route_point=" << i << ' ' + << point[0] / static_cast(Q) << ',' + << point[1] / static_cast(Q) << ',' + << point[2] / static_cast(Q) << '\n'; + } + const double maxGrade = static_cast(planned->recipe.routeConstraints.maximumGradeQ16) / 65536.0; + for (std::size_t i = 0; i + 1 < routePoints.size(); ++i) + { + const auto& a = routePoints[i].positionSubvoxels; + const auto& b = routePoints[i + 1].positionSubvoxels; + const double horizontal = std::hypot(static_cast(b[0] - a[0]), + static_cast(b[1] - a[1])); + const double grade = horizontal > 0.0 ? std::abs(static_cast(b[2] - a[2])) / horizontal : 1e9; + if (grade > maxGrade + 1e-7) + std::cout << "grade_segment=" << i << " ratio=" << grade << " limit=" << maxGrade + << " from=" << a[0] / static_cast(Q) << ',' << a[1] / static_cast(Q) + << ',' << a[2] / static_cast(Q) << " to=" << b[0] / static_cast(Q) + << ',' << b[1] / static_cast(Q) << ',' << b[2] / static_cast(Q) << '\n'; + } + for (std::size_t i = 0; i < planned->plan.islands.size(); ++i) + { + const auto& island = planned->plan.islands[i]; + std::cout << "island=" << i << " center=" << island.centerSubvoxels[0] / static_cast(Q) + << ',' << island.centerSubvoxels[1] / static_cast(Q) << ',' + << island.topZSubvoxels / static_cast(Q) << " radius=" + << island.radiusSubvoxels / static_cast(Q) << '\n'; + } + const auto& constraints = planned->recipe.routeConstraints; + const auto& unsupported = route.firstUnsupportedCenterSubvoxels; + const double foot = unsupported[2] / static_cast(Q) + - constraints.capsuleHeightSubvoxels / (2.0 * Q); + std::cout << "unsupported_route_probe=" << unsupported[0] / static_cast(Q) + << ',' << unsupported[1] / static_cast(Q) << ',' + << unsupported[2] / static_cast(Q) << " foot=" << foot << '\n'; + const double probeRadius = constraints.supportProbeRadiusSubvoxels / static_cast(Q); + const std::array, 5> supportPoints{{ + {0.0,0.0},{probeRadius,0.0},{-probeRadius,0.0},{0.0,probeRadius},{0.0,-probeRadius}}}; + for (std::size_t probeIndex = 0; probeIndex < supportPoints.size(); ++probeIndex) + { + double firstSolidDepth = -1.0; + for (double depth = (constraints.minimumClearanceSubvoxels + 0.5 * Q) / static_cast(Q); + depth <= (constraints.minimumClearanceSubvoxels + constraints.supportSearchDepthSubvoxels) + / static_cast(Q) + 0.001; depth += 0.5) + { + const auto density = field.densitySubvoxels( + std::llround(unsupported[0] + supportPoints[probeIndex][0] * Q), + std::llround(unsupported[1] + supportPoints[probeIndex][1] * Q), + std::llround((foot - depth) * Q)); + if (density < 0) { firstSolidDepth = depth; break; } + } + std::cout << "support_probe=" << probeIndex << " first_solid_depth_vox=" << firstSolidDepth << '\n'; + std::cout << "support_profile=" << probeIndex << ':'; + for (double depth = 0.5; depth <= 5.0; depth += 0.5) + { + const auto density = field.densitySubvoxels( + std::llround(unsupported[0] + supportPoints[probeIndex][0] * Q), + std::llround(unsupported[1] + supportPoints[probeIndex][1] * Q), + std::llround((foot - depth) * Q)); + std::cout << ' ' << depth << '=' << density; + } + std::cout << '\n'; + } + for (std::size_t i = 0; i < planned->plan.clusterEdges.size(); ++i) + { + const auto& edge = planned->plan.clusterEdges[i]; + std::cout << "connector=" << i << " start=" << edge.startSubvoxels[0] / static_cast(Q) + << ',' << edge.startSubvoxels[1] / static_cast(Q) << ',' + << edge.startSubvoxels[2] / static_cast(Q) << " end=" + << edge.endSubvoxels[0] / static_cast(Q) << ',' + << edge.endSubvoxels[1] / static_cast(Q) << ',' + << edge.endSubvoxels[2] / static_cast(Q) << '\n'; + } + } + if (!route.passed) throw std::runtime_error("native route capsule sweep failed"); + if (raisedRoute.passed || (raisedRoute.unsupportedCenterSamples == 0 + && raisedRoute.blockedCapsuleSamples == 0)) + throw std::runtime_error("raised-route negative control did not detect unsupported traversal"); + + std::array meshes{}; + const auto clusterBounds = field.influenceBounds(); + if (!clusterBounds.finite) throw std::runtime_error("native cluster mesh bounds are not finite"); + const Point clusterMin{clusterBounds.minimumSubvoxels[0] - 8 * Q, + clusterBounds.minimumSubvoxels[1] - 8 * Q, clusterBounds.minimumSubvoxels[2] - 8 * Q}; + const Point clusterMax{clusterBounds.maximumSubvoxels[0] + 8 * Q, + clusterBounds.maximumSubvoxels[1] + 8 * Q, clusterBounds.maximumSubvoxels[2] + 8 * Q}; + const Point detailMin{coreAnchorQ[0] - 64 * Q, coreAnchorQ[1] - 64 * Q, coreAnchorQ[2] - 80 * Q}; + const Point detailMax{coreAnchorQ[0] + 64 * Q, coreAnchorQ[1] + 64 * Q, coreAnchorQ[2] + 16 * Q}; + meshes[0] = ExtractMesh(outputDir / "cluster_legacy.vfmesh", clusterMin, clusterMax, 4 * Q, + [&](const Point& q) { return SampleLegacy(owner, q); }); + meshes[1] = ExtractMesh(outputDir / "cluster_native.vfmesh", clusterMin, clusterMax, 4 * Q, + [&](const Point& q) { return SampleNative(field, q); }); + meshes[2] = ExtractMesh(outputDir / "detail_legacy.vfmesh", detailMin, detailMax, 2 * Q, + [&](const Point& q) { return SampleLegacy(owner, q); }); + meshes[3] = ExtractMesh(outputDir / "detail_native.vfmesh", detailMin, detailMax, 2 * Q, + [&](const Point& q) { return SampleNative(field, q); }); + if (meshes[0].triangles == 0 || meshes[1].triangles == 0 + || meshes[2].triangles == 0 || meshes[3].triangles == 0) + throw std::runtime_error("matched legacy/native mesh extraction produced an empty field"); + + WriteJson(outputDir / "audit.json", *planned, bounds, route, raisedRoute, + w16, thickness, thinThickness, anchor, meshes, hashes, oldHashes, openSky); + WriteIslandImportManifest(standaloneRoot, *planned, *islandSlot, outputDir); + std::cout << std::setprecision(4) + << "legacy_anchor_owner_voxels=" << anchor.surfaceQ[0] / static_cast(Q) << ',' + << anchor.surfaceQ[1] / static_cast(Q) << ',' + << anchor.surfaceQ[2] / static_cast(Q) + << " comparison_anchor_core_voxels=" << coreAnchorQ[0] / static_cast(Q) << ',' + << coreAnchorQ[1] / static_cast(Q) << ',' + << coreAnchorQ[2] / static_cast(Q) + << " plateau_samples=" << anchor.supportScore << '\n' + << "recipe_hash=" << Hex64(planned->recipe.contentHash) + << " plan_hash=" << Hex64(planned->plan.contentHash) + << " manifest_hash=" << Hex64(planned->contentHash) + << " prior_m1_hash=" << oldHashes.round1Manifest + << " prior_surfaceworld_hash=" << oldHashes.surfaceWorldRound4Manifest + << " islands=" << planned->plan.islands.size() + << " connectors=" << planned->plan.clusterEdges.size() + << " route_points=" << planned->plan.route.size() + << " planning_ms=" << std::chrono::duration(endPlanning - beginPlanning).count() << '\n' + << "bounds_pass=" << bounds.passed << " boxes=" << bounds.boxesTested + << " point_samples=" << bounds.pointSamples << " unknown=" << bounds.unknownBounds + << " missed=" << bounds.missedBounds << " void_air=" << bounds.voidBoxesProvedAir + << " negative_control=" << bounds.negativeControlDetected << '\n' + << "route_pass=" << route.passed << " segments=" << route.segmentsTested + << " centers=" << route.centerSamples << " capsule_samples=" << route.capsuleFieldSamples + << " blocked=" << route.blockedCapsuleSamples << " support_probes=" << route.supportProbeSamples + << " unsupported=" << route.unsupportedCenterSamples << " grade_failures=" << route.gradeFailures + << " clearance_q=" << route.minimumClearanceSubvoxels << '\n' + << "raised_route_detected=" << (!raisedRoute.passed && raisedRoute.unsupportedCenterSamples > 0) + << " unsupported=" << raisedRoute.unsupportedCenterSamples + << " blocked=" << raisedRoute.blockedCapsuleSamples << '\n' + << "thickness_pass=" << thickness.passed << " islands=" << thickness.islandsTested + << " min_depth_diameter=" << thickness.minimumDepthToDiameterRatio + << " threshold=" << thickness.requiredDepthToDiameterRatio + << " thin_pad_detected=" << (!thinThickness.passed + && thinThickness.islandsBelowMinimum == thinThickness.islandsTested) + << " thin_ratio=" << thinThickness.minimumDepthToDiameterRatio << '\n' + << "field_hashes W1=" << w1.hash << " W8=" << w8.hash << " W16=" << w16.hash + << " legacy_ms=" << w16.legacyMilliseconds << " native_ms=" << w16.nativeMilliseconds + << " mismatch_percent=" << (100.0 * w16.signMismatches / 32768.0) + << " legacy_solid=" << w16.legacySolidSamples << " native_solid=" << w16.nativeSolidSamples + << " legacy_only=" << w16.legacyOnlySolidSamples << " native_only=" << w16.nativeOnlySolidSamples << '\n' + << "mesh_samples=" << meshes[0].gridSamples << ',' << meshes[1].gridSamples + << ',' << meshes[2].gridSamples << ',' << meshes[3].gridSamples + << " triangles=" << meshes[0].triangles << ',' << meshes[1].triangles + << ',' << meshes[2].triangles << ',' << meshes[3].triangles << '\n' + << "audit_json=" << (outputDir / "audit.json").string() << '\n'; + return 0; + } + catch (const std::exception& exception) + { + std::cerr << "FLOATING_ISLAND_AUDIT_FAIL: " << exception.what() << '\n'; + return 1; + } +} diff --git a/Standalone/Research/WorldAssemblerM3/Round3/REPORT.md b/Standalone/Research/WorldAssemblerM3/Round3/REPORT.md new file mode 100644 index 0000000..e230e56 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round3/REPORT.md @@ -0,0 +1,83 @@ +# World assembler M3 round 3 + +Date: 2026-10-10 + +## Status + +The island recipe, manifest import, BuildHost world proof, and matched offline captures are in place. The final profiling pass found a remaining LOD0 sprint budget miss, and a follow-up XY-column cache was then added to `FloatingIslandsInternal.h`. That last source edit has **not** been compiled or exercised: the command runner began failing to start with Windows status `0xC0000142` after a completed profile run. Therefore the verified BuildHost results below apply to the root-parameter-cache version immediately before the XY-column-cache edit, not to the current tree. Treat round 3 as **not fully verified / do not hand off the current binary yet** until that edit is either verified or reverted and rebuilt. + +## Scale contract + +The owner's legacy voxel size is **12.5 cm** (`Source/VoxelForge/Public/VoxelTypes.h`, `VOXEL_SIZE`). Core operates at **25 cm**, so owner-to-Core lengths multiply by **0.5** and Core-to-owner lengths multiply by **2**. Island radii are 24–48 owner voxels = 3–6 m = 12–24 Core voxels. Site spacing is 112 owner voxels = 14 m = 56 Core voxels. Standalone's old 25 cm metadata was not authoritative; the import validator and audit metadata now pin 12.5/25 cm and the 1:2 ratio. The radius comment is corrected to this contract. + +## Shape review and measurements + +Round 3's downscaled, matched offline captures show four distinct masses with sky visible between them; the three connectors remain traversable and read as narrow spans. The underside center is a shaded solid buttress/dimple, not a through-hole. The edge capture does not show the former sawtooth rim; the crust edge is smooth at the displayed scale. These are offline CPU renders, not captures from the owner's Unreal viewport. + +At the sampled mid-height plane, the open-sky measure is **1,462 / 1,591 = 91.89%** of the cluster XY bounding box. Sampling is at 1 m spacing, Core Z `-964.039`. Island radii are 3–6 m; connector full width is 1.25 m, thickness 1 m, blend 0.75 m, and roughness 0.25 m. The plan has four islands, three connectors, and 28 route segments. The route audit checks 68,816 capsule samples and reports no blocked, unsupported, or over-grade samples; minimum clearance is 99 subvoxel samples. + +The mass audit checked 515 bounds / 13,851 samples with no missed island bounds. The thin-pad negative control was caught on all four islands (measured ratio 0.2999 against required 0.70; passing minimum was 0.75). The raised-route negative control was also caught. The underside audit samples the center column at nine points; rerun the audit after resolving the unverified cache edit to retain that numeric output in `results/audit.json`. + +## Import and season hashing + +Import `Standalone/Research/WorldAssemblerM3/Round3/results/world_assembly_floating_islands.json` through the existing `UVoxelSettings` manifest JSON import (`SourceWorldAssemblyManifestJson` / **Import World Assembly Manifest JSON**). Then assign that settings asset to `AVoxelWorld` and enable **Voxel World → Core → Use VoxelForge Core** (`bUseVoxelForgeCore`). The recipe remains on the Core world-assembly route. Its assembly content hash enters the season hash only when Core is enabled; Core-off keeps the legacy season identity. Water remains fixed for the season and player edits remain unrestricted. + +Verified artifact hashes: + +- Plan: `61fd58f65c5fe157` +- Recipe: `ef1340d32b072ba3` +- Nested floating-island manifest: `54160b4d25d0279f` +- World assembly content: `94b563f1ce622def` +- W1/W8/W16 standalone determinism: `5764b702482740e8` + +## BuildHost proof recorded before the final unverified edit + +BuildHost runs used `Tools/BuildVoxelForgeCoreHost.ps1` and the test scripts with a 900 second timeout, BelowNormal priority, and at most four parallel build actions. No owner project/editor was opened or built. Measured process peaks stayed below 4 GB. The passing island run recorded: + +- 2,553 standalone tile hashes matched; Core reported 512 LOD0 tiles, 448 LOD1 tiles, 432 LOD transitions, 573 local-coverage checks, and 3,438 neighbor-face checks. +- Aggregate hashes: density `e46d4280f73f03f3`, mesh `a7c92c6ce22ba08b3`, payload `040e800d58e1c2f1`, topology `da689102032f94da`. +- Coverage completed; collision was cooked and capsule contact passed on both an island top and a span. +- Height/anchor agreement passed for the island and span; content anchors reported 14 trees and no floaters. Corrected strate and atmosphere strate both reported 1; water was enabled at local `-49,376 cm` and unchanged. +- Blueprint edit passed: carve/fill on an island with legacy radius 6 (=75 cm) and strength 10; the 90 cm outside sample had zero density delta. +- Core-off passed with legacy density unchanged; the Core-only hash was omitted while Core was off. +- SurfaceWorld M1 regression test passed with the original worker hash `0x8cc18e2981c50c4e`; HybridV1 and HybridV2 M2 recipe fixtures passed. + +Logs are in `results/m1_surfaceworld_buildhost.*`, `results/m2_hybrid_v1_buildhost.*`, `results/m2_hybrid_v2_buildhost.*`, and `results/m3_floating_islands_buildhost.*`. These pre-profile logs do not prove the final XY-column-cache edit. + +## Streaming profile and remaining work + +The final instrumented BuildHost commandlet completed and its internal validator printed PASS. The outer PowerShell wrapper then exited 1 because its console-title channel failed (`0xE9`); this is recorded as a wrapper failure despite the commandlet pass marker. The timing profile showed density/meshing dominates; classifier and column work were negligible. + +The same measured per-tile p95 is compared with the available time at each movement speed: at 600 cm/s the deadlines are 1,333.333 ms for LOD0 and 2,666.667 ms for LOD1; at 6,000 cm/s they are 133.333 ms and 266.667 ms, respectively. Core and legacy timings from the instrumented run were: + +| LOD | Tile kind | Core p95 | Legacy p95 | Walk deadline / result | Sprint deadline / result | +|---|---|---:|---:|---|---| +| 0 | Air | 0.017 ms | 38.842 ms | 1,333.333 ms / pass | 133.333 ms / pass | +| 0 | Island | 157.671 ms | 43.702 ms | 1,333.333 ms / pass | 133.333 ms / 24.338 ms late | +| 0 | Mixed/span | 153.082 ms | 42.146 ms | 1,333.333 ms / pass | 133.333 ms / 19.749 ms late | +| 1 | Air | 0.009 ms | 46.549 ms | 2,666.667 ms / pass | 266.667 ms / pass | +| 1 | Mixed/span | 151.685 ms | 42.701 ms | 2,666.667 ms / pass | 266.667 ms / pass | + +LOD1 mixed was 151.685 ms against its 266.667 ms sprint deadline; LOD1 air was 0.009 ms. The root-parameter cache preserved hashes but did not clear the LOD0 sprint miss. The XY-column cache was added afterward to target the measured mesher/density hot path; it has not been compiled or measured. Resolve that before claiming the streaming goal complete. + +## Run ledger and incidents + +- SurfaceWorld M1, HybridV1 M2, HybridV2 M2, and the island BuildHost proof all passed on the verified source state described above. +- Earlier island BuildHost attempts, in order, failed as follows: (1) desired tiles were 2,227 instead of 2,553 because the route player was absent; (2) the GameMode spawned an origin pawn; (3) possession caused 2,557 desired tiles because dynamic collision support was active; (4) the support tile key did not match; (5) disabling the collision gate restored 2,553 tiles but exposed a hash mismatch at `(-2,0,-31)`; (6) `CreateLocalPlayer(true)` hung and was manually stopped after roughly 3–4 minutes; (7) the base UPlayer fixture lacked controller registration/root transform and failed preflight after 5:10; (8) explicit `AddController`, root setup, and `SetActorLocation` fixed the fixture and the island proof passed. The manual stop was not a crash. +- Earlier compile failures included an invalid `DuplicateObject` overload, private helper access, an unresolved `GetEffectiveWorldSeed` export, and integer narrowing. They were corrected; subsequent builds passed. +- One initial standalone build used the wrong `-Workers` switch and failed immediately; `-CompileWorkers 4` succeeded. +- M1 threshold experiments before restoring the original chunk mapping failed at LOD8 edge bounds: threshold 4.0 measured 52,075.827 > 51,200 at tile `(2,0,-1)`; 4.125 measured 53,960.427 > 52,800; 4.25 measured 54,806.919 > 54,400. Threshold 4.5 passed that edge check but left no full-span LOD0 candidate. Restoring SurfaceWorld's original 32-voxel chunk mapping, using the separate 16-Core-voxel island mapping, and restoring threshold 4.0 made M1 pass. +- The last commandlet itself passed, but the wrapper returned 1 on console error `0xE9`. Subsequent PowerShell and `cmd.exe` launches returned `0xC0000142`; the requested retry was made, then additional shell inspection launches also failed. No further build was attempted. No new `UECC-*` folder was reported by the latest BuildHost crash check; only historical folders dated 2026-09-17 and 2026-10-08 were present. +- No `0xC0000142` BuildHost retry was observed in the completed runs. Peak recorded BuildHost test process memory was about 3.98 GB. + +## Owner hand-off + +After the final cache edit is verified, import the manifest above into `UVoxelSettings`, assign the settings asset to the world, and turn on `bUseVoxelForgeCore`. To find the cluster, start at the actor-local route point `(-380, 631.8, -23,837) cm` and follow the route toward the middle island (roughly +X / -Y). The center island is near actor-local `(600, -306, -23,872) cm`; the span contact is near `(113, 161, -23,860) cm`. + +For an underside view, use a free camera near `(600, -306, -24,750) cm`, looking upward toward `(600, -306, -24,300) cm`. For an overview, start near `(680, -5,000, -21,500) cm` and look toward `(678, -556, -24,100) cm`. These are actor-local suggestions; apply the actor transform in the level. The contact sheet and matched captures are under this Round3 `results/` directory and are downscaled offline renders; BuildHost validation uses null RHI. + +Likely Unreal compile spots are `VoxelSettings.h/.cpp`, `VoxelContentManager.h/.cpp`, `VoxelWorld.cpp`, `VoxelForgeCoreAdapter.cpp`, and `VoxelForgeExploreCommandlet.cpp`; canonical Core changes are in `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ShapeWorldInternal.h`, `FloatingIslandsInternal.h`, and `WorldAssemblerInternal.h`. + +## Source and artifact inventory + +Round 3 adds these research/support sources: `Standalone/Research/WorldAssemblerM3/Round3/FloatingIslandsAudit.cpp`, `BuildFloatingIslandsAudit.ps1`, and `RenderFloatingIslandRound3.py`. Existing source edits include `Standalone/Source/OwnerWorldLoader.h`, `Standalone/Core/src/FloatingIslandsInternal.h`, `Standalone/Core/src/ShapeWorldInternal.h`, and `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp`. There is no new Unreal runtime source file for the manifest path. BuildHost scripts and `.log`/capture/JSON outputs are evidence/support artifacts. diff --git a/Standalone/Research/WorldAssemblerM3/Round3/RenderFloatingIslandRound3.py b/Standalone/Research/WorldAssemblerM3/Round3/RenderFloatingIslandRound3.py new file mode 100644 index 0000000..a188567 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round3/RenderFloatingIslandRound3.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +"""Small CPU-only matched views of the legacy and native FloatingIslands fields.""" +from __future__ import annotations + +import ctypes +import json +import math +import struct +from pathlib import Path + +from PIL import Image, ImageDraw + + +ROOT = Path(__file__).resolve().parent +RESULTS = ROOT / "results" +CAPTURES = RESULTS / "captures" +CAPTURES.mkdir(parents=True, exist_ok=True) +WIDTH, HEIGHT = 320, 226 +BACKGROUND = (236, 240, 242) +LEGACY_COLOR = (133, 127, 105) +NATIVE_COLOR = (78, 135, 151) +ROUTE_COLOR = (223, 72, 45) +TILE_COLOR = (178, 60, 52) +LOD_COLOR = (211, 136, 45) +Q = 256.0 + + +def lower_priority() -> None: + if hasattr(ctypes, "windll"): + ctypes.windll.kernel32.SetPriorityClass(ctypes.windll.kernel32.GetCurrentProcess(), 0x00004000) + + +def read_mesh(path: Path): + raw = path.read_bytes() + if len(raw) < 8 or raw[:4] != b"VFMI": + raise RuntimeError(f"invalid field mesh: {path}") + count = struct.unpack_from(" 1.0e-12 else (0.0, 0.0, 1.0) + + +def dot(a, b): + return sum(x * y for x, y in zip(a, b)) + + +def cross(a, b): + return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) + + +def project(points, target, view_dir, scale=None): + direction = normalize(view_dir) + forward = tuple(-value for value in direction) + reference_up = (0.0, 1.0, 0.0) if abs(forward[2]) > 0.98 else (0.0, 0.0, 1.0) + right = normalize(cross(forward, reference_up)) + up = normalize(cross(right, forward)) + camera_points = [] + for point in points: + delta = tuple(point[i] - target[i] for i in range(3)) + camera_points.append((dot(delta, right), dot(delta, up), dot(delta, forward))) + if scale is None: + span_x = max((p[0] for p in camera_points), default=1.0) - min((p[0] for p in camera_points), default=0.0) + span_y = max((p[1] for p in camera_points), default=1.0) - min((p[1] for p in camera_points), default=0.0) + scale = 0.86 * min((WIDTH - 30) / max(span_x, 1.0e-6), (HEIGHT - 30) / max(span_y, 1.0e-6)) + pixels = [ + (WIDTH * 0.5 + point[0] * scale, 31 + (HEIGHT - 31) * 0.5 - point[1] * scale, point[2]) + for point in camera_points + ] + return pixels, scale, right, up, forward + + +def common_scale(mesh_pair, target, view_dir): + vertices = mesh_pair[0][1] + mesh_pair[1][1] + _, scale, _, _, _ = project(vertices, target, view_dir) + return scale + + +def projected_line(a, b, target, basis, scale): + right, up, forward = basis + points = [] + for point in (a, b): + delta = tuple(point[i] - target[i] for i in range(3)) + points.append((WIDTH * 0.5 + dot(delta, right) * scale, + 31 + (HEIGHT - 31) * 0.5 - dot(delta, up) * scale)) + return points + + +def render(mesh, mesh_pair, method, target, view_dir, title, subtitle, *, route=None, + guide=None, camera_eye=None, framing_radius=None): + triangles, vertices = mesh + perspective = camera_eye is not None + if perspective: + forward = normalize(tuple(target[i] - camera_eye[i] for i in range(3))) + reference_up = (0.0, 1.0, 0.0) if abs(forward[2]) > 0.98 else (0.0, 0.0, 1.0) + right = normalize(cross(forward, reference_up)) + up = normalize(cross(right, forward)) + scale = WIDTH * 0.91 + origin = camera_eye + else: + if framing_radius is None: + scale = common_scale(mesh_pair, target, view_dir) + else: + scale = 0.86 * min((WIDTH - 30) / (2.0 * framing_radius), + (HEIGHT - 30) / (2.0 * framing_radius)) + _, _, right, up, forward = project((), target, view_dir, scale) + origin = target + + def screen(point): + delta = tuple(point[i] - origin[i] for i in range(3)) + cx, cy, depth = dot(delta, right), dot(delta, up), dot(delta, forward) + if perspective: + if depth <= 1.0: + return None + return (WIDTH * 0.5 + cx * scale / depth, + 31 + (HEIGHT - 31) * 0.5 - cy * scale / depth, depth) + return (WIDTH * 0.5 + cx * scale, + 31 + (HEIGHT - 31) * 0.5 - cy * scale, depth) + + background = Image.new("RGB", (WIDTH, HEIGHT + 32), BACKGROUND) + draw = ImageDraw.Draw(background) + draw.rectangle((0, 0, WIDTH - 1, 31), fill=(34, 46, 54)) + draw.text((8, 4), title, fill=(250, 252, 253)) + draw.text((8, 17), subtitle, fill=(190, 207, 215)) + pixels = background.load() + zbuffer = [float("inf")] * (WIDTH * (HEIGHT - 31)) + camera_fill = forward if perspective else normalize(view_dir) + light = normalize((-0.25 + 1.2 * camera_fill[0], + -0.35 + 1.2 * camera_fill[1], + 0.45 + 1.2 * camera_fill[2])) + base = LEGACY_COLOR if method == "legacy" else NATIVE_COLOR + for tri in triangles: + points = [screen(tri[offset:offset + 3]) for offset in (0, 3, 6)] + if any(point is None for point in points): + continue + x0 = max(0, int(math.floor(min(p[0] for p in points)))) + x1 = min(WIDTH - 1, int(math.ceil(max(p[0] for p in points)))) + y0 = max(31, int(math.floor(min(p[1] for p in points)))) + y1 = min(HEIGHT - 1, int(math.ceil(max(p[1] for p in points)))) + if x0 > x1 or y0 > y1: + continue + (ax, ay, az), (bx, by, bz), (cx, cy, cz) = points + denominator = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(denominator) < 1.0e-10: + continue + normals = [normalize(tri[offset:offset + 3]) for offset in (9, 12, 15)] + for y in range(y0, y1 + 1): + row = (y - 31) * WIDTH + for x in range(x0, x1 + 1): + sx, sy = x + 0.5, y + 0.5 + u = ((by - cy) * (sx - cx) + (cx - bx) * (sy - cy)) / denominator + v = ((cy - ay) * (sx - cx) + (ax - cx) * (sy - cy)) / denominator + w = 1.0 - u - v + if u < -1e-8 or v < -1e-8 or w < -1e-8: + continue + if perspective: + depth = 1.0 / (u / az + v / bz + w / cz) + else: + depth = u * az + v * bz + w * cz + normal = normalize(tuple(u * normals[0][axis] + v * normals[1][axis] + + w * normals[2][axis] for axis in range(3))) + shade = 0.50 + 0.50 * max(0.0, dot(normal, light)) + color = tuple(max(0, min(255, int(channel * shade))) for channel in base) + index = row + x + if depth < zbuffer[index]: + zbuffer[index] = depth + pixels[x, y] = color + + basis = (right, up, forward) + if guide: + origin = guide["origin_voxels"] + top = guide["top_voxels"] + for x_delta, color, label in ((32.0, TILE_COLOR, "32v tile"), (64.0, LOD_COLOR, "64v LOD1")): + line = projected_line((origin[0] + x_delta, origin[1] - 190.0, top - 65.0), + (origin[0] + x_delta, origin[1] + 190.0, top + 14.0), + target, basis, scale) + draw.line(line, fill=color, width=2) + draw.text((max(2, min(WIDTH - 55, int(line[1][0]) + 2)), + max(33, min(HEIGHT - 11, int(line[1][1]) - 12))), label, fill=color) + if route: + points = [screen(point) for point in route] + points = [point[:2] for point in points if point is not None] + if len(points) >= 2: + draw.line(points, fill=(250, 248, 239), width=5, joint="curve") + draw.line(points, fill=ROUTE_COLOR, width=3, joint="curve") + for index in (0, len(points) - 1): + x, y = points[index] + draw.ellipse((x - 4, y - 4, x + 4, y + 4), fill=(250, 248, 239), outline=ROUTE_COLOR, width=2) + return background + + +def titled_pair(left, right, category): + pair = Image.new("RGB", (WIDTH * 2, HEIGHT + 32), (220, 226, 229)) + pair.paste(left, (0, 0)) + pair.paste(right, (WIDTH, 0)) + draw = ImageDraw.Draw(pair) + draw.rectangle((0, 0, WIDTH * 2 - 1, HEIGHT + 31), outline=(72, 84, 91), width=1) + draw.text((8, HEIGHT + 14), f"{category} | same target, camera and scale", fill=(45, 55, 61)) + return pair + + +def main(): + lower_priority() + data = json.loads((RESULTS / "audit.json").read_text(encoding="utf-8")) + anchor = data["comparison_anchor_core_voxels"] + route = data["route_points_core_voxels"] + profiles = data["island_profiles"] + middle_island = profiles[1] + cluster = { + "legacy": read_mesh(RESULTS / "cluster_legacy.vfmesh"), + "native": read_mesh(RESULTS / "cluster_native.vfmesh"), + } + detail = { + "legacy": read_mesh(RESULTS / "detail_legacy.vfmesh"), + "native": read_mesh(RESULTS / "detail_native.vfmesh"), + } + a = anchor + scenes = ( + ("overview", "Cluster overview", cluster, (a[0], a[1], a[2] - 28.0), (8.0, -10.0, 7.0), None, None), + ("underside", "Cluster underside and lobed roots", detail, + (middle_island["center_voxels"][0], middle_island["center_voxels"][1], + middle_island["bottom_voxels"] - 6.0), (0.12, -0.08, -1.0), None, None), + ("edge_cliff", "Thick crust and bounded rock detail", detail, + (middle_island["center_voxels"][0], middle_island["center_voxels"][1], + middle_island["top_voxels"] - middle_island["radius_voxels"] * 0.65), + (1.0, -0.25, 0.18), None, None), + ("route", "Supported route across the cluster", cluster, + (middle_island["center_voxels"][0], middle_island["center_voxels"][1], + middle_island["top_voxels"] - 18.0), (8.0, -10.0, 7.0), route, None), + ) + pairs = [] + for key, title, meshes, target, camera, path, grid in scenes: + framing_radius = 28.0 if key in ("underside", "edge_cliff") else None + left = render(meshes["legacy"], (meshes["legacy"], meshes["native"]), "legacy", + target, camera, title, "Legacy bridge | exact owner field", route=None, + guide=grid, framing_radius=framing_radius) + route_caption = "Core native | planner route" if path else "Core native | matched field view" + right = render(meshes["native"], (meshes["legacy"], meshes["native"]), "native", + target, camera, title, route_caption, route=path, guide=grid, + framing_radius=framing_radius) + pair = titled_pair(left, right, title) + out = CAPTURES / f"{key}_matched.jpg" + pair.save(out, quality=82, optimize=True) + pairs.append(pair) + print(f"capture={out.name} bytes={out.stat().st_size} size={pair.size[0]}x{pair.size[1]}") + + eye_index = 5 + eye_route = route[eye_index] + eye = (eye_route[0], eye_route[1], eye_route[2] + 3.68) + next_island = profiles[1] + look = (next_island["center_voxels"][0], next_island["center_voxels"][1], + next_island["top_voxels"] + 5.0) + perspective_pair = [] + for method, meshes in (("legacy", cluster), ("native", cluster)): + perspective_pair.append(render(meshes[method], (meshes["legacy"], meshes["native"]), method, + look, (0.0, 0.0, -1.0), "Island-edge player view", "Eye at 1.8 m | route span to next island", + route=None, camera_eye=eye)) + eye_pair = titled_pair(perspective_pair[0], perspective_pair[1], "Island-edge player view") + eye_path = CAPTURES / "player_eye_matched.jpg" + eye_pair.save(eye_path, quality=80, optimize=True) + pairs.insert(3, eye_pair) + print(f"capture={eye_path.name} bytes={eye_path.stat().st_size} size={eye_pair.size[0]}x{eye_pair.size[1]}") + + sheet = Image.new("RGB", (WIDTH * 2, (HEIGHT + 32) * len(pairs)), (218, 225, 228)) + for row, pair in enumerate(pairs): + sheet.paste(pair, (0, row * (HEIGHT + 32))) + sheet_path = CAPTURES / "contact_sheet.jpg" + sheet.save(sheet_path, quality=76, optimize=True) + oversized = [(path.name, path.stat().st_size) for path in sorted(CAPTURES.glob("*.jpg")) + if path.stat().st_size >= 300_000] + for path in sorted(CAPTURES.glob("*.jpg")): + print(f"capture_size={path.name} bytes={path.stat().st_size}") + if oversized: + raise RuntimeError(f"captures exceed the 300 KB limit: {oversized}") + print(f"contact_sheet={sheet_path.name} bytes={sheet_path.stat().st_size} views=5 matched_pairs=5") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/audit.json b/Standalone/Research/WorldAssemblerM3/Round3/results/audit.json new file mode 100644 index 0000000..dbe450f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round3/results/audit.json @@ -0,0 +1,30 @@ +{ + "recipe_hash":"ef1340d32b072ba3", + "plan_hash":"61fd58f65c5fe157", + "manifest_hash":"54160b4d25d0279f", + "unchanged_existing_hashes":{"m1_round1_manifest":"c87ae279b0e26f53","surfaceworld_round4_manifest":"8933693d9560ba9f"}, + "geometry_version":2, + "owner_voxel_size_cm":12.5, + "core_voxel_size_cm":25, + "owner_to_core_length_scale":0.5, + "plan_islands":4, + "mid_height_open_sky":{"sampling_step_m":1.0,"z_core_voxels":-964.0390625,"bounds_xy_core_voxels":[-45.58203125,-107.33984375,99.73828125,62.82421875],"open_samples":1462,"total_samples":1591,"open_fraction":0.918918918919}, + "plan_connectors":3, + "route_points":29, + "legacy_anchor_owner_voxels":[48,16,-1904.03515625], + "comparison_anchor_core_voxels":[24,8,-952.015625], + "legacy_anchor_plateau_samples":25, + "recipe_parameters":{"seed":0,"primary_island_count":3,"density_q16":32768,"spacing_voxels":56,"radius_voxels":[12,24],"thickness_ratio_q16":39322,"vertical_jitter_q16":39322,"top_flatten_q16":39322,"contour_warp_voxels":6.30078125,"crust_depth_bonus_q16":9830,"root_depth_ratio_q16":49152,"roughness_voxels":1,"blend_voxels":3,"bridge_half_width_voxels":2.5,"bridge_thickness_voxels":4}, + "stable_island_ids":["e6e461e18b3a9c87","2bc1820baba047f6","adc259cbe2a395e9","7855fb022cc561c9"], + "stable_connector_ids":["eaa5dd310e846520","034023599b466c51","4962fa885545fb26"], + "island_profiles":[{"id":"e6e461e18b3a9c87","ordinal":0,"center_voxels":[-15.19921875,25.2734375,-960.5078125],"radius_voxels":14.08203125,"top_voxels":-957.69140625,"bottom_voxels":-978.81640625,"depth_to_diameter":0.750069348128},{"id":"2bc1820baba047f6","ordinal":1,"center_voxels":[24,-12.2265625,-954.8984375],"radius_voxels":14.42578125,"top_voxels":-952.015625,"bottom_voxels":-973.65625,"depth_to_diameter":0.75006769564},{"id":"adc259cbe2a395e9","ordinal":2,"center_voxels":[63.19921875,26.28515625,-955.57421875],"radius_voxels":20.23828125,"top_voxels":-951.52734375,"bottom_voxels":-981.88671875,"depth_to_diameter":0.750048253233},{"id":"7855fb022cc561c9","ordinal":3,"center_voxels":[-1.19921875,-68.2265625,-950.75390625],"radius_voxels":22.8125,"top_voxels":-946.19140625,"bottom_voxels":-980.41015625,"depth_to_diameter":0.75}], + "bounds":{"passed":true,"negative_control_detected":true,"boxes":515,"samples":13851,"unknown":72,"missed":0,"island_boxes":4,"connector_boxes":3,"void_boxes_proved_air":1}, + "thickness":{"passed":true,"islands_tested":4,"islands_below_minimum":0,"minimum_depth_to_diameter_ratio":0.75,"required_depth_to_diameter_ratio":0.7,"minimum_island_id":"7855fb022cc561c9"}, + "thin_pad_negative_control":{"detected":true,"failed_islands":4,"minimum_depth_to_diameter_ratio":0.299891686975}, + "route":{"passed":true,"segments":28,"centers":253,"capsule_samples":68816,"blocked":0,"support_samples":1344,"unsupported":0,"grade_failures":0,"minimum_clearance_subvoxels":99}, + "raised_route_negative_control":{"detected":true,"passed":false,"unsupported":253,"blocked":0}, + "determinism":{"W1":"5764b702482740e8","W8":"5764b702482740e8","W16":"5764b702482740e8","plan_repeat":true,"hash_samples":32768}, + "timing":{"same_queries":32768,"legacy_ms":64.3305,"native_ms":46.2981,"native_over_legacy":0.71969128174,"sign_mismatches":1473,"sign_mismatch_percent":4.49523925781,"legacy_solid_samples":100,"native_solid_samples":1399,"legacy_only_solid_samples":87,"native_only_solid_samples":1386}, + "mesh":[{"name":"cluster_legacy","grid_samples":34686,"triangles":1512,"bytes":72584},{"name":"cluster_native","grid_samples":34686,"triangles":8312,"bytes":398984},{"name":"detail_legacy","grid_samples":207025,"triangles":4124,"bytes":197960},{"name":"detail_native","grid_samples":207025,"triangles":26122,"bytes":1253864}], + "route_points_core_voxels":[[-15.19921875,25.2734375,-953.48046875],[-12.3984375,22.59375,-953.57421875],[-9.59765625,19.91796875,-952.78515625],[-6.80078125,17.23828125,-952.28515625],[-4,14.55859375,-951.765625],[-1.19921875,11.87890625,-951.25],[1.6015625,9.203125,-950.66015625],[4.40234375,6.5234375,-950.12890625],[7.19921875,3.84375,-949.6015625],[10,1.16796875,-948.97265625],[12.80078125,-1.51171875,-948.3203125],[15.6015625,-4.19140625,-947.796875],[18.3984375,-6.87109375,-947.4375],[21.19921875,-9.546875,-947.9296875],[24,-12.2265625,-947.890625],[26.80078125,-9.4765625,-948.04296875],[29.6015625,-6.7265625,-947.046875],[32.3984375,-3.97265625,-947.21875],[35.19921875,-1.22265625,-947.36328125],[38,1.52734375,-947.37890625],[40.80078125,4.27734375,-947.3125],[43.6015625,7.03125,-947.15234375],[46.3984375,9.78125,-947.1953125],[49.19921875,12.53125,-947.16015625],[52,15.28125,-947.03125],[54.80078125,18.03125,-946.94921875],[57.59765625,20.78515625,-947.55078125],[60.3984375,23.53515625,-947.46875],[63.19921875,26.28515625,-947.34765625]] +} diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/captures/contact_sheet.jpg b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/contact_sheet.jpg new file mode 100644 index 0000000..41910b1 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/contact_sheet.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/captures/edge_cliff_matched.jpg b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/edge_cliff_matched.jpg new file mode 100644 index 0000000..99d77e6 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/edge_cliff_matched.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/captures/overview_matched.jpg b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/overview_matched.jpg new file mode 100644 index 0000000..6bb7137 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/overview_matched.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/captures/player_eye_matched.jpg b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/player_eye_matched.jpg new file mode 100644 index 0000000..7158412 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/player_eye_matched.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/captures/route_matched.jpg b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/route_matched.jpg new file mode 100644 index 0000000..c7deaa5 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/route_matched.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/captures/underside_matched.jpg b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/underside_matched.jpg new file mode 100644 index 0000000..2cf2db6 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round3/results/captures/underside_matched.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/cluster_legacy.vfmesh b/Standalone/Research/WorldAssemblerM3/Round3/results/cluster_legacy.vfmesh new file mode 100644 index 0000000..05e1d87 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round3/results/cluster_legacy.vfmesh differ diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/cluster_native.vfmesh b/Standalone/Research/WorldAssemblerM3/Round3/results/cluster_native.vfmesh new file mode 100644 index 0000000..31fcb88 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round3/results/cluster_native.vfmesh differ diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/detail_legacy.vfmesh b/Standalone/Research/WorldAssemblerM3/Round3/results/detail_legacy.vfmesh new file mode 100644 index 0000000..e12a387 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round3/results/detail_legacy.vfmesh differ diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/detail_native.vfmesh b/Standalone/Research/WorldAssemblerM3/Round3/results/detail_native.vfmesh new file mode 100644 index 0000000..aba4f7e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Round3/results/detail_native.vfmesh differ diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/floating_island_manifest.json b/Standalone/Research/WorldAssemblerM3/Round3/results/floating_island_manifest.json new file mode 100644 index 0000000..88ee29f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round3/results/floating_island_manifest.json @@ -0,0 +1 @@ +{"schema_version":1,"content_hash":"54160b4d25d0279f","recipe":{"schema_version":1,"units_per_voxel":256,"owner_voxel_size_microns":125000,"core_voxel_size_microns":250000,"owner_to_core_scale_numerator":1,"owner_to_core_scale_denominator":2,"seed":0,"origin_x_q":6144,"origin_y_q":2048,"center_top_z_q":-243716,"layout_min_z_q":-264196,"layout_max_z_q":-231428,"primary_island_count":3,"island_density_q16":32768,"spacing_q":14336,"minimum_radius_q":3072,"maximum_radius_q":6144,"thickness_ratio_q16":39322,"vertical_jitter_q16":39322,"top_flatten_q16":39322,"taper_end_min_q16":29491,"taper_end_max_q16":45875,"geometry_version":2,"crust_depth_bonus_q16":9830,"root_depth_ratio_q16":49152,"blend_radius_q":768,"contour_warp_q":1613,"roughness_q":256,"boundary_seal_q":512,"bridge_half_width_q":640,"bridge_thickness_q":1024,"route_constraints":{"version":1,"capsule_radius_q":348,"capsule_height_q":1802,"maximum_grade_q16":17560,"sweep_spacing_q":128,"support_probe_radius_q":230,"support_search_depth_q":512,"minimum_clearance_q":128,"minimum_support_probes":3},"content_hash":"ef1340d32b072ba3"},"plan":{"content_hash":"61fd58f65c5fe157","islands":[{"stable_id":"e6e461e18b3a9c87","ordinal":0,"center_q":[-3891,6470,-245890],"radius_q":3605,"top_half_q":721,"top_z_q":-245169,"bottom_z_q":-250577,"taper_end_q16":41663,"bounds_min_q":[-11669,-1308,-253137],"bounds_max_q":[3887,14248,-242609]},{"stable_id":"2bc1820baba047f6","ordinal":1,"center_q":[6144,-3130,-244454],"radius_q":3693,"top_half_q":738,"top_z_q":-243716,"bottom_z_q":-249256,"taper_end_q16":33506,"bounds_min_q":[-1722,-10996,-251816],"bounds_max_q":[14010,4736,-241156]},{"stable_id":"adc259cbe2a395e9","ordinal":2,"center_q":[16179,6729,-244627],"radius_q":5181,"top_half_q":1036,"top_z_q":-243591,"bottom_z_q":-251363,"taper_end_q16":39164,"bounds_min_q":[6825,-2625,-253923],"bounds_max_q":[25533,16083,-241031]},{"stable_id":"7855fb022cc561c9","ordinal":3,"center_q":[-307,-17466,-243393],"radius_q":5840,"top_half_q":1168,"top_z_q":-242225,"bottom_z_q":-250985,"taper_end_q16":33440,"bounds_min_q":[-10320,-27479,-253545],"bounds_max_q":[9706,-7453,-239665]}],"cluster_edges":[{"stable_id":"eaa5dd310e846520","from_island_id":"e6e461e18b3a9c87","to_island_id":"2bc1820baba047f6","start_q":[-2536,5174,-245089],"end_q":[4756,-1803,-243568],"half_width_q":640,"thickness_q":1024,"bounds_min_q":[-4200,-3467,-249185],"bounds_max_q":[6420,6838,-242544],"sharp_tag_ready":true},{"stable_id":"034023599b466c51","from_island_id":"2bc1820baba047f6","to_island_id":"adc259cbe2a395e9","start_q":[7514,-1784,-243592],"end_q":[14257,4841,-243505],"half_width_q":640,"thickness_q":1024,"bounds_min_q":[5850,-3448,-247688],"bounds_max_q":[15921,6505,-242481],"sharp_tag_ready":true},{"stable_id":"4962fa885545fb26","from_island_id":"2bc1820baba047f6","to_island_id":"7855fb022cc561c9","start_q":[5356,-4881,-243607],"end_q":[939,-14697,-242382],"half_width_q":640,"thickness_q":1024,"bounds_min_q":[-725,-16361,-247703],"bounds_max_q":[7020,-3217,-241358],"sharp_tag_ready":true}],"route":[{"feature_id":"2e805a1f1f778cbb","position_q":[-3891,6470,-244091],"radius_q":1024},{"feature_id":"5be3b2b1e01722ea","position_q":[-3174,5784,-244115],"radius_q":1024},{"feature_id":"ab4eda612c1f03b5","position_q":[-2457,5099,-243913],"radius_q":1024},{"feature_id":"ba2fd2b050d0dd8c","position_q":[-1741,4413,-243785],"radius_q":1024},{"feature_id":"fc5829ee129fdc4f","position_q":[-1024,3727,-243652],"radius_q":1024},{"feature_id":"1393bb00329a9d5e","position_q":[-307,3041,-243520],"radius_q":1024},{"feature_id":"1dabc9715c93e2c9","position_q":[410,2356,-243369],"radius_q":1024},{"feature_id":"0ea2008a85e73050","position_q":[1127,1670,-243233],"radius_q":1024},{"feature_id":"754f55a5f1465cf3","position_q":[1843,984,-243098],"radius_q":1024},{"feature_id":"1b6752776b4a7d22","position_q":[2560,299,-242937],"radius_q":1024},{"feature_id":"0ab3b704db76084d","position_q":[3277,-387,-242770],"radius_q":1024},{"feature_id":"bd6980911e0879c4","position_q":[3994,-1073,-242636],"radius_q":1024},{"feature_id":"ad164d7d0c049d87","position_q":[4710,-1759,-242544],"radius_q":1024},{"feature_id":"9661463acbada2d6","position_q":[5427,-2444,-242670],"radius_q":1024},{"feature_id":"280a96547fadb0a1","position_q":[6144,-3130,-242660],"radius_q":1024},{"feature_id":"531455b42fc13108","position_q":[6861,-2426,-242699],"radius_q":1024},{"feature_id":"55d04c20849d7fcb","position_q":[7578,-1722,-242444],"radius_q":1024},{"feature_id":"ac4ebe80860dbdfa","position_q":[8294,-1017,-242488],"radius_q":1024},{"feature_id":"ff942855bf8e3fc5","position_q":[9011,-313,-242525],"radius_q":1024},{"feature_id":"e6b8bba6c6acaf9c","position_q":[9728,391,-242529],"radius_q":1024},{"feature_id":"ae47379bbd4f7fdf","position_q":[10445,1095,-242512],"radius_q":1024},{"feature_id":"98779442384a30ee","position_q":[11162,1800,-242471],"radius_q":1024},{"feature_id":"f4c7757b2ee9fe19","position_q":[11878,2504,-242482],"radius_q":1024},{"feature_id":"3ea33fae0f75b9e0","position_q":[12595,3208,-242473],"radius_q":1024},{"feature_id":"38a8dfe9adb37e83","position_q":[13312,3912,-242440],"radius_q":1024},{"feature_id":"dfde44af59268bf2","position_q":[14029,4616,-242419],"radius_q":1024},{"feature_id":"0378552bc60e49dd","position_q":[14745,5321,-242573],"radius_q":1024},{"feature_id":"40dd5b33f799a194","position_q":[15462,6025,-242552],"radius_q":1024},{"feature_id":"fc2c5b613f6a5cd7","position_q":[16179,6729,-242521],"radius_q":1024}]}} diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/run_ledger.txt b/Standalone/Research/WorldAssemblerM3/Round3/results/run_ledger.txt new file mode 100644 index 0000000..f6bdc4a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round3/results/run_ledger.txt @@ -0,0 +1,12 @@ +attempt=1 status=passed elapsed_s=1.74 peak_private_MiB=45 exit=0 +Round 3 supplemental run ledger (2026-10-10): +PASS: M1 SurfaceWorld BuildHost after restoring the original 32-voxel mapping and 4.0 edge threshold; original worker hash preserved. +PASS: M2 HybridV1 and HybridV2 BuildHost fixtures. +PASS: M3 floating-island BuildHost on the root-parameter-cache source state; see m3_floating_islands_buildhost.*. Peak observed process memory about 3.44 GB. Tile hashes, coverage/LOD, collision/capsule contact, content anchors, height/strate/atmosphere/water agreement, Blueprint carve/fill, W1/W8/W16, and Core-off legacy checks passed. +PASS: Instrumented M3 commandlet after the root-cache change printed PASS and pass-complete=1; wrapper exited 1 because console-title channel failed with 0xE9. Timings are in REPORT.md. This is recorded as a wrapper failure as well as an internal commandlet pass. +FAIL: Island fixture attempts in order: desired tiles 2227/2553 (route player absent); origin pawn from GameMode; 2557 desired tiles after possession (dynamic collision support); support tile-key mismatch; collision gate off restored 2553 but hash differed at (-2,0,-31); CreateLocalPlayer(true) hung and was manually stopped after ~3–4 minutes; base UPlayer fixture lacked controller registration/root transform and failed preflight after 5:10. Corrected explicit controller/root/location fixture passed. +FAIL: M1 edge-threshold experiments: 4.0 -> 52075.827 > 51200 at LOD8 tile (2,0,-1); 4.125 -> 53960.427 > 52800; 4.25 -> 54806.919 > 54400; 4.5 passed edge bounds but yielded no full-span LOD0 candidate. Restoring the correct split chunk mapping and baseline 4.0 threshold passed. +FAIL: Earlier compile errors were corrected: DuplicateObject overload, private helper access, unresolved GetEffectiveWorldSeed export, and uint32-to-int32 narrowing. Initial standalone invocation with -Workers failed; -CompileWorkers 4 succeeded. +RETRY/ENVIRONMENT: One requested retry followed a 0xC0000142 shell-start failure; additional PowerShell/cmd inspection launches also failed with 0xC0000142. No subsequent BuildHost attempt was made. No 0xC0000142 BuildHost retry occurred during the completed runs. +CRASH CHECK: No new UECC-* folder reported; only historical folders dated 2026-09-17 and 2026-10-08. The CreateLocalPlayer hang was manually stopped, not a crash. Latest BuildHost test peak was below 4 GB; all completed test/build steps stayed within 15 minutes. +CURRENT TREE CAVEAT: XY-column cache was edited after the passing/profiled BuildHost run. It remains uncompiled and unmeasured due shell startup failures; current tree is not verified until that edit is built/tested or reverted. diff --git a/Standalone/Research/WorldAssemblerM3/Round3/results/world_assembly_floating_islands.json b/Standalone/Research/WorldAssemblerM3/Round3/results/world_assembly_floating_islands.json new file mode 100644 index 0000000..1b92aae --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round3/results/world_assembly_floating_islands.json @@ -0,0 +1 @@ +{"schema_version":1,"content_hash":"94b563f1ce622def","recipe":{"schema_version":1,"evaluator_version":4,"units_per_voxel":256,"seed":0,"biome_cell_size_q":204800,"biome_warp_q":64000,"biome_warp_wavelength_q":142222,"biome_border_blend_q":30720,"climate_relief_wavelength_q":853333,"climate_moisture_wavelength_q":640000,"climate_relief_contrast_q16":131072,"content_hash":"c37de78b759c0a4c","biomes":[{"id":4097,"base_height_q":-516096,"continent_amplitude_q":128000,"continent_wavelength_q":256000,"mountain_amplitude_q":0,"mountain_wavelength_q":128000,"detail_amplitude_q":512,"detail_wavelength_q":6400,"relief_min_q16":0,"relief_max_q16":19661,"moisture_min_q16":0,"moisture_max_q16":19661,"relief_strength_q16":6554,"relief_contrast_q16":104858,"relief_wavelength_q":2560000,"height_warp_strength_q":12288,"height_warp_wavelength_q":32000,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":256000,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4098,"base_height_q":-484639,"continent_amplitude_q":128000,"continent_wavelength_q":256000,"mountain_amplitude_q":128000,"mountain_wavelength_q":512000,"detail_amplitude_q":512,"detail_wavelength_q":25600,"relief_min_q16":52429,"relief_max_q16":65536,"moisture_min_q16":52429,"moisture_max_q16":65536,"relief_strength_q16":45875,"relief_contrast_q16":104858,"relief_wavelength_q":170667,"height_warp_strength_q":12288,"height_warp_wavelength_q":25600,"beach_width_q":3072,"cliff_strength_q16":65536,"cliff_slope_threshold_q16":19661,"cliff_sharpness_q16":327680,"cliff_sample_distance_q":5120,"cliff_max_displacement_q":2570240,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":230400,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4099,"base_height_q":-516096,"continent_amplitude_q":20480,"continent_wavelength_q":85333,"mountain_amplitude_q":0,"mountain_wavelength_q":128000,"detail_amplitude_q":512,"detail_wavelength_q":6400,"relief_min_q16":6554,"relief_max_q16":26214,"moisture_min_q16":32768,"moisture_max_q16":52429,"relief_strength_q16":0,"relief_contrast_q16":16384,"relief_wavelength_q":170752,"height_warp_strength_q":16640,"height_warp_wavelength_q":32000,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":256000,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800},{"id":4100,"base_height_q":-385024,"continent_amplitude_q":20480,"continent_wavelength_q":256000,"mountain_amplitude_q":20480,"mountain_wavelength_q":512000,"detail_amplitude_q":512,"detail_wavelength_q":25600,"relief_min_q16":64881,"relief_max_q16":65536,"moisture_min_q16":52429,"moisture_max_q16":65536,"relief_strength_q16":45875,"relief_contrast_q16":104858,"relief_wavelength_q":170667,"height_warp_strength_q":12288,"height_warp_wavelength_q":25600,"beach_width_q":3072,"cliff_strength_q16":0,"cliff_slope_threshold_q16":32768,"cliff_sharpness_q16":131072,"cliff_sample_distance_q":512,"cliff_max_displacement_q":0,"cap_enabled":true,"cap_height_q":-149094,"cap_warp_strength_q":0,"cap_warp_wavelength_q":25600,"cap_undulation_q":230400,"cap_wavelength_q":1280000,"cap_detail_amplitude_q":2560,"cap_detail_wavelength_q":25600,"cap_ridge_amplitude_q":0,"cap_ridge_wavelength_q":12800}]},"plan":{"schema_version":1,"content_hash":"89cd31291d2aefa4","strates":[]},"floating_islands":[{"strate_index":1,"top_chunk_z":-16,"bottom_chunk_z":-79,"manifest":{"schema_version":1,"content_hash":"54160b4d25d0279f","recipe":{"schema_version":1,"units_per_voxel":256,"owner_voxel_size_microns":125000,"core_voxel_size_microns":250000,"owner_to_core_scale_numerator":1,"owner_to_core_scale_denominator":2,"seed":0,"origin_x_q":6144,"origin_y_q":2048,"center_top_z_q":-243716,"layout_min_z_q":-264196,"layout_max_z_q":-231428,"primary_island_count":3,"island_density_q16":32768,"spacing_q":14336,"minimum_radius_q":3072,"maximum_radius_q":6144,"thickness_ratio_q16":39322,"vertical_jitter_q16":39322,"top_flatten_q16":39322,"taper_end_min_q16":29491,"taper_end_max_q16":45875,"geometry_version":2,"crust_depth_bonus_q16":9830,"root_depth_ratio_q16":49152,"blend_radius_q":768,"contour_warp_q":1613,"roughness_q":256,"boundary_seal_q":512,"bridge_half_width_q":640,"bridge_thickness_q":1024,"route_constraints":{"version":1,"capsule_radius_q":348,"capsule_height_q":1802,"maximum_grade_q16":17560,"sweep_spacing_q":128,"support_probe_radius_q":230,"support_search_depth_q":512,"minimum_clearance_q":128,"minimum_support_probes":3},"content_hash":"ef1340d32b072ba3"},"plan":{"content_hash":"61fd58f65c5fe157","islands":[{"stable_id":"e6e461e18b3a9c87","ordinal":0,"center_q":[-3891,6470,-245890],"radius_q":3605,"top_half_q":721,"top_z_q":-245169,"bottom_z_q":-250577,"taper_end_q16":41663,"bounds_min_q":[-11669,-1308,-253137],"bounds_max_q":[3887,14248,-242609]},{"stable_id":"2bc1820baba047f6","ordinal":1,"center_q":[6144,-3130,-244454],"radius_q":3693,"top_half_q":738,"top_z_q":-243716,"bottom_z_q":-249256,"taper_end_q16":33506,"bounds_min_q":[-1722,-10996,-251816],"bounds_max_q":[14010,4736,-241156]},{"stable_id":"adc259cbe2a395e9","ordinal":2,"center_q":[16179,6729,-244627],"radius_q":5181,"top_half_q":1036,"top_z_q":-243591,"bottom_z_q":-251363,"taper_end_q16":39164,"bounds_min_q":[6825,-2625,-253923],"bounds_max_q":[25533,16083,-241031]},{"stable_id":"7855fb022cc561c9","ordinal":3,"center_q":[-307,-17466,-243393],"radius_q":5840,"top_half_q":1168,"top_z_q":-242225,"bottom_z_q":-250985,"taper_end_q16":33440,"bounds_min_q":[-10320,-27479,-253545],"bounds_max_q":[9706,-7453,-239665]}],"cluster_edges":[{"stable_id":"eaa5dd310e846520","from_island_id":"e6e461e18b3a9c87","to_island_id":"2bc1820baba047f6","start_q":[-2536,5174,-245089],"end_q":[4756,-1803,-243568],"half_width_q":640,"thickness_q":1024,"bounds_min_q":[-4200,-3467,-249185],"bounds_max_q":[6420,6838,-242544],"sharp_tag_ready":true},{"stable_id":"034023599b466c51","from_island_id":"2bc1820baba047f6","to_island_id":"adc259cbe2a395e9","start_q":[7514,-1784,-243592],"end_q":[14257,4841,-243505],"half_width_q":640,"thickness_q":1024,"bounds_min_q":[5850,-3448,-247688],"bounds_max_q":[15921,6505,-242481],"sharp_tag_ready":true},{"stable_id":"4962fa885545fb26","from_island_id":"2bc1820baba047f6","to_island_id":"7855fb022cc561c9","start_q":[5356,-4881,-243607],"end_q":[939,-14697,-242382],"half_width_q":640,"thickness_q":1024,"bounds_min_q":[-725,-16361,-247703],"bounds_max_q":[7020,-3217,-241358],"sharp_tag_ready":true}],"route":[{"feature_id":"2e805a1f1f778cbb","position_q":[-3891,6470,-244091],"radius_q":1024},{"feature_id":"5be3b2b1e01722ea","position_q":[-3174,5784,-244115],"radius_q":1024},{"feature_id":"ab4eda612c1f03b5","position_q":[-2457,5099,-243913],"radius_q":1024},{"feature_id":"ba2fd2b050d0dd8c","position_q":[-1741,4413,-243785],"radius_q":1024},{"feature_id":"fc5829ee129fdc4f","position_q":[-1024,3727,-243652],"radius_q":1024},{"feature_id":"1393bb00329a9d5e","position_q":[-307,3041,-243520],"radius_q":1024},{"feature_id":"1dabc9715c93e2c9","position_q":[410,2356,-243369],"radius_q":1024},{"feature_id":"0ea2008a85e73050","position_q":[1127,1670,-243233],"radius_q":1024},{"feature_id":"754f55a5f1465cf3","position_q":[1843,984,-243098],"radius_q":1024},{"feature_id":"1b6752776b4a7d22","position_q":[2560,299,-242937],"radius_q":1024},{"feature_id":"0ab3b704db76084d","position_q":[3277,-387,-242770],"radius_q":1024},{"feature_id":"bd6980911e0879c4","position_q":[3994,-1073,-242636],"radius_q":1024},{"feature_id":"ad164d7d0c049d87","position_q":[4710,-1759,-242544],"radius_q":1024},{"feature_id":"9661463acbada2d6","position_q":[5427,-2444,-242670],"radius_q":1024},{"feature_id":"280a96547fadb0a1","position_q":[6144,-3130,-242660],"radius_q":1024},{"feature_id":"531455b42fc13108","position_q":[6861,-2426,-242699],"radius_q":1024},{"feature_id":"55d04c20849d7fcb","position_q":[7578,-1722,-242444],"radius_q":1024},{"feature_id":"ac4ebe80860dbdfa","position_q":[8294,-1017,-242488],"radius_q":1024},{"feature_id":"ff942855bf8e3fc5","position_q":[9011,-313,-242525],"radius_q":1024},{"feature_id":"e6b8bba6c6acaf9c","position_q":[9728,391,-242529],"radius_q":1024},{"feature_id":"ae47379bbd4f7fdf","position_q":[10445,1095,-242512],"radius_q":1024},{"feature_id":"98779442384a30ee","position_q":[11162,1800,-242471],"radius_q":1024},{"feature_id":"f4c7757b2ee9fe19","position_q":[11878,2504,-242482],"radius_q":1024},{"feature_id":"3ea33fae0f75b9e0","position_q":[12595,3208,-242473],"radius_q":1024},{"feature_id":"38a8dfe9adb37e83","position_q":[13312,3912,-242440],"radius_q":1024},{"feature_id":"dfde44af59268bf2","position_q":[14029,4616,-242419],"radius_q":1024},{"feature_id":"0378552bc60e49dd","position_q":[14745,5321,-242573],"radius_q":1024},{"feature_id":"40dd5b33f799a194","position_q":[15462,6025,-242552],"radius_q":1024},{"feature_id":"fc2c5b613f6a5cd7","position_q":[16179,6729,-242521],"radius_q":1024}]}}}]} diff --git a/Standalone/Source/OwnerWorldLoader.h b/Standalone/Source/OwnerWorldLoader.h index 549e864..ca3f8b7 100644 --- a/Standalone/Source/OwnerWorldLoader.h +++ b/Standalone/Source/OwnerWorldLoader.h @@ -299,6 +299,17 @@ namespace StandaloneOwnerLoader Error = "export voxel_size_cm does not match the standalone VOXEL_SIZE"; return false; } + const VFJson::Value& OwnerVoxelSize = Root.Get("owner_voxel_size_cm"); + const VFJson::Value& CoreVoxelSize = Root.Get("core_voxel_size_cm"); + const VFJson::Value& OwnerToCoreScale = Root.Get("owner_to_core_length_scale"); + if (!std::holds_alternative(OwnerVoxelSize.Data) + && (!OwnerVoxelSize.IsNumber() || OwnerVoxelSize.Number() != 12.5 + || !CoreVoxelSize.IsNumber() || CoreVoxelSize.Number() != 25.0 + || !OwnerToCoreScale.IsNumber() || OwnerToCoreScale.Number() != 0.5)) + { + Error = "owner-to-Core import scale must be 12.5 cm to 25 cm at a 0.5 length ratio"; + return false; + } Out.Seed = static_cast(Root.Get("seed").Integer()); Out.ClipRadius = static_cast(Root.Get("clip_radius").Integer(6)); Out.MaxClipLevel = static_cast(Root.Get("max_clip_level").Integer(8)); diff --git a/Standalone/Source/VoxelForge/Public/VoxelStrateTypes.h b/Standalone/Source/VoxelForge/Public/VoxelStrateTypes.h index 5578cf4..a7464f3 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelStrateTypes.h +++ b/Standalone/Source/VoxelForge/Public/VoxelStrateTypes.h @@ -1741,7 +1741,8 @@ struct VOXELFORGE_API FFloatingIslandParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0", ClampMax = "1.0")) float IslandDensity = 0.5f; - // Island horizontal radius range (voxels): 12-24 m landmass diameters by default. + // Owner scale: 24–48 legacy voxels = 3–6 m radius at 12.5 cm/voxel (6–12 m diameter). + // Echelle proprietaire : 24–48 voxels legacy = rayon de 3–6 m a 12,5 cm/voxel (diametre 6–12 m). UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "2.0")) float IslandMinRadius = 24.0f; @@ -1765,7 +1766,8 @@ struct VOXELFORGE_API FFloatingIslandParams UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0")) float SurfaceRoughness = 3.0f; - // SmoothMin blend radius for merging overlapping islands (voxels): 1.5 m by default. + // SmoothMin blend radius: 6 legacy voxels = 0.75 m at the owner's 12.5 cm scale. + // Rayon de fusion SmoothMin : 6 voxels legacy = 0,75 m a l'echelle proprietaire de 12,5 cm. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Islands", meta = (ClampMin = "0.0")) float SDFBlendRadius = 6.0f; diff --git a/Tools/BuildVoxelForgeCoreHost.ps1 b/Tools/BuildVoxelForgeCoreHost.ps1 index 20fcf6f..aa314fc 100644 --- a/Tools/BuildVoxelForgeCoreHost.ps1 +++ b/Tools/BuildVoxelForgeCoreHost.ps1 @@ -130,11 +130,11 @@ $buildSucceeded = $false for ($attempt = 1; $attempt -le 2 -and -not $buildSucceeded; ++$attempt) { $attemptStdout = if ($attempt -eq 1) { $BuildStdout } else { Join-Path $LogRoot 'UBT-CoreIntegration.retry.stdout.log' } $attemptStderr = if ($attempt -eq 1) { $BuildStderr } else { Join-Path $LogRoot 'UBT-CoreIntegration.retry.stderr.log' } - Write-Host "BuildHost UBT attempt $attempt of 2; max parallel actions=$MaxParallelActions; normal priority." + Write-Host "BuildHost UBT attempt $attempt of 2; max parallel actions=$MaxParallelActions; BelowNormal priority." $process = Start-Process -FilePath $dotnet -ArgumentList $argumentList -WorkingDirectory $PluginRoot ` -RedirectStandardOutput $attemptStdout -RedirectStandardError $attemptStderr ` -WindowStyle Hidden -PassThru - $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal + $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal $started = [datetime]::UtcNow while (-not $process.HasExited) { Start-Sleep -Seconds 5 diff --git a/Tools/TestVoxelForgeCoreWorldHost.ps1 b/Tools/TestVoxelForgeCoreWorldHost.ps1 index 3742686..33ac887 100644 --- a/Tools/TestVoxelForgeCoreWorldHost.ps1 +++ b/Tools/TestVoxelForgeCoreWorldHost.ps1 @@ -5,7 +5,8 @@ param( [ValidateSet('MarchingCubesV1', 'WorldAssemblerHybridV1', 'WorldAssemblerHybridV2')][string]$CoreMesherRecipe = 'MarchingCubesV1', [switch]$EnableHybridFixturePlacement, [switch]$HybridCliffOnly, - [switch]$HybridFixturesOnly + [switch]$HybridFixturesOnly, + [switch]$FloatingIslandsOnly ) # Exercise AVoxelWorld Core streaming, collision, anchor movement, and carving in BuildHost only. @@ -41,7 +42,11 @@ function Assert-UnderPath([string]$Candidate, [string]$Root) { Assert-UnderPath $HostRoot (Join-Path $PluginRoot 'Saved\BuildHost') Assert-UnderPath $OutputRoot (Join-Path $StagePlugin 'Saved') if ([string]::IsNullOrWhiteSpace($WorldAssemblyManifestPath)) { - $WorldAssemblyManifestPath = Join-Path $PluginRoot 'Standalone\Research\WorldAssemblerM1\round4\world_assembly.json' + $WorldAssemblyManifestPath = if ($FloatingIslandsOnly) { + Join-Path $PluginRoot 'Standalone\Research\WorldAssemblerM3\Round3\results\world_assembly_floating_islands.json' + } else { + Join-Path $PluginRoot 'Standalone\Research\WorldAssemblerM1\round4\world_assembly.json' + } } $WorldAssemblyManifestPath = [IO.Path]::GetFullPath($WorldAssemblyManifestPath) Assert-UnderPath $WorldAssemblyManifestPath $PluginRoot @@ -84,7 +89,13 @@ foreach ($directory in @($OutputDirectory, $InputDirectory, $LogDirectory, $Loca } Copy-Item -LiteralPath $WorldAssemblyManifestPath -Destination $StagedWorldAssembly -Force $ManifestData = Get-Content -LiteralPath $StagedWorldAssembly -Raw | ConvertFrom-Json -if ($ManifestData.content_hash -ne '8933693d9560ba9f') { +if ($FloatingIslandsOnly) { + if ($ManifestData.content_hash -ne '94b563f1ce622def' -or + @($ManifestData.floating_islands).Count -ne 1 -or + @($ManifestData.plan.strates).Count -ne 0) { + throw "BuildHost world assembly input is not the reviewed round-3 island manifest: $($ManifestData.content_hash)" + } +} elseif ($ManifestData.content_hash -ne '8933693d9560ba9f') { throw "BuildHost world assembly input is not the reviewed round-4 manifest: $($ManifestData.content_hash)" } if ($EnableHybridFixturePlacement -and $CoreMesherRecipe -eq 'MarchingCubesV1') { @@ -102,6 +113,10 @@ if ($HybridFixturesOnly -and $CoreMesherRecipe -eq 'MarchingCubesV1') { if ($HybridFixturesOnly -and ($HybridCliffOnly -or $EnableHybridFixturePlacement)) { throw '-HybridFixturesOnly cannot be combined with the other focused hybrid modes.' } +if ($FloatingIslandsOnly -and ($CoreMesherRecipe -ne 'MarchingCubesV1' -or + $EnableHybridFixturePlacement -or $HybridCliffOnly -or $HybridFixturesOnly)) { + throw '-FloatingIslandsOnly uses the reviewed MC island manifest and cannot be combined with hybrid modes.' +} $CrashBefore = @{} foreach ($CrashRoot in $CrashRoots) { @@ -134,6 +149,7 @@ $HybridMesherFlag = switch ($CoreMesherRecipe) { $HybridFixturePlacementFlag = if ($EnableHybridFixturePlacement) { 1 } else { 0 } $HybridCliffOnlyFlag = if ($HybridCliffOnly) { 1 } else { 0 } $HybridFixturesOnlyFlag = if ($HybridFixturesOnly) { 1 } else { 0 } +$FloatingIslandsOnlyFlag = if ($FloatingIslandsOnly) { 1 } else { 0 } $BaseArguments = @( ('-project="{0}"' -f $HostProject), '-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound', @@ -144,6 +160,7 @@ $BaseArguments = @( ('-LocalDataCachePath="{0}"' -f $DerivedDataCache), '-run=VoxelForgeExplore', '-seed=0', '-archetype=TunnelNetwork', '-slot=0', '-modes=export', '-gameconfig=1', '-coreworldtest=1', + ("-coreislandtest=$FloatingIslandsOnlyFlag"), '-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings', ('-coreworldassembly="{0}"' -f $StagedWorldAssembly), ("-corehybridmesher=$HybridMesherFlag"), @@ -152,6 +169,11 @@ $BaseArguments = @( ("-corehybridfixturesonly=$HybridFixturesOnlyFlag"), ('-out="{0}"' -f $OutputDirectory) ) +if ($FloatingIslandsOnly) { + # Keep this stationary route fixture out of the movement gate; island/span cooked contacts are probed directly. + # Garder ce fixture immobile hors du gate de mouvement ; les contacts cuisines ile/pont sont verifies directement. + $BaseArguments += '-voxel.CollisionGate=0' +} $OldLocalAppData = $env:LOCALAPPDATA $env:LOCALAPPDATA = $LocalAppData @@ -171,11 +193,11 @@ try { $StdoutPath = Join-Path $LogDirectory $StdoutFileName $StderrPath = Join-Path $LogDirectory $StderrFileName $Arguments = @($BaseArguments) + ('-abslog="{0}"' -f $LogPath) - Write-Host "BuildHost Core actor commandlet attempt $Attempt of 2; normal priority." + Write-Host "BuildHost Core actor commandlet attempt $Attempt of 2; BelowNormal priority." $Process = Start-Process -FilePath $EditorCmd -ArgumentList $Arguments -WorkingDirectory $HostRoot ` -RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath ` -WindowStyle Hidden -PassThru - $Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal + $Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal $StartedUtc = [datetime]::UtcNow $PeakPrivateBytes = [int64]0 while (-not $Process.HasExited) { @@ -198,7 +220,8 @@ try { $ExitCode = $Process.ExitCode if ([string]::IsNullOrEmpty([string]$ExitCode) -and (Test-Path -LiteralPath $LogPath -PathType Leaf)) { $CompletedLog = Get-Content -LiteralPath $LogPath -Raw - if ($CompletedLog -match '\[VoxelForgeCoreWorldTest\] pass complete=1' -and + if (($CompletedLog -match '\[VoxelForgeCoreWorldTest\] pass complete=1' -or + $CompletedLog -match '\[VoxelForgeCoreIslandTest\] pass complete=1') -and $CompletedLog -match 'Commandlet VoxelForgeExploreCommandlet_\d+ finished execution \(result 0\)') { $ExitCode = 0 } elseif ($CompletedLog -match 'Commandlet VoxelForgeExploreCommandlet_\d+ finished execution \(result (-?\d+)\)') { @@ -230,6 +253,44 @@ if (-not (Test-Path -LiteralPath $FinalLog -PathType Leaf)) { $FinalLog = Join-Path $LogDirectory 'CoreWorldValidation.retry.log' } $Text = Get-Content -LiteralPath $FinalLog -Raw +if ($FloatingIslandsOnly) { + foreach ($Marker in @( + '[VoxelForgeCoreIslandTest] manifest_import=PASS', + '[VoxelForgeCoreIslandTest] standalone_reference=PASS', + '[VoxelForgeCoreIslandTest] worker_hash_matrix=1/8/16 stable=1', + '[VoxelForgeCoreIslandTest] pass phase=initial', + '[VoxelForgeCoreIslandTest] height_anchor_agreement=PASS', + '[VoxelForgeCoreIslandTest] content_anchor=PASS', + '[VoxelForgeCoreIslandTest] blueprint_edit=PASS', + '[VoxelForgeCoreIslandTest] switch_off_legacy=PASS', + '[VoxelForgeCoreIslandTest] pass complete=1', + '[VoxelForgePawnSpeedFixture] source=staged_blueprints walk_cm_s=600.000 sprint_cm_s=6000.000' + )) { + if ($Text.IndexOf($Marker, [StringComparison]::Ordinal) -lt 0) { + throw "BuildHost floating-island log is missing '$Marker'. See $FinalLog." + } + } + foreach ($Surface in @('island_top', 'span')) { + if ($Text -notmatch ('\[VoxelForgeCoreIslandTest\] capsule_contact=PASS surface=' + + [regex]::Escape($Surface) + ' .*collision_ready=1')) { + throw "BuildHost island log is missing cooked capsule contact for '$Surface'. See $FinalLog." + } + } + foreach ($Lod in @(0, 1)) { + if ($Text -notmatch ('\[VoxelForgeIslandStreaming\] timing lod=' + $Lod + ' kind=.*air .*core_p95_ms=') -or + $Text -notmatch ('\[VoxelForgeIslandStreaming\] timing lod=' + $Lod + ' kind=.*(island|mixed|span) .*legacy_p95_ms=')) { + throw "BuildHost island log is missing matched air and surface timing at LOD$Lod. See $FinalLog." + } + } + $NewCrashes = @(Get-NewCrashFolders) + if ($NewCrashes.Count -gt 0) { + throw "New UECC crash folders appeared during the island actor test: $($NewCrashes -join '; ')" + } + Write-Host "VOXELFORGE_CORE_FLOATING_ISLANDS_PASS manifest=$($ManifestData.content_hash) hashes=standalone water_fixed=1 lod=0,1 workers=1,8,16" + Write-Host "Core actor log: $FinalLog" + Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes" + return +} if ($HybridFixturesOnly) { foreach ($Fixture in @('corner90', 'doorway', 'riser1m', 'cliff_ledge', 'organic_cave_chamber')) { $FixtureMarker = '\[WorldAssemblerHybridPluginFixture\] name=' +