wip(unreal): AVoxelWorld Core mode -- streaming, collision and carving through VoxelForgeCore
Luna, integration round 2 (BuildHost only): AVoxelWorld::bUseVoxelForgeCore (default false) routes desired tiles, generation, upload/collision and carving through Core. Core-only CoreClipRadius=3 / CoreMaxClipLevel=8 (owner's asset untouched); 25 cm Core voxels in every cm<->voxel conversion; legacy brush sizes scaled to keep physical size; UE-task JobScheduler, epoch-checked Mpsc results, existing RMC upload + collision cook; carving via sequenced applyEditTransaction. Switch off: BuildDesiredTiles takes the old path unchanged. BuildHost proof (actor commandlet, staged copies of the owner's assets): all 2,226 initial tiles hash-match standalone Core; collision bodies as expected; anchor move requests 1 / releases 1 tile; carved tile hash matches standalone; peak ~2.68 GiB. Legacy CrossPlatformDigest automation: 115,000 samples pass (wrapper flagged an analytics write outside isolation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
29ca99af7f
commit
075e58187e
7 files changed
+2069
-118
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -34,6 +34,18 @@ class UMaterialInstanceDynamic;
|
||||
class FScopedGenerationPause;
|
||||
class ACameraActor;
|
||||
namespace RealtimeMesh { struct FRealtimeMeshStreamSet; } // T1.f — worker-built geometry buffers
|
||||
class FVoxelForgeCoreRuntimeState;
|
||||
|
||||
struct FVoxelForgeCoreTileHashRecord
|
||||
{
|
||||
uint64 Density = 0;
|
||||
uint64 Mesh = 0;
|
||||
uint64 Payload = 0;
|
||||
uint64 Topology = 0;
|
||||
int32 Triangles = 0;
|
||||
bool bEmpty = true;
|
||||
bool bHasHashes = false;
|
||||
};
|
||||
|
||||
/** Per-request cancellation state. The game thread flips the flag when a pending tile leaves the
|
||||
* desired set; workers only read it and return an aborted result through the normal MPSC queue. */
|
||||
@@ -141,6 +153,11 @@ struct FChunkResult
|
||||
// section for a non-empty polygroup — configuring a missing one is invalid).
|
||||
bool bHasGroundTris = false;
|
||||
bool bHasCeilingTris = false;
|
||||
uint64 CoreDensityHash = 0;
|
||||
uint64 CoreMeshHash = 0;
|
||||
uint64 CorePayloadHash = 0;
|
||||
uint64 CoreTopologyHash = 0;
|
||||
bool bHasCoreHashes = false;
|
||||
// STRATE CONTENT CUT — the chunk-Z band this tile was MESHED with (MIN/MAX = uncut). The
|
||||
// apply path clamps its strate/material lookups into it (a coarse tile's raw min corner can
|
||||
// sit in a strate whose content was cut out of the mesh entirely).
|
||||
@@ -248,6 +265,25 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World")
|
||||
bool bManageAtmosphere = true;
|
||||
|
||||
/** Route terrain generation, streaming and edits through Standalone Core (opt-in). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (DisplayName = "Use VoxelForge Core"))
|
||||
bool bUseVoxelForgeCore = false;
|
||||
|
||||
/** Core-only clipmap radius, in level-0 Core tiles. Does not read Settings->ClipRadius. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (ClampMin = "1", ClampMax = "64"))
|
||||
int32 CoreClipRadius = 3;
|
||||
|
||||
/** Core-only maximum clip level. Does not read Settings->MaxClipLevel. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel World|Core", meta = (ClampMin = "0", ClampMax = "8"))
|
||||
int32 CoreMaxClipLevel = 8;
|
||||
|
||||
/** Commandlet-only identity capture; normal Core streaming leaves Core's hash path disabled. */
|
||||
bool bCaptureVoxelForgeCoreHashesForValidation = false;
|
||||
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> VoxelForgeCoreTileHashes;
|
||||
uint64 LastVoxelForgeCoreEditDigest = 0;
|
||||
uint64 LastVoxelForgeCoreEditSequence = 0;
|
||||
TArray<FVoxelTileKey> LastVoxelForgeCoreAffectedTiles;
|
||||
|
||||
//=========================================================================
|
||||
// CHUNK STORAGE
|
||||
//=========================================================================
|
||||
@@ -388,7 +424,8 @@ public:
|
||||
* Applies a spherical brush that subtracts density.
|
||||
*
|
||||
* @param Position - World-space center of the carve brush
|
||||
* @param Radius - Brush radius in voxels (default 6 voxels / 0.75 m at 12.5 cm per voxel)
|
||||
* @param Radius - Brush radius in legacy Runtime voxels (12.5 cm); Core converts it to
|
||||
* 25 cm voxels to preserve the physical brush size.
|
||||
* @param Strength - How aggressively to carve (default 10, higher = deeper)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||
@@ -399,7 +436,8 @@ public:
|
||||
* Applies a spherical brush that adds density.
|
||||
*
|
||||
* @param Position - World-space center of the fill brush
|
||||
* @param Radius - Brush radius in voxels (default 6 voxels / 0.75 m at 12.5 cm per voxel)
|
||||
* @param Radius - Brush radius in legacy Runtime voxels (12.5 cm); Core converts it to
|
||||
* 25 cm voxels to preserve the physical brush size.
|
||||
* @param Strength - How aggressively to fill (default 10, higher = more solid)
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||
@@ -407,7 +445,8 @@ public:
|
||||
|
||||
/**
|
||||
* Box brush carve/fill. Position is world-space (Unreal units); ExtentVoxels is the
|
||||
* box half-size in voxels. Strength magnitude controls aggressiveness (sign forced).
|
||||
* box half-size in legacy Runtime voxels (12.5 cm). Core converts it to 25 cm voxels.
|
||||
* Strength magnitude controls aggressiveness (sign forced).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||
void CarveBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
|
||||
@@ -416,8 +455,9 @@ public:
|
||||
void FillBox(FVector Position, FVector ExtentVoxels, float Strength = 12.0f);
|
||||
|
||||
/**
|
||||
* Capsule brush carve/fill between two world-space points (Unreal units), with a
|
||||
* tube radius in voxels. Great for boring tunnels or laying solid pillars/walls.
|
||||
* Capsule brush carve/fill between two world-space points (Unreal units), with a tube radius
|
||||
* in legacy Runtime voxels (12.5 cm). Core converts it to 25 cm voxels.
|
||||
* Great for boring tunnels or laying solid pillars/walls.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||
void CarveCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 6.0f, float Strength = 12.0f);
|
||||
@@ -426,8 +466,9 @@ public:
|
||||
void FillCapsule(FVector WorldA, FVector WorldB, float RadiusVoxels = 6.0f, float Strength = 12.0f);
|
||||
|
||||
/**
|
||||
* Apply a fully-specified modification (any shape). Centers/endpoints are expected
|
||||
* in VOXEL coordinates here (this is the low-level entry the helpers build on).
|
||||
* Apply a fully-specified modification (any shape). Centers/endpoints and brush dimensions
|
||||
* are expected in active voxel coordinates here (12.5 cm legacy, 25 cm Core); high-level
|
||||
* helpers convert legacy brush sizes to preserve physical dimensions.
|
||||
* Returns nothing; re-meshes affected chunks.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Voxel World|Modification")
|
||||
@@ -523,6 +564,11 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "Voxel World|Space")
|
||||
FVector LocalVoxelToWorld(FVector VoxelPos) const;
|
||||
|
||||
/** Core-route state accessors used by the isolated BuildHost commandlet. */
|
||||
bool IsVoxelForgeCoreReady() const;
|
||||
FVector GetVoxelForgeCoreDefaultCenterWorld() const;
|
||||
bool QueryVoxelForgeCoreDensityAt(FVector WorldLocation, float& OutDensity) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Voxel World|Strate")
|
||||
int32 GetStrateAtPosition(FVector WorldPosition) const;
|
||||
|
||||
@@ -601,6 +647,20 @@ private:
|
||||
/** Put all terrain MIDs into the material's neutral, volume-disabled state. */
|
||||
void SetDensityVolumeMaterialParamsDisabled();
|
||||
|
||||
/** Runtime Core snapshot, world lifetime, and worker-side streaming bridge. */
|
||||
bool InitializeVoxelForgeCoreWorld();
|
||||
void BuildCoreDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition,
|
||||
APawn* PlayerPawn, const FVector& PlayerHeading,
|
||||
TArray<FVoxelTileKey>& OutLeavers);
|
||||
void StartPendingCoreRegionCompile();
|
||||
void LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority);
|
||||
void ApplyCoreModification(const FVoxelModification& Modification);
|
||||
void BuildDesiredTransitionIndex();
|
||||
float GetActiveVoxelSizeCm() const;
|
||||
float GetLegacyVoxelToActiveVoxelScale() const;
|
||||
FIntVector GetTileCoordAtLocalCm(const FVector& LocalPositionCm) const;
|
||||
FIntVector GetTileCoordAtVoxelPosition(const FVector& VoxelPosition) const;
|
||||
|
||||
/** Explicitly opt-in headless streaming validation. This is a command-line-only harness path;
|
||||
* normal gameplay never moves the pawn here. It supplies a repeatable moving session and lets
|
||||
* -nullrhi request a graceful EndPlay so the clean latency summary is flushed. */
|
||||
@@ -652,6 +712,12 @@ private:
|
||||
// UpdateOrbLightMPC, which deliberately rewrites every frame).
|
||||
TWeakObjectPtr<UVolumeTexture> LastBoundVolTex0; // re-push MIDs if the L0 texture was recreated
|
||||
|
||||
TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> VoxelForgeCoreState;
|
||||
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
|
||||
bool bCoreRegionCompilePending = false;
|
||||
TArray<FVoxelTileKey> PendingCoreRegionTiles;
|
||||
TArray<FVoxelTileKey> PendingCoreRetainedTiles;
|
||||
|
||||
public:
|
||||
|
||||
//=========================================================================
|
||||
@@ -717,7 +783,7 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush")
|
||||
FVector EditorBrushCenter = FVector::ZeroVector;
|
||||
|
||||
/** Brush radius in voxels for the editor buttons. */
|
||||
/** Brush radius in legacy Runtime voxels; Core helpers preserve the same physical radius. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editor Brush", meta = (ClampMin = "0.5"))
|
||||
float EditorBrushRadius = 1.5f * 100.0f / VOXEL_SIZE;
|
||||
|
||||
|
||||
@@ -6,7 +6,15 @@
|
||||
#include "Async/ParallelFor.h"
|
||||
#include "Commandlets/Commandlet.h"
|
||||
#include "Containers/StringConv.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "GameFramework/WorldSettings.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "HAL/PlatformProcess.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
#include "HAL/PlatformTLS.h"
|
||||
#include "IImageWrapper.h"
|
||||
@@ -48,6 +56,8 @@
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelTypes.h"
|
||||
#include "VoxelWorld.h"
|
||||
#include "VoxelForgeCoreAssetAdapter.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -395,6 +405,7 @@ struct FExploreArguments
|
||||
bool bPassageFloorWritesOverride = false;
|
||||
int32 PassageFloorWrites = 1;
|
||||
bool bStandaloneWorldExport = false;
|
||||
bool bCoreWorldTest = false;
|
||||
bool bRenderCameraOverride = false;
|
||||
FVector RenderCamera = FVector::ZeroVector;
|
||||
bool bRenderTargetOverride = false;
|
||||
@@ -437,6 +448,9 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
int32 StandaloneWorldExport = 0;
|
||||
FParse::Value(*Params, TEXT("standaloneworldexport="), StandaloneWorldExport);
|
||||
OutArguments.bStandaloneWorldExport = StandaloneWorldExport != 0;
|
||||
int32 CoreWorldTest = 0;
|
||||
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
|
||||
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
|
||||
FParse::Value(*Params, TEXT("archetype="), ArchetypeText);
|
||||
FParse::Value(*Params, TEXT("slot="), OutArguments.Slot);
|
||||
FParse::Value(*Params, TEXT("strateref="), OutArguments.StrateReference);
|
||||
@@ -965,6 +979,12 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
OutError = TEXT("standaloneworldexport requires modes=export and settingsref, and must omit strateref.");
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.bCoreWorldTest
|
||||
&& (OutArguments.SettingsReference.IsEmpty() || !OutArguments.StrateReference.IsEmpty()))
|
||||
{
|
||||
OutError = TEXT("coreworldtest requires settingsref and must omit strateref.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (OutText.IsEmpty())
|
||||
{
|
||||
@@ -13035,6 +13055,561 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FString& OutError)
|
||||
{
|
||||
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(
|
||||
nullptr, *Arguments.SettingsReference);
|
||||
if (!AuthoredSettings)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Could not load Core actor test settings '%s'."),
|
||||
*Arguments.SettingsReference);
|
||||
return false;
|
||||
}
|
||||
|
||||
TStrongObjectPtr<UVoxelStrateManager> Manager(NewObject<UVoxelStrateManager>(
|
||||
GetTransientPackage(), NAME_None, RF_Transient));
|
||||
int32 ResolvedSeed = AuthoredSettings->Seed;
|
||||
if (const UVoxelSeasonAsset* Season = AuthoredSettings->Season.LoadSynchronous())
|
||||
{
|
||||
ResolvedSeed = Season->GetSeasonSeed();
|
||||
}
|
||||
if (!Manager.IsValid() || !Manager->Initialize(AuthoredSettings, ResolvedSeed))
|
||||
{
|
||||
OutError = TEXT("Core actor test could not resolve the owner's strate manager.");
|
||||
return false;
|
||||
}
|
||||
FString Json;
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
AuthoredSettings, Manager.Get(), 3, 8, Json, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build one Standalone reference before creating the actor world so the two full Core worlds
|
||||
// never overlap in memory. The reference also precomputes the exact edit result and hashes.
|
||||
// Construire la référence Standalone avant le monde acteur pour ne jamais garder deux mondes
|
||||
// Core complets en mémoire simultanément ; elle calcule aussi les hachages après édition.
|
||||
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedHashes;
|
||||
TMap<FVoxelTileKey, FVoxelForgeCoreTileHashRecord> ExpectedPostEditHashes;
|
||||
TSet<FVoxelTileKey> ExpectedInitialDesiredSet;
|
||||
TArray<FVoxelTileKey> ExpectedAffectedTiles;
|
||||
FVector CarveLocalPositionCm = FVector::ZeroVector;
|
||||
FVoxelTileKey CarveTargetTile;
|
||||
constexpr float CarveBrushRadiusLegacyVoxels = 5.0f;
|
||||
constexpr double CoreVoxelSizeCm = 25.0;
|
||||
const double CarveBrushRadiusCoreVoxels =
|
||||
CarveBrushRadiusLegacyVoxels * static_cast<double>(VOXEL_SIZE) / CoreVoxelSizeCm;
|
||||
uint64 ExpectedEditDigest = 0;
|
||||
std::array<std::uint64_t, 4> InitialAggregates{};
|
||||
std::size_t ExpectedInitialTileCount = 0;
|
||||
{
|
||||
FTCHARToUTF8 Utf8Json(*Json);
|
||||
std::string CoreError;
|
||||
std::optional<VoxelForgeCore::ResolvedWorldSnapshot> Snapshot =
|
||||
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
|
||||
std::string_view(Utf8Json.Get(), static_cast<std::size_t>(Utf8Json.Length())),
|
||||
CoreError);
|
||||
if (!Snapshot)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core rejected runtime adapter JSON: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
|
||||
VoxelForgeCore::World ReferenceWorld(std::move(*Snapshot));
|
||||
const std::vector<VoxelForgeCore::TileKey> Desired = ReferenceWorld.desiredTiles({});
|
||||
if (Desired.empty())
|
||||
{
|
||||
OutError = TEXT("Standalone Core returned an empty reference clipmap at the origin.");
|
||||
return false;
|
||||
}
|
||||
ExpectedInitialTileCount = Desired.size();
|
||||
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 Origin[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], Origin[Axis]);
|
||||
ClipMax[Axis] = FMath::Max(ClipMax[Axis], Origin[Axis] + Extent);
|
||||
if (Key.lod < ReferenceWorld.configuration().fullResolutionLodCount)
|
||||
{
|
||||
FineMin[Axis] = FMath::Min(FineMin[Axis], Origin[Axis]);
|
||||
FineMax[Axis] = FMath::Max(FineMax[Axis], Origin[Axis] + Extent);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (FineMin[0] == MAX_int32 || ClipMin[0] == MAX_int32)
|
||||
{
|
||||
OutError = TEXT("Standalone Core reference clipmap bounds are invalid.");
|
||||
return false;
|
||||
}
|
||||
|
||||
VoxelForgeCore::ShapeBuildRequest Request;
|
||||
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 reference compile failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
ReferenceWorld.retainTileRegions(Desired);
|
||||
|
||||
VoxelForgeCore::TileGenerationOptions Options;
|
||||
Options.hashDensity = true;
|
||||
std::vector<FStandaloneCoreAdapterTileHashes> AggregateRows;
|
||||
AggregateRows.reserve(Desired.size());
|
||||
bool bFoundCarveVertex = false;
|
||||
for (const VoxelForgeCore::TileKey& Key : Desired)
|
||||
{
|
||||
const std::optional<VoxelForgeCore::TileMesh> Mesh =
|
||||
ReferenceWorld.generateTile(Key, Options, CoreError);
|
||||
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
|
||||
|| !Mesh->hasPayloadHash || !Mesh->hasTopologyHash)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Standalone Core hash 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<int32>(Mesh->indices.size() / 3);
|
||||
Record.bEmpty = Mesh->indices.empty();
|
||||
Record.bHasHashes = true;
|
||||
ExpectedHashes.Add(RuntimeKey, Record);
|
||||
ExpectedInitialDesiredSet.Add(RuntimeKey);
|
||||
AggregateRows.push_back({Key, {Record.Density, Record.Mesh, Record.Payload, Record.Topology}});
|
||||
if (!bFoundCarveVertex && Key.lod == 0 && !Mesh->vertices.empty()
|
||||
&& !Mesh->indices.empty())
|
||||
{
|
||||
const VoxelForgeCore::Vec3d Vertex = Mesh->vertices.front().position;
|
||||
CarveLocalPositionCm = FVector(Vertex.x, Vertex.y, Vertex.z);
|
||||
CarveTargetTile = RuntimeKey;
|
||||
bFoundCarveVertex = true;
|
||||
}
|
||||
}
|
||||
InitialAggregates = StandaloneCoreAggregateHashes(AggregateRows);
|
||||
if (!bFoundCarveVertex)
|
||||
{
|
||||
OutError = TEXT("Standalone Core reference has no level-0 surface vertex for the carve fixture.");
|
||||
return false;
|
||||
}
|
||||
|
||||
VoxelForgeCore::EditTransaction Transaction;
|
||||
Transaction.sequence = 1;
|
||||
VoxelForgeCore::EditBrush Brush;
|
||||
Brush.shape = VoxelForgeCore::EditBrush::Shape::sphere;
|
||||
Brush.operation = VoxelForgeCore::EditBrush::Operation::densityOffset;
|
||||
Brush.a = {CarveLocalPositionCm.X / CoreVoxelSizeCm,
|
||||
CarveLocalPositionCm.Y / CoreVoxelSizeCm,
|
||||
CarveLocalPositionCm.Z / CoreVoxelSizeCm};
|
||||
Brush.halfExtent = {5.0, 5.0, 5.0};
|
||||
Brush.radius = CarveBrushRadiusCoreVoxels;
|
||||
Brush.falloff = 2.0f;
|
||||
Brush.strength = -40.0f;
|
||||
Transaction.brushes.push_back(Brush);
|
||||
const std::optional<VoxelForgeCore::EditResult> Edit =
|
||||
ReferenceWorld.applyEditTransaction(Transaction, Desired, CoreError);
|
||||
if (!Edit)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Standalone Core reference edit failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
ExpectedEditDigest = Edit->digest;
|
||||
for (const std::vector<VoxelForgeCore::TileKey>& ByLod : Edit->affectedByLod)
|
||||
{
|
||||
for (const VoxelForgeCore::TileKey& Key : ByLod)
|
||||
{
|
||||
const FVoxelTileKey RuntimeKey(
|
||||
FIntVector(Key.coordinate.x, Key.coordinate.y, Key.coordinate.z), Key.lod);
|
||||
ExpectedAffectedTiles.Add(RuntimeKey);
|
||||
const std::optional<VoxelForgeCore::TileMesh> Mesh =
|
||||
ReferenceWorld.generateTile(Key, Options, CoreError);
|
||||
if (!Mesh || !Mesh->hasDensityHash || !Mesh->hasMeshHash
|
||||
|| !Mesh->hasPayloadHash || !Mesh->hasTopologyHash)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Standalone Core edited tile hash failed: %s"),
|
||||
UTF8_TO_TCHAR(CoreError.c_str()));
|
||||
return false;
|
||||
}
|
||||
FVoxelForgeCoreTileHashRecord Record;
|
||||
Record.Density = Mesh->densityHash;
|
||||
Record.Mesh = Mesh->meshHash;
|
||||
Record.Payload = Mesh->payloadHash;
|
||||
Record.Topology = Mesh->topologyHash;
|
||||
Record.Triangles = static_cast<int32>(Mesh->indices.size() / 3);
|
||||
Record.bEmpty = Mesh->indices.empty();
|
||||
Record.bHasHashes = true;
|
||||
ExpectedPostEditHashes.Add(RuntimeKey, Record);
|
||||
}
|
||||
}
|
||||
ExpectedAffectedTiles.Sort([](const FVoxelTileKey& A, const FVoxelTileKey& B)
|
||||
{
|
||||
if (A.Level != B.Level) return A.Level < B.Level;
|
||||
if (A.Coord.X != B.Coord.X) return A.Coord.X < B.Coord.X;
|
||||
if (A.Coord.Y != B.Coord.Y) return A.Coord.Y < B.Coord.Y;
|
||||
return A.Coord.Z < B.Coord.Z;
|
||||
});
|
||||
if (!ExpectedPostEditHashes.Contains(CarveTargetTile)
|
||||
|| ExpectedAffectedTiles.IsEmpty())
|
||||
{
|
||||
OutError = TEXT("Standalone Core edit fixture did not affect its carved level-0 tile.");
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] standalone_reference=1 tiles=%llu affected=%d target=(%d,%d,%d) lod=%d"),
|
||||
static_cast<unsigned long long>(ExpectedInitialTileCount),
|
||||
ExpectedAffectedTiles.Num(), CarveTargetTile.Coord.X, CarveTargetTile.Coord.Y,
|
||||
CarveTargetTile.Coord.Z, CarveTargetTile.Level);
|
||||
}
|
||||
|
||||
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("VoxelForgeCoreActorValidationWorld")),
|
||||
GetTransientPackage(), false, ERHIFeatureLevel::Num, &WorldInitialization);
|
||||
if (TestWorld == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not create the transient BuildHost actor test world.");
|
||||
return false;
|
||||
}
|
||||
UGameInstance* TestGameInstance = NewObject<UGameInstance>(GEngine);
|
||||
FWorldContext& TestWorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
||||
TestWorldContext.OwningGameInstance = TestGameInstance;
|
||||
TestWorldContext.SetCurrentWorld(TestWorld);
|
||||
TestWorld->SetGameInstance(TestGameInstance);
|
||||
TestWorld->SetShouldTick(false);
|
||||
TestGameInstance->Init();
|
||||
TestWorld->AddToRoot();
|
||||
struct FTransientWorldCleanup
|
||||
{
|
||||
UWorld* World = nullptr;
|
||||
bool bHasEngineWorldContext = false;
|
||||
uint64 PreviousFrameCounter = GFrameCounter;
|
||||
~FTransientWorldCleanup()
|
||||
{
|
||||
if (World != nullptr)
|
||||
{
|
||||
if (World->HasBegunPlay())
|
||||
{
|
||||
World->BeginTearingDown();
|
||||
World->EndPlay(EEndPlayReason::Quit);
|
||||
}
|
||||
if (UGameInstance* GameInstance = World->GetGameInstance())
|
||||
{
|
||||
GameInstance->Shutdown();
|
||||
}
|
||||
if (bHasEngineWorldContext && GEngine != nullptr)
|
||||
{
|
||||
GEngine->DestroyWorldContext(World);
|
||||
}
|
||||
World->DestroyWorld(false);
|
||||
if (World->IsRooted()) World->RemoveFromRoot();
|
||||
}
|
||||
GFrameCounter = PreviousFrameCounter;
|
||||
}
|
||||
} Cleanup{TestWorld, true, GFrameCounter};
|
||||
const FTransform ActorTransform = FTransform::Identity;
|
||||
AVoxelWorld* Actor = TestWorld->SpawnActorDeferred<AVoxelWorld>(
|
||||
AVoxelWorld::StaticClass(), ActorTransform, nullptr, nullptr,
|
||||
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
|
||||
if (Actor == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not spawn the transient AVoxelWorld for Core validation.");
|
||||
return false;
|
||||
}
|
||||
Actor->Settings = AuthoredSettings;
|
||||
Actor->bManageAtmosphere = false;
|
||||
Actor->bUseVoxelForgeCore = true;
|
||||
Actor->CoreClipRadius = 3;
|
||||
Actor->CoreMaxClipLevel = 8;
|
||||
Actor->bCaptureVoxelForgeCoreHashesForValidation = true;
|
||||
Actor->FinishSpawning(ActorTransform);
|
||||
if (!TestWorld->SetGameMode(FURL()))
|
||||
{
|
||||
OutError = TEXT("Could not create the transient BuildHost GameMode for actor validation.");
|
||||
return false;
|
||||
}
|
||||
TestWorld->InitializeActorsForPlay(FURL());
|
||||
TestWorld->BeginPlay();
|
||||
Actor->SetActorTickEnabled(true);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] world_initialized=1 world_begun=%d actor_initialized=%d actor_beginplay=%d"),
|
||||
TestWorld->HasBegunPlay() ? 1 : 0, Actor->IsActorInitialized() ? 1 : 0,
|
||||
Actor->HasActorBegunPlay() ? 1 : 0);
|
||||
|
||||
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;
|
||||
};
|
||||
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;
|
||||
};
|
||||
auto WaitForStable = [&](double TimeoutSeconds)
|
||||
{
|
||||
const double Deadline = FPlatformTime::Seconds() + TimeoutSeconds;
|
||||
const double ProgressIntervalSeconds = 10.0;
|
||||
double LastProgressSeconds = FPlatformTime::Seconds();
|
||||
int32 TickCount = 0;
|
||||
while (FPlatformTime::Seconds() < Deadline)
|
||||
{
|
||||
TestWorld->Tick(LEVELTICK_All, 0.016f);
|
||||
// Match FTestWorldWrapper::TickTestWorld: each manually driven world tick must advance
|
||||
// the global frame counter or registered actor ticks run only once in this harness.
|
||||
++GFrameCounter;
|
||||
++TickCount;
|
||||
FPlatformProcess::Sleep(0.001f);
|
||||
if (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))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const double NowSeconds = FPlatformTime::Seconds();
|
||||
if (NowSeconds - LastProgressSeconds >= ProgressIntervalSeconds)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] wait ticks=%d ready=%d desired=%d loaded=%d pending=%d cooks=%d active=%d"),
|
||||
TickCount, Actor->IsVoxelForgeCoreReady() ? 1 : 0,
|
||||
Actor->DesiredSorted.Num(), Actor->LoadedTiles.Num(), Actor->PendingTiles.Num(),
|
||||
Actor->PendingCollisionCooks.Num(),
|
||||
Actor->ActiveTaskCount.load(std::memory_order_acquire));
|
||||
LastProgressSeconds = NowSeconds;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!WaitForStable(600.0))
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("AVoxelWorld Core initial 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<FVoxelTileKey> ActualInitialDesiredSet;
|
||||
for (const FVoxelTileKey& Key : Actor->DesiredSorted) ActualInitialDesiredSet.Add(Key);
|
||||
if (ActualInitialDesiredSet.Num() != ExpectedInitialDesiredSet.Num())
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core actor desired %d tiles; standalone reference desired %d."),
|
||||
ActualInitialDesiredSet.Num(), ExpectedInitialDesiredSet.Num());
|
||||
return false;
|
||||
}
|
||||
for (const FVoxelTileKey& Key : ExpectedInitialDesiredSet)
|
||||
{
|
||||
if (!ActualInitialDesiredSet.Contains(Key))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core actor initial clipmap omitted tile (%d,%d,%d) lod=%d."),
|
||||
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
|
||||
return false;
|
||||
}
|
||||
const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key);
|
||||
const FVoxelForgeCoreTileHashRecord* Expected = ExpectedHashes.Find(Key);
|
||||
if (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("Core actor tile hash mismatch at (%d,%d,%d) lod=%d."),
|
||||
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const int32 CollisionRadius = FMath::Clamp(AuthoredSettings->PawnCollisionRadiusChunks, 1, 4);
|
||||
TSet<FVoxelTileKey> ExpectedCollisionSet;
|
||||
int32 ExpectedBodyCount = 0;
|
||||
int32 ExpectedNoBodyCount = 0;
|
||||
int32 ActualNoBodyCount = 0;
|
||||
for (int32 Z = -CollisionRadius; Z <= CollisionRadius; ++Z)
|
||||
for (int32 Y = -CollisionRadius; Y <= CollisionRadius; ++Y)
|
||||
for (int32 X = -CollisionRadius; X <= CollisionRadius; ++X)
|
||||
{
|
||||
const FVoxelTileKey Key(FIntVector(X, Y, Z), 0);
|
||||
ExpectedCollisionSet.Add(Key);
|
||||
const FVoxelForgeCoreTileHashRecord* Record = ExpectedHashes.Find(Key);
|
||||
if (Record == nullptr)
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Standalone reference omitted collision tile (%d,%d,%d)."),
|
||||
X, Y, Z);
|
||||
return false;
|
||||
}
|
||||
if (Record->bEmpty) ++ExpectedNoBodyCount;
|
||||
else ++ExpectedBodyCount;
|
||||
if (Actor->CollisionNotRequiredTiles.Contains(Key)) ++ActualNoBodyCount;
|
||||
}
|
||||
if (Actor->CollisionDesiredTiles.Num() != ExpectedCollisionSet.Num()
|
||||
|| Actor->CollisionReadyTiles.Num() != ExpectedBodyCount
|
||||
|| ActualNoBodyCount != ExpectedNoBodyCount)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core collision count mismatch: desired=%d/%d ready=%d/%d no_body=%d/%d."),
|
||||
Actor->CollisionDesiredTiles.Num(), ExpectedCollisionSet.Num(),
|
||||
Actor->CollisionReadyTiles.Num(), ExpectedBodyCount,
|
||||
ActualNoBodyCount, ExpectedNoBodyCount);
|
||||
return false;
|
||||
}
|
||||
for (const FVoxelTileKey& Key : ExpectedCollisionSet)
|
||||
{
|
||||
const FVoxelForgeCoreTileHashRecord& Record = ExpectedHashes.FindChecked(Key);
|
||||
const bool bExpectedCollisionReady = !Record.bEmpty;
|
||||
if (!Actor->CollisionDesiredTiles.Contains(Key)
|
||||
|| (bExpectedCollisionReady && !Actor->CollisionReadyTiles.Contains(Key))
|
||||
|| (!bExpectedCollisionReady && !Actor->CollisionNotRequiredTiles.Contains(Key)))
|
||||
{
|
||||
OutError = FString::Printf(TEXT("Core collision result differs for tile (%d,%d,%d)."),
|
||||
Key.Coord.X, Key.Coord.Y, Key.Coord.Z);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] pass phase=initial tiles=%llu collision_desired=%d collision_ready=%d collision_no_body=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx"),
|
||||
static_cast<unsigned long long>(ExpectedInitialTileCount), ExpectedCollisionSet.Num(),
|
||||
Actor->CollisionReadyTiles.Num(), Actor->CollisionNotRequiredTiles.Num(),
|
||||
static_cast<unsigned long long>(InitialAggregates[0]),
|
||||
static_cast<unsigned long long>(InitialAggregates[1]),
|
||||
static_cast<unsigned long long>(InitialAggregates[2]),
|
||||
static_cast<unsigned long long>(InitialAggregates[3]));
|
||||
|
||||
AActor* Anchor = TestWorld->SpawnActor<AActor>(AActor::StaticClass());
|
||||
if (Anchor == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not spawn the Core streaming anchor fixture.");
|
||||
return false;
|
||||
}
|
||||
USceneComponent* AnchorRoot = NewObject<USceneComponent>(Anchor, NAME_None, RF_Transient);
|
||||
if (AnchorRoot == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not create the Core streaming anchor root component.");
|
||||
return false;
|
||||
}
|
||||
Anchor->SetRootComponent(AnchorRoot);
|
||||
AnchorRoot->RegisterComponent();
|
||||
constexpr double CoreTileExtentCm = 32.0 * 25.0;
|
||||
const FVector OldAnchorPosition(CoreTileExtentCm * 20.0, 0.0, 0.0);
|
||||
const FVector NewAnchorPosition(CoreTileExtentCm * 21.0, 0.0, 0.0);
|
||||
Anchor->SetActorLocation(OldAnchorPosition);
|
||||
Actor->RegisterStreamingAnchor(
|
||||
Anchor, EVoxelAnchorPolicy::CollisionOnly, 0, 0, 0);
|
||||
if (!WaitForStable(180.0))
|
||||
{
|
||||
OutError = TEXT("Core CollisionOnly anchor did not finish its initial tile request.");
|
||||
return false;
|
||||
}
|
||||
const FVoxelTileKey OldAnchorKey(FIntVector(20, 0, 0), 0);
|
||||
const FVoxelTileKey NewAnchorKey(FIntVector(21, 0, 0), 0);
|
||||
if (!Actor->DesiredSorted.Contains(OldAnchorKey)
|
||||
|| !Actor->LoadedTiles.Contains(OldAnchorKey)
|
||||
|| !Actor->CollisionDesiredTiles.Contains(OldAnchorKey))
|
||||
{
|
||||
OutError = TEXT("Core did not request and load the original anchor tile.");
|
||||
return false;
|
||||
}
|
||||
Anchor->SetActorLocation(NewAnchorPosition);
|
||||
if (!WaitForStable(180.0))
|
||||
{
|
||||
OutError = TEXT("Core did not settle after moving the streaming anchor by one tile.");
|
||||
return false;
|
||||
}
|
||||
if (!Actor->DesiredSorted.Contains(NewAnchorKey)
|
||||
|| !Actor->LoadedTiles.Contains(NewAnchorKey)
|
||||
|| !Actor->CollisionDesiredTiles.Contains(NewAnchorKey)
|
||||
|| Actor->DesiredSorted.Contains(OldAnchorKey)
|
||||
|| Actor->LoadedTiles.Contains(OldAnchorKey)
|
||||
|| Actor->CollisionDesiredTiles.Contains(OldAnchorKey))
|
||||
{
|
||||
OutError = TEXT("Core anchor move did not request the entering tile and release the leaving tile.");
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] pass phase=anchor old=(20,0,0) new=(21,0,0) requested=1 released=1"));
|
||||
|
||||
Actor->CarveAtPosition(Actor->GetActorTransform().TransformPosition(CarveLocalPositionCm),
|
||||
CarveBrushRadiusLegacyVoxels, 40.0f);
|
||||
if (Actor->LastVoxelForgeCoreEditSequence != 1
|
||||
|| Actor->LastVoxelForgeCoreEditDigest != ExpectedEditDigest
|
||||
|| Actor->LastVoxelForgeCoreAffectedTiles != ExpectedAffectedTiles)
|
||||
{
|
||||
OutError = TEXT("Core actor carve transaction sequence, digest, or affected tile list differs from Standalone.");
|
||||
return false;
|
||||
}
|
||||
if (!WaitForStable(240.0))
|
||||
{
|
||||
OutError = TEXT("Core actor carve remesh/collision work did not settle.");
|
||||
return false;
|
||||
}
|
||||
for (const FVoxelTileKey& Key : ExpectedAffectedTiles)
|
||||
{
|
||||
const FVoxelForgeCoreTileHashRecord* Actual = Actor->VoxelForgeCoreTileHashes.Find(Key);
|
||||
const FVoxelForgeCoreTileHashRecord* Expected = ExpectedPostEditHashes.Find(Key);
|
||||
if (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("Core actor edited tile hash differs from Standalone at (%d,%d,%d) lod=%d."),
|
||||
Key.Coord.X, Key.Coord.Y, Key.Coord.Z, Key.Level);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldTest] pass phase=edit sequence=%llu digest=0x%016llx affected=%d target=(%d,%d,%d) lod=%d hash=0x%016llx"),
|
||||
static_cast<unsigned long long>(Actor->LastVoxelForgeCoreEditSequence),
|
||||
static_cast<unsigned long long>(Actor->LastVoxelForgeCoreEditDigest),
|
||||
ExpectedAffectedTiles.Num(), CarveTargetTile.Coord.X, CarveTargetTile.Coord.Y,
|
||||
CarveTargetTile.Coord.Z, CarveTargetTile.Level,
|
||||
static_cast<unsigned long long>(ExpectedPostEditHashes.FindChecked(CarveTargetTile).Mesh));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet()
|
||||
@@ -13078,6 +13653,17 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (Arguments.bCoreWorldTest)
|
||||
{
|
||||
if (!RunCoreWorldActorIntegrationTest(Arguments, Error))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("[VoxelForgeCoreWorldTest] failed: %s"), *Error);
|
||||
return 1;
|
||||
}
|
||||
UE_LOG(LogTemp, Display, TEXT("[VoxelForgeCoreWorldTest] pass complete=1"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Arguments.bStandaloneWorldExport)
|
||||
{
|
||||
if (!RunStandaloneWorldExport(Arguments, Error, OutJson))
|
||||
|
||||
Reference in new issue
Block a user