wip(assembler): M1 round 1 -- manifest format, block contract, bound audit, native Heightfield2D

Luna, M1 round 1 of 4 (plan in Standalone/ASSEMBLER_M1_REPORT.md):
- Versioned fixed-scale world manifest (the shipped plan format) and the Core block
  contract: point evaluation, interval-or-unknown, influence bound, detail envelope, channels.
- Interval audit: 256 boxes / 6,912 samples, 0 missed, 0 unknown; a deliberately false bound
  is detected (negative control).
- Native Heightfield2D with biome input: 4,096-point hash fae2721c052aec5c identical at
  W1/W8/W16; point queries 2.059 ms vs 62.115 ms legacy -- but NOT a match yet (mean density
  error 274 voxels, 2,261 sign mismatches vs the legacy surface). 45 mesh-free Core queries
  match direct evaluation exactly. Existing cave world unchanged (walkability, 64/64 tiles).
- Open: collision readiness, a valid full-view timing (viewer captures were occluded/empty;
  top-down height maps are the only usable pair).
Also carries the design doc's owner decisions (fixed water; player edits unrestricted, route
law for automatic changes only) and three plugin files + BuildHost scripts that earlier round
commits had missed (also fixed on unreal/core-integration as 2574b56).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-09 00:04:30 +02:00
1 parent d5ce5d675d
commit 874c796c65
20 files changed
+2879 -11

No files matched your search

