wip
This commit is contained in:
@@ -109,6 +109,27 @@ $env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
|
||||
.\run.ps1
|
||||
```
|
||||
|
||||
Для быстрой разработки интерфейса без пересборки MPQ:
|
||||
|
||||
```powershell
|
||||
# Наложить src\Data\...\Interface на клиент и следить за изменениями
|
||||
.\hotreload.ps1
|
||||
|
||||
# То же самое, но запустить клиент и автоматически завершить сессию после его выхода
|
||||
.\hotreload.ps1 -Launch
|
||||
|
||||
# Явно указать клиент, если WOW_HOME не задан
|
||||
.\hotreload.ps1 -ClientPath 'D:\Games\World of Warcraft'
|
||||
|
||||
# Подключиться к другому серверу вместо локального
|
||||
.\hotreload.ps1 -Realmlist 'logon.example.org'
|
||||
```
|
||||
|
||||
Во время сессии изменённые файлы сразу копируются в `WOW_HOME\Interface`; увидеть изменения в игре
|
||||
можно после `/reload`. По `Ctrl+C` или после выхода запущенного через `-Launch` клиента временные
|
||||
файлы удаляются, а существовавшие до запуска файлы восстанавливаются. По умолчанию на время сессии
|
||||
в `Data\ruRU\realmlist.wtf` записывается `127.0.0.1`; исходный realmlist также восстанавливается.
|
||||
|
||||
Диагностика запуска находится в `Logs\wxl-core.log` и `Logs\d3d9proxy.log` внутри клиента.
|
||||
|
||||
## MPQ-пакеты
|
||||
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ClientPath = '',
|
||||
[ValidatePattern('^[^\r\n]+$')]
|
||||
[string]$Realmlist = '127.0.0.1',
|
||||
[ValidateRange(100, 10000)]
|
||||
[int]$PollIntervalMilliseconds = 500,
|
||||
[switch]$NoInitialSync,
|
||||
[switch]$Launch
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = [System.IO.Path]::GetFullPath($PSScriptRoot).TrimEnd('\')
|
||||
|
||||
function Import-ProjectEnvironment {
|
||||
$envFile = Join-Path $repoRoot '.env'
|
||||
if (-not (Test-Path -LiteralPath $envFile -PathType Leaf)) {
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($line in Get-Content -LiteralPath $envFile -Encoding UTF8) {
|
||||
if ($line -notmatch '^\s*([^#][^=]+)=(.*)$') {
|
||||
continue
|
||||
}
|
||||
|
||||
$name = $matches[1].Trim()
|
||||
$value = $matches[2].Trim().Trim('"').Trim("'")
|
||||
if ($name -and -not (Test-Path "Env:$name")) {
|
||||
Set-Item -Path "Env:$name" -Value $value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-InterfaceSourceRoots {
|
||||
param([Parameter(Mandatory)][string]$RepositoryPath)
|
||||
|
||||
$dataRoot = Join-Path $RepositoryPath 'src\Data'
|
||||
if (-not (Test-Path -LiteralPath $dataRoot -PathType Container)) {
|
||||
throw "Patch source directory was not found: $dataRoot"
|
||||
}
|
||||
|
||||
$roots = @()
|
||||
foreach ($dataEntry in Get-ChildItem -LiteralPath $dataRoot -Directory | Sort-Object Name) {
|
||||
$directInterface = Join-Path $dataEntry.FullName 'Interface'
|
||||
if (Test-Path -LiteralPath $directInterface -PathType Container) {
|
||||
$roots += [System.IO.Path]::GetFullPath($directInterface).TrimEnd('\')
|
||||
}
|
||||
|
||||
if ($dataEntry.Name -match '^[a-z]{2}[A-Z]{2}$') {
|
||||
foreach ($localePatch in Get-ChildItem -LiteralPath $dataEntry.FullName -Directory | Sort-Object Name) {
|
||||
$localeInterface = Join-Path $localePatch.FullName 'Interface'
|
||||
if (Test-Path -LiteralPath $localeInterface -PathType Container) {
|
||||
$roots += [System.IO.Path]::GetFullPath($localeInterface).TrimEnd('\')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return @($roots)
|
||||
}
|
||||
|
||||
function Get-EffectiveInterfaceFiles {
|
||||
param([Parameter(Mandatory)][string[]]$SourceRoots)
|
||||
|
||||
# Later patch roots win, matching the order in which locale and higher patch
|
||||
# layers override earlier archives in the client.
|
||||
$files = @{}
|
||||
foreach ($sourceRoot in $SourceRoots) {
|
||||
$sourcePrefix = $sourceRoot + '\'
|
||||
foreach ($file in Get-ChildItem -LiteralPath $sourceRoot -Recurse -Force -File) {
|
||||
$relativePath = $file.FullName.Substring($sourcePrefix.Length)
|
||||
if ($relativePath.EndsWith('.manifest.json', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$files[$relativePath] = [pscustomobject]@{
|
||||
RelativePath = $relativePath
|
||||
SourcePath = $file.FullName
|
||||
Signature = '{0}:{1}' -f $file.Length, $file.LastWriteTimeUtc.Ticks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files
|
||||
}
|
||||
|
||||
function Get-SafeTargetPath {
|
||||
param([Parameter(Mandatory)][string]$RelativePath)
|
||||
|
||||
$targetPath = [System.IO.Path]::GetFullPath((Join-Path $interfaceRoot $RelativePath))
|
||||
if (-not $targetPath.StartsWith($interfacePrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Interface path escapes the client Interface directory: $RelativePath"
|
||||
}
|
||||
return $targetPath
|
||||
}
|
||||
|
||||
function Add-CreatedDirectory {
|
||||
param([Parameter(Mandatory)][string]$DirectoryPath)
|
||||
|
||||
if (Test-Path -LiteralPath $DirectoryPath -PathType Container) {
|
||||
return
|
||||
}
|
||||
|
||||
$missing = @()
|
||||
$candidate = $DirectoryPath
|
||||
while ($candidate.StartsWith($interfacePrefix, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||
-not (Test-Path -LiteralPath $candidate -PathType Container)) {
|
||||
$missing += $candidate
|
||||
$candidate = Split-Path -Parent $candidate
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $DirectoryPath -Force | Out-Null
|
||||
foreach ($created in $missing) {
|
||||
$createdDirectories[$created] = $true
|
||||
}
|
||||
}
|
||||
|
||||
function Install-HotReloadFile {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$RelativePath,
|
||||
[Parameter(Mandatory)][string]$SourcePath,
|
||||
[switch]$Quiet
|
||||
)
|
||||
|
||||
$targetPath = Get-SafeTargetPath -RelativePath $RelativePath
|
||||
if (-not $ownedTargets.ContainsKey($RelativePath)) {
|
||||
$hadOriginal = Test-Path -LiteralPath $targetPath -PathType Leaf
|
||||
$backupPath = $null
|
||||
if ($hadOriginal) {
|
||||
$backupPath = Join-Path $backupRoot $RelativePath
|
||||
Add-CreatedDirectory -DirectoryPath (Split-Path -Parent $backupPath)
|
||||
Copy-Item -LiteralPath $targetPath -Destination $backupPath -Force
|
||||
}
|
||||
|
||||
$ownedTargets[$RelativePath] = [pscustomobject]@{
|
||||
RelativePath = $RelativePath
|
||||
TargetPath = $targetPath
|
||||
HadOriginal = $hadOriginal
|
||||
BackupPath = $backupPath
|
||||
}
|
||||
}
|
||||
|
||||
Add-CreatedDirectory -DirectoryPath (Split-Path -Parent $targetPath)
|
||||
Copy-Item -LiteralPath $SourcePath -Destination $targetPath -Force
|
||||
if (-not $Quiet) {
|
||||
Write-Host "Updated: Interface\$RelativePath" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
function Restore-HotReloadTarget {
|
||||
param(
|
||||
[Parameter(Mandatory)]$State,
|
||||
[switch]$Quiet
|
||||
)
|
||||
|
||||
if ($State.HadOriginal) {
|
||||
Add-CreatedDirectory -DirectoryPath (Split-Path -Parent $State.TargetPath)
|
||||
Copy-Item -LiteralPath $State.BackupPath -Destination $State.TargetPath -Force
|
||||
if (-not $Quiet) {
|
||||
Write-Host "Restored: Interface\$($State.RelativePath)"
|
||||
}
|
||||
} elseif (Test-Path -LiteralPath $State.TargetPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $State.TargetPath -Force
|
||||
if (-not $Quiet) {
|
||||
Write-Host "Removed: Interface\$($State.RelativePath)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Import-ProjectEnvironment
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
|
||||
$ClientPath = $env:WOW_HOME
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
|
||||
throw 'ClientPath was not supplied and WOW_HOME is not set.'
|
||||
}
|
||||
|
||||
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\')
|
||||
$clientPrefix = $clientRoot + '\'
|
||||
$wowExecutable = Join-Path $clientRoot 'Wow.exe'
|
||||
if (-not (Test-Path -LiteralPath $wowExecutable -PathType Leaf)) {
|
||||
throw "Wow.exe was not found in the client directory: $clientRoot"
|
||||
}
|
||||
if ($clientRoot.Equals($repoRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
$repoRoot.StartsWith($clientRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw 'The client directory must be separate from the repository.'
|
||||
}
|
||||
|
||||
$sourceRoots = @(Get-InterfaceSourceRoots -RepositoryPath $repoRoot)
|
||||
if (-not $sourceRoots.Count) {
|
||||
throw 'No src\Data\...\Interface patch directories were found.'
|
||||
}
|
||||
|
||||
$interfaceRoot = Join-Path $clientRoot 'Interface'
|
||||
$interfaceRoot = [System.IO.Path]::GetFullPath($interfaceRoot).TrimEnd('\')
|
||||
$interfacePrefix = $interfaceRoot + '\'
|
||||
$systemTempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()).TrimEnd('\')
|
||||
$systemTempPrefix = $systemTempRoot + '\'
|
||||
$backupRoot = [System.IO.Path]::GetFullPath((
|
||||
Join-Path $systemTempRoot ('MoonWell-hotreload-' + [Guid]::NewGuid().ToString('N'))
|
||||
))
|
||||
if (-not $backupRoot.StartsWith($systemTempPrefix, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
-not ([System.IO.Path]::GetFileName($backupRoot)).StartsWith(
|
||||
'MoonWell-hotreload-',
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)) {
|
||||
throw "Unsafe hot reload backup path: $backupRoot"
|
||||
}
|
||||
$ownedTargets = @{}
|
||||
$createdDirectories = @{}
|
||||
$createdRealmlistDirectories = @()
|
||||
$realmlistState = $null
|
||||
$cleanupFailed = $false
|
||||
$launchedProcess = $null
|
||||
|
||||
New-Item -ItemType Directory -Path $interfaceRoot, $backupRoot -Force | Out-Null
|
||||
|
||||
Write-Host 'MoonWell Interface hot reload'
|
||||
Write-Host " Client: $clientRoot"
|
||||
Write-Host " Sources: $($sourceRoots.Count) patch directories"
|
||||
|
||||
try {
|
||||
$realmlistPath = [System.IO.Path]::GetFullPath((
|
||||
Join-Path $clientRoot 'Data\ruRU\realmlist.wtf'
|
||||
))
|
||||
if (-not $realmlistPath.StartsWith(
|
||||
$clientPrefix,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)) {
|
||||
throw "Realmlist path escapes the client directory: $realmlistPath"
|
||||
}
|
||||
|
||||
$realmlistDirectory = Split-Path -Parent $realmlistPath
|
||||
$candidateDirectory = $realmlistDirectory
|
||||
while ($candidateDirectory.StartsWith(
|
||||
$clientPrefix,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
) -and
|
||||
-not (Test-Path -LiteralPath $candidateDirectory -PathType Container)) {
|
||||
$createdRealmlistDirectories += $candidateDirectory
|
||||
$candidateDirectory = Split-Path -Parent $candidateDirectory
|
||||
}
|
||||
New-Item -ItemType Directory -Path $realmlistDirectory -Force | Out-Null
|
||||
|
||||
$hadOriginalRealmlist = Test-Path -LiteralPath $realmlistPath -PathType Leaf
|
||||
$realmlistBackupPath = $null
|
||||
if ($hadOriginalRealmlist) {
|
||||
$realmlistBackupPath = Join-Path $backupRoot 'realmlist.wtf.original'
|
||||
Copy-Item -LiteralPath $realmlistPath -Destination $realmlistBackupPath -Force
|
||||
}
|
||||
$realmlistState = [pscustomobject]@{
|
||||
Path = $realmlistPath
|
||||
HadOriginal = $hadOriginalRealmlist
|
||||
BackupPath = $realmlistBackupPath
|
||||
}
|
||||
Set-Content -LiteralPath $realmlistPath -Value "set realmlist $Realmlist" -Encoding ASCII
|
||||
Write-Host " Realmlist: $Realmlist"
|
||||
|
||||
$previousFiles = Get-EffectiveInterfaceFiles -SourceRoots $sourceRoots
|
||||
|
||||
if (-not $NoInitialSync) {
|
||||
Write-Host "Installing $($previousFiles.Count) loose Interface files..."
|
||||
foreach ($entry in $previousFiles.GetEnumerator()) {
|
||||
Install-HotReloadFile `
|
||||
-RelativePath $entry.Value.RelativePath `
|
||||
-SourcePath $entry.Value.SourcePath `
|
||||
-Quiet
|
||||
}
|
||||
Write-Host 'Initial Interface overlay is ready.' -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host 'Initial sync skipped; only subsequent changes will be overlaid.'
|
||||
}
|
||||
|
||||
if ($Launch) {
|
||||
Write-Host 'Launching Wow.exe...'
|
||||
$launchedProcess = Start-Process `
|
||||
-FilePath $wowExecutable `
|
||||
-WorkingDirectory $clientRoot `
|
||||
-PassThru
|
||||
Write-Host 'Watching until the launched client exits. Press Ctrl+C to stop earlier.'
|
||||
} else {
|
||||
Write-Host 'Watching for changes. Press Ctrl+C to stop and clean the client.'
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
Start-Sleep -Milliseconds $PollIntervalMilliseconds
|
||||
|
||||
if ($launchedProcess) {
|
||||
$launchedProcess.Refresh()
|
||||
if ($launchedProcess.HasExited) {
|
||||
Write-Host 'The launched client has exited.'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$currentFiles = Get-EffectiveInterfaceFiles -SourceRoots $sourceRoots
|
||||
|
||||
foreach ($entry in $currentFiles.GetEnumerator()) {
|
||||
$relativePath = $entry.Key
|
||||
$previous = $previousFiles[$relativePath]
|
||||
if ($null -eq $previous -or $previous.Signature -ne $entry.Value.Signature -or
|
||||
$previous.SourcePath -ne $entry.Value.SourcePath) {
|
||||
Install-HotReloadFile `
|
||||
-RelativePath $entry.Value.RelativePath `
|
||||
-SourcePath $entry.Value.SourcePath
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entry in $previousFiles.GetEnumerator()) {
|
||||
if (-not $currentFiles.ContainsKey($entry.Key) -and $ownedTargets.ContainsKey($entry.Key)) {
|
||||
Restore-HotReloadTarget -State $ownedTargets[$entry.Key]
|
||||
}
|
||||
}
|
||||
|
||||
$previousFiles = $currentFiles
|
||||
}
|
||||
} finally {
|
||||
Write-Host "Cleaning $($ownedTargets.Count) hot reload files from the client..."
|
||||
foreach ($state in $ownedTargets.Values) {
|
||||
try {
|
||||
Restore-HotReloadTarget -State $state -Quiet
|
||||
} catch {
|
||||
$cleanupFailed = $true
|
||||
Write-Warning "Could not clean Interface\$($state.RelativePath): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $realmlistState) {
|
||||
try {
|
||||
if ($realmlistState.HadOriginal) {
|
||||
Copy-Item `
|
||||
-LiteralPath $realmlistState.BackupPath `
|
||||
-Destination $realmlistState.Path `
|
||||
-Force
|
||||
} elseif (Test-Path -LiteralPath $realmlistState.Path -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $realmlistState.Path -Force
|
||||
}
|
||||
} catch {
|
||||
$cleanupFailed = $true
|
||||
Write-Warning "Could not restore realmlist.wtf: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($directoryPath in @($createdRealmlistDirectories | Sort-Object Length -Descending)) {
|
||||
try {
|
||||
if ((Test-Path -LiteralPath $directoryPath -PathType Container) -and
|
||||
-not @(Get-ChildItem -LiteralPath $directoryPath -Force).Count) {
|
||||
Remove-Item -LiteralPath $directoryPath -Force
|
||||
}
|
||||
} catch {
|
||||
$cleanupFailed = $true
|
||||
Write-Warning "Could not remove empty directory $directoryPath"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($directoryPath in @($createdDirectories.Keys | Sort-Object Length -Descending)) {
|
||||
try {
|
||||
if ((Test-Path -LiteralPath $directoryPath -PathType Container) -and
|
||||
-not @(Get-ChildItem -LiteralPath $directoryPath -Force).Count) {
|
||||
Remove-Item -LiteralPath $directoryPath -Force
|
||||
}
|
||||
} catch {
|
||||
$cleanupFailed = $true
|
||||
Write-Warning "Could not remove empty directory $directoryPath"
|
||||
}
|
||||
}
|
||||
|
||||
if ($cleanupFailed) {
|
||||
Write-Warning "Cleanup was incomplete. Original-file backups were kept in: $backupRoot"
|
||||
} else {
|
||||
if (Test-Path -LiteralPath $backupRoot) {
|
||||
$resolvedBackupRoot = (Resolve-Path -LiteralPath $backupRoot).Path
|
||||
if (-not $resolvedBackupRoot.Equals(
|
||||
$backupRoot,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
) -or
|
||||
-not $resolvedBackupRoot.StartsWith(
|
||||
$systemTempPrefix,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)) {
|
||||
throw "Refusing to remove unexpected backup path: $resolvedBackupRoot"
|
||||
}
|
||||
Remove-Item -LiteralPath $resolvedBackupRoot -Recurse -Force
|
||||
}
|
||||
Write-Host 'Hot reload overlay removed; original client files restored.' -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -336,13 +336,13 @@ local function applyRank()
|
||||
elseif progress and progress > 0 then
|
||||
rankBlock.icon:Hide()
|
||||
anchorTitle(false)
|
||||
rankBlock.title:SetText(L("PVP_UNRANKED", "Unranked"))
|
||||
rankBlock.title:SetText(L("PVP_UNRANKED", "Без ранга"))
|
||||
rankBlock.bar:Show()
|
||||
rankBlock.hint:Hide()
|
||||
else
|
||||
rankBlock.icon:Hide()
|
||||
anchorTitle(false)
|
||||
rankBlock.title:SetText(L("PVP_UNRANKED", "Unranked"))
|
||||
rankBlock.title:SetText(L("PVP_UNRANKED", "Без ранга"))
|
||||
rankBlock.bar:Hide()
|
||||
rankBlock.hint:Show()
|
||||
end
|
||||
|
||||
Vendored
+1
-1
Submodule vendor/warcraftxl updated: 1508beb6ec...083a62568c
Reference in New Issue
Block a user