diff --git a/.env.example b/.env.example index 96f07f9..d6042d0 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,4 @@ AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION= AWS_BUCKET= AWS_ENDPOINT= -AWS_USE_PATH_STYLE_ENDPOINT= \ No newline at end of file +AWS_USE_PATH_STYLE_ENDPOINT= diff --git a/.gitignore b/.gitignore index f914e60..ed2187a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ __pycache__/ manifest.json .claude .vscode -dist \ No newline at end of file +dist +build/ +Wow*.exe +*.backup.exe +Logs/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1c08366 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,15 @@ +[submodule "vendor/warcraftxl"] + path = vendor/warcraftxl + url = https://github.com/WarcraftXL/wxl-core.git +[submodule "vendor/modules/wxl-modern-assets"] + path = vendor/modules/wxl-modern-assets + url = https://github.com/WarcraftXL/wxl-modern-assets.git +[submodule "vendor/modules/wxl-modern-render"] + path = vendor/modules/wxl-modern-render + url = https://github.com/WarcraftXL/wxl-modern-render.git +[submodule "vendor/modules/wxl-unit-outline"] + path = vendor/modules/wxl-unit-outline + url = https://github.com/WarcraftXL/wxl-unit-outline.git +[submodule "vendor/modules/wxl-modern-adt"] + path = vendor/modules/wxl-modern-adt + url = https://github.com/WarcraftXL/wxl-modern-adt.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6af59e1 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.20) +project(MoonWellWarcraftXL LANGUAGES CXX) + +# WarcraftXL remains an upstream submodule. MoonWell modules are attached to +# its aggregate DLL target here, so updating the framework does not mix our +# client-specific offsets and policy into the upstream checkout. +add_subdirectory(vendor/warcraftxl) + +set(WXL_EXTERNAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/modules") + +function(wxl_collect_sources output) + file(GLOB_RECURSE collected CONFIGURE_DEPENDS ${ARGN}) + set(${output} "${collected}" PARENT_SCOPE) +endfunction() + +if(TARGET WarcraftXL) + wxl_collect_sources(WXL_MODERN_ASSETS_RUNTIME + "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/src/*.cpp" + "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp") + if(WXL_MODERN_ASSETS_EXCLUDE_REGEX) + list(FILTER WXL_MODERN_ASSETS_RUNTIME EXCLUDE REGEX + "${WXL_MODERN_ASSETS_EXCLUDE_REGEX}") + endif() + wxl_collect_sources(WXL_MODERN_RENDER_RUNTIME + "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/src/*.cpp") + wxl_collect_sources(WXL_UNIT_OUTLINE_RUNTIME + "${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline/src/*.cpp") + wxl_collect_sources(WXL_MODERN_ADT_RUNTIME + "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/src/*.cpp" + "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp") + + target_sources(WarcraftXL PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/src/MoonWell.cpp" + ${WXL_MODERN_ASSETS_RUNTIME} + ${WXL_MODERN_RENDER_RUNTIME} + ${WXL_UNIT_OUTLINE_RUNTIME} + ${WXL_MODERN_ADT_RUNTIME}) + + # wxl-modern-render needs its module-root includes and D3D12 import library. + include("${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/module.cmake") +endif() + +if(TARGET WarcraftXLHost) + wxl_collect_sources(WXL_MODERN_ASSETS_HOST + "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/host/*.cpp" + "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp") + if(WXL_MODERN_ASSETS_EXCLUDE_REGEX) + list(FILTER WXL_MODERN_ASSETS_HOST EXCLUDE REGEX + "${WXL_MODERN_ASSETS_EXCLUDE_REGEX}") + endif() + wxl_collect_sources(WXL_MODERN_ADT_HOST + "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/host/*.cpp" + "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp") + + target_sources(WarcraftXLHost PRIVATE + ${WXL_MODERN_ASSETS_HOST} + ${WXL_MODERN_ADT_HOST}) +endif() + +# Minimal native-D3D9 proxy retained as a recovery renderer. The regular +# package uses WarcraftXL's D3D9On12 proxy required by wxl-modern-render. +if(CMAKE_SIZEOF_VOID_P EQUAL 4) + add_library(MoonWellLoader SHARED + "${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.def") + set_target_properties(MoonWellLoader PROPERTIES + OUTPUT_NAME "d3d9-native" + PREFIX "" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/artifacts") + target_compile_definitions(MoonWellLoader PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) +endif() diff --git a/README.md b/README.md index 7748f09..3055a40 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,143 @@ # MoonWell Client Sources -Репозиторий с ресурсами и исполняемым файлом модифицированного клиента World of Warcraft 3.3.5a. +Исходники клиентских изменений MoonWell для World of Warcraft 3.3.5a (build 12340). -Основная цель проекта: хранить и сопровождать клиентские изменения поверх стандартных MPQ-пакетов WoW, включая интерфейс, графику, звук и дополнительные данные для кастомных систем сервера. +Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL): `Wow.exe` больше не содержит +MoonWell-патчей. Неподписанный оригинальный executable загружается как обычно, локальный +`d3d9.dll` proxy подхватывает `WarcraftXL.dll`, а модуль MoonWell применяет проверенные изменения +только к памяти запущенного процесса. -## Что находится в репозитории +## Что перенесено из модифицированного Wow.exe -- `Wow.exe` - модифицированный клиентский исполняемый файл. - - Поддерживает использование до 4 ГБ ОЗУ. - - Игнорирует блокирование изменений в исходниках `GlueXML`, что позволяет вносить свои изменения в интерфейс и модели. -- `patch-4/` - наши изменения поверх стандартного `patch-4.MPQ`. - - Содержит графические элементы внутриигрового магазина. - - Основная зона изменений: `Interface/Store_UI`. -- `patch-ruRU-4/` - наши изменения поверх стандартных `patch-ruRU-1.MPQ`, `patch-ruRU-2.MPQ` и `patch-ruRU-3.MPQ`. - - Содержит ресурсы локали `ruRU`, связанные с экраном входа, `GlueXML`, заставками и звуком. - - Внутри есть `Interface/GlueXML`, `Interface/Glues`, `Interface/LoginScreen`, `Sound/Ambience`, `Sound/Music`. -- `patch-ruRU-5/` - пакет русской локализации и дополнительных интерфейсных ресурсов. - - Содержит перевод на русский язык для glue-интерфейса и связанных экранов клиента. - - Внутри есть `Interface/GlueXML`, `Interface/Glues`, `Interface/GluesVideo`, `Interface/Loginscreen`, `Interface/tooltips`, `Interface/cinematics`. -- `patch-Z/` - кастомный пакет для модуля Mythic+. - - Содержит графические ресурсы Mythic+. - - Также включает связанные клиентские данные в `DBFielsClient/` и иконки в `Interface/Icons/`. +- разблокировка пользовательского `GlueXML`; +- `CreateCharacter(name, modeId)` и поле режима в `CMSG_CHAR_CREATE`; +- одиннадцатое значение `GetCharacterInfo` — `charFlags`, включая флаг предателя `0x40000000`; +- загрузка WarcraftXL без import-table patch и без отдельного injector. -## Структура по назначению +Large Address Aware намеренно не включён: Windows читает этот PE-флаг до загрузки DLL, поэтому +сохранить его и одновременно оставить файл байт-в-байт оригинальным невозможно. Это означает +стандартный 2-ГБ лимит адресного пространства для 32-битного клиента. -### Внутриигровой магазин +## Структура -Файлы магазина находятся в [`patch-4/Interface/Store_UI`](./patch-4/Interface/Store_UI). +```text +modules/moonwell/ MoonWell runtime-модуль WarcraftXL +vendor/warcraftxl/ закреплённый upstream git submodule +vendor/modules/ закреплённые модули WarcraftXL +src/Data/ исходники MPQ-патчей +tool/ сборщик MPQ +build-warcraftxl.ps1 сборка Win32 runtime/proxy и x64 asset host +run.ps1 полная сборка, установка и запуск клиента +Wow_Original.exe локальный оригинал build 12340 (не хранится в Git) +``` -Примеры ресурсов: +Подключены модули `wxl-modern-assets`, `wxl-modern-render`, `wxl-unit-outline` и +`wxl-modern-adt`. Их точные ревизии вместе с ревизией ядра закреплены git submodule. +Для обновления всех зависимостей WarcraftXL: -- `Currencies/Gold.blp` -- `Currencies/Token.blp` -- `Frames/StoreFrame_Main.blp` +```powershell +git submodule update --remote vendor/warcraftxl vendor/modules/* +``` -### Экран входа и локализованные ресурсы +После обновления обязательно пересоберите и проверьте вход/создание персонажа: API WarcraftXL +пока развивается. -Файлы экрана входа и связанные ресурсы находятся в [`patch-ruRU-4`](./patch-ruRU-4). +## Требования -Основные каталоги: +- Visual Studio 2022 с C++ toolchain для Win32 и x64; +- CMake 3.20+; +- Rust/Cargo для существующего MPQ-сборщика; +- локальный оригинальный `Wow_Original.exe` с SHA-256 + `AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`. -- `Interface/GlueXML` - Lua/XML-исходники интерфейса загрузки и логина -- `Interface/Glues` - графика и модели для glue-экрана -- `Interface/LoginScreen` - фон и связанные изображения -- `Sound/Ambience/GlueScreen` - фоновые звуки -- `Sound/Music/GlueScreenMusic` - музыка экрана входа +После клонирования инициализируйте submodule: -### Русский перевод для `patch-ruRU-5` +```powershell +git submodule update --init --recursive +``` -Файлы русской локализации и расширенного glue-интерфейса находятся в [`patch-ruRU-5`](./patch-ruRU-5). +## Сборка WarcraftXL -Основные каталоги: +Быстрое полное развёртывание клиента одной командой: -- `Interface/GlueXML` - Lua/XML-исходники экранов логина, выбора реалма, выбора и создания персонажа -- `Interface/Glues` - текстуры, кнопки, логотипы и элементы интерфейса для `CharacterCreate` и `CharacterSelect` -- `Interface/GluesVideo` - видеоресурсы и ассеты glue-экранов -- `Interface/Loginscreen` - фон и изображения экрана входа -- `Interface/tooltips` - локализованные рамки и оформление тултипов -- `Interface/cinematics` - элементы интерфейса для роликов и заставок +```powershell +.\deploy.ps1 +``` -Этот пакет используется как слой русификации для дополнительных экранов клиента и их визуальных элементов. +Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, D3D9 proxy и x64 Host, +обновляет `dist`, устанавливает комплект в клиент и формирует `manifest.json`. Путь берётся из +`WOW_HOME`/`.env`, а при их отсутствии используется `C:\Program Files (x86)\World of Warcraft`. -### Mythic+ +Полезные варианты: -Файлы Mythic+ находятся в [`patch-Z`](./patch-Z). +```powershell +# Явный путь и запуск клиента после установки +.\deploy.ps1 -ClientPath 'D:\Games\World of Warcraft' -StopClient -Launch -Основные каталоги: +# Быстро обновить только WarcraftXL без пересборки MPQ +.\deploy.ps1 -SkipDataBuild -- `Interface/MythicPlus/textures` - текстуры интерфейса Mythic+ -- `Interface/MythicPlus/sounds` - звуки таймеров и событий -- `Interface/Icons` - дополнительные иконки -- `DBFielsClient` - клиентские DBC-данные, связанные с отображением предметов и интерфейсом +# Аварийный native D3D9 вместо D3D9On12 +.\deploy.ps1 -NativeRenderer +``` -## Как воспринимать содержимое репозитория +Без установки в клиент: -- Каталоги `patch-*` представляют содержимое соответствующих MPQ-патчей в распакованном виде. -- Названия каталогов соответствуют именам клиентских патч-пакетов, поверх которых вносятся наши изменения. -- Репозиторий не является полной копией клиента WoW, а только набором модифицированных клиентских ресурсов и исполняемого файла. +```powershell +.\build-warcraftxl.ps1 -Configuration Release +``` -## Для чего используется +С установкой: -Проект нужен для сопровождения клиентской части MoonWell: +```powershell +.\build-warcraftxl.ps1 -Configuration Release ` + -ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy +``` -- кастомизации GlueXML; -- поддержки русского перевода для `patch-ruRU-5`; -- поддержки внутриигрового магазина; -- добавления графики и данных для Mythic+; -- хранения пакетов графических улучшений; -- работы с модифицированным `Wow.exe` для WoW 3.3.5a. +При `-Deploy` скрипт: + +1. проверяет хеш `Wow_Original.exe`; +2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`; +3. восстанавливает оригинальный `Wow.exe`; +4. устанавливает `WarcraftXL.dll`, D3D9On12 proxy и `Utils\WarcraftXLHost.exe`; +5. монтирует шейдеры modern ADT как loose patch `Data\Patch-WXL.MPQ`; +6. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3. + +Основной proxy использует D3D9On12, необходимый `wxl-modern-render`. Эффекты постобработки в самом +модуле по умолчанию выключены. Для диагностики или несовместимой видеосистемы можно собрать и +установить native-переходник без modern-render: + +```powershell +.\build-warcraftxl.ps1 -Configuration Release -NativeRenderer ` + -ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy +``` + +Резервная DLL всегда также сохраняется как `Utils\d3d9-native.dll`. + +Полная сборка MPQ, установка WarcraftXL и запуск: + +```powershell +$env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft' +.\run.ps1 +``` + +Диагностика запуска находится в `Logs\wxl-core.log`, `Logs\d3d9proxy.log` и +`Utils\WarcraftXLHost.log` внутри клиента. + +## MPQ-пакеты + +Распакованные изменяемые слои уже находятся в `src/Data/`; полный набор базовых MPQ для сборки +WarcraftXL не требуется и в объектное хранилище не загружается. + +Основные пакеты: + +- `patch-4` — ресурсы внутриигрового магазина; +- `patch-ruRU-4` — экран входа и GlueXML; +- `patch-ruRU-5` — русская локализация и интерфейс; +- `patch-Z` — Mythic+ ресурсы и клиентские данные. + +## Лицензирование + +WarcraftXL распространяется по GPL-3.0 и подключён как отдельный upstream submodule. MoonWell-модуль, +скомпонованный в `WarcraftXL.dll`, должен распространяться с соблюдением GPL-3.0 и доступным +соответствующим исходным кодом. Репозиторий не должен публиковать Blizzard assets или `Wow.exe`. diff --git a/Wow.exe b/Wow.exe deleted file mode 100644 index fb862d7..0000000 Binary files a/Wow.exe and /dev/null differ diff --git a/build-warcraftxl.ps1 b/build-warcraftxl.ps1 new file mode 100644 index 0000000..4ad9486 --- /dev/null +++ b/build-warcraftxl.ps1 @@ -0,0 +1,106 @@ +[CmdletBinding()] +param( + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Release', + [string]$ClientPath = '', + [string]$PackagePath = '', + [switch]$Deploy, + [switch]$NativeRenderer +) + +$ErrorActionPreference = 'Stop' +$repoRoot = $PSScriptRoot +$cmake = 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe' +if (-not (Test-Path -LiteralPath $cmake)) { + $cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue + if (-not $cmakeCommand) { throw 'CMake 3.20+ was not found.' } + $cmake = $cmakeCommand.Source +} + +& git -C $repoRoot submodule update --init --recursive +if ($LASTEXITCODE -ne 0) { throw 'Failed to initialize WarcraftXL submodules.' } + +$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32' +$hostBuildDir = Join-Path $repoRoot 'build\warcraftxl-host-x64' +$stockExe = Join-Path $repoRoot 'Wow_Original.exe' + +if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) { + if (-not (Test-Path -LiteralPath $stockExe -PathType Leaf)) { + throw "Stock executable was not found: $stockExe" + } + $expectedStockHash = 'AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8' + $stockHash = (Get-FileHash -LiteralPath $stockExe -Algorithm SHA256).Hash + if ($stockHash -ne $expectedStockHash) { + throw "Wow_Original.exe is not the verified MoonWell 3.3.5a stock executable (SHA-256 $stockHash)." + } +} + +& $cmake -S $repoRoot -B $win32BuildDir -A Win32 '-DCLIENT_PATH=' +if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 configure failed.' } +& $cmake --build $win32BuildDir --config $Configuration --target WarcraftXL d3d9 MoonWellLoader +if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 build failed.' } + +& $cmake -S $repoRoot -B $hostBuildDir -A x64 '-DWXL_BUILD_HOST=ON' '-DCLIENT_PATH=' +if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host configure failed.' } +& $cmake --build $hostBuildDir --config $Configuration --target WarcraftXLHost +if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host build failed.' } + +$win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration" +$hostArtifactDir = Join-Path $hostBuildDir "vendor\warcraftxl\$Configuration" +$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll' +$modernProxy = Join-Path $win32ArtifactDir 'd3d9.dll' +$nativeProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll" +$hostExe = Join-Path $hostArtifactDir 'WarcraftXLHost.exe' +$selectedProxy = if ($NativeRenderer) { $nativeProxy } else { $modernProxy } +$adtResources = Join-Path $repoRoot 'vendor\modules\wxl-modern-adt\_resources' + +function Install-WarcraftXLArtifacts { + param([Parameter(Mandatory)][string]$Destination) + + $utils = Join-Path $Destination 'Utils' + $loosePatch = Join-Path $Destination 'Data\Patch-WXL.MPQ' + New-Item -ItemType Directory -Path $Destination, $utils, $loosePatch -Force | Out-Null + + Copy-Item -LiteralPath $stockExe -Destination (Join-Path $Destination 'Wow.exe') -Force + Copy-Item -LiteralPath $warcraftXL -Destination (Join-Path $Destination 'WarcraftXL.dll') -Force + Copy-Item -LiteralPath $selectedProxy -Destination (Join-Path $Destination 'd3d9.dll') -Force + Copy-Item -LiteralPath $nativeProxy -Destination (Join-Path $utils 'd3d9-native.dll') -Force + Copy-Item -LiteralPath $hostExe -Destination (Join-Path $utils 'WarcraftXLHost.exe') -Force + Get-ChildItem -LiteralPath $adtResources | ForEach-Object { + Copy-Item -LiteralPath $_.FullName -Destination $loosePatch -Recurse -Force + } +} + +if ($Deploy) { + if ([string]::IsNullOrWhiteSpace($ClientPath)) { + $ClientPath = 'C:\Program Files (x86)\World of Warcraft' + } + $ClientPath = [System.IO.Path]::GetFullPath($ClientPath) + if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) { + throw "Client directory was not found: $ClientPath" + } + + $clientExe = Join-Path $ClientPath 'Wow.exe' + if (Test-Path -LiteralPath $clientExe -PathType Leaf) { + $clientHash = (Get-FileHash -LiteralPath $clientExe -Algorithm SHA256).Hash + if ($clientHash -ne $stockHash) { + $legacyBackup = Join-Path $ClientPath 'Wow.moonwell-patched.backup.exe' + if (-not (Test-Path -LiteralPath $legacyBackup)) { + Copy-Item -LiteralPath $clientExe -Destination $legacyBackup + } + Write-Host "Legacy executable saved as $legacyBackup" + } + } + + Install-WarcraftXLArtifacts -Destination $ClientPath + Write-Host "Installed WarcraftXL runtime, host and modules in $ClientPath" +} + +if (-not [string]::IsNullOrWhiteSpace($PackagePath)) { + $PackagePath = [System.IO.Path]::GetFullPath($PackagePath) + Install-WarcraftXLArtifacts -Destination $PackagePath + Write-Host "Packaged stock Wow.exe and WarcraftXL artifacts in $PackagePath" +} + +$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12 proxy' } +Write-Host "WarcraftXL build complete ($renderer): $win32ArtifactDir" diff --git a/build_manifest.py b/build_manifest.py index 3b66ad2..c03be34 100644 --- a/build_manifest.py +++ b/build_manifest.py @@ -21,6 +21,10 @@ IGNORED_DIRS_ANYWHERE = { ".git", ".moonwell_launcher", } +IGNORED_TOP_LEVEL_FILES = { + "Wow.moonwell-patched.backup.exe", + "WarcraftXL_on12.disable", +} ENV_VAR_NAME = "WOW_HOME" @@ -42,6 +46,8 @@ def is_ignored(rel_path: Path) -> bool: return False if rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS: return True + if len(rel_path.parts) == 1 and rel_path.name in IGNORED_TOP_LEVEL_FILES: + return True return any(part in IGNORED_DIRS_ANYWHERE for part in rel_path.parts[:-1]) diff --git a/deploy.ps1 b/deploy.ps1 new file mode 100644 index 0000000..7e47f14 --- /dev/null +++ b/deploy.ps1 @@ -0,0 +1,135 @@ +[CmdletBinding()] +param( + [string]$ClientPath = '', + [string]$PackagePath = '', + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Release', + [switch]$SkipDataBuild, + [switch]$SkipManifest, + [switch]$NativeRenderer, + [switch]$StopClient, + [switch]$Launch +) + +$ErrorActionPreference = 'Stop' +$repoRoot = $PSScriptRoot + +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 + } + } +} + +Import-ProjectEnvironment + +if ([string]::IsNullOrWhiteSpace($ClientPath)) { + $ClientPath = if ($env:WOW_HOME) { + $env:WOW_HOME + } else { + 'C:\Program Files (x86)\World of Warcraft' + } +} +if ([string]::IsNullOrWhiteSpace($PackagePath)) { + $PackagePath = Join-Path $repoRoot 'dist' +} + +$ClientPath = [System.IO.Path]::GetFullPath($ClientPath) +$PackagePath = [System.IO.Path]::GetFullPath($PackagePath) +if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) { + throw "Client directory was not found: $ClientPath" +} + +$runningClients = @(Get-Process Wow -ErrorAction SilentlyContinue | Where-Object { + $_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase) +}) +if ($runningClients.Count) { + if (-not $StopClient) { + throw 'Wow.exe is running. Close the client or repeat deployment with -StopClient.' + } + + Write-Host 'Stopping the running WoW client...' + $runningClients | Stop-Process -Force + Start-Sleep -Seconds 2 + Get-Process WarcraftXLHost -ErrorAction SilentlyContinue | Where-Object { + $_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase) + } | Stop-Process -Force +} + +$env:WOW_HOME = $ClientPath +Write-Host "MoonWell client: $ClientPath" +Write-Host "Package staging: $PackagePath" + +Write-Host '[1/5] Initializing WarcraftXL dependencies...' +& git -C $repoRoot submodule update --init --recursive +if ($LASTEXITCODE -ne 0) { throw 'Git submodule initialization failed.' } + +New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null + +if (-not $SkipDataBuild) { + Write-Host '[2/5] Building MPQ patches...' + $tool = Join-Path $repoRoot 'tool\target\release\tool.exe' + if (-not (Test-Path -LiteralPath $tool -PathType Leaf)) { + $cargo = Get-Command cargo -ErrorAction SilentlyContinue + if (-not $cargo) { throw 'Cargo was not found and the MPQ builder is not compiled.' } + & $cargo.Source build --release --manifest-path (Join-Path $repoRoot 'tool\Cargo.toml') + if ($LASTEXITCODE -ne 0) { throw 'MPQ builder compilation failed.' } + } + + & $tool (Join-Path $repoRoot 'src') $PackagePath + if ($LASTEXITCODE -ne 0) { throw 'MPQ patch build failed.' } +} else { + Write-Host '[2/5] MPQ build skipped.' +} + +Write-Host '[3/5] Building and installing WarcraftXL...' +$buildArguments = @{ + Configuration = $Configuration + ClientPath = $ClientPath + PackagePath = $PackagePath + Deploy = $true +} +if ($NativeRenderer) { $buildArguments.NativeRenderer = $true } +& (Join-Path $repoRoot 'build-warcraftxl.ps1') @buildArguments +if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL deployment failed.' } + +Write-Host '[4/5] Synchronizing package files...' +& robocopy $PackagePath $ClientPath /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP +$robocopyExitCode = $LASTEXITCODE +if ($robocopyExitCode -ge 8) { + throw "Package synchronization failed with robocopy exit code $robocopyExitCode." +} + +if (-not $SkipManifest) { + Write-Host '[5/5] Building launcher manifest...' + $python = Get-Command python -ErrorAction SilentlyContinue + if (-not $python) { throw 'Python was not found; launcher manifest was not generated.' } + & $python.Source (Join-Path $repoRoot 'build_manifest.py') ` + --dir $PackagePath --output (Join-Path $repoRoot 'manifest.json') + if ($LASTEXITCODE -ne 0) { throw 'Launcher manifest generation failed.' } +} else { + Write-Host '[5/5] Manifest generation skipped.' +} + +$wowHash = (Get-FileHash -LiteralPath (Join-Path $ClientPath 'Wow.exe') -Algorithm SHA256).Hash +$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12' } +Write-Host '' +Write-Host 'Deployment complete.' +Write-Host " Renderer: $renderer" +Write-Host " Wow.exe SHA-256: $wowHash" +Write-Host " Host: $(Join-Path $ClientPath 'Utils\WarcraftXLHost.exe')" + +if ($Launch) { + Write-Host 'Launching Wow.exe...' + Start-Process -FilePath (Join-Path $ClientPath 'Wow.exe') -WorkingDirectory $ClientPath +} + +# robocopy uses successful non-zero exit codes; do not leak one to callers. +$global:LASTEXITCODE = 0 diff --git a/loader/d3d9.cpp b/loader/d3d9.cpp new file mode 100644 index 0000000..f29a1aa --- /dev/null +++ b/loader/d3d9.cpp @@ -0,0 +1,65 @@ +// Minimal D3D9 forwarding proxy for loading WarcraftXL without editing Wow.exe. + +#include +#include + +namespace +{ + using Create9Fn = IDirect3D9* (WINAPI*)(UINT); + using Create9ExFn = HRESULT (WINAPI*)(UINT, IDirect3D9Ex**); + + HMODULE g_systemD3D9 = nullptr; + Create9Fn g_create9 = nullptr; + Create9ExFn g_create9Ex = nullptr; + + void ResolveSystemD3D9() + { + if (g_systemD3D9) return; + char path[MAX_PATH] = {}; + const UINT length = GetSystemDirectoryA(path, MAX_PATH); + if (!length || length >= MAX_PATH - 10) return; + lstrcatA(path, "\\d3d9.dll"); + g_systemD3D9 = LoadLibraryA(path); + if (!g_systemD3D9) return; + g_create9 = reinterpret_cast(GetProcAddress(g_systemD3D9, "Direct3DCreate9")); + g_create9Ex = reinterpret_cast(GetProcAddress(g_systemD3D9, "Direct3DCreate9Ex")); + } + + void LoadWarcraftXL() + { + static bool attempted = false; + if (attempted) return; + attempted = true; + LoadLibraryA("WarcraftXL.dll"); + } +} + +extern "C" IDirect3D9* WINAPI Direct3DCreate9(UINT sdkVersion) +{ + ResolveSystemD3D9(); + LoadWarcraftXL(); + return g_create9 ? g_create9(sdkVersion) : nullptr; +} + +extern "C" HRESULT WINAPI Direct3DCreate9Ex(UINT sdkVersion, IDirect3D9Ex** output) +{ + ResolveSystemD3D9(); + LoadWarcraftXL(); + if (g_create9Ex) return g_create9Ex(sdkVersion, output); + if (output) *output = nullptr; + return E_NOINTERFACE; +} + +// Compatibility exports referenced by the current WarcraftXL core. MoonWell +// does not enable its D3D9On12 backend, so these intentionally report no D3D12 +// device and a neutral supersampling factor. +extern "C" void* WxlD3D12Device() { return nullptr; } +extern "C" void* WxlD3D12Queue() { return nullptr; } +extern "C" void WxlD3D12DrainDebug() {} +extern "C" void WxlSetSsaaFactor(float) {} +extern "C" float WxlGetSsaaFactor() { return 1.0f; } + +BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID) +{ + return TRUE; +} diff --git a/loader/d3d9.def b/loader/d3d9.def new file mode 100644 index 0000000..192ee5f --- /dev/null +++ b/loader/d3d9.def @@ -0,0 +1,9 @@ +LIBRARY d3d9 +EXPORTS + Direct3DCreate9 + Direct3DCreate9Ex + WxlD3D12Device + WxlD3D12Queue + WxlD3D12DrainDebug + WxlSetSsaaFactor + WxlGetSsaaFactor diff --git a/make_modes_in_exe.py b/make_modes_in_exe.py deleted file mode 100644 index fdbf1a6..0000000 --- a/make_modes_in_exe.py +++ /dev/null @@ -1,339 +0,0 @@ -""" -Wow.exe binary patcher for MoonWell custom features. - -Patches: - 1. Script_CreateCharacter: reads lua_tonumber(L, 2) as outfitId, - stores to global, passes to CMSG_CHAR_CREATE packet instead of 0. - 2. GetCharacterInfo: adds 11th return value — charFlags as a Lua number, - read from struct_offset +0x170 (same field as ghost flag 0x2000). - Lua checks bit 0x40000000 (CHARACTER_FLAG_UNK31) for traitor status. - -Usage: - python make_modes_in_exe.py - -A backup (Wow.exe.bak) is created before patching. -""" - -import struct -import sys -import shutil -from pathlib import Path - - -# --------------------------------------------------------------------------- -# PE helpers -# --------------------------------------------------------------------------- - -def _u16(data, off): - return struct.unpack_from("= size consecutive CC bytes after search_from_va.""" - text = next((s for s in sections if s[0] == ".text"), None) - if not text is None: - _, va0, vs, ro, rs = text - else: - raise ValueError("No .text section") - - rva_start = max(search_from_va - image_base, va0) - off_start = ro + (rva_start - va0) - off_end = ro + min(vs, rs) - - i = off_start - while i <= off_end - size: - if data[i] != 0xcc: - i += 1 - continue - j = i - while j < off_end and data[j] == 0xcc: - j += 1 - if j - i >= size: - return offset_to_va(i, sections, image_base) - i = j - raise ValueError(f"No {size}-byte code cave found from VA 0x{search_from_va:08x}") - - -# --------------------------------------------------------------------------- -# Patch 1 & 2: outfitId in CMSG_CHAR_CREATE (fixed addresses) -# --------------------------------------------------------------------------- - -_FIXED_PATCHES = [ - ( - 0x004e0c60, - bytes.fromhex( - "558bec568b75086a0156e8f1d23600" - "83c40885c0740d6a006a0156e860d4" - "360083c40c6a006a0156e853d43600" - "50e8edf6ffff83c41033c05e5dc3cc" - "cccccccc" - ), - bytes.fromhex( - "558bec568b75086a0256e8c1d33600" - "db1c245883c404a221 42ac006a006a" - "0156e85bd4360050e8f5f6ffff83c4" - "1033c05e5dc38855f08a1521 42ac00" - "8855f4c3" - ).replace(b" ", b""), # spaces stripped for readability - "Script_CreateCharacter: read outfitId from Lua arg 2 -> global", - ), - ( - 0x004e042f, - bytes.fromhex("8855f0c645f400"), - bytes.fromhex("e85f08000090 90").replace(b" ", b""), - "Packet builder: CALL helper -> load face + outfitId from global", - ), -] - -# --------------------------------------------------------------------------- -# Patch 3: GetCharacterInfo — add 11th return value (charFlags as number) -# --------------------------------------------------------------------------- -# -# Assembly context (FUN_004e3170 epilogue, valid branch): -# -# 004e31cd 56 PUSH ESI ← saves caller's ESI -# 004e31ce 8b f0 MOV ESI, EAX ← ESI = char struct ptr -# ... [function body — ESI used throughout for char struct] ... -# 004e32e5 8b 8e 70010000 MOV ECX, [ESI+0x170] ← charFlags (ghost flag check) -# ... -# 004e332d 83 c4 2c ADD ESP, 0x2c ← clean accumulated args ← PATCH HERE -# 004e3330 5e POP ESI ← restores caller's ESI (ESI valid until here) -# 004e3331 b8 0a 00 00 00 MOV EAX, 0xa (dead after patch) -# 004e3336 5f POP EDI ← JMP back target -# 004e3337 8b e5 MOV ESP, EBP -# 004e3339 5d POP EBP -# 004e333a c3 RET -# -# KEY: ESI = char struct ptr up to and including ADD ESP,0x2c (004e332d). -# After POP ESI (004e3330) it is caller's ESI — NOT char struct. -# Previous approach: CALL cave at 004e3331 (after POP ESI) -> ESI already wrong. -# Fix: JMP cave at 004e332d (before POP ESI) -> ESI still valid in cave. -# -# New approach: patch 5 bytes at 004e332d with JMP cave. -# Old bytes: 83 c4 2c (ADD ESP,0x2c) + 5e (POP ESI) + b8 (MOV EAX first byte) -# New bytes: e9 XX XX XX XX JMP cave -# The 4 dead bytes at 004e3332-004e3335 are unreachable. -# -# Cave (30 bytes, found automatically via CC scan over entire .text): -# 83 ec 08 SUB ESP, 8 ; make room for double -# db 86 70 01 00 00 FILD [ESI+0x170] ; load charFlags into FPU (ESI valid here!) -# dd 1c 24 FSTP [ESP] ; store as double -# 57 PUSH EDI ; lua_State* L -# e8 XX XX XX XX CALL FUN_0084e2a0 ; lua_pushnumber(L, charFlags) -# 83 c4 38 ADD ESP, 0x38 ; merged: own 0xc + stolen 0x2c -# 5e POP ESI ; stolen: restore caller's ESI -# 6a 0b PUSH 0xb ; \ EAX = 11 in 3 bytes -# 58 POP EAX ; / (vs MOV EAX,0xb = 5 bytes) -# e9 XX XX XX XX JMP 004e3336 ; continue epilogue (POP EDI ...) -# -# Lua code reads info[11] and checks bit 0x40000000 (CHARACTER_FLAG_UNK31 = traitor). -# charFlags confirmed at [ESI+0x170]: same field used for ghost flag (AND ECX,0x2000). - -_CHARINFO_PATCH_VA = 0x004e332d # ADD ESP,0x2c (5 bytes including POP ESI + MOV EAX[0]) -_CHARINFO_JMPBACK_VA = 0x004e3336 # POP EDI — resume epilogue after cave -_CHARINFO_CAVE_SIZE = 30 -_CHARINFO_CAVE_SEARCH_FROM = 0x00401000 # search entire .text for first 35-byte CC block -_LUA_PUSHNUMBER_VA = 0x0084e2a0 # lua_pushnumber - - -def _build_charinfo_patches(data, sections, image_base): - """ - Build the dynamic GetCharacterInfo patches. - Returns list of (va, old_bytes, new_bytes, description), [] if already applied, - or None on unrecoverable error. - """ - patch_off = va_to_offset(_CHARINFO_PATCH_VA, sections, image_base) - current = bytes(data[patch_off:patch_off + 5]) - - # Already patched with new JMP approach? - if current[0:1] == b"\xe9": - return [] - - # Verify first 4 bytes look like the epilogue (ADD ESP,0x2c + POP ESI). - # Accept both unpatched (b8 at [4]) and old-CALL-patched (e8 at [4]) states. - if current[0:4] != bytes.fromhex("83c42c5e"): - print(f"[WARN] 0x{_CHARINFO_PATCH_VA:08x}: unexpected bytes {current.hex(' ')}") - print(" Expected ADD ESP,0x2c + POP ESI at this address.") - print(" Restore Wow.exe.bak and re-run.") - return None - - old_b = current # exact 5 bytes currently in file (b8 or e8 at index 4) - - # Find a 35-byte code cave in .text - cave_va = find_cave(data, sections, image_base, - _CHARINFO_CAVE_SIZE, _CHARINFO_CAVE_SEARCH_FROM) - - # Cave layout offsets (30 bytes total): - # 0 SUB ESP,8 (3) - # 3 FILD [ESI+0x170] (6) - # 9 FSTP [ESP] (3) - # 12 PUSH EDI (1) - # 13 CALL lua_pushnumber (5) — next instr at 18 - # 18 ADD ESP, 0x38 (3) merged: own 0xc + stolen 0x2c - # 21 POP ESI (1) stolen - # 22 PUSH 0xb (2) \ shorter than MOV EAX,0xb (5 bytes) - # 24 POP EAX (1) / - # 25 JMP 004e3336 (5) — next instr at cave_va+30 - call_rel = _LUA_PUSHNUMBER_VA - (cave_va + 18) - jmp_back_rel = _CHARINFO_JMPBACK_VA - (cave_va + 30) - - cave_bytes = ( - bytes.fromhex("83ec08") # SUB ESP, 8 (3) - + bytes.fromhex("db8670010000") # FILD [ESI+0x170] (6) - + bytes.fromhex("dd1c24") # FSTP [ESP] (3) - + bytes.fromhex("57") # PUSH EDI (1) - + b"\xe8" + struct.pack(" EAX=11 (1) - + b"\xe9" + struct.pack("") - sys.exit(1) - - exe_path = Path(sys.argv[1]) - if not exe_path.exists(): - print(f"Error: file not found: {exe_path}") - sys.exit(1) - - data = bytearray(exe_path.read_bytes()) - image_base, sections = parse_pe_sections(data) - - print(f"Image base: 0x{image_base:08x}") - print(f"Sections: {len(sections)}") - for n, va, vs, ro, rs in sections: - print(f" {n:8s} VA=0x{va:08x} VSize=0x{vs:08x} Raw=0x{ro:08x} RSize=0x{rs:08x}") - print() - - charinfo_patches = _build_charinfo_patches(data, sections, image_base) - if charinfo_patches is None: - sys.exit(1) - all_patches = list(_FIXED_PATCHES) + charinfo_patches - - # Verify all first - ok = True - for va, old_b, new_b, desc in all_patches: - off = va_to_offset(va, sections, image_base) - cur = bytes(data[off:off + len(old_b)]) - if cur == new_b: - print(f"[SKIP] 0x{va:08x}: already patched — {desc}") - elif cur == old_b: - print(f"[ OK ] 0x{va:08x}: verified — {desc}") - else: - print(f"[FAIL] 0x{va:08x}: {desc}") - print(f" Expected: {old_b.hex(' ')}") - print(f" Found: {cur.hex(' ')}") - ok = False - - if not ok: - print("\nByte mismatch — aborting. Is this the correct Wow.exe (3.3.5a build 12340)?") - sys.exit(1) - - # Backup - bak = exe_path.with_suffix(".exe.bak") - if not bak.exists(): - shutil.copy2(exe_path, bak) - print(f"\nBackup: {bak}") - else: - print(f"\nBackup already exists: {bak}") - - # Apply - applied = 0 - for va, old_b, new_b, desc in all_patches: - off = va_to_offset(va, sections, image_base) - cur = bytes(data[off:off + len(old_b)]) - if cur == new_b: - continue - data[off:off + len(old_b)] = new_b - applied += 1 - print(f"[PATCH] 0x{va:08x}: {desc}") - - if applied == 0: - print("\nNothing to patch.") - else: - exe_path.write_bytes(data) - print(f"\n{applied} patch(es) written to {exe_path}") - - print("Done!") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/modules/moonwell/src/MoonWell.cpp b/modules/moonwell/src/MoonWell.cpp new file mode 100644 index 0000000..e7857aa --- /dev/null +++ b/modules/moonwell/src/MoonWell.cpp @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// MoonWell compatibility module for WarcraftXL. +// +// The stock 3.3.5a executable stays untouched on disk. These narrowly scoped +// changes are verified and applied to the process image during WarcraftXL's +// boot phase, before GlueXML is loaded. + +#include "core/Logger.hpp" +#include "core/Mem.hpp" +#include "runtime/ModuleInstall.hpp" + +#include + +#include +#include +#include +#include + +namespace moonwell +{ + namespace + { + struct Patch + { + const char* name; + uintptr_t address; + const uint8_t* expected; + const uint8_t* replacement; + size_t size; + }; + + template + bool Apply(const char* name, uintptr_t address, + const std::array& expected, + const std::array& replacement) + { + const auto* current = reinterpret_cast(address); + if (std::memcmp(current, replacement.data(), N) == 0) + return true; + if (std::memcmp(current, expected.data(), N) != 0) + { + WLOG_ERROR("moonwell: '%s' byte mismatch at %p; stock build 12340 required", + name, reinterpret_cast(address)); + return false; + } + if (!wxl::core::mem::Patch(reinterpret_cast(address), replacement.data(), N)) + { + WLOG_ERROR("moonwell: '%s' patch failed at %p", name, + reinterpret_cast(address)); + return false; + } + WLOG_INFO("moonwell: applied '%s' at %p", name, reinterpret_cast(address)); + return true; + } + + bool InstallGlueUnlock() + { + // Same stock-client checks formerly changed in the distributed Wow.exe. + // They allow custom GlueXML while keeping WarcraftXL's callback validator intact. + bool ok = true; + ok &= Apply("glue archive override", 0x005F4DBF, + std::array{0x74}, std::array{0xEB}); + ok &= Apply("glue callback gate", 0x00816625, + std::array{0x75}, std::array{0xEB}); + ok &= Apply("glue callback result A", 0x0081663F, + std::array{0x01}, std::array{0x03}); + ok &= Apply("glue callback result B", 0x00816695, + std::array{0x01}, std::array{0x03}); + ok &= Apply("glue callback compare", 0x00816746, + std::array{0x7F}, std::array{0xEB}); + ok &= Apply("glue callback return", 0x0081675F, + std::array{0x83,0xC0,0x03,0x5E,0x8B,0xE5,0x5D}, + std::array{0xB8,0x03,0x00,0x00,0x00,0xEB,0xED}); + return ok; + } + + bool InstallCharacterModePacket() + { + // CreateCharacter(name, modeId): capture Lua argument 2 and place it in + // the final CMSG_CHAR_CREATE byte that is zero in the stock client. + constexpr std::array expectedCreate = { + 0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x01,0x56,0xE8,0xF1,0xD2,0x36,0x00, + 0x83,0xC4,0x08,0x85,0xC0,0x74,0x0D,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x60,0xD4, + 0x36,0x00,0x83,0xC4,0x0C,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x53,0xD4,0x36,0x00, + 0x50,0xE8,0xED,0xF6,0xFF,0xFF,0x83,0xC4,0x10,0x33,0xC0,0x5E,0x5D,0xC3, + 0xCC,0xCC,0xCC,0xCC,0xCC + }; + constexpr std::array replacementCreate = { + 0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x02,0x56,0xE8,0xC1,0xD3,0x36,0x00, + 0xDB,0x1C,0x24,0x58,0x83,0xC4,0x04,0xA2,0x21,0x42,0xAC,0x00,0x6A,0x00,0x6A, + 0x01,0x56,0xE8,0x5B,0xD4,0x36,0x00,0x50,0xE8,0xF5,0xF6,0xFF,0xFF,0x83,0xC4, + 0x10,0x33,0xC0,0x5E,0x5D,0xC3,0x88,0x55,0xF0,0x8A,0x15,0x21,0x42,0xAC, + 0x00,0x88,0x55,0xF4,0xC3 + }; + constexpr std::array expectedPacket = + {0x88,0x55,0xF0,0xC6,0x45,0xF4,0x00}; + constexpr std::array replacementPacket = + {0xE8,0x5F,0x08,0x00,0x00,0x90,0x90}; + + return Apply("CreateCharacter mode argument", 0x004E0C60, + expectedCreate, replacementCreate) + && Apply("CMSG_CHAR_CREATE mode byte", 0x004E042F, + expectedPacket, replacementPacket); + } + + bool InstallCharacterFlags() + { + // GetCharacterInfo(index) gains return value 11: the character flags + // from CharacterInfo+0x170. The glue code uses bit 0x40000000. + constexpr uintptr_t patchAddress = 0x004E332D; + constexpr uintptr_t returnAddress = 0x004E3336; + constexpr uintptr_t luaPushNumber = 0x0084E2A0; + constexpr std::array expected = {0x83,0xC4,0x2C,0x5E,0xB8}; + + if (*reinterpret_cast(patchAddress) == 0xE9) + return true; + if (std::memcmp(reinterpret_cast(patchAddress), expected.data(), expected.size()) != 0) + { + WLOG_ERROR("moonwell: GetCharacterInfo byte mismatch at %p", + reinterpret_cast(patchAddress)); + return false; + } + + auto* cave = static_cast(VirtualAlloc(nullptr, 30, MEM_COMMIT | MEM_RESERVE, + PAGE_EXECUTE_READWRITE)); + if (!cave) + { + WLOG_ERROR("moonwell: cannot allocate GetCharacterInfo thunk (win32=%lu)", GetLastError()); + return false; + } + + const uint8_t prefix[] = { + 0x83,0xEC,0x08, // sub esp, 8 + 0xDB,0x86,0x70,0x01,0x00,0x00, // fild dword ptr [esi+170h] + 0xDD,0x1C,0x24, // fstp qword ptr [esp] + 0x57, // push edi (lua_State*) + 0xE8,0,0,0,0, // call lua_pushnumber + 0x83,0xC4,0x38, // cleanup own args + stolen add esp,2Ch + 0x5E, // stolen pop esi + 0x6A,0x0B,0x58, // eax = 11 + 0xE9,0,0,0,0 // jump to stock epilogue + }; + static_assert(sizeof(prefix) == 30); + std::memcpy(cave, prefix, sizeof(prefix)); + + const auto callRel = static_cast(luaPushNumber - + (reinterpret_cast(cave) + 18)); + const auto backRel = static_cast(returnAddress - + (reinterpret_cast(cave) + 30)); + std::memcpy(cave + 14, &callRel, sizeof(callRel)); + std::memcpy(cave + 26, &backRel, sizeof(backRel)); + FlushInstructionCache(GetCurrentProcess(), cave, 30); + + std::array jump{0xE9,0,0,0,0}; + const auto caveRel = static_cast(reinterpret_cast(cave) - + (patchAddress + jump.size())); + std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel)); + if (!wxl::core::mem::Patch(reinterpret_cast(patchAddress), jump.data(), jump.size())) + { + VirtualFree(cave, 0, MEM_RELEASE); + WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed"); + return false; + } + WLOG_INFO("moonwell: GetCharacterInfo now returns charFlags as value 11"); + return true; + } + + void InstallBoot() + { + const bool glue = InstallGlueUnlock(); + const bool create = InstallCharacterModePacket(); + const bool flags = InstallCharacterFlags(); + if (!glue || !create || !flags) + WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above"); + else + WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)"); + wxl::core::log::Flush(); + } + + DWORD WINAPI FlushRuntimeLog(LPVOID) + { + // Let the remainder of RunAll() and the core-ready line reach the + // shared buffered logger, then make startup diagnostics durable. + Sleep(1000); + wxl::core::log::Flush(); + return 0; + } + + void InstallRuntimeLogFlush() + { + HANDLE thread = CreateThread(nullptr, 0, &FlushRuntimeLog, nullptr, 0, nullptr); + if (thread) CloseHandle(thread); + } + + struct Registration + { + Registration() + { + wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot); + wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush); + } + } g_registration; + } +} diff --git a/run.ps1 b/run.ps1 index e139258..e645090 100644 --- a/run.ps1 +++ b/run.ps1 @@ -36,6 +36,7 @@ $SRC_DIR = Join-Path $ROOT "src" $DIST_DIR = Join-Path $ROOT "dist" $TOOL = Join-Path $ROOT "tool\target\release\tool.exe" $RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat" +$WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1" # --- Ensure tool exists if (!(Test-Path $TOOL)) { @@ -59,6 +60,16 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) { exit 1 } +# --- Build/package/deploy WarcraftXL before the dist sync. The packaged files +# are uploaded by upload_to_s3.py and consumed by the launcher manifest. +Write-Host "Building and packaging WarcraftXL..." +& $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME ` + -PackagePath $DIST_DIR -Deploy +if ($LASTEXITCODE -ne 0) { + Write-Error "WarcraftXL build/deploy failed!" + exit 1 +} + # --- Sync dist/ -> WOW_HOME Write-Host "Syncing dist/ -> WOW_HOME..." robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL diff --git a/vendor/modules/wxl-modern-adt b/vendor/modules/wxl-modern-adt new file mode 160000 index 0000000..0412b69 --- /dev/null +++ b/vendor/modules/wxl-modern-adt @@ -0,0 +1 @@ +Subproject commit 0412b690c04e0efc92b0078989dd9dbd182f131a diff --git a/vendor/modules/wxl-modern-assets b/vendor/modules/wxl-modern-assets new file mode 160000 index 0000000..eb7c7c6 --- /dev/null +++ b/vendor/modules/wxl-modern-assets @@ -0,0 +1 @@ +Subproject commit eb7c7c66461bda2941c549943e6a8884a006f436 diff --git a/vendor/modules/wxl-modern-render b/vendor/modules/wxl-modern-render new file mode 160000 index 0000000..3f246a5 --- /dev/null +++ b/vendor/modules/wxl-modern-render @@ -0,0 +1 @@ +Subproject commit 3f246a5e207a3c9b805c4d88d64076c8ef1f3504 diff --git a/vendor/modules/wxl-unit-outline b/vendor/modules/wxl-unit-outline new file mode 160000 index 0000000..29e5e29 --- /dev/null +++ b/vendor/modules/wxl-unit-outline @@ -0,0 +1 @@ +Subproject commit 29e5e29fa97e0ef356ebc9ae865d5db6f6841a4e diff --git a/vendor/warcraftxl b/vendor/warcraftxl new file mode 160000 index 0000000..9d98112 --- /dev/null +++ b/vendor/warcraftxl @@ -0,0 +1 @@ +Subproject commit 9d981125e1ec8cf880ee65ad04c19fe3f925905e