$script:MoonWellExtensionNames = @( 'MoonWell', 'wxl-db2', 'wxl-fdid-moonwell', 'wxl-grasswind', 'wxl-modern-adt', 'wxl-modern-blp', 'wxl-modern-m2', 'wxl-modern-wmo', 'wxl-moonwell-storage-fallback', 'wxl-unit-outline' ) $patchLayoutPath = Join-Path $PSScriptRoot 'patch-layout.json' if (-not (Test-Path -LiteralPath $patchLayoutPath -PathType Leaf)) { throw "Patch layout was not found: $patchLayoutPath" } $patchLayout = Get-Content -LiteralPath $patchLayoutPath -Raw -Encoding UTF8 | ConvertFrom-Json $script:MoonWellRepositoryPatches = @($patchLayout.repositoryPatches) $script:MoonWellGraphicsPatches = @($patchLayout.graphicsPatches) function Get-MoonWellExtensionNames { return @($script:MoonWellExtensionNames) } function Get-MoonWellRepositoryPatchPaths { return @($script:MoonWellRepositoryPatches) } function Get-MoonWellGraphicsPatchPaths { return @($script:MoonWellGraphicsPatches) } function Test-MoonWellRepositoryPatch { param([Parameter(Mandatory)][string]$RelativePath) foreach ($repositoryPatch in $script:MoonWellRepositoryPatches) { if ($RelativePath.Replace('/', '\').Equals( $repositoryPatch.Replace('/', '\'), [System.StringComparison]::OrdinalIgnoreCase)) { return $true } } return $false } function Test-MoonWellGraphicsPatch { param([Parameter(Mandatory)][string]$RelativePath) foreach ($graphicsPatch in $script:MoonWellGraphicsPatches) { if ($RelativePath.Replace('/', '\').Equals( $graphicsPatch.Replace('/', '\'), [System.StringComparison]::OrdinalIgnoreCase)) { return $true } } return $false } function Test-MoonWellPreservedDataPatch { param([Parameter(Mandatory)][string]$RelativePath) if (Test-MoonWellRepositoryPatch -RelativePath $RelativePath) { return $false } return Test-MoonWellGraphicsPatch -RelativePath $RelativePath } function Assert-SafePackagePath { param( [Parameter(Mandatory)] [string]$PackagePath, [Parameter(Mandatory)] [string]$RepositoryPath, [Parameter()] [string]$ClientPath ) $packageRoot = [System.IO.Path]::GetFullPath($PackagePath).TrimEnd('\') $repositoryRoot = [System.IO.Path]::GetFullPath($RepositoryPath).TrimEnd('\') $pathRoot = [System.IO.Path]::GetPathRoot($packageRoot).TrimEnd('\') if (-not $packageRoot -or $packageRoot -eq $pathRoot) { throw "Refusing to use a filesystem root as package staging: $packageRoot" } if ($packageRoot.Equals($repositoryRoot, [System.StringComparison]::OrdinalIgnoreCase) -or $repositoryRoot.StartsWith($packageRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)) { throw 'Package staging must not be the repository root or one of its ancestors.' } if (-not [string]::IsNullOrWhiteSpace($ClientPath)) { $clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\') $packageIsClientOrChild = $packageRoot.Equals($clientRoot, [System.StringComparison]::OrdinalIgnoreCase) -or $packageRoot.StartsWith($clientRoot + '\', [System.StringComparison]::OrdinalIgnoreCase) $packageContainsClient = $clientRoot.StartsWith($packageRoot + '\', [System.StringComparison]::OrdinalIgnoreCase) if ($packageIsClientOrChild -or $packageContainsClient) { throw 'Package staging must be separate from the installed client tree.' } } } function Get-RelativeFilePaths { param([Parameter(Mandatory)][string]$Root) if (-not (Test-Path -LiteralPath $Root -PathType Container)) { return @() } $absoluteRoot = [System.IO.Path]::GetFullPath($Root).TrimEnd('\') return @( Get-ChildItem -LiteralPath $absoluteRoot -Recurse -Force -File | ForEach-Object { $_.FullName.Substring($absoluteRoot.Length + 1) } ) } function Remove-EmptyMoonWellDirectories { param([Parameter(Mandatory)][string]$Destination) foreach ($relativeRoot in @('Data\Patch-WXL.MPQ', 'Extensions', 'Utils')) { $managedRoot = Join-Path $Destination $relativeRoot if (-not (Test-Path -LiteralPath $managedRoot -PathType Container)) { continue } $directories = @( Get-ChildItem -LiteralPath $managedRoot -Recurse -Force -Directory | Sort-Object { $_.FullName.Length } -Descending ) $directories += Get-Item -LiteralPath $managedRoot foreach ($directory in $directories) { if (-not @(Get-ChildItem -LiteralPath $directory.FullName -Force).Count) { Remove-Item -LiteralPath $directory.FullName -Force Write-Host "Removed empty managed directory: $($directory.FullName)" } } } } function Reset-MoonWellPackageStaging { param( [Parameter(Mandatory)][string]$PackagePath, [Parameter(Mandatory)][string]$RepositoryPath, [Parameter(Mandatory)][string]$ClientPath ) Assert-SafePackagePath ` -PackagePath $PackagePath ` -RepositoryPath $RepositoryPath ` -ClientPath $ClientPath if (Test-Path -LiteralPath $PackagePath) { Remove-Item -LiteralPath $PackagePath -Recurse -Force } New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null } function Remove-MoonWellRuntimeGarbage { param([Parameter(Mandatory)][string]$Destination) if (-not (Test-Path -LiteralPath $Destination -PathType Container)) { return } $allowedExtensions = @{} foreach ($name in $script:MoonWellExtensionNames) { $allowedExtensions[$name.ToLowerInvariant()] = $true } $extensionsRoot = Join-Path $Destination 'Extensions' if (Test-Path -LiteralPath $extensionsRoot -PathType Container) { foreach ($entry in Get-ChildItem -LiteralPath $extensionsRoot -Force) { $isAllowedDirectory = $entry.PSIsContainer -and $allowedExtensions.ContainsKey($entry.Name.ToLowerInvariant()) if (-not $isAllowedDirectory) { Remove-Item -LiteralPath $entry.FullName -Recurse -Force Write-Host "Removed unmanaged WarcraftXL extension artifact: $($entry.FullName)" continue } foreach ($dll in Get-ChildItem -LiteralPath $entry.FullName -Force -File -Filter '*.dll') { $expectedName = "$($entry.Name).dll" if (-not $dll.Name.Equals($expectedName, [System.StringComparison]::OrdinalIgnoreCase)) { Remove-Item -LiteralPath $dll.FullName -Force Write-Host "Removed stale extension DLL: $($dll.FullName)" } } } } foreach ($dll in Get-ChildItem -LiteralPath $Destination -Force -File -Filter 'WarcraftXL*.dll') { if (-not $dll.Name.Equals('WarcraftXL.dll', [System.StringComparison]::OrdinalIgnoreCase)) { Remove-Item -LiteralPath $dll.FullName -Force Write-Host "Removed stale WarcraftXL DLL: $($dll.FullName)" } } $utilsRoot = Join-Path $Destination 'Utils' if (Test-Path -LiteralPath $utilsRoot -PathType Container) { foreach ($legacyFile in Get-ChildItem -LiteralPath $utilsRoot -Force -File -Filter 'WarcraftXLHost*') { Remove-Item -LiteralPath $legacyFile.FullName -Force Write-Host "Removed obsolete WarcraftXL 1.0 host artifact: $($legacyFile.FullName)" } } Remove-EmptyMoonWellDirectories -Destination $Destination } function Remove-StalePackageFilesFromClient { param( [Parameter(Mandatory)][string]$ClientPath, [Parameter(Mandatory)][AllowEmptyCollection()][string[]]$PreviousFiles, [Parameter(Mandatory)][AllowEmptyCollection()][string[]]$CurrentFiles ) $current = @{} foreach ($relativePath in $CurrentFiles) { $current[$relativePath.ToLowerInvariant()] = $true } $clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\') foreach ($relativePath in $PreviousFiles) { if ($current.ContainsKey($relativePath.ToLowerInvariant()) -or (Test-MoonWellPreservedDataPatch -RelativePath $relativePath)) { continue } $target = [System.IO.Path]::GetFullPath((Join-Path $clientRoot $relativePath)) if (-not $target.StartsWith($clientRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)) { throw "Package inventory contains a path outside the client: $relativePath" } if (Test-Path -LiteralPath $target -PathType Leaf) { Remove-Item -LiteralPath $target -Force Write-Host "Removed stale packaged file from client: $target" } } Remove-EmptyMoonWellDirectories -Destination $ClientPath }