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:
1 parent
d5ce5d675d
commit
874c796c65
20 files changed
+2880
-12
No files matched your search
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user