diff --git a/Docs/BINARY_PROVENANCE.md b/Docs/BINARY_PROVENANCE.md index 45dd921..3c9131d 100644 --- a/Docs/BINARY_PROVENANCE.md +++ b/Docs/BINARY_PROVENANCE.md @@ -21,10 +21,10 @@ No remaining payload binary was modified during this provenance pass. | ClassicAPI | bundled fallback + online update | brues-code/ClassicAPI | GPL v3 verified | | AuctionQueryThrottle | bundled fallback + online update | brues-code/AuctionQueryThrottle | GPL v3 verified | | SuperWoW | **upstream-only** | balakethelock/SuperWoW release | redistribution restricted upstream | -| SuperAPI | **upstream-only** | balakethelock/SuperAPI master | no bundled copy after this audit | +| SuperAPI | **upstream-only** | balakethelock/SuperAPI master | no bundled copy; current master revision is resolved before download | | UnitXP_SP3 | bundled fallback + online update | brues-code/UnitXP_SP3 v90 | project-wide license not independently located | | Interact | bundled fallback + online update | lookino/Interact v1.0.4 | project-wide license not independently located | -| No1600x1200 | bundled fallback + online update | RetroCro/TurtleWoW-Mods | project-wide license not independently located | +| No1600x1200 | **bundled known-good only** | RetroCro/TurtleWoW-Mods archive source | project-wide license not independently located | | PerfBoost | bundled | RetroCro/TurtleWoW-Mods backup source | project-wide license not independently located | | WeirdPerformance | bundled | Dusk92/WeirdUtils 0.7.3 | provenance recorded; license record pending | | WeirdUtils modules | bundled | MarcelineVQ/WeirdUtils releases | provenance recorded; license record pending | @@ -46,8 +46,8 @@ That bundled fallback is removed by this audit because the current upstream license restricts redistribution without express written permission. The installation feature itself is retained: `remote_packages.install_superwow` -already downloads the official upstream release and downloads SuperAPI directly -from its upstream repository. +downloads the official SuperWoW release and the current SuperAPI master +revision directly from its upstream repository. ## SuperAPI change diff --git a/README.md b/README.md index 44790f3..0c568b8 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,6 @@ Download the latest version from: **[GitHub Releases](https://github.com/Dusk-92/Modernization-Tool/releases/latest)** -Current major release: **v2.2** - No installation is required. Download and run: `WoW_Modernization_Tool.exe` @@ -46,60 +44,58 @@ The launcher shortcut is automatically created: Two rendering modes are available: -### VanillaFixes — DirectX 9 +### [VanillaFixes](https://github.com/hannesmann/vanillafixes) — DirectX 9 The default option. Uses WoW's native DirectX 9 renderer together with VanillaFixes. -### VanillaFixes + DXVK — Vulkan +### VanillaFixes + [DXVK](https://github.com/doitsujin/dxvk) — Vulkan Uses DXVK to translate DirectX 9 to Vulkan. -The bundled version is currently: - -**DXVK 2.6.1 x86** +DXVK is bundled with the tool and can be selected directly from the renderer options. Existing custom DirectX wrapper files are backed up rather than blindly deleted when switching renderers. --- -# 🧩 Client Plugins +# 🧩 Client Plugins & Sources ## Recommended Core The tool can install and configure several recommended client extensions: -- **Nampower** — improves spell responsiveness and latency handling. -- **UnitXP_SP3** — adds networking, targeting, Lua and client improvements. -- **SuperWoW** — expands the Vanilla Lua API and improves addon compatibility. -- **TransmogFix** — prevents performance drops caused by rapid appearance updates. -- **PerfBoost** — provides additional unit rendering/performance controls. -- **WeirdPerformance** — lightweight client optimizations aimed at improving FPS and reducing UI memory leaks for smoother gameplay. -- **VanillaHelpers** — extends several Vanilla client limits. -- **ClassicAPI** — adds newer WoW API functions for compatible addons. -- **AuctionQueryThrottle** — removes the fixed delay between Auction House queries. +- **[Nampower](https://github.com/brues-code/nampower)** — improves spell responsiveness and latency handling. Settings addon: [NampowerSettings](https://github.com/brues-code/NampowerSettings). +- **[UnitXP_SP3](https://github.com/brues-code/UnitXP_SP3)** — adds networking, targeting, Lua and client improvements. +- **[SuperWoW](https://github.com/balakethelock/SuperWoW)** — expands the Vanilla Lua API and improves addon compatibility. Its companion API addon is [SuperAPI](https://github.com/balakethelock/SuperAPI). +- **[TransmogFix](https://codeberg.org/MarcelineVQ/WeirdUtils)** — prevents performance drops caused by rapid appearance updates. +- **[PerfBoost](https://github.com/RetroCro/TurtleWoW-Mods)** — provides additional unit rendering/performance controls. Configuration addon: [PerfBoost Settings](https://gitea.com/avitasia/PerfBoostSettings). +- **[WeirdPerformance](https://codeberg.org/Dusk92/WeirdUtils)** — lightweight client optimizations aimed at improving FPS and reducing UI memory leaks for smoother gameplay. +- **[VanillaHelpers](https://github.com/isfir/VanillaHelpers)** — extends several Vanilla client limits. +- **[ClassicAPI](https://github.com/brues-code/ClassicAPI)** — adds newer WoW API functions for compatible addons. +- **[AuctionQueryThrottle](https://github.com/brues-code/AuctionQueryThrottle)** — removes the fixed delay between Auction House queries. ## Optional Plugins Optional client-side improvements include: -- **VanillaMultiMonitorFix** — improved resolution, refresh-rate and monitor detection. -- **Interact** — adds a modern Interact key for NPCs, objects, gathering nodes and loot. -- **No1600x1200** — fixes the old Vanilla resolution limitation on some systems. -- **BigCursor** — upscales the hardware cursor for better visibility on modern high-resolution displays without sacrificing sharpness. -- **CustomAssets** — allows loose game files to be loaded directly from the `Data/` folder and supports custom multi-character patch names without repacking MPQ archives. -- **LogSessions** — automatically organizes combat and chat logs into clean per-character, per-day files when you log in. -- **MinimapIcons** — adds TBC/WotLK-style minimap tracking icons for NPCs and game objects, with a combined tracking menu and saved preferences. -- **PNG Screenshots** — saves screenshots as compressed PNG files instead of the default uncompressed TGA format, using background processing to minimize frame-time impact. -- **WorldMarkers** — lets party or raid leaders place up to five animated, Cataclysm-style colored world markers for positioning and tactical planning. -- **Discord Rich Presence** — installs and keeps **WowPresence** up to date from [Dusk-92/WowPresence](https://github.com/Dusk-92/WowPresence). The Tool preconfigures the OctoWoW Discord Application ID and lets you choose which character details are shown on Discord, including **Name, Guild, Race, Faction, Class, Level and Zone**. Custom Discord Application IDs are preserved across updates. +- **[VanillaMultiMonitorFix](https://github.com/Mates1500/VanillaMultiMonitorFix)** — improved resolution, refresh-rate and monitor detection. +- **[Interact](https://github.com/lookino/Interact)** — adds a modern Interact key for NPCs, objects, gathering nodes and loot. +- **[No1600x1200](https://github.com/RetroCro/TurtleWoW-Mods#no1600x1200)** — fixes the old Vanilla resolution limitation on some systems. +- **[BigCursor](https://codeberg.org/MarcelineVQ/WeirdUtils)** — upscales the hardware cursor for better visibility on modern high-resolution displays without sacrificing sharpness. +- **[CustomAssets](https://codeberg.org/MarcelineVQ/WeirdUtils)** — allows loose game files to be loaded directly from the `Data/` folder and supports custom multi-character patch names without repacking MPQ archives. +- **[LogSessions](https://codeberg.org/MarcelineVQ/WeirdUtils)** — automatically organizes combat and chat logs into clean per-character, per-day files when you log in. +- **[MinimapIcons](https://codeberg.org/MarcelineVQ/WeirdUtils)** — adds TBC/WotLK-style minimap tracking icons for NPCs and game objects, with a combined tracking menu and saved preferences. +- **[PNG Screenshots](https://codeberg.org/MarcelineVQ/WeirdUtils)** — saves screenshots as compressed PNG files instead of the default uncompressed TGA format, using background processing to minimize frame-time impact. +- **[WorldMarkers](https://codeberg.org/MarcelineVQ/WeirdUtils)** — lets party or raid leaders place up to five animated, Cataclysm-style colored world markers for positioning and tactical planning. +- **[Discord Rich Presence](https://github.com/Dusk-92/WowPresence)** — installs and keeps **WowPresence** up to date. The Tool preconfigures the OctoWoW Discord Application ID and lets you choose which character details are shown on Discord, including **Name, Guild, Race, Faction, Class, Level and Zone**. Custom Discord Application IDs are preserved across updates. --- # ⚙️ Vanilla Tweaks -Modernization Tool integrates the modern **tubtubs/vanilla-tweaks** patcher. +Modernization Tool integrates the modern **[tubtubs/vanilla-tweaks](https://github.com/tubtubs/vanilla-tweaks)** patcher. Available settings include: @@ -117,7 +113,7 @@ Available settings include: - Unlimited AddOn Script Memory - Cross-Faction Resurrection Fix - Custom Glues Patch -- Automatic WDB management +- [Automatic WDB management](https://github.com/RetroCro/TurtleWoW-Mods#automatically-clear-wdb-folder-every-time-you-launch-turtle-wow) When **SuperWoW** already provides a feature, the equivalent vanilla-tweaks patch is automatically skipped to avoid duplicate modifications. @@ -127,12 +123,12 @@ When **SuperWoW** already provides a feature, the equivalent vanilla-tweaks patc Optional visual modifications are available directly from the tool: -- **Bluemoon Patch** — restores the rare blue moon effect. -- **Darker Nights** — darker and more atmospheric nights. -- **Pretty Night Sky** — improved starry night sky. -- **Epoch Water** — alternative water textures. -- **Fog Pushback** — moves environmental fog farther away. -- **Pink Herbs** — makes herb nodes much easier to spot. +- **[Bluemoon Patch](https://github.com/tubtubs/vanilla-tweaks)** — restores the rare blue moon effect. +- **[Darker Nights](https://projectreforged.github.io/vanilla/downloads/turtle/)** — darker and more atmospheric nights. +- **[Pretty Night Sky](https://github.com/RetroCro/TurtleWoW-Mods)** — improved starry night sky. +- **[Epoch Water](https://github.com/RetroCro/TurtleWoW-Mods)** — alternative water textures. +- **[Fog Pushback](https://github.com/RetroCro/TurtleWoW-Mods)** — moves environmental fog farther away. +- **[Pink Herbs](https://github.com/seacrabsam/patch-herb)** — makes herb nodes much easier to spot. All visual mods are **disabled by default**. @@ -142,9 +138,9 @@ All visual mods are **disabled by default**. Optional sound replacements include: -- **NoErrorSounds** — removes repetitive error/fizzle and interface sounds. -- **FishPing** — replaces the fishing bite sound with a clearer ping. -- **Warlock Muted Demons** — mutes repetitive Warlock demon voice lines. +- **[NoErrorSounds](https://github.com/Macumbafeh/NoErrorSounds)** — removes repetitive error/fizzle and interface sounds. +- **[FishPing](https://github.com/notsureawake/FishPing)** — replaces the fishing bite sound with a clearer ping. +- **[Warlock Muted Demons](https://github.com/spzilyk/Warlock-Muted-Demons)** — mutes repetitive Warlock demon voice lines. All audio mods are **disabled by default**. @@ -162,11 +158,15 @@ Depending on the component, the tool can: - use a bundled known-good fallback where redistribution terms allow it; - avoid re-downloading files that are already current. -**SuperWoW and SuperAPI are upstream-only:** they are downloaded directly from -their official upstream projects when enabled and are not bundled as offline -fallbacks. A first-time SuperWoW installation therefore requires network access. +**SuperWoW and SuperAPI are upstream-only:** SuperWoW is downloaded from its +official stable release and SuperAPI follows the current upstream `master` +revision. They are not bundled as offline fallbacks, so a first-time SuperWoW +installation requires network access. -Downloaded DLLs are also validated before installation. +**No1600x1200 uses the bundled known-good copy** instead of following the +RetroCro archive repository automatically. + +Downloaded and bundled DLLs are validated before installation. --- @@ -188,7 +188,7 @@ It also: # 🔐 AutoLogin -AutoLogin can optionally save account and character shortcuts on the login screen. +[Vanilla AutoLogin](https://github.com/MarcelineVQ/turtle-autologin) can optionally save account and character shortcuts on the login screen. When **AutoLogin + Nampower** are enabled together, Modernization Tool automatically creates or reuses the Windows user encryption key required by Nampower. @@ -200,27 +200,6 @@ Existing encryption keys are never replaced. --- -# 📦 Current Notable Versions - -| Component | Version | -|---|---| -| VanillaFixes | 1.5.3 | -| DXVK | 2.6.1 x86 | -| Nampower | 4.6.1 | -| ClassicAPI | 1.13.3 | -| AuctionQueryThrottle | 1.2.0 | -| UnitXP_SP3 | v90 | -| SuperWoW | 2.2 | -| VanillaHelpers | 1.1.2 | -| WeirdPerformance | 0.7.3 | -| WorldMarkers | 0.7.1 | -| VanillaMultiMonitorFix | 0.2 | -| Interact | 1.0.4 | - -Where supported, the tool will attempt to obtain the latest compatible upstream version automatically. - ---- - # 📝 Updating Modernization Tool Updating from an older version does **not** require reinstalling WoW. @@ -238,7 +217,9 @@ Your per-installation settings will be restored automatically. # 🛡️ Antivirus Notice -Because Modernization Tool modifies a game executable and installs client-side DLL plugins, some antivirus products may flag the executable or some included components. +Because Modernization Tool creates a patched `WoW_Modernized.exe` and installs client-side DLL plugins, some antivirus products may flag the executable or some included components. + +The original `WoW.exe` is not modified in place. Always download Modernization Tool from the official GitHub Releases page. @@ -247,54 +228,15 @@ Always download Modernization Tool from the official GitHub Releases page. # 🔗 Useful Links - **[Latest Release](https://github.com/Dusk-92/Modernization-Tool/releases/latest)** -- **[v2.2 Release Notes](https://github.com/Dusk-92/Modernization-Tool/releases/tag/v2.2)** - **[OctoWoW Installation & Modernization Guide](https://octowow.st/forum/viewtopic.php?t=831)** --- # ❤️ Credits -Modernization Tool brings together work from several community projects and developers. +Modernization Tool builds on the work of the community projects and developers linked throughout this README. -## Rendering & Client Patching - -- [VanillaFixes](https://github.com/hannesmann/vanillafixes) -- [DXVK](https://github.com/doitsujin/dxvk) -- [vanilla-tweaks](https://github.com/tubtubs/vanilla-tweaks) - -## Core Engine & API Plugins - -- [VanillaHelpers](https://github.com/isfir/VanillaHelpers) -- [PerfBoost Settings](https://gitea.com/avitasia/PerfBoostSettings) -- [UnitXP_SP3](https://github.com/brues-code/UnitXP_SP3) -- [SuperWoW](https://github.com/balakethelock/SuperWoW) -- [SuperAPI](https://github.com/balakethelock/SuperAPI) -- [ClassicAPI](https://github.com/brues-code/ClassicAPI) -- [AuctionQueryThrottle](https://github.com/brues-code/AuctionQueryThrottle) -- [Nampower](https://github.com/brues-code/nampower) -- [NampowerSettings](https://github.com/brues-code/NampowerSettings) -- [No1600x1200](https://github.com/RetroCro/TurtleWoW-Mods#no1600x1200) -- [VanillaMultiMonitorFix](https://github.com/Mates1500/VanillaMultiMonitorFix) -- [Interact](https://github.com/lookino/Interact) -- [WowPresence](https://github.com/Dusk-92/WowPresence) - -## Visual & Audio Mods - -- [Bluemoon Patch via vanilla-tweaks](https://github.com/tubtubs/vanilla-tweaks) -- [Darker Nights — Project Reforged](https://projectreforged.github.io/vanilla/downloads/turtle/) -- [Pretty Night Sky / Epoch Water / Fog Pushback — RetroCro TurtleWoW Mods](https://github.com/RetroCro/TurtleWoW-Mods) -- [Pink Herbs](https://github.com/seacrabsam/patch-herb) -- [NoErrorSounds](https://github.com/Macumbafeh/NoErrorSounds) -- [FishPing](https://github.com/notsureawake/FishPing) -- [Warlock Muted Demons](https://github.com/spzilyk/Warlock-Muted-Demons) -- [Automatic WDB management guide](https://github.com/RetroCro/TurtleWoW-Mods#automatically-clear-wdb-folder-every-time-you-launch-turtle-wow) - -## Other Bundled Enhancements - -- [Vanilla AutoLogin](https://github.com/MarcelineVQ/turtle-autologin) -- [WeirdUtils](https://codeberg.org/Dusk92/WeirdUtils) - -Additional attribution and source links are available directly in the **Credits** tab of Modernization Tool. +Thank you to all upstream authors and contributors whose work makes this project possible. Additional attribution is available directly in the **Credits** tab of Modernization Tool. --- @@ -311,8 +253,8 @@ For detailed redistribution and provenance information, see: - [Docs/ASSET_PROVENANCE.md](Docs/ASSET_PROVENANCE.md) - [LICENSES/](LICENSES/) -SuperWoW and SuperAPI are downloaded directly from their upstream projects and -are not bundled as offline fallbacks. +SuperWoW and the current SuperAPI master revision are downloaded directly from +their upstream projects and are not bundled as offline fallbacks. --- diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4e145af..e76da06 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -14,18 +14,20 @@ - Added a `Show character details` option to quickly enable all Discord details. - Updated WowPresence integration for the new privacy system. -## 🔄 Updates & Downloads +## 🔄 Updates & Reliability - Added smart update checks for remote components. - Unchanged components are no longer downloaded again on every Apply. - Modified or missing managed files are automatically detected and repaired. - Release assets are detected even when an upstream project replaces a file under the same tag. - Improved vanilla-tweaks update detection to avoid unnecessary downloads and repatching. -- Improved update checks for GitHub branch-based components. - -## 📚 Documentation - -- Updated Discord Rich Presence documentation. +- Improved branch-based component downloads to use the exact resolved revision. +- SuperAPI continues to follow upstream `master` while installing the exact detected revision. +- Added integrity checks for bundled components before installation. +- Improved atomic installation of bundled components and dependent addons. +- Improved Nampower and UnitXP addon handling during updates and offline fallback. +- No1600x1200 now uses the bundled known-good version. +- Fixed stale `.modernization-backup-*` files remaining after successful updates. ## ✅ Updating from v2.1 diff --git a/remote_packages.py b/remote_packages.py index afd239b..bd6dd61 100644 --- a/remote_packages.py +++ b/remote_packages.py @@ -353,6 +353,31 @@ def _remove_path(path): os.remove(path) +def _cleanup_transaction_backups(target): + """Best-effort cleanup of stale backups once a valid live target exists.""" + target = os.path.abspath(target) + if not os.path.lexists(target): + return + + parent = os.path.dirname(target) + prefix = os.path.basename(target) + ".modernization-backup-" + try: + names = os.listdir(parent) + except OSError: + return + + for name in names: + if not name.startswith(prefix): + continue + backup = os.path.join(parent, name) + try: + _remove_path(backup) + except OSError: + # The old file may still be locked by WoW/Windows. Keep it safe + # and retry automatically the next time this package is verified. + pass + + def _transactional_replace_bundle(items, label="component bundle"): """Replace a group of files/directories as one rollback-safe transaction. @@ -469,15 +494,10 @@ def _transactional_replace_bundle(items, label="component bundle"): ) from exc else: - # Commit succeeded. Old copies are no longer needed. + # Commit succeeded. Old copies are no longer needed. Clean the current + # transaction backup plus any stale backup left by an earlier run. for record in records: - if record["backup_created"] and os.path.lexists(record["backup"]): - try: - _remove_path(record["backup"]) - except OSError: - # A stale backup is harmless; never invalidate a successful - # installation just because cleanup was denied. - pass + _cleanup_transaction_backups(record["target"]) finally: # Staged paths are safe to remove. Backups are intentionally not @@ -1162,7 +1182,17 @@ def _package_state_is_current(target_dir, package_id, revision): current = _snapshot_package_paths(target_dir, paths.keys()) except OSError: return False - return current == paths + + is_current = current == paths + if is_current: + # A verified current package no longer needs rollback artifacts from + # earlier successful installs. Retry best-effort cleanup here so a + # Windows file lock does not leave them around indefinitely. + for relative_path in paths: + _cleanup_transaction_backups( + os.path.join(target_dir, relative_path) + ) + return is_current def _record_package_state_safely(target_dir, package_id, revision, relative_paths): @@ -1726,7 +1756,7 @@ def install_pink_herbs(target_dir, progress=None): return f"seacrabsam/patch-herb main@{revision[:7]}" temp_path = _download( - "https://raw.githubusercontent.com/seacrabsam/patch-herb/main/patch-H.mpq", + f"https://raw.githubusercontent.com/seacrabsam/patch-herb/{revision}/patch-H.mpq", suffix=".mpq", progress=progress, label="Downloading Pink Herbs", @@ -1762,8 +1792,15 @@ def install_pink_herbs(target_dir, progress=None): return f"seacrabsam/patch-herb main@{revision[:7]}" -def _download_github_branch_archive(repo, branch, progress=None, label="Downloading sound mod"): - url = f"https://codeload.github.com/{repo}/zip/refs/heads/{branch}" +def _download_github_branch_archive( + repo, + branch, + progress=None, + label="Downloading sound mod", + revision=None, +): + revision = revision or _branch_head_sha(repo, branch) + url = f"https://codeload.github.com/{repo}/zip/{revision}" return _download( url, suffix=".zip", @@ -1812,6 +1849,7 @@ def _install_github_sound_pack(target_dir, mod_id, repo, branch, source_folder, branch, progress=progress, label=label, + revision=revision, ) extract_root = tempfile.mkdtemp(prefix=f"modernization_{mod_id}_") try: @@ -1992,7 +2030,7 @@ def install_no1600x1200(target_dir, progress=None): url = ( "https://raw.githubusercontent.com/RetroCro/TurtleWoW-Mods/" - "refs/heads/main/Archive/DLL%20BACKUP/no1600x1200.dll" + f"{revision}/Archive/DLL%20BACKUP/no1600x1200.dll" ) temp_path = _download( url, @@ -2198,7 +2236,7 @@ def install_superwow(target_dir, progress=None): # Prepare SuperAPI completely before changing the installed DLL. _emit_progress(progress, "Preparing SuperAPI update...", None, None) superapi_zip = _download( - "https://codeload.github.com/balakethelock/SuperAPI/zip/refs/heads/master", + f"https://codeload.github.com/balakethelock/SuperAPI/zip/{superapi_revision}", suffix=".zip", progress=progress, label="Downloading SuperAPI addon", diff --git a/setup_tool_dynamic.py b/setup_tool_dynamic.py index 2e9b8d4..29256de 100644 --- a/setup_tool_dynamic.py +++ b/setup_tool_dynamic.py @@ -43,6 +43,136 @@ class ModernWowSetupTool(WowSetupTool): self._download_indeterminate = False super().__init__(root) + def _bundled_manifest_record(self, relative_path): + """Return the expected size/hash record for one bundled payload file.""" + metadata_path = os.path.join( + get_base_path(), + "Payload", + "Fallback", + "versions.json", + ) + wanted = relative_path.replace("\\", "/").casefold() + try: + with open(metadata_path, "r", encoding="utf-8") as handle: + data = json.load(handle) + except (OSError, json.JSONDecodeError, ValueError, TypeError) as exc: + raise RuntimeError( + "Could not read bundled component integrity metadata." + ) from exc + + components = data.get("components") if isinstance(data, dict) else None + if not isinstance(components, dict): + raise RuntimeError("Bundled component integrity metadata is invalid.") + + for component in components.values(): + files = component.get("files") if isinstance(component, dict) else None + if not isinstance(files, list): + continue + for record in files: + if not isinstance(record, dict): + continue + path = record.get("path") + if isinstance(path, str) and path.replace("\\", "/").casefold() == wanted: + return record + + raise RuntimeError( + f"No integrity record exists for bundled file {relative_path}." + ) + + def _verified_bundled_file(self, relative_path, label): + """Validate a bundled file before it is allowed to touch the WoW folder.""" + record = self._bundled_manifest_record(relative_path) + source = os.path.join( + get_base_path(), + *relative_path.replace("\\", "/").split("/"), + ) + if not os.path.isfile(source): + raise RuntimeError(f"Bundled file is missing: {relative_path}") + + expected_size = record.get("size") + if not isinstance(expected_size, int) or expected_size <= 0: + raise RuntimeError(f"Invalid bundled size metadata for {relative_path}.") + if os.path.getsize(source) != expected_size: + raise RuntimeError( + f"Bundled {label} has an unexpected size and will not be installed." + ) + + expected_sha = record.get("sha256") + if ( + not isinstance(expected_sha, str) + or len(expected_sha) != 64 + or any(ch not in "0123456789abcdefABCDEF" for ch in expected_sha) + ): + raise RuntimeError(f"Invalid bundled SHA-256 metadata for {relative_path}.") + expected_sha = expected_sha.lower() + + if self._file_sha256(source).lower() != expected_sha: + raise RuntimeError( + f"Bundled {label} failed its SHA-256 integrity check." + ) + + if os.path.splitext(source)[1].lower() in (".dll", ".exe"): + remote_packages._verify_x86_pe(source, f"bundled {label}") + + return source, expected_sha + + def _install_verified_bundled_file(self, relative_path, target_path, label): + source, expected_sha = self._verified_bundled_file(relative_path, label) + if os.path.isfile(target_path): + try: + if self._file_sha256(target_path).lower() == expected_sha: + return + except OSError: + pass + remote_packages._atomic_replace_file(source, target_path) + + def copy_base_files(self, target): + """Install only true base files; component addons are handled with their DLLs.""" + payload_dir = os.path.join(get_base_path(), "Payload") + if not os.path.isdir(payload_dir): + return + + if self.install_autologin.get(): + data_source = os.path.join(payload_dir, "Data") + if os.path.isdir(data_source): + shutil.copytree( + data_source, + os.path.join(target, "Data"), + dirs_exist_ok=True, + ) + + # Do not copy Payload/Interface wholesale here. Nampower and UnitXP + # addons must stay paired with the exact DLL version selected later. + vanilla_fixes, vf_sha = self._verified_bundled_file( + "Payload/VanillaFixes.exe", + "VanillaFixes.exe", + ) + vf_patcher, patcher_sha = self._verified_bundled_file( + "Payload/VfPatcher.dll", + "VfPatcher.dll", + ) + target_fixes = os.path.join(target, "VanillaFixes.exe") + target_patcher = os.path.join(target, "VfPatcher.dll") + + already_current = False + if os.path.isfile(target_fixes) and os.path.isfile(target_patcher): + try: + already_current = ( + self._file_sha256(target_fixes).lower() == vf_sha + and self._file_sha256(target_patcher).lower() == patcher_sha + ) + except OSError: + already_current = False + + if not already_current: + remote_packages._transactional_replace_bundle( + [ + ("file", vanilla_fixes, target_fixes), + ("file", vf_patcher, target_patcher), + ], + label="VanillaFixes", + ) + def _collect_settings(self): settings = super()._collect_settings() settings["discord_presence"] = { @@ -421,7 +551,7 @@ class ModernWowSetupTool(WowSetupTool): "SuperWoWhook.dll": ("SuperWoW", "by balakethelock"), "transmogfix.dll": ("TransmogFix", "by MarcelineVQ"), "perf_boost.dll": ("PerfBoost", "by avitasia"), - "weirdperformance.dll": ("WeirdPerformance", "Dusk92 build 0.7.3"), + "weirdperformance.dll": ("WeirdPerformance", "by Dusk-92"), "VanillaHelpers.dll": ("VanillaHelpers", "by isfir"), } @@ -606,9 +736,9 @@ class ModernWowSetupTool(WowSetupTool): f"Could not download the latest {dll_name}, and no bundled fallback exists.\n\n{error}" ) from error - remote_packages._verify_x86_pe( - source_dll, - f"bundled fallback {dll_name}", + self._verified_bundled_file( + f"Payload/{dll_name}", + f"{dll_name} fallback", ) source_addon = None @@ -1053,10 +1183,48 @@ class ModernWowSetupTool(WowSetupTool): except Exception as exc: self._fallback_core_dll(payload_base, target, dll_name, exc) + elif dll_name == "perf_boost.dll": + source_dll, _ = self._verified_bundled_file( + "Payload/perf_boost.dll", + "PerfBoost", + ) + source_addon = os.path.join( + payload_base, + "Interface", + "Addons", + "perfboostsettings", + ) + if not os.path.isdir(source_addon): + raise RuntimeError( + "Bundled PerfBoost settings addon is missing." + ) + remote_packages._transactional_replace_bundle( + [ + ( + "file", + source_dll, + os.path.join(target, "perf_boost.dll"), + ), + ( + "dir", + source_addon, + os.path.join( + target, + "Interface", + "AddOns", + "perfboostsettings", + ), + ), + ], + label="PerfBoost", + ) + else: - source_dll = os.path.join(payload_base, dll_name) - if os.path.exists(source_dll): - shutil.copy2(source_dll, target) + self._install_verified_bundled_file( + f"Payload/{dll_name}", + os.path.join(target, dll_name), + dll_name, + ) dlls_text_lines.append(dll_name) @@ -1093,18 +1261,14 @@ class ModernWowSetupTool(WowSetupTool): # Optional client fixes, in the same order as the UI. no1600_var = self.optional_plugins["no1600x1200.dll"] if no1600_var.get(): - try: - remote_packages.install_no1600x1200( - target, - progress=self._report_download_progress, - ) - except Exception as exc: - self._fallback_core_dll( - payload_base, - target, - "no1600x1200.dll", - exc, - ) + # No1600x1200 comes from an archive/mirror repository rather + # than a maintained release channel. Use the bundled, + # hash-verified known-good copy for predictable installs. + self._install_verified_bundled_file( + "Payload/no1600x1200.dll", + os.path.join(target, "no1600x1200.dll"), + "no1600x1200.dll", + ) dlls_text_lines.append("no1600x1200.dll") if self.vmmfix_enabled.get(): @@ -1175,9 +1339,11 @@ class ModernWowSetupTool(WowSetupTool): self._discord_broadcast_mask(), ) else: - source_dll = os.path.join(payload_weirdu, dll_name) - if os.path.exists(source_dll): - shutil.copy2(source_dll, target) + self._install_verified_bundled_file( + f"Payload/WeirdUtils/{dll_name}", + os.path.join(target, dll_name), + dll_name, + ) dlls_text_lines.append(dll_name) diff --git a/tests/test_safety.py b/tests/test_safety.py index c1d360f..16b3862 100644 --- a/tests/test_safety.py +++ b/tests/test_safety.py @@ -1,3 +1,4 @@ +import hashlib import json import os import struct @@ -645,6 +646,62 @@ class SmartUpdateTests(unittest.TestCase): remote_packages._package_state_is_current(root, "example", "v1") ) + def test_current_package_cleans_stale_transaction_backup(self): + with tempfile.TemporaryDirectory() as root: + dll = os.path.join(root, "UnitXP_SP3.dll") + with open(dll, "wb") as handle: + handle.write(b"current-unitxp") + + remote_packages._record_package_state( + root, + "unitxp_sp3", + "v90", + ["UnitXP_SP3.dll"], + ) + + stale_backup = dll + ".modernization-backup-941614e28c62" + with open(stale_backup, "wb") as handle: + handle.write(b"old-unitxp") + + self.assertTrue( + remote_packages._package_state_is_current( + root, + "unitxp_sp3", + "v90", + ) + ) + self.assertFalse(os.path.exists(stale_backup)) + self.assertTrue(os.path.isfile(dll)) + + def test_non_current_package_keeps_transaction_backup_for_recovery(self): + with tempfile.TemporaryDirectory() as root: + dll = os.path.join(root, "UnitXP_SP3.dll") + with open(dll, "wb") as handle: + handle.write(b"current-unitxp") + + remote_packages._record_package_state( + root, + "unitxp_sp3", + "v90", + ["UnitXP_SP3.dll"], + ) + + stale_backup = dll + ".modernization-backup-recovery" + with open(stale_backup, "wb") as handle: + handle.write(b"old-unitxp") + + with open(dll, "wb") as handle: + handle.write(b"modified-after-state") + + self.assertFalse( + remote_packages._package_state_is_current( + root, + "unitxp_sp3", + "v90", + ) + ) + self.assertTrue(os.path.exists(stale_backup)) + def test_release_asset_revision_detects_replaced_asset_under_same_tag(self): release_a = { "tag_name": "Release", @@ -1172,5 +1229,190 @@ class SettingsRecoveryTests(unittest.TestCase): self.assertEqual(handle.read(), damaged) +class BundledComponentSafetyTests(unittest.TestCase): + def test_bundled_integrity_uses_recorded_sha256_and_size(self): + tool = ModernWowSetupTool.__new__(ModernWowSetupTool) + + with tempfile.TemporaryDirectory() as root: + payload = os.path.join(root, "Payload") + fallback = os.path.join(payload, "Fallback") + os.makedirs(fallback) + source = os.path.join(payload, "component.bin") + with open(source, "wb") as handle: + handle.write(b"known bundled component") + + expected_sha = hashlib.sha256( + b"known bundled component" + ).hexdigest() + + with open( + os.path.join(fallback, "versions.json"), + "w", + encoding="utf-8", + ) as handle: + json.dump( + { + "components": { + "test": { + "files": [ + { + "path": "Payload/component.bin", + "size": os.path.getsize(source), + "sha256": expected_sha, + } + ] + } + } + }, + handle, + ) + + with mock.patch("setup_tool_dynamic.get_base_path", return_value=root): + verified, digest = tool._verified_bundled_file( + "Payload/component.bin", + "test component", + ) + self.assertEqual(verified, source) + self.assertEqual(digest, expected_sha) + + with open(source, "ab") as handle: + handle.write(b"!") + + with self.assertRaises(RuntimeError): + tool._verified_bundled_file( + "Payload/component.bin", + "test component", + ) + + def test_copy_base_files_does_not_copy_component_addons(self): + tool = ModernWowSetupTool.__new__(ModernWowSetupTool) + tool.install_autologin = FakeVar(False) + + with tempfile.TemporaryDirectory() as root: + payload = os.path.join(root, "Payload") + os.makedirs(os.path.join(payload, "Interface", "Addons", "nampowersettings")) + vanilla_fixes = os.path.join(payload, "VanillaFixes.exe") + vf_patcher = os.path.join(payload, "VfPatcher.dll") + for path in (vanilla_fixes, vf_patcher): + with open(path, "wb") as handle: + handle.write(b"test") + + tool._verified_bundled_file = mock.Mock( + side_effect=[ + (vanilla_fixes, "a" * 64), + (vf_patcher, "b" * 64), + ] + ) + + target = os.path.join(root, "game") + os.makedirs(target) + + with ( + mock.patch("setup_tool_dynamic.get_base_path", return_value=root), + mock.patch.object( + remote_packages, + "_transactional_replace_bundle", + ) as transaction, + ): + tool.copy_base_files(target) + + self.assertFalse(os.path.exists(os.path.join(target, "Interface"))) + transaction.assert_called_once() + items = transaction.call_args.args[0] + self.assertEqual( + [os.path.basename(item[2]) for item in items], + ["VanillaFixes.exe", "VfPatcher.dll"], + ) + + def test_no1600_uses_bundled_copy_without_online_update(self): + tool = ModernWowSetupTool.__new__(ModernWowSetupTool) + tool.rendering_mode = FakeVar("directx9") + tool.core_plugins = {} + tool.classicapi_enabled = FakeVar(False) + tool.auction_throttle_enabled = FakeVar(False) + tool.addon_dependencies = {} + tool.optional_plugins = { + "no1600x1200.dll": FakeVar(True), + } + tool.vmmfix_enabled = FakeVar(False) + tool.interact_enabled = FakeVar(False) + tool._install_verified_bundled_file = mock.Mock() + tool._write_dlls_file = mock.Mock() + tool._close_download_progress = mock.Mock() + tool._report_download_progress = mock.Mock() + + with tempfile.TemporaryDirectory() as root: + with mock.patch.object( + remote_packages, + "install_no1600x1200", + ) as online_installer: + tool.configure_plugins(root) + + online_installer.assert_not_called() + tool._install_verified_bundled_file.assert_called_once_with( + "Payload/no1600x1200.dll", + mock.ANY, + "no1600x1200.dll", + ) + self.assertEqual( + os.path.basename( + tool._install_verified_bundled_file.call_args.args[1] + ), + "no1600x1200.dll", + ) + + def test_superapi_tracks_master_revision(self): + release = { + "name": "SuperWoW 2.2", + "tag_name": "Release", + "id": 1, + "assets": [ + { + "name": "SuperWoW.zip", + "id": 2, + "updated_at": "2026-07-16T00:00:00Z", + "size": 123, + } + ], + } + + with ( + mock.patch.object(remote_packages, "_latest_release", return_value=release), + mock.patch.object( + remote_packages, + "_package_state_is_current", + return_value=True, + ) as state, + mock.patch.object( + remote_packages, + "_branch_head_sha", + return_value="abc1234", + ) as branch_head, + ): + remote_packages.install_superwow("C:/WoW") + + branch_head.assert_called_once_with("balakethelock/SuperAPI", "master") + revision = state.call_args.args[2] + self.assertIn("superapi:abc1234", revision) + + def test_branch_archive_download_can_be_pinned_to_resolved_revision(self): + with ( + mock.patch.object(remote_packages, "_download", return_value="archive.zip") as download, + mock.patch.object(remote_packages, "_branch_head_sha") as branch_head, + ): + result = remote_packages._download_github_branch_archive( + "owner/repo", + "main", + revision="abc1234", + ) + + self.assertEqual(result, "archive.zip") + branch_head.assert_not_called() + self.assertEqual( + download.call_args.args[0], + "https://codeload.github.com/owner/repo/zip/abc1234", + ) + + if __name__ == "__main__": unittest.main()