[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"