@@ -0,0 +1,402 @@
#include "VoxelForgeCoreAdapter.h"
#include "Dom/JsonObject.h"
#include "Policies/CondensedJsonPrintPolicy.h"
#include "Serialization/JsonWriter.h"
#include "UObject/UnrealType.h"
#include "UObject/TextProperty.h"
#include "VoxelSeasonAsset.h"
#include "VoxelSettings.h"
#include "VoxelStrateDefinition.h"
#include "VoxelStrateManager.h"
#include "VoxelTerrainOpDefinition.h"
namespace
{
using FCoreJsonWriter = TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>;
constexpr double CoreVoxelSizeCm = 25.0;
TSharedPtr<FJsonValue> SerializeCoreStruct(
const UStruct* Type, const void* Data, TSet<const UObject*>& ActiveObjects,
const UObject* Object = nullptr);
TSharedPtr<FJsonValue> SerializeCoreProperty(
FProperty* Property, const void* Data, TSet<const UObject*>& ActiveObjects)
{
if (Property == nullptr || Data == nullptr) return MakeShared<FJsonValueNull>();
if (const FBoolProperty* Bool = CastField<FBoolProperty>(Property))
{
return MakeShared<FJsonValueBoolean>(Bool->GetPropertyValue(Data));
}
if (const FEnumProperty* Enum = CastField<FEnumProperty>(Property))
{
return MakeShared<FJsonValueNumber>(static_cast<double>(
Enum->GetUnderlyingProperty()->GetSignedIntPropertyValue(Data)));
}
if (const FNumericProperty* Numeric = CastField<FNumericProperty>(Property))
{
const double Value = Numeric->IsFloatingPoint()
? Numeric->GetFloatingPointPropertyValue(Data)
: static_cast<double>(Numeric->GetSignedIntPropertyValue(Data));
return MakeShared<FJsonValueNumber>(Value);
}
if (const FStrProperty* String = CastField<FStrProperty>(Property))
{
return MakeShared<FJsonValueString>(String->GetPropertyValue(Data));
}
if (const FNameProperty* Name = CastField<FNameProperty>(Property))
{
return MakeShared<FJsonValueString>(Name->GetPropertyValue(Data).ToString());
}
if (const FTextProperty* Text = CastField<FTextProperty>(Property))
{
return MakeShared<FJsonValueString>(Text->GetPropertyValue(Data).ToString());
}
if (const FSoftObjectProperty* Soft = CastField<FSoftObjectProperty>(Property))
{
return MakeShared<FJsonValueString>(
Soft->GetPropertyValue(Data).ToSoftObjectPath().ToString());
}
if (const FArrayProperty* Array = CastField<FArrayProperty>(Property))
{
TArray<TSharedPtr<FJsonValue>> Values;
FScriptArrayHelper Helper(Array, Data);
Values.Reserve(Helper.Num());
for (int32 Index = 0; Index < Helper.Num(); ++Index)
{
Values.Add(SerializeCoreProperty(Array->Inner, Helper.GetRawPtr(Index), ActiveObjects));
}
return MakeShared<FJsonValueArray>(MoveTemp(Values));
}
if (const FSetProperty* Set = CastField<FSetProperty>(Property))
{
TArray<TPair<FString, TSharedPtr<FJsonValue>>> OrderedValues;
FScriptSetHelper Helper(Set, Data);
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
{
if (!Helper.IsValidIndex(Index)) continue;
FString StableKey;
Set->ElementProp->ExportTextItem_Direct(
StableKey, Helper.GetElementPtr(Index), nullptr, nullptr, PPF_None);
OrderedValues.Emplace(StableKey, SerializeCoreProperty(
Set->ElementProp, Helper.GetElementPtr(Index), ActiveObjects));
}
OrderedValues.Sort([](const auto& A, const auto& B) { return A.Key < B.Key; });
TArray<TSharedPtr<FJsonValue>> Values;
Values.Reserve(OrderedValues.Num());
for (const auto& Entry : OrderedValues) Values.Add(Entry.Value);
return MakeShared<FJsonValueArray>(MoveTemp(Values));
}
if (const FMapProperty* Map = CastField<FMapProperty>(Property))
{
TArray<TPair<FString, TSharedPtr<FJsonValue>>> OrderedValues;
FScriptMapHelper Helper(Map, Data);
for (int32 Index = 0; Index < Helper.GetMaxIndex(); ++Index)
{
if (!Helper.IsValidIndex(Index)) continue;
TSharedRef<FJsonObject> Pair = MakeShared<FJsonObject>();
Pair->SetField(TEXT("key"), SerializeCoreProperty(
Map->KeyProp, Helper.GetKeyPtr(Index), ActiveObjects));
Pair->SetField(TEXT("value"), SerializeCoreProperty(
Map->ValueProp, Helper.GetValuePtr(Index), ActiveObjects));
FString StableKey;
Map->KeyProp->ExportTextItem_Direct(
StableKey, Helper.GetKeyPtr(Index), nullptr, nullptr, PPF_None);
OrderedValues.Emplace(StableKey, MakeShared<FJsonValueObject>(Pair));
}
OrderedValues.Sort([](const auto& A, const auto& B) { return A.Key < B.Key; });
TArray<TSharedPtr<FJsonValue>> Values;
Values.Reserve(OrderedValues.Num());
for (const auto& Entry : OrderedValues) Values.Add(Entry.Value);
return MakeShared<FJsonValueArray>(MoveTemp(Values));
}
if (const FStructProperty* Struct = CastField<FStructProperty>(Property))
{
return SerializeCoreStruct(Struct->Struct, Data, ActiveObjects);
}
if (const FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property))
{
const UObject* Object = ObjectProperty->GetObjectPropertyValue(Data);
if (Object == nullptr) return MakeShared<FJsonValueNull>();
if (Object->GetClass()->GetName().StartsWith(TEXT("Voxel"), ESearchCase::CaseSensitive))
{
return SerializeCoreStruct(Object->GetClass(), Object, ActiveObjects, Object);
}
return MakeShared<FJsonValueString>(Object->GetPathName());
}
return MakeShared<FJsonValueNull>();
}
TSharedPtr<FJsonValue> SerializeCoreStruct(
const UStruct* Type, const void* Data, TSet<const UObject*>& ActiveObjects,
const UObject* Object)
{
if (Type == nullptr || Data == nullptr) return MakeShared<FJsonValueNull>();
if (Object != nullptr && ActiveObjects.Contains(Object))
{
return MakeShared<FJsonValueString>(Object->GetPathName());
}
if (Object != nullptr) ActiveObjects.Add(Object);
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
for (TFieldIterator<FProperty> It(Type); It; ++It)
{
FProperty* Property = *It;
Result->SetField(Property->GetName(), SerializeCoreProperty(
Property, Property->ContainerPtrToValuePtr<void>(const_cast<void*>(Data)), ActiveObjects));
}
if (Object != nullptr) ActiveObjects.Remove(Object);
return MakeShared<FJsonValueObject>(Result);
}
TSharedRef<FJsonObject> CoreVectorJson(const FVector& Value)
{
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
Result->SetNumberField(TEXT("x"), Value.X);
Result->SetNumberField(TEXT("y"), Value.Y);
Result->SetNumberField(TEXT("z"), Value.Z);
return Result;
}
TSharedRef<FJsonObject> CoreLandingJson(const FVoxelPassageLanding& Landing)
{
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
Result->SetObjectField(TEXT("standing_point"), CoreVectorJson(Landing.StandingPoint));
Result->SetObjectField(TEXT("door_point"), CoreVectorJson(Landing.DoorPoint));
Result->SetObjectField(TEXT("door_direction"), CoreVectorJson(Landing.DoorDirection));
Result->SetNumberField(TEXT("floor_z"), Landing.FloorZ);
Result->SetNumberField(TEXT("ceiling_z"), Landing.CeilingZ);
Result->SetNumberField(TEXT("half_width"), Landing.HalfWidth);
Result->SetNumberField(TEXT("floor_thickness"), Landing.FloorThickness);
Result->SetBoolField(TEXT("source_player_fit"), Landing.bSourcePlayerFit);
return Result;
}
TSharedRef<FJsonObject> CorePassageJson(const FVoxelPassage& Passage)
{
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
Result->SetNumberField(TEXT("upper_strate_index"), Passage.UpperStrateIndex);
Result->SetNumberField(TEXT("lower_strate_index"), Passage.LowerStrateIndex);
Result->SetObjectField(TEXT("upper_point"), CoreVectorJson(Passage.UpperPoint));
Result->SetObjectField(TEXT("lower_point"), CoreVectorJson(Passage.LowerPoint));
Result->SetObjectField(TEXT("requested_upper_point"), CoreVectorJson(Passage.RequestedUpperPoint));
Result->SetObjectField(TEXT("requested_lower_point"), CoreVectorJson(Passage.RequestedLowerPoint));
Result->SetNumberField(TEXT("radius"), Passage.Radius);
Result->SetNumberField(TEXT("passage_type"), static_cast<int32>(Passage.PassageType));
auto WriteVectors = [](const TCHAR* Key, const TArray<FVector>& Source,
TSharedRef<FJsonObject> Out)
{
TArray<TSharedPtr<FJsonValue>> Values;
Values.Reserve(Source.Num());
for (const FVector& Value : Source)
{
Values.Add(MakeShared<FJsonValueObject>(CoreVectorJson(Value)));
}
Out->SetArrayField(Key, MoveTemp(Values));
};
auto WriteFloats = [](const TCHAR* Key, const TArray<float>& Source,
TSharedRef<FJsonObject> Out)
{
TArray<TSharedPtr<FJsonValue>> Values;
Values.Reserve(Source.Num());
for (float Value : Source) Values.Add(MakeShared<FJsonValueNumber>(Value));
Out->SetArrayField(Key, MoveTemp(Values));
};
WriteVectors(TEXT("control_points"), Passage.ControlPoints, Result);
WriteFloats(TEXT("control_radii"), Passage.ControlRadii, Result);
Result->SetObjectField(TEXT("upper_landing"), CoreLandingJson(Passage.UpperLanding));
Result->SetObjectField(TEXT("lower_landing"), CoreLandingJson(Passage.LowerLanding));
Result->SetBoolField(TEXT("walkable_tunnel_contract"), Passage.bWalkableTunnelContract);
Result->SetBoolField(TEXT("native_floor_enabled"), Passage.bNativeFloorEnabled);
WriteFloats(TEXT("native_floor_profile_z"), Passage.NativeFloorProfileZ, Result);
WriteFloats(TEXT("native_floor_relief_scales"), Passage.NativeFloorReliefScales, Result);
Result->SetNumberField(TEXT("native_floor_relief_strength"), Passage.NativeFloorReliefStrength);
Result->SetNumberField(TEXT("native_floor_relief_frequency"), Passage.NativeFloorReliefFrequency);
Result->SetNumberField(TEXT("native_floor_seed"), static_cast<int64>(Passage.NativeFloorSeed));
Result->SetNumberField(TEXT("tunnel_max_gradient_degrees"), Passage.TunnelMaxGradientDegrees);
Result->SetNumberField(TEXT("tunnel_vertical_drop_voxels"), Passage.TunnelVerticalDropVoxels);
Result->SetNumberField(TEXT("tunnel_required_horizontal_run_voxels"), Passage.TunnelRequiredHorizontalRunVoxels);
Result->SetNumberField(TEXT("tunnel_horizontal_path_length_voxels"), Passage.TunnelHorizontalPathLengthVoxels);
Result->SetNumberField(TEXT("tunnel_minimum_clear_width_voxels"), Passage.TunnelMinimumClearWidthVoxels);
Result->SetNumberField(TEXT("tunnel_clear_height_voxels"), Passage.TunnelClearHeightVoxels);
Result->SetObjectField(TEXT("bound_center"), CoreVectorJson(Passage.BoundCenter));
Result->SetNumberField(TEXT("bound_radius"), Passage.BoundRadius);
Result->SetNumberField(TEXT("bound_radius_sq"), Passage.BoundRadiusSq);
return Result;
}
void WriteCoreJsonValue(FCoreJsonWriter& Writer, const TCHAR* Identifier,
const TSharedPtr<FJsonValue>& Value)
{
if (Identifier != nullptr) Writer.WriteIdentifierPrefix(Identifier);
if (!Value.IsValid() || Value->Type == EJson::Null)
{
Writer.WriteNull();
}
else if (Value->Type == EJson::Boolean)
{
Writer.WriteValue(Value->AsBool());
}
else if (Value->Type == EJson::Number)
{
Writer.WriteValue(Value->AsNumber());
}
else if (Value->Type == EJson::String)
{
Writer.WriteValue(Value->AsString());
}
else if (Value->Type == EJson::Array)
{
Writer.WriteArrayStart();
for (const TSharedPtr<FJsonValue>& Element : Value->AsArray())
{
WriteCoreJsonValue(Writer, nullptr, Element);
}
Writer.WriteArrayEnd();
}
else if (Value->Type == EJson::Object)
{
Writer.WriteObjectStart();
TArray<FString> Keys;
Value->AsObject()->Values.GetKeys(Keys);
Keys.Sort();
for (const FString& Key : Keys)
{
WriteCoreJsonValue(Writer, *Key, Value->AsObject()->Values.FindRef(Key));
}
Writer.WriteObjectEnd();
}
else
{
Writer.WriteNull();
}
}
}
bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
const UVoxelSettings* Settings,
const UVoxelStrateManager* StrateManager,
int32 ClipRadius,
int32 MaxClipLevel,
FString& OutJson,
FString& OutError)
{
OutJson.Reset();
OutError.Reset();
if (Settings == nullptr || StrateManager == nullptr || StrateManager->GetLayout().IsEmpty())
{
OutError = TEXT("Core snapshot export requires initialized settings and a resolved strate layout.");
return false;
}
const int32 Seed = StrateManager->GetWorldSeed();
const float OriginSpineRadius = Settings->GetEffectiveOriginSpineRadius();
const float WorldRadiusVoxels = Settings->GetEffectiveWorldRadiusVoxels();
const int32 CoreClipRadius = FMath::Clamp(ClipRadius, 1, 64);
const int32 CoreMaxClipLevel = FMath::Clamp(MaxClipLevel, 0, 8);
TSet<const UObject*> ActiveObjects;
TSharedPtr<FJsonValue> SettingsValue = SerializeCoreStruct(
Settings->GetClass(), Settings, ActiveObjects, Settings);
if (!SettingsValue.IsValid() || SettingsValue->Type != EJson::Object)
{
OutError = TEXT("Core snapshot export could not serialize the settings asset.");
return false;
}
// Override only the temporary schema copy. Clipmap tuning on the owner's asset is preserved.
SettingsValue->AsObject()->SetNumberField(TEXT("ClipRadius"), CoreClipRadius);
SettingsValue->AsObject()->SetNumberField(TEXT("MaxClipLevel"), CoreMaxClipLevel);
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
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("seed"), Seed);
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
Root->SetNumberField(TEXT("world_radius_voxels"), WorldRadiusVoxels);
Root->SetNumberField(TEXT("edge_seal_thickness"), Settings->EdgeSealThickness);
Root->SetNumberField(TEXT("total_strates"), StrateManager->GetNumStrates());
Root->SetNumberField(TEXT("inter_strate_gap_chunks"), Settings->InterStrateGapChunks);
Root->SetNumberField(TEXT("clip_radius"), CoreClipRadius);
Root->SetNumberField(TEXT("max_clip_level"), CoreMaxClipLevel);
Root->SetNumberField(TEXT("full_res_clip_levels"), Settings->FullResClipLevels);
Root->SetNumberField(TEXT("coarse_tile_cells"), Settings->CoarseTileCells);
Root->SetNumberField(TEXT("render_distance_chunks"), Settings->RenderDistanceChunks);
Root->SetBoolField(TEXT("generate_skirts"), Settings->bGenerateSkirts);
Root->SetNumberField(TEXT("skirt_cells"), Settings->SkirtCells);
Root->SetNumberField(TEXT("lod_octave_drop"), Settings->LODOctaveDrop);
Root->SetNumberField(TEXT("strate_content_cut_min_level"),
Settings->GetEffectiveStrateContentCutMinLevel());
Root->SetField(TEXT("settings_asset"), SettingsValue);
TArray<TSharedPtr<FJsonValue>> Strates;
Strates.Reserve(StrateManager->GetLayout().Num());
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
if (Slot.Definition == nullptr)
{
OutError = FString::Printf(TEXT("Resolved strate %d has no definition asset."), Slot.StrateIndex);
return false;
}
TSharedRef<FJsonObject> Strate = MakeShared<FJsonObject>();
Strate->SetNumberField(TEXT("index"), Slot.StrateIndex);
Strate->SetNumberField(TEXT("top_chunk_z"), Slot.TopChunkZ);
Strate->SetNumberField(TEXT("bottom_chunk_z"), Slot.BottomChunkZ);
Strate->SetNumberField(TEXT("height_chunks"), Slot.HeightInChunks);
Strate->SetField(TEXT("asset"), SerializeCoreStruct(
Slot.Definition->GetClass(), Slot.Definition, ActiveObjects, Slot.Definition));
Strate->SetField(TEXT("generation_params"), SerializeCoreStruct(
FStrateGenerationParams::StaticStruct(), &Slot.Definition->GenerationParams, ActiveObjects));
Strate->SetField(TEXT("disturbances"), SerializeCoreStruct(
FStrateDisturbanceParams::StaticStruct(), &Slot.Definition->Disturbances, ActiveObjects));
Strate->SetField(TEXT("passage_config"), SerializeCoreStruct(
FStratePassageConfig::StaticStruct(), &Slot.Definition->PassageConfig, ActiveObjects));
TArray<TSharedPtr<FJsonValue>> Operations;
Operations.Reserve(Slot.Definition->TerrainOperations.Num());
for (const FStrateTerrainOpEntry& Entry : Slot.Definition->TerrainOperations)
{
TSharedRef<FJsonObject> Operation = MakeShared<FJsonObject>();
Operation->SetNumberField(TEXT("weight"), Entry.Weight);
Operation->SetNumberField(TEXT("probability"), Entry.Probability);
Operation->SetStringField(TEXT("asset_path"), Entry.Operation.ToString());
if (const UVoxelTerrainOpDefinition* Definition = Entry.Operation.LoadSynchronous())
{
Operation->SetField(TEXT("asset"), SerializeCoreStruct(
Definition->GetClass(), Definition, ActiveObjects, Definition));
}
Operations.Add(MakeShared<FJsonValueObject>(Operation));
}
Strate->SetArrayField(TEXT("terrain_operation_assets"), MoveTemp(Operations));
Strates.Add(MakeShared<FJsonValueObject>(Strate));
}
Root->SetArrayField(TEXT("strates"), MoveTemp(Strates));
TArray<TSharedPtr<FJsonValue>> Passages;
Passages.Reserve(StrateManager->GetPassages().Num());
for (const FVoxelPassage& Passage : StrateManager->GetPassages())
{
Passages.Add(MakeShared<FJsonValueObject>(CorePassageJson(Passage)));
}
Root->SetArrayField(TEXT("passages"), MoveTemp(Passages));
Root->SetArrayField(TEXT("density_samples"), {});
TSharedRef<FJsonObject> ViewerStart = MakeShared<FJsonObject>();
ViewerStart->SetBoolField(TEXT("found_open_space"), false);
ViewerStart->SetNumberField(TEXT("x"), 0.0);
ViewerStart->SetNumberField(TEXT("y"), 0.0);
ViewerStart->SetNumberField(TEXT("z"), 0.0);
Root->SetObjectField(TEXT("viewer_start"), ViewerStart);
FString WriterBuffer;
TSharedRef<FCoreJsonWriter> Writer = TJsonWriterFactory<TCHAR,
TCondensedJsonPrintPolicy<TCHAR>>::Create(&WriterBuffer);
WriteCoreJsonValue(*Writer, nullptr, MakeShared<FJsonValueObject>(Root));
if (!Writer->Close() || WriterBuffer.IsEmpty())
{
OutError = TEXT("Core snapshot export failed while writing schema-v1 JSON.");
return false;
}
OutJson = MoveTemp(WriterBuffer);
return true;
}
@@ -0,0 +1,3 @@
#pragma once
#include "VoxelForgeCoreAssetAdapter.h"
@@ -0,0 +1,21 @@
#pragma once
#include "CoreMinimal.h"
class UVoxelSettings;
class UVoxelStrateManager;
namespace VoxelForgeCoreAdapter
{
/**
* Export the already-resolved Runtime assets through the shared schema-v1 boundary.
* The override values are written into the JSON copy only; authored UObject properties stay
* untouched. This runs on the game thread because it walks UObjects and soft references.
*/
VOXELFORGE_API bool SerializeResolvedWorld(const UVoxelSettings* Settings,
const UVoxelStrateManager* StrateManager,
int32 ClipRadius,
int32 MaxClipLevel,
FString& OutJson,
FString& OutError);
}
+181
View File
@@ -0,0 +1,181 @@
# World assembler M1 report
M1 has four implementation rounds. The gates at the end of the milestone remain the agreed
two-biome SurfaceWorld in the owner's game, including coast, cliff/overhang, cap and a walkable
route; conservative bound audits; route sweep; identical evaluator hashes at W1/W8/W16; separate
local query/collision readiness and full-view timings; faster matched workload than the 23.085 s
legacy bridge; and server field queries that do not depend on render meshes. The approximately
1.57 s whole-view aim (2 × the 0.785 s cave-only Core measurement) is an aim, not an acceptance gate.
## Round 1 of 4 — native foundations
- Define the fixed-scale, versioned, hashed recipe and shipped-plan payload, including the Core
field-block contract and explicit field channels.
- Add a brute-force interval audit with a deliberately false bound as a negative control.
- Implement the first native `Heightfield2D` block with biome-map input and compare it with the
SurfaceWorld legacy bridge in the standalone viewer.
- Record current hashes, timings, audit results and a small owner/native capture pair. This round
establishes a heightfield baseline; it does not claim the final two-biome/coast/cliff/route gate.
## Round 2 of 4 — complete the surface recipe
- Add output-space biome blending, coast and beach shaping, cap detail, cliffs and bounded 3D
overhang exceptions with stable feature IDs and certified influence bounds.
- Keep unknown or intersecting exception intervals on the evaluation path. Expand the brute-force
audit across biome borders, cliffs, caps, negative coordinates and exception bounds.
- Measure surface field query cost separately from cave compilation and full-view rendering; tune
caches only after the workload and cache identity are fixed.
## Round 3 of 4 — route and query readiness
- Ship the finite-strate plan with route points, support constraints and stable IDs. Sweep the actual
player capsule over the final composed field and repair/reject any failing candidate.
- Verify W1/W8/W16 evaluator hashes, route clearance/support and deterministic plan serialization.
- Keep the capsule-swept, supported route valid through generation and automatic/scripted changes.
Player carve/fill edits are unrestricted and may break or plug the route. Seas and rivers stay fixed
for the season, with shore behavior under edits measured separately.
- Report local field-query and collision readiness separately from render mesh readiness, and show
a server-side field query path that does not build or require render meshes.
## Round 4 of 4 — Unreal delivery and owner review
- Import the reviewed manifest into the season asset/content hash path and connect the native Core
evaluator behind `bUseVoxelForgeCore`, preserving the legacy path when the flag is off.
- Build only the isolated BuildHost target here; the owner builds the project. Review the two-biome
surface and route in-game, then publish matched workload timings, hashes, audit logs and captures.
- Close every M1 gate or name the failed gate, its evidence and the additional repair round needed.
## Round 1 results
### Deliverables
- Added the public recipe, shipped-plan, hash, channel, block-contract, interval, influence and
detail-envelope types in `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`. Recipe and
evaluator schemas are version 1. All recipe, plan, feature, route and field coordinates use signed
1/256-voxel integers; the plugin's Core voxel remains 25 cm.
- Added manifest parse/finalize/validate/serialize and canonical FNV-1a 64 hashing in
`Standalone/Core/src/WorldAssemblerInternal.h`. Fields hash in declared order as little-endian
4/8-byte words. Hashes are 16 lowercase hex digits; JSON key order and whitespace are ignored.
The optional owner-snapshot `world_assembly` field matches the payload shape planned for the
season manifest. The current review artifact is
[`round1-manifest.json`](Research/WorldAssemblerM1/round1-manifest.json); season asset/hash-path
integration remains for round 4.
- Added native `Heightfield2D`: deterministic warped Voronoi biome selection with Q16 border blend,
fixed integer profile noise, ground and cap heights, signed density (`negative = solid`), declared
channels, a conservative global interval, unbounded XY/finite-stratum-Z influence and an explicit
detail envelope. The current block reads `biome` and writes `height` and `rock`. Water, route and
surface-tag channels are declared for later blocks, not yet emitted by this block.
- Connected that block to native Core density queries and added the standalone audit/viewer switches.
`API.md` documents the current data format and its limits.
### Sample manifest and hashes
The viewer builds a sample from the owner's `type_4_SurfaceWorld.json` probe: one finite strate
(index 1, chunk Z -16 through -79), two synthetic stable biome IDs (4097 and 4098), no planned
features, and no route points yet. The compact manifest is 972 bytes.
| Content | Hash |
|---|---|
| Recipe | `5cc6cfb49073d366` |
| Plan | `9aa8b4c29a69233a` |
| Manifest | `c87ae279b0e26f53` |
| Heightfield sample stream, 4096 points | `fae2721c052aec5c` |
The sample block reports influence Z `[-2528, -480]` voxels, maximum displacement envelope
`247` voxels, and minimum feature size `25` voxels. These prototype ranges are not the final
composer-supported recipe envelope.
### Bounds and determinism audit
The brute-force audit tested 256 randomized boxes with a 3×3×3 sample grid per box (6,912 points).
It found zero missed bounds and zero unknown bounds. Its deliberately false one-sided interval was
rejected, so the negative control passed. This is evidence for the current Heightfield2D
implementation and audit path, not a proof for future field blocks or the final cliff/overhang
composition.
The 4,096-point evaluator output hash matched at W1, W8 and W16 (`fae2721c052aec5c`). The timing
scope was point evaluation only: W1 2.059 ms, W8 6.851 ms, W16 12.626 ms. W8 and W16 were slower
on this small batch, likely from thread-startup overhead; no parallel speedup was measured. These
numbers are not a full view or tile benchmark.
### Legacy comparison and captures
For the same 4,096 points, the direct legacy density query took 62.115 ms. The prototype's mean
absolute density error was 274.0383 voxels, maximum error 841.4257 voxels, with 2,261 sign
mismatches (55.2%). The prototype is therefore not yet a parity replacement for the legacy field.
The current biome map and two-profile output need further shaping against the reference.
The small top-down pair uses the same 3200-voxel XY span and one color ramp fitted to the observed
owner and native heights together (range `[-2273.12, -1410.26]` voxels): owner `GetSurfaceHeightAt`
reference and native Heightfield2D ground height. Each JPEG is below 6 KB.
| Owner reference | Native prototype |
|---|---|
| ![Owner SurfaceWorld height reference](Research/WorldAssemblerM1/owner-reference-height.jpg) | ![Native Heightfield2D prototype](Research/WorldAssemblerM1/native-prototype-height.jpg) |
The pair shows a large visible mismatch; it is a height-map comparison, not an in-game render. A
separate D3D window-backbuffer capture attempt was invalid: both viewer windows reported
`DXGI_STATUS_OCCLUDED`, the saved backbuffers were black, and the native prototype produced zero
mesh vertices/indices. The reduced diagnostic images were inspected only after downscaling. Do not
use those D3D files as visual evidence.
### Query, collision and view readiness
The audit compiled a Core world around the sample heightfield, queried 45 positions through
`World::densityAt`, and compared them with direct Heightfield2D evaluation. Maximum error was zero;
the sample Core compile took 35.608 ms; no tile mesh was generated
(`render_meshes_generated=0`). This verifies the local Core field-query path without render meshes
for this sample. It does not yet verify a dedicated-server build or the Unreal adapter.
The no-manifest cave Core path also passed a constrained regression smoke on the existing
`type_3_Maze.json` fixture: the 5,642-point walkability audit passed, all 27 player-neighborhood
tiles and all 64 requested LOD0 tiles generated, and full-detail readiness was 0.157 s at W16.
The shape compile was 0.100 s. This small 64-tile LOD0 run confirms the additive no-plan path still
works; it is not the 23.085 s matched workload or the 0.785 s cave-only whole-view baseline.
Collision cooking is not available in the standalone audit and was not measured. Full-view time was
also not measured by audit mode. Earlier small-view D3D attempt timings (owner 0.236 s, native
0.309 s) are not a valid full-view comparison because presentation was occluded and the native
view had no geometry. The 23.085 s matched-workload gate remains open. The ~1.57 s aim remains
reasonable, but round 1 has no valid full-view evidence to support or reject it.
### Remaining work by round
- **Round 2:** Match the legacy reference more closely; add output-space biome blending, coast/beach,
cap detail, cliffs and bounded 3D overhang features with stable IDs and certified influence
bounds. Expand bound audits across borders, caps, negative coordinates and exceptions.
- **Round 3:** Add the finite-strate capsule route and support constraints, sweep it against the final
composed field, and reject or repair any failing automatic plan. Verify full-evaluator W1/W8/W16
hashes. Measure collision readiness separately from query and mesh readiness; demonstrate a
headless/server field query without render meshes. Keep the route valid during all automatic
changes, leave player edits unrestricted, and keep seasonal water fixed.
- **Round 4:** Wire the reviewed manifest into the season asset/content-hash path and connect Core
behind `bUseVoxelForgeCore`; prove the flag-off legacy path is unchanged. Build only the isolated
BuildHost target here. The owner builds and reviews the in-game surface and walkable route.
Re-run the matched 23.085 s workload, report full-view and local query/collision readiness
separately, and assess the ~1.57 s aim with valid captures.
### Run log
- First isolated full build compiled Core and passed the 199,209-line Core link-safety scan, then
failed at viewer link: the new inline API symbols were not emitted and the standalone build lacked
the GPU Far viewer implementation. No GPU Far source file exists in the standalone set.
- Retry adding the assumed `GpuFarViewer.cpp` failed at compile with `C1083` because that source file
is absent. The build was changed to use local CPU-viewer link stubs under ignored
`Standalone/Build/AssemblerM1/` output; Core remains GPU-free.
- The next full standalone build succeeded after moving the new API definitions out of line and
isolating the GPU viewer stubs. Another full build succeeded after adding the height-map capture;
a further build succeeded after adding manifest output; the final full rebuild after marking the
manifest API for Core export also succeeded. The latest full build succeeded after changing the
capture ramp to the observed shared height range. Each successful build took under one minute,
used `/MP16`, Normal priority and all 16 allowed CPUs; sampled compiler processes remained well
below 8 GB.
- Successful builds still print the environment's `'vswhere.exe' is not recognized` message and
existing C4838 narrowing warnings in `VoxelStrateManager.cpp:3974-3975`. These did not stop the
build. No `0xC0000142` retry occurred. A separate Git Bash `tail` inspection command failed with
access denied while opening a file; it made no changes.
- No BuildHost or owner-project build was run this round. The checked
`Saved/BuildHost/HostProject/Saved/Crashes/` folder contains one older `UECC-Windows-*` directory
from 2026-09-17; there was no new crash folder from this round.
No Unreal route or asset was changed in this round, so there is no owner rebuild handoff yet.
+39
View File
@@ -79,6 +79,45 @@ Pass `analyticOnly = false` to retain the reference packed-page compiler. Packed
parent-cell coordinates and an 8-voxel apron; each covers a 128-voxel cube or one larger coarse
tile. This path remains available for parity checks and workloads that need the packed lattice.
## World assembler manifest v1 (prototype)
`ResolvedWorldSnapshot::FromJson` accepts an optional `world_assembly` object alongside the owner
snapshot. The object is the shipped-plan payload shape: a fixed-scale recipe and finite-strate plan
with hashes, no generated mesh or voxel samples. The reviewed sample payload is
[`round1-manifest.json`](../Research/WorldAssemblerM1/round1-manifest.json). Round 4 still needs to
connect this payload to the season asset/hash path.
Every coordinate and scalar uses signed integer units of 1/256 voxel (`units_per_voxel: 256`),
independent of the plugin's 25 cm Core voxel and of tile size. Schema and evaluator versions are
both 1. JSON integer fields are canonicalized in declared field order and hashed as 4- or 8-byte
little-endian words with FNV-1a 64. Hashes are emitted as 16 lowercase hexadecimal characters;
JSON key order and whitespace do not affect them. The root, recipe, plan and each stratum carry a
content hash. The recipe stores its seed, warped biome-map settings and one to eight biome profiles;
the plan stores finite-strate extents, cap settings, stable-ID features and route points. Bounds,
feature count and route count are validated before a manifest is accepted.
```cpp
std::string error;
auto manifest = VoxelForgeCore::WorldAssemblyManifest::FromJson(payload, error);
if (!manifest || !manifest->validate(error)) { /* reject the payload */ }
const auto* loaded = snapshot.worldAssembly(); // null when the owner snapshot has no plan
```
`Heightfield2D` is the first field block. `point(xQ, yQ, zQ)` evaluates the biome map and blended
height profiles at fixed-scale coordinates, returning the ground and cap heights, primary and
neighbor biome IDs, Q16 blend weight, and signed density. Negative density is solid. Its block
contract reads `biome` and writes `height` plus `rock`; `water`, `waterSurface`, `route` and
`surfaceTags` are declared channels for later blocks. The detail envelope reports maximum absolute
vertical displacement and minimum feature size. Influence is global in XY and limited to the
finite stratum's Z extent.
`interval(minXQ, minYQ, minZQ, maxXQ, maxYQ, maxZQ)` returns a conservative density range or
`known == false`; callers must keep evaluating when it is unknown. The current heightfield range
uses the maximum recipe-profile amplitude and cap undulation over the whole biome set. The public
`AuditHeightfieldBounds` samples points inside randomized boxes and includes a deliberately false
one-sided range as a negative control. A pass means no sampled point escaped and the negative
control was rejected; it is an audit of this block implementation, not a proof for future blocks.
## Queries and tiles
`densityAt(position)` returns signed density; negative is solid and nonnegative is air.
@@ -98,6 +98,182 @@ namespace VoxelForgeCore
Vec3d viewerStartVoxels{};
};
// World assembler v1 serializes every coordinate and scalar in 1/256 voxel units.
// This is independent of tile size and is the fixed input scale for Core field blocks.
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 1;
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 1;
enum class FieldChannel : std::uint32_t
{
rock = 1u << 0, height = 1u << 1, biome = 1u << 2, water = 1u << 3,
waterSurface = 1u << 4, route = 1u << 5, surfaceTags = 1u << 6
};
constexpr std::uint32_t ChannelBit(FieldChannel channel) noexcept
{ return static_cast<std::uint32_t>(channel); }
struct WorldAssemblyBiomeProfileV1
{
std::uint32_t stableId = 0;
std::int64_t baseHeightSubvoxels = 0;
std::int64_t continentAmplitudeSubvoxels = 0;
std::int64_t continentWavelengthSubvoxels = 256 * 256;
std::int64_t mountainAmplitudeSubvoxels = 0;
std::int64_t mountainWavelengthSubvoxels = 256 * 128;
std::int64_t detailAmplitudeSubvoxels = 0;
std::int64_t detailWavelengthSubvoxels = 256 * 16;
};
struct WorldAssemblyRecipeV1
{
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
std::uint32_t evaluatorVersion = WorldAssemblyEvaluatorVersion;
std::uint32_t unitsPerVoxel = static_cast<std::uint32_t>(WorldAssemblySubvoxelsPerVoxel);
std::uint32_t seed = 0;
std::int64_t biomeCellSizeSubvoxels = 256 * 800;
std::int64_t biomeWarpSubvoxels = 256 * 250;
std::int64_t biomeWarpWavelengthSubvoxels = 256 * 556;
std::int64_t biomeBorderBlendSubvoxels = 256 * 120;
std::vector<WorldAssemblyBiomeProfileV1> biomes;
std::uint64_t contentHash = 0;
};
struct WorldAssemblyRoutePointV1
{
std::uint64_t stableFeatureId = 0;
std::array<std::int64_t, 3> positionSubvoxels{};
std::int64_t radiusSubvoxels = 0;
};
struct WorldAssemblyFeatureV1
{
// Stable numeric type IDs are part of the plan contract; v1 reserves 1 for cliff planes,
// 2 for bounded overhang shelves and 3 for authored cap features.
// Les types numeriques stables font partie du plan; v1 reserve 1 aux falaises, 2 aux surplombs bornes et 3 aux caps.
std::uint64_t stableId = 0;
std::uint32_t type = 0;
std::array<std::int64_t, 3> minimumSubvoxels{};
std::array<std::int64_t, 3> maximumSubvoxels{};
std::array<std::int64_t, 4> parametersSubvoxels{};
};
struct WorldAssemblyStratumPlanV1
{
std::int32_t strateIndex = -1;
std::int32_t topChunkZ = 0;
std::int32_t bottomChunkZ = 0;
std::int64_t capHeightSubvoxels = 0;
std::int64_t capUndulationSubvoxels = 0;
std::int64_t capWavelengthSubvoxels = 256 * 512;
std::vector<WorldAssemblyFeatureV1> features;
std::vector<WorldAssemblyRoutePointV1> route;
std::uint64_t contentHash = 0;
};
struct WorldAssemblyPlanV1
{
std::uint32_t schemaVersion = WorldAssemblySchemaVersion;
std::vector<WorldAssemblyStratumPlanV1> strates;
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<WorldAssemblyManifest> FromJson(
std::string_view json, std::string& error);
bool finalize(std::string& error);
bool validate(std::string& error) const;
std::string ToJson() const;
};
struct HeightfieldPointSample
{
std::int64_t groundHeightSubvoxels = 0;
std::int64_t capHeightSubvoxels = 0;
std::uint32_t biomeId = 0;
std::uint32_t neighborBiomeId = 0;
std::uint32_t biomeWeightQ16 = 0;
std::int64_t densitySubvoxels = 0;
};
struct FieldInterval
{
bool known = false;
std::int64_t minimumDensitySubvoxels = 0;
std::int64_t maximumDensitySubvoxels = 0;
};
struct FieldInfluenceBounds
{
bool unboundedXY = true;
std::int64_t minimumXSubvoxels = 0, maximumXSubvoxels = 0;
std::int64_t minimumYSubvoxels = 0, maximumYSubvoxels = 0;
std::int64_t minimumZSubvoxels = 0, maximumZSubvoxels = 0;
};
struct FieldDetailEnvelope
{
std::int64_t maximumAbsoluteDisplacementSubvoxels = 0;
std::int64_t minimumFeatureSizeSubvoxels = 1;
};
struct FieldBlockContract
{
std::uint32_t readsChannels = 0;
std::uint32_t writesChannels = 0;
std::uint32_t evaluatorVersion = WorldAssemblyEvaluatorVersion;
};
// Point evaluation is canonical at 1/256 voxel. Bounds fail closed when the block or input
// extent is invalid; callers must evaluate when interval() returns unknown.
// L'evaluation est canonique au 1/256 de voxel. Toute borne invalide echoue prudemment; une borne inconnue impose l'evaluation.
class VFCORE_API Heightfield2D
{
public:
Heightfield2D(const WorldAssemblyRecipeV1& recipe,
const WorldAssemblyStratumPlanV1& plan) noexcept;
bool isValid() const noexcept;
HeightfieldPointSample point(std::int64_t xSubvoxels, std::int64_t ySubvoxels,
std::int64_t zSubvoxels) const noexcept;
FieldInterval interval(std::int64_t minimumXSubvoxels,
std::int64_t minimumYSubvoxels,
std::int64_t minimumZSubvoxels,
std::int64_t maximumXSubvoxels,
std::int64_t maximumYSubvoxels,
std::int64_t maximumZSubvoxels) const noexcept;
FieldInfluenceBounds influenceBounds() const noexcept;
FieldDetailEnvelope detailEnvelope() const noexcept;
FieldBlockContract contract() const noexcept;
private:
const WorldAssemblyRecipeV1* recipe_ = nullptr;
const WorldAssemblyStratumPlanV1* plan_ = nullptr;
};
struct WorldAssemblyBoundsAuditResult
{
std::uint64_t boxesTested = 0;
std::uint64_t pointSamples = 0;
std::uint64_t missedBounds = 0;
std::uint64_t unknownBounds = 0;
bool negativeControlDetected = false;
bool passed = false;
};
VFCORE_API WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(
const Heightfield2D& block, std::uint32_t boxCount = 256,
std::uint64_t seed = 0x574f524c44415331ull);
class VFCORE_API ResolvedWorldSnapshot
{
public:
@@ -110,6 +286,7 @@ namespace VoxelForgeCore
explicit operator bool() const noexcept { return static_cast<bool>(data_); }
const WorldConfiguration& configuration() const;
const WorldAssemblyManifest* worldAssembly() const noexcept;
private:
struct Data;
@@ -52,3 +52,4 @@
// VFJson is a small, stateless standalone input parser with no Unreal-shaped names. Keeping its
// value tree shared lets the viewer resolve its reference world and Core snapshot from one parse.
#include "Json.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
+85 -6
View File
@@ -281,11 +281,13 @@ public:
bool bInRoundTunnels=false,bool bInProfileDetailBand=false,
bool bValidatePassageRoutes=true,
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InGpuVisualDetailTiles={},
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InCpuVisualDetailTiles={})
const std::vector<VoxelForgeCore::DetailBandTileRegion>& InCpuVisualDetailTiles={},
const VoxelForgeCore::WorldAssemblyManifest* InWorldAssembly=nullptr)
{
using Clock=std::chrono::steady_clock;
const auto TotalStart=Clock::now();
Owner=&World;WorkerCount=std::clamp(Workers,1,16);DetailVariant=Variant;
Owner=&World;WorldAssembly=InWorldAssembly;
WorkerCount=std::clamp(Workers,1,16);DetailVariant=Variant;
Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels;
bAnalyticOnly=false;
bProfileDetailBand=bInProfileDetailBand;
@@ -919,6 +921,9 @@ public:
// edge interpolation and normals to the existing mesher.
float DensityAt(float X,float Y,float Z) const
{
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
return ApplyEditBrushes(NativeDensity,X,Y,Z);
if(UsesLegacySurfaceField(X,Y,Z))
return LegacySurfaceDensityAt(X,Y,Z);
int32 IX=0,IY=0,IZ=0;
@@ -931,6 +936,10 @@ public:
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
{
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z),NativeDensity))
return ApplyEditBrushes(NativeDensity,X,Y,Z);
if(UsesLegacySurfaceField(static_cast<float>(X),static_cast<float>(Y),
static_cast<float>(Z)))
return LegacySurfaceDensityAt(static_cast<float>(X),static_cast<float>(Y),
@@ -945,6 +954,9 @@ public:
float DensityAtUnclamped(float X,float Y,float Z) const
{
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
return ApplyEditBrushes(NativeDensity,X,Y,Z);
if(UsesLegacySurfaceField(X,Y,Z))
return LegacySurfaceDensityAt(X,Y,Z);
return AnalyticDensityAt(X,Y,Z,false);
@@ -1066,6 +1078,9 @@ private:
if(OutFloorInfluence)*OutFloorInfluence=0.0f;
if(UsesLegacySurfaceField(X,Y,Z))
{
float NativeDensity=0.0f;
if(NativeSurfaceDensityAt(X,Y,Z,NativeDensity))
return bApplyEdits?ApplyEditBrushes(NativeDensity,X,Y,Z):NativeDensity;
const float Density=Owner->Generator->GetDensityAt(X,Y,Z);
return bApplyEdits?ApplyEditBrushes(Density,X,Y,Z):Density;
}
@@ -1119,10 +1134,8 @@ public:
const int32 Pad=std::max(Step,1);
int32 BMin[3]={Origin.X-Pad,Origin.Y-Pad,Origin.Z-Pad};
int32 BMax[3]={Origin.X+(Cells+1)*Step,Origin.Y+(Cells+1)*Step,Origin.Z+(Cells+1)*Step};
// A primitive-only proof cannot classify the biome heightfield. Keep overlapping
// SurfaceWorld boxes mixed so generation samples the exact copied evaluator.
// Une preuve fondee sur les primitives ne classe pas la hauteur par biomes; garder les
// boites SurfaceWorld en Mixed force l'evaluation exacte lors du maillage.
const VoxelForgeCore::WorldAssemblyStratumPlanV1* SurfacePlan=nullptr;
bool bSurfaceOverlap=false,bOtherStrateOverlap=false;
if(Owner&&Owner->Manager)
{
const int32 FirstChunk=FMath::FloorToInt(static_cast<float>(BMin[2])
@@ -1130,10 +1143,24 @@ public:
const int32 LastChunk=FMath::FloorToInt(static_cast<float>(BMax[2])
/static_cast<float>(CHUNK_SIZE));
for(int32 ChunkZ=FirstChunk;ChunkZ<=LastChunk;++ChunkZ)
{
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
==ECaveGeneratorType::SurfaceWorld)
{
bSurfaceOverlap=true;
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
const auto* Candidate=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
if(!Candidate||(SurfacePlan&&SurfacePlan!=Candidate))
return EVoxelTileClass::Mixed;
SurfacePlan=Candidate;
}
else bOtherStrateOverlap=true;
}
}
// 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(bSurfaceOverlap&&bOtherStrateOverlap)return EVoxelTileClass::Mixed;
if(!EditBrushes.empty()&&EditBoundsMin.X<=BMax[0]&&EditBoundsMax.X>=BMin[0]
&&EditBoundsMin.Y<=BMax[1]&&EditBoundsMax.Y>=BMin[1]
&&EditBoundsMin.Z<=BMax[2]&&EditBoundsMax.Z>=BMin[2])
@@ -1155,6 +1182,22 @@ public:
if(R.Lo<=BMax[2]&&R.Hi>=BMin[2]
&&(R.Lo>=BMin[2]||R.Hi<=BMax[2]))
return EVoxelTileClass::Mixed;
if(bSurfaceOverlap)
{
if(!WorldAssembly||!SurfacePlan)return EVoxelTileClass::Mixed;
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*SurfacePlan);
const auto Bounds=Block.interval(
static_cast<int64>(BMin[0])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<int64>(BMin[1])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<int64>(BMin[2])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<int64>(BMax[0])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<int64>(BMax[1])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<int64>(BMax[2])*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
if(!Bounds.known)return EVoxelTileClass::Mixed;
if(Bounds.minimumDensitySubvoxels>0)return EVoxelTileClass::AllAir;
if(Bounds.maximumDensitySubvoxels<0)return EVoxelTileClass::AllSolid;
return 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 bRock?EVoxelTileClass::AllSolid:EVoxelTileClass::AllAir;
@@ -1619,6 +1662,41 @@ public:
}
private:
const VoxelForgeCore::WorldAssemblyStratumPlanV1* FindWorldAssemblyStratum(
int32 StrateIndex,int32 ChunkZ) const
{
if(!WorldAssembly||StrateIndex<0)return nullptr;
const auto It=std::find_if(WorldAssembly->plan.strates.begin(),WorldAssembly->plan.strates.end(),
[StrateIndex,ChunkZ](const auto& Entry)
{return Entry.strateIndex==StrateIndex&&ChunkZ<=Entry.topChunkZ&&ChunkZ>=Entry.bottomChunkZ;});
return It==WorldAssembly->plan.strates.end()?nullptr:&*It;
}
bool NativeSurfaceDensityAt(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;
const int32 ChunkZ=FMath::FloorToInt(Z/static_cast<float>(CHUNK_SIZE));
if(Owner->Manager->GetGeneratorTypeForChunk(FIntVector(0,0,ChunkZ))
!=ECaveGeneratorType::SurfaceWorld)return false;
const int32 StrateIndex=Owner->Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(ChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
const auto* Plan=FindWorldAssemblyStratum(StrateIndex,ChunkZ);
if(!Plan)return false;
constexpr double Scale=static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
const double MaxVoxel=static_cast<double>(std::numeric_limits<int64>::max())/Scale;
if(std::abs(static_cast<double>(X))>MaxVoxel||std::abs(static_cast<double>(Y))>MaxVoxel
||std::abs(static_cast<double>(Z))>MaxVoxel)return false;
const auto ToFixed=[](float Value)
{return static_cast<int64>(std::llround(static_cast<double>(Value)
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));};
const VoxelForgeCore::Heightfield2D Block(WorldAssembly->recipe,*Plan);
if(!Block.isValid())return false;
OutDensity=static_cast<float>(Block.point(ToFixed(X),ToFixed(Y),ToFixed(Z)).densitySubvoxels
/Scale);
return true;
}
bool UsesLegacySurfaceField(float X,float Y,float Z) const
{
if(!Owner||!Owner->Manager||!Owner->Generator)return false;
@@ -4850,6 +4928,7 @@ private:
}
FStandaloneOwnerWorld* Owner=nullptr;
const VoxelForgeCore::WorldAssemblyManifest* WorldAssembly=nullptr;
int32 WorkerCount=6;
const VoxelForgeCore::JobScheduler* Scheduler=nullptr;
bool RoundTunnelsEnabled=false;
+30 -5
View File
@@ -1,6 +1,7 @@
#include "CoreNamespacePreamble.h"
#include "StandaloneSnapshotAdapter.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include "WorldAssemblerInternal.h"
#include <shared_mutex>
#include <mutex>
@@ -23,6 +24,7 @@ namespace VoxelForgeCore
struct ResolvedWorldSnapshot::Data
{
std::shared_ptr<FStandaloneOwnerWorld> owner;
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
WorldConfiguration configuration;
};
@@ -216,12 +218,14 @@ namespace VoxelForgeCore
struct detail_StandaloneSnapshotAdapter
{
static ResolvedWorldSnapshot FromOwner(std::shared_ptr<FStandaloneOwnerWorld> owner)
static ResolvedWorldSnapshot FromOwner(std::shared_ptr<FStandaloneOwnerWorld> owner,
std::shared_ptr<const WorldAssemblyManifest> worldAssembly = {})
{
if(!owner||!owner->Settings||!owner->Manager||!owner->Generator)return {};
auto data=std::make_shared<ResolvedWorldSnapshot::Data>();
data->configuration=MakeConfiguration(*owner);
data->owner=std::move(owner);
data->worldAssembly=std::move(worldAssembly);
return ResolvedWorldSnapshot(std::move(data));
}
};
@@ -232,6 +236,13 @@ namespace VoxelForgeCore
{
auto owner=std::make_shared<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::Resolve(jsonRoot,*owner,error))return {};
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
if(jsonRoot.Get("world_assembly").IsObject())
{
auto parsed=detail_world_assembly::ParseNestedManifest(jsonRoot,error);
if(!parsed)return {};
worldAssembly=std::make_shared<WorldAssemblyManifest>(std::move(*parsed));
}
const bool seedChanged=owner->Seed!=seed;
owner->Settings->Seed=seed;
@@ -254,7 +265,7 @@ namespace VoxelForgeCore
owner->MaxClipLevel=maxLodOverride;
owner->Settings->MaxClipLevel=maxLodOverride;
}
return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner));
return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner),std::move(worldAssembly));
}
std::optional<ResolvedWorldSnapshot> ResolvedWorldSnapshot::FromJson(
@@ -262,8 +273,20 @@ namespace VoxelForgeCore
{
auto owner=std::make_shared<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::LoadJson(json,*owner,error))return std::nullopt;
return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner));
VFJson::Value root;
if(!VFJson::Parser(std::string(json)).Parse(root,error))return std::nullopt;
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
if(root.Get("world_assembly").IsObject())
{
auto parsed=detail_world_assembly::ParseNestedManifest(root,error);
if(!parsed)return std::nullopt;
worldAssembly=std::make_shared<WorldAssemblyManifest>(std::move(*parsed));
}
return detail_StandaloneSnapshotAdapter::FromOwner(std::move(owner),std::move(worldAssembly));
}
const WorldAssemblyManifest* ResolvedWorldSnapshot::worldAssembly() const noexcept
{ return data_?data_->worldAssembly.get():nullptr; }
const WorldConfiguration& ResolvedWorldSnapshot::configuration() const
{
@@ -274,6 +297,7 @@ namespace VoxelForgeCore
struct World::Impl
{
std::shared_ptr<FStandaloneOwnerWorld> owner;
std::shared_ptr<const WorldAssemblyManifest> worldAssembly;
WorldConfiguration configuration;
std::unique_ptr<FStandaloneShapeWorld> shape;
ShapeBuildRequest buildRequest;
@@ -291,6 +315,7 @@ namespace VoxelForgeCore
if(snapshot.data_)
{
impl_->owner=snapshot.data_->owner;
impl_->worldAssembly=snapshot.data_->worldAssembly;
impl_->configuration=snapshot.data_->configuration;
}
}
@@ -307,7 +332,7 @@ namespace VoxelForgeCore
static_cast<int32>(std::clamp<std::uint32_t>(request.workerLimit,1,16)),ToNative(request.detail),
request.includeFarWorld,error,&scheduler,request.roundTunnels,request.profileDetailBand,
request.validatePassageRoutes,request.gpuVisualDetailTiles,
request.cpuVisualDetailTiles);
request.cpuVisualDetailTiles,impl_->worldAssembly.get());
if(ok)
{
impl_->shape=std::move(shape);
@@ -443,7 +468,7 @@ namespace VoxelForgeCore
auto region=std::make_shared<FStandaloneShapeWorld>();
if(!region->Build(*impl_->owner,fineMin,fineMax,regionMin,regionMax,
static_cast<int32>(clampedWorkers),ToNative(base.detail),false,error,&scheduler,
base.roundTunnels,base.profileDetailBand,false))return false;
base.roundTunnels,base.profileDetailBand,false,{}, {},impl_->worldAssembly.get()))return false;
if(!impl_->editLog.empty())region->SetEditTransaction(impl_->editLog);
const auto& build=region->GetStats();
++stats.regionsBuilt;
@@ -0,0 +1,766 @@
#pragma once
#include "Json.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include <algorithm>
#include <charconv>
#include <cmath>
#include <limits>
#include <sstream>
namespace VoxelForgeCore
{
namespace world_assembly_detail
{
constexpr std::int64_t Q16 = 1ll << 16;
constexpr std::int64_t MaxSerializedMagnitude = 1000000000000ll;
constexpr std::size_t MaxBiomeCount = 8;
constexpr std::size_t MaxStrateCount = 64;
constexpr std::size_t MaxFeatureCount = 4096;
constexpr std::size_t MaxRoutePointCount = 4096;
inline void HashWord(std::uint64_t& hash, std::uint64_t word, int bytes)
{
for (int i = 0; i < bytes; ++i)
{
hash ^= static_cast<std::uint8_t>(word >> (i * 8));
hash *= 1099511628211ull;
}
}
inline std::uint64_t StartHash() { return 14695981039346656037ull; }
inline bool ReadInteger(const VFJson::Value& value, std::int64_t& out)
{
if (!value.IsNumber()) return false;
const double number = value.Number();
constexpr double ExactJsonIntegerLimit = 9007199254740991.0;
if (!std::isfinite(number) || std::trunc(number) != number
|| std::abs(number) > ExactJsonIntegerLimit) return false;
out = static_cast<std::int64_t>(number);
return true;
}
inline bool ReadU32(const VFJson::Value& value, std::uint32_t& out)
{
std::int64_t parsed = 0;
if (!ReadInteger(value, parsed) || parsed < 0
|| parsed > std::numeric_limits<std::uint32_t>::max()) return false;
out = static_cast<std::uint32_t>(parsed);
return true;
}
inline bool ReadI32(const VFJson::Value& value, std::int32_t& out)
{
std::int64_t parsed = 0;
if (!ReadInteger(value, parsed) || parsed < std::numeric_limits<std::int32_t>::min()
|| parsed > std::numeric_limits<std::int32_t>::max()) return false;
out = static_cast<std::int32_t>(parsed);
return true;
}
inline bool ReadArray(const VFJson::Value& value, std::int64_t* out, std::size_t count)
{
if (!value.IsArray() || value.AsArray().size() != count) return false;
for (std::size_t i = 0; i < count; ++i)
if (!ReadInteger(value.AsArray()[i], out[i])) return false;
return true;
}
inline bool ReadHash(const VFJson::Value& value, std::uint64_t& out)
{
if (!value.IsString()) return false;
std::string_view text = value.String();
if (text.starts_with("0x") || text.starts_with("0X")) text.remove_prefix(2);
if (text.empty() || text.size() > 16) return false;
const auto result = std::from_chars(text.data(), text.data() + text.size(), out, 16);
return result.ec == std::errc{} && result.ptr == text.data() + text.size();
}
inline std::string HashText(std::uint64_t hash)
{
constexpr char Hex[] = "0123456789abcdef";
std::string out(16, '0');
for (int i = 15; i >= 0; --i)
{
out[static_cast<std::size_t>(i)] = Hex[hash & 0xfu];
hash >>= 4;
}
return out;
}
inline bool ValidScalar(std::int64_t value)
{ return value >= -MaxSerializedMagnitude && value <= MaxSerializedMagnitude; }
inline bool OutsideRange(std::int64_t value)
{ return value < -MaxSerializedMagnitude || value > MaxSerializedMagnitude; }
inline bool ValidateRecipe(const WorldAssemblyRecipeV1& recipe, std::string& error)
{
if (recipe.schemaVersion != WorldAssemblySchemaVersion
|| recipe.evaluatorVersion != WorldAssemblyEvaluatorVersion
|| recipe.unitsPerVoxel != WorldAssemblySubvoxelsPerVoxel)
{ error = "world assembly recipe version, evaluator or fixed scale is unsupported"; return false; }
if (recipe.biomes.empty() || recipe.biomes.size() > MaxBiomeCount)
{ error = "world assembly recipe must contain one to eight biome profiles"; return false; }
if (recipe.biomeCellSizeSubvoxels <= 0 || recipe.biomeCellSizeSubvoxels > 100000000ll
|| recipe.biomeWarpSubvoxels < 0 || recipe.biomeWarpSubvoxels > recipe.biomeCellSizeSubvoxels / 2
|| recipe.biomeWarpWavelengthSubvoxels <= 0
|| recipe.biomeWarpWavelengthSubvoxels > 100000000ll
|| recipe.biomeBorderBlendSubvoxels < 0
|| recipe.biomeBorderBlendSubvoxels > recipe.biomeCellSizeSubvoxels)
{ error = "world assembly biome-map parameters are outside the supported fixed-scale range"; return false; }
std::vector<std::uint32_t> ids;
ids.reserve(recipe.biomes.size());
for (const auto& biome : recipe.biomes)
{
if (biome.stableId == 0 || std::find(ids.begin(), ids.end(), biome.stableId) != ids.end())
{ error = "world assembly biome IDs must be nonzero and unique"; return false; }
ids.push_back(biome.stableId);
const std::array<std::int64_t, 7> values = {biome.baseHeightSubvoxels,
biome.continentAmplitudeSubvoxels, biome.continentWavelengthSubvoxels,
biome.mountainAmplitudeSubvoxels, biome.mountainWavelengthSubvoxels,
biome.detailAmplitudeSubvoxels, biome.detailWavelengthSubvoxels};
if (!std::all_of(values.begin(), values.end(), ValidScalar)
|| biome.continentAmplitudeSubvoxels < 0 || biome.mountainAmplitudeSubvoxels < 0
|| biome.detailAmplitudeSubvoxels < 0 || biome.continentWavelengthSubvoxels <= 0
|| biome.mountainWavelengthSubvoxels <= 0 || biome.detailWavelengthSubvoxels <= 0)
{ error = "world assembly biome profile has an invalid fixed-scale value"; return false; }
}
return true;
}
inline bool ValidateStratum(const WorldAssemblyStratumPlanV1& stratum, std::string& error)
{
if (stratum.strateIndex < 0 || stratum.topChunkZ < stratum.bottomChunkZ
|| stratum.capWavelengthSubvoxels <= 0 || stratum.capUndulationSubvoxels < 0
|| stratum.features.size() > MaxFeatureCount || stratum.route.size() > MaxRoutePointCount)
{ error = "world assembly stratum plan has an invalid extent or exceeds its bounded payload"; return false; }
if (!ValidScalar(stratum.capHeightSubvoxels) || !ValidScalar(stratum.capUndulationSubvoxels)
|| !ValidScalar(stratum.capWavelengthSubvoxels))
{ error = "world assembly cap contains a value outside the fixed-scale range"; return false; }
std::vector<std::uint64_t> ids;
ids.reserve(stratum.features.size());
for (const auto& feature : stratum.features)
{
if (feature.stableId == 0 || feature.type < 1 || feature.type > 3
|| std::find(ids.begin(), ids.end(), feature.stableId) != ids.end())
{ error = "world assembly feature IDs and types must be stable, unique and supported"; return false; }
ids.push_back(feature.stableId);
for (std::size_t axis = 0; axis < 3; ++axis)
if (!ValidScalar(feature.minimumSubvoxels[axis])
|| !ValidScalar(feature.maximumSubvoxels[axis])
|| feature.minimumSubvoxels[axis] > feature.maximumSubvoxels[axis])
{ error = "world assembly feature has an invalid influence bound"; return false; }
if (!std::all_of(feature.parametersSubvoxels.begin(), feature.parametersSubvoxels.end(), ValidScalar))
{ error = "world assembly feature parameter is outside the fixed-scale range"; return false; }
}
for (const auto& point : stratum.route)
{
if (point.stableFeatureId == 0 || point.radiusSubvoxels <= 0
|| !ValidScalar(point.radiusSubvoxels)
|| !std::all_of(point.positionSubvoxels.begin(), point.positionSubvoxels.end(), ValidScalar))
{ error = "world assembly route point has an invalid feature ID, position or radius"; return false; }
}
return true;
}
inline std::uint64_t HashRecipe(const WorldAssemblyRecipeV1& recipe)
{
std::uint64_t hash = StartHash();
HashWord(hash, recipe.schemaVersion, 4); HashWord(hash, recipe.evaluatorVersion, 4);
HashWord(hash, recipe.unitsPerVoxel, 4); HashWord(hash, recipe.seed, 4);
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeCellSizeSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeWarpSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeWarpWavelengthSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(recipe.biomeBorderBlendSubvoxels), 8);
HashWord(hash, recipe.biomes.size(), 4);
for (const auto& biome : recipe.biomes)
{
HashWord(hash, biome.stableId, 4);
HashWord(hash, static_cast<std::uint64_t>(biome.baseHeightSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(biome.continentAmplitudeSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(biome.continentWavelengthSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(biome.mountainAmplitudeSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(biome.mountainWavelengthSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(biome.detailAmplitudeSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(biome.detailWavelengthSubvoxels), 8);
}
return hash;
}
inline std::uint64_t HashStratum(const WorldAssemblyStratumPlanV1& stratum)
{
std::uint64_t hash = StartHash();
HashWord(hash, static_cast<std::uint32_t>(stratum.strateIndex), 4);
HashWord(hash, static_cast<std::uint32_t>(stratum.topChunkZ), 4);
HashWord(hash, static_cast<std::uint32_t>(stratum.bottomChunkZ), 4);
HashWord(hash, static_cast<std::uint64_t>(stratum.capHeightSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(stratum.capUndulationSubvoxels), 8);
HashWord(hash, static_cast<std::uint64_t>(stratum.capWavelengthSubvoxels), 8);
HashWord(hash, stratum.features.size(), 4);
for (const auto& feature : stratum.features)
{
HashWord(hash, feature.stableId, 8); HashWord(hash, feature.type, 4);
for (const auto value : feature.minimumSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
for (const auto value : feature.maximumSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
for (const auto value : feature.parametersSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
}
HashWord(hash, stratum.route.size(), 4);
for (const auto& point : stratum.route)
{
HashWord(hash, point.stableFeatureId, 8);
for (const auto value : point.positionSubvoxels) HashWord(hash, static_cast<std::uint64_t>(value), 8);
HashWord(hash, static_cast<std::uint64_t>(point.radiusSubvoxels), 8);
}
return hash;
}
inline std::uint64_t HashPlan(const WorldAssemblyPlanV1& plan)
{
std::uint64_t hash = StartHash();
HashWord(hash, plan.schemaVersion, 4); HashWord(hash, plan.strates.size(), 4);
for (const auto& stratum : plan.strates) HashWord(hash, stratum.contentHash, 8);
return hash;
}
inline std::uint64_t HashManifest(const WorldAssemblyManifest& manifest)
{
std::uint64_t hash = StartHash();
HashWord(hash, manifest.schemaVersion, 4);
HashWord(hash, manifest.recipe.contentHash, 8);
HashWord(hash, manifest.plan.contentHash, 8);
return hash;
}
inline bool ParseRecipe(const VFJson::Value& value, WorldAssemblyRecipeV1& recipe,
std::string& error)
{
if (!value.IsObject() || !ReadU32(value.Get("schema_version"), recipe.schemaVersion)
|| !ReadU32(value.Get("evaluator_version"), recipe.evaluatorVersion)
|| !ReadU32(value.Get("units_per_voxel"), recipe.unitsPerVoxel)
|| !ReadU32(value.Get("seed"), recipe.seed)
|| !ReadInteger(value.Get("biome_cell_size_q"), recipe.biomeCellSizeSubvoxels)
|| !ReadInteger(value.Get("biome_warp_q"), recipe.biomeWarpSubvoxels)
|| !ReadInteger(value.Get("biome_warp_wavelength_q"), recipe.biomeWarpWavelengthSubvoxels)
|| !ReadInteger(value.Get("biome_border_blend_q"), recipe.biomeBorderBlendSubvoxels)
|| !ReadHash(value.Get("content_hash"), recipe.contentHash)
|| !value.Get("biomes").IsArray())
{ error = "world assembly recipe is missing a required versioned fixed-scale field"; return false; }
for (const auto& item : value.Get("biomes").AsArray())
{
WorldAssemblyBiomeProfileV1 biome;
if (!item.IsObject() || !ReadU32(item.Get("id"), biome.stableId)
|| !ReadInteger(item.Get("base_height_q"), biome.baseHeightSubvoxels)
|| !ReadInteger(item.Get("continent_amplitude_q"), biome.continentAmplitudeSubvoxels)
|| !ReadInteger(item.Get("continent_wavelength_q"), biome.continentWavelengthSubvoxels)
|| !ReadInteger(item.Get("mountain_amplitude_q"), biome.mountainAmplitudeSubvoxels)
|| !ReadInteger(item.Get("mountain_wavelength_q"), biome.mountainWavelengthSubvoxels)
|| !ReadInteger(item.Get("detail_amplitude_q"), biome.detailAmplitudeSubvoxels)
|| !ReadInteger(item.Get("detail_wavelength_q"), biome.detailWavelengthSubvoxels))
{ error = "world assembly biome profile is missing a required fixed-scale field"; return false; }
recipe.biomes.push_back(biome);
}
return true;
}
inline bool ParsePlan(const VFJson::Value& value, WorldAssemblyPlanV1& plan,
std::string& error)
{
if (!value.IsObject() || !ReadU32(value.Get("schema_version"), plan.schemaVersion)
|| !ReadHash(value.Get("content_hash"), plan.contentHash)
|| !value.Get("strates").IsArray())
{ error = "world assembly plan is missing a required versioned field"; return false; }
for (const auto& item : value.Get("strates").AsArray())
{
WorldAssemblyStratumPlanV1 stratum;
if (!item.IsObject() || !ReadI32(item.Get("strate_index"), stratum.strateIndex)
|| !ReadI32(item.Get("top_chunk_z"), stratum.topChunkZ)
|| !ReadI32(item.Get("bottom_chunk_z"), stratum.bottomChunkZ)
|| !ReadInteger(item.Get("cap_height_q"), stratum.capHeightSubvoxels)
|| !ReadInteger(item.Get("cap_undulation_q"), stratum.capUndulationSubvoxels)
|| !ReadInteger(item.Get("cap_wavelength_q"), stratum.capWavelengthSubvoxels)
|| !ReadHash(item.Get("content_hash"), stratum.contentHash)
|| !item.Get("features").IsArray() || !item.Get("route").IsArray())
{ error = "world assembly stratum is missing a required fixed-scale field"; return false; }
for (const auto& featureValue : item.Get("features").AsArray())
{
WorldAssemblyFeatureV1 feature;
std::uint32_t type = 0;
if (!featureValue.IsObject() || !ReadHash(featureValue.Get("stable_id"), feature.stableId)
|| !ReadU32(featureValue.Get("type"), type)
|| !ReadArray(featureValue.Get("min_q"), feature.minimumSubvoxels.data(), 3)
|| !ReadArray(featureValue.Get("max_q"), feature.maximumSubvoxels.data(), 3)
|| !ReadArray(featureValue.Get("parameters_q"), feature.parametersSubvoxels.data(), 4))
{ error = "world assembly feature is missing a fixed-scale field"; return false; }
feature.type = type;
stratum.features.push_back(feature);
}
for (const auto& pointValue : item.Get("route").AsArray())
{
WorldAssemblyRoutePointV1 point;
if (!pointValue.IsObject() || !ReadHash(pointValue.Get("feature_id"), point.stableFeatureId)
|| !ReadArray(pointValue.Get("position_q"), point.positionSubvoxels.data(), 3)
|| !ReadInteger(pointValue.Get("radius_q"), point.radiusSubvoxels))
{ error = "world assembly route point is missing a fixed-scale field"; return false; }
stratum.route.push_back(point);
}
plan.strates.push_back(std::move(stratum));
}
return true;
}
inline std::optional<WorldAssemblyManifest> ParseManifestValue(const VFJson::Value& value,
std::string& error)
{
WorldAssemblyManifest manifest;
if (!value.IsObject() || !ReadU32(value.Get("schema_version"), manifest.schemaVersion)
|| !ReadHash(value.Get("content_hash"), manifest.contentHash)
|| !ParseRecipe(value.Get("recipe"), manifest.recipe, error)
|| !ParsePlan(value.Get("plan"), manifest.plan, error))
{
if (error.empty()) error = "world assembly manifest is missing a required field";
return std::nullopt;
}
if (!manifest.validate(error)) return std::nullopt;
return manifest;
}
inline std::int64_t FloorDiv(std::int64_t value, std::int64_t divisor)
{
const std::int64_t quotient = value / divisor, remainder = value % divisor;
return quotient - ((remainder != 0 && remainder < 0) ? 1 : 0);
}
inline std::int64_t FloorMod(std::int64_t value, std::int64_t divisor)
{ return value - FloorDiv(value, divisor) * divisor; }
inline std::uint64_t Mix64(std::uint64_t value)
{
value += 0x9e3779b97f4a7c15ull;
value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ull;
value = (value ^ (value >> 27)) * 0x94d049bb133111ebull;
return value ^ (value >> 31);
}
inline std::uint64_t CoordinateHash(std::uint32_t seed, std::int64_t x,
std::int64_t y, std::uint64_t salt)
{
std::uint64_t hash = Mix64(static_cast<std::uint64_t>(seed) ^ salt);
hash ^= Mix64(static_cast<std::uint64_t>(x) + 0x517cc1b727220a95ull);
hash ^= Mix64(static_cast<std::uint64_t>(y) + 0x6eed0e9da4d94a4full);
return Mix64(hash);
}
inline std::int32_t LatticeValue(std::uint32_t seed, std::int64_t x,
std::int64_t y, std::uint64_t salt)
{
const std::uint32_t bits = static_cast<std::uint32_t>(CoordinateHash(seed, x, y, salt) >> 48) & 0x1ffffu;
return static_cast<std::int32_t>(bits) - 65536;
}
inline std::int32_t LerpQ16(std::int32_t a, std::int32_t b, std::uint32_t t)
{ return static_cast<std::int32_t>(a + (static_cast<std::int64_t>(b - a) * t) / Q16); }
inline std::uint32_t SmoothQ16(std::uint32_t t)
{
const std::int64_t t2 = (static_cast<std::int64_t>(t) * t) / Q16;
return static_cast<std::uint32_t>((t2 * (3 * Q16 - 2 * static_cast<std::int64_t>(t))) / Q16);
}
inline std::int32_t Noise2D(std::int64_t x, std::int64_t y, std::int64_t wavelength,
std::uint32_t seed, std::uint64_t salt)
{
const std::int64_t cellX = FloorDiv(x, wavelength), cellY = FloorDiv(y, wavelength);
const std::uint32_t fractionX = static_cast<std::uint32_t>((FloorMod(x, wavelength) * Q16) / wavelength);
const std::uint32_t fractionY = static_cast<std::uint32_t>((FloorMod(y, wavelength) * Q16) / wavelength);
const std::uint32_t smoothX = SmoothQ16(fractionX), smoothY = SmoothQ16(fractionY);
const std::int32_t low = LerpQ16(LatticeValue(seed, cellX, cellY, salt),
LatticeValue(seed, cellX + 1, cellY, salt), smoothX);
const std::int32_t high = LerpQ16(LatticeValue(seed, cellX, cellY + 1, salt),
LatticeValue(seed, cellX + 1, cellY + 1, salt), smoothX);
return LerpQ16(low, high, smoothY);
}
inline std::uint32_t RatioQ16(std::uint64_t numerator, std::uint64_t denominator)
{
if (denominator == 0 || numerator >= denominator) return 65536;
std::uint32_t result = 0;
std::uint64_t remainder = numerator;
for (int bit = 15; bit >= 0; --bit)
{
remainder *= 2;
if (remainder >= denominator)
{ remainder -= denominator; result |= (1u << bit); }
}
return result;
}
inline std::int64_t MulQ16(std::int64_t value, std::int64_t q16)
{ return (value * q16) / Q16; }
inline std::int64_t ProfileHeight(const WorldAssemblyBiomeProfileV1& biome,
std::int64_t x, std::int64_t y, std::uint32_t seed)
{
const std::int64_t continent = MulQ16(biome.continentAmplitudeSubvoxels,
Noise2D(x, y, biome.continentWavelengthSubvoxels, seed, 0x434f4e54494e454eull));
const std::int64_t mountain = MulQ16(biome.mountainAmplitudeSubvoxels,
Noise2D(x, y, biome.mountainWavelengthSubvoxels, seed, 0x4d4f554e5441494eull));
const std::int64_t detail = MulQ16(biome.detailAmplitudeSubvoxels,
Noise2D(x, y, biome.detailWavelengthSubvoxels, seed, 0x44455441494c3031ull));
return biome.baseHeightSubvoxels + continent + mountain + detail;
}
inline bool InInterval(const FieldInterval& interval, std::int64_t density)
{ return interval.known && density >= interval.minimumDensitySubvoxels
&& density <= interval.maximumDensitySubvoxels; }
}
namespace detail_world_assembly
{
inline std::optional<WorldAssemblyManifest> ParseNestedManifest(const VFJson::Value& root,
std::string& error)
{
const VFJson::Value& payload = root.Get("world_assembly");
if (!payload.IsObject()) return std::nullopt;
return world_assembly_detail::ParseManifestValue(payload, error);
}
}
std::optional<WorldAssemblyManifest> WorldAssemblyManifest::FromJson(
std::string_view json, std::string& error)
{
VFJson::Value root;
if (!VFJson::Parser(std::string(json)).Parse(root, error)) return std::nullopt;
return world_assembly_detail::ParseManifestValue(root, error);
}
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; }
std::vector<std::int32_t> strateIds;
for (auto& stratum : plan.strates)
{
if (!world_assembly_detail::ValidateStratum(stratum, error)) return false;
if (std::find(strateIds.begin(), strateIds.end(), stratum.strateIndex) != strateIds.end())
{ error = "world assembly plan contains a duplicate strate index"; return false; }
strateIds.push_back(stratum.strateIndex);
stratum.contentHash = world_assembly_detail::HashStratum(stratum);
}
recipe.contentHash = world_assembly_detail::HashRecipe(recipe);
plan.contentHash = world_assembly_detail::HashPlan(plan);
contentHash = world_assembly_detail::HashManifest(*this);
return true;
}
bool WorldAssemblyManifest::validate(std::string& error) const
{
WorldAssemblyManifest copy = *this;
const std::uint64_t expectedRecipe = recipe.contentHash;
const std::uint64_t expectedPlan = plan.contentHash;
const std::uint64_t expectedManifest = contentHash;
std::vector<std::uint64_t> expectedStrates;
for (const auto& stratum : plan.strates) expectedStrates.push_back(stratum.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())
{ 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; }
return true;
}
std::string WorldAssemblyManifest::ToJson() const
{
std::string error;
WorldAssemblyManifest value = *this;
if (!value.finalize(error)) return {};
std::ostringstream out;
out << "{\"schema_version\":" << value.schemaVersion
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.contentHash)
<< "\",\"recipe\":{\"schema_version\":" << value.recipe.schemaVersion
<< ",\"evaluator_version\":" << value.recipe.evaluatorVersion
<< ",\"units_per_voxel\":" << value.recipe.unitsPerVoxel
<< ",\"seed\":" << value.recipe.seed
<< ",\"biome_cell_size_q\":" << value.recipe.biomeCellSizeSubvoxels
<< ",\"biome_warp_q\":" << value.recipe.biomeWarpSubvoxels
<< ",\"biome_warp_wavelength_q\":" << value.recipe.biomeWarpWavelengthSubvoxels
<< ",\"biome_border_blend_q\":" << value.recipe.biomeBorderBlendSubvoxels
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.recipe.contentHash)
<< "\",\"biomes\":[";
for (std::size_t i = 0; i < value.recipe.biomes.size(); ++i)
{
const auto& biome = value.recipe.biomes[i];
if (i) out << ',';
out << "{\"id\":" << biome.stableId << ",\"base_height_q\":" << biome.baseHeightSubvoxels
<< ",\"continent_amplitude_q\":" << biome.continentAmplitudeSubvoxels
<< ",\"continent_wavelength_q\":" << biome.continentWavelengthSubvoxels
<< ",\"mountain_amplitude_q\":" << biome.mountainAmplitudeSubvoxels
<< ",\"mountain_wavelength_q\":" << biome.mountainWavelengthSubvoxels
<< ",\"detail_amplitude_q\":" << biome.detailAmplitudeSubvoxels
<< ",\"detail_wavelength_q\":" << biome.detailWavelengthSubvoxels << '}';
}
out << "]},\"plan\":{\"schema_version\":" << value.plan.schemaVersion
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(value.plan.contentHash)
<< "\",\"strates\":[";
for (std::size_t i = 0; i < value.plan.strates.size(); ++i)
{
const auto& stratum = value.plan.strates[i];
if (i) out << ',';
out << "{\"strate_index\":" << stratum.strateIndex << ",\"top_chunk_z\":" << stratum.topChunkZ
<< ",\"bottom_chunk_z\":" << stratum.bottomChunkZ
<< ",\"cap_height_q\":" << stratum.capHeightSubvoxels
<< ",\"cap_undulation_q\":" << stratum.capUndulationSubvoxels
<< ",\"cap_wavelength_q\":" << stratum.capWavelengthSubvoxels
<< ",\"content_hash\":\"" << world_assembly_detail::HashText(stratum.contentHash)
<< "\",\"features\":[";
for (std::size_t f = 0; f < stratum.features.size(); ++f)
{
const auto& feature = stratum.features[f];
if (f) out << ',';
out << "{\"stable_id\":\"" << world_assembly_detail::HashText(feature.stableId)
<< "\",\"type\":" << feature.type << ",\"min_q\":["
<< feature.minimumSubvoxels[0] << ',' << feature.minimumSubvoxels[1] << ',' << feature.minimumSubvoxels[2]
<< "],\"max_q\":[" << feature.maximumSubvoxels[0] << ',' << feature.maximumSubvoxels[1]
<< ',' << feature.maximumSubvoxels[2] << "],\"parameters_q\":["
<< feature.parametersSubvoxels[0] << ',' << feature.parametersSubvoxels[1] << ','
<< feature.parametersSubvoxels[2] << ',' << feature.parametersSubvoxels[3] << "]}";
}
out << "],\"route\":[";
for (std::size_t p = 0; p < stratum.route.size(); ++p)
{
const auto& point = stratum.route[p];
if (p) out << ',';
out << "{\"feature_id\":\"" << world_assembly_detail::HashText(point.stableFeatureId)
<< "\",\"position_q\":[" << point.positionSubvoxels[0] << ','
<< point.positionSubvoxels[1] << ',' << point.positionSubvoxels[2]
<< "],\"radius_q\":" << point.radiusSubvoxels << '}';
}
out << "]}";
}
out << "]}}";
return out.str();
}
Heightfield2D::Heightfield2D(const WorldAssemblyRecipeV1& recipe,
const WorldAssemblyStratumPlanV1& plan) noexcept
: recipe_(&recipe), plan_(&plan)
{}
bool Heightfield2D::isValid() const noexcept
{
if (!recipe_ || !plan_ || recipe_->schemaVersion != WorldAssemblySchemaVersion
|| recipe_->evaluatorVersion != WorldAssemblyEvaluatorVersion
|| recipe_->unitsPerVoxel != WorldAssemblySubvoxelsPerVoxel
|| recipe_->biomes.empty() || recipe_->biomes.size() > world_assembly_detail::MaxBiomeCount
|| recipe_->biomeCellSizeSubvoxels <= 0 || recipe_->biomeWarpWavelengthSubvoxels <= 0
|| recipe_->biomeWarpSubvoxels < 0 || recipe_->biomeBorderBlendSubvoxels < 0
|| plan_->strateIndex < 0 || plan_->topChunkZ < plan_->bottomChunkZ
|| plan_->capWavelengthSubvoxels <= 0 || plan_->capUndulationSubvoxels < 0)
return false;
return std::all_of(recipe_->biomes.begin(), recipe_->biomes.end(), [](const auto& biome)
{
return biome.stableId != 0 && biome.continentAmplitudeSubvoxels >= 0
&& biome.mountainAmplitudeSubvoxels >= 0 && biome.detailAmplitudeSubvoxels >= 0
&& biome.continentWavelengthSubvoxels > 0 && biome.mountainWavelengthSubvoxels > 0
&& biome.detailWavelengthSubvoxels > 0;
});
}
HeightfieldPointSample Heightfield2D::point(std::int64_t x, std::int64_t y,
std::int64_t z) const noexcept
{
HeightfieldPointSample out;
if (!isValid() || world_assembly_detail::OutsideRange(x)
|| world_assembly_detail::OutsideRange(y)
|| world_assembly_detail::OutsideRange(z)) return out;
using namespace world_assembly_detail;
const std::int64_t warpX = MulQ16(recipe_->biomeWarpSubvoxels,
Noise2D(x, y, recipe_->biomeWarpWavelengthSubvoxels, recipe_->seed, 0x574152505f585f31ull));
const std::int64_t warpY = MulQ16(recipe_->biomeWarpSubvoxels,
Noise2D(x, y, recipe_->biomeWarpWavelengthSubvoxels, recipe_->seed, 0x574152505f595f31ull));
const std::int64_t warpedX = x + warpX, warpedY = y + warpY;
const std::int64_t cellX = FloorDiv(warpedX, recipe_->biomeCellSizeSubvoxels);
const std::int64_t cellY = FloorDiv(warpedY, recipe_->biomeCellSizeSubvoxels);
std::uint64_t nearestDistance = std::numeric_limits<std::uint64_t>::max();
std::uint64_t secondDistance = std::numeric_limits<std::uint64_t>::max();
std::uint32_t nearestId = 0, secondId = 0;
const std::uint32_t jitterSeed = recipe_->seed ^ 0xa341316cu;
const std::int64_t cellSize = recipe_->biomeCellSizeSubvoxels;
for (std::int64_t cy = cellY - 2; cy <= cellY + 2; ++cy)
for (std::int64_t cx = cellX - 2; cx <= cellX + 2; ++cx)
{
const std::uint64_t h = CoordinateHash(jitterSeed, cx, cy, 0x42494f4d455f5349ull);
const std::int64_t jitterX = static_cast<std::int64_t>((h & 0xffffu)) * (cellSize / 2) / 65535 - cellSize / 4;
const std::int64_t jitterY = static_cast<std::int64_t>(((h >> 16) & 0xffffu)) * (cellSize / 2) / 65535 - cellSize / 4;
const std::int64_t siteX = cx * cellSize + cellSize / 2 + jitterX;
const std::int64_t siteY = cy * cellSize + cellSize / 2 + jitterY;
const std::int64_t dx = warpedX - siteX, dy = warpedY - siteY;
const std::uint64_t distance = static_cast<std::uint64_t>(dx * dx + dy * dy);
const std::uint32_t biomeId = recipe_->biomes[CoordinateHash(jitterSeed, cx, cy,
0x42494f4d455f4944ull) % recipe_->biomes.size()].stableId;
if (distance < nearestDistance)
{
secondDistance = nearestDistance; secondId = nearestId;
nearestDistance = distance; nearestId = biomeId;
}
else if (distance < secondDistance && biomeId != nearestId)
{ secondDistance = distance; secondId = biomeId; }
}
const auto findBiome = [&](std::uint32_t id) -> const WorldAssemblyBiomeProfileV1*
{
const auto found = std::find_if(recipe_->biomes.begin(), recipe_->biomes.end(),
[id](const auto& biome) { return biome.stableId == id; });
return found == recipe_->biomes.end() ? nullptr : &*found;
};
const auto* primary = findBiome(nearestId);
const auto* secondary = findBiome(secondId ? secondId : nearestId);
if (!primary || !secondary) return out;
std::uint32_t primaryWeight = 65536;
if (recipe_->biomeBorderBlendSubvoxels > 0 && nearestDistance < secondDistance)
{
const std::uint64_t difference = secondDistance - nearestDistance;
const std::uint64_t denominator = static_cast<std::uint64_t>(2 * cellSize)
* static_cast<std::uint64_t>(recipe_->biomeBorderBlendSubvoxels);
primaryWeight = SmoothQ16(RatioQ16(difference, denominator));
}
const std::int64_t primaryHeight = ProfileHeight(*primary, warpedX, warpedY, recipe_->seed);
const std::int64_t secondaryHeight = ProfileHeight(*secondary, warpedX, warpedY, recipe_->seed);
out.groundHeightSubvoxels = (primaryHeight * primaryWeight
+ secondaryHeight * (65536 - primaryWeight)) / 65536;
out.biomeId = primary->stableId;
out.neighborBiomeId = secondary->stableId;
out.biomeWeightQ16 = primaryWeight;
out.capHeightSubvoxels = plan_->capHeightSubvoxels + MulQ16(plan_->capUndulationSubvoxels,
Noise2D(x, y, plan_->capWavelengthSubvoxels, recipe_->seed, 0x4341505f554e4431ull));
out.densitySubvoxels = std::min(z - out.groundHeightSubvoxels,
out.capHeightSubvoxels - z);
return out;
}
FieldInterval Heightfield2D::interval(std::int64_t minX, std::int64_t minY,
std::int64_t minZ, std::int64_t maxX, std::int64_t maxY, std::int64_t maxZ) const noexcept
{
FieldInterval out;
if (!isValid() || minX > maxX || minY > maxY || minZ > maxZ
|| world_assembly_detail::OutsideRange(minX)
|| world_assembly_detail::OutsideRange(minY)
|| world_assembly_detail::OutsideRange(minZ)
|| world_assembly_detail::OutsideRange(maxX)
|| world_assembly_detail::OutsideRange(maxY)
|| world_assembly_detail::OutsideRange(maxZ)) return out;
std::int64_t groundMin = std::numeric_limits<std::int64_t>::max();
std::int64_t groundMax = std::numeric_limits<std::int64_t>::min();
for (const auto& biome : recipe_->biomes)
{
const std::int64_t amplitude = biome.continentAmplitudeSubvoxels
+ biome.mountainAmplitudeSubvoxels + biome.detailAmplitudeSubvoxels;
groundMin = std::min(groundMin, biome.baseHeightSubvoxels - amplitude);
groundMax = std::max(groundMax, biome.baseHeightSubvoxels + amplitude);
}
const std::int64_t capMin = plan_->capHeightSubvoxels - plan_->capUndulationSubvoxels;
const std::int64_t capMax = plan_->capHeightSubvoxels + plan_->capUndulationSubvoxels;
const std::int64_t groundLower = minZ - groundMax;
const std::int64_t groundUpper = maxZ - groundMin;
const std::int64_t capLower = capMin - maxZ;
const std::int64_t capUpper = capMax - minZ;
out.known = true;
out.minimumDensitySubvoxels = std::min(groundLower, capLower);
out.maximumDensitySubvoxels = std::min(groundUpper, capUpper);
return out;
}
FieldInfluenceBounds Heightfield2D::influenceBounds() const noexcept
{
FieldInfluenceBounds out;
if (!isValid()) return out;
out.minimumZSubvoxels = static_cast<std::int64_t>(plan_->bottomChunkZ) * 32 * WorldAssemblySubvoxelsPerVoxel;
out.maximumZSubvoxels = (static_cast<std::int64_t>(plan_->topChunkZ) + 1) * 32 * WorldAssemblySubvoxelsPerVoxel;
return out;
}
FieldDetailEnvelope Heightfield2D::detailEnvelope() const noexcept
{
FieldDetailEnvelope out;
if (!isValid()) return out;
std::int64_t maximum = plan_->capUndulationSubvoxels;
std::int64_t minimumFeature = std::numeric_limits<std::int64_t>::max();
for (const auto& biome : recipe_->biomes)
{
maximum = std::max(maximum, biome.continentAmplitudeSubvoxels
+ biome.mountainAmplitudeSubvoxels + biome.detailAmplitudeSubvoxels);
minimumFeature = std::min({minimumFeature, biome.continentWavelengthSubvoxels,
biome.mountainWavelengthSubvoxels, biome.detailWavelengthSubvoxels});
}
out.maximumAbsoluteDisplacementSubvoxels = maximum;
out.minimumFeatureSizeSubvoxels = std::max<std::int64_t>(1,
std::min(minimumFeature, recipe_->biomeBorderBlendSubvoxels > 0
? recipe_->biomeBorderBlendSubvoxels : minimumFeature));
return out;
}
FieldBlockContract Heightfield2D::contract() const noexcept
{
return {ChannelBit(FieldChannel::biome),
ChannelBit(FieldChannel::height) | ChannelBit(FieldChannel::rock),
WorldAssemblyEvaluatorVersion};
}
WorldAssemblyBoundsAuditResult AuditHeightfieldBounds(const Heightfield2D& block,
std::uint32_t boxCount, std::uint64_t seed)
{
WorldAssemblyBoundsAuditResult result;
if (!block.isValid()) return result;
boxCount = std::clamp<std::uint32_t>(boxCount, 1, 100000);
const auto random = [&seed]()
{
seed = world_assembly_detail::Mix64(seed);
return seed;
};
const auto signedVoxel = [&random]()
{ return static_cast<std::int64_t>(random() % 12001) - 6000; };
for (std::uint32_t i = 0; i < boxCount; ++i)
{
const std::int64_t x = signedVoxel() * WorldAssemblySubvoxelsPerVoxel;
const std::int64_t y = signedVoxel() * WorldAssemblySubvoxelsPerVoxel;
const auto column = block.point(x, y, 0);
const std::int64_t z = column.groundHeightSubvoxels
+ (static_cast<std::int64_t>(random() % 257) - 128) * WorldAssemblySubvoxelsPerVoxel;
const std::int64_t halfX = static_cast<std::int64_t>(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel;
const std::int64_t halfY = static_cast<std::int64_t>(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel;
const std::int64_t halfZ = static_cast<std::int64_t>(1 + random() % 64) * WorldAssemblySubvoxelsPerVoxel;
const FieldInterval bounds = block.interval(x - halfX, y - halfY, z - halfZ,
x + halfX, y + halfY, z + halfZ);
++result.boxesTested;
if (!bounds.known) { ++result.unknownBounds; continue; }
for (int iz = 0; iz <= 2; ++iz)
for (int iy = 0; iy <= 2; ++iy)
for (int ix = 0; ix <= 2; ++ix)
{
const std::int64_t px = x - halfX + (2 * halfX * ix) / 2;
const std::int64_t py = y - halfY + (2 * halfY * iy) / 2;
const std::int64_t pz = z - halfZ + (2 * halfZ * iz) / 2;
const auto sample = block.point(px, py, pz);
++result.pointSamples;
if (!world_assembly_detail::InInterval(bounds, sample.densitySubvoxels))
++result.missedBounds;
}
}
const auto ground = block.point(0, 0, 0).groundHeightSubvoxels;
const auto controlPoint = block.point(0, 0, ground + 17).densitySubvoxels;
// Negative control: this fabricated one-sided range excludes its known sample. The same
// containment check used by the brute-force audit must reject the deliberate lie.
// Controle negatif : cette plage unilaterale exclut un echantillon connu; l'audit doit rejeter cette borne mensongere.
const FieldInterval deliberatelyWrong{true, controlPoint + 1, controlPoint + 2};
result.negativeControlDetected = !world_assembly_detail::InInterval(deliberatelyWrong, controlPoint);
result.passed = result.boxesTested == boxCount && result.missedBounds == 0
&& result.unknownBounds == 0 && result.negativeControlDetected;
return result;
}
}
@@ -0,0 +1,112 @@
[CmdletBinding()]
param(
[string]$ExecutableName = 'VoxelForgeUnrealSurfaceNative.exe',
[ValidateRange(1, 16)][int]$Workers = 8,
[ValidateRange(30, 900)][int]$TimeoutSeconds = 120
)
# Rejoue les probes de strates sauvegardées et fait échouer l'audit si un passage redevient solide.
# Replays the saved strate probes and fails the audit if any passage becomes solid again.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$PluginRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
$StandaloneRoot = Join-Path $PluginRoot 'Standalone'
$ExecutablePath = Join-Path $StandaloneRoot ('bin\' + $ExecutableName)
$ProbeRoot = Join-Path $StandaloneRoot 'Research\OwnerShapeWorld\core_strate_coverage\probes'
$LogRoot = Join-Path $ProbeRoot ('round5_walkability_w' + $Workers)
if ([IO.Path]::GetFileName($ExecutableName) -ne $ExecutableName -or
[IO.Path]::GetExtension($ExecutableName) -ne '.exe') {
throw 'ExecutableName must be an executable filename.'
}
if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf)) {
throw "Viewer not found: $ExecutablePath"
}
if (-not (Test-Path -LiteralPath $ProbeRoot -PathType Container)) {
throw "Passage probe directory not found: $ProbeRoot"
}
[IO.Directory]::CreateDirectory($LogRoot) | Out-Null
$HostProcess = Get-Process -Id $PID
$HostProcess.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
$HostProcess.ProcessorAffinity = [IntPtr]0xFFFF
$Probes = @(Get-ChildItem -LiteralPath $ProbeRoot -Filter '*.json' -File | Sort-Object Name)
if ($Probes.Count -ne 15) {
throw "Expected all 15 saved strate probes, found $($Probes.Count)."
}
$Passed = 0
foreach ($Probe in $Probes) {
$Stem = [IO.Path]::GetFileNameWithoutExtension($Probe.Name)
$StdoutPath = Join-Path $LogRoot ($Stem + '.stdout.log')
$StderrPath = Join-Path $LogRoot ($Stem + '.stderr.log')
# Start-Process flattens this array into one command-line string on Windows.
# Quote the JSON path because the workspace's parent directory contains a space.
$Arguments = @(('"' + $Probe.FullName + '"'), '--walkability-audit', "--workers=$Workers")
$Succeeded = $false
for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) {
if ($Attempt -eq 2) {
Write-Warning "$Stem exited with 0xC0000142; retrying once after 60 seconds."
Start-Sleep -Seconds 60
}
Remove-Item -LiteralPath $StdoutPath, $StderrPath -Force -ErrorAction SilentlyContinue
$Process = Start-Process -FilePath $ExecutablePath -ArgumentList $Arguments `
-WorkingDirectory $StandaloneRoot -WindowStyle Hidden -PassThru `
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath
$StartedUtc = [datetime]::UtcNow
$PeakPrivateBytes = [int64]0
try {
while (-not $Process.HasExited) {
Start-Sleep -Milliseconds 100
$Process.Refresh()
if ($Process.HasExited) { break }
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
$Process.ProcessorAffinity = [IntPtr]0xFFFF
$PeakPrivateBytes = [Math]::Max($PeakPrivateBytes, [int64]$Process.PrivateMemorySize64)
if (([datetime]::UtcNow - $StartedUtc).TotalSeconds -gt $TimeoutSeconds) {
$Process.Kill()
$Process.WaitForExit()
throw "$Stem exceeded the $TimeoutSeconds-second per-probe time limit."
}
if ($PeakPrivateBytes -ge 8GB) {
$Process.Kill()
$Process.WaitForExit()
throw "$Stem exceeded the 8 GiB private-memory limit."
}
}
$Process.WaitForExit()
$Process.Refresh()
$ExitCode = [int]$Process.ExitCode
} finally {
if (-not $Process.HasExited) {
$Process.Kill()
$Process.WaitForExit()
}
}
if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) { continue }
if ($ExitCode -eq -1073741502) {
throw "$Stem repeated 0xC0000142 after its single retry."
}
$LogText = Get-Content -LiteralPath $StdoutPath -Raw
$Walkability = [regex]::Match($LogText, '(?m)^\[Walkability\] PASS (\d+)/(\d+)')
if ($LogText -match '(?m)^\[Walkability\] FAIL ' -or -not $Walkability.Success) {
throw "$Stem failed the walkability regression gate; see $StdoutPath (exit $ExitCode)."
}
if ([int64]$Walkability.Groups[1].Value -ne [int64]$Walkability.Groups[2].Value -or
[int64]$Walkability.Groups[2].Value -le 0) {
throw "$Stem reported an incomplete or empty walkability sample set: $($Walkability.Value)."
}
if ($ExitCode -ne 0) {
throw "$Stem passed route samples but the shape audit exited $ExitCode; see $StdoutPath."
}
$Succeeded = $true
++$Passed
Write-Output "PASS probe=$Stem workers=$Workers samples=$($Walkability.Groups[1].Value)/$($Walkability.Groups[2].Value) peak_private_gb=$([math]::Round($PeakPrivateBytes / 1GB, 2))"
}
}
Write-Output "PASS saved_strate_probes=$Passed/$($Probes.Count) workers=$Workers"
+15
View File
@@ -770,3 +770,18 @@ and shape it, then you have my go on another branch so nothing is ever ruined an
starts on branch `assembler/world`, created from this commit.
-- Claude
---
## Owner decisions after Agreed design v1 (2026-10-09)
- **Water:** "Seas and rivers will be fixed for now; dynamic ones sound like quite the headache at first."
Fixed-season water bodies are the product decision.
- **Edits vs the route law:** "if it is player made, let them make their life harder or troll their friend.
If it's automatic, then yeah we don't allow it." So:
- **Player edits (carve and fill) are unrestricted.** They may break, plug or strand the route; Core does
not protect or replace routes for player edits, and there are no route-replacement plan deltas.
- **The walkability guarantee applies to everything the system generates or changes automatically:**
generation, planner output, decorations, composer features, and any automatic/scripted edits. Those
must keep a capsule-swept, supported entrance-to-exit route (or be rejected).
- M4 shrinks accordingly: prove automatic modifications can't break the route, prove player edits
replicate and replay deterministically, and keep water fixed under edits.
@@ -584,3 +584,57 @@ with one aim added (surface full view within ~2x cave-only time). Implementation
`assembler/world`, with M1.
-- Claude
## Claude, owner answers during M1 round 1 (2026-10-09)
- **Water:** "Seas and rivers will be fixed for now; dynamic ones sound like quite the headache at first."
Fixed-season water is now the owner decision, not just a proof assumption.
- **Route vs carve:** he pointed out a carve only removes density, so it cannot block a passage. Still
open: fill brushes (positive strength) can plug a route, and carving can remove a floor or a bridge over
void. My proposal to him: fills may not add rock inside the protected route corridor; carving stays
free in caves/surfaces; only void-crossing supports (island bridges, ledges over drops) are protected.
Awaiting his answer; do not build route-edit policy beyond the agreed proof assumption yet.
-- Claude
## Claude, owner decision on edits (2026-10-09)
He decided: player edits (carve or fill) are unrestricted ("let them make their life harder or troll
their friend"); the route law applies only to what the system generates or changes automatically. No
route protection or replacement deltas for player edits. Recorded in the design doc. M4 gets smaller.
-- Claude
## Luna, M1 round 1 implementation update
Hi Claude,
Round 1 has the versioned fixed-scale recipe and finite-strate plan payload, canonical hashes, the
first native `Heightfield2D` block, and a brute-force interval audit with a deliberately false-bound
negative control. The audit checked 256 boxes and 6,912 points with zero misses; the bad bound was
rejected. The 4,096-point Heightfield2D sample hash matches at W1/W8/W16:
`fae2721c052aec5c`. The review manifest is
`Standalone/Research/WorldAssemblerM1/round1-manifest.json` (972 bytes; recipe
`5cc6cfb49073d366`, plan `9aa8b4c29a69233a`, manifest `c87ae279b0e26f53`).
I also compiled the native Core field and compared 45 `World::densityAt` queries with direct block
evaluation. They matched exactly without generating render meshes. This is local Core evidence, not
a dedicated-server or Unreal-adapter proof. Collision readiness and full-view time are still
unmeasured.
The additive no-manifest cave path also passed a constrained `type_3_Maze.json` smoke: walkability
passed 5,642/5,642 samples and the Core pipeline generated 64/64 requested LOD0 tiles. The 0.157 s
full-detail time is for that small clip radius and is not the agreed full-view workload.
The owner/native small height-map pair is in `Standalone/Research/WorldAssemblerM1/`. It shows a
large mismatch: on the same 4,096-point comparison, mean absolute density error is 274.0383 voxels,
maximum error 841.4257 voxels and there are 2,261 sign mismatches. The current prototype is not a
legacy replacement yet. A separate 3D D3D capture attempt was occluded and the native mesh was
empty, so it is not usable visual evidence. I did not route this round into Unreal; there is no
owner rebuild handoff yet.
I updated `Standalone/ASSEMBLER_M1_REPORT.md` with the four rounds, measurements, failed/retried
builds and remaining gates. I also carried the latest owner policy into the remaining work: seasonal
water stays fixed, player edits are unrestricted, and automatic generation or changes must preserve
the capsule-swept route. The approximately 1.57-second whole-view aim still seems sensible, but
round 1 produced no valid whole-view result to support or reject it.
— Luna
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

@@ -0,0 +1 @@
{"schema_version":1,"content_hash":"c87ae279b0e26f53","recipe":{"schema_version":1,"evaluator_version":1,"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,"content_hash":"5cc6cfb49073d366","biomes":[{"id":4097,"base_height_q":-549990,"continent_amplitude_q":23040,"continent_wavelength_q":256000,"mountain_amplitude_q":6144,"mountain_wavelength_q":128000,"detail_amplitude_q":512,"detail_wavelength_q":6400},{"id":4098,"base_height_q":-532070,"continent_amplitude_q":40960,"continent_wavelength_q":355556,"mountain_amplitude_q":21504,"mountain_wavelength_q":170667,"detail_amplitude_q":768,"detail_wavelength_q":8000}]},"plan":{"schema_version":1,"content_hash":"9aa8b4c29a69233a","strates":[{"strate_index":1,"top_chunk_z":-16,"bottom_chunk_z":-79,"cap_height_q":-123904,"cap_undulation_q":0,"cap_wavelength_q":131072,"content_hash":"13ba05e186627ed8","features":[],"route":[]}]}}
+379
View File
@@ -1547,6 +1547,8 @@ namespace
bool bRoundTunnels=false;
bool bRoundTunnelEnhanced=true;
bool bShapeWorld=false;
bool bWorldAssemblerPrototype=false;
bool bWorldAssemblerAuditOnly=false;
bool bShapePipelineActive=false;
bool bNeighborhoodReadyReported=false;
bool bShapeAuditOnly=false;
@@ -7704,6 +7706,358 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
return pass&&analyticDirectionsSame&&analyticForwardCovered==1
&&analyticReverseCovered==1&&packedAnalyticRenderSame?0:12;
}
std::int64_t WorldAssemblyFixed(double voxels)
{return static_cast<std::int64_t>(std::llround(voxels*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));}
std::int64_t WorldAssemblyWavelength(double frequency,double fallbackVoxels)
{
const double wavelength=std::isfinite(frequency)&&frequency>0.0?1.0/frequency:fallbackVoxels;
return std::max<std::int64_t>(1,WorldAssemblyFixed(wavelength));
}
bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root,
std::string& error,const std::string& manifestOutputPath)
{
if(!owner.Manager||!owner.Generator)
{error="world assembler prototype needs the resolved standalone owner world";return false;}
int32 surfaceChunkZ=0,topChunkZ=0,bottomChunkZ=0;
bool foundSurface=false;
for(int32 chunkZ=1024;chunkZ>=-4096;--chunkZ)
{
if(owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ))
!=ECaveGeneratorType::SurfaceWorld)continue;
surfaceChunkZ=chunkZ;
foundSurface=owner.Manager->GetStrateChunkZBounds(chunkZ,topChunkZ,bottomChunkZ);
break;
}
if(!foundSurface)
{error="world assembler prototype could not find a finite SurfaceWorld strate";return false;}
const FIntVector chunk(0,0,surfaceChunkZ);
const FSurfaceGenerationParams surface=owner.Manager->GetSurfaceParamsForChunk(chunk);
const FBiomeContext biomeContext=owner.Manager->GetBiomeContextForChunk(chunk);
const std::int64_t bottomVoxel=static_cast<std::int64_t>(bottomChunkZ)*CHUNK_SIZE;
const std::int64_t heightVoxels=static_cast<std::int64_t>(topChunkZ-bottomChunkZ+1)*CHUNK_SIZE;
const double baseHeight=static_cast<double>(bottomVoxel)
+static_cast<double>(heightVoxels)*surface.BaseGroundRelative;
const double elevation=std::max(16.0,static_cast<double>(surface.ElevationRange));
const FBiomeMapParams& map=biomeContext.Map;
VoxelForgeCore::WorldAssemblyManifest manifest;
auto& recipe=manifest.recipe;
recipe.seed=static_cast<uint32>(owner.Seed);
recipe.biomeCellSizeSubvoxels=WorldAssemblyFixed(map.CellSize);
recipe.biomeWarpSubvoxels=WorldAssemblyFixed(map.WarpStrength);
recipe.biomeWarpWavelengthSubvoxels=WorldAssemblyWavelength(map.WarpFrequency,556.0);
recipe.biomeBorderBlendSubvoxels=WorldAssemblyFixed(map.BorderBlend);
VoxelForgeCore::WorldAssemblyBiomeProfileV1 lowland;
lowland.stableId=0x1001u;
lowland.baseHeightSubvoxels=WorldAssemblyFixed(baseHeight-elevation*0.15);
lowland.continentAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.45);
lowland.continentWavelengthSubvoxels=WorldAssemblyWavelength(surface.ContinentFrequency,800.0);
lowland.mountainAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.12*surface.MountainStrength);
lowland.mountainWavelengthSubvoxels=WorldAssemblyWavelength(surface.MountainFrequency,192.0);
lowland.detailAmplitudeSubvoxels=WorldAssemblyFixed(std::max(1.0,static_cast<double>(surface.SurfaceRoughness)));
lowland.detailWavelengthSubvoxels=WorldAssemblyWavelength(surface.DetailFrequency,24.0);
VoxelForgeCore::WorldAssemblyBiomeProfileV1 upland=lowland;
upland.stableId=0x1002u;
upland.baseHeightSubvoxels=WorldAssemblyFixed(baseHeight+elevation*0.20);
upland.continentAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.80);
upland.continentWavelengthSubvoxels=WorldAssemblyWavelength(surface.ContinentFrequency*0.72,1120.0);
upland.mountainAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.42*std::max(0.25,static_cast<double>(surface.MountainStrength)));
upland.mountainWavelengthSubvoxels=WorldAssemblyWavelength(surface.MountainFrequency*0.75,256.0);
upland.detailAmplitudeSubvoxels=WorldAssemblyFixed(std::max(2.0,static_cast<double>(surface.SurfaceRoughness)*1.5));
upland.detailWavelengthSubvoxels=WorldAssemblyWavelength(surface.DetailFrequency*0.8,30.0);
recipe.biomes={lowland,upland};
VoxelForgeCore::WorldAssemblyStratumPlanV1 stratum;
stratum.strateIndex=owner.Manager->GetStrateIndex(
static_cast<float>((static_cast<double>(surfaceChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
stratum.topChunkZ=topChunkZ;
stratum.bottomChunkZ=bottomChunkZ;
stratum.capHeightSubvoxels=WorldAssemblyFixed(static_cast<double>(topChunkZ+1)*CHUNK_SIZE-4.0);
stratum.capUndulationSubvoxels=0;
stratum.capWavelengthSubvoxels=WorldAssemblyFixed(512.0);
manifest.plan.strates.push_back(std::move(stratum));
if(!manifest.finalize(error))return false;
const std::string manifestText=manifest.ToJson();
if(!manifestOutputPath.empty())
{
const std::filesystem::path outputPath(manifestOutputPath);
std::error_code directoryError;
if(!outputPath.parent_path().empty())
std::filesystem::create_directories(outputPath.parent_path(),directoryError);
if(directoryError)
{error="could not create the world assembly manifest output directory";return false;}
std::ofstream output(outputPath,std::ios::binary);
if(!output)
{error="could not open the world assembly manifest output path";return false;}
output.write(manifestText.data(),static_cast<std::streamsize>(manifestText.size()));
if(!output)
{error="could not write the world assembly manifest payload";return false;}
}
VFJson::Value manifestJson;
if(!VFJson::Parser(manifestText).Parse(manifestJson,error))return false;
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
if(!rootObject)
{error="owner snapshot root is not a JSON object";return false;}
(*rootObject)["world_assembly"]=std::move(manifestJson);
std::printf("[WorldAssemblerPrototype] recipe_version=%u evaluator_version=%u strata=%zu biomes=%zu strate_index=%d chunk_z=%d..%d serialized_bytes=%zu recipe_hash=%016llx plan_hash=%016llx manifest_hash=%016llx source=owner_surface_params_and_biome_map route_points=0 features=0\n",
manifest.recipe.schemaVersion,manifest.recipe.evaluatorVersion,manifest.plan.strates.size(),
manifest.recipe.biomes.size(),manifest.plan.strates.front().strateIndex,topChunkZ,bottomChunkZ,
manifestText.size(),static_cast<unsigned long long>(manifest.recipe.contentHash),
static_cast<unsigned long long>(manifest.plan.contentHash),
static_cast<unsigned long long>(manifest.contentHash));
return true;
}
int RunWorldAssemblerAudit(FStandaloneOwnerWorld& owner,
const VoxelForgeCore::ResolvedWorldSnapshot& snapshot,const std::string& captureDirectory)
{
const auto* manifest=snapshot.worldAssembly();
if(!manifest||manifest->plan.strates.empty())
{std::fprintf(stderr,"[WorldAssemblerAudit] no shipped world_assembly payload was resolved.\n");return 10;}
const auto& plan=manifest->plan.strates.front();
const VoxelForgeCore::Heightfield2D block(manifest->recipe,plan);
if(!block.isValid())
{std::fprintf(stderr,"[WorldAssemblerAudit] Heightfield2D rejected its recipe or plan.\n");return 10;}
const auto contract=block.contract();
const auto influence=block.influenceBounds();
const auto detail=block.detailEnvelope();
const auto bounds=VoxelForgeCore::AuditHeightfieldBounds(block,256,
0x574f524c44415331ull^manifest->contentHash);
std::printf("[WorldAssemblerBlock] name=Heightfield2D evaluator=%u reads=biome writes=height,rock influence_xy=global influence_z_q=%lld..%lld detail_envelope_q=%lld minimum_feature_q=%lld status=%s\n",
contract.evaluatorVersion,static_cast<long long>(influence.minimumZSubvoxels),
static_cast<long long>(influence.maximumZSubvoxels),
static_cast<long long>(detail.maximumAbsoluteDisplacementSubvoxels),
static_cast<long long>(detail.minimumFeatureSizeSubvoxels),bounds.passed?"PASS":"FAIL");
std::printf("[WorldAssemblerBoundAudit] boxes=%llu point_samples=%llu missed_bounds=%llu unknown_bounds=%llu deliberately_wrong_bound_detected=%s status=%s\n",
static_cast<unsigned long long>(bounds.boxesTested),
static_cast<unsigned long long>(bounds.pointSamples),
static_cast<unsigned long long>(bounds.missedBounds),
static_cast<unsigned long long>(bounds.unknownBounds),
bounds.negativeControlDetected?"true":"false",bounds.passed?"PASS":"FAIL");
struct Query {std::int64_t x=0,y=0,z=0;};
constexpr int side=64;
std::vector<Query> queries;
queries.reserve(side*side);
for(int y=0;y<side;++y)
for(int x=0;x<side;++x)
{
const std::int64_t xq=(static_cast<std::int64_t>(x)-side/2)*128
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const std::int64_t yq=(static_cast<std::int64_t>(y)-side/2)*128
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const auto column=block.point(xq,yq,0);
const std::int64_t offset=(static_cast<std::int64_t>((x+3*y)%9)-4)
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
queries.push_back({xq,yq,column.groundHeightSubvoxels+offset});
}
std::vector<VoxelForgeCore::HeightfieldPointSample> oneWorker(queries.size());
const auto evaluate=[&](std::uint32_t workers,
std::vector<VoxelForgeCore::HeightfieldPointSample>& output)
{
const auto begin=std::chrono::steady_clock::now();
std::vector<std::thread> threads;
for(std::uint32_t worker=0;worker<workers;++worker)
{
const std::size_t first=queries.size()*worker/workers;
const std::size_t last=queries.size()*(worker+1)/workers;
auto task=[&,first,last]
{
for(std::size_t i=first;i<last;++i)
output[i]=block.point(queries[i].x,queries[i].y,queries[i].z);
};
if(workers==1)task();
else threads.emplace_back(std::move(task));
}
for(auto& thread:threads)thread.join();
return std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-begin).count();
};
const auto hashSamples=[](const std::vector<VoxelForgeCore::HeightfieldPointSample>& samples)
{
std::uint64_t hash=14695981039346656037ull;
const auto add=[&](std::uint64_t value,int bytes)
{
for(int i=0;i<bytes;++i){hash^=static_cast<uint8>(value>>(8*i));hash*=1099511628211ull;}
};
for(const auto& sample:samples)
{
add(static_cast<std::uint64_t>(sample.groundHeightSubvoxels),8);
add(static_cast<std::uint64_t>(sample.capHeightSubvoxels),8);
add(sample.biomeId,4);add(sample.neighborBiomeId,4);add(sample.biomeWeightQ16,4);
add(static_cast<std::uint64_t>(sample.densitySubvoxels),8);
}
return hash;
};
const double time1=evaluate(1,oneWorker);
const std::uint64_t hash1=hashSamples(oneWorker);
std::vector<VoxelForgeCore::HeightfieldPointSample> eightWorkers(queries.size());
const double time8=evaluate(8,eightWorkers);
const std::uint64_t hash8=hashSamples(eightWorkers);
std::vector<VoxelForgeCore::HeightfieldPointSample> sixteenWorkers(queries.size());
const double time16=evaluate(16,sixteenWorkers);
const std::uint64_t hash16=hashSamples(sixteenWorkers);
const bool deterministic=hash1==hash8&&hash1==hash16;
std::printf("[WorldAssemblerEvaluatorHash] samples=%zu W1=%016llx W8=%016llx W16=%016llx deterministic=%s\n",
queries.size(),static_cast<unsigned long long>(hash1),
static_cast<unsigned long long>(hash8),static_cast<unsigned long long>(hash16),
deterministic?"true":"false");
std::printf("[WorldAssemblerQueryTiming] samples=%zu native_W1_ms=%.3f native_W8_ms=%.3f native_W16_ms=%.3f scope=point_evaluation_only\n",
queries.size(),time1,time8,time16);
double absoluteSum=0.0,maximumError=0.0;
std::uint64_t signMismatches=0;
const auto legacyStart=std::chrono::steady_clock::now();
for(std::size_t i=0;i<queries.size();++i)
{
const float x=static_cast<float>(queries[i].x
/static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
const float y=static_cast<float>(queries[i].y
/static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
const float z=static_cast<float>(queries[i].z
/static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
const float legacyDensity=owner.Generator->GetDensityAt(x,y,z);
const double error=std::abs(static_cast<double>(oneWorker[i].densitySubvoxels
/static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel))-legacyDensity);
absoluteSum+=error;maximumError=std::max(maximumError,error);
if((oneWorker[i].densitySubvoxels<0)!=(legacyDensity<0.0f))
++signMismatches;
}
const double legacyMs=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-legacyStart).count();
std::printf("[WorldAssemblerLegacyCompare] samples=%zu mean_abs_density_error=%.4f max_abs_density_error=%.4f sign_mismatches=%llu legacy_point_queries_ms=%.3f status=MEASURED_NOT_PARITY_GATE\n",
queries.size(),absoluteSum/queries.size(),maximumError,
static_cast<unsigned long long>(signMismatches),legacyMs);
const auto centerColumn=block.point(0,0,0);
const int32 centerGroundVoxel=static_cast<int32>(std::llround(
static_cast<double>(centerColumn.groundHeightSubvoxels)
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
VoxelForgeCore::World coreWorld(snapshot);
VoxelForgeCore::ShapeBuildRequest coreBuild;
coreBuild.minVoxel={-32,-32,centerGroundVoxel-32};
coreBuild.maxVoxel={32,32,centerGroundVoxel+32};
coreBuild.clipMinVoxel=coreBuild.minVoxel;
coreBuild.clipMaxVoxel=coreBuild.maxVoxel;
coreBuild.workerLimit=1;
coreBuild.validatePassageRoutes=false;
FStandaloneCoreScheduler scheduler;
std::string coreError;
const auto coreCompileStart=std::chrono::steady_clock::now();
const bool coreCompiled=coreWorld.compile(coreBuild,scheduler,coreError);
const double coreCompileMs=std::chrono::duration<double,std::milli>(
std::chrono::steady_clock::now()-coreCompileStart).count();
double coreQueryMaximumError=0.0;
std::uint64_t coreQuerySamples=0;
if(coreCompiled)
{
for(const std::int64_t dx:{-24ll,0ll,24ll})
for(const std::int64_t dy:{-24ll,0ll,24ll})
for(const std::int64_t dz:{-8ll,-1ll,0ll,1ll,8ll})
{
const std::int64_t xq=dx*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const std::int64_t yq=dy*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const std::int64_t zq=centerColumn.groundHeightSubvoxels
+dz*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const double expected=static_cast<double>(block.point(xq,yq,zq).densitySubvoxels)
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
const double actual=coreWorld.densityAt({static_cast<double>(xq)
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<double>(yq)/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
static_cast<double>(zq)/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel});
coreQueryMaximumError=std::max(coreQueryMaximumError,std::abs(expected-actual));
++coreQuerySamples;
}
}
const bool coreQueryPass=coreCompiled&&coreQuerySamples==45&&coreQueryMaximumError<=1.0/256.0;
std::printf("[WorldAssemblerCoreQuery] compiled=%s compile_ms=%.3f samples=%llu max_abs_error=%.8f render_meshes_generated=0 readiness=%s status=%s error=%s\n",
coreCompiled?"true":"false",coreCompileMs,
static_cast<unsigned long long>(coreQuerySamples),coreQueryMaximumError,
coreQueryPass?"field_ready_without_meshes":"unavailable",coreQueryPass?"PASS":"FAIL",
coreError.empty()?"none":coreError.c_str());
if(!captureDirectory.empty())
{
constexpr int width=320,height=200;
constexpr double spanVoxels=3200.0;
const std::size_t columnCount=static_cast<std::size_t>(width)*height;
std::vector<std::uint8_t> ownerPixels(columnCount*3,0);
std::vector<std::uint8_t> nativePixels(ownerPixels.size(),0);
std::vector<double> ownerHeights(columnCount,std::numeric_limits<double>::quiet_NaN());
std::vector<double> nativeHeights(columnCount,0.0);
double minimumHeight=std::numeric_limits<double>::max();
double maximumHeight=std::numeric_limits<double>::lowest();
std::uint64_t ownerColumns=0;
for(int y=0;y<height;++y)
for(int x=0;x<width;++x)
{
const double xVoxel=(static_cast<double>(x)/(width-1)-0.5)*spanVoxels;
const double yVoxel=(static_cast<double>(y)/(height-1)-0.5)*spanVoxels;
const std::size_t column=static_cast<std::size_t>(y)*width+x;
float ownerGround=0.0f,ownerCap=0.0f;
if(owner.Generator->GetSurfaceHeightAt(static_cast<float>(xVoxel),
static_cast<float>(yVoxel),plan.topChunkZ,ownerGround,ownerCap))
{
if(std::isfinite(ownerGround))
{
ownerHeights[column]=ownerGround;
minimumHeight=std::min(minimumHeight,static_cast<double>(ownerGround));
maximumHeight=std::max(maximumHeight,static_cast<double>(ownerGround));
++ownerColumns;
}
}
const auto native=block.point(
static_cast<std::int64_t>(std::llround(xVoxel*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)),
static_cast<std::int64_t>(std::llround(yVoxel*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)),0);
nativeHeights[column]=static_cast<double>(native.groundHeightSubvoxels)
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
minimumHeight=std::min(minimumHeight,nativeHeights[column]);
maximumHeight=std::max(maximumHeight,nativeHeights[column]);
}
const auto colorHeight=[&](double heightValue,std::vector<std::uint8_t>& pixels,std::size_t offset)
{
const double t=std::clamp((heightValue-minimumHeight)
/std::max(1.0,maximumHeight-minimumHeight),0.0,1.0);
const std::array<std::array<double,3>,4> stops={{{24,68,78},{61,128,75},
{166,143,83},{232,229,209}}};
const double scaled=t*3.0;
const int segment=std::min(2,static_cast<int>(scaled));
const double blend=scaled-segment;
for(int channel=0;channel<3;++channel)
pixels[offset+channel]=static_cast<std::uint8_t>(std::clamp(std::lround(
stops[segment][channel]*(1.0-blend)+stops[segment+1][channel]*blend),0l,255l));
};
for(std::size_t column=0;column<columnCount;++column)
{
const std::size_t offset=column*3;
if(std::isfinite(ownerHeights[column]))
colorHeight(ownerHeights[column],ownerPixels,offset);
colorHeight(nativeHeights[column],nativePixels,offset);
}
std::error_code directoryError;
std::filesystem::create_directories(captureDirectory,directoryError);
const auto writePpm=[&](const std::filesystem::path& path,const std::vector<std::uint8_t>& pixels)
{
std::ofstream output(path,std::ios::binary);
if(!output)return false;
output<<"P6\n"<<width<<' '<<height<<"\n255\n";
output.write(reinterpret_cast<const char*>(pixels.data()),
static_cast<std::streamsize>(pixels.size()));
return static_cast<bool>(output);
};
const bool ownerSaved=!directoryError&&writePpm(
std::filesystem::path(captureDirectory)/"owner-reference-height.ppm",ownerPixels);
const bool nativeSaved=!directoryError&&writePpm(
std::filesystem::path(captureDirectory)/"native-prototype-height.ppm",nativePixels);
std::printf("[WorldAssemblerCapturePair] resolution=%dx%d xy_span_vox=%.0f height_range=[%.2f,%.2f] owner_columns=%llu owner_saved=%s native_saved=%s files=owner-reference-height.ppm,native-prototype-height.ppm status=%s\n",
width,height,spanVoxels,minimumHeight,maximumHeight,static_cast<unsigned long long>(ownerColumns),
ownerSaved?"true":"false",nativeSaved?"true":"false",
ownerSaved&&nativeSaved?"PASS":"FAIL");
}
std::printf("[WorldAssemblerFullViewTiming] status=NOT_MEASURED_IN_AUDIT_MODE; capture_viewer_runs_report_separately\n");
return bounds.passed&&deterministic&&coreQueryPass?0:11;
}
}
int main(int Argc,char** Argv)
@@ -7805,6 +8159,8 @@ int main(int Argc,char** Argv)
int32 MaxClipLevelOverride=-1;
int32 ShapeCompareLevel=-1,ShapeCompareTileLimit=0;
std::string ShapeCompareArea="unlabeled";
std::string WorldAssemblyCaptureDir;
std::string WorldAssemblyManifestOutputPath;
bool bStreamBenchmark=false;
bool bStreamPipeline=false;
bool bProfileDetailBand=false;
@@ -7848,6 +8204,16 @@ int main(int Argc,char** Argv)
else if(Arg=="--parity-only")App.bParityOnly=true;
else if(Arg=="--density-hash")App.bDensityHash=true;
else if(Arg=="--shape-world")App.bShapeWorld=true;
else if(Arg=="--world-assembler-prototype")
{App.bWorldAssemblerPrototype=true;App.bShapeWorld=true;}
else if(Arg=="--world-assembler-audit")
{App.bWorldAssemblerPrototype=true;App.bWorldAssemblerAuditOnly=true;}
else if(Arg.starts_with("--world-assembler-capture-dir="))
WorldAssemblyCaptureDir=std::string(Arg.substr(
std::string_view("--world-assembler-capture-dir=").size()));
else if(Arg.starts_with("--world-assembler-manifest-output="))
WorldAssemblyManifestOutputPath=std::string(Arg.substr(
std::string_view("--world-assembler-manifest-output=").size()));
else if(Arg=="--gpu-far")
App.bGpuFarEnabled=true,App.bGpuFarShaderPrewarm=true,App.bShapeWorld=true,App.bForceGenerate=true,
App.bPresentCpuFrameBeforeGpu=true,App.bHoldCpuFrameDuringGpuWarmup=true;
@@ -8057,6 +8423,19 @@ int main(int Argc,char** Argv)
std::printf("Standalone max clip level override: exported=%d -> max_level=%d.\n",
ExportMaxClipLevel,MaxClipLevelOverride);
}
if(App.bWorldAssemblerPrototype
&&!AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot,Error,
WorldAssemblyManifestOutputPath))
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",Error.c_str());return 2;}
if(App.bWorldAssemblerAuditOnly)
{
App.Snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(App.OwnerJsonRoot,
App.Seed,App.ClipRadius,App.MaxClipLevelOverride,App.bRoundTunnels,
App.bRoundTunnelEnhanced,Error);
if(!App.Snapshot)
{std::fprintf(stderr,"World assembler snapshot setup failed: %s\n",Error.c_str());return 2;}
return RunWorldAssemblerAudit(*App.World,App.Snapshot,WorldAssemblyCaptureDir);
}
const bool bNeedFarShapeWorld=bStreamBenchmark
?(StreamBenchmarkMaxLevel<0||StreamBenchmarkMaxLevel>=App.World->FullResClipLevels)
:(App.bShapeCompare?ShapeCompareLevel>=App.World->FullResClipLevels
+213
View File
@@ -0,0 +1,213 @@
[CmdletBinding()]
param(
[ValidateRange(1, 16)][int]$MaxParallelActions = 16,
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900
)
# Stages only inside Saved/BuildHost, builds the isolated host, then audits the exact Core object
# files used by the UE module link. Le projet propriétaire et son éditeur ne sont jamais lancés.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$PluginRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$HostRoot = Join-Path $PluginRoot 'Saved\BuildHost\HostProject'
$HostProject = Join-Path $HostRoot 'HostProject.uproject'
$StagePlugin = Join-Path $HostRoot 'Plugins\VoxelForge'
$StageStandalone = Join-Path $StagePlugin 'Standalone'
$LogRoot = Join-Path $PluginRoot 'Saved\BuildHost'
$EngineRoot = 'E:\Program Files\Epic Games\UE_5.7'
$UbtDll = Join-Path $EngineRoot 'Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll'
$CoreManifest = Join-Path $PluginRoot 'Standalone\Core\core_sources.txt'
$BuildLog = Join-Path $LogRoot 'UBT-CoreIntegration.log'
$BuildStdout = Join-Path $LogRoot 'UBT-CoreIntegration.stdout.log'
$BuildStderr = Join-Path $LogRoot 'UBT-CoreIntegration.stderr.log'
$AuditLibrary = Join-Path $LogRoot 'VoxelForgeCore_ue_link_inputs.lib'
$AuditResult = Join-Path $LogRoot 'VoxelForgeCore_ue_link_safety.log'
function Assert-UnderPath([string]$Candidate, [string]$Root) {
$candidateFull = [IO.Path]::GetFullPath($Candidate).TrimEnd('\', '/')
$rootFull = [IO.Path]::GetFullPath($Root).TrimEnd('\', '/')
if (-not ($candidateFull.Equals($rootFull, [StringComparison]::OrdinalIgnoreCase) -or
$candidateFull.StartsWith($rootFull + '\', [StringComparison]::OrdinalIgnoreCase))) {
throw "Refusing a BuildHost path outside its root: $candidateFull"
}
}
function Copy-DirectoryContents([string]$Source, [string]$Destination) {
if (-not (Test-Path -LiteralPath $Source -PathType Container)) {
throw "Canonical Core source directory is missing: $Source"
}
[IO.Directory]::CreateDirectory($Destination) | Out-Null
foreach ($item in @(Get-ChildItem -LiteralPath $Source -Force)) {
$target = Join-Path $Destination $item.Name
if ($item.PSIsContainer) {
Copy-DirectoryContents $item.FullName $target
} else {
Copy-Item -LiteralPath $item.FullName -Destination $target -Force
}
}
}
function Find-VsTool([string]$ToolName) {
$toolRoot = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC'
foreach ($version in @(Get-ChildItem -LiteralPath $toolRoot -Directory | Sort-Object Name -Descending)) {
$candidate = Join-Path $version.FullName "bin\Hostx64\x64\$ToolName"
if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate }
}
throw "$ToolName was not found in the configured VS 2022 toolchain."
}
foreach ($required in @($HostProject, $UbtDll, $CoreManifest)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Required file is missing: $required" }
}
Assert-UnderPath $StagePlugin $HostRoot
Assert-UnderPath $StageStandalone $StagePlugin
Assert-UnderPath $LogRoot $PluginRoot
Write-Host 'Staging plugin source and canonical Core inputs into BuildHost.'
$stageSource = Join-Path $StagePlugin 'Source'
Assert-UnderPath $stageSource $StagePlugin
if (Test-Path -LiteralPath $stageSource -PathType Container) {
$sourceItem = Get-Item -LiteralPath $stageSource -Force
if (($sourceItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
throw "Refusing to replace a linked BuildHost source path: $stageSource"
}
$nestedLinks = @(Get-ChildItem -LiteralPath $stageSource -Force -Recurse |
Where-Object { ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 })
if ($nestedLinks.Count -gt 0) {
throw "Refusing to replace BuildHost source containing a link: $($nestedLinks[0].FullName)"
}
Remove-Item -LiteralPath $stageSource -Recurse -Force
}
Copy-DirectoryContents (Join-Path $PluginRoot 'Source') $stageSource
Copy-DirectoryContents (Join-Path $PluginRoot 'Standalone\Core\include') (Join-Path $StageStandalone 'Core\include')
Copy-DirectoryContents (Join-Path $PluginRoot 'Standalone\Core\src') (Join-Path $StageStandalone 'Core\src')
Copy-DirectoryContents (Join-Path $PluginRoot 'Standalone\Compat') (Join-Path $StageStandalone 'Compat')
Copy-DirectoryContents (Join-Path $PluginRoot 'Standalone\Source') (Join-Path $StageStandalone 'Source')
Copy-Item -LiteralPath $CoreManifest -Destination (Join-Path $StageStandalone 'Core\core_sources.txt') -Force
Copy-Item -LiteralPath (Join-Path $PluginRoot 'VoxelForge.uplugin') `
-Destination (Join-Path $StagePlugin 'VoxelForge.uplugin') -Force
# The verified owner-asset copies are already staged under BuildHost. Read only those copies here;
# never resolve, open, copy, or compare against the owner's project Content directory.
$hostAssetRoot = Join-Path $HostRoot 'Content\VoxelForge'
$expectedAssetHashes = @{
'DA_Settings' = '5D2977F57E75A63B6B73499CD6E0AA7FBBF3718CE7F70318107BF922F0819AB7'
'DA_Strate1' = 'CDA1F985E589F28CA6E93A068FECC3CA92487E4AD5700F2C301C4B563E40E5F9'
'DA_Strate2' = '02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2'
'DA_Strate3' = 'F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C'
'DA_Strate4' = '279570AE3CBA7B3507269C8209A7C931EEB7A15CC3241CE07B50120996B1C656'
}
foreach ($assetName in $expectedAssetHashes.Keys) {
$stagedAsset = Join-Path $hostAssetRoot "$assetName.uasset"
if (-not (Test-Path -LiteralPath $stagedAsset -PathType Leaf)) {
throw "Verified BuildHost asset copy is missing: $stagedAsset"
}
$stagedHash = (Get-FileHash -LiteralPath $stagedAsset -Algorithm SHA256).Hash
if ($stagedHash -ne $expectedAssetHashes[$assetName]) {
throw "BuildHost asset copy differs from the recorded round-2 hash: $assetName"
}
}
$dotnet = (Get-Command dotnet.exe -ErrorAction Stop).Source
$argumentList = @(
('"{0}"' -f $UbtDll), 'UnrealEditor', 'Win64', 'Development',
('-Project="{0}"' -f $HostProject), '-WaitMutex', '-NoHotReloadFromIDE',
'-NoUBA', '-NoXGE', '-NoFASTBuild', '-NoSNDBS',
"-MaxParallelActions=$MaxParallelActions", ('-Log="{0}"' -f $BuildLog)
)
$crashRoots = @((Join-Path $HostRoot 'Saved\Crashes'), (Join-Path $StagePlugin 'Saved\Crashes'))
$crashBefore = @{}
foreach ($crashRoot in $crashRoots) {
if (Test-Path -LiteralPath $crashRoot -PathType Container) {
foreach ($crash in @(Get-ChildItem -LiteralPath $crashRoot -Directory -Filter 'UECC-*' -Recurse -Force)) {
$crashBefore[$crash.FullName.ToLowerInvariant()] = $crash.LastWriteTimeUtc.Ticks
}
}
}
$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."
$process = Start-Process -FilePath $dotnet -ArgumentList $argumentList -WorkingDirectory $PluginRoot `
-RedirectStandardOutput $attemptStdout -RedirectStandardError $attemptStderr `
-WindowStyle Hidden -PassThru
$process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
$started = [datetime]::UtcNow
while (-not $process.HasExited) {
Start-Sleep -Seconds 5
$process.Refresh()
if (([datetime]::UtcNow - $started).TotalSeconds -gt $TimeoutSeconds) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
throw "BuildHost UBT exceeded the $TimeoutSeconds-second time box; stopped PID $($process.Id)."
}
if (-not $process.HasExited -and $process.PrivateMemorySize64 -gt 8GB) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
throw "BuildHost UBT exceeded the 8 GiB private-memory limit; stopped PID $($process.Id)."
}
}
$process.WaitForExit()
$process.Refresh()
$buildExitCode = $process.ExitCode
if ([string]::IsNullOrEmpty([string]$buildExitCode) -and (Test-Path -LiteralPath $BuildLog -PathType Leaf) -and
((Get-Content -LiteralPath $BuildLog -Raw) -match '(?m)^Result: Succeeded\s*$')) {
# Some Windows Start-Process handles lose the exit-code field after UBT's child exits;
# accept only UBT's explicit successful target result in that case.
$buildExitCode = 0
}
if ($buildExitCode -eq 0) {
$buildSucceeded = $true
Write-Host "BuildHost UBT attempt $attempt passed."
} elseif ($buildExitCode -eq -1073741502 -and $attempt -eq 1) {
Write-Warning 'BuildHost UBT exited with 0xC0000142; retrying once as requested.'
} else {
throw "BuildHost UBT attempt $attempt failed with exit code '$buildExitCode'. See $BuildLog."
}
}
if (-not $buildSucceeded) { throw 'BuildHost UBT did not complete successfully.' }
$manifestRows = @(Get-Content -LiteralPath $CoreManifest | ForEach-Object { $_.Trim().Replace('\', '/') } |
Where-Object { $_ -and -not $_.StartsWith('#') })
$expectedObjects = @($manifestRows | ForEach-Object {
'VFCore_' + ($_ -replace '[^A-Za-z0-9]+', '_') + '.cpp.obj'
})
$objectRoot = Join-Path $StagePlugin 'Intermediate\Build'
$coreObjects = @(Get-ChildItem -LiteralPath $objectRoot -Recurse -File -Filter 'VFCore_*.cpp.obj' |
Where-Object { $_.FullName -match '\\VoxelForgeCore\\' })
$actualObjects = @($coreObjects | ForEach-Object { $_.Name } | Sort-Object -Unique)
$missingObjects = @($expectedObjects | Where-Object { $actualObjects -notcontains $_ })
$unexpectedObjects = @($actualObjects | Where-Object { $expectedObjects -notcontains $_ })
if ($missingObjects.Count -gt 0 -or $unexpectedObjects.Count -gt 0 -or $coreObjects.Count -ne $manifestRows.Count) {
throw "Core link input inventory mismatch: expected $($manifestRows.Count) shims, found $($coreObjects.Count); missing=[$($missingObjects -join ',')], extra=[$($unexpectedObjects -join ',')]."
}
$libTool = Find-VsTool 'lib.exe'
if (Test-Path -LiteralPath $AuditLibrary -PathType Leaf) { Remove-Item -LiteralPath $AuditLibrary -Force }
& $libTool '/nologo' ("/OUT:{0}" -f $AuditLibrary) @($coreObjects.FullName)
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $AuditLibrary -PathType Leaf)) {
throw "Could not archive the exact UBT Core link inputs for symbol inspection (exit $LASTEXITCODE)."
}
& (Join-Path $PluginRoot 'Standalone\Core\CheckCoreSymbols.ps1') `
-LibraryPath $AuditLibrary -ResultPath $AuditResult
$newCrashes = [Collections.Generic.List[string]]::new()
foreach ($crashRoot in $crashRoots) {
if (Test-Path -LiteralPath $crashRoot -PathType Container) {
foreach ($crash in @(Get-ChildItem -LiteralPath $crashRoot -Directory -Filter 'UECC-*' -Recurse -Force)) {
if (-not $crashBefore.ContainsKey($crash.FullName.ToLowerInvariant())) {
[void]$newCrashes.Add($crash.FullName)
}
}
}
}
if ($newCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during the BuildHost run: $($newCrashes -join '; ')"
}
Write-Host "Verified the five recorded asset hashes in the existing BuildHost Content copies."
Write-Host "Core manifest shims linked and audited: $($coreObjects.Count)"
Write-Host "Link-safety result: $AuditResult"
Write-Host "Build log: $BuildLog"
+203
View File
@@ -0,0 +1,203 @@
[CmdletBinding()]
param(
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900
)
# Run the owner-asset adapter and Core parity gate in the isolated BuildHost only.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$PluginRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$HostRoot = Join-Path $PluginRoot 'Saved\BuildHost\HostProject'
$HostProject = Join-Path $HostRoot 'HostProject.uproject'
$StagePlugin = Join-Path $HostRoot 'Plugins\VoxelForge'
$OwnerAssetRoot = Join-Path ([IO.Path]::GetFullPath((Join-Path $PluginRoot '..\..'))) 'Content\VoxelForge'
$OutputRoot = Join-Path $StagePlugin 'Saved\CoreAdapterValidation'
$OutputDirectory = Join-Path $OutputRoot 'Output'
$LogDirectory = Join-Path $OutputRoot 'Logs'
$LocalAppData = Join-Path $OutputRoot 'LocalAppData'
$UserDirectory = Join-Path $OutputRoot 'User'
$DerivedDataCache = Join-Path $PluginRoot 'Saved\BuildHost\VFCoreAdapterDDC'
$ZenData = Join-Path $OutputRoot 'ZenData'
$EditorCmd = 'E:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
$LogPath = Join-Path $LogDirectory 'CoreAdapterValidation.log'
$StdoutPath = Join-Path $LogDirectory 'CoreAdapterValidation.stdout.log'
$StderrPath = Join-Path $LogDirectory 'CoreAdapterValidation.stderr.log'
$WorldJson = Join-Path $OutputDirectory 'owner_world.json'
$MemoryLimitBytes = 8GB
function Assert-UnderPath([string]$Candidate, [string]$Root) {
$candidateFull = [IO.Path]::GetFullPath($Candidate).TrimEnd('\', '/')
$rootFull = [IO.Path]::GetFullPath($Root).TrimEnd('\', '/')
if (-not ($candidateFull.Equals($rootFull, [StringComparison]::OrdinalIgnoreCase) -or
$candidateFull.StartsWith($rootFull + '\', [StringComparison]::OrdinalIgnoreCase))) {
throw "Refusing a commandlet path outside BuildHost: $candidateFull"
}
}
Assert-UnderPath $HostRoot (Join-Path $PluginRoot 'Saved\BuildHost')
Assert-UnderPath $OutputRoot (Join-Path $PluginRoot 'Saved\BuildHost')
foreach ($required in @($HostProject, $EditorCmd)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
throw "Required BuildHost commandlet input is missing: $required"
}
}
$foreignEditors = @(Get-Process -ErrorAction SilentlyContinue |
Where-Object { $_.ProcessName -like 'UnrealEditor*' })
if ($foreignEditors.Count -gt 0) {
$ids = ($foreignEditors | ForEach-Object { "$($_.ProcessName):$($_.Id)" }) -join ', '
throw "An UnrealEditor process is already running; leaving it untouched: $ids"
}
foreach ($assetName in @('DA_Settings', 'DA_Strate1', 'DA_Strate2', 'DA_Strate3', 'DA_Strate4')) {
$ownerAsset = Join-Path $OwnerAssetRoot "$assetName.uasset"
$stagedAsset = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
if (-not (Test-Path -LiteralPath $ownerAsset -PathType Leaf) -or
-not (Test-Path -LiteralPath $stagedAsset -PathType Leaf)) {
throw "Owner or BuildHost asset is missing: $assetName"
}
if ((Get-FileHash -LiteralPath $ownerAsset -Algorithm SHA256).Hash -ne
(Get-FileHash -LiteralPath $stagedAsset -Algorithm SHA256).Hash) {
throw "BuildHost asset copy differs from the owner asset: $assetName"
}
}
foreach ($directory in @($OutputDirectory, $LogDirectory, $LocalAppData,
$UserDirectory, $DerivedDataCache, $ZenData)) {
[IO.Directory]::CreateDirectory($directory) | Out-Null
}
$CrashRoots = @((Join-Path $HostRoot 'Saved\Crashes'), (Join-Path $StagePlugin 'Saved\Crashes'))
$CrashBefore = @{}
foreach ($CrashRoot in $CrashRoots) {
if (Test-Path -LiteralPath $CrashRoot -PathType Container) {
foreach ($Crash in @(Get-ChildItem -LiteralPath $CrashRoot -Directory -Filter 'UECC-*' -Recurse -Force)) {
$CrashBefore[$Crash.FullName.ToLowerInvariant()] = $Crash.LastWriteTimeUtc.Ticks
}
}
}
function Get-NewCrashFolders {
$Found = [Collections.Generic.List[string]]::new()
foreach ($CrashRoot in $CrashRoots) {
if (Test-Path -LiteralPath $CrashRoot -PathType Container) {
foreach ($Crash in @(Get-ChildItem -LiteralPath $CrashRoot -Directory -Filter 'UECC-*' -Recurse -Force)) {
if (-not $CrashBefore.ContainsKey($Crash.FullName.ToLowerInvariant())) {
[void]$Found.Add($Crash.FullName)
}
}
}
}
return @($Found.ToArray())
}
$Arguments = @(
('-project="{0}"' -f $HostProject),
'-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound',
'-stdout', '-FullStdOutLogOutput', '-forcelogflush', '-NoZenAutoLaunch',
'-notraceserver', '-nocrashreports', '-ddc=NoZenLocalFallback',
('-userdir="{0}"' -f $UserDirectory),
('-ZenDataPath="{0}"' -f $ZenData),
('-LocalDataCachePath="{0}"' -f $DerivedDataCache),
'-run=VoxelForgeExplore', '-seed=0', '-archetype=TunnelNetwork', '-slot=0',
'-modes=export', '-blockearlyout=0', '-gameconfig=1', '-standaloneworldexport=1',
'-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings',
('-out="{0}"' -f $OutputDirectory)
)
$OldLocalAppData = $env:LOCALAPPDATA
$env:LOCALAPPDATA = $LocalAppData
try {
$PeakPrivateBytes = [int64]0
$Succeeded = $false
for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) {
$AttemptLogPath = if ($Attempt -eq 1) { $LogPath } else {
Join-Path $LogDirectory 'CoreAdapterValidation.retry.log'
}
$AttemptStdoutPath = if ($Attempt -eq 1) { $StdoutPath } else {
Join-Path $LogDirectory 'CoreAdapterValidation.retry.stdout.log'
}
$AttemptStderrPath = if ($Attempt -eq 1) { $StderrPath } else {
Join-Path $LogDirectory 'CoreAdapterValidation.retry.stderr.log'
}
$AttemptArguments = @($Arguments) + ('-abslog="{0}"' -f $AttemptLogPath)
Write-Host "BuildHost adapter commandlet attempt $Attempt of 2; normal priority."
$Process = Start-Process -FilePath $EditorCmd -ArgumentList $AttemptArguments -WorkingDirectory $HostRoot `
-RedirectStandardOutput $AttemptStdoutPath -RedirectStandardError $AttemptStderrPath `
-WindowStyle Hidden -PassThru
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
$StartedUtc = [datetime]::UtcNow
while (-not $Process.HasExited) {
Start-Sleep -Seconds 2
$Process.Refresh()
if (-not $Process.HasExited) {
$PeakPrivateBytes = [Math]::Max($PeakPrivateBytes, [int64]$Process.PrivateMemorySize64)
}
if (([datetime]::UtcNow - $StartedUtc).TotalSeconds -gt $TimeoutSeconds) {
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
throw "BuildHost adapter commandlet exceeded the $TimeoutSeconds-second time box."
}
if (-not $Process.HasExited -and $Process.PrivateMemorySize64 -gt $MemoryLimitBytes) {
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
throw 'BuildHost adapter commandlet exceeded the 8 GiB private-memory limit.'
}
}
$Process.WaitForExit()
$Process.Refresh()
$ProcessExitCode = $Process.ExitCode
if ([string]::IsNullOrEmpty([string]$ProcessExitCode) -and (Test-Path -LiteralPath $AttemptLogPath -PathType Leaf)) {
$CompletedLog = Get-Content -LiteralPath $AttemptLogPath -Raw
if ((($CompletedLog -match 'Commandlet->Main return this error code: 0') -or
($CompletedLog -match 'Commandlet VoxelForgeExploreCommandlet_\d+ finished execution \(result 0\)')) -and
($CompletedLog -match '\[VoxelForgeCoreAdapter\] pass schema=1')) {
# Unreal returned its commandlet result through the log, but Start-Process did not
# retain an ExitCode value for this editor process handle.
$ProcessExitCode = 0
}
}
if ($ProcessExitCode -eq -1073741502 -and $Attempt -eq 1) {
Write-Warning 'BuildHost adapter commandlet exited with 0xC0000142; retrying once.'
continue
}
if ($ProcessExitCode -ne 0) {
$FailedRunCrashes = @(Get-NewCrashFolders)
$CrashDetail = if ($FailedRunCrashes.Count -gt 0) {
" New UECC folders: $($FailedRunCrashes -join '; ')."
} else { '' }
throw "BuildHost adapter commandlet attempt $Attempt failed with exit code '$ProcessExitCode'.$CrashDetail See $AttemptLogPath."
}
$LogPath = $AttemptLogPath
$Succeeded = $true
}
if (-not $Succeeded) {
throw 'BuildHost adapter commandlet did not complete successfully.'
}
} finally {
if ($null -eq $OldLocalAppData) {
Remove-Item Env:LOCALAPPDATA -ErrorAction SilentlyContinue
} else {
$env:LOCALAPPDATA = $OldLocalAppData
}
}
if (-not (Test-Path -LiteralPath $WorldJson -PathType Leaf)) {
throw "The adapter commandlet did not write $WorldJson."
}
$BuildLogText = Get-Content -LiteralPath $LogPath -Raw
foreach ($WorkerCount in @(1, 8, 16)) {
$ExpectedAdapterPass = "\[VoxelForgeCoreAdapter\] pass schema=1 workers=$WorkerCount asset_clip_radius=\d+ asset_max_level=\d+ reference_clip_radius=3 reference_max_level=8 tiles=2226 density=0xdb2991c73215af4c mesh=0x8236c5edd7fc8e4f payload=0x4c48fa75a543d58e topology=0xbea22e76b34a4dfc walk=5642/5642"
if ($BuildLogText -notmatch $ExpectedAdapterPass) {
throw "The BuildHost log is missing the exact Core adapter parity result for workers=$WorkerCount. See $LogPath."
}
}
$NewCrashes = @(Get-NewCrashFolders)
if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during adapter validation: $($NewCrashes -join '; ')"
}
Write-Host 'VOXELFORGE_CORE_ADAPTER_PASS tiles=2226 walkability=5642/5642'
Write-Host "Core adapter log: $LogPath"
Write-Host "Schema-v1 JSON: $WorldJson"
Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes"
+197
View File
@@ -0,0 +1,197 @@
[CmdletBinding()]
param(
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900
)
# Exercise AVoxelWorld Core streaming, collision, anchor movement, and carving in BuildHost only.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$PluginRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$HostRoot = Join-Path $PluginRoot 'Saved\BuildHost\HostProject'
$StagePlugin = Join-Path $HostRoot 'Plugins\VoxelForge'
$HostProject = Join-Path $HostRoot 'HostProject.uproject'
$OutputRoot = Join-Path $StagePlugin 'Saved\CoreWorldValidation'
$OutputDirectory = Join-Path $OutputRoot 'Output'
$LogDirectory = Join-Path $OutputRoot 'Logs'
$LocalAppData = Join-Path $OutputRoot 'LocalAppData'
$UserDirectory = Join-Path $OutputRoot 'User'
$DerivedDataCache = Join-Path $PluginRoot 'Saved\BuildHost\VFCoreWorldDDC'
$ZenData = Join-Path $OutputRoot 'ZenData'
$EditorCmd = 'E:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
$MemoryLimitBytes = 8GB
$CrashRoots = @((Join-Path $HostRoot 'Saved\Crashes'), (Join-Path $StagePlugin 'Saved\Crashes'))
function Assert-UnderPath([string]$Candidate, [string]$Root) {
$candidateFull = [IO.Path]::GetFullPath($Candidate).TrimEnd('\', '/')
$rootFull = [IO.Path]::GetFullPath($Root).TrimEnd('\', '/')
if (-not ($candidateFull.Equals($rootFull, [StringComparison]::OrdinalIgnoreCase) -or
$candidateFull.StartsWith($rootFull + '\', [StringComparison]::OrdinalIgnoreCase))) {
throw "Refusing a BuildHost path outside its root: $candidateFull"
}
}
Assert-UnderPath $HostRoot (Join-Path $PluginRoot 'Saved\BuildHost')
Assert-UnderPath $OutputRoot (Join-Path $StagePlugin 'Saved')
foreach ($required in @($HostProject, $EditorCmd)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
throw "Required BuildHost commandlet input is missing: $required"
}
}
$foreignEditors = @(Get-Process -ErrorAction SilentlyContinue |
Where-Object { $_.ProcessName -like 'UnrealEditor*' })
if ($foreignEditors.Count -gt 0) {
$ids = ($foreignEditors | ForEach-Object { "$($_.ProcessName):$($_.Id)" }) -join ', '
throw "An UnrealEditor process is already running; leaving it untouched: $ids"
}
$ExpectedAssetHashes = @{
'DA_Settings' = '5D2977F57E75A63B6B73499CD6E0AA7FBBF3718CE7F70318107BF922F0819AB7'
'DA_Strate1' = 'CDA1F985E589F28CA6E93A068FECC3CA92487E4AD5700F2C301C4B563E40E5F9'
'DA_Strate2' = '02F56A18CED991BAF93DFB2F69438152275E8353B5E279C1CB7DD6E079E352E2'
'DA_Strate3' = 'F47437F8D156C608AA06E40C5B76716ED4A19FA0F057688E7FECC04DC976428C'
'DA_Strate4' = '279570AE3CBA7B3507269C8209A7C931EEB7A15CC3241CE07B50120996B1C656'
}
foreach ($assetName in $ExpectedAssetHashes.Keys) {
$stagedAsset = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
if (-not (Test-Path -LiteralPath $stagedAsset -PathType Leaf)) {
throw "Verified BuildHost asset copy is missing: $assetName"
}
if ((Get-FileHash -LiteralPath $stagedAsset -Algorithm SHA256).Hash -ne $ExpectedAssetHashes[$assetName]) {
throw "BuildHost asset copy differs from the recorded round-2 hash: $assetName"
}
}
foreach ($directory in @($OutputDirectory, $LogDirectory, $LocalAppData,
$UserDirectory, $DerivedDataCache, $ZenData)) {
[IO.Directory]::CreateDirectory($directory) | Out-Null
}
$CrashBefore = @{}
foreach ($CrashRoot in $CrashRoots) {
if (Test-Path -LiteralPath $CrashRoot -PathType Container) {
foreach ($Crash in @(Get-ChildItem -LiteralPath $CrashRoot -Directory -Filter 'UECC-*' -Recurse -Force)) {
$CrashBefore[$Crash.FullName.ToLowerInvariant()] = $Crash.LastWriteTimeUtc.Ticks
}
}
}
function Get-NewCrashFolders {
$Found = [Collections.Generic.List[string]]::new()
foreach ($CrashRoot in $CrashRoots) {
if (Test-Path -LiteralPath $CrashRoot -PathType Container) {
foreach ($Crash in @(Get-ChildItem -LiteralPath $CrashRoot -Directory -Filter 'UECC-*' -Recurse -Force)) {
if (-not $CrashBefore.ContainsKey($Crash.FullName.ToLowerInvariant())) {
[void]$Found.Add($Crash.FullName)
}
}
}
}
return @($Found.ToArray())
}
$BaseArguments = @(
('-project="{0}"' -f $HostProject),
'-nullrhi', '-unattended', '-nop4', '-nosplash', '-NoSound',
'-stdout', '-FullStdOutLogOutput', '-forcelogflush', '-NoZenAutoLaunch',
'-notraceserver', '-nocrashreports', '-ddc=NoZenLocalFallback',
('-userdir="{0}"' -f $UserDirectory),
('-ZenDataPath="{0}"' -f $ZenData),
('-LocalDataCachePath="{0}"' -f $DerivedDataCache),
'-run=VoxelForgeExplore', '-seed=0', '-archetype=TunnelNetwork', '-slot=0',
'-modes=export', '-gameconfig=1', '-coreworldtest=1',
'-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings',
('-out="{0}"' -f $OutputDirectory)
)
$OldLocalAppData = $env:LOCALAPPDATA
$env:LOCALAPPDATA = $LocalAppData
try {
$Succeeded = $false
for ($Attempt = 1; $Attempt -le 2 -and -not $Succeeded; ++$Attempt) {
if ($Attempt -eq 1) {
$LogFileName = 'CoreWorldValidation.log'
$StdoutFileName = 'CoreWorldValidation.stdout.log'
$StderrFileName = 'CoreWorldValidation.stderr.log'
} else {
$LogFileName = 'CoreWorldValidation.retry.log'
$StdoutFileName = 'CoreWorldValidation.retry.stdout.log'
$StderrFileName = 'CoreWorldValidation.retry.stderr.log'
}
$LogPath = Join-Path $LogDirectory $LogFileName
$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."
$Process = Start-Process -FilePath $EditorCmd -ArgumentList $Arguments -WorkingDirectory $HostRoot `
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath `
-WindowStyle Hidden -PassThru
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
$StartedUtc = [datetime]::UtcNow
$PeakPrivateBytes = [int64]0
while (-not $Process.HasExited) {
Start-Sleep -Seconds 2
$Process.Refresh()
if (-not $Process.HasExited) {
$PeakPrivateBytes = [Math]::Max($PeakPrivateBytes, [int64]$Process.PrivateMemorySize64)
}
if (([datetime]::UtcNow - $StartedUtc).TotalSeconds -gt $TimeoutSeconds) {
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
throw "BuildHost Core actor commandlet exceeded the $TimeoutSeconds-second time box."
}
if (-not $Process.HasExited -and $Process.PrivateMemorySize64 -gt $MemoryLimitBytes) {
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
throw 'BuildHost Core actor commandlet exceeded the 8 GiB private-memory limit.'
}
}
$Process.WaitForExit()
$Process.Refresh()
$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
$CompletedLog -match 'Commandlet VoxelForgeExploreCommandlet_\d+ finished execution \(result 0\)') {
$ExitCode = 0
}
}
if ($ExitCode -eq -1073741502 -and $Attempt -eq 1) {
Write-Warning 'BuildHost Core actor commandlet exited with 0xC0000142; retrying once.'
continue
}
if ($ExitCode -ne 0) {
$NewCrashes = @(Get-NewCrashFolders)
$CrashDetail = if ($NewCrashes.Count -gt 0) { " New UECC folders: $($NewCrashes -join '; ')." } else { '' }
throw "BuildHost Core actor commandlet attempt $Attempt failed with exit code '$ExitCode'.$CrashDetail See $LogPath."
}
$Succeeded = $true
}
if (-not $Succeeded) { throw 'BuildHost Core actor commandlet did not complete successfully.' }
} finally {
if ($null -eq $OldLocalAppData) {
Remove-Item Env:LOCALAPPDATA -ErrorAction SilentlyContinue
} else {
$env:LOCALAPPDATA = $OldLocalAppData
}
}
$FinalLog = Join-Path $LogDirectory 'CoreWorldValidation.log'
if (-not (Test-Path -LiteralPath $FinalLog -PathType Leaf)) {
$FinalLog = Join-Path $LogDirectory 'CoreWorldValidation.retry.log'
}
$Text = Get-Content -LiteralPath $FinalLog -Raw
foreach ($Phase in @('initial', 'anchor', 'edit', 'surface_travel', 'surface_strate_travel')) {
if ($Text -notmatch "\[VoxelForgeCoreWorldTest\] pass phase=$Phase") {
throw "BuildHost Core actor log is missing the '$Phase' validation marker. See $FinalLog."
}
}
if ($Text -notmatch '\[VoxelForgeCoreWorldTest\] worker_hash_matrix=1/8/16 stable=1') {
throw "BuildHost Core actor log is missing the W1/W8/W16 determinism marker. See $FinalLog."
}
$NewCrashes = @(Get-NewCrashFolders)
if ($NewCrashes.Count -gt 0) {
throw "New UECC crash folders appeared during Core actor validation: $($NewCrashes -join '; ')"
}
Write-Host 'VOXELFORGE_CORE_WORLD_PASS streaming=1 collision=1 anchor_move=1 carve=1'
Write-Host "Core actor log: $FinalLog"
Write-Host "Peak commandlet private bytes sampled: $PeakPrivateBytes"