176 Commits

Author SHA1 Message Date
Dusk 3973c5b302 Téléverser les fichiers vers "Payload/Fallback" 2026-09-12 13:36:39 +00:00
Dusk f99bd7e7b2 Actualiser .github/workflows/build.yml 2026-09-12 13:35:38 +00:00
Dusk f0e36e7fc9 Actualiser README.md
Build Windows EXE / build (push) Has been cancelled
2026-09-12 13:02:43 +00:00
Dusk d29d6b8020 Actualiser README.md
Build Windows EXE / build (push) Has been cancelled
2026-09-12 13:02:23 +00:00
Dusk de08b39896 Actualiser README.md
Build Windows EXE / build (push) Has been cancelled
2026-09-12 13:00:44 +00:00
Dusk 5cadbda323 Actualiser README.md
Build Windows EXE / build (push) Has been cancelled
2026-09-12 13:00:24 +00:00
Dusk 5ebcebffdd Actualiser README.md
Build Windows EXE / build (push) Has been cancelled
2026-09-12 12:59:41 +00:00
Dusk-92 2d4126ad5e Reduce CI build and artifact usage
Build Windows EXE / build (push) Has been cancelled
Run validation-only checks on direct test-branch pushes, keep full Windows builds for PRs, main, tags and manual runs, and store Actions artifacts only for explicit manual builds with 7-day retention. Release publishing remains unchanged.
2026-09-08 10:12:07 +02:00
Dusk-92 a02dc97793 release v2.3 2026-09-06 21:22:45 +02:00
Dusk-92 bea8a8e0ed Remove temporary Farclip CI trigger 2026-09-06 21:20:30 +02:00
Dusk-92 c9f8b78df7 Trigger final Farclip CI 2026-09-06 21:20:12 +02:00
github-actions[bot] 26dc319155 Finalize Farclip release polish 2026-09-06 19:19:16 +00:00
Dusk-92 1b06e33421 Prepare Farclip final release polish 2026-09-06 21:19:08 +02:00
Dusk-92 85a81d9259 Align safety test with fixed Farclip ceiling 2026-09-06 21:07:15 +02:00
Dusk-92 c644cb3481 Update FoV tests for Farclip synchronization 2026-09-06 21:06:43 +02:00
Dusk-92 0faf9ce25b Harden fixed Farclip ceiling and migration tests 2026-09-06 21:03:45 +02:00
Dusk-92 0eb62fbf04 Move Farclip policy into dynamic installer core 2026-09-06 21:00:31 +02:00
Dusk-92 d59b922f97 Add Farclip synchronization regression tests 2026-09-06 20:45:51 +02:00
Dusk-92 5aaf852b79 Synchronize Farclip runtime value with executable ceiling 2026-09-06 20:45:33 +02:00
Dusk-92 998b9650d8 release v2.3 2026-09-06 10:42:19 +02:00
Dusk-92 776b5f1d0f Update v2.3 release notes for DXVK FPS limiting 2026-09-06 10:42:09 +02:00
Dusk-92 5ef038041d release v2.3 2026-09-06 10:40:22 +02:00
Dusk-92 509a09b4a0 Merge DXVK FPS limiting
Adds refresh-rate based DXVK FPS limiting with editable values, DX9 isolation, persistence, compatibility handling, and focused safety tests.
2026-09-06 10:40:03 +02:00
Dusk-92 49c7eb5376 Keep DXVK FPS UI behavior unchanged after safety fixes 2026-09-06 10:22:02 +02:00
Dusk-92 acd385ac25 Keep DXVK FPS tests deterministic on Windows CI 2026-09-06 10:21:37 +02:00
Dusk-92 48078cc827 Add DXVK FPS files to syntax validation 2026-09-06 10:20:50 +02:00
Dusk-92 4d239a2e30 Expand DXVK FPS multi-monitor safety tests 2026-09-06 10:20:37 +02:00
Dusk-92 a6c16e4b2b Handle signed DXVK frame-rate settings safely 2026-09-06 10:20:11 +02:00
Dusk-92 a703884a84 Harden DXVK FPS control state handling 2026-09-06 10:06:43 +02:00
Dusk-92 514e937d0e Expand DXVK FPS limit safety tests 2026-09-06 10:06:09 +02:00
Dusk-92 4f0b747159 Test DXVK FPS limit configuration 2026-09-06 10:05:10 +02:00
Dusk-92 26445a2731 Add configurable DXVK FPS limit UI 2026-09-06 10:04:56 +02:00
Dusk-92 8d4ce2d7c6 Add DXVK FPS limit helpers 2026-09-06 10:04:09 +02:00
Dusk-92 7a1a3c9d12 release v2.3 2026-09-06 09:30:52 +02:00
Dusk-92 05b0b761c1 release v2.3
Make the Modernization Tool UI scaling-safe
2026-09-06 09:30:31 +02:00
Dusk-92 29d7f3c174 release v2.3 2026-09-06 09:30:06 +02:00
Dusk-92 3e5fbe47dd release v2.3 2026-09-06 09:29:52 +02:00
Dusk-92 178f1460a9 Finalize responsive UI build workflow 2026-09-06 09:29:43 +02:00
Dusk-92 e385229f4a Remove temporary UI scaling test entry point 2026-09-06 09:28:29 +02:00
Dusk-92 cd8a3e19f9 Build responsive UI entry point 2026-09-06 09:28:23 +02:00
Dusk-92 62b736c205 Add responsive production UI entry point 2026-09-06 09:28:00 +02:00
Dusk-92 7841c58a64 Build isolated UI scaling test artifact 2026-09-06 09:05:49 +02:00
Dusk-92 99e8aafacf Add isolated UI scaling test build 2026-09-06 09:05:27 +02:00
Dusk-92 f9a0315c1c release v2.3 2026-09-06 09:04:59 +02:00
Dusk-92 8cba9f9bfa release v2.3 2026-09-06 08:57:14 +02:00
Dusk-92 9b46d98e6a release v2.3 2026-09-05 17:14:22 +02:00
Dusk-92 d0627a574f release v2.3 2026-09-05 11:13:48 +02:00
Dusk-92 315d32d52f Sync SuperWoW FoV with Modernization Tool
Keep the Modernization Tool FoV selection synchronized with SuperWoW's FoV CVar and add regression coverage.
2026-09-05 11:13:19 +02:00
Dusk-92 881e25ca97 release v2.3 2026-09-04 17:03:00 +02:00
Dusk-92 16d2a8d8ec Prepare v2.3 release workflow 2026-09-04 17:00:13 +02:00
Dusk-92 10e4be0fec Prepare v2.3 release notes 2026-09-04 16:59:54 +02:00
Dusk-92 363eb89fd0 Test real installation ordering 2026-09-04 16:31:31 +02:00
Dusk-92 2dbd335d8f Remove obsolete staged numeric validation test 2026-09-04 16:03:19 +02:00
Dusk-92 1a6a60b643 Update Vanilla Tweaks validation tests 2026-09-04 16:02:55 +02:00
Dusk-92 a1c70df8e8 Clean up relaxed Vanilla Tweaks validation 2026-09-04 16:02:02 +02:00
Dusk-92 3cda29653b Relax intermediate Vanilla Tweaks numeric checks 2026-09-04 15:49:11 +02:00
Dusk-92 14ca95f553 Update safety tests for flexible client identity 2026-09-04 15:22:58 +02:00
Dusk-92 acbe46e5ba Remove fixed client build identity gate 2026-09-04 15:20:11 +02:00
Dusk-92 81b6cd6a30 Remove fixed build identity guard test 2026-09-04 15:18:53 +02:00
Dusk-92 94a613a815 Tighten MPQ user-data validation 2026-09-04 15:01:09 +02:00
Dusk-92 50ed6f20fa Tighten executable and wrapped MPQ validation 2026-09-04 14:42:50 +02:00
Dusk-92 b92e9c2987 Preserve wrapped MPQ compatibility and strict PE staging 2026-09-04 14:22:47 +02:00
Dusk-92 0602e012c1 Align hardening tests with scoped runtime hooks 2026-09-04 14:17:39 +02:00
Dusk-92 a263ec6fb8 Scope strict MPQ checks to visual installs 2026-09-04 14:16:51 +02:00
Dusk-92 f51ef200a4 Test final safety hardening 2026-09-04 14:10:50 +02:00
Dusk-92 95e1b25f4e Harden executable and MPQ safety checks 2026-09-04 14:10:20 +02:00
Dusk-92 31eb2c911c Test source preflight and client GlueXML preservation 2026-09-04 13:35:35 +02:00
Dusk-92 c992cee4cf Preflight Vanilla Tweaks before patching 2026-09-04 13:34:28 +02:00
Dusk-92 7471b4f12e Preserve dynamic module test compatibility 2026-09-04 11:24:20 +02:00
Dusk-92 eca8599f0c Make Vanilla Tweaks selections fully authoritative 2026-09-04 11:21:16 +02:00
Dusk-92 2338c005b8 Test transactional Vanilla Tweaks output 2026-09-04 10:33:03 +02:00
Dusk-92 13f3c247d0 Make Vanilla Tweaks output transaction-safe 2026-09-04 10:32:11 +02:00
Dusk-92 7c9d69179f Test authoritative Vanilla Tweaks normalization 2026-09-04 08:46:27 +02:00
Dusk-92 655c444353 Make selected Vanilla Tweaks authoritative 2026-09-04 08:45:53 +02:00
Dusk-92 600719652e Test WowPresence legacy and local failure handling 2026-09-04 08:32:32 +02:00
Dusk-92 8658700a76 Harden WowPresence legacy recovery 2026-09-04 08:32:00 +02:00
Dusk-92 f8a8678e2e Remove one-time SuperWoW UI cleanup workflow 2026-09-03 23:37:23 +02:00
github-actions[bot] e88d2cd28d Remove SuperWoW Vanilla Tweaks coupling 2026-09-03 21:37:03 +00:00
Dusk-92 798408c559 Cleanup obsolete SuperWoW tweak UI 2026-09-03 23:36:55 +02:00
Dusk-92 17059d8a76 Cleanup obsolete SuperWoW tweak UI 2026-09-03 23:36:22 +02:00
Dusk-92 9569bbd296 Remove one-time tweak migration script 2026-09-03 23:33:52 +02:00
Dusk-92 e5e4dc954a Remove one-time tweak migration workflow 2026-09-03 23:33:46 +02:00
github-actions[bot] b861ad89e2 Keep Vanilla Tweaks independent from SuperWoW 2026-09-03 21:33:23 +00:00
Dusk-92 a46bb67d0d Run independent tweak migration 2026-09-03 23:33:14 +02:00
Dusk-92 41592e7f8c Run independent tweak migration 2026-09-03 23:32:40 +02:00
Dusk-92 b12001584e Fix tweak policy migration script 2026-09-03 23:32:28 +02:00
Dusk-92 036ff0c1cb Run independent tweak migration 2026-09-03 23:31:03 +02:00
Dusk-92 56f0ff9854 Add one-time tweak policy migration 2026-09-03 23:30:50 +02:00
Dusk-92 3d3eea3c6a Apply independent vanilla tweak policy 2026-09-03 23:28:59 +02:00
Dusk-92 7c7ff0d146 Test exact QuickLoot restoration 2026-09-03 23:06:41 +02:00
Dusk-92 ae8abe2e16 Restore exact vanilla QuickLoot branches 2026-09-03 23:06:27 +02:00
Dusk-92 0b85ec4faf Test SuperWoW inherited patch normalization 2026-09-03 23:05:16 +02:00
Dusk-92 30ab74a4f0 Normalize SuperWoW-managed executable patches 2026-09-03 23:04:40 +02:00
Dusk-92 b727c928bb Version SuperWoW executable patch policy 2026-09-03 23:04:17 +02:00
Dusk-92 1f8c87ae47 Test offline repair of modified WowPresence 2026-09-03 22:21:54 +02:00
Dusk-92 3fb218e296 Repair modified managed WowPresence offline 2026-09-03 22:21:24 +02:00
Dusk-92 64b7612c90 Classify WowPresence managed install trust 2026-09-03 22:21:02 +02:00
Dusk-92 6c6d6e2b7d Clarify WowPresence cache trust rules 2026-09-03 21:43:00 +02:00
Dusk-92 c8bce690a5 Test strict MPQ errors and WowPresence cache trust 2026-09-03 21:42:49 +02:00
Dusk-92 96cc98d52b Do not mask local MPQ install failures 2026-09-03 21:42:18 +02:00
Dusk-92 e9c7b3b219 Separate remote MPQ failures from local install errors 2026-09-03 21:42:07 +02:00
Dusk-92 669361aeee Test MPQ fallback removal cleanup 2026-09-03 21:31:36 +02:00
Dusk-92 b0394ac662 Preserve only valid installed MPQs 2026-09-03 21:31:24 +02:00
Dusk-92 c140fdfe98 Clean obsolete MPQ fallback caches 2026-09-03 21:31:20 +02:00
Dusk-92 386917c9f0 Remove unused MPQ cache helper 2026-09-03 21:30:07 +02:00
Dusk-92 859fe4037e Update tests for online-only MPQs 2026-09-03 21:29:47 +02:00
Dusk-92 77ace2d642 Remove MPQ fallback provenance 2026-09-03 21:29:29 +02:00
Dusk-92 a42608f635 Remove MPQ fallback redistribution notes 2026-09-03 21:29:25 +02:00
Dusk-92 98325b403c Document online-only visual MPQs 2026-09-03 21:29:23 +02:00
Dusk-92 2b7de2613c Restore online-only MPQ behavior 2026-09-03 21:29:19 +02:00
Dusk-92 3e32b7985b Stop bundling MPQ fallbacks 2026-09-03 21:28:55 +02:00
Dusk-92 9ff415ef3f Remove MPQ fallback behavior 2026-09-03 21:28:44 +02:00
Dusk-92 63aa6328cc Clarify visual fallback failure message 2026-09-03 21:23:58 +02:00
Dusk-92 c9c878dc19 Prepare fallbacks in local builds 2026-09-03 21:21:58 +02:00
Dusk-92 08bd69d186 Pin generated fallback identities 2026-09-03 21:21:10 +02:00
Dusk-92 6fff497457 Preserve exact SuperAPI fallback bytes 2026-09-03 21:21:07 +02:00
Dusk-92 c6a65ab044 Validate fallback build script syntax 2026-09-03 21:20:03 +02:00
Dusk-92 4a0f068465 Test fresh offline bundled fallbacks 2026-09-03 21:20:00 +02:00
Dusk-92 166ae58a9f Document generated remote fallback provenance 2026-09-03 21:19:38 +02:00
Dusk-92 e59bc1fc53 Document build-time remote fallbacks 2026-09-03 21:19:35 +02:00
Dusk-92 065ff4ee4f Document bundled remote fallbacks 2026-09-03 21:19:32 +02:00
Dusk-92 34b3affeb1 Fix SuperAPI fallback blob identity 2026-09-03 21:18:55 +02:00
Dusk-92 c304cd0542 Add bundled WowPresence fallback path 2026-09-03 21:18:02 +02:00
Dusk-92 627e9392b5 Use bundled fallbacks for fresh offline installs 2026-09-03 21:17:51 +02:00
Dusk-92 74a6dd3891 Bundle remote fallbacks during builds 2026-09-03 21:17:15 +02:00
Dusk-92 890949b8db Prepare bundled remote fallbacks for release builds 2026-09-03 21:17:11 +02:00
Dusk-92 39c3963d5f Refresh SuperWoW fallback comments 2026-09-03 21:15:32 +02:00
Dusk-92 0416e21b54 Clarify visual fallback errors 2026-09-03 21:15:29 +02:00
Dusk-92 90f9247eb1 Verify real bundled SuperWoW fallback 2026-09-03 21:14:48 +02:00
Dusk-92 352330b048 Reject duplicate bundled tree metadata 2026-09-03 21:14:43 +02:00
Dusk-92 764d177638 Update SuperWoW fallback test coverage 2026-09-03 21:13:40 +02:00
Dusk-92 943caba91c Harden package cache paths 2026-09-03 21:13:37 +02:00
Dusk-92 422b076f19 Expand offline fallback regression coverage 2026-09-03 21:12:54 +02:00
Dusk-92 51e772d959 Refresh third-party audit date 2026-09-03 21:12:18 +02:00
Dusk-92 de0515c84c Refresh fallback provenance audit notes 2026-09-03 21:12:10 +02:00
Dusk-92 a5c6feee95 Clarify offline fallback behavior 2026-09-03 21:12:04 +02:00
Dusk-92 5f7792b9d9 Let visual installers seed offline caches 2026-09-03 21:11:45 +02:00
Dusk-92 1e568db8ec Verify SuperAPI fallback integrity 2026-09-03 21:11:28 +02:00
Dusk-92 e1c9ee4fb0 Record exact SuperAPI fallback tree 2026-09-03 21:10:56 +02:00
Dusk-92 6883fb318d Harden remote fallback cache behavior 2026-09-03 21:10:39 +02:00
Dusk-92 1b80a904b7 Fix WowPresence cache test import 2026-09-03 20:53:37 +02:00
Dusk-92 ad62557e59 Update SuperWoW license scope 2026-09-03 20:53:06 +02:00
Dusk-92 ae3a23be17 Update SuperWoW redistribution notice 2026-09-03 20:53:01 +02:00
Dusk-92 480d20f39e Add remote fallback regression tests 2026-09-03 20:51:46 +02:00
Dusk-92 d49fb9acc0 Document restored SuperWoW fallback 2026-09-03 20:51:15 +02:00
Dusk-92 fa764ba40f Document expanded offline fallbacks 2026-09-03 20:51:01 +02:00
Dusk-92 023dd7ba1d Record SuperWoW fallback provenance 2026-09-03 20:50:48 +02:00
Dusk-92 fb9275e13a Use cached WowPresence fallback offline 2026-09-03 20:50:38 +02:00
Dusk-92 846dffb59b Add local fallbacks for remote packages 2026-09-03 20:50:11 +02:00
Dusk-92 fc8c7eaa8e Restore SuperWoW and SuperAPI fallbacks 2026-09-03 20:46:41 +02:00
Dusk-92 9c397218a7 release v2.2 2026-09-03 16:42:37 +02:00
Dusk-92 098852175e Update ClassicAPI version in README 2026-09-03 10:13:19 +02:00
Dusk-92 bfa1e09b7a release v2.2 2026-09-02 16:00:48 +02:00
Dusk-92 2dee327763 Update README for Modernization Tool v2.2 2026-09-02 15:57:18 +02:00
Dusk-92 44b2c31339 Prepare Modernization Tool v2.2 release notes 2026-09-02 15:57:01 +02:00
Dusk-92 ef41957f11 Prepare Modernization Tool v2.2 workflow 2026-09-02 15:56:49 +02:00
Dusk-92 253dd69de1 Test stable WowPresence v1.3 integration 2026-09-02 15:56:36 +02:00
Dusk-92 3b424bd377 Use stable WowPresence releases for v2.2 2026-09-02 15:56:15 +02:00
Dusk-92 5a57ef47a4 Document Discord Rich Presence privacy controls 2026-09-02 15:32:22 +02:00
Dusk-92 f431b7b70b Test WowPresence prerelease test channel 2026-09-02 15:10:02 +02:00
Dusk-92 a7bb6fbdbe Connect Discord test branch to WowPresence prerelease 2026-09-02 15:09:32 +02:00
Dusk-92 2f01828bba Combine Discord and smart update tests 2026-09-02 14:43:03 +02:00
Dusk-92 4c34b8ab7b Add smart visual update checks to Discord test branch 2026-09-02 14:42:25 +02:00
Dusk-92 dd52e49c99 Combine smart vanilla-tweaks checks with Discord UI 2026-09-02 14:41:54 +02:00
Dusk-92 a30406c046 Combine smart updates with Discord detail flags 2026-09-02 14:41:37 +02:00
Dusk-92 e7cb7677a0 Test all-details toggle behavior 2026-09-02 11:42:29 +02:00
Dusk-92 afb4993dee Make character details an all-details toggle 2026-09-02 11:42:10 +02:00
Dusk-92 85c7ca9d98 Fix detail preference persistence test 2026-09-02 11:28:39 +02:00
Dusk-92 cd64ea0b31 Test Discord Rich Presence detail preferences 2026-09-02 11:26:46 +02:00
Dusk-92 f3aa797317 Add Discord Rich Presence detail controls 2026-09-02 11:26:22 +02:00
Dusk-92 b2ed9782fc Add WowPresence detail mask helpers 2026-09-02 11:25:48 +02:00
Dusk-92 fb1c5f7701 release v2.1 2026-09-02 11:15:20 +02:00
Dusk-92 4237125bcf Refresh v2.1 release assets 2026-09-02 11:15:06 +02:00
Dusk-92 5c72b69f56 Test WowPresence ZIP installation 2026-09-02 11:12:02 +02:00
Dusk-92 dadf7801a3 Install WowPresence from release ZIP 2026-09-02 11:11:53 +02:00
Dusk-92 7fd9c04fff release v2.1 2026-09-01 18:33:32 +02:00
46 changed files with 24768 additions and 1271 deletions
+1
View File
@@ -0,0 +1 @@
Payload/Interface/Addons/SuperAPI/** text eol=lf
+34 -53
View File
@@ -4,8 +4,7 @@ on:
push:
branches:
- 'main'
- 'feature/client-tweaks'
- 'test/discord-presence'
- 'test/**'
tags:
- 'v*'
pull_request:
@@ -13,25 +12,31 @@ on:
- 'main'
workflow_dispatch:
permissions:
contents: write
concurrency:
group: modernization-${{ github.workflow }}-${{ github.ref }}
group: modernization-${{ forgejo.workflow }}-${{ forgejo.event_name }}-${{ forgejo.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: windows-2025
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: https://code.forgejo.org/actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11.9'
# Self-hosted Windows runner: use the Python already installed on the host.
# actions/setup-python tries to modify machine-wide registry keys and needs
# elevated rights on this runner.
- name: Verify runner Python
shell: powershell
run: |
$version = python -c "import platform; print(platform.python_version())"
if ($LASTEXITCODE -ne 0) { throw "Python is not available on the runner PATH." }
Write-Host "Runner Python: $version"
if ($version -ne "3.12.10") {
throw "Expected Python 3.12.10 on the self-hosted runner, found $version."
}
- name: Install pinned build toolchain
run: python -m pip install --disable-pip-version-check -r requirements-build.txt
@@ -42,18 +47,20 @@ jobs:
pyinstaller --version
python -m pip show pyinstaller pyinstaller-hooks-contrib
- name: Configure x86 MSVC
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x86
# Direct pushes to test/** run validation only. PRs, main, tags and
# explicit manual runs still validate a real Windows build.
- name: Prepare bundled remote fallbacks
if: ${{ forgejo.event_name != 'push' || forgejo.ref == 'refs/heads/main' || startsWith(forgejo.ref, 'refs/tags/v') }}
run: python scripts/prepare_remote_fallbacks.py
- name: Syntax check
run: python -m py_compile setup_tool.py setup_tool_dynamic.py remote_packages.py tests/test_safety.py
run: python -m py_compile setup_tool.py setup_tool_dynamic.py setup_tool_responsive.py dxvk_fps.py remote_packages.py tests/test_safety.py tests/test_dxvk_fps_limit.py scripts/prepare_remote_fallbacks.py
- name: Unit safety tests
run: python -m unittest discover -s tests -v
- name: Build EXE
if: ${{ forgejo.event_name != 'push' || forgejo.ref == 'refs/heads/main' || startsWith(forgejo.ref, 'refs/tags/v') }}
run: >
pyinstaller
--noconsole
@@ -63,16 +70,18 @@ jobs:
--add-data "Payload;Payload"
--add-data "vanilla-tweaks.exe;."
--name "WoW_Modernization_Tool"
setup_tool_dynamic.py
setup_tool_responsive.py
- name: Verify EXE exists
shell: pwsh
if: ${{ forgejo.event_name != 'push' || forgejo.ref == 'refs/heads/main' || startsWith(forgejo.ref, 'refs/tags/v') }}
shell: powershell
run: |
if (-not (Test-Path "dist/WoW_Modernization_Tool.exe")) { throw "Build output missing" }
Get-Item "dist/WoW_Modernization_Tool.exe" | Format-List Name,Length,LastWriteTime
- name: Create ZIP package
shell: pwsh
if: ${{ forgejo.event_name != 'push' || forgejo.ref == 'refs/heads/main' || startsWith(forgejo.ref, 'refs/tags/v') }}
shell: powershell
run: |
$zip = "dist/WoW_Modernization_Tool.zip"
if (Test-Path $zip) { Remove-Item $zip -Force }
@@ -80,43 +89,15 @@ jobs:
if (-not (Test-Path $zip)) { throw "ZIP package missing" }
Get-Item $zip | Format-List Name,Length,LastWriteTime
- name: Upload test artifact
if: ${{ !startsWith(github.ref, 'refs/tags/') && !(github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v2.1') }}
uses: actions/upload-artifact@v4
# Keep the finished EXE/ZIP available for manual release publishing.
# Artifacts are uploaded for explicit manual runs and version tags.
- name: Upload build artifact
if: ${{ forgejo.event_name == 'workflow_dispatch' || startsWith(forgejo.ref, 'refs/tags/v') }}
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: WoW_Modernization_Tool-test
path: |
dist/WoW_Modernization_Tool.exe
dist/WoW_Modernization_Tool.zip
if-no-files-found: error
# One-time production promotion for v2.1. The exact commit message keeps
# normal main pushes from publishing or modifying the v2.0 release.
- name: Create GitHub Release v2.1
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v2.1' }}
uses: softprops/action-gh-release@v3
with:
tag_name: v2.1
target_commitish: ${{ github.sha }}
files: |
dist/WoW_Modernization_Tool.exe
dist/WoW_Modernization_Tool.zip
fail_on_unmatched_files: true
name: "WoW Modernization Tool v2.1"
body_path: RELEASE_NOTES.md
generate_release_notes: true
previous_tag: v2.0
make_latest: true
- name: Create GitHub Release
if: ${{ startsWith(github.ref, 'refs/tags/') }}
uses: softprops/action-gh-release@v3
with:
files: |
dist/WoW_Modernization_Tool.exe
dist/WoW_Modernization_Tool.zip
fail_on_unmatched_files: true
name: "WoW Modernization Tool ${{ github.ref_name }}"
body_path: RELEASE_NOTES.md
generate_release_notes: true
make_latest: true
retention-days: 7
+4 -1
View File
@@ -14,7 +14,10 @@ if errorlevel 1 exit /b 1
py -3.11 -m pip install --disable-pip-version-check -r requirements-build.txt
if errorlevel 1 exit /b 1
py -3.11 -m py_compile setup_tool.py setup_tool_dynamic.py remote_packages.py tests/test_safety.py
py -3.11 scripts\prepare_remote_fallbacks.py
if errorlevel 1 exit /b 1
py -3.11 -m py_compile setup_tool.py setup_tool_dynamic.py remote_packages.py tests/test_safety.py scripts\prepare_remote_fallbacks.py
if errorlevel 1 exit /b 1
py -3.11 -m unittest discover -s tests -v
+37 -25
View File
@@ -1,12 +1,12 @@
# Modernization Tool binary provenance
Audit date: 2026-08-31
Audit date: 2026-09-03
This document is the human-readable companion to
`Payload/Fallback/versions.json`, which contains machine-readable source,
version, SHA-256, and size records for many bundled components.
No remaining payload binary was modified during this provenance pass.
This provenance pass restores the previously recorded SuperWoW fallback and verifies the paired SuperAPI source revision.
## Distribution status
@@ -20,46 +20,58 @@ No remaining payload binary was modified during this provenance pass.
| VanillaMultiMonitorFix | bundled fallback + online update | Mates1500/VanillaMultiMonitorFix 0.2 | MIT verified |
| 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 |
| SuperWoW | bundled fallback + online update | balakethelock/SuperWoW release 2.2 | redistribution permission reported by the Modernization Tool maintainer on 2026-09-03 |
| SuperAPI | bundled fallback + online update | balakethelock/SuperAPI master | redistribution permission reported by the Modernization Tool maintainer on 2026-09-03 |
| 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 |
| FishPing | bundled fallback + online source | notsureawake/FishPing | project-wide license not independently located |
| NoErrorSounds | bundled fallback + online source | Macumbafeh/NoErrorSounds | project-wide license not independently located |
| Warlock Muted Demons | bundled fallback + online source | spzilyk/Warlock-Muted-Demons | project-wide license not independently located |
| WowPresence | release-build bundled fallback + online update | Dusk-92/WowPresence v1.3 | project-owned integration; see component notices |
## SuperWoW change
## SuperWoW / SuperAPI fallback
Before this audit the repository contained:
The bundled SuperWoW + SuperAPI fallback has been restored after the
Modernization Tool maintainer reported upstream redistribution permission on
2026-09-03.
The online source remains preferred:
- SuperWoW is resolved from the official stable release;
- SuperAPI follows the current upstream `master` revision and is downloaded
from the exact resolved commit SHA.
If the online installation fails and there is no complete existing install to
preserve, the tool can install the bundled known-good pair instead.
Bundled fallback provenance:
- `Payload/SuperWoWhook.dll`
- SHA-256 recorded in `versions.json`:
- SuperWoW version: 2.2
- SHA-256:
`bd214b32c878649e94ce654835946bd05e0ce7710e8f01bae10d8ab50a89351d`
- source:
`balakethelock/SuperWoW` release package
- bundled SuperAPI revision:
`901322dc88890a2ea10610b8228fb43c9c2a3610`
- bundled SuperAPI Git tree:
`95bb25752b1f31f7d4ca8d5e416986b5e3031b33`
That bundled fallback is removed by this audit because the current upstream
license restricts redistribution without express written permission.
The exact fallback metadata is also recorded in
`Payload/Fallback/versions.json`.
The installation feature itself is retained: `remote_packages.install_superwow`
already downloads the official upstream release and downloads SuperAPI directly
from its upstream repository.
## Hash manifests
## SuperAPI change
For components stored directly in the repository, the canonical
machine-readable hashes are kept in `Payload/Fallback/versions.json`.
The previous bundled fallback under
`Payload/Interface/Addons/SuperAPI/` is removed together with the SuperWoW
fallback. The installer continues to obtain SuperAPI directly from
`balakethelock/SuperAPI` when SuperWoW is installed.
## Hash manifest
For components that remain bundled, the canonical machine-readable hashes are
kept in `Payload/Fallback/versions.json`.
WowPresence is prepared as a release-build fallback by
`scripts/prepare_remote_fallbacks.py`. The script validates the release ZIP,
records its size and SHA-256, and writes
`Payload/Fallback/remote_fallbacks.json` before PyInstaller packages it into
the executable. Visual MPQ mods are intentionally not bundled as fallbacks.
When updating a binary:
+1 -1
View File
@@ -18,7 +18,7 @@ Preserved files:
- `VanillaMultiMonitorFix-MIT.txt`
- `GPL-3.0.txt` — reference copy for GPL-v3 components such as ClassicAPI and
AuctionQueryThrottle
- `SuperWoW-LICENSE.txt` — preserved for the upstream-only SuperWoW component
- `SuperWoW-LICENSE.txt` — preserved for the SuperWoW component and bundled fallback
See `../THIRD_PARTY_NOTICES.md` and `../Docs/BINARY_PROVENANCE.md` for the
scope, distribution mode, and source of each component.
+14
View File
@@ -0,0 +1,14 @@
{
"fallbacks": {
"wowpresence": {
"filename": "WowPresence.zip",
"kind": "wowpresence_zip",
"revision": "v1.3",
"sha256": "9b2dca9c69b5ab3b6dae3ef3b3efc9671779a227802ebb6ac98cb22bc6ea448a",
"size": 141423,
"source": "https://octowow.st/git/Dusk/WowPresence/releases/download/v1.3/WowPresence.zip"
}
},
"generated_for": "Modernization Tool release build",
"schema": 1
}
+102
View File
@@ -590,6 +590,108 @@
"sha256": "7b0b0fcdb8bb9e82c5a18ab395af670f57da3e03035b27d5e244e4f8b457fdf2"
}
]
},
"SuperWoW fallback": {
"version": "2.2",
"source": "https://github.com/balakethelock/SuperWoW/releases/download/Release/SuperWoW.release.2.2.zip",
"superapi_source": "https://github.com/balakethelock/SuperAPI",
"superapi_revision": "901322dc88890a2ea10610b8228fb43c9c2a3610",
"superapi_tree_sha": "95bb25752b1f31f7d4ca8d5e416986b5e3031b33",
"note": "Bundled fallback restored after redistribution permission was reported by the Modernization Tool maintainer on 2026-09-03.",
"files": [
{
"path": "Payload/SuperWoWhook.dll",
"sha256": "bd214b32c878649e94ce654835946bd05e0ce7710e8f01bae10d8ab50a89351d",
"size": 1000448
}
],
"superapi_files": [
{
"path": "Payload/Interface/Addons/SuperAPI/README.md",
"size": 1020,
"git_blob_sha1": "9f38a6a316516de3614e7526b28823cc823a4406"
},
{
"path": "Payload/Interface/Addons/SuperAPI/SuperAPI.lua",
"size": 6904,
"git_blob_sha1": "0161abda07898691885a2612e881332d783e6345"
},
{
"path": "Payload/Interface/Addons/SuperAPI/SuperAPI.toc",
"size": 741,
"git_blob_sha1": "3204821da231172087cff33848924991ced8e9bc"
},
{
"path": "Payload/Interface/Addons/SuperAPI/SuperAPIOptions.lua",
"size": 10908,
"git_blob_sha1": "3fabab494f706814b5fa02c0e0e0d1b9221d52d0"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/AceAddon-2.0/AceAddon-2.0.lua",
"size": 34783,
"git_blob_sha1": "1c5e2d59c71e09ca05e5ebe4e7f03ddd3e417f65"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/AceConsole-2.0/AceConsole-2.0.lua",
"size": 67656,
"git_blob_sha1": "d4d2f2a43aca30bdf6eb7b8266a0802e6e1fe717"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/AceDB-2.0/AceDB-2.0.lua",
"size": 45257,
"git_blob_sha1": "a6bbd10a83bce8682f563c4d0315b246ec554b9b"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/AceDebug-2.0/AceDebug-2.0.lua",
"size": 8049,
"git_blob_sha1": "8683c8c76cf91f527fb5f815e762470da96d87ca"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/AceEvent-2.0/AceEvent-2.0.lua",
"size": 28954,
"git_blob_sha1": "c6545688770d0a5669ce3948bf649fd4fdb47694"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/AceHook-2.1/AceHook-2.1.lua",
"size": 16946,
"git_blob_sha1": "8beaf6607d905b494d9d1e01785156aca86c9f43"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/AceLibrary/AceLibrary.lua",
"size": 23114,
"git_blob_sha1": "0f2b2378859a0245d33daecf112cd409594ecb3e"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/AceLocale-2.2/AceLocale-2.2.lua",
"size": 15957,
"git_blob_sha1": "fbf9c87306fd7d40d53bd8290a320e73a25e1f12"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/AceModuleCore-2.0/AceModuleCore-2.0.lua",
"size": 13981,
"git_blob_sha1": "32c89fb8535f7badc20781178073fd1626f390ec"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/AceOO-2.0/AceOO-2.0.lua",
"size": 30469,
"git_blob_sha1": "cd372b49650b1943b0ac72f0793c9b22fc1213ff"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/Dewdrop/Dewdrop-2.0.lua",
"size": 82495,
"git_blob_sha1": "bc72ba607679866c860bcdbcd3000b7faf8df3a0"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/FuBarPlugin/FuBarPlugin-2.0.lua",
"size": 44995,
"git_blob_sha1": "292c8fcdfa4aa642a159c4a17abbc8d882c2a6af"
},
{
"path": "Payload/Interface/Addons/SuperAPI/libs/Tablet/Tablet-2.0.lua",
"size": 83748,
"git_blob_sha1": "70f340d9172c30eb7a153d675ed69f1e1f07dbe4"
}
]
}
}
}
@@ -0,0 +1,22 @@
### Addon to allow compability between the mod and vanilla default interface. Also adds a minimap icon to change some mod settings. It is recommended for addon makers to read this to get a gist of some of the mod's features
### Settings
Right click the minimap icon to choose persistent options for:
- Autoloot
- Clickthrough
- GUID in combat log/events
- FoV
- Background Sound
- Uncapped Sounds
- Targetting circle style
**NOTE: If using Vanilla Tweaks quickloot all of the Autoloot options will be reversed (always on will actually be always off, Shift to toggle on will be Shift to toggle off etc).**
### Other features
- Adds "mouseover" support to all default blizzard unitframes
- Shows item charges in bag
- Allows Shift clicking a spell in your spellbook to link it to chat
- Adds chat command to toggle showing unit names or GUIDs in combat log
## Modules:
https://github.com/balakethelock/SuperAPI_Castlib provides improved castbars, alternatively the latest version of pfui/shaguplates now do the same
@@ -0,0 +1,192 @@
-- No superwow, no superapi
local version = tonumber(SUPERWOW_VERSION)
if not version or version < 2.2 then
StaticPopupDialogs["UPDATE_SUPERWOW"] = {
text = "No SuperWoW detected. Please download latest version.",
button1 = "Download",
button2 = TEXT(IGNORE),
OnAccept = function(data)
local url = "https://github.com/balakethelock/SuperWoW/releases/tag/Release"
if not ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Show()
end
ChatFrameEditBox:SetText(url)
ChatFrameEditBox:SetFocus()
ChatFrameEditBox:HighlightText()
end,
timeout = 0,
exclusive = 1,
whileDead = 1,
hideOnEscape = 1
};
DEFAULT_CHAT_FRAME:AddMessage("No SuperWoW detected. Please download latest version https://github.com/balakethelock/SuperWoW ");
StaticPopup_Show("UPDATE_SUPERWOW");
return
end
SUPERAPI_ContainerItemsTable = {}
SuperAPI = AceLibrary("AceAddon-2.0"):new("AceEvent-2.0", "AceDebug-2.0", "AceModuleCore-2.0", "AceConsole-2.0", "AceDB-2.0", "AceHook-2.1")
SuperAPI:RegisterDB("SuperAPIDB")
SuperAPI.frame = CreateFrame("Frame", "SuperAPI", UIParent)
function SuperAPI:OnEnable()
-- Let macro frame allow 511 characters
MacroFrame_LoadUI();
MacroFrameText:SetMaxLetters(511);
MACROFRAME_CHAR_LIMIT = "%d/511 Characters Used";
-- Change chat bubbles options name
OPTION_TOOLTIP_PARTY_CHAT_BUBBLES = "Shows whisper, party, raid, and battleground chat text in speech bubbles above characters' heads.";
PARTY_CHAT_BUBBLES_TEXT = "Show Whisper and Group Chat Bubbles";
SuperAPI.SetItemRefOriginal = SetItemRef
SuperAPI.SpellButton_OnClickOriginal = SpellButton_OnClick
SuperAPI.SetItemButtonCountOriginal = SetItemButtonCount
SuperAPI.SetActionOriginal = GameTooltip.SetAction
SuperAPI.UnitFrame_OnEnterOriginal = UnitFrame_OnEnter
SuperAPI.UnitFrame_OnLeaveOriginal = UnitFrame_OnLeave
SuperAPI.CombatText_AddMessageOriginal = CombatText_AddMessage
SuperAPI.QuestLogTitleButton_OnClickOriginal = QuestLogTitleButton_OnClick
-- activate hooks
SetItemRef = SuperAPI.SetItemRef
SpellButton_OnClick = SuperAPI.SpellButton_OnClick
SetItemButtonCount = SuperAPI.SetItemButtonCount
GameTooltip.SetAction = SuperAPI.SetAction
UnitFrame_OnEnter = SuperAPI.UnitFrame_OnEnter
UnitFrame_OnLeave = SuperAPI.UnitFrame_OnLeave
CombatText_AddMessage = SuperAPI.CombatText_AddMessage
QuestLogTitleButton_OnClick = SuperAPI.QuestLogTitleButton_OnClick
-- SuperAPI.frame:RegisterEvent("BAG_UPDATE")
-- SuperAPI.frame:RegisterEvent("BAG_UPDATE_COOLDOWN")
SuperAPI.frame:SetScript("OnEvent", SuperAPI.OnEvent)
-- this chatcommand is empty. It is essential for showing tooltips of macros
-- the format for showing a tooltip on a macro is EXACTLY this: /tooltip spell:spellid and then skip line
SLASH_MACROTOOLTIP1 = "/tooltip"
SlashCmdList["MACROTOOLTIP"] = function(cmd)
end
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00SuperAPI|cffffaaaa Loaded. Check the minimap icon for options.")
end
function SuperAPI:OnEvent()
if (event == "BAG_UPDATE_COOLDOWN" or event == "BAG_UPDATE") then
SUPERAPI_ContainerItemsTable = {}
for ibag = 0, 4 do
for islot = 1, GetContainerNumSlots(ibag) do
local bagitemlink = GetContainerItemLink(ibag, islot)
if bagitemlink then
local _, _, bagitemID = strfind(bagitemlink, "item:(%d+)")
bagitemID = tonumber(bagitemID)
SUPERAPI_ContainerItemsTable[bagitemID] = { bag = ibag; slot = islot }
end
end
end
end
end
-- HOOKS --
-- Global function to get a spell link from its exact id
SuperAPI.GetSpellLink = function(id)
local spellname = SpellInfo(id)
local link = "\124cffffffff\124Henchant:" .. id .. "\124h[" .. spellname .. "]\124h\124r"
return link
end
-- reformat "Enchant" itemlinks to better supported "Spell" itemlinks
SuperAPI.SetItemRef = function(link, text, button)
link = gsub(link, "spell:", "enchant:")
SuperAPI.SetItemRefOriginal(link, text, button)
end
-- hooking spellbook frame to get a spell link on shift clicking a spell's button with chatframe open
SuperAPI.SpellButton_OnClick = function(drag)
if ((not drag) and IsShiftKeyDown() and ChatFrameEditBox:IsVisible() and (not MacroFrame or not MacroFrame:IsVisible())) then
local bookId = SpellBook_GetSpellID(this:GetID());
local _, _, spellID = GetSpellName(bookId, SpellBookFrame.bookType)
local link = SuperAPI.GetSpellLink(spellID)
ChatFrameEditBox:Insert(link)
else
SuperAPI.SpellButton_OnClickOriginal(drag)
end
end
-- hooking bags item button frames to show uses count
SuperAPI.SetItemButtonCount = function(button, count)
if not button or not count then
return SuperAPI.SetItemButtonCountOriginal(button, count)
end
if (count < 0) then
if (count < -999) then
count = "*";
end
getglobal(button:GetName() .. "Count"):SetText(-count);
getglobal(button:GetName() .. "Count"):Show();
getglobal(button:GetName() .. "Count"):SetFontObject(NumberFontNormalYellow);
else
getglobal(button:GetName() .. "Count"):SetFontObject(NumberFontNormal);
SuperAPI.SetItemButtonCountOriginal(button, count)
end
end
-- hooking actionbutton tooltip to show item tooltip on macros
SuperAPI.SetAction = function(this, buttonID)
--local name, actiontype, macroID = GetActionText(buttonID)
--if actiontype == "MACRO" then
-- local _,_, body = GetMacroInfo(macroID)
-- local _,_, itemID = strfind(body, "^/tooltip item:(%d+)")
-- if itemID then
-- itemID = tonumber(itemID)
-- iteminfo = SUPERAPI_ContainerItemsTable[itemID]
-- if iteminfo then
-- return this:SetBagItem(iteminfo.bag, iteminfo.slot)
-- end
-- end
--end
--
return SuperAPI.SetActionOriginal(this, buttonID)
end
-- Add Mouseover casting to default blizzard unitframes and all unitframe addons that use the same function
SuperAPI.UnitFrame_OnEnter = function()
SuperAPI.UnitFrame_OnEnterOriginal()
SetMouseoverUnit(this.unit)
end
SuperAPI.UnitFrame_OnLeave = function()
SuperAPI.UnitFrame_OnLeaveOriginal()
SetMouseoverUnit()
end
-- Fix scrolling combat text healer name
SuperAPI.CombatText_AddMessage = function(message, scrollFunction, r, g, b, displayType, isStaggered)
local newMessage = gsub(message, "(%s%[)(0x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x)(%])", function(bracket1, hex, bracket2)
if UnitIsUnit(hex, "player") then return nil
else return " ["..UnitName(hex).."]" end
end)
return SuperAPI.CombatText_AddMessageOriginal(newMessage, scrollFunction, r, g, b, displayType, isStaggered)
end
-- Link quest in chat from Blizzard QuestLog
SuperAPI.QuestLogTitleButton_OnClick = function(button)
local questIndex = this:GetID() + FauxScrollFrame_GetOffset(QuestLogListScrollFrame);
if ( IsShiftKeyDown() ) then
if ( not this.isHeader ) then
if ( ChatFrameEditBox:IsVisible() ) then
local questLink = GetQuestLinkForLogIndex(questIndex)
ChatFrameEditBox:Insert(questLink);
return
end
end
end
return SuperAPI.QuestLogTitleButton_OnClickOriginal(button)
end
@@ -0,0 +1,25 @@
## Interface: 11200
## Title: SuperAPI |cFFFF8080-balake-|r
## Notes: the companion compatibility addon to the mod of the same name.
## Author: balake
## SavedVariablesPerCharacter: SuperAPIDB
## X-Embeds: Ace2, FuBarPlugin-2.0, CompostLib, DewDropLib, TabletLib
libs\AceLibrary\AceLibrary.lua
libs\AceOO-2.0\AceOO-2.0.lua
libs\AceAddon-2.0\AceAddon-2.0.lua
libs\AceConsole-2.0\AceConsole-2.0.lua
libs\AceHook-2.1\AceHook-2.1.lua
libs\AceDB-2.0\AceDB-2.0.lua
libs\AceDebug-2.0\AceDebug-2.0.lua
libs\AceEvent-2.0\AceEvent-2.0.lua
libs\AceLocale-2.2\AceLocale-2.2.lua
libs\AceModuleCore-2.0\AceModuleCore-2.0.lua
libs\Dewdrop\Dewdrop-2.0.lua
libs\Tablet\Tablet-2.0.lua
libs\FuBarPlugin\FuBarPlugin-2.0.lua
SuperAPI.lua
SuperAPIOptions.lua
@@ -0,0 +1,416 @@
-- No superwow, no superapi
if not SuperAPI then
return
end
SuperAPI.AUTOLOOT_OPTIONS = {
"Always on",
"Always off",
"Shift to toggle on",
"Shift to toggle off",
}
SuperAPI.SELECTION_CIRCLE_STYLE = {
"Default - incomplete circle",
"Full circle (must download texture)",
"Full circle with arrow for facing direction (must download texture)",
"Classic incomplete circle oriented in facing direction",
}
SuperAPI.NAMEPLATE_MOTION = {
"Overlap",
"Default spread",
"Smart spread",
"Compact spread",
}
SuperAPI:RegisterDefaults("profile", {
autoloot = SuperAPI.AUTOLOOT_OPTIONS[3],
clickthrough = false,
})
SuperAPI.IfShiftAutoloot = function()
if IsShiftKeyDown() then
SetAutoloot(1)
else
SetAutoloot(0)
end
end
SuperAPI.IfShiftNoAutoloot = function()
if IsShiftKeyDown() then
SetAutoloot(0)
else
SetAutoloot(1)
end
end
SuperAPI.cmdtable = {
type = "group",
handler = SuperAPI,
args = {
autoloot = {
type = "text",
name = "Autoloot (Read tooltip)",
desc = "Specifies autoloot behavior. If using Vanilla Tweaks quickloot all of these will be reversed (always on will actually be always off, Shift to toggle on will be Shift to toggle off etc).",
order = 10,
validate = SuperAPI.AUTOLOOT_OPTIONS,
get = function()
return SuperAPI.db.profile.autoloot
end,
set = function(v)
SuperAPI.db.profile.autoloot = v
if v == SuperAPI.AUTOLOOT_OPTIONS[1] then
-- "Always on"
SetAutoloot(1)
SuperAPI.frame:SetScript("OnUpdate", nil)
elseif v == SuperAPI.AUTOLOOT_OPTIONS[2] then
-- "Always off"
SetAutoloot(0)
SuperAPI.frame:SetScript("OnUpdate", nil)
elseif v == SuperAPI.AUTOLOOT_OPTIONS[3] then
-- "Shift to toggle on"
SetAutoloot(0)
SuperAPI.frame:SetScript("OnUpdate", SuperAPI.IfShiftAutoloot)
elseif v == SuperAPI.AUTOLOOT_OPTIONS[4] then
-- "Shift to toggle off"
SetAutoloot(1)
SuperAPI.frame:SetScript("OnUpdate", SuperAPI.IfShiftNoAutoloot)
end
end,
},
clickthrough = {
type = "toggle",
name = "Clickthrough corpses",
desc = "Allows you to click through corpses to loot corpses underneath them.",
order = 20,
get = function()
return Clickthrough() == 1
end,
set = function(v)
if v == true then
Clickthrough(1)
else
Clickthrough(0)
end
SuperAPI.db.profile.clickthrough = v
end,
},
fov = {
type = "range",
name = "Field of view",
desc = "Changes the field of view of the game. Requires UI Reload.",
order = 30,
min = 0.1,
max = 3.14,
step = 0.05,
get = function()
return GetCVar("FoV")
end,
set = function(v)
SetCVar("FoV", v)
end,
},
selectioncircle = {
type = "text",
name = "Selection circle style",
desc = "Changes the style of the selection circle.",
order = 40,
validate = SuperAPI.SELECTION_CIRCLE_STYLE,
get = function()
local selectioncircle = GetCVar("SelectionCircleStyle")
if selectioncircle then
return SuperAPI.SELECTION_CIRCLE_STYLE[tonumber(selectioncircle)]
end
end,
set = function(v)
if v == SuperAPI.SELECTION_CIRCLE_STYLE[1] then
SetCVar("SelectionCircleStyle", "1")
elseif v == SuperAPI.SELECTION_CIRCLE_STYLE[2] then
SetCVar("SelectionCircleStyle", "2")
elseif v == SuperAPI.SELECTION_CIRCLE_STYLE[3] then
SetCVar("SelectionCircleStyle", "3")
elseif v == SuperAPI.SELECTION_CIRCLE_STYLE[4] then
SetCVar("SelectionCircleStyle", "4")
end
end,
},
backgroundsound = {
type = "toggle",
name = "Background sound",
desc = "Allows game sound to play even when the window is in the background.",
order = 60,
get = function()
return GetCVar("BackgroundSound") == "1"
end,
set = function(v)
if v == true then
SetCVar("BackgroundSound", "1")
else
SetCVar("BackgroundSound", "0")
end
end,
},
uncappedsounds = {
type = "toggle",
name = "Uncapped sounds",
desc = "Allows more game sounds to play at the same time by removing hardcoded limit. This will also set SoundSoftwareChannels and SoundMaxHardwareChannels to 64. If you experience any weird crashes you may want to turn this off.",
order = 70,
get = function()
return GetCVar("UncapSounds") == "1"
end,
set = function(v)
if v == true then
SetCVar("UncapSounds", "1")
SetCVar("SoundSoftwareChannels", "64")
SetCVar("SoundMaxHardwareChannels", "64")
else
SetCVar("UncapSounds", "0")
SetCVar("SoundSoftwareChannels", "12")
SetCVar("SoundMaxHardwareChannels", "12")
end
end,
},
lootsparkle = {
type = "toggle",
name = "Loot Sparkle",
desc = "Toggle loot sparkle effect on lootable treasure.",
order = 80,
get = function()
return GetCVar("LootSparkle") == "1"
end,
set = function(v)
if v == true then
SetCVar("LootSparkle", "1")
else
SetCVar("LootSparkle", "0")
end
end,
},
healingtext = {
type = "toggle",
name = "Floating Healing Text",
desc = "Toggle display of in-world healing feedback.",
order = 85,
get = function()
return GetCVar("HealingText") == "1"
end,
set = function(v)
if v == true then
SetCVar("HealingText", "1")
else
SetCVar("HealingText", "0")
end
end,
},
nameplates = {
type = "group",
name = "Nameplate Settings",
desc = "Group of settings related to Nameplates",
order = 90,
args = {
nameplaterange = {
type = "range",
name = "Nameplate range",
desc = "Changes the range at which Nameplates appear.",
order = 1,
min = 10,
max = 80,
step = 1,
get = function()
return GetCVar("NameplateRange")
end,
set = function(v)
SetCVar("NameplateRange", v)
end,
},
nameplatemotion = {
type = "text",
name = "Nameplate Motion",
desc = "Changes the behavior of moving nameplates.",
order = 2,
validate = SuperAPI.NAMEPLATE_MOTION,
get = function()
local nameplatemotionSetting = GetCVar("NameplateMotion")
if nameplatemotionSetting then
return SuperAPI.NAMEPLATE_MOTION[(tonumber(nameplatemotionSetting))+1]
end
end,
set = function(v)
if v == SuperAPI.NAMEPLATE_MOTION[1] then
SetCVar("NameplateMotion", "0")
elseif v == SuperAPI.NAMEPLATE_MOTION[2] then
SetCVar("NameplateMotion", "1")
elseif v == SuperAPI.NAMEPLATE_MOTION[3] then
SetCVar("NameplateMotion", "2")
elseif v == SuperAPI.NAMEPLATE_MOTION[4] then
SetCVar("NameplateMotion", "3")
end
end,
},
}
},
chatbubbles = {
type = "group",
name = "Chat Bubble Settings",
desc = "Group of settings related to Chat Bubbles that appear above units",
order = 100,
args = {
chatbubblerange = {
type = "range",
name = "Chat Bubbles range",
desc = "Changes the range at which Chat Bubbles appear.",
order = 1007,
min = 10,
max = 200,
step = 5,
get = function()
return GetCVar("ChatBubbleRange")
end,
set = function(v)
SetCVar("ChatBubbleRange", v)
end,
},
togglesay = {
type = "toggle",
name = "Say Chat Bubbles",
desc = "Toggle say and yell chat bubbles on/off",
order = 1009,
get = function()
return GetCVar("ChatBubbles") == "1"
end,
set = function(v)
if v == true then
SetCVar("ChatBubbles", "1")
else
SetCVar("ChatBubbles", "0")
end
end,
},
togglepartybubbles = {
type = "toggle",
name = "Party Chat Bubbles",
desc = "Toggle party chat bubbles on/off",
order = 1008,
get = function()
return GetCVar("ChatBubblesParty") == "1"
end,
set = function(v)
if v == true then
SetCVar("ChatBubblesParty", "1")
else
SetCVar("ChatBubblesParty", "0")
end
end,
},
toggleraidbubbles = {
type = "toggle",
name = "Raid Chat Bubbles",
desc = "Toggle raid chat bubbles on/off",
order = 1010,
get = function()
return GetCVar("ChatBubblesRaid") == "1"
end,
set = function(v)
if v == true then
SetCVar("ChatBubblesRaid", "1")
else
SetCVar("ChatBubblesRaid", "0")
end
end,
},
togglebgbubbles = {
type = "toggle",
name = "Battleground Chat Bubbles",
desc = "Toggle battleground chat bubbles on/off",
order = 1020,
get = function()
return GetCVar("ChatBubblesBattleground") == "1"
end,
set = function(v)
if v == true then
SetCVar("ChatBubblesBattleground", "1")
else
SetCVar("ChatBubblesBattleground", "0")
end
end,
},
togglewhisperbubbles = {
type = "toggle",
name = "Whisper Chat Bubbles",
desc = "Toggle whisper chat bubbles on/off",
order = 1030,
get = function()
return GetCVar("ChatBubblesWhisper") == "1"
end,
set = function(v)
if v == true then
SetCVar("ChatBubblesWhisper", "1")
else
SetCVar("ChatBubblesWhisper", "0")
end
end,
},
togglecreaturebubbles = {
type = "toggle",
name = "Creature Chat Bubbles",
desc = "Toggle chat bubbles on creatures on/off",
order = 1040,
get = function()
return GetCVar("ChatBubblesCreatures") == "1"
end,
set = function(v)
if v == true then
SetCVar("ChatBubblesCreatures", "1")
else
SetCVar("ChatBubblesCreatures", "0")
end
end,
},
}
},
superwowLink = {
type = "execute",
name = "Support The Project",
desc = "All your donations are deeply appreciated! The community support is what keeps SuperWoW going",
func = function()
local url = "https://ko-fi.com/balakesuperwow"
if not ChatFrameEditBox:IsVisible() then
ChatFrameEditBox:Show()
end
ChatFrameEditBox:SetText(url)
ChatFrameEditBox:SetFocus()
ChatFrameEditBox:HighlightText()
end,
},
}
}
local deuce = SuperAPI:NewModule("SuperAPI Options Menu")
deuce.hasFuBar = IsAddOnLoaded("FuBar") and FuBar
deuce.consoleCmd = not deuce.hasFuBar
SuperAPIOptions = AceLibrary("AceAddon-2.0"):new("AceDB-2.0", "FuBarPlugin-2.0")
SuperAPIOptions.name = "FuBar - SuperAPI"
SuperAPIOptions:RegisterDB("SuperAPIDB")
SuperAPIOptions.hasIcon = "Interface\\Icons\\inv_misc_book_06"
SuperAPIOptions.defaultMinimapPosition = 180
SuperAPIOptions.independentProfile = true
SuperAPIOptions.hideWithoutStandby = false
SuperAPIOptions.OnMenuRequest = SuperAPI.cmdtable
local args = AceLibrary("FuBarPlugin-2.0"):GetAceOptionsDataTable(SuperAPIOptions)
for k, v in pairs(args) do
if SuperAPIOptions.OnMenuRequest.args[k] == nil then
SuperAPIOptions.OnMenuRequest.args[k] = v
end
end
function SuperAPIOptions:OnEnable()
-- activate saved settings
SuperAPI.cmdtable.args.autoloot.set(SuperAPI.db.profile.autoloot)
SuperAPI.cmdtable.args.clickthrough.set(SuperAPI.db.profile.clickthrough)
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,245 @@
--[[
Name: AceDebug-2.0
Revision: $Rev: 17638 $
Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team)
Inspired By: Ace 1.x by Turan (turan@gryphon.com)
Website: http://www.wowace.com/
Documentation: http://www.wowace.com/index.php/AceDebug-2.0
SVN: http://svn.wowace.com/root/trunk/Ace2/AceDebug-2.0
Description: Mixin to allow for simple debugging capabilities.
Dependencies: AceLibrary, AceOO-2.0
]]
local MAJOR_VERSION = "AceDebug-2.0"
local MINOR_VERSION = "$Revision: 17639 $"
if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end
if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end
if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check
if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end
local DEBUGGING, TOGGLE_DEBUGGING
if GetLocale() == "frFR" then
DEBUGGING = "D\195\169boguage"
TOGGLE_DEBUGGING = "Activer/d\195\169sactiver le d\195\169boguage"
elseif GetLocale() == "esES" then
DEBUGGING = "Depurar"
TOGGLE_DEBUGGING = "Activar/desactivar depurar"
elseif GetLocale() == "deDE" then
DEBUGGING = "Debuggen"
TOGGLE_DEBUGGING = "Aktiviert/Deaktiviert Debugging"
elseif GetLocale() == "koKR" then
DEBUGGING = "디버깅"
TOGGLE_DEBUGGING = "디버깅 기능 사용함/사용안함"
elseif GetLocale() == "zhTW" then
DEBUGGING = "除錯"
TOGGLE_DEBUGGING = "啟用/停用除錯功能"
elseif GetLocale() == "zhCN" then
DEBUGGING = "\232\176\131\232\175\149"
TOGGLE_DEBUGGING = "\229\144\175\231\148\168/\231\166\129\231\148\168 \232\176\131\232\175\149"
elseif GetLocale() == "ruRU" then
DEBUGGING = "Отладка"
TOGGLE_DEBUGGING = "Вкл/Выкл отладку для этого аддона."
else -- enUS
DEBUGGING = "Debugging"
TOGGLE_DEBUGGING = "Enable/disable debugging"
end
local table_setn
do
local version = GetBuildInfo()
if string.find(version, "^2%.") then
-- 2.0.0
table_setn = function() end
else
table_setn = table.setn
end
end
local math_mod = math.mod or math.fmod
local AceOO = AceLibrary:GetInstance("AceOO-2.0")
local AceDebug = AceOO.Mixin {"Debug", "CustomDebug", "IsDebugging", "SetDebugging", "SetDebugLevel", "LevelDebug", "CustomLevelDebug", "GetDebugLevel"}
local function print(text, r, g, b, frame, delay)
(frame or DEFAULT_CHAT_FRAME):AddMessage(text, r, g, b, 1, delay or 5)
end
local tmp
function AceDebug:CustomDebug(r, g, b, frame, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if not self.debugging then return end
local output = string.format("|cff7fff7f(DEBUG) %s:[%s%3d]|r", tostring(self), date("%H:%M:%S"), math_mod(GetTime(), 1) * 1000)
if string.find(tostring(a1), "%%") then
output = output .. " " .. string.format(tostring(a1), tostring(a2), tostring(a3), tostring(a4), tostring(a5), tostring(a6), tostring(a7), tostring(a8), tostring(a9), tostring(a10), tostring(a11), tostring(a12), tostring(a13), tostring(a14), tostring(a15), tostring(a16), tostring(a17), tostring(a18), tostring(a19), tostring(a20))
else
if not tmp then
tmp = {}
end
-- This block dynamically rebuilds the tmp array stopping on the first nil.
table.insert(tmp, output)
table.insert(tmp, tostring(a1))
table.insert(tmp, a2)
table.insert(tmp, a3)
table.insert(tmp, a4)
table.insert(tmp, a5)
table.insert(tmp, a6)
table.insert(tmp, a7)
table.insert(tmp, a8)
table.insert(tmp, a9)
table.insert(tmp, a10)
table.insert(tmp, a11)
table.insert(tmp, a12)
table.insert(tmp, a13)
table.insert(tmp, a14)
table.insert(tmp, a15)
table.insert(tmp, a16)
table.insert(tmp, a17)
table.insert(tmp, a18)
table.insert(tmp, a19)
table.insert(tmp, a20)
while tmp[table.getn(tmp)] == nil do
table.remove(tmp)
end
for k = 1, table.getn(tmp) do
tmp[k] = tostring(tmp[k])
end
output = table.concat(tmp, " ")
for k,v in pairs(tmp) do
tmp[k] = nil
end
table_setn(tmp, 0)
end
print(output, r, g, b, frame or self.debugFrame, delay)
end
function AceDebug:Debug(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
AceDebug.CustomDebug(self, nil, nil, nil, nil, nil, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
end
function AceDebug:IsDebugging()
return self.debugging
end
function AceDebug:SetDebugging(debugging)
self.debugging = debugging
end
-- Takes a number 1-3
-- Level 1: Critical messages that every user should receive
-- Level 2: Should be used for local debugging (function calls, etc)
-- Level 3: Very verbose debugging, will dump everything and anything
-- If set to nil, you will receive no debug information
function AceDebug:SetDebugLevel(level)
AceDebug:argCheck(level, 1, "number", "nil")
if not level then
self.debuglevel = nil
return
end
if level < 1 or level > 3 then
AceDebug:error("Bad argument #1 to `SetDebugLevel`, must be a number 1-3")
end
self.debuglevel = level
end
function AceDebug:GetDebugLevel()
return self.debuglevel
end
function AceDebug:CustomLevelDebug(level, r, g, b, frame, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if not self.debugging or not self.debuglevel then return end
AceDebug:argCheck(level, 1, "number")
if level < 1 or level > 3 then
AceDebug:error("Bad argument #1 to `LevelDebug`, must be a number 1-3")
end
if level > self.debuglevel then return end
local output = string.format("|cff7fff7f(DEBUG) %s:[%s.%3d]|r", tostring(self), date("%H:%M:%S"), math_mod(GetTime(), 1) * 1000)
if string.find(tostring(a1), "%%") then
output = output .. " " .. string.format(tostring(a1), tostring(a2), tostring(a3), tostring(a4), tostring(a5), tostring(a6), tostring(a7), tostring(a8), tostring(a9), tostring(a10), tostring(a11), tostring(a12), tostring(a13), tostring(a14), tostring(a15), tostring(a16), tostring(a17), tostring(a18), tostring(a19), tostring(a20))
else
if not tmp then
tmp = {}
end
-- This block dynamically rebuilds the tmp array stopping on the first nil.
table.insert(tmp, output)
table.insert(tmp, tostring(a1))
table.insert(tmp, a2)
table.insert(tmp, a3)
table.insert(tmp, a4)
table.insert(tmp, a5)
table.insert(tmp, a6)
table.insert(tmp, a7)
table.insert(tmp, a8)
table.insert(tmp, a9)
table.insert(tmp, a10)
table.insert(tmp, a11)
table.insert(tmp, a12)
table.insert(tmp, a13)
table.insert(tmp, a14)
table.insert(tmp, a15)
table.insert(tmp, a16)
table.insert(tmp, a17)
table.insert(tmp, a18)
table.insert(tmp, a19)
table.insert(tmp, a20)
while tmp[table.getn(tmp)] == nil do
table.remove(tmp)
end
for k = 1, table.getn(tmp) do
tmp[k] = tostring(tmp[k])
end
output = table.concat(tmp, " ")
for k,v in pairs(tmp) do
tmp[k] = nil
end
table_setn(tmp, 0)
end
print(output, r, g, b, frame or self.debugFrame, delay)
end
function AceDebug:LevelDebug(level, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if not self.debugging or not self.debuglevel then return end
AceDebug:argCheck(level, 1, "number")
if level < 1 or level > 3 then
AceDebug:error("Bad argument #1 to `LevelDebug`, must be a number 1-3")
end
if level > self.debuglevel then return end
AceDebug.CustomLevelDebug(self, level, nil, nil, nil, nil, nil, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
end
local options
function AceDebug:GetAceOptionsDataTable(target)
if not options then
options = {
debug = {
name = DEBUGGING,
desc = TOGGLE_DEBUGGING,
type = "toggle",
get = "IsDebugging",
set = "SetDebugging",
order = -2,
}
}
end
return options
end
AceLibrary:Register(AceDebug, MAJOR_VERSION, MINOR_VERSION, AceDebug.activate)
AceDebug = AceLibrary(MAJOR_VERSION)
@@ -0,0 +1,973 @@
--[[
Name: AceEvent-2.0
Revision: $Rev: 17803 $
Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team)
Inspired By: Ace 1.x by Turan (turan@gryphon.com)
Website: http://www.wowace.com/
Documentation: http://www.wowace.com/index.php/AceEvent-2.0
SVN: http://svn.wowace.com/root/trunk/Ace2/AceEvent-2.0
Description: Mixin to allow for event handling, scheduling, and inter-addon
communication.
Dependencies: AceLibrary, AceOO-2.0
]]
local MAJOR_VERSION = "AceEvent-2.0"
local MINOR_VERSION = "$Revision: 17803 $"
if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end
if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end
if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check
if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end
local AceOO = AceLibrary:GetInstance("AceOO-2.0")
local Mixin = AceOO.Mixin
local AceEvent = Mixin {
"RegisterEvent",
"RegisterAllEvents",
"UnregisterEvent",
"UnregisterAllEvents",
"TriggerEvent",
"ScheduleEvent",
"ScheduleRepeatingEvent",
"CancelScheduledEvent",
"CancelAllScheduledEvents",
"IsEventRegistered",
"IsEventScheduled",
"RegisterBucketEvent",
"UnregisterBucketEvent",
"UnregisterAllBucketEvents",
"IsBucketEventRegistered",
}
local table_setn
do
local version = GetBuildInfo()
if string.find(version, "^2%.") then
-- 2.0.0
table_setn = function() end
else
table_setn = table.setn
end
end
local weakKey = {__mode="k"}
local new, del
do
local list = setmetatable({}, weakKey)
function new()
local t = next(list)
if t then
list[t] = nil
return t
else
return {}
end
end
function del(t)
setmetatable(t, nil)
for k in pairs(t) do
t[k] = nil
end
list[t] = true
end
end
local FAKE_NIL
local RATE
local eventsWhichHappenOnce = {
PLAYER_LOGIN = true,
AceEvent_FullyInitialized = true,
VARIABLES_LOADED = true,
PLAYER_LOGOUT = true,
}
local registeringFromAceEvent
function AceEvent:RegisterEvent(event, method, once)
AceEvent:argCheck(event, 2, "string")
if self == AceEvent and not registeringFromAceEvent then
AceEvent:argCheck(method, 3, "function")
self = method
else
AceEvent:argCheck(method, 3, "string", "function", "nil", "boolean", "number")
if type(method) == "boolean" or type(method) == "number" then
AceEvent:argCheck(once, 4, "nil")
once, method = method, event
end
end
AceEvent:argCheck(once, 4, "number", "boolean", "nil")
if eventsWhichHappenOnce[event] then
once = true
end
local throttleRate
if type(once) == "number" then
throttleRate, once = once
end
if not method then
method = event
end
if type(method) == "string" and type(self[method]) ~= "function" then
AceEvent:error("Cannot register event %q to method %q, it does not exist", event, method)
else
assert(type(method) == "function" or type(method) == "string")
end
local AceEvent_registry = AceEvent.registry
if not AceEvent_registry[event] then
AceEvent_registry[event] = new()
AceEvent.frame:RegisterEvent(event)
end
local remember = true
if AceEvent_registry[event][self] then
remember = false
end
AceEvent_registry[event][self] = method
local AceEvent_onceRegistry = AceEvent.onceRegistry
if once then
if not AceEvent_onceRegistry then
AceEvent.onceRegistry = new()
AceEvent_onceRegistry = AceEvent.onceRegistry
end
if not AceEvent_onceRegistry[event] then
AceEvent_onceRegistry[event] = new()
end
AceEvent_onceRegistry[event][self] = true
else
if AceEvent_onceRegistry and AceEvent_onceRegistry[event] then
AceEvent_onceRegistry[event][self] = nil
if not next(AceEvent_onceRegistry[event]) then
AceEvent_onceRegistry[event] = del(AceEvent_onceRegistry[event])
end
end
end
local AceEvent_throttleRegistry = AceEvent.throttleRegistry
if throttleRate then
if not AceEvent_throttleRegistry then
AceEvent.throttleRegistry = new()
AceEvent_throttleRegistry = AceEvent.throttleRegistry
end
if not AceEvent_throttleRegistry[event] then
AceEvent_throttleRegistry[event] = new()
end
if AceEvent_throttleRegistry[event][self] then
AceEvent_throttleRegistry[event][self] = del(AceEvent_throttleRegistry[event][self])
end
AceEvent_throttleRegistry[event][self] = setmetatable(new(), weakKey)
local t = AceEvent_throttleRegistry[event][self]
t[RATE] = throttleRate
else
if AceEvent_throttleRegistry and AceEvent_throttleRegistry[event] then
if AceEvent_throttleRegistry[event][self] then
AceEvent_throttleRegistry[event][self] = del(AceEvent_throttleRegistry[event][self])
end
if not next(AceEvent_throttleRegistry[event]) then
AceEvent_throttleRegistry[event] = del(AceEvent_throttleRegistry[event])
end
end
end
if remember then
AceEvent:TriggerEvent("AceEvent_EventRegistered", self, event)
end
end
local ALL_EVENTS
function AceEvent:RegisterAllEvents(method)
if self == AceEvent then
AceEvent:argCheck(method, 1, "function")
self = method
else
AceEvent:argCheck(method, 1, "string", "function")
if type(method) == "string" and type(self[method]) ~= "function" then
AceEvent:error("Cannot register all events to method %q, it does not exist", method)
end
end
local AceEvent_registry = AceEvent.registry
if not AceEvent_registry[ALL_EVENTS] then
AceEvent_registry[ALL_EVENTS] = new()
AceEvent.frame:RegisterAllEvents()
end
AceEvent_registry[ALL_EVENTS][self] = method
end
local _G = getfenv(0)
local memstack, timestack = {}, {}
local memdiff, timediff
function AceEvent:TriggerEvent(event, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
AceEvent:argCheck(event, 2, "string")
local AceEvent_registry = AceEvent.registry
if (not AceEvent_registry[event] or not next(AceEvent_registry[event])) and (not AceEvent_registry[ALL_EVENTS] or not next(AceEvent_registry[ALL_EVENTS])) then
return
end
local _G_event = _G.event
_G.event = event
local lastEvent = AceEvent.currentEvent
AceEvent.currentEvent = event
local AceEvent_onceRegistry = AceEvent.onceRegistry
local AceEvent_debugTable = AceEvent.debugTable
if AceEvent_onceRegistry and AceEvent_onceRegistry[event] then
local tmp = new()
for obj, method in pairs(AceEvent_onceRegistry[event]) do
tmp[obj] = AceEvent_registry[event] and AceEvent_registry[event][obj] or nil
end
local obj = next(tmp)
while obj do
local mem, time
if AceEvent_debugTable then
if not AceEvent_debugTable[event] then
AceEvent_debugTable[event] = new()
end
if not AceEvent_debugTable[event][obj] then
AceEvent_debugTable[event][obj] = new()
AceEvent_debugTable[event][obj].mem = 0
AceEvent_debugTable[event][obj].time = 0
AceEvent_debugTable[event][obj].count = 0
end
if memdiff then
table.insert(memstack, memdiff)
table.insert(timestack, timediff)
end
memdiff, timediff = 0, 0
mem, time = gcinfo(), GetTime()
end
local method = tmp[obj]
AceEvent.UnregisterEvent(obj, event)
if type(method) == "string" then
local obj_method = obj[method]
if obj_method then
obj_method(obj, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
end
elseif method then -- function
method(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
end
if AceEvent_debugTable then
local dmem, dtime = memdiff, timediff
mem, time = gcinfo() - mem - memdiff, GetTime() - time - timediff
AceEvent_debugTable[event][obj].mem = AceEvent_debugTable[event][obj].mem + mem
AceEvent_debugTable[event][obj].time = AceEvent_debugTable[event][obj].time + time
AceEvent_debugTable[event][obj].count = AceEvent_debugTable[event][obj].count + 1
memdiff, timediff = table.remove(memstack), table.remove(timestack)
if memdiff then
memdiff = memdiff + mem + dmem
timediff = timediff + time + dtime
end
end
tmp[obj] = nil
obj = next(tmp)
end
del(tmp)
end
local AceEvent_throttleRegistry = AceEvent.throttleRegistry
local throttleTable = AceEvent_throttleRegistry and AceEvent_throttleRegistry[event]
if AceEvent_registry[event] then
local tmp = new()
for obj, method in pairs(AceEvent_registry[event]) do
tmp[obj] = method
end
local obj = next(tmp)
while obj do
local method = tmp[obj]
local continue = false
if throttleTable and throttleTable[obj] then
local a1 = a1
if a1 == nil then
a1 = FAKE_NIL
end
if not throttleTable[obj][a1] or GetTime() - throttleTable[obj][a1] >= throttleTable[obj][RATE] then
throttleTable[obj][a1] = GetTime()
else
continue = true
end
end
if not continue then
local mem, time
if AceEvent_debugTable then
if not AceEvent_debugTable[event] then
AceEvent_debugTable[event] = new()
end
if not AceEvent_debugTable[event][obj] then
AceEvent_debugTable[event][obj] = new()
AceEvent_debugTable[event][obj].mem = 0
AceEvent_debugTable[event][obj].time = 0
AceEvent_debugTable[event][obj].count = 0
end
if memdiff then
table.insert(memstack, memdiff)
table.insert(timestack, timediff)
end
memdiff, timediff = 0, 0
mem, time = gcinfo(), GetTime()
end
if type(method) == "string" then
local obj_method = obj[method]
if obj_method then
obj_method(obj, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
end
elseif method then -- function
method(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
end
if AceEvent_debugTable then
local dmem, dtime = memdiff, timediff
mem, time = gcinfo() - mem - memdiff, GetTime() - time - timediff
AceEvent_debugTable[event][obj].mem = AceEvent_debugTable[event][obj].mem + mem
AceEvent_debugTable[event][obj].time = AceEvent_debugTable[event][obj].time + time
AceEvent_debugTable[event][obj].count = AceEvent_debugTable[event][obj].count + 1
memdiff, timediff = table.remove(memstack), table.remove(timestack)
if memdiff then
memdiff = memdiff + mem + dmem
timediff = timediff + time + dtime
end
end
end
tmp[obj] = nil
obj = next(tmp)
end
del(tmp)
end
if AceEvent_registry[ALL_EVENTS] then
local tmp = new()
for obj, method in pairs(AceEvent_registry[ALL_EVENTS]) do
tmp[obj] = method
end
local obj = next(tmp)
while obj do
local method = tmp[obj]
local mem, time
if AceEvent_debugTable then
if not AceEvent_debugTable[event] then
AceEvent_debugTable[event] = new()
end
if not AceEvent_debugTable[event][obj] then
AceEvent_debugTable[event][obj] = new()
AceEvent_debugTable[event][obj].mem = 0
AceEvent_debugTable[event][obj].time = 0
AceEvent_debugTable[event][obj].count = 0
end
if memdiff then
table.insert(memstack, memdiff)
table.insert(timestack, timediff)
end
memdiff, timediff = 0, 0
mem, time = gcinfo(), GetTime()
end
if type(method) == "string" then
local obj_method = obj[method]
if obj_method then
obj_method(obj, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
end
elseif method then -- function
method(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
end
if AceEvent_debugTable then
local dmem, dtime = memdiff, timediff
mem, time = gcinfo() - mem - memdiff, GetTime() - time - timediff
AceEvent_debugTable[event][obj].mem = AceEvent_debugTable[event][obj].mem + mem
AceEvent_debugTable[event][obj].time = AceEvent_debugTable[event][obj].time + time
AceEvent_debugTable[event][obj].count = AceEvent_debugTable[event][obj].count + 1
memdiff, timediff = table.remove(memstack), table.remove(timestack)
if memdiff then
memdiff = memdiff + mem + dmem
timediff = timediff + time + dtime
end
end
tmp[obj] = nil
obj = next(tmp)
end
del(tmp)
end
_G.event = _G_event
AceEvent.currentEvent = lastEvent
end
-- local accessors
local getn = table.getn
local tinsert = table.insert
local tremove = table.remove
local floor = math.floor
local GetTime = GetTime
local next = next
local pairs = pairs
local unpack = unpack
local delayRegistry
local tmp = {}
local function OnUpdate()
local t = GetTime()
for k,v in pairs(delayRegistry) do
tmp[k] = true
end
for k in pairs(tmp) do
local v = delayRegistry[k]
if v then
local v_time = v.time
if not v_time then
delayRegistry[k] = del(v)
elseif v_time <= t then
local v_repeatDelay = v.repeatDelay
if v_repeatDelay then
-- use the event time, not the current time, else timing inaccuracies add up over time
v.time = v_time + v_repeatDelay
end
local event = v.event
local mem, time
if AceEvent_debugTable then
mem, time = gcinfo(), GetTime()
end
if type(event) == "function" then
event(unpack(v))
else
AceEvent:TriggerEvent(event, unpack(v))
end
if AceEvent_debugTable then
mem, time = gcinfo() - mem, GetTime() - time
v.mem = v.mem + mem
v.timeSpent = v.timeSpent + time
v.count = v.count + 1
end
if not v_repeatDelay then
local x = delayRegistry[k]
if x and x.time == v_time then -- check if it was manually reset
delayRegistry[k] = del(v)
end
end
end
end
end
for k in pairs(tmp) do
tmp[k] = nil
end
if not next(delayRegistry) then
AceEvent.frame:Hide()
end
end
local function ScheduleEvent(self, repeating, event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
local id
if type(event) == "string" or type(event) == "table" then
if type(event) == "table" then
if not delayRegistry or not delayRegistry[event] then
AceEvent:error("Bad argument #2 to `ScheduleEvent'. Improper id table fed in.")
end
end
if type(delay) ~= "number" then
id, event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20 = event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20
AceEvent:argCheck(event, 3, "string", "function", --[[ so message is right ]] "number")
AceEvent:argCheck(delay, 4, "number")
self:CancelScheduledEvent(id)
end
else
AceEvent:argCheck(event, 2, "string", "function")
AceEvent:argCheck(delay, 3, "number")
end
if not delayRegistry then
AceEvent.delayRegistry = new()
delayRegistry = AceEvent.delayRegistry
AceEvent.frame:SetScript("OnUpdate", OnUpdate)
end
local t
if type(id) == "table" then
for k in pairs(id) do
id[k] = nil
end
t = id
else
t = new()
end
t[1] = a1
t[2] = a2
t[3] = a3
t[4] = a4
t[5] = a5
t[6] = a6
t[7] = a7
t[8] = a8
t[9] = a9
t[10] = a10
t[11] = a11
t[12] = a12
t[13] = a13
t[14] = a14
t[15] = a15
t[16] = a16
t[17] = a17
t[18] = a18
t[19] = a19
t[20] = a20
table_setn(t, 20)
t.event = event
t.time = GetTime() + delay
t.self = self
t.id = id or t
t.repeatDelay = repeating and delay
if AceEvent_debugTable then
t.mem = 0
t.count = 0
t.timeSpent = 0
end
delayRegistry[t.id] = t
AceEvent.frame:Show()
return t.id
end
function AceEvent:ScheduleEvent(event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if type(event) == "string" or type(event) == "table" then
if type(event) == "table" then
if not delayRegistry or not delayRegistry[event] then
AceEvent:error("Bad argument #2 to `ScheduleEvent'. Improper id table fed in.")
end
end
if type(delay) ~= "number" then
AceEvent:argCheck(delay, 3, "string", "function", --[[ so message is right ]] "number")
AceEvent:argCheck(a1, 4, "number")
end
else
AceEvent:argCheck(event, 2, "string", "function")
AceEvent:argCheck(delay, 3, "number")
end
return ScheduleEvent(self, false, event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
end
function AceEvent:ScheduleRepeatingEvent(event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if type(event) == "string" or type(event) == "table" then
if type(event) == "table" then
if not delayRegistry or not delayRegistry[event] then
AceEvent:error("Bad argument #2 to `ScheduleEvent'. Improper id table fed in.")
end
end
if type(delay) ~= "number" then
AceEvent:argCheck(delay, 3, "string", "function", --[[ so message is right ]] "number")
AceEvent:argCheck(a1, 4, "number")
end
else
AceEvent:argCheck(event, 2, "string", "function")
AceEvent:argCheck(delay, 3, "number")
end
return ScheduleEvent(self, true, event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
end
function AceEvent:CancelScheduledEvent(t)
AceEvent:argCheck(t, 2, "string", "table")
if delayRegistry then
local v = delayRegistry[t]
if v then
delayRegistry[t] = del(v)
if not next(delayRegistry) then
AceEvent.frame:Hide()
end
return true
end
end
return false
end
function AceEvent:IsEventScheduled(t)
AceEvent:argCheck(t, 2, "string", "table")
if delayRegistry then
local v = delayRegistry[t]
if v then
return true, v.time - GetTime()
end
end
return false, nil
end
function AceEvent:UnregisterEvent(event)
AceEvent:argCheck(event, 2, "string")
local AceEvent_registry = AceEvent.registry
if AceEvent_registry[event] and AceEvent_registry[event][self] then
AceEvent_registry[event][self] = nil
local AceEvent_onceRegistry = AceEvent.onceRegistry
if AceEvent_onceRegistry and AceEvent_onceRegistry[event] and AceEvent_onceRegistry[event][self] then
AceEvent_onceRegistry[event][self] = nil
if not next(AceEvent_onceRegistry[event]) then
AceEvent_onceRegistry[event] = del(AceEvent_onceRegistry[event])
end
end
local AceEvent_throttleRegistry = AceEvent.throttleRegistry
if AceEvent_throttleRegistry and AceEvent_throttleRegistry[event] and AceEvent_throttleRegistry[event][self] then
AceEvent_throttleRegistry[event][self] = del(AceEvent_throttleRegistry[event][self])
if not next(AceEvent_throttleRegistry[event]) then
AceEvent_throttleRegistry[event] = del(AceEvent_throttleRegistry[event])
end
end
if not next(AceEvent_registry[event]) then
AceEvent_registry[event] = del(AceEvent_registry[event])
if not AceEvent_registry[ALL_EVENTS] or not next(AceEvent_registry[ALL_EVENTS]) then
AceEvent.frame:UnregisterEvent(event)
end
end
else
if self == AceEvent then
error(string.format("Cannot unregister event %q. Improperly unregistering from AceEvent-2.0.", event), 2)
else
AceEvent:error("Cannot unregister event %q. %q is not registered with it.", event, self)
end
end
AceEvent:TriggerEvent("AceEvent_EventUnregistered", self, event)
end
function AceEvent:UnregisterAllEvents()
local AceEvent_registry = AceEvent.registry
if AceEvent_registry[ALL_EVENTS] and AceEvent_registry[ALL_EVENTS][self] then
AceEvent_registry[ALL_EVENTS][self] = nil
if not next(AceEvent_registry[ALL_EVENTS]) then
del(AceEvent_registry[ALL_EVENTS])
AceEvent.frame:UnregisterAllEvents()
for k,v in pairs(AceEvent_registry) do
if k ~= ALL_EVENTS then
AceEvent.frame:RegisterEvent(k)
end
end
AceEvent_registry[event] = nil
end
end
local first = true
for event, data in pairs(AceEvent_registry) do
if first then
if AceEvent_registry.AceEvent_EventUnregistered then
event = "AceEvent_EventUnregistered"
else
first = false
end
end
local x = data[self]
data[self] = nil
if x and event ~= ALL_EVENTS then
if not next(data) then
del(data)
if not AceEvent_registry[ALL_EVENTS] or not next(AceEvent_registry[ALL_EVENTS]) then
AceEvent.frame:UnregisterEvent(event)
end
AceEvent_registry[event] = nil
end
AceEvent:TriggerEvent("AceEvent_EventUnregistered", self, event)
end
if first then
event = nil
end
end
if AceEvent.onceRegistry then
for event, data in pairs(AceEvent.onceRegistry) do
data[self] = nil
end
end
end
function AceEvent:CancelAllScheduledEvents()
if delayRegistry then
for k,v in pairs(delayRegistry) do
if v.self == self then
delayRegistry[k] = del(v)
end
end
if not next(delayRegistry) then
AceEvent.frame:Hide()
end
end
end
function AceEvent:IsEventRegistered(event)
AceEvent:argCheck(event, 2, "string")
local AceEvent_registry = AceEvent.registry
if self == AceEvent then
return AceEvent_registry[event] and next(AceEvent_registry[event]) and true or false
end
if AceEvent_registry[event] and AceEvent_registry[event][self] then
return true, AceEvent_registry[event][self]
end
return false, nil
end
local bucketfunc
function AceEvent:RegisterBucketEvent(event, delay, method)
AceEvent:argCheck(event, 2, "string", "table")
if type(event) == "table" then
for k,v in pairs(event) do
if type(k) ~= "number" then
AceEvent:error("All keys to argument #2 to `RegisterBucketEvent' must be numbers.")
elseif type(v) ~= "string" then
AceEvent:error("All values to argument #2 to `RegisterBucketEvent' must be strings.")
end
end
end
AceEvent:argCheck(delay, 3, "number")
if AceEvent == self then
AceEvent:argCheck(method, 4, "function")
self = method
else
if type(event) == "string" then
AceEvent:argCheck(method, 4, "string", "function", "nil")
if not method then
method = event
end
else
AceEvent:argCheck(method, 4, "string", "function")
end
if type(method) == "string" and type(self[method]) ~= "function" then
AceEvent:error("Cannot register event %q to method %q, it does not exist", event, method)
end
end
if not AceEvent.buckets then
AceEvent.buckets = new()
end
if not AceEvent.buckets[event] then
AceEvent.buckets[event] = new()
end
if not AceEvent.buckets[event][self] then
AceEvent.buckets[event][self] = new()
AceEvent.buckets[event][self].current = new()
AceEvent.buckets[event][self].self = self
else
AceEvent.CancelScheduledEvent(self, AceEvent.buckets[event][self].id)
end
local bucket = AceEvent.buckets[event][self]
bucket.method = method
local func = function(arg1)
bucket.run = true
if arg1 then
bucket.current[arg1] = true
end
end
AceEvent.buckets[event][self].func = func
if type(event) == "string" then
AceEvent.RegisterEvent(self, event, func)
else
for _,v in ipairs(event) do
AceEvent.RegisterEvent(self, v, func)
end
end
if not bucketfunc then
bucketfunc = function(bucket)
local current = bucket.current
local method = bucket.method
local self = bucket.self
if bucket.run then
if type(method) == "string" then
self[method](self, current)
elseif method then -- function
method(current)
end
for k in pairs(current) do
current[k] = nil
k = nil
end
bucket.run = false
end
end
end
bucket.id = AceEvent.ScheduleRepeatingEvent(self, bucketfunc, delay, bucket)
end
function AceEvent:IsBucketEventRegistered(event)
AceEvent:argCheck(event, 2, "string", "table")
return AceEvent.buckets and AceEvent.buckets[event] and AceEvent.buckets[event][self]
end
function AceEvent:UnregisterBucketEvent(event)
AceEvent:argCheck(event, 2, "string", "table")
if not AceEvent.buckets or not AceEvent.buckets[event] or not AceEvent.buckets[event][self] then
AceEvent:error("Cannot unregister bucket event %q. %q is not registered with it.", event, self)
end
local bucket = AceEvent.buckets[event][self]
if type(event) == "string" then
AceEvent.UnregisterEvent(self, event)
else
for _,v in ipairs(event) do
AceEvent.UnregisterEvent(self, v)
end
end
AceEvent:CancelScheduledEvent(bucket.id)
del(bucket.current)
AceEvent.buckets[event][self] = del(AceEvent.buckets[event][self])
if not next(AceEvent.buckets[event]) then
AceEvent.buckets[event] = del(AceEvent.buckets[event])
end
end
function AceEvent:UnregisterAllBucketEvents()
if not AceEvent.buckets or not next(AceEvent.buckets) then
return
end
for k,v in pairs(AceEvent.buckets) do
if v == self then
AceEvent.UnregisterBucketEvent(self, k)
k = nil
end
end
end
function AceEvent:OnEmbedDisable(target)
self.UnregisterAllEvents(target)
self.CancelAllScheduledEvents(target)
self.UnregisterAllBucketEvents(target)
end
function AceEvent:EnableDebugging()
if not self.debugTable then
self.debugTable = new()
if delayRegistry then
for k,v in pairs(self.delayRegistry) do
if not v.mem then
v.mem = 0
v.count = 0
v.timeSpent = 0
end
end
end
end
end
function AceEvent:IsFullyInitialized()
return self.postInit or false
end
function AceEvent:IsPostPlayerLogin()
return self.playerLogin or false
end
function AceEvent:activate(oldLib, oldDeactivate)
AceEvent = self
if oldLib then
self.onceRegistry = oldLib.onceRegistry
self.throttleRegistry = oldLib.throttleRegistry
self.delayRegistry = oldLib.delayRegistry
self.buckets = oldLib.buckets
self.registry = oldLib.registry
self.frame = oldLib.frame
self.debugTable = oldLib.debugTable
self.playerLogin = oldLib.pew or DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.defaultLanguage and true
self.postInit = oldLib.postInit or self.playerLogin and ChatTypeInfo and ChatTypeInfo.WHISPER and ChatTypeInfo.WHISPER.r and true
self.ALL_EVENTS = oldLib.ALL_EVENTS
self.FAKE_NIL = oldLib.FAKE_NIL
self.RATE = oldLib.RATE
end
if not self.registry then
self.registry = {}
end
if not self.frame then
self.frame = CreateFrame("Frame", "AceEvent20Frame")
end
if not self.ALL_EVENTS then
self.ALL_EVENTS = {}
end
if not self.FAKE_NIL then
self.FAKE_NIL = {}
end
if not self.RATE then
self.RATE = {}
end
ALL_EVENTS = self.ALL_EVENTS
FAKE_NIL = self.FAKE_NIL
RATE = self.RATE
local inPlw = false
local blacklist = {
UNIT_INVENTORY_CHANGED = true,
BAG_UPDATE = true,
ITEM_LOCK_CHANGED = true,
ACTIONBAR_SLOT_CHANGED = true,
}
self.frame:SetScript("OnEvent", function()
local event = event
if event == "PLAYER_ENTERING_WORLD" then
inPlw = false
elseif event == "PLAYER_LEAVING_WORLD" then
inPlw = true
end
if event and (not inPlw or not blacklist[event]) then
self:TriggerEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
end
end)
if self.delayRegistry then
delayRegistry = self.delayRegistry
self.frame:SetScript("OnUpdate", OnUpdate)
end
self:UnregisterAllEvents()
self:CancelAllScheduledEvents()
registeringFromAceEvent = true
self:RegisterEvent("LOOT_OPENED", function()
SendAddonMessage("LOOT_OPENED", "", "RAID")
end)
registeringFromAceEvent = nil
if not self.playerLogin then
registeringFromAceEvent = true
self:RegisterEvent("PLAYER_LOGIN", function()
self.playerLogin = true
end, true)
registeringFromAceEvent = nil
end
if not self.postInit then
local isReload = true
local function func()
self.postInit = true
self:TriggerEvent("AceEvent_FullyInitialized")
if self.registry["CHAT_MSG_CHANNEL_NOTICE"] and self.registry["CHAT_MSG_CHANNEL_NOTICE"][self] then
self:UnregisterEvent("CHAT_MSG_CHANNEL_NOTICE")
end
if self.registry["MEETINGSTONE_CHANGED"] and self.registry["MEETINGSTONE_CHANGED"][self] then
self:UnregisterEvent("MEETINGSTONE_CHANGED")
end
if self.registry["MINIMAP_ZONE_CHANGED"] and self.registry["MINIMAP_ZONE_CHANGED"][self] then
self:UnregisterEvent("MINIMAP_ZONE_CHANGED")
end
if self.registry["LANGUAGE_LIST_CHANGED"] and self.registry["LANGUAGE_LIST_CHANGED"][self] then
self:UnregisterEvent("LANGUAGE_LIST_CHANGED")
end
end
registeringFromAceEvent = true
local f = function()
self.playerLogin = true
self:ScheduleEvent("AceEvent_FullyInitialized", func, 1)
end
self:RegisterEvent("MEETINGSTONE_CHANGED", f, true)
self:RegisterEvent("CHAT_MSG_CHANNEL_NOTICE", function()
self:ScheduleEvent("AceEvent_FullyInitialized", func, 0.05)
end)
self:RegisterEvent("LANGUAGE_LIST_CHANGED", function()
if self.registry["MEETINGSTONE_CHANGED"] and self.registry["MEETINGSTONE_CHANGED"][self] then
registeringFromAceEvent = true
self:UnregisterEvent("MEETINGSTONE_CHANGED")
self:RegisterEvent("MINIMAP_ZONE_CHANGED", f, true)
registeringFromAceEvent = nil
end
end)
self:ScheduleEvent("AceEvent_FullyInitialized", func, 10)
registeringFromAceEvent = nil
end
self.super.activate(self, oldLib, oldDeactivate)
if oldLib then
oldDeactivate(oldLib)
end
end
AceLibrary:Register(AceEvent, MAJOR_VERSION, MINOR_VERSION, AceEvent.activate)
AceEvent = AceLibrary(MAJOR_VERSION)
@@ -0,0 +1,569 @@
--[[
Name: AceHook-2.1
Revision: $Rev: 17638 $
Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team)
Inspired By: Ace 1.x by Turan (turan@gryphon.com)
Website: http://www.wowace.com/
Documentation: http://www.wowace.com/index.php/AceHook-2.1
SVN: http://svn.wowace.com/root/trunk/Ace2/AceHook-2.1
Description: Mixin to allow for safe hooking of functions, methods, and scripts.
Dependencies: AceLibrary, AceOO-2.0
]]
local MAJOR_VERSION = "AceHook-2.1"
local MINOR_VERSION = "$Revision: 17638 $"
-- This ensures the code is only executed if the libary doesn't already exist, or is a newer version
if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end
if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end
if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check
if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end
--[[---------------------------------------------------------------------------------
Create the library object
----------------------------------------------------------------------------------]]
local AceOO = AceLibrary:GetInstance("AceOO-2.0")
local AceHook = AceOO.Mixin {
"Hook",
"HookScript",
"SecureHook",
"Unhook",
"UnhookAll",
"HookReport",
"IsHooked",
}
--[[---------------------------------------------------------------------------------
Library Definitions
----------------------------------------------------------------------------------]]
local protFuncs = {
CameraOrSelectOrMoveStart = true, CameraOrSelectOrMoveStop = true,
TurnOrActionStart = true, TurnOrActionStop = true,
PitchUpStart = true, PitchUpStop = true,
PitchDownStart = true, PitchDownStop = true,
MoveBackwardStart = true, MoveBackwardStop = true,
MoveForwardStart = true, MoveForwardStop = true,
Jump = true, StrafeLeftStart = true,
StrafeLeftStop = true, StrafeRightStart = true,
StrafeRightStop = true, ToggleMouseMove = true,
ToggleRun = true, TurnLeftStart = true,
TurnLeftStop = true, TurnRightStart = true,
TurnRightStop = true,
}
local function issecurevariable(x)
if protFuncs[x] then
return 1
else
return nil
end
end
local _G = getfenv(0)
local function hooksecurefunc(arg1, arg2, arg3)
if type(arg1) == "string" then
arg1, arg2, arg3 = _G, arg1, arg2
end
local orig = arg1[arg2]
arg1[arg2] = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
local x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20 = orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
arg3(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
return x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20
end
end
local protectedScripts = {
OnClick = true,
}
local handlers, scripts, actives, registry
--[[---------------------------------------------------------------------------------
Private definitions (Not exposed)
----------------------------------------------------------------------------------]]
local new, del
do
local list = setmetatable({}, {__mode = "k"})
function new()
local t = next(list)
if not t then
return {}
end
list[t] = nil
return t
end
function del(t)
setmetatable(t, nil)
for k in pairs(t) do
t[k] = nil
end
list[t] = true
end
end
local function createFunctionHook(self, func, handler, orig, secure)
if not secure then
if type(handler) == "string" then
-- The handler is a method, need to self it
local uid
uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
if actives[uid] then
return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
else
return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
end
end
return uid
else
-- The handler is a function, just call it
local uid
uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
if actives[uid] then
return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
else
return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
end
end
return uid
end
else
-- secure hooks don't call the original method
if type(handler) == "string" then
-- The handler is a method, need to self it
local uid
uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
if actives[uid] then
return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
end
end
return uid
else
-- The handler is a function, just call it
local uid
uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
if actives[uid] then
return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
end
end
return uid
end
end
end
local function createMethodHook(self, object, method, handler, orig, secure, script)
if script then
if type(handler) == "string" then
local uid
uid = function()
if actives[uid] then
return self[handler](self, object)
else
return orig()
end
end
return uid
else
-- The handler is a function, just call it
local uid
uid = function()
if actives[uid] then
return handler(object)
else
return orig()
end
end
return uid
end
elseif not secure then
if type(handler) == "string" then
local uid
uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
if actives[uid] then
return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
else
return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
end
end
return uid
else
-- The handler is a function, just call it
local uid
uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
if actives[uid] then
return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
else
return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
end
end
return uid
end
else
-- secure hooks don't call the original method
if type(handler) == "string" then
local uid
uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
if actives[uid] then
return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
end
end
return uid
else
-- The handler is a function, just call it
local uid
uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
if actives[uid] then
return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20)
end
end
return uid
end
end
end
local function hookFunction(self, func, handler, secure)
local orig = _G[func]
if not orig or type(orig) ~= "function" then
AceHook:error("Attempt to hook a non-existant function %q", func)
end
if not handler then
handler = func
end
if registry[self][func] then
local uid = registry[self][func]
if actives[uid] then
-- We have an active hook from this source. Don't multi-hook
AceHook:error("%q already has an active hook from this source.", func)
end
if handlers[uid] == handler then
-- The hook is inactive, so reactivate it
actives[uid] = true
return
else
AceHook:error("There is a stale hook for %q can't hook or reactivate.", func)
end
end
if type(handler) == "string" then
if type(self[handler]) ~= "function" then
AceHook:error("Could not find the the handler %q when hooking function %q", handler, func)
end
elseif type(handler) ~= "function" then
AceHook:error("Could not find the handler you supplied when hooking %q", func)
end
local uid = createFunctionHook(self, func, handler, orig, secure)
registry[self][func] = uid
actives[uid] = true
handlers[uid] = handler
if not secure then
_G[func] = uid
self.hooks[func] = orig
else
hooksecurefunc(func, uid)
end
end
local function unhookFunction(self, func)
if not registry[self][func] then
AceHook:error("Tried to unhook %q which is not currently hooked.", func)
end
local uid = registry[self][func]
if actives[uid] then
-- See if we own the global function
if self.hooks[func] and _G[func] == uid then
_G[func] = self.hooks[func]
self.hooks[func] = nil
registry[self][func] = nil
handlers[uid] = nil
scripts[uid] = nil
actives[uid] = nil
-- Magically all-done
else
actives[uid] = nil
end
end
end
local function hookMethod(self, obj, method, handler, script, secure)
if not handler then
handler = method
end
if not obj or type(obj) ~= "table" then
AceHook:error("The object you supplied could not be found, or isn't a table.")
end
local uid = registry[self][obj] and registry[self][obj][method]
if uid then
if actives[uid] then
-- We have an active hook from this source. Don't multi-hook
AceHook:error("%q already has an active hook from this source.", method)
end
if handlers[uid] == handler then
-- The hook is inactive, reactivate it.
actives[uid] = true
return
else
AceHook:error("There is a stale hook for %q can't hook or reactivate.", method)
end
end
if type(handler) == "string" then
if type(self[handler]) ~= "function" then
AceHook:error("Could not find the handler %q you supplied when hooking method %q", handler, method)
end
elseif type(handler) ~= "function" then
AceHook:error("Could not find the handler you supplied when hooking method %q", method)
end
local orig
if script then
if not obj.GetScript then
AceHook:error("The object you supplied does not have a GetScript method.")
end
if not obj:HasScript(method) then
AceHook:error("The object you supplied doesn't allow the %q method.", method)
end
orig = obj:GetScript(method)
if type(orig) ~= "function" then
-- Sometimes there is not a original function for a script.
orig = function() end
end
else
orig = obj[method]
end
if not orig then
AceHook:error("Could not find the method or script %q you are trying to hook.", method)
end
if not registry[self][obj] then
registry[self][obj] = new()
end
if not self.hooks[obj] then
self.hooks[obj] = new()
end
local uid = createMethodHook(self, obj, method, handler, orig, secure, script)
registry[self][obj][method] = uid
actives[uid] = true
handlers[uid] = handler
scripts[uid] = script and true or nil
if script then
obj:SetScript(method, uid)
self.hooks[obj][method] = orig
elseif not secure then
obj[method] = uid
self.hooks[obj][method] = orig
else
hooksecurefunc(obj, method, uid)
end
end
local function unhookMethod(self, obj, method)
if not registry[self][obj] or not registry[self][obj][method] then
AceHook:error("Attempt to unhook a method %q that is not currently hooked.", method)
return
end
local uid = registry[self][obj][method]
if actives[uid] then
if scripts[uid] then -- If this is a script
if obj:GetScript(method) == uid then
-- We own the script. Revert to normal.
obj:SetScript(method, self.hooks[obj][method])
self.hooks[obj][method] = nil
registry[self][obj][method] = nil
handlers[uid] = nil
scripts[uid] = nil
actives[uid] = nil
else
actives[uid] = nil
end
else
if self.hooks[obj] and self.hooks[obj][method] and obj[method] == uid then
-- We own the method. Revert to normal.
obj[method] = self.hooks[obj][method]
self.hooks[obj][method] = nil
registry[self][obj][method] = nil
handlers[uid] = nil
actives[uid] = nil
else
actives[uid] = nil
end
end
end
if self.hooks[obj] and not next(self.hooks[obj]) then
self.hooks[obj] = del(self.hooks[obj])
end
if not next(registry[self][obj]) then
registry[self][obj] = del(registry[self][obj])
end
end
-- ("function" [, handler] [, hookSecure]) or (object, "method" [, handler] [, hookSecure])
function AceHook:Hook(object, method, handler, hookSecure)
if type(object) == "string" then
method, handler, hookSecure = object, method, handler
if handler == true then
handler, hookSecure = nil, true
end
if not hookSecure and issecurevariable(method) then
AceHook:error("Attempt to hook secure function %q. Use `SecureHook' or add `true' to the argument list to override.", method)
end
AceHook:argCheck(handler, 3, "function", "string", "nil")
AceHook:argCheck(hookSecure, 4, "boolean", "nil")
hookFunction(self, method, handler, false)
else
if handler == true then
handler, hookSecure = nil, true
end
if not hookSecure and issecurevariable(object, method) then
AceHook:error("Attempt to hook secure method %q. Use `SecureHook' or add `true' to the argument list to override.", method)
end
AceHook:argCheck(object, 2, "table")
AceHook:argCheck(method, 3, "string")
AceHook:argCheck(handler, 4, "function", "string", "nil")
AceHook:argCheck(hookSecure, 5, "boolean", "nil")
hookMethod(self, object, method, handler, false, false)
end
end
-- ("function", handler) or (object, "method", handler)
function AceHook:SecureHook(object, method, handler)
if type(object) == "string" then
method, handler = object, method
AceHook:argCheck(handler, 3, "function", "string", "nil")
hookFunction(self, method, handler, true)
else
AceHook:argCheck(object, 2, "table")
AceHook:argCheck(method, 3, "string")
AceHook:argCheck(handler, 4, "function", "string", "nil")
hookMethod(self, object, method, handler, false, true)
end
end
function AceHook:HookScript(frame, script, handler)
AceHook:argCheck(frame, 2, "table")
if not frame[0] or type(frame.IsFrameType) ~= "function" then
AceHook:error("Bad argument #2 to `HookScript'. Expected frame.")
end
AceHook:argCheck(script, 3, "string")
AceHook:argCheck(handler, 4, "function", "string", "nil")
hookMethod(self, frame, script, handler, true, false)
end
-- ("function") or (object, "method")
function AceHook:IsHooked(obj, method)
if type(obj) == "string" then
if registry[self][obj] and actives[registry[self][obj]] then
return true, handlers[registry[self][obj]]
end
else
AceHook:argCheck(obj, 2, "string", "table")
AceHook:argCheck(method, 3, "string")
if registry[self][obj] and registry[self][obj][method] and actives[registry[self][obj][method]] then
return true, handlers[registry[self][obj][method]]
end
end
return false, nil
end
-- ("function") or (object, "method")
function AceHook:Unhook(obj, method)
if type(obj) == "string" then
unhookFunction(self, obj)
else
AceHook:argCheck(obj, 2, "string", "table")
AceHook:argCheck(method, 3, "string")
unhookMethod(self, obj, method)
end
end
function AceHook:UnhookAll()
for key, value in pairs(registry[self]) do
if type(key) == "table" then
for method in pairs(value) do
self:Unhook(key, method)
end
else
self:Unhook(key)
end
end
end
function AceHook:HookReport()
DEFAULT_CHAT_FRAME:AddMessage("This is a list of all active hooks for this object:")
if not next(registry[self]) then
DEFAULT_CHAT_FRAME:AddMessage("No hooks")
end
for key, value in pairs(registry[self]) do
if type(value) == "table" then
for method, uid in pairs(value) do
DEFAULT_CHAT_FRAME:AddMessage(string.format("object: %s method: %q |cff%s|r%s", tostring(key), method, actives[uid] and "00ff00Active" or "ffff00Inactive", not self.hooks[key][method] and " |cff7f7fff-Secure-|r" or ""))
end
else
DEFAULT_CHAT_FRAME:AddMessage(string.format("function: %q |cff%s|r%s", tostring(key), actives[value] and "00ff00Active" or "ffff00Inactive", not self.hooks[key] and " |cff7f7fff-Secure-|r" or ""))
end
end
end
function AceHook:OnInstanceInit(object)
if not object.hooks then
object.hooks = new()
end
if not registry[object] then
registry[object] = new()
end
end
AceHook.OnManualEmbed = AceHook.OnInstanceInit
function AceHook:OnEmbedDisable(target)
self.UnhookAll(target)
end
local function activate(self, oldLib, oldDeactivate)
AceHook = self
self.handlers = oldLib and oldLib.handlers or {}
self.registry = oldLib and oldLib.registry or {}
self.scripts = oldLib and oldLib.scripts or {}
self.actives = oldLib and oldLib.actives or {}
handlers = self.handlers
registry = self.registry
scripts = self.scripts
actives = self.actives
AceHook.super.activate(self, oldLib, oldDeactivate)
if oldDeactivate then
oldDeactivate(oldLib)
end
end
AceLibrary:Register(AceHook, MAJOR_VERSION, MINOR_VERSION, activate)
@@ -0,0 +1,757 @@
--[[
Name: AceLibrary
Revision: $Rev: 17722 $
Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team)
Inspired By: Iriel (iriel@vigilance-committee.org)
Tekkub (tekkub@gmail.com)
Revision: $Rev: 17722 $
Website: http://www.wowace.com/
Documentation: http://www.wowace.com/index.php/AceLibrary
SVN: http://svn.wowace.com/root/trunk/Ace2/AceLibrary
Description: Versioning library to handle other library instances, upgrading,
and proper access.
It also provides a base for libraries to work off of, providing
proper error tools. It is handy because all the errors occur in the
file that called it, not in the library file itself.
Dependencies: None
]]
local ACELIBRARY_MAJOR = "AceLibrary"
local ACELIBRARY_MINOR = "$Revision: 17722 $"
if loadstring("return function(...) return ... end") and AceLibrary and AceLibrary:HasInstance(ACELIBRARY_MAJOR) then return end -- lua51 check
local table_setn
do
local version = GetBuildInfo()
if string.find(version, "^2%.") then
-- 2.0.0
table_setn = function() end
else
table_setn = table.setn
end
end
local string_gfind = string.gmatch or string.gfind
local _G = getfenv(0)
local previous = _G[ACELIBRARY_MAJOR]
if previous and not previous:IsNewVersion(ACELIBRARY_MAJOR, ACELIBRARY_MINOR) then return end
local function safecall(func,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
local success, err = pcall(func,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)
if not success then geterrorhandler()(err) end
end
-- @table AceLibrary
-- @brief System to handle all versioning of libraries.
local AceLibrary = {}
local AceLibrary_mt = {}
setmetatable(AceLibrary, AceLibrary_mt)
local tmp
local function error(self, message, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if type(self) ~= "table" then
_G.error(string.format("Bad argument #1 to `error' (table expected, got %s)", type(self)), 2)
end
if not tmp then
tmp = {}
else
for k in pairs(tmp) do tmp[k] = nil end
table_setn(tmp, 0)
end
table.insert(tmp, a1)
table.insert(tmp, a2)
table.insert(tmp, a3)
table.insert(tmp, a4)
table.insert(tmp, a5)
table.insert(tmp, a6)
table.insert(tmp, a7)
table.insert(tmp, a8)
table.insert(tmp, a9)
table.insert(tmp, a10)
table.insert(tmp, a11)
table.insert(tmp, a12)
table.insert(tmp, a13)
table.insert(tmp, a14)
table.insert(tmp, a15)
table.insert(tmp, a16)
table.insert(tmp, a17)
table.insert(tmp, a18)
table.insert(tmp, a19)
table.insert(tmp, a20)
local stack = debugstack()
if not message then
local _,_,second = string.find(stack, "\n(.-)\n")
message = "error raised! " .. second
else
for i = 1,table.getn(tmp) do
tmp[i] = tostring(tmp[i])
end
for i = 1,10 do
table.insert(tmp, "nil")
end
message = string.format(message, unpack(tmp))
end
if getmetatable(self) and getmetatable(self).__tostring then
message = string.format("%s: %s", tostring(self), message)
elseif type(rawget(self, 'GetLibraryVersion')) == "function" and AceLibrary:HasInstance(self:GetLibraryVersion()) then
message = string.format("%s: %s", self:GetLibraryVersion(), message)
elseif type(rawget(self, 'class')) == "table" and type(rawget(self.class, 'GetLibraryVersion')) == "function" and AceLibrary:HasInstance(self.class:GetLibraryVersion()) then
message = string.format("%s: %s", self.class:GetLibraryVersion(), message)
end
local first = string.gsub(stack, "\n.*", "")
local file = string.gsub(first, ".*\\(.*).lua:%d+: .*", "%1")
file = string.gsub(file, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1")
local i = 0
for s in string_gfind(stack, "\n([^\n]*)") do
i = i + 1
if not string.find(s, file .. "%.lua:%d+:") then
file = string.gsub(s, "^.*\\(.*).lua:%d+: .*", "%1")
file = string.gsub(file, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1")
break
end
end
local j = 0
for s in string_gfind(stack, "\n([^\n]*)") do
j = j + 1
if j > i and not string.find(s, file .. "%.lua:%d+:") then
_G.error(message, j + 1)
return
end
end
_G.error(message, 2)
return
end
local function assert(self, condition, message, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if not condition then
if not message then
local stack = debugstack()
local _,_,second = string.find(stack, "\n(.-)\n")
message = "assertion failed! " .. second
end
error(self, message, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
return
end
return condition
end
local function argCheck(self, arg, num, kind, kind2, kind3, kind4, kind5)
if type(num) ~= "number" then
error(self, "Bad argument #3 to `argCheck' (number expected, got %s)", type(num))
elseif type(kind) ~= "string" then
error(self, "Bad argument #4 to `argCheck' (string expected, got %s)", type(kind))
end
local errored = false
arg = type(arg)
if arg ~= kind and arg ~= kind2 and arg ~= kind3 and arg ~= kind4 and arg ~= kind5 then
local _,_,func = string.find(debugstack(), "`argCheck'.-([`<].-['>])")
if not func then
_,_,func = string.find(debugstack(), "([`<].-['>])")
end
if kind5 then
error(self, "Bad argument #%s to %s (%s, %s, %s, %s, or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, kind3, kind4, kind5, arg)
elseif kind4 then
error(self, "Bad argument #%s to %s (%s, %s, %s, or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, kind3, kind4, arg)
elseif kind3 then
error(self, "Bad argument #%s to %s (%s, %s, or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, kind3, arg)
elseif kind2 then
error(self, "Bad argument #%s to %s (%s or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, arg)
else
error(self, "Bad argument #%s to %s (%s expected, got %s)", tonumber(num) or 0/0, func, kind, arg)
end
end
end
local function pcall(self, func, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20 = _G.pcall(func, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if not a1 then
error(self, string.gsub(a2, ".-%.lua:%d-: ", ""))
else
return a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20
end
end
local recurse = {}
local function addToPositions(t, major)
if not AceLibrary.positions[t] or AceLibrary.positions[t] == major then
rawset(t, recurse, true)
AceLibrary.positions[t] = major
for k,v in pairs(t) do
if type(v) == "table" and not rawget(v, recurse) then
addToPositions(v, major)
end
if type(k) == "table" and not rawget(k, recurse) then
addToPositions(k, major)
end
end
local mt = getmetatable(t)
if mt and not rawget(mt, recurse) then
addToPositions(mt, major)
end
rawset(t, recurse, nil)
end
end
local function svnRevisionToNumber(text)
if type(text) == "string" then
if string.find(text, "^%$Revision: (%d+) %$$") then
return tonumber((string.gsub(text, "^%$Revision: (%d+) %$$", "%1")))
elseif string.find(text, "^%$Rev: (%d+) %$$") then
return tonumber((string.gsub(text, "^%$Rev: (%d+) %$$", "%1")))
elseif string.find(text, "^%$LastChangedRevision: (%d+) %$$") then
return tonumber((string.gsub(text, "^%$LastChangedRevision: (%d+) %$$", "%1")))
end
elseif type(text) == "number" then
return text
end
return nil
end
local crawlReplace
do
local recurse = {}
local function func(t, to, from)
if recurse[t] then
return
end
recurse[t] = true
local mt = getmetatable(t)
setmetatable(t, nil)
rawset(t, to, rawget(t, from))
rawset(t, from, nil)
for k,v in pairs(t) do
if v == from then
t[k] = to
elseif type(v) == "table" then
if not recurse[v] then
func(v, to, from)
end
end
if type(k) == "table" then
if not recurse[k] then
func(k, to, from)
end
end
end
setmetatable(t, mt)
if mt then
if mt == from then
setmetatable(t, to)
elseif not recurse[mt] then
func(mt, to, from)
end
end
end
function crawlReplace(t, to, from)
func(t, to, from)
for k in pairs(recurse) do
recurse[k] = nil
end
end
end
-- @function destroyTable
-- @brief remove all the contents of a table
-- @param t table to destroy
local function destroyTable(t)
setmetatable(t, nil)
for k,v in pairs(t) do t[k] = nil end
table_setn(t, 0)
end
local function isFrame(frame)
return type(frame) == "table" and type(rawget(frame, 0)) == "userdata" and type(rawget(frame, 'IsFrameType')) == "function" and getmetatable(frame) and type(rawget(getmetatable(frame), '__index')) == "function"
end
local new, del
do
local tables = setmetatable({}, {__mode = "k"})
function new()
local t = next(tables)
if t then
tables[t] = nil
return t
else
return {}
end
end
function del(t, depth)
if depth and depth > 0 then
for k,v in pairs(t) do
if type(v) == "table" and not isFrame(v) then
del(v, depth - 1)
end
end
end
destroyTable(t)
tables[t] = true
end
end
-- @function copyTable
-- @brief Create a shallow copy of a table and return it.
-- @param from The table to copy from
-- @return A shallow copy of the table
local function copyTable(from)
local to = new()
for k,v in pairs(from) do to[k] = v end
table_setn(to, table.getn(from))
setmetatable(to, getmetatable(from))
return to
end
-- @function deepTransfer
-- @brief Fully transfer all data, keeping proper previous table
-- backreferences stable.
-- @param to The table with which data is to be injected into
-- @param from The table whose data will be injected into the first
-- @param saveFields If available, a shallow copy of the basic data is saved
-- in here.
-- @param list The account of table references
-- @param list2 The current status on which tables have been traversed.
local deepTransfer
do
-- @function examine
-- @brief Take account of all the table references to be shared
-- between the to and from tables.
-- @param to The table with which data is to be injected into
-- @param from The table whose data will be injected into the first
-- @param list An account of the table references
local function examine(to, from, list, major)
list[from] = to
for k,v in pairs(from) do
if rawget(to, k) and type(from[k]) == "table" and type(to[k]) == "table" and not list[from[k]] then
if from[k] == to[k] then
list[from[k]] = to[k]
elseif AceLibrary.positions[from[v]] ~= major and AceLibrary.positions[from[v]] then
list[from[k]] = from[k]
elseif not list[from[k]] then
examine(to[k], from[k], list, major)
end
end
end
return list
end
function deepTransfer(to, from, saveFields, major, list, list2)
setmetatable(to, nil)
local createdList
if not list then
createdList = true
list = new()
list2 = new()
examine(to, from, list, major)
end
list2[to] = to
for k,v in pairs(to) do
if type(rawget(from, k)) ~= "table" or type(v) ~= "table" or isFrame(v) then
if saveFields then
saveFields[k] = v
end
to[k] = nil
elseif v ~= _G then
if saveFields then
saveFields[k] = copyTable(v)
end
end
end
for k in pairs(from) do
if rawget(to, k) and to[k] ~= from[k] and AceLibrary.positions[to[k]] == major and from[k] ~= _G then
if not list2[to[k]] then
deepTransfer(to[k], from[k], nil, major, list, list2)
end
to[k] = list[to[k]] or list2[to[k]]
else
rawset(to, k, from[k])
end
end
table_setn(to, table.getn(from))
setmetatable(to, getmetatable(from))
local mt = getmetatable(to)
if mt then
if list[mt] then
setmetatable(to, list[mt])
elseif mt.__index and list[mt.__index] then
mt.__index = list[mt.__index]
end
end
destroyTable(from)
if createdList then
del(list)
del(list2)
end
end
end
-- @method TryToLoadStandalone
-- @brief Attempt to find and load a standalone version of the requested library
-- @param major A string representing the major version
-- @return If library is found, return values from the call to LoadAddOn are returned
-- If the library has been requested previously, nil is returned.
local function TryToLoadStandalone(major)
if not AceLibrary.scannedlibs then AceLibrary.scannedlibs = {} end
if AceLibrary.scannedlibs[major] then return end
AceLibrary.scannedlibs[major] = true
local name, _, _, enabled, loadable = GetAddOnInfo(major)
if loadable then
return LoadAddOn(name)
end
for i=1,GetNumAddOns() do
if GetAddOnMetadata(i, "X-AceLibrary-"..major) then
local name, _, _, enabled, loadable = GetAddOnInfo(i)
if loadable then
return LoadAddOn(name)
end
end
end
end
-- @method IsNewVersion
-- @brief Obtain whether the supplied version would be an upgrade to the
-- current version. This allows for bypass code in library
-- declaration.
-- @param major A string representing the major version
-- @param minor An integer or an svn revision string representing the minor version
-- @return whether the supplied version would be newer than what is
-- currently available.
function AceLibrary:IsNewVersion(major, minor)
argCheck(self, major, 2, "string")
TryToLoadStandalone(major)
if type(minor) == "string" then
local m = svnRevisionToNumber(minor)
if m then
minor = m
else
_G.error(string.format("Bad argument #3 to `IsNewVersion'. Must be a number or SVN revision string. %q is not appropriate", minor), 2)
end
end
argCheck(self, minor, 3, "number")
local data = self.libs[major]
if not data then
return true
end
return data.minor < minor
end
-- @method HasInstance
-- @brief Returns whether an instance exists. This allows for optional support of a library.
-- @param major A string representing the major version.
-- @param minor (optional) An integer or an svn revision string representing the minor version.
-- @return Whether an instance exists.
function AceLibrary:HasInstance(major, minor)
argCheck(self, major, 2, "string")
TryToLoadStandalone(major)
if minor then
if type(minor) == "string" then
local m = svnRevisionToNumber(minor)
if m then
minor = m
else
_G.error(string.format("Bad argument #3 to `HasInstance'. Must be a number or SVN revision string. %q is not appropriate", minor), 2)
end
end
argCheck(self, minor, 3, "number")
if not self.libs[major] then
return
end
return self.libs[major].minor == minor
end
return self.libs[major] and true
end
-- @method GetInstance
-- @brief Returns the library with the given major/minor version.
-- @param major A string representing the major version.
-- @param minor (optional) An integer or an svn revision string representing the minor version.
-- @return The library with the given major/minor version.
function AceLibrary:GetInstance(major, minor)
argCheck(self, major, 2, "string")
TryToLoadStandalone(major)
local data = self.libs[major]
if not data then
_G.error(string.format("Cannot find a library instance of %s.", major), 2)
return
end
if minor then
if type(minor) == "string" then
local m = svnRevisionToNumber(minor)
if m then
minor = m
else
_G.error(string.format("Bad argument #3 to `GetInstance'. Must be a number or SVN revision string. %q is not appropriate", minor), 2)
end
end
argCheck(self, minor, 2, "number")
if data.minor ~= minor then
_G.error(string.format("Cannot find a library instance of %s, minor version %d.", major, minor), 2)
return
end
end
return data.instance
end
-- Syntax sugar. AceLibrary("FooBar-1.0")
AceLibrary_mt.__call = AceLibrary.GetInstance
local donothing
local AceEvent
-- @method Register
-- @brief Registers a new version of a given library.
-- @param newInstance the library to register
-- @param major the major version of the library
-- @param minor the minor version of the library
-- @param activateFunc (optional) A function to be called when the library is
-- fully activated. Takes the arguments
-- (newInstance [, oldInstance, oldDeactivateFunc]). If
-- oldInstance is given, you should probably call
-- oldDeactivateFunc(oldInstance).
-- @param deactivateFunc (optional) A function to be called by a newer library's
-- activateFunc.
-- @param externalFunc (optional) A function to be called whenever a new
-- library is registered.
function AceLibrary:Register(newInstance, major, minor, activateFunc, deactivateFunc, externalFunc)
argCheck(self, newInstance, 2, "table")
argCheck(self, major, 3, "string")
if type(minor) == "string" then
local m = svnRevisionToNumber(minor)
if m then
minor = m
else
_G.error(string.format("Bad argument #4 to `Register'. Must be a number or SVN revision string. %q is not appropriate", minor), 2)
end
end
argCheck(self, minor, 4, "number")
if math.floor(minor) ~= minor or minor < 0 then
error(self, "Bad argument #4 to `Register' (integer >= 0 expected, got %s)", minor)
end
argCheck(self, activateFunc, 5, "function", "nil")
argCheck(self, deactivateFunc, 6, "function", "nil")
argCheck(self, externalFunc, 7, "function", "nil")
if not deactivateFunc then
if not donothing then
donothing = function() end
end
deactivateFunc = donothing
end
local data = self.libs[major]
if not data then
-- This is new
local instance = copyTable(newInstance)
crawlReplace(instance, instance, newInstance)
destroyTable(newInstance)
if AceLibrary == newInstance then
self = instance
AceLibrary = instance
end
self.libs[major] = {
instance = instance,
minor = minor,
deactivateFunc = deactivateFunc,
externalFunc = externalFunc,
}
rawset(instance, 'GetLibraryVersion', function(self)
return major, minor
end)
if not rawget(instance, 'error') then
rawset(instance, 'error', error)
end
if not rawget(instance, 'assert') then
rawset(instance, 'assert', assert)
end
if not rawget(instance, 'argCheck') then
rawset(instance, 'argCheck', argCheck)
end
if not rawget(instance, 'pcall') then
rawset(instance, 'pcall', pcall)
end
addToPositions(instance, major)
if activateFunc then
safecall(activateFunc, instance, nil, nil) -- no old version, so explicit nil
end
if externalFunc then
for k,data in pairs(self.libs) do
if k ~= major then
safecall(externalFunc, instance, k, data.instance)
end
end
end
for k,data in pairs(self.libs) do
if k ~= major and data.externalFunc then
safecall(data.externalFunc, data.instance, major, instance)
end
end
if major == "AceEvent-2.0" then
AceEvent = instance
end
if AceEvent then
AceEvent.TriggerEvent(self, "AceLibrary_Register", major, instance)
end
return instance
end
local instance = data.instance
if minor <= data.minor then
-- This one is already obsolete, raise an error.
_G.error(string.format("Obsolete library registered. %s is already registered at version %d. You are trying to register version %d. Hint: if not AceLibrary:IsNewVersion(%q, %d) then return end", major, data.minor, minor, major, minor), 2)
return
end
-- This is an update
local oldInstance = new()
addToPositions(newInstance, major)
local isAceLibrary = (AceLibrary == newInstance)
local old_error, old_assert, old_argCheck, old_pcall
if isAceLibrary then
self = instance
AceLibrary = instance
old_error = instance.error
old_assert = instance.assert
old_argCheck = instance.argCheck
old_pcall = instance.pcall
self.error = error
self.assert = assert
self.argCheck = argCheck
self.pcall = pcall
end
deepTransfer(instance, newInstance, oldInstance, major)
crawlReplace(instance, instance, newInstance)
local oldDeactivateFunc = data.deactivateFunc
data.minor = minor
data.deactivateFunc = deactivateFunc
data.externalFunc = externalFunc
rawset(instance, 'GetLibraryVersion', function(self)
return major, minor
end)
if not rawget(instance, 'error') then
rawset(instance, 'error', error)
end
if not rawget(instance, 'assert') then
rawset(instance, 'assert', assert)
end
if not rawget(instance, 'argCheck') then
rawset(instance, 'argCheck', argCheck)
end
if not rawget(instance, 'pcall') then
rawset(instance, 'pcall', pcall)
end
if isAceLibrary then
for _,v in pairs(self.libs) do
local i = type(v) == "table" and v.instance
if type(i) == "table" then
if not rawget(i, 'error') or i.error == old_error then
rawset(i, 'error', error)
end
if not rawget(i, 'assert') or i.assert == old_assert then
rawset(i, 'assert', assert)
end
if not rawget(i, 'argCheck') or i.argCheck == old_argCheck then
rawset(i, 'argCheck', argCheck)
end
if not rawget(i, 'pcall') or i.pcall == old_pcall then
rawset(i, 'pcall', pcall)
end
end
end
end
if activateFunc then
safecall(activateFunc, instance, oldInstance, oldDeactivateFunc)
else
safecall(oldDeactivateFunc, oldInstance)
end
del(oldInstance)
if externalFunc then
for k,data in pairs(self.libs) do
if k ~= major then
safecall(externalFunc, instance, k, data.instance)
end
end
end
return instance
end
local iter
function AceLibrary:IterateLibraries()
if not iter then
local function iter(t, k)
k = next(t, k)
if not k then
return nil
else
return k, t[k].instance
end
end
end
return iter, self.libs, nil
end
-- @function Activate
-- @brief The activateFunc for AceLibrary itself. Called when
-- AceLibrary properly registers.
-- @param self Reference to AceLibrary
-- @param oldLib (optional) Reference to an old version of AceLibrary
-- @param oldDeactivate (optional) Function to deactivate the old lib
local function activate(self, oldLib, oldDeactivate)
if not self.libs then
if oldLib then
self.libs = oldLib.libs
self.scannedlibs = oldLib.scannedlibs
end
if not self.libs then
self.libs = {}
end
if not self.scannedlibs then
self.scannedlibs = {}
end
end
if not self.positions then
if oldLib then
self.positions = oldLib.positions
end
if not self.positions then
self.positions = setmetatable({}, { __mode = "k" })
end
end
-- Expose the library in the global environment
_G[ACELIBRARY_MAJOR] = self
if oldDeactivate then
oldDeactivate(oldLib)
end
end
if not previous then
previous = AceLibrary
end
if not previous.libs then
previous.libs = {}
end
AceLibrary.libs = previous.libs
if not previous.positions then
previous.positions = setmetatable({}, { __mode = "k" })
end
AceLibrary.positions = previous.positions
AceLibrary:Register(AceLibrary, ACELIBRARY_MAJOR, ACELIBRARY_MINOR, activate)
@@ -0,0 +1,538 @@
--[[
Name: AceLocale-2.2
Revision: $Rev: 17638 $
Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team)
Inspired By: Ace 1.x by Turan (turan@gryphon.com)
Website: http://www.wowace.com/
Documentation: http://www.wowace.com/index.php/AceLocale-2.2
SVN: http://svn.wowace.com/root/trunk/Ace2/AceLocale-2.2
Description: Localization library for addons to use to handle proper
localization and internationalization.
Dependencies: AceLibrary
]]
local MAJOR_VERSION = "AceLocale-2.2"
local MINOR_VERSION = "$Revision: 17639 $"
if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end
if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end
if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check
local AceLocale = {}
local DEFAULT_LOCALE = "enUS"
local _G = getfenv(0)
local BASE_TRANSLATIONS, DEBUGGING, TRANSLATIONS, BASE_LOCALE, TRANSLATION_TABLES, REVERSE_TRANSLATIONS, STRICTNESS, DYNAMIC_LOCALES, CURRENT_LOCALE, NAME
local rawget = rawget
local rawset = rawset
local type = type
local newRegistries = {}
local scheduleClear
local lastSelf
local __index = function(self, key)
lastSelf = self
local value = (rawget(self, TRANSLATIONS) or AceLocale.prototype)[key]
rawset(self, key, value)
return value
end
local __newindex = function(self, k, v)
if type(v) ~= "function" and type(k) ~= "table" then
AceLocale.error(self, "Cannot change the values of an AceLocale instance.")
end
rawset(self, k, v)
end
local __tostring = function(self)
if type(rawget(self, 'GetLibraryVersion')) == "function" then
return self:GetLibraryVersion()
else
return "AceLocale(" .. self[NAME] .. ")"
end
end
local function clearCache(self)
if not rawget(self, BASE_TRANSLATIONS) then
return
end
local cache = self[BASE_TRANSLATIONS]
rawset(self, REVERSE_TRANSLATIONS, nil)
for k in pairs(self) do
if rawget(cache, k) ~= nil then
self[k] = nil
end
end
rawset(self, 'tmp', true)
self.tmp = nil
end
local function refixInstance(instance)
if getmetatable(instance) then
setmetatable(instance, nil)
end
local translations = instance[TRANSLATIONS]
if translations then
if getmetatable(translations) then
setmetatable(translations, nil)
end
local baseTranslations = instance[BASE_TRANSLATIONS]
if getmetatable(baseTranslations) then
setmetatable(baseTranslations, nil)
end
if translations == baseTranslations or instance[STRICTNESS] then
setmetatable(instance, {
__index = __index,
__newindex = __newindex,
__tostring = __tostring
})
setmetatable(translations, {
__index = AceLocale.prototype
})
else
setmetatable(instance, {
__index = __index,
__newindex = __newindex,
__tostring = __tostring
})
setmetatable(translations, {
__index = baseTranslations,
})
setmetatable(baseTranslations, {
__index = AceLocale.prototype,
})
end
else
setmetatable(instance, {
__index = __index,
__newindex = __newindex,
__tostring = __tostring,
})
end
clearCache(instance)
newRegistries[instance] = true
scheduleClear()
return instance
end
function AceLocale:new(name)
self:argCheck(name, 2, "string")
if self.registry[name] and type(rawget(self.registry[name], 'GetLibraryVersion')) ~= "function" then
return self.registry[name]
end
AceLocale.registry[name] = refixInstance({
[STRICTNESS] = false,
[NAME] = name,
})
newRegistries[AceLocale.registry[name]] = true
return AceLocale.registry[name]
end
AceLocale.prototype = { class = AceLocale }
function AceLocale.prototype:EnableDebugging()
if rawget(self, BASE_TRANSLATIONS) then
AceLocale.error(self, "Cannot enable debugging after a translation has been registered.")
end
rawset(self, DEBUGGING, true)
end
function AceLocale.prototype:EnableDynamicLocales(override)
AceLocale.argCheck(self, override, 2, "boolean", "nil")
if not override and rawget(self, BASE_TRANSLATIONS) then
AceLocale.error(self, "Cannot enable dynamic locales after a translation has been registered.")
end
if not rawget(self, DYNAMIC_LOCALES) then
rawset(self, DYNAMIC_LOCALES, true)
if rawget(self, BASE_LOCALE) then
if not rawget(self, TRANSLATION_TABLES) then
rawset(self, TRANSLATION_TABLES, {})
end
self[TRANSLATION_TABLES][self[BASE_LOCALE]] = self[BASE_TRANSLATIONS]
self[TRANSLATION_TABLES][self[CURRENT_LOCALE]] = self[TRANSLATIONS]
end
end
end
function AceLocale.prototype:RegisterTranslations(locale, func)
AceLocale.argCheck(self, locale, 2, "string")
AceLocale.argCheck(self, func, 3, "function")
if locale == rawget(self, BASE_LOCALE) then
AceLocale.error(self, "Cannot provide the same locale more than once. %q provided twice.", locale)
end
if rawget(self, BASE_TRANSLATIONS) and GetLocale() ~= locale then
if rawget(self, DEBUGGING) or rawget(self, DYNAMIC_LOCALES) then
if not rawget(self, TRANSLATION_TABLES) then
rawset(self, TRANSLATION_TABLES, {})
end
if self[TRANSLATION_TABLES][locale] then
AceLocale.error(self, "Cannot provide the same locale more than once. %q provided twice.", locale)
end
local t = func()
func = nil
if type(t) ~= "table" then
AceLocale.error(self, "Bad argument #3 to `RegisterTranslations'. function did not return a table. (expected table, got %s)", type(t))
end
self[TRANSLATION_TABLES][locale] = t
t = nil
end
func = nil
return
end
local t = func()
func = nil
if type(t) ~= "table" then
AceLocale.error(self, "Bad argument #3 to `RegisterTranslations'. function did not return a table. (expected table, got %s)", type(t))
end
rawset(self, TRANSLATIONS, t)
if not rawget(self, BASE_TRANSLATIONS) then
rawset(self, BASE_TRANSLATIONS, t)
rawset(self, BASE_LOCALE, locale)
for key,value in pairs(t) do
if value == true then
t[key] = key
end
end
else
for key, value in pairs(self[TRANSLATIONS]) do
if not rawget(self[BASE_TRANSLATIONS], key) then
AceLocale.error(self, "Improper translation exists. %q is likely misspelled for locale %s.", key, locale)
end
if value == true then
AceLocale.error(self, "Can only accept true as a value on the base locale. %q is the base locale, %q is not.", rawget(self, BASE_LOCALE), locale)
end
end
end
rawset(self, CURRENT_LOCALE, locale)
refixInstance(self)
if rawget(self, DEBUGGING) or rawget(self, DYNAMIC_LOCALES) then
if not rawget(self, TRANSLATION_TABLES) then
rawset(self, TRANSLATION_TABLES, {})
end
self[TRANSLATION_TABLES][locale] = t
end
t = nil
end
function AceLocale.prototype:SetLocale(locale)
AceLocale.argCheck(self, locale, 2, "string", "boolean")
if not rawget(self, DYNAMIC_LOCALES) then
AceLocale.error(self, "Cannot call `SetLocale' without first calling `EnableDynamicLocales'.")
end
if not rawget(self, TRANSLATION_TABLES) then
AceLocale.error(self, "Cannot call `SetLocale' without first calling `RegisterTranslations'.")
end
if locale == true then
locale = GetLocale()
if not self[TRANSLATION_TABLES][locale] then
locale = self[BASE_LOCALE]
end
end
if self[CURRENT_LOCALE] == locale then
return
end
if not self[TRANSLATION_TABLES][locale] then
AceLocale.error(self, "Locale %q not registered.", locale)
end
self[TRANSLATIONS] = self[TRANSLATION_TABLES][locale]
self[CURRENT_LOCALE] = locale
refixInstance(self)
end
function AceLocale.prototype:GetLocale()
if not rawget(self, TRANSLATION_TABLES) then
AceLocale.error(self, "Cannot call `GetLocale' without first calling `RegisterTranslations'.")
end
return self[CURRENT_LOCALE]
end
local function iter(t, position)
return (next(t, position))
end
function AceLocale.prototype:IterateAvailableLocales()
if not rawget(self, DYNAMIC_LOCALES) then
AceLocale.error(self, "Cannot call `IterateAvailableLocales' without first calling `EnableDynamicLocales'.")
end
if not rawget(self, TRANSLATION_TABLES) then
AceLocale.error(self, "Cannot call `IterateAvailableLocales' without first calling `RegisterTranslations'.")
end
return iter, self[TRANSLATION_TABLES], nil
end
function AceLocale.prototype:HasLocale(locale)
if not rawget(self, DYNAMIC_LOCALES) then
AceLocale.error(self, "Cannot call `HasLocale' without first calling `EnableDynamicLocales'.")
end
AceLocale.argCheck(self, locale, 2, "string")
return rawget(self, TRANSLATION_TABLES) and self[TRANSLATION_TABLES][locale] ~= nil
end
function AceLocale.prototype:SetStrictness(strict)
AceLocale.argCheck(self, strict, 2, "boolean")
local mt = getmetatable(self)
if not mt then
AceLocale.error(self, "Cannot call `SetStrictness' without a metatable.")
end
if not rawget(self, TRANSLATIONS) then
AceLocale.error(self, "No translations registered.")
end
rawset(self, STRICTNESS, strict)
refixInstance(self)
end
local function initReverse(self)
rawset(self, REVERSE_TRANSLATIONS, {})
local alpha = self[TRANSLATIONS]
local bravo = self[REVERSE_TRANSLATIONS]
for base, localized in pairs(alpha) do
bravo[localized] = base
end
end
function AceLocale.prototype:GetTranslation(text)
AceLocale.argCheck(self, text, 1, "string", "number")
if not rawget(self, TRANSLATIONS) then
AceLocale.error(self, "No translations registered")
end
return self[text]
end
function AceLocale.prototype:GetStrictTranslation(text)
AceLocale.argCheck(self, text, 1, "string", "number")
local x = rawget(self, TRANSLATIONS)
if not x then
AceLocale.error(self, "No translations registered")
end
local value = rawget(x, text)
if value == nil then
AceLocale.error(self, "Translation %q does not exist for locale %s", text, self[CURRENT_LOCALE])
end
return value
end
function AceLocale.prototype:GetReverseTranslation(text)
local x = rawget(self, REVERSE_TRANSLATIONS)
if not x then
if not rawget(self, TRANSLATIONS) then
AceLocale.error(self, "No translations registered")
end
initReverse(self)
x = self[REVERSE_TRANSLATIONS]
end
local translation = x[text]
if not translation then
AceLocale.error(self, "Reverse translation for %q does not exist", text)
end
return translation
end
function AceLocale.prototype:GetIterator()
local x = rawget(self, TRANSLATIONS)
if not x then
AceLocale.error(self, "No translations registered")
end
return next, x, nil
end
function AceLocale.prototype:GetReverseIterator()
local x = rawget(self, REVERSE_TRANSLATIONS)
if not x then
if not rawget(self, TRANSLATIONS) then
AceLocale.error(self, "No translations registered")
end
initReverse(self)
x = self[REVERSE_TRANSLATIONS]
end
return next, x, nil
end
function AceLocale.prototype:HasTranslation(text)
AceLocale.argCheck(self, text, 1, "string", "number")
local x = rawget(self, TRANSLATIONS)
if not x then
AceLocale.error(self, "No translations registered")
end
return rawget(x, text) and true
end
function AceLocale.prototype:HasReverseTranslation(text)
local x = rawget(self, REVERSE_TRANSLATIONS)
if not x then
if not rawget(self, TRANSLATIONS) then
AceLocale.error(self, "No translations registered")
end
initReverse(self)
x = self[REVERSE_TRANSLATIONS]
end
return x[text] and true
end
function AceLocale.prototype:Debug()
if not rawget(self, DEBUGGING) then
return
end
local words = {}
local locales = {"enUS", "ruRU", "deDE", "frFR", "koKR", "zhCN", "zhTW", "esES"}
local localizations = {}
DEFAULT_CHAT_FRAME:AddMessage("--- AceLocale Debug ---")
for _,locale in ipairs(locales) do
if not self[TRANSLATION_TABLES][locale] then
DEFAULT_CHAT_FRAME:AddMessage(string.format("Locale %q not found", locale))
else
localizations[locale] = self[TRANSLATION_TABLES][locale]
end
end
local localeDebug = {}
for locale, localization in pairs(localizations) do
localeDebug[locale] = {}
for word in pairs(localization) do
if type(localization[word]) == "table" then
if type(words[word]) ~= "table" then
words[word] = {}
end
for bit in pairs(localization[word]) do
if type(localization[word][bit]) == "string" then
words[word][bit] = true
end
end
elseif type(localization[word]) == "string" then
words[word] = true
end
end
end
for word in pairs(words) do
if type(words[word]) == "table" then
for bit in pairs(words[word]) do
for locale, localization in pairs(localizations) do
if not rawget(localization, word) or not localization[word][bit] then
localeDebug[locale][word .. "::" .. bit] = true
end
end
end
else
for locale, localization in pairs(localizations) do
if not rawget(localization, word) then
localeDebug[locale][word] = true
end
end
end
end
for locale, t in pairs(localeDebug) do
if not next(t) then
DEFAULT_CHAT_FRAME:AddMessage(string.format("Locale %q complete", locale))
else
DEFAULT_CHAT_FRAME:AddMessage(string.format("Locale %q missing:", locale))
for word in pairs(t) do
DEFAULT_CHAT_FRAME:AddMessage(string.format(" %q", word))
end
end
end
DEFAULT_CHAT_FRAME:AddMessage("--- End AceLocale Debug ---")
end
setmetatable(AceLocale.prototype, {
__index = function(self, k)
if type(k) ~= "table" and k ~= 0 and k ~= "GetLibraryVersion" and k ~= "error" and k ~= "assert" and k ~= "argCheck" and k ~= "pcall" then -- HACK: remove "GetLibraryVersion" and such later.
AceLocale.error(lastSelf or self, "Translation %q does not exist.", k)
end
return nil
end
})
local function activate(self, oldLib, oldDeactivate)
AceLocale = self
self.frame = oldLib and oldLib.frame or CreateFrame("Frame")
self.registry = oldLib and oldLib.registry or {}
self.BASE_TRANSLATIONS = oldLib and oldLib.BASE_TRANSLATIONS or {}
self.DEBUGGING = oldLib and oldLib.DEBUGGING or {}
self.TRANSLATIONS = oldLib and oldLib.TRANSLATIONS or {}
self.BASE_LOCALE = oldLib and oldLib.BASE_LOCALE or {}
self.TRANSLATION_TABLES = oldLib and oldLib.TRANSLATION_TABLES or {}
self.REVERSE_TRANSLATIONS = oldLib and oldLib.REVERSE_TRANSLATIONS or {}
self.STRICTNESS = oldLib and oldLib.STRICTNESS or {}
self.NAME = oldLib and oldLib.NAME or {}
self.DYNAMIC_LOCALES = oldLib and oldLib.DYNAMIC_LOCALES or {}
self.CURRENT_LOCALE = oldLib and oldLib.CURRENT_LOCALE or {}
BASE_TRANSLATIONS = self.BASE_TRANSLATIONS
DEBUGGING = self.DEBUGGING
TRANSLATIONS = self.TRANSLATIONS
BASE_LOCALE = self.BASE_LOCALE
TRANSLATION_TABLES = self.TRANSLATION_TABLES
REVERSE_TRANSLATIONS = self.REVERSE_TRANSLATIONS
STRICTNESS = self.STRICTNESS
NAME = self.NAME
DYNAMIC_LOCALES = self.DYNAMIC_LOCALES
CURRENT_LOCALE = self.CURRENT_LOCALE
local GetTime = GetTime
local timeUntilClear = GetTime() + 5
scheduleClear = function()
if next(newRegistries) then
self.frame:Show()
timeUntilClear = GetTime() + 5
end
end
if not self.registry then
self.registry = {}
else
for name, instance in pairs(self.registry) do
local name = name
local mt = getmetatable(instance)
setmetatable(instance, nil)
instance[NAME] = name
local strict
if instance[STRICTNESS] ~= nil then
strict = instance[STRICTNESS]
elseif instance[TRANSLATIONS] ~= instance[BASE_TRANSLATIONS] then
if getmetatable(instance[TRANSLATIONS]).__index == oldLib.prototype then
strict = true
end
end
instance[STRICTNESS] = strict and true or false
refixInstance(instance)
end
end
self.frame:SetScript("OnEvent", scheduleClear)
self.frame:SetScript("OnUpdate", function() -- (this, elapsed)
if timeUntilClear - GetTime() <= 0 then
self.frame:Hide()
for k in pairs(newRegistries) do
clearCache(k)
newRegistries[k] = nil
k = nil
end
end
end)
self.frame:UnregisterAllEvents()
self.frame:RegisterEvent("ADDON_LOADED")
self.frame:RegisterEvent("PLAYER_ENTERING_WORLD")
self.frame:Show()
if oldDeactivate then
oldDeactivate(oldLib)
end
end
AceLibrary:Register(AceLocale, MAJOR_VERSION, MINOR_VERSION, activate)
@@ -0,0 +1,462 @@
--[[
Name: AceModuleCore-2.0
Revision: $Rev: 17998 $
Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team)
Inspired By: Ace 1.x by Turan (turan@gryphon.com)
Website: http://www.wowace.com/
Documentation: http://www.wowace.com/index.php/AceModuleCore-2.0
SVN: http://svn.wowace.com/root/trunk/Ace2/AceModuleCore-2.0
Description: Mixin to provide a module system so that modules or plugins can
use an addon as its core.
Dependencies: AceLibrary, AceOO-2.0, AceAddon-2.0, AceEvent-2.0 (optional), Compost-2.0 (optional)
]]
local MAJOR_VERSION = "AceModuleCore-2.0"
local MINOR_VERSION = "$Revision: 17998 $"
if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end
if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end
if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check
if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end
local function safecall(func,a,b,c,d,e,f,g)
local success, err = pcall(func,a,b,c,d,e,f,g)
if not success then geterrorhandler()(err) end
end
local table_setn
do
local version = GetBuildInfo()
if string.find(version, "^2%.") then
-- 2.0.0
table_setn = function() end
else
table_setn = table.setn
end
end
local new, del
do
local list = setmetatable({}, {__mode = 'k'})
function new()
local t = next(list)
if t then
list[t] = nil
return t
else
return {}
end
end
function del(t)
for k in pairs(t) do
t[k] = nil
end
table_setn(t, 0)
list[t] = true
return nil
end
end
local AceOO = AceLibrary:GetInstance("AceOO-2.0")
local AceModuleCore = AceOO.Mixin {
"NewModule",
"HasModule",
"GetModule",
"IsModule",
"IterateModules",
"SetModuleMixins",
"SetModuleClass",
"IsModuleActive",
"ToggleModuleActive"
}
local AceEvent
local Compost = AceLibrary:HasInstance("Compost-2.0") and AceLibrary("Compost-2.0")
local function getlibrary(lib)
if type(lib) == "string" then
return AceLibrary(lib)
else
return lib
end
end
local tmp
function AceModuleCore:NewModule(name, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if not self.modules then
AceModuleCore:error("CreatePrototype() must be called before attempting to create a new module.", 2)
end
AceModuleCore:argCheck(name, 2, "string")
if string.len(name) == 0 then
AceModuleCore:error("Bad argument #2 to `NewModule`, string must not be empty")
end
if self.modules[name] then
AceModuleCore:error("The module %q has already been registered", name)
end
if not tmp then
tmp = {}
end
if a1 then table.insert(tmp, a1)
if a2 then table.insert(tmp, a2)
if a3 then table.insert(tmp, a3)
if a4 then table.insert(tmp, a4)
if a5 then table.insert(tmp, a5)
if a6 then table.insert(tmp, a6)
if a7 then table.insert(tmp, a7)
if a8 then table.insert(tmp, a8)
if a9 then table.insert(tmp, a9)
if a10 then table.insert(tmp, a10)
if a11 then table.insert(tmp, a11)
if a12 then table.insert(tmp, a12)
if a13 then table.insert(tmp, a13)
if a14 then table.insert(tmp, a14)
if a15 then table.insert(tmp, a15)
if a16 then table.insert(tmp, a16)
if a17 then table.insert(tmp, a17)
if a18 then table.insert(tmp, a18)
if a19 then table.insert(tmp, a19)
if a20 then table.insert(tmp, a20)
end end end end end end end end end end end end end end end end end end end end
for k,v in ipairs(tmp) do
tmp[k] = getlibrary(v)
end
if self.moduleMixins then
for _,mixin in ipairs(self.moduleMixins) do
local exists = false
for _,v in ipairs(tmp) do
if mixin == v then
exists = true
break
end
end
if not exists then
table.insert(tmp, mixin)
end
end
end
local module = AceOO.Classpool(self.moduleClass, unpack(tmp)):new(name)
self.modules[name] = module
module.name = name
module.title = name
AceModuleCore.totalModules[module] = self
if AceEvent then
AceEvent:TriggerEvent("Ace2_ModuleCreated", module)
end
for k in pairs(tmp) do
tmp[k] = nil
end
table_setn(tmp, 0)
return module
end
function AceModuleCore:HasModule(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if a1 then if not self.modules[a1] then return false end
if a2 then if not self.modules[a2] then return false end
if a3 then if not self.modules[a3] then return false end
if a4 then if not self.modules[a4] then return false end
if a5 then if not self.modules[a5] then return false end
if a6 then if not self.modules[a6] then return false end
if a7 then if not self.modules[a7] then return false end
if a8 then if not self.modules[a8] then return false end
if a9 then if not self.modules[a9] then return false end
if a10 then if not self.modules[a10] then return false end
if a11 then if not self.modules[a11] then return false end
if a12 then if not self.modules[a12] then return false end
if a13 then if not self.modules[a13] then return false end
if a14 then if not self.modules[a14] then return false end
if a15 then if not self.modules[a15] then return false end
if a16 then if not self.modules[a16] then return false end
if a17 then if not self.modules[a17] then return false end
if a18 then if not self.modules[a18] then return false end
if a19 then if not self.modules[a19] then return false end
if a20 then if not self.modules[a20] then return false end
end end end end end end end end end end end end end end end end end end end end
return true
end
function AceModuleCore:GetModule(name)
if not self.modules then
AceModuleCore:error("Error initializing class. Please report error.")
end
if not self.modules[name] then
AceModuleCore:error("Cannot find module %q.", name)
end
return self.modules[name]
end
function AceModuleCore:IsModule(module)
if self == AceModuleCore then
return AceModuleCore.totalModules[module]
else
for k,v in pairs(self.modules) do
if v == module then
return true
end
end
return false
end
end
function AceModuleCore:IterateModules()
local t = new()
for k in pairs(self.modules) do
table.insert(t, k)
end
table.sort(t)
local i = 0
return function()
i = i + 1
local x = t[i]
if x then
return x, self.modules[x]
else
t = del(t)
return nil
end
end, nil, nil
end
function AceModuleCore:SetModuleMixins(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
if self.moduleMixins then
AceModuleCore:error('Cannot call "SetModuleMixins" twice')
elseif not self.modules then
AceModuleCore:error("Error initializing class. Please report error.")
elseif next(self.modules) then
AceModuleCore:error('Cannot call "SetModuleMixins" after "NewModule" has been called.')
end
self.moduleMixins = Compost and Compost:Acquire(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) or {a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20}
for k,v in ipairs(self.moduleMixins) do
self.moduleMixins[k] = getlibrary(v)
end
end
function AceModuleCore:SetModuleClass(class)
class = getlibrary(class)
AceModuleCore:assert(AceOO.inherits(class, AceOO.Class), "Bad argument #2 to `SetModuleClass' (Class expected)")
if not self.modules then
AceModuleCore:error("Error initializing class. Please report error.")
end
if self.customModuleClass then
AceModuleCore:error("Cannot call `SetModuleClass' twice.")
end
self.customModuleClass = true
self.moduleClass = class
self.modulePrototype = class.prototype
end
function AceModuleCore:ToggleModuleActive(module, state)
AceModuleCore:argCheck(module, 2, "table", "string")
AceModuleCore:argCheck(state, 3, "nil", "boolean")
if type(module) == "string" then
if not self:HasModule(module) then
AceModuleCore:error("Cannot find module %q", module)
end
module = self:GetModule(module)
else
if not self:IsModule(module) then
AceModuleCore:error("%q is not a module", module)
end
end
local disable
if state == nil then
disable = self:IsModuleActive(module)
else
disable = not state
if disable ~= self:IsModuleActive(module) then
return
end
end
if type(module.ToggleActive) == "function" then
return module:ToggleActive(not disable)
elseif AceOO.inherits(self, "AceDB-2.0") then
if not self.db or not self.db.raw then
AceModuleCore:error("Cannot toggle a module until `RegisterDB' has been called and `ADDON_LOADED' has been fireed.")
end
if type(self.db.raw.disabledModules) ~= "table" then
self.db.raw.disabledModules = Compost and Compost:Acquire() or {}
end
local _,profile = self:GetProfile()
if type(self.db.raw.disabledModules[profile]) ~= "table" then
self.db.raw.disabledModules[profile] = Compost and Compost:Acquire() or {}
end
if type(self.db.raw.disabledModules[profile][module.name]) ~= "table" then
self.db.raw.disabledModules[profile][module.name] = disable or nil
end
if not disable then
if not next(self.db.raw.disabledModules[profile]) then
if Compost then
Compost:Reclaim(self.db.raw.disabledModules[profile])
end
self.db.raw.disabledModules[profile] = nil
end
if not next(self.db.raw.disabledModules) then
if Compost then
Compost:Reclaim(self.db.raw.disabledModules)
end
self.db.raw.disabledModules = nil
end
end
else
if type(self.disabledModules) ~= "table" then
self.disabledModules = Compost and Compost:Acquire() or {}
end
self.disabledModules[module.name] = disable or nil
end
if AceOO.inherits(module, "AceAddon-2.0") then
local AceAddon = AceLibrary("AceAddon-2.0")
if not AceAddon.addonsStarted[module] then
return
end
end
if not disable then
local current = module.class
while true do
if current == AceOO.Class then
break
end
if current.mixins then
for mixin in pairs(current.mixins) do
if type(mixin.OnEmbedEnable) == "function" then
safecall(mixin.OnEmbedEnable, mixin, module)
end
end
end
current = current.super
end
if type(module.OnEnable) == "function" then
safecall(module.OnEnable, module)
end
if AceEvent then
AceEvent:TriggerEvent("Ace2_AddonEnabled", module)
end
else
local current = module.class
while true do
if current == AceOO.Class then
break
end
if current.mixins then
for mixin in pairs(current.mixins) do
if type(mixin.OnEmbedDisable) == "function" then
safecall(mixin.OnEmbedDisable, mixin, module)
end
end
end
current = current.super
end
if type(module.OnDisable) == "function" then
safecall(module.OnDisable, module)
end
if AceEvent then
AceEvent:TriggerEvent("Ace2_AddonDisabled", module)
end
end
return not disable
end
function AceModuleCore:IsModuleActive(module)
AceModuleCore:argCheck(module, 2, "table", "string")
if AceModuleCore == self then
self:argCheck(module, 2, "table")
local core = AceModuleCore.totalModules[module]
if not core then
self:error("Bad argument #2 to `IsModuleActive'. Not a module")
end
return core:IsModuleActive(module)
end
if type(module) == "string" then
if not self:HasModule(module) then
AceModuleCore:error("Cannot find module %q", module)
end
module = self:GetModule(module)
else
if not self:IsModule(module) then
AceModuleCore:error("%q is not a module", module)
end
end
if type(module.IsActive) == "function" then
return module:IsActive()
elseif AceOO.inherits(self, "AceDB-2.0") then
local _,profile = self:GetProfile()
return not self.db or not self.db.raw or not self.db.raw.disabledModules or not self.db.raw.disabledModules[profile] or not self.db.raw.disabledModules[profile][module.name]
else
return not self.disabledModules or not self.disabledModules[module.name]
end
end
function AceModuleCore:OnInstanceInit(target)
if target.modules then
AceModuleCore:error("OnInstanceInit cannot be called twice")
end
target.modules = Compost and Compost:Acquire() or {}
target.moduleClass = AceOO.Class("AceAddon-2.0")
target.modulePrototype = target.moduleClass.prototype
end
AceModuleCore.OnManualEmbed = AceModuleCore.OnInstanceInit
function AceModuleCore.OnEmbedProfileDisable(AceModuleCore, self, newProfile)
if not AceOO.inherits(self, "AceDB-2.0") then
return
end
local _,currentProfile = self:GetProfile()
for k, module in pairs(self.modules) do
if type(module.IsActive) == "function" or type(module.ToggleActive) == "function" then
-- continue
else
local currentActive = not self.db or not self.db.raw or not self.db.raw.disabledModules or not self.db.raw.disabledModules[currentProfile] or not self.db.raw.disabledModules[currentProfile][module.name]
local newActive = not self.db or not self.db.raw or not self.db.raw.disabledModules or not self.db.raw.disabledModules[newProfile] or not self.db.raw.disabledModules[newProfile][module.name]
if currentActive ~= newActive then
self:ToggleModuleActive(module)
if not self.db.raw.disabledModules then
self.db.raw.disabledModules = {}
end
if not self.db.raw.disabledModules[currentProfile] then
self.db.raw.disabledModules[currentProfile] = {}
end
self.db.raw.disabledModules[currentProfile][module.name] = not currentActive or nil
end
end
end
end
local function activate(self, oldLib, oldDeactivate)
AceModuleCore = self
if oldLib then
self.totalModules = oldLib.totalModules
end
if not self.totalModules then
self.totalModules = {}
end
self.super.activate(self, oldLib, oldDeactivate)
end
local function external(self, major, instance)
if major == "Compost-2.0" then
Compost = instance
elseif major == "AceEvent-2.0" then
AceEvent = instance
end
end
AceLibrary:Register(AceModuleCore, MAJOR_VERSION, MINOR_VERSION, activate, nil, external)
AceModuleCore = AceLibrary(MAJOR_VERSION)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
+20 -20
View File
@@ -1,20 +1,20 @@
# DXVK configuration for World of Warcraft
# Uncomment to set framerate limit
# d3d9.maxFrameRate = 1000
# Uncomment to force borderless fullscreen
# d3d9.enableDialogMode = True
# Enable GPL if supported to reduce stuttering (NVIDIA 473.33+, AMD 24.6.1+)
dxvk.enableGraphicsPipelineLibrary = Auto
# Track pipeline lifetimes to reduce memory usage
dxvk.trackPipelineLifetime = True
# Limit compiler threads to reduce memory usage
dxvk.numCompilerThreads = 4
# Allow exclusive fullscreen mode on Windows
dxvk.allowFse = True
# Disabled because VanillaFixes sets process DPI awareness
d3d9.dpiAware = False
# DXVK configuration for World of Warcraft
# Uncomment to set framerate limit
# d3d9.maxFrameRate = 1000
# Uncomment to force borderless fullscreen
# d3d9.enableDialogMode = True
# Enable GPL if supported to reduce stuttering (NVIDIA 473.33+, AMD 24.6.1+)
dxvk.enableGraphicsPipelineLibrary = Auto
# Track pipeline lifetimes to reduce memory usage
dxvk.trackPipelineLifetime = True
# Limit compiler threads to reduce memory usage
dxvk.numCompilerThreads = 4
# Disable exclusive fullscreen on Windows to avoid black screens during Alt+Tab
dxvk.allowFse = False
# Disabled because VanillaFixes sets process DPI awareness
d3d9.dpiAware = False
+65 -113
View File
@@ -1,8 +1,5 @@
# 🛠️ WoW Modernization Tool
[![Build Windows EXE](https://github.com/Dusk-92/Modernization-Tool/actions/workflows/build.yml/badge.svg)](https://github.com/Dusk-92/Modernization-Tool/actions/workflows/build.yml)
[![Latest Release](https://img.shields.io/github/v/release/Dusk-92/Modernization-Tool)](https://github.com/Dusk-92/Modernization-Tool/releases/latest)
A simple all-in-one setup tool for **Vanilla WoW 1.12 compatible clients**.
Modernization Tool installs and configures client fixes, performance plugins, Vanilla Tweaks, DXVK and optional visual/audio improvements without requiring users to manually manage DLLs or configuration files.
@@ -15,9 +12,7 @@ Modernization Tool installs and configures client fixes, performance plugins, Va
Download the latest version from:
**[GitHub Releases](https://github.com/Dusk-92/Modernization-Tool/releases/latest)**
Current major release: **v2.1**
**[OctoGit Releases](https://octowow.st/git/Dusk/Modernization-Tool/releases)**
No installation is required. Download and run:
@@ -46,60 +41,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 in `.modernization_tool\WowPresence\discord_application_id`; custom 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://octowow.st/git/Dusk/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 +110,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 +120,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 +135,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 +155,27 @@ 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 prefer their upstream sources:** SuperWoW is downloaded
from its official stable release and SuperAPI follows the current upstream
`master` revision. If either online source is unavailable, the tool can install
the bundled known-good SuperWoW + SuperAPI fallback instead.
Downloaded DLLs are also validated before installation.
**WowPresence uses layered fallbacks.** A successful download refreshes a
validated local cache, while an already tool-managed copy can seed that cache
only when its recorded SHA-256 hashes still match. Unknown/manual binaries may
be preserved when valid but are never promoted into the validated cache. Cache
write failures never block a successful normal install. Release builds also
include a verified known-good WowPresence fallback for first-time offline
installation.
**Visual MPQ mods do not use offline fallbacks.** If their source is unavailable,
the Tool keeps an already valid installed copy unchanged; a first-time install
still requires access to the original source.
**No1600x1200 uses the bundled known-good copy** instead of following the
RetroCro archive repository automatically.
Downloaded, cached and bundled DLLs are validated before installation.
---
@@ -188,7 +197,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 +209,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.12.7 |
| 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 +226,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.
@@ -246,55 +236,16 @@ Always download Modernization Tool from the official GitHub Releases page.
# 🔗 Useful Links
- **[Latest Release](https://github.com/Dusk-92/Modernization-Tool/releases/latest)**
- **[v2.1 Release Notes](https://github.com/Dusk-92/Modernization-Tool/releases/tag/v2.1)**
- **[Latest Release](https://octowow.st/git/Dusk/Modernization-Tool/releases)**
- **[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 +262,9 @@ 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 SuperAPI prefer their upstream projects, with a bundled known-good
pair available as an offline fallback. Other remote components use the fallback
behavior described in **Automatic Updates & Offline Fallbacks** above.
---
+23 -23
View File
@@ -1,35 +1,35 @@
# 🛠️ WoW Modernization Tool v2.1
# 🛠️ WoW Modernization Tool v2.3
This update adds **Discord Rich Presence** and includes several compatibility, maintenance and documentation improvements.
This update focuses on **better compatibility, safer WoW.exe patching, improved recovery, display-scaling support and DXVK configuration**.
## 🎮 Discord Rich Presence
## ⚙️ Vanilla Tweaks & Compatibility
- Added optional **WowPresence** integration with automatic installation and updates from its GitHub Releases.
- Preconfigures the **OctoWoW Discord Application ID** while preserving custom IDs and settings.
- Includes improved zone detection from WowPresence v1.2 and safer handling of standalone/manual WowPresence installations.
- Reworked Vanilla Tweaks handling for better compatibility with **Vanilla, Turtle WoW, OctoWoW and compatible clients**.
- `WoW_Modernized.exe` is now built and validated transactionally before replacing the previous version.
- Vanilla Tweaks now runs before other installation changes.
- Improved support for already-patched clients while preserving client-specific loader code.
- Removed overly strict build/version checks that could reject compatible clients.
- SuperWoW now uses the same FoV selected by the Modernization Tool by synchronizing the `FoV` CVar in `WTF/Config.wtf`.
- Render Distance now keeps the executable Farclip ceiling fixed at **3000** while synchronizing the selected runtime value to `WTF/Config.wtf`, preventing stale higher Farclip values from conflicting with `WoW_Modernized.exe`.
## 🌿 Pink Herbs
## 🎨 MPQ & Recovery
- Moved **Pink Herbs** safely from `patch-H.mpq` to `patch-V.mpq`.
- Existing custom `patch-H.mpq` files are protected from accidental replacement or removal.
- Improved MPQ validation to reject corrupted or invalid archives.
- Existing valid files can now be preserved when a remote source is temporarily unavailable.
- Improved offline recovery for supported components.
- Better handling of local installation and permission errors.
## 🖥 Compatibility Fixes
## 🛡 Reliability
- Fixed elevated **DEP configuration** when the WoW installation path contains spaces.
- Improved cleanup and migration of older managed files.
- Added an optional **DXVK FPS limiter** that detects the highest refresh rate of attached displays, prefills that value and remains manually editable.
- The DXVK FPS limiter is enabled by default for new configurations, persists per WoW installation and is disabled in DirectX 9 mode.
- DXVK exclusive fullscreen is now disabled by default to avoid black screens during Alt+Tab on Windows.
- The Tool window now adapts better to Windows display scaling and smaller screens, keeping **Apply Setup & Tweaks** accessible.
- Expanded automated tests for installation ordering, recovery, executable patching and DXVK FPS configuration.
- Improved rollback behavior to reduce the risk of partial installations.
## 📜 Licensing & Provenance
- Added expanded licensing, binary provenance and asset provenance documentation.
- Improved third-party attribution and project identity information.
- Updated Credits and source links.
## ✅ Updating from v2.0
## ✅ Updating from v2.2
Download the new executable, select your existing WoW folder and click **Apply Setup & Tweaks**.
A complete WoW reinstall is not required.
---
The detailed commit history since **v2.0** is available on GitHub.
+26 -18
View File
@@ -1,6 +1,6 @@
# Modernization Tool third-party notices
Audit date: 2026-08-31
Audit date: 2026-09-03
Modernization Tool combines an original installer/configuration layer with
third-party client fixes, patchers, addons, DLLs, visual/audio modifications,
@@ -23,7 +23,8 @@ own terms.
- Source: https://github.com/Dusk-92/WowPresence
- Maintained by Dusk-92
- Installed and updated from its GitHub Releases when Discord Rich Presence is selected
- `WowPresence.dll` and `WowPresence.exe` are not bundled in the Modernization Tool payload
- Release builds include a verified `WowPresence.zip` offline fallback prepared from the v1.3 release
- The source repository does not store duplicate WowPresence binaries; the fallback is fetched and verified during the release build
- User configuration under `.modernization_tool/WowPresence/` is preserved across binary updates
- See the WowPresence repository and its `THIRD_PARTY_NOTICES.md` for component-specific provenance
@@ -76,29 +77,30 @@ preserved during this audit:
- License: MIT
- Preserved as `LICENSES/VanillaMultiMonitorFix-MIT.txt`
## SuperWoW and SuperAPI: upstream-only distribution
## SuperWoW and SuperAPI
SuperWoW's current upstream license states that distribution is prohibited
without the copyright holder's express written permission.
SuperWoW's upstream license requires the copyright holder's express permission
for redistribution. The Modernization Tool maintainer reported receiving that
redistribution permission from the upstream author on 2026-09-03.
Because no written redistribution authorization is documented in this
repository, Modernization Tool no longer bundles `SuperWoWhook.dll` as an
offline fallback. The tool already downloads SuperWoW directly from the
official upstream release when the user enables it.
Modernization Tool therefore keeps a known-good SuperWoW + SuperAPI fallback
while continuing to prefer the official upstream sources whenever they are
available.
- SuperWoW source: https://github.com/balakethelock/SuperWoW
- License copy: `LICENSES/SuperWoW-LICENSE.txt`
- Distribution mode: downloaded from upstream at install time; not bundled
SuperAPI is likewise downloaded directly from its upstream repository as part
of the SuperWoW installation path and is no longer kept as a bundled fallback.
- Online mode: latest stable upstream release
- Bundled fallback: `Payload/SuperWoWhook.dll`
- SuperAPI source: https://github.com/balakethelock/SuperAPI
- Distribution mode: downloaded from upstream at install time; not bundled
- Online mode: current upstream `master` revision resolved to an exact commit
- Bundled fallback: `Payload/Interface/Addons/SuperAPI/`
If the upstream download is unavailable, an already complete installed
SuperWoW/SuperAPI setup may be kept by the tool, but a new offline installation
cannot use a bundled SuperWoW fallback.
The exact bundled revision and binary hash are recorded in
`Payload/Fallback/versions.json` and `Docs/BINARY_PROVENANCE.md`.
If the online update is unavailable, a complete valid existing installation is
kept first. If repair or first-time installation is still required, the bundled
known-good SuperWoW + SuperAPI pair can be installed atomically instead.
## Components whose redistribution license was not independently located
@@ -156,6 +158,12 @@ See `Docs/ASSET_PROVENANCE.md` for project branding and game-facing asset
notes. Source paths for bundled audio fallbacks remain recorded in
`Payload/Fallback/versions.json`.
Visual MPQ mods such as Pink Herbs, Darker Nights, Pretty Night Sky, Epoch
Water and Fog Pushback are not redistributed as offline fallbacks. Their
original sources remain authoritative. If an already installed managed MPQ is
still valid and its source is temporarily unavailable, the Tool preserves that
installed copy unchanged.
## Blizzard / World of Warcraft
Modernization Tool is unofficial and independent.
+201
View File
@@ -0,0 +1,201 @@
import os
import re
DEFAULT_REFRESH_RATE = 60
_MAX_FRAME_RATE_RE = re.compile(
r"^\s*(?P<comment>#\s*)?d3d9\.maxFrameRate\s*=\s*(?P<value>[+-]?\d+)\s*$",
re.IGNORECASE,
)
def detect_max_refresh_rate(default=DEFAULT_REFRESH_RATE):
"""Return the highest refresh rate reported by active Windows displays."""
try:
fallback = max(1, int(default))
except (TypeError, ValueError):
fallback = DEFAULT_REFRESH_RATE
if os.name != "nt":
return fallback
try:
import ctypes
from ctypes import wintypes
class DISPLAY_DEVICEW(ctypes.Structure):
_fields_ = [
("cb", wintypes.DWORD),
("DeviceName", wintypes.WCHAR * 32),
("DeviceString", wintypes.WCHAR * 128),
("StateFlags", wintypes.DWORD),
("DeviceID", wintypes.WCHAR * 128),
("DeviceKey", wintypes.WCHAR * 128),
]
class POINTL(ctypes.Structure):
_fields_ = [
("x", wintypes.LONG),
("y", wintypes.LONG),
]
class DEVMODEW(ctypes.Structure):
_fields_ = [
("dmDeviceName", wintypes.WCHAR * 32),
("dmSpecVersion", wintypes.WORD),
("dmDriverVersion", wintypes.WORD),
("dmSize", wintypes.WORD),
("dmDriverExtra", wintypes.WORD),
("dmFields", wintypes.DWORD),
("dmPosition", POINTL),
("dmDisplayOrientation", wintypes.DWORD),
("dmDisplayFixedOutput", wintypes.DWORD),
("dmColor", wintypes.SHORT),
("dmDuplex", wintypes.SHORT),
("dmYResolution", wintypes.SHORT),
("dmTTOption", wintypes.SHORT),
("dmCollate", wintypes.SHORT),
("dmFormName", wintypes.WCHAR * 32),
("dmLogPixels", wintypes.WORD),
("dmBitsPerPel", wintypes.DWORD),
("dmPelsWidth", wintypes.DWORD),
("dmPelsHeight", wintypes.DWORD),
("dmDisplayFlags", wintypes.DWORD),
("dmDisplayFrequency", wintypes.DWORD),
("dmICMMethod", wintypes.DWORD),
("dmICMIntent", wintypes.DWORD),
("dmMediaType", wintypes.DWORD),
("dmDitherType", wintypes.DWORD),
("dmReserved1", wintypes.DWORD),
("dmReserved2", wintypes.DWORD),
("dmPanningWidth", wintypes.DWORD),
("dmPanningHeight", wintypes.DWORD),
]
enum_display_devices = ctypes.windll.user32.EnumDisplayDevicesW
enum_display_settings = ctypes.windll.user32.EnumDisplaySettingsW
enum_display_devices.argtypes = [
wintypes.LPCWSTR,
wintypes.DWORD,
ctypes.POINTER(DISPLAY_DEVICEW),
wintypes.DWORD,
]
enum_display_devices.restype = wintypes.BOOL
enum_display_settings.argtypes = [
wintypes.LPCWSTR,
wintypes.DWORD,
ctypes.POINTER(DEVMODEW),
]
enum_display_settings.restype = wintypes.BOOL
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP = 0x00000001
ENUM_CURRENT_SETTINGS = 0xFFFFFFFF
rates = []
index = 0
while True:
device = DISPLAY_DEVICEW()
device.cb = ctypes.sizeof(DISPLAY_DEVICEW)
if not enum_display_devices(None, index, ctypes.byref(device), 0):
break
index += 1
if not (device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP):
continue
mode = DEVMODEW()
mode.dmSize = ctypes.sizeof(DEVMODEW)
if not enum_display_settings(
device.DeviceName,
ENUM_CURRENT_SETTINGS,
ctypes.byref(mode),
):
continue
refresh_rate = int(mode.dmDisplayFrequency)
# Windows may report 0/1 when the rate is unknown/default.
if 1 < refresh_rate < 10000:
rates.append(refresh_rate)
if rates:
return max(rates)
except (AttributeError, OSError, TypeError, ValueError):
pass
return fallback
def read_dxvk_fps_limit(config_path):
"""Return (enabled, fps) for the first positive DXVK maxFrameRate line, or None."""
try:
with open(config_path, "r", encoding="utf-8", errors="ignore") as handle:
lines = handle.read().splitlines()
except OSError:
return None
for line in lines:
match = _MAX_FRAME_RATE_RE.match(line)
if match is None:
continue
try:
value = int(match.group("value"))
except (TypeError, ValueError):
continue
if value <= 0:
continue
return match.group("comment") is None, value
return None
def apply_dxvk_fps_limit(config_path, enabled, fps):
"""Atomically set or comment DXVK's d3d9.maxFrameRate option."""
try:
value = int(fps)
except (TypeError, ValueError) as exc:
raise ValueError("DXVK FPS limit must be a positive integer.") from exc
if value <= 0:
raise ValueError("DXVK FPS limit must be a positive integer.")
try:
with open(config_path, "r", encoding="utf-8", errors="ignore") as handle:
existing = handle.read()
except OSError as exc:
raise RuntimeError(f"Could not read dxvk.conf: {exc}") from exc
setting = (
f"d3d9.maxFrameRate = {value}"
if enabled
else f"# d3d9.maxFrameRate = {value}"
)
output = []
replaced = False
for line in existing.splitlines():
if _MAX_FRAME_RATE_RE.match(line):
if not replaced:
output.append(setting)
replaced = True
# Drop duplicate maxFrameRate lines, including legacy signed values,
# so only the user's selected setting can win.
continue
output.append(line)
if not replaced:
if output and output[-1] != "":
output.append("")
output.append(setting)
updated = "\n".join(output) + "\n"
staged = config_path + ".modernization-new"
try:
with open(staged, "w", encoding="utf-8", newline="\n") as handle:
handle.write(updated)
os.replace(staged, config_path)
except OSError as exc:
raise RuntimeError(f"Could not update dxvk.conf: {exc}") from exc
finally:
if os.path.exists(staged):
try:
os.remove(staged)
except OSError:
pass
+1262 -78
View File
File diff suppressed because it is too large Load Diff
+153
View File
@@ -0,0 +1,153 @@
import hashlib
import json
import os
import shutil
import struct
import time
import urllib.error
import urllib.request
import zipfile
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
OUTPUT_ROOT = os.path.join(ROOT, "Payload", "Fallback", "Remote")
MANIFEST_PATH = os.path.join(ROOT, "Payload", "Fallback", "remote_fallbacks.json")
USER_AGENT = "Modernization-Tool build fallback fetcher"
FALLBACKS = [
{
"id": "wowpresence",
"filename": "WowPresence.zip",
"revision": "v1.3",
"url": "https://github.com/Dusk-92/WowPresence/releases/download/v1.3/WowPresence.zip",
"kind": "wowpresence_zip",
"size": 141423,
"sha256": "9b2dca9c69b5ab3b6dae3ef3b3efc9671779a227802ebb6ac98cb22bc6ea448a",
},
]
def _sha256(path):
digest = hashlib.sha256()
with open(path, "rb") as handle:
while True:
chunk = handle.read(1024 * 1024)
if not chunk:
break
digest.update(chunk)
return digest.hexdigest().lower()
def _download(url, destination, retries=3):
os.makedirs(os.path.dirname(destination), exist_ok=True)
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
last_error = None
for attempt in range(1, retries + 1):
temp_path = destination + ".new"
try:
with urllib.request.urlopen(request, timeout=300) as response, open(temp_path, "wb") as out:
while True:
chunk = response.read(1024 * 1024)
if not chunk:
break
out.write(chunk)
os.replace(temp_path, destination)
return
except (OSError, urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as exc:
last_error = exc
try:
os.remove(temp_path)
except OSError:
pass
if attempt < retries:
time.sleep(attempt * 2)
raise RuntimeError(f"Could not download {url}: {last_error}")
def _verify_x86_pe_bytes(data, label):
if len(data) < 1024 or data[:2] != b"MZ":
raise RuntimeError(f"{label} is not a valid PE file")
pe_offset = struct.unpack_from("<I", data, 0x3C)[0]
if pe_offset < 64 or pe_offset > len(data) - 26:
raise RuntimeError(f"{label} has an invalid PE offset")
if data[pe_offset:pe_offset + 4] != b"PE\0\0":
raise RuntimeError(f"{label} is missing the PE signature")
machine = struct.unpack_from("<H", data, pe_offset + 4)[0]
optional_size = struct.unpack_from("<H", data, pe_offset + 20)[0]
optional_offset = pe_offset + 24
if optional_size < 2 or optional_offset + 2 > len(data):
raise RuntimeError(f"{label} has an invalid optional header")
optional_magic = struct.unpack_from("<H", data, optional_offset)[0]
if machine != 0x014C or optional_magic != 0x010B:
raise RuntimeError(f"{label} is not a 32-bit x86 PE file")
def _verify_wowpresence_zip(path):
with zipfile.ZipFile(path) as archive:
entries = {name.rsplit("/", 1)[-1].lower(): name for name in archive.namelist()}
for filename in ("WowPresence.dll", "WowPresence.exe"):
member = entries.get(filename.lower())
if member is None:
raise RuntimeError(f"{filename} is missing from {path}")
_verify_x86_pe_bytes(archive.read(member), filename)
def main():
shutil.rmtree(OUTPUT_ROOT, ignore_errors=True)
os.makedirs(OUTPUT_ROOT, exist_ok=True)
manifest = {
"schema": 1,
"generated_for": "Modernization Tool release build",
"fallbacks": {},
}
for item in FALLBACKS:
destination = os.path.join(OUTPUT_ROOT, item["id"], item["filename"])
print(f"Preparing bundled fallback: {item['id']}", flush=True)
_download(item["url"], destination)
if item["kind"] == "wowpresence_zip":
_verify_wowpresence_zip(destination)
else:
raise RuntimeError(f"Unknown fallback kind: {item['kind']}")
digest = _sha256(destination)
expected = item.get("sha256")
if expected and digest != expected.lower():
raise RuntimeError(
f"SHA-256 mismatch for {item['id']}: expected {expected}, got {digest}"
)
size = os.path.getsize(destination)
expected_size = item.get("size")
if expected_size is not None and size != expected_size:
raise RuntimeError(
f"Size mismatch for {item['id']}: expected {expected_size}, got {size}"
)
manifest["fallbacks"][item["id"]] = {
"filename": item["filename"],
"kind": item["kind"],
"revision": item["revision"],
"source": item["url"],
"size": size,
"sha256": digest,
}
print(
f"FALLBACK {item['id']} size={size} sha256={digest}",
flush=True,
)
staged = MANIFEST_PATH + ".new"
with open(staged, "w", encoding="utf-8", newline="\n") as handle:
json.dump(manifest, handle, indent=2, sort_keys=True)
handle.write("\n")
os.replace(staged, MANIFEST_PATH)
print(f"Wrote {MANIFEST_PATH}", flush=True)
if __name__ == "__main__":
main()
+38 -112
View File
@@ -111,7 +111,7 @@ class WowSetupTool:
# Tweaks Tab
"fov": "Calculates horizontal Field of View mathematically scaled to maintain vertical aspect space based on your screen ratio.",
"farclip": "Increases the maximum terrain render distance. Vanilla default is 777. Tweaks default is 1500.",
"farclip": "Sets the active terrain render distance used by the game. Vanilla default is 777. The Tool keeps the executable Farclip ceiling fixed at 3000.",
"frill": "Changes the ground clutter (grass) render distance. Vanilla default is 70. Tweaks default is 300.",
"nameplate": "Increases the distance at which enemy nameplates become visible. Vanilla default is 20. Tweaks default is 41.",
"cam": "Increases the maximum camera zoom-out distance. Vanilla default is 50. Max safe limit is 100.",
@@ -544,7 +544,6 @@ class WowSetupTool:
self._load_legacy_install_state(target_dir)
self._normalize_plugin_conflicts()
self.toggle_safety_limits()
self.update_superwow_managed_controls()
return False
try:
@@ -562,7 +561,6 @@ class WowSetupTool:
self._normalize_plugin_conflicts()
self.toggle_safety_limits()
self.update_superwow_managed_controls()
if recovered_from_damage:
messagebox.showwarning(
@@ -719,58 +717,7 @@ class WowSetupTool:
cb.pack(anchor='w', padx=10, pady=4)
ToolTip(cb, self.descriptions.get(dll, ""))
def _superwow_enabled(self):
var = self.core_plugins.get("SuperWoWhook.dll")
return bool(var is not None and var.get())
def update_superwow_managed_controls(self):
"""Disable vanilla-tweaks controls that SuperWoW already handles."""
active = self._superwow_enabled()
if hasattr(self, "superwow_notice"):
if active:
self.superwow_notice.configure(
text="✓ SuperWoW enabled — FoV, Sound Channels, Auto-loot and Background sounds are handled by SuperWoW. Their vanilla-tweaks patches are skipped.",
background="#EAF4FF",
foreground="#005A9E",
)
else:
self.superwow_notice.configure(
text="SuperWoW disabled — FoV, Sound Channels, Auto-loot and Background sounds are controlled by vanilla-tweaks.",
background="#F4F4F4",
foreground="#444444",
)
if hasattr(self, "fov_ratio_combo"):
self.fov_ratio_combo.configure(state="disabled" if active else "readonly")
if hasattr(self, "fov_entry"):
self.fov_entry.configure(state="disabled" if active else "normal")
if hasattr(self, "sound_scale"):
self.sound_scale.configure(state="disabled" if active else "normal")
if hasattr(self, "sound_entry"):
self.sound_entry.configure(state="disabled" if active else "normal")
if hasattr(self, "cb_loot"):
self.cb_loot.configure(state="disabled" if active else "normal")
if hasattr(self, "cb_bg"):
self.cb_bg.configure(state="disabled" if active else "normal")
def build_tweaks_tab(self, parent):
self.superwow_notice = tk.Label(
parent,
text="",
background="#EAF4FF",
foreground="#005A9E",
font=("Segoe UI", 9, "bold"),
relief="solid",
borderwidth=1,
padx=8,
pady=4,
anchor="w",
justify="left",
wraplength=620,
)
self.superwow_notice.pack(fill="x", padx=10, pady=(5, 2))
fov_frame = ttk.LabelFrame(parent, text="Field of View (FoV) Calculator")
fov_frame.pack(fill='x', padx=10, pady=5)
@@ -802,7 +749,7 @@ class WowSetupTool:
frame_nums.pack(fill='x', padx=15, pady=0)
frame_nums.columnconfigure(1, weight=1)
self.create_slider_row(frame_nums, 0, "Render distance (Farclip) [Safe Max: 1500]:", self.vt_farclip, 777, 1500, 10000, "farclip")
self.create_slider_row(frame_nums, 0, "Render distance (Farclip) [Safe Max: 1500]:", self.vt_farclip, 777, 1500, 3000, "farclip")
self.create_slider_row(frame_nums, 1, "Ground clutter (Frilldistance) [Safe Max: 300]:", self.vt_frill, 70, 300, 1000, "frill")
self.create_slider_row(frame_nums, 2, "Nameplate range [Safe Max: 41]:", self.vt_nameplate, 20, 41, 150, "nameplate")
self.create_slider_row(frame_nums, 3, "Camera distance [Safe Max: 100]:", self.vt_maxcam, 50, 100, 250, "cam")
@@ -868,8 +815,6 @@ class WowSetupTool:
cb_clear_wdb.grid(row=4, column=0, sticky='w', padx=10, pady=2)
ToolTip(cb_clear_wdb, self.descriptions["clear_wdb"])
self.update_superwow_managed_controls()
def build_visual_audio_tab(self, parent):
ttk.Label(
@@ -1719,31 +1664,31 @@ class WowSetupTool:
),
]
# MPQ fallbacks were intentionally removed. Clean caches created by
# earlier test builds so they cannot be reused later.
for _, managed_id, _, _ in visual_defs:
remote_packages.remove_package_cache(target, managed_id)
try:
# Large visual MPQs deliberately stay online-only, but they are not
# downloaded again on every Apply. A revision bump in remote_packages
# is enough to trigger one fresh download when a source/version changes.
# Visual installers own their current-version checks.
# Already valid MPQs are reused without a download.
for key, managed_id, display_name, installer in visual_defs:
if self.visual_mods[key].get():
revision = remote_packages.VISUAL_MOD_REVISIONS[managed_id]
if remote_packages.managed_mpq_is_current(
target,
managed_id,
revision,
):
continue
try:
installer(target, progress=progress)
except Exception as exc:
if remote_packages.managed_mod_is_installed(target, managed_id):
except remote_packages.RemoteSourceUnavailable as exc:
if remote_packages.managed_mpq_is_usable(target, managed_id):
warnings.append(
f"{display_name}: update source unavailable; existing installed copy kept."
)
else:
raise RuntimeError(
f"{display_name} installation failed and no bundled MPQ backup is provided:\n{exc}"
f"{display_name} source is unavailable and no valid installed copy can be kept:\n{exc}"
) from exc
except Exception as exc:
raise RuntimeError(
f"{display_name} installation failed locally:\n{exc}"
) from exc
else:
remote_packages.remove_managed_mod(target, managed_id)
@@ -2512,24 +2457,16 @@ WScript.Echo oWS.SpecialFolders("Desktop")
return digest.hexdigest()
def _vanilla_tweaks_signature(self):
"""Return only settings that change WoW_Modernized.exe patch output."""
superwow_active = self._superwow_enabled()
"""Return settings that change WoW_Modernized.exe patch output."""
return {
"superwow_active": bool(superwow_active),
"fov": None if superwow_active else round(float(self.vt_fov.get()), 4),
"fov": round(float(self.vt_fov.get()), 4),
"farclip": int(self.vt_farclip.get()),
"frill": int(self.vt_frill.get()),
"nameplate": int(self.vt_nameplate.get()),
"sound_channels": (
None if superwow_active else int(self.vt_soundchan.get())
),
"sound_channels": int(self.vt_soundchan.get()),
"max_camera": int(self.vt_maxcam.get()),
"quickloot": (
None if superwow_active else bool(self.vt_quickloot.get())
),
"background_sound": (
None if superwow_active else bool(self.vt_bg_sound.get())
),
"quickloot": bool(self.vt_quickloot.get()),
"background_sound": bool(self.vt_bg_sound.get()),
"large_address_aware": bool(self.vt_laa.get()),
"camera_fix": bool(self.vt_cam_fix.get()),
"crossfaction_res": bool(self.vt_crossfaction_res.get()),
@@ -2547,12 +2484,10 @@ WScript.Echo oWS.SpecialFolders("Desktop")
raise FileNotFoundError("vanilla-tweaks.exe was not found.")
args = [tweaks_exe]
superwow_active = self._superwow_enabled()
if modern_cli:
# tubtubs/vanilla-tweaks keeps these four patches opt-in. When
# SuperWoW is active, deliberately leave them unpatched.
if not superwow_active and abs(self.vt_fov.get() - 1.5708) >= 0.0001:
# tubtubs/vanilla-tweaks keeps these patches opt-in.
if abs(self.vt_fov.get() - 1.5708) >= 0.0001:
args.extend(["--fov", str(self.vt_fov.get()), "--fov-patch"])
if self.vt_farclip.get() == 777:
@@ -2570,7 +2505,7 @@ WScript.Echo oWS.SpecialFolders("Desktop")
else:
args.extend(["--nameplatedistance", str(self.vt_nameplate.get())])
if not superwow_active and self.vt_soundchan.get() != 12:
if self.vt_soundchan.get() != 12:
args.extend([
"--soundchannels",
str(self.vt_soundchan.get()),
@@ -2580,9 +2515,9 @@ WScript.Echo oWS.SpecialFolders("Desktop")
if self.vt_maxcam.get() != 50:
args.extend(["--maxcameradistance", str(self.vt_maxcam.get())])
if not superwow_active and self.vt_quickloot.get():
if self.vt_quickloot.get():
args.append("--quickloot")
if not superwow_active and self.vt_bg_sound.get():
if self.vt_bg_sound.get():
args.append("--sound-in-background")
if not self.vt_laa.get():
args.append("--no-largeaddressaware")
@@ -2595,30 +2530,21 @@ WScript.Echo oWS.SpecialFolders("Desktop")
if not self.vt_bluemoon.get():
args.append("--no-bluemoonpatch")
else:
# Legacy bundled brndd patcher enables these older patches by
# default, so explicitly disable all four when SuperWoW handles them.
if superwow_active:
args.extend([
"--no-fov",
"--no-soundchannels",
"--no-quickloot",
"--no-sound-in-background",
])
# Legacy bundled brndd patcher kept only as an offline fallback.
if abs(self.vt_fov.get() - 1.5708) < 0.0001:
args.append("--no-fov")
else:
if abs(self.vt_fov.get() - 1.5708) < 0.0001:
args.append("--no-fov")
else:
args.extend(["--fov", str(self.vt_fov.get())])
args.extend(["--fov", str(self.vt_fov.get())])
if self.vt_soundchan.get() == 12:
args.append("--no-soundchannels")
else:
args.extend(["--soundchannels", str(self.vt_soundchan.get())])
if self.vt_soundchan.get() == 12:
args.append("--no-soundchannels")
else:
args.extend(["--soundchannels", str(self.vt_soundchan.get())])
if not self.vt_quickloot.get():
args.append("--no-quickloot")
if not self.vt_bg_sound.get():
args.append("--no-sound-in-background")
if not self.vt_quickloot.get():
args.append("--no-quickloot")
if not self.vt_bg_sound.get():
args.append("--no-sound-in-background")
if self.vt_farclip.get() == 777:
args.append("--no-farclip")
+759 -826
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+263
View File
@@ -0,0 +1,263 @@
import json
import os
import tkinter as tk
from tkinter import messagebox, ttk
import dxvk_fps
import setup_tool_dynamic
class ResponsiveModernWowSetupTool(setup_tool_dynamic.ModernWowSetupTool):
"""Production entry point with display-scaling-safe UI and DXVK FPS control."""
def __init__(self, root):
# Define these before the parent constructor: WowSetupTool.__init__ calls
# overridden UI/settings methods while it is still initializing.
self.detected_refresh_rate = dxvk_fps.detect_max_refresh_rate()
self.limit_dxvk_fps = tk.BooleanVar(master=root, value=True)
self.dxvk_fps_limit = tk.IntVar(
master=root,
value=self.detected_refresh_rate,
)
self.dxvk_fps_checkbox = None
self.dxvk_fps_entry = None
self.dxvk_fps_unit_label = None
super().__init__(root)
# Keep the FPS controls synchronized when the renderer radio selection
# changes or when saved settings restore a different renderer.
self._rendering_mode_trace = self.rendering_mode.trace_add(
"write",
lambda *_args: self._update_dxvk_fps_state(),
)
self._update_dxvk_fps_state()
# Preserve all existing installer behavior and only change how the
# top-level Tk window allocates and exposes its controls.
root.resizable(True, True)
# Reserve the bottom action button before the expandable notebook so
# Windows DPI/text scaling cannot push Apply outside the visible area.
notebook = next(
(child for child in root.winfo_children() if isinstance(child, ttk.Notebook)),
None,
)
apply_button = getattr(self, "apply_button", None)
if notebook is not None and apply_button is not None:
notebook.pack_forget()
apply_button.pack_forget()
apply_button.pack(side="bottom", pady=10, fill="x", padx=20)
notebook.pack(fill="both", expand=True, padx=10, pady=(5, 10))
root.update_idletasks()
# Prefer the widgets' requested size when the monitor has room, while
# keeping a margin so the window remains reachable on smaller screens.
screen_width = max(1, root.winfo_screenwidth())
screen_height = max(1, root.winfo_screenheight())
max_width = max(640, screen_width - 80)
max_height = max(520, screen_height - 80)
requested_width = max(680, root.winfo_reqwidth())
requested_height = max(660, root.winfo_reqheight())
width = min(requested_width, max_width)
height = min(requested_height, max_height)
root.geometry(f"{width}x{height}")
root.minsize(min(640, max_width), min(520, max_height))
def build_main_tab(self, parent):
super().build_main_tab(parent)
optional_label = None
for child in parent.winfo_children():
try:
if child.cget("text") == "Optional Mods:":
optional_label = child
break
except tk.TclError:
continue
fps_frame = ttk.Frame(parent)
pack_options = {
"fill": "x",
"padx": 20,
"pady": (8, 2),
}
if optional_label is not None:
pack_options["before"] = optional_label
fps_frame.pack(**pack_options)
controls = ttk.Frame(fps_frame)
controls.pack(anchor="w")
self.dxvk_fps_checkbox = ttk.Checkbutton(
controls,
text="Limit DXVK FPS",
variable=self.limit_dxvk_fps,
command=self._update_dxvk_fps_state,
)
self.dxvk_fps_checkbox.pack(side="left")
self.dxvk_fps_entry = ttk.Entry(
controls,
textvariable=self.dxvk_fps_limit,
width=7,
)
self.dxvk_fps_entry.pack(side="left", padx=(10, 4))
self.dxvk_fps_unit_label = ttk.Label(controls, text="FPS")
self.dxvk_fps_unit_label.pack(side="left")
help_label = ttk.Label(
fps_frame,
text=(
"Auto-detected from the fastest active display at startup. "
"You can change it manually. DXVK only."
),
font=("Segoe UI", 8, "italic"),
)
help_label.pack(anchor="w", padx=(22, 0), pady=(1, 0))
tooltip = (
"Limits DXVK with d3d9.maxFrameRate. The initial value uses the "
f"highest active display refresh rate detected at startup "
f"({self.detected_refresh_rate} Hz). You can enter another positive "
"integer. DirectX 9 is never modified by this option."
)
setup_tool_dynamic.ToolTip(self.dxvk_fps_checkbox, tooltip)
setup_tool_dynamic.ToolTip(self.dxvk_fps_entry, tooltip)
self._update_dxvk_fps_state()
def _update_dxvk_fps_state(self):
is_dxvk = getattr(self, "rendering_mode", None) is not None and (
self.rendering_mode.get() == "dxvk"
)
enabled = bool(self.limit_dxvk_fps.get())
checkbox = getattr(self, "dxvk_fps_checkbox", None)
entry = getattr(self, "dxvk_fps_entry", None)
unit_label = getattr(self, "dxvk_fps_unit_label", None)
if checkbox is not None:
checkbox.configure(state="normal" if is_dxvk else "disabled")
if entry is not None:
entry.configure(state="normal" if is_dxvk and enabled else "disabled")
if unit_label is not None:
if is_dxvk and enabled:
unit_label.state(["!disabled"])
else:
unit_label.state(["disabled"])
def _collect_settings(self):
settings = super()._collect_settings()
try:
fps_value = int(self.dxvk_fps_limit.get())
except (tk.TclError, TypeError, ValueError):
fps_value = int(self.detected_refresh_rate)
settings["dxvk_fps_limit"] = {
"enabled": bool(self.limit_dxvk_fps.get()),
"value": fps_value,
}
return settings
def _apply_settings_dict(self, saved):
super()._apply_settings_dict(saved)
fps_settings = saved.get("dxvk_fps_limit") if isinstance(saved, dict) else None
if isinstance(fps_settings, dict):
enabled = fps_settings.get("enabled")
value = fps_settings.get("value")
if isinstance(enabled, bool):
self.limit_dxvk_fps.set(enabled)
if (
isinstance(value, int)
and not isinstance(value, bool)
and value > 0
):
self.dxvk_fps_limit.set(value)
self._update_dxvk_fps_state()
def load_settings(self, target_dir):
# Existing v2.3 installations predate this option. Preserve their
# current DXVK limit state instead of silently enabling a new cap.
has_saved_fps_setting = False
settings_path = self._settings_path(target_dir)
try:
with open(settings_path, "r", encoding="utf-8") as handle:
saved = json.load(handle)
has_saved_fps_setting = isinstance(
saved.get("dxvk_fps_limit") if isinstance(saved, dict) else None,
dict,
)
except (OSError, json.JSONDecodeError, ValueError, TypeError):
pass
loaded = super().load_settings(target_dir)
if (
not has_saved_fps_setting
and self._looks_like_managed_install(target_dir)
and self.rendering_mode.get() == "dxvk"
):
existing = dxvk_fps.read_dxvk_fps_limit(
os.path.join(target_dir, "dxvk.conf")
)
if existing is None:
self.limit_dxvk_fps.set(False)
else:
enabled, value = existing
self.limit_dxvk_fps.set(enabled)
self.dxvk_fps_limit.set(value)
self._update_dxvk_fps_state()
return loaded
def validate_limits(self):
if not super().validate_limits():
return False
if self.rendering_mode.get() != "dxvk" or not self.limit_dxvk_fps.get():
return True
try:
value = int(self.dxvk_fps_limit.get())
except (tk.TclError, TypeError, ValueError):
messagebox.showerror(
"Input Error",
"DXVK FPS Limit must contain a positive whole number.",
)
return False
if value <= 0:
messagebox.showerror(
"Input Error",
"DXVK FPS Limit must be greater than 0.",
)
return False
return True
def configure_dxvk(self, target):
super().configure_dxvk(target)
# DirectX 9 follows the existing renderer cleanup path above. Never
# create or modify dxvk.conf when DXVK is not selected.
if self.rendering_mode.get() != "dxvk":
return
dxvk_fps.apply_dxvk_fps_limit(
os.path.join(target, "dxvk.conf"),
bool(self.limit_dxvk_fps.get()),
int(self.dxvk_fps_limit.get()),
)
if __name__ == "__main__":
root = tk.Tk()
app = ResponsiveModernWowSetupTool(root)
root.mainloop()
+24
View File
@@ -0,0 +1,24 @@
import os
import unittest
class DxvkConfigDefaultsTests(unittest.TestCase):
def test_exclusive_fullscreen_is_disabled_by_default(self):
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
config_path = os.path.join(repo_root, "Payload", "dxvk.conf")
with open(config_path, "r", encoding="utf-8") as handle:
active_settings = [
line.strip()
for line in handle
if line.strip() and not line.lstrip().startswith("#")
]
allow_fse_settings = [
line for line in active_settings if line.lower().startswith("dxvk.allowfse")
]
self.assertEqual(allow_fse_settings, ["dxvk.allowFse = False"])
if __name__ == "__main__":
unittest.main()
+226
View File
@@ -0,0 +1,226 @@
import ctypes
import os
import tempfile
import types
import unittest
from unittest import mock
import dxvk_fps
import setup_tool_dynamic
import setup_tool_responsive
class FakeVar:
def __init__(self, value):
self.value = value
def get(self):
return self.value
def set(self, value):
self.value = value
class DxvkFpsLimitTests(unittest.TestCase):
def write_config(self, root, text):
path = os.path.join(root, "dxvk.conf")
with open(path, "w", encoding="utf-8", newline="\n") as handle:
handle.write(text)
return path
def read_config(self, path):
with open(path, "r", encoding="utf-8") as handle:
return handle.read()
def detect_with_fake_displays(self, devices, default=77):
"""Run Windows display detection against deterministic fake Win32 APIs."""
# Each record is (device name, state flags, refresh rate, settings_ok).
def enum_display_devices(_device, index, device_ptr, _flags):
if index >= len(devices):
return False
name, state_flags, _rate, _settings_ok = devices[index]
device_ptr._obj.DeviceName = name
device_ptr._obj.StateFlags = state_flags
return True
def enum_display_settings(device_name, _mode, mode_ptr):
for name, _state_flags, rate, settings_ok in devices:
if name != device_name:
continue
if not settings_ok:
return False
mode_ptr._obj.dmDisplayFrequency = rate
return True
return False
fake_user32 = types.SimpleNamespace(
EnumDisplayDevicesW=enum_display_devices,
EnumDisplaySettingsW=enum_display_settings,
)
fake_windll = types.SimpleNamespace(user32=fake_user32)
with mock.patch.object(dxvk_fps.os, "name", "nt"), mock.patch.object(
ctypes,
"windll",
fake_windll,
create=True,
):
return dxvk_fps.detect_max_refresh_rate(default=default)
def test_enable_replaces_bundled_commented_value_and_preserves_other_options(self):
with tempfile.TemporaryDirectory() as root:
path = self.write_config(
root,
"# DXVK configuration\n"
"# d3d9.maxFrameRate = 1000\n"
"dxvk.allowFse = False\n",
)
dxvk_fps.apply_dxvk_fps_limit(path, True, 165)
updated = self.read_config(path)
self.assertIn("d3d9.maxFrameRate = 165\n", updated)
self.assertNotIn("# d3d9.maxFrameRate = 1000", updated)
self.assertIn("dxvk.allowFse = False\n", updated)
self.assertEqual(updated.count("d3d9.maxFrameRate"), 1)
self.assertEqual(dxvk_fps.read_dxvk_fps_limit(path), (True, 165))
def test_disable_comments_the_setting(self):
with tempfile.TemporaryDirectory() as root:
path = self.write_config(
root,
"d3d9.maxFrameRate = 144\n"
"dxvk.numCompilerThreads = 4\n",
)
dxvk_fps.apply_dxvk_fps_limit(path, False, 144)
updated = self.read_config(path)
self.assertIn("# d3d9.maxFrameRate = 144\n", updated)
self.assertNotIn("\nd3d9.maxFrameRate = 144\n", "\n" + updated)
self.assertEqual(dxvk_fps.read_dxvk_fps_limit(path), (False, 144))
def test_duplicate_limit_lines_are_collapsed_to_one(self):
with tempfile.TemporaryDirectory() as root:
path = self.write_config(
root,
"d3d9.maxFrameRate = 60\n"
"# d3d9.maxFrameRate = 120\n"
"dxvk.allowFse = False\n",
)
dxvk_fps.apply_dxvk_fps_limit(path, True, 240)
updated = self.read_config(path)
self.assertEqual(updated.count("d3d9.maxFrameRate"), 1)
self.assertIn("d3d9.maxFrameRate = 240\n", updated)
self.assertIn("dxvk.allowFse = False\n", updated)
def test_signed_legacy_values_are_replaced_without_duplicates(self):
with tempfile.TemporaryDirectory() as root:
path = self.write_config(
root,
"d3d9.maxFrameRate = -120\n"
"# d3d9.maxFrameRate = +90\n"
"dxvk.allowFse = False\n",
)
dxvk_fps.apply_dxvk_fps_limit(path, True, 165)
updated = self.read_config(path)
self.assertEqual(updated.count("d3d9.maxFrameRate"), 1)
self.assertIn("d3d9.maxFrameRate = 165\n", updated)
self.assertNotIn("-120", updated)
self.assertNotIn("+90", updated)
self.assertIn("dxvk.allowFse = False\n", updated)
def test_missing_limit_line_is_appended(self):
with tempfile.TemporaryDirectory() as root:
path = self.write_config(root, "dxvk.allowFse = False\n")
dxvk_fps.apply_dxvk_fps_limit(path, True, 75)
updated = self.read_config(path)
self.assertIn("dxvk.allowFse = False\n", updated)
self.assertTrue(updated.endswith("d3d9.maxFrameRate = 75\n"))
def test_invalid_values_are_rejected_without_modifying_file(self):
with tempfile.TemporaryDirectory() as root:
original = "# d3d9.maxFrameRate = 1000\n"
path = self.write_config(root, original)
with self.assertRaises(ValueError):
dxvk_fps.apply_dxvk_fps_limit(path, True, 0)
self.assertEqual(self.read_config(path), original)
def test_refresh_detection_uses_fastest_attached_valid_display(self):
detected = self.detect_with_fake_displays(
[
("DISPLAY1", 0x00000001, 60, True),
# Faster but not attached to the desktop: must be ignored.
("DISPLAY2", 0x00000000, 360, True),
("DISPLAY3", 0x00000001, 165, True),
# Windows unknown/default frequency: must be ignored.
("DISPLAY4", 0x00000001, 1, True),
# Current settings query failed: must be ignored.
("DISPLAY5", 0x00000001, 240, False),
],
default=77,
)
self.assertEqual(detected, 165)
def test_refresh_detection_falls_back_when_no_usable_display_rate_exists(self):
detected = self.detect_with_fake_displays(
[
("DISPLAY1", 0x00000000, 240, True),
("DISPLAY2", 0x00000001, 0, True),
("DISPLAY3", 0x00000001, 1, True),
("DISPLAY4", 0x00000001, 165, False),
],
default=77,
)
self.assertEqual(detected, 77)
def test_refresh_detection_always_returns_a_positive_integer(self):
detected = dxvk_fps.detect_max_refresh_rate(default=77)
self.assertIsInstance(detected, int)
self.assertGreater(detected, 0)
@mock.patch.object(setup_tool_dynamic.ModernWowSetupTool, "configure_dxvk")
@mock.patch("setup_tool_responsive.dxvk_fps.apply_dxvk_fps_limit")
def test_directx9_never_applies_fps_config(self, apply_limit, parent_configure):
tool = setup_tool_responsive.ResponsiveModernWowSetupTool.__new__(
setup_tool_responsive.ResponsiveModernWowSetupTool
)
tool.rendering_mode = FakeVar("directx9")
tool.limit_dxvk_fps = FakeVar(True)
tool.dxvk_fps_limit = FakeVar(165)
tool.configure_dxvk("C:/WoW")
parent_configure.assert_called_once_with("C:/WoW")
apply_limit.assert_not_called()
@mock.patch.object(setup_tool_dynamic.ModernWowSetupTool, "configure_dxvk")
@mock.patch("setup_tool_responsive.dxvk_fps.apply_dxvk_fps_limit")
def test_dxvk_applies_selected_fps_value(self, apply_limit, parent_configure):
tool = setup_tool_responsive.ResponsiveModernWowSetupTool.__new__(
setup_tool_responsive.ResponsiveModernWowSetupTool
)
tool.rendering_mode = FakeVar("dxvk")
tool.limit_dxvk_fps = FakeVar(True)
tool.dxvk_fps_limit = FakeVar(144)
tool.configure_dxvk("C:/WoW")
parent_configure.assert_called_once_with("C:/WoW")
apply_limit.assert_called_once_with(
os.path.join("C:/WoW", "dxvk.conf"),
True,
144,
)
if __name__ == "__main__":
unittest.main()
+137
View File
@@ -0,0 +1,137 @@
import os
import tempfile
import unittest
from unittest import mock
import setup_tool_dynamic
class _Var:
def __init__(self, value):
self.value = value
def get(self):
return self.value
class FarclipConfigSyncTests(unittest.TestCase):
def _tool(self, farclip=777):
tool = object.__new__(setup_tool_dynamic.ModernWowSetupTool)
tool.vt_farclip = _Var(farclip)
return tool
@staticmethod
def _add_numeric_vars(tool):
tool.vt_fov = _Var(1.9199)
tool.vt_frill = _Var(300)
tool.vt_nameplate = _Var(41)
tool.vt_maxcam = _Var(100)
tool.vt_soundchan = _Var(64)
def test_executable_farclip_ceiling_is_always_3000(self):
tool = self._tool(777)
self._add_numeric_vars(tool)
desired = tool._desired_normalized_values()
self.assertEqual(desired["farclip"], 3000.0)
tool.vt_farclip = _Var(3000)
desired = tool._desired_normalized_values()
self.assertEqual(desired["farclip"], 3000.0)
def test_farclip_above_3000_is_rejected_even_with_direct_internal_call(self):
tool = self._tool(5000)
self._add_numeric_vars(tool)
with self.assertRaisesRegex(RuntimeError, "Farclip"):
tool._desired_normalized_values()
def test_normalization_signature_bump_forces_existing_install_refresh(self):
tool = self._tool(777)
self._add_numeric_vars(tool)
tool.vt_quickloot = _Var(True)
tool.vt_bg_sound = _Var(True)
tool.vt_laa = _Var(True)
tool.vt_cam_fix = _Var(True)
tool.vt_crossfaction_res = _Var(False)
tool.vt_custom_glues = _Var(True)
tool.vt_bluemoon = _Var(False)
signature = tool._vanilla_tweaks_signature()
self.assertEqual(signature["selected_patch_normalization"], 4)
self.assertEqual(signature["farclip"], 3000)
tool.vt_farclip = _Var(1500)
runtime_changed = tool._vanilla_tweaks_signature()
self.assertEqual(runtime_changed["farclip"], 3000)
self.assertEqual(signature, runtime_changed)
def test_existing_farclip_cvar_is_replaced_without_touching_other_settings(self):
tool = self._tool(777)
with tempfile.TemporaryDirectory() as root:
wtf_dir = os.path.join(root, "WTF")
os.makedirs(wtf_dir)
config_path = os.path.join(wtf_dir, "Config.wtf")
with open(config_path, "w", encoding="utf-8") as handle:
handle.write('SET locale "enUS"\nSET farclip "2100"\nSET gxWindow "1"\n')
tool._configure_farclip_cvar(root)
with open(config_path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertIn('SET locale "enUS"', result)
self.assertIn('SET gxWindow "1"', result)
self.assertIn('SET farclip "777"', result)
self.assertNotIn('SET farclip "2100"', result)
def test_missing_farclip_cvar_is_appended(self):
tool = self._tool(1500)
with tempfile.TemporaryDirectory() as root:
wtf_dir = os.path.join(root, "WTF")
os.makedirs(wtf_dir)
config_path = os.path.join(wtf_dir, "Config.wtf")
with open(config_path, "w", encoding="utf-8") as handle:
handle.write('SET locale "enUS"')
tool._configure_farclip_cvar(root)
with open(config_path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertEqual(
result,
'SET locale "enUS"\nSET farclip "1500"\n',
)
def test_missing_config_wtf_is_created(self):
tool = self._tool(1000)
with tempfile.TemporaryDirectory() as root:
tool._configure_farclip_cvar(root)
config_path = os.path.join(root, "WTF", "Config.wtf")
with open(config_path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertEqual(result, 'SET farclip "1000"\n')
def test_farclip_cvar_rejects_value_above_fixed_ceiling(self):
tool = self._tool(3001)
with tempfile.TemporaryDirectory() as root:
with self.assertRaisesRegex(RuntimeError, "3000"):
tool._configure_farclip_cvar(root)
self.assertFalse(os.path.exists(os.path.join(root, "WTF", "Config.wtf")))
def test_configure_script_memory_also_synchronizes_farclip(self):
tool = self._tool(777)
tool.core_plugins = {"SuperWoWhook.dll": _Var(False)}
with mock.patch.object(
setup_tool_dynamic._ModernWowSetupToolCore,
"configure_script_memory",
) as parent_configure, mock.patch.object(
tool,
"_configure_farclip_cvar",
) as sync_farclip:
tool.configure_script_memory("C:/WoW")
parent_configure.assert_called_once_with("C:/WoW")
sync_farclip.assert_called_once_with("C:/WoW")
if __name__ == "__main__":
unittest.main()
+301
View File
@@ -0,0 +1,301 @@
import math
import os
import struct
import tempfile
import unittest
from unittest import mock
import remote_packages
import setup_tool_dynamic as dynamic
class _Var:
def __init__(self, value):
self.value = value
def get(self):
return self.value
def _make_tool():
tool = object.__new__(dynamic.ModernWowSetupTool)
tool.vt_fov = _Var(1.9199)
tool.vt_farclip = _Var(1500)
tool.vt_frill = _Var(300)
tool.vt_nameplate = _Var(41)
tool.vt_maxcam = _Var(100)
tool.vt_soundchan = _Var(64)
tool.vt_quickloot = _Var(True)
tool.vt_bg_sound = _Var(True)
tool.vt_laa = _Var(True)
tool.vt_cam_fix = _Var(True)
tool.vt_custom_glues = _Var(True)
return tool
def _base_client_data():
data = bytearray(0x46795C)
data[:2] = b"MZ"
data[
dynamic._CLIENT_BUILD_OFFSET:
dynamic._CLIENT_BUILD_OFFSET + len(dynamic._CLIENT_BUILD)
] = dynamic._CLIENT_BUILD
data[
dynamic._CLIENT_VERSION_OFFSET:
dynamic._CLIENT_VERSION_OFFSET + len(dynamic._CLIENT_VERSION)
] = dynamic._CLIENT_VERSION
data[0x0C1ECF:0x0C1ED1] = b"\x74\x10"
data[0x0C2B25:0x0C2B27] = b"\x74\x0B"
data[0x3A4869] = 0x14
data[0x126:0x128] = b"\x0F\x01"
for offset, original, _patched in dynamic._CAMERA_REGIONS:
data[offset:offset + len(original)] = original
for offset, original, _patched in dynamic._CUSTOM_GLUES_SITES:
data[offset] = original
struct.pack_into("<f", data, 0x4089B4, math.pi / 2)
struct.pack_into("<f", data, 0x40FED8, 777.0)
struct.pack_into("<f", data, 0x467958, 70.0)
struct.pack_into("<f", data, 0x40C448, 20.0)
struct.pack_into("<f", data, 0x4089A4, 50.0)
data[0x435D38:0x435D3C] = b"12\x00\x00"
return data
def _write_valid_mpq(path):
archive_size = 96
header = b"MPQ\x1A" + struct.pack(
"<IIHHIIII",
32,
archive_size,
0,
3,
32,
48,
1,
1,
)
data = bytearray(archive_size)
data[:32] = header
with open(path, "wb") as handle:
handle.write(data)
class MpqValidationTests(unittest.TestCase):
def test_strict_mpq_accepts_structurally_valid_classic_header(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "patch.mpq")
_write_valid_mpq(path)
dynamic._strict_verify_mpq(path)
def test_strict_mpq_rejects_magic_only_or_truncated_archive(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "patch.mpq")
with open(path, "wb") as handle:
handle.write(b"MPQ\x1A" + b"\x00" * 60)
with self.assertRaises(remote_packages.RemotePackageError):
dynamic._strict_verify_mpq(path)
def test_visual_install_scopes_strict_hooks_and_restores_helpers(self):
tool = object.__new__(dynamic.ModernWowSetupTool)
original_verify = remote_packages._verify_mpq
original_current = remote_packages.managed_mpq_is_current
calls = []
def fake_visual_install(_instance, target):
calls.append(target)
self.assertIs(remote_packages._verify_mpq, dynamic._strict_verify_mpq)
self.assertIs(
remote_packages.managed_mpq_is_current,
dynamic._strict_managed_mpq_is_current,
)
return "done"
with mock.patch.object(
dynamic._ModernWowSetupToolCore,
"configure_visual_audio",
fake_visual_install,
):
self.assertEqual(tool.configure_visual_audio("GAME"), "done")
self.assertEqual(calls, ["GAME"])
self.assertIs(remote_packages._verify_mpq, original_verify)
self.assertIs(remote_packages.managed_mpq_is_current, original_current)
def test_strict_current_mpq_check_uses_full_header_validation(self):
with tempfile.TemporaryDirectory() as temp:
data_dir = os.path.join(temp, "Data")
os.makedirs(data_dir)
path = os.path.join(data_dir, "patch-X.mpq")
with open(path, "wb") as handle:
handle.write(b"MPQ\x1A" + b"\x00" * 60)
with mock.patch.object(
remote_packages,
"managed_mod_is_current",
return_value=True,
), mock.patch.object(
remote_packages,
"_load_managed_manifest",
return_value=[os.path.join("Data", "patch-X.mpq")],
):
self.assertFalse(
dynamic._strict_managed_mpq_is_current(
temp,
"visual_test",
"1",
)
)
class VanillaTweaksSourceValidationTests(unittest.TestCase):
def test_community_numeric_values_remain_compatible(self):
tool = _make_tool()
data = _base_client_data()
# Legitimate community launchers can already carry user-selected values.
struct.pack_into("<f", data, 0x4089B4, math.radians(100))
struct.pack_into("<f", data, 0x40FED8, 2500.0)
struct.pack_into("<f", data, 0x467958, 120.0)
struct.pack_into("<f", data, 0x40C448, 35.0)
struct.pack_into("<f", data, 0x4089A4, 80.0)
data[0x435D38:0x435D3C] = b"32\x00\x00"
before = bytes(data)
tool._validate_client_identity(data)
tool._validate_vanilla_tweaks_state(
data,
allow_foreign_custom_glues=True,
)
tool._validate_source_numeric_values(data)
self.assertEqual(bytes(data), before)
def test_fixed_build_string_does_not_block_compatible_patch_sites(self):
tool = _make_tool()
data = _base_client_data()
data[
dynamic._CLIENT_BUILD_OFFSET:
dynamic._CLIENT_BUILD_OFFSET + len(dynamic._CLIENT_BUILD)
] = b"9999"
tool._validate_client_identity(data)
tool._validate_vanilla_tweaks_state(
data,
allow_foreign_custom_glues=True,
)
def test_out_of_range_source_numeric_state_is_rejected(self):
tool = _make_tool()
data = _base_client_data()
struct.pack_into("<f", data, 0x40FED8, 50000.0)
with self.assertRaisesRegex(RuntimeError, "Farclip"):
tool._validate_source_numeric_values(data)
def test_normalization_overwrites_intermediate_numeric_values(self):
tool = _make_tool()
data = _base_client_data()
# The upstream patcher may temporarily emit different numeric values.
# They are not trusted as final output: the Tool overwrites them below.
struct.pack_into("<f", data, 0x4089B4, 2.5)
struct.pack_into("<f", data, 0x40FED8, 2345.0)
struct.pack_into("<f", data, 0x467958, 444.0)
struct.pack_into("<f", data, 0x40C448, 88.0)
struct.pack_into("<f", data, 0x4089A4, 123.0)
data[0x435D38:0x435D3C] = b"96\x00\x00"
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "WoW_Modernized.exe")
with open(path, "wb") as handle:
handle.write(data)
tool._normalize_selected_vanilla_tweaks_output(path)
with open(path, "rb") as handle:
result = handle.read()
self.assertAlmostEqual(
struct.unpack_from("<f", result, 0x4089B4)[0],
1.9199,
places=4,
)
self.assertEqual(struct.unpack_from("<f", result, 0x40FED8)[0], 3000.0)
self.assertEqual(struct.unpack_from("<f", result, 0x467958)[0], 300.0)
self.assertEqual(struct.unpack_from("<f", result, 0x40C448)[0], 41.0)
self.assertEqual(struct.unpack_from("<f", result, 0x4089A4)[0], 100.0)
self.assertEqual(result[0x435D38:0x435D3C], b"64\x00\x00")
class InstallationOrderingTests(unittest.TestCase):
def test_vanilla_tweaks_runs_once_before_first_install_write(self):
tool = object.__new__(dynamic.ModernWowSetupTool)
events = []
original_clean = lambda target: events.append(("clean", target))
original_vt = lambda target: (
events.append(("vanilla_tweaks", target))
or os.path.join(target, "WoW_Modernized.exe")
)
tool.clean_unselected_files = original_clean
tool.run_vanilla_tweaks = original_vt
def fake_base_install(instance):
events.append(("base", "validated"))
instance.clean_unselected_files("GAME")
events.append(("base", "after_clean"))
instance.run_vanilla_tweaks("GAME")
return "done"
with mock.patch.object(
dynamic._ModernWowSetupToolCore,
"run_installation",
fake_base_install,
):
self.assertEqual(tool.run_installation(), "done")
self.assertEqual(
events,
[
("base", "validated"),
("vanilla_tweaks", "GAME"),
("clean", "GAME"),
("base", "after_clean"),
],
)
self.assertIs(tool.clean_unselected_files, original_clean)
self.assertIs(tool.run_vanilla_tweaks, original_vt)
def test_vanilla_tweaks_failure_prevents_first_install_write(self):
tool = object.__new__(dynamic.ModernWowSetupTool)
events = []
def original_clean(target):
events.append(("clean", target))
def original_vt(_target):
events.append(("vanilla_tweaks", "failed"))
raise RuntimeError("patcher failed")
tool.clean_unselected_files = original_clean
tool.run_vanilla_tweaks = original_vt
def fake_base_install(instance):
instance.clean_unselected_files("GAME")
self.fail("clean_unselected_files should not complete after VT failure")
with mock.patch.object(
dynamic._ModernWowSetupToolCore,
"run_installation",
fake_base_install,
):
with self.assertRaisesRegex(RuntimeError, "patcher failed"):
tool.run_installation()
self.assertEqual(events, [("vanilla_tweaks", "failed")])
self.assertIs(tool.clean_unselected_files, original_clean)
self.assertIs(tool.run_vanilla_tweaks, original_vt)
if __name__ == "__main__":
unittest.main()
+193
View File
@@ -0,0 +1,193 @@
import os
import tempfile
import unittest
from unittest import mock
import setup_tool_dynamic as dynamic
class _Var:
def __init__(self, value):
self.value = value
def get(self):
return self.value
class _Root:
def configure(self, **_kwargs):
return None
def update_idletasks(self):
return None
def _make_runtime_tool(target, events):
"""Create the minimum state needed to execute the real base run_installation."""
tool = object.__new__(dynamic.ModernWowSetupTool)
tool._install_in_progress = False
tool.wow_dir = _Var(target)
tool.root = _Root()
tool._close_download_progress = lambda: None
tool.validate_installation_dir = lambda value: (
events.append(("validate_directory", value)) or True
)
tool.validate_limits = lambda: (
events.append(("validate_limits", target)) or True
)
tool.validate_plugin_conflicts = lambda: (
events.append(("preflight", target)) or True
)
def record_target(name):
return lambda value: events.append((name, value))
tool.copy_base_files = record_target("copy_base_files")
tool.configure_dxvk = record_target("configure_dxvk")
tool.configure_plugins = record_target("configure_plugins")
tool.configure_autologin_encryption = lambda: events.append(
("configure_autologin_encryption", target)
)
tool.configure_visual_audio = record_target("configure_visual_audio")
tool.configure_script_memory = record_target("configure_script_memory")
tool.configure_wdb_cache = record_target("configure_wdb_cache")
tool.apply_process_mitigations = record_target("apply_process_mitigations")
tool.create_launcher_shortcut = record_target("create_launcher_shortcut")
tool.cleanup_legacy_outputs = record_target("cleanup_legacy_outputs")
tool.save_settings = record_target("save_settings")
return tool
class RealInstallationOrderingTests(unittest.TestCase):
def test_real_installation_runs_vanilla_tweaks_once_before_every_write(self):
events = []
with tempfile.TemporaryDirectory() as target:
initial_entries = set(os.listdir(target))
tool = _make_runtime_tool(target, events)
def fake_clean(_instance, value):
events.append(("clean_unselected_files", value))
def fake_vanilla_tweaks(_instance, value):
events.append(("vanilla_tweaks", value))
return os.path.join(value, "WoW_Modernized.exe")
with mock.patch.object(
dynamic._ModernWowSetupToolCore,
"clean_unselected_files",
fake_clean,
create=True,
), mock.patch.object(
dynamic._ModernWowSetupToolCore,
"run_vanilla_tweaks",
fake_vanilla_tweaks,
create=True,
), mock.patch(
"setup_tool.messagebox.showinfo"
) as showinfo, mock.patch(
"setup_tool.messagebox.showerror"
) as showerror:
self.assertIsNone(tool.run_installation())
self.assertEqual(
events,
[
("validate_directory", target),
("validate_limits", target),
("preflight", target),
("vanilla_tweaks", target),
("clean_unselected_files", target),
("copy_base_files", target),
("configure_dxvk", target),
("configure_plugins", target),
("configure_autologin_encryption", target),
("configure_visual_audio", target),
("configure_script_memory", target),
("configure_wdb_cache", target),
("apply_process_mitigations", target),
("create_launcher_shortcut", target),
("cleanup_legacy_outputs", target),
("save_settings", target),
],
)
showinfo.assert_called_once()
showerror.assert_not_called()
# run_installation installs temporary instance wrappers. They
# must be gone when Apply returns so a later Apply starts clean.
self.assertNotIn("clean_unselected_files", tool.__dict__)
self.assertNotIn("run_vanilla_tweaks", tool.__dict__)
self.assertIs(tool.clean_unselected_files.__func__, fake_clean)
self.assertIs(tool.run_vanilla_tweaks.__func__, fake_vanilla_tweaks)
# All real mutators above were replaced with record-only callbacks.
# Any newly-added direct filesystem write in run_installation would
# make this integration-style test visibly change the temp folder.
self.assertEqual(set(os.listdir(target)), initial_entries)
self.assertFalse(tool._install_in_progress)
def test_real_installation_vanilla_tweaks_failure_blocks_all_writes(self):
events = []
with tempfile.TemporaryDirectory() as target:
initial_entries = set(os.listdir(target))
tool = _make_runtime_tool(target, events)
def fake_clean(_instance, value):
events.append(("clean_unselected_files", value))
def failing_vanilla_tweaks(_instance, value):
events.append(("vanilla_tweaks_failed", value))
raise RuntimeError("patcher failed before install writes")
with mock.patch.object(
dynamic._ModernWowSetupToolCore,
"clean_unselected_files",
fake_clean,
create=True,
), mock.patch.object(
dynamic._ModernWowSetupToolCore,
"run_vanilla_tweaks",
failing_vanilla_tweaks,
create=True,
), mock.patch(
"setup_tool.messagebox.showinfo"
) as showinfo, mock.patch(
"setup_tool.messagebox.showerror"
) as showerror:
self.assertIsNone(tool.run_installation())
self.assertEqual(
events,
[
("validate_directory", target),
("validate_limits", target),
("preflight", target),
("vanilla_tweaks_failed", target),
],
)
showinfo.assert_not_called()
showerror.assert_called_once()
self.assertEqual(showerror.call_args.args[0], "Installation Error")
self.assertIn(
"patcher failed before install writes",
showerror.call_args.args[1],
)
# The temporary wrappers must also be restored on the error path.
self.assertNotIn("clean_unselected_files", tool.__dict__)
self.assertNotIn("run_vanilla_tweaks", tool.__dict__)
self.assertIs(tool.clean_unselected_files.__func__, fake_clean)
self.assertIs(
tool.run_vanilla_tweaks.__func__,
failing_vanilla_tweaks,
)
self.assertEqual(set(os.listdir(target)), initial_entries)
self.assertFalse(tool._install_in_progress)
if __name__ == "__main__":
unittest.main()
+1373 -1
View File
File diff suppressed because it is too large Load Diff
+171
View File
@@ -0,0 +1,171 @@
import os
import struct
import tempfile
import unittest
import remote_packages
import setup_tool_dynamic as dynamic
def _classic_mpq_bytes(
*,
archive_size=96,
hash_table_offset=32,
block_table_offset=48,
inner_magic=b"MPQ\x1A",
sector_size_shift=3,
):
header = inner_magic + struct.pack(
"<IIHHIIII",
32,
archive_size,
0,
sector_size_shift,
hash_table_offset,
block_table_offset,
1,
1,
)
data = bytearray(archive_size)
data[:32] = header
return data
def _write_wrapped_mpq(
path,
*,
header_offset=32,
user_header_size=16,
user_data_size=None,
inner_magic=b"MPQ\x1A",
hash_table_offset=32,
block_table_offset=48,
sector_size_shift=3,
):
inner = _classic_mpq_bytes(
inner_magic=inner_magic,
hash_table_offset=hash_table_offset,
block_table_offset=block_table_offset,
sector_size_shift=sector_size_shift,
)
total_size = max(16, header_offset + len(inner))
data = bytearray(total_size)
if user_data_size is None:
user_data_size = max(0, header_offset - user_header_size)
data[:16] = b"MPQ\x1B" + struct.pack(
"<III",
user_data_size,
header_offset,
user_header_size,
)
if 0 <= header_offset <= total_size - len(inner):
data[header_offset:header_offset + len(inner)] = inner
with open(path, "wb") as handle:
handle.write(data)
class ExecutableCompatibilityTests(unittest.TestCase):
def test_fixed_build_version_identity_guard_is_disabled(self):
data = bytearray(0x46795C + 16)
dynamic.ModernWowSetupTool._validate_client_identity(data)
class WrappedMpqValidationTests(unittest.TestCase):
def test_accepts_valid_user_data_wrapped_mpq(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "wrapped.mpq")
_write_wrapped_mpq(path)
dynamic._strict_verify_mpq(path)
def test_rejects_user_data_wrapper_with_invalid_archive_offset(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "wrapped.mpq")
data = bytearray(128)
data[:16] = b"MPQ\x1B" + struct.pack("<III", 0, 8, 16)
with open(path, "wb") as handle:
handle.write(data)
with self.assertRaisesRegex(
remote_packages.RemotePackageError,
"invalid nested archive offset",
):
dynamic._strict_verify_mpq(path)
def test_rejects_user_data_wrapper_with_out_of_bounds_archive_offset(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "wrapped.mpq")
data = bytearray(128)
data[:16] = b"MPQ\x1B" + struct.pack("<III", 0, 120, 16)
with open(path, "wb") as handle:
handle.write(data)
with self.assertRaisesRegex(
remote_packages.RemotePackageError,
"invalid nested archive offset",
):
dynamic._strict_verify_mpq(path)
def test_rejects_user_data_wrapper_with_invalid_header_size(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "wrapped.mpq")
data = bytearray(128)
data[:16] = b"MPQ\x1B" + struct.pack("<III", 24, 32, 8)
with open(path, "wb") as handle:
handle.write(data)
with self.assertRaisesRegex(
remote_packages.RemotePackageError,
"invalid nested archive offset",
):
dynamic._strict_verify_mpq(path)
def test_rejects_user_data_size_smaller_than_user_header(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "wrapped.mpq")
_write_wrapped_mpq(path, user_data_size=8)
with self.assertRaisesRegex(
remote_packages.RemotePackageError,
"invalid user-data size",
):
dynamic._strict_verify_mpq(path)
def test_rejects_user_data_size_beyond_nested_archive_offset(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "wrapped.mpq")
_write_wrapped_mpq(path, user_data_size=40)
with self.assertRaisesRegex(
remote_packages.RemotePackageError,
"invalid user-data size",
):
dynamic._strict_verify_mpq(path)
def test_rejects_wrapper_with_corrupt_nested_mpq(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "wrapped.mpq")
_write_wrapped_mpq(path, inner_magic=b"NOPE")
with self.assertRaisesRegex(
remote_packages.RemotePackageError,
"not a valid MPQ archive",
):
dynamic._strict_verify_mpq(path)
def test_rejects_wrapped_mpq_with_out_of_bounds_tables(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "wrapped.mpq")
_write_wrapped_mpq(path, block_table_offset=88)
with self.assertRaisesRegex(
remote_packages.RemotePackageError,
"out-of-bounds block table",
):
dynamic._strict_verify_mpq(path)
def test_rejects_zero_sector_size_shift(self):
with tempfile.TemporaryDirectory() as temp:
path = os.path.join(temp, "wrapped.mpq")
_write_wrapped_mpq(path, sector_size_shift=0)
with self.assertRaisesRegex(
remote_packages.RemotePackageError,
"invalid sector-size shift",
):
dynamic._strict_verify_mpq(path)
if __name__ == "__main__":
unittest.main()
+117
View File
@@ -0,0 +1,117 @@
import os
import stat
import tempfile
import unittest
import setup_tool_dynamic as dynamic
class FakeVar:
def __init__(self, value):
self.value = value
def get(self):
return self.value
class SuperWowFovSyncTests(unittest.TestCase):
def _tool(self, *, superwow=True, fov=1.9199, farclip=777, script_memory=False):
tool = object.__new__(dynamic.ModernWowSetupTool)
tool.core_plugins = {
"SuperWoWhook.dll": FakeVar(superwow),
}
tool.vt_fov = FakeVar(fov)
tool.vt_farclip = FakeVar(farclip)
tool.vt_script_memory = FakeVar(script_memory)
return tool
@staticmethod
def _config_path(root):
return os.path.join(root, "WTF", "Config.wtf")
def test_superwow_enabled_updates_existing_fov(self):
with tempfile.TemporaryDirectory() as root:
path = self._config_path(root)
os.makedirs(os.path.dirname(path))
with open(path, "w", encoding="utf-8") as handle:
handle.write(
'SET gxWindow "1"\n'
'SET FoV "1.5"\n'
'SET locale "enUS"\n'
)
self._tool().configure_script_memory(root)
with open(path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertIn('SET FoV "1.9199"', result)
self.assertIn('SET farclip "777"', result)
self.assertIn('SET gxWindow "1"', result)
self.assertIn('SET locale "enUS"', result)
self.assertNotIn('SET FoV "1.5"', result)
def test_superwow_enabled_creates_missing_fov_setting(self):
with tempfile.TemporaryDirectory() as root:
path = self._config_path(root)
self._tool(fov=2.1).configure_script_memory(root)
with open(path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertEqual(
result,
'SET FoV "2.1"\nSET farclip "777"\n',
)
def test_superwow_disabled_leaves_existing_fov_untouched(self):
with tempfile.TemporaryDirectory() as root:
path = self._config_path(root)
os.makedirs(os.path.dirname(path))
original = 'SET FoV "1.5"\nSET locale "frFR"\n'
with open(path, "w", encoding="utf-8") as handle:
handle.write(original)
self._tool(superwow=False).configure_script_memory(root)
with open(path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertIn('SET FoV "1.5"', result)
self.assertIn('SET locale "frFR"', result)
self.assertIn('SET farclip "777"', result)
self.assertNotIn('SET FoV "1.9199"', result)
def test_script_memory_and_superwow_fov_are_both_applied(self):
with tempfile.TemporaryDirectory() as root:
path = self._config_path(root)
os.makedirs(os.path.dirname(path))
with open(path, "w", encoding="utf-8") as handle:
handle.write('SET locale "enUS"\n')
self._tool(script_memory=True).configure_script_memory(root)
with open(path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertIn('SET scriptMemory "0"', result)
self.assertIn('SET FoV "1.9199"', result)
self.assertIn('SET farclip "777"', result)
def test_readonly_config_mode_is_restored(self):
with tempfile.TemporaryDirectory() as root:
path = self._config_path(root)
os.makedirs(os.path.dirname(path))
with open(path, "w", encoding="utf-8") as handle:
handle.write('SET FoV "1.5"\n')
os.chmod(path, stat.S_IREAD)
self._tool().configure_script_memory(root)
mode = os.stat(path).st_mode
self.assertFalse(mode & stat.S_IWRITE)
with open(path, "r", encoding="utf-8") as handle:
result = handle.read()
self.assertIn('SET FoV "1.9199"', result)
self.assertIn('SET farclip "777"', result)
if __name__ == "__main__":
unittest.main()
+637
View File
@@ -0,0 +1,637 @@
import os
import struct
import tempfile
import unittest
from unittest import mock
import setup_tool_dynamic
from setup_tool import WowSetupTool
from setup_tool_dynamic import ModernWowSetupTool
class FakeVar:
def __init__(self, value):
self.value = value
def get(self):
return self.value
def set(self, value):
self.value = value
class VanillaTweaksNormalizationTests(unittest.TestCase):
def _tool(
self,
*,
fov,
sound,
quickloot,
background,
farclip=777,
frill=300,
nameplate=41,
maxcam=100,
laa=True,
camera=True,
custom_glues=True,
crossfaction=False,
bluemoon=False,
):
tool = ModernWowSetupTool.__new__(ModernWowSetupTool)
tool.vt_fov = FakeVar(fov)
tool.vt_soundchan = FakeVar(sound)
tool.vt_quickloot = FakeVar(quickloot)
tool.vt_bg_sound = FakeVar(background)
tool.vt_farclip = FakeVar(farclip)
tool.vt_frill = FakeVar(frill)
tool.vt_nameplate = FakeVar(nameplate)
tool.vt_maxcam = FakeVar(maxcam)
tool.vt_laa = FakeVar(laa)
tool.vt_cam_fix = FakeVar(camera)
tool.vt_crossfaction_res = FakeVar(crossfaction)
tool.vt_custom_glues = FakeVar(custom_glues)
tool.vt_bluemoon = FakeVar(bluemoon)
tool.core_plugins = {"SuperWoWhook.dll": FakeVar(True)}
return tool
def _write_exe(
self,
root,
quick1,
quick2,
background,
fov,
sound,
*,
farclip=777.0,
frill=70.0,
nameplate=20.0,
maxcam=50.0,
laa_patched=False,
camera_patched=False,
custom_patched=False,
crossfaction_byte=0x01,
bluemoon_bytes=None,
filename="WoW_Modernized.exe",
):
data = bytearray(0x46795C + 16)
# Use a minimal but structurally valid PE32 header. The selected PE
# offset deliberately places COFF Characteristics at 0x126, matching
# the real client's Large Address Aware patch location.
pe_offset = 0x110
data[:2] = b"MZ"
struct.pack_into("<I", data, 0x3C, pe_offset)
data[pe_offset:pe_offset + 4] = b"PE\x00\x00"
struct.pack_into("<H", data, pe_offset + 4, 0x014C)
struct.pack_into("<H", data, pe_offset + 6, 3)
struct.pack_into("<H", data, pe_offset + 20, 0x00E0)
struct.pack_into("<H", data, pe_offset + 24, 0x010B)
data[
setup_tool_dynamic._CLIENT_BUILD_OFFSET:
setup_tool_dynamic._CLIENT_BUILD_OFFSET
+ len(setup_tool_dynamic._CLIENT_BUILD)
] = setup_tool_dynamic._CLIENT_BUILD
data[
setup_tool_dynamic._CLIENT_VERSION_OFFSET:
setup_tool_dynamic._CLIENT_VERSION_OFFSET
+ len(setup_tool_dynamic._CLIENT_VERSION)
] = setup_tool_dynamic._CLIENT_VERSION
data[0x0C1ECF:0x0C1ED1] = quick1
data[0x0C2B25:0x0C2B27] = quick2
data[0x3A4869] = background
data[0x126:0x128] = b"\x2F\x01" if laa_patched else b"\x0F\x01"
for offset, original, patched in setup_tool_dynamic._CAMERA_REGIONS:
selected = patched if camera_patched else original
data[offset:offset + len(selected)] = selected
for offset, original, patched in setup_tool_dynamic._CUSTOM_GLUES_SITES:
data[offset] = patched if custom_patched else original
struct.pack_into("<f", data, 0x4089B4, fov)
struct.pack_into("<f", data, 0x40FED8, farclip)
struct.pack_into("<f", data, 0x467958, frill)
struct.pack_into("<f", data, 0x40C448, nameplate)
struct.pack_into("<f", data, 0x4089A4, maxcam)
data[0x435D38:0x435D3C] = sound
data[0x2067DE] = crossfaction_byte
if bluemoon_bytes is None:
bluemoon_bytes = bytes(range(1, 14))
data[0x3E5B83:0x3E5B83 + len(bluemoon_bytes)] = bluemoon_bytes
# Unrelated data must survive normalization byte-for-byte.
data[0x123456:0x12345E] = b"KEEPTHIS"
path = os.path.join(root, filename)
with open(path, "wb") as handle:
handle.write(data)
return path
@staticmethod
def _read_float(data, offset):
return struct.unpack("<f", data[offset:offset + 4])[0]
@staticmethod
def _write_custom_state(path, values):
with open(path, "r+b") as handle:
for value, (offset, _original, _patched) in zip(
values,
setup_tool_dynamic._CUSTOM_GLUES_SITES,
):
handle.seek(offset)
handle.write(bytes((value,)))
@staticmethod
def _read_custom_state(data):
return tuple(
data[offset]
for offset, _original, _patched in setup_tool_dynamic._CUSTOM_GLUES_SITES
)
def test_disabled_selections_restore_inherited_patches(self):
tool = self._tool(
fov=1.5708,
sound=12,
quickloot=False,
background=False,
farclip=777,
frill=70,
nameplate=20,
maxcam=50,
laa=False,
camera=False,
custom_glues=False,
)
with tempfile.TemporaryDirectory() as root:
path = self._write_exe(
root,
b"\x75\x10",
b"\x75\x0B",
0x27,
1.919862,
b"64\x00\x00",
farclip=3000.0,
frill=300.0,
nameplate=41.0,
maxcam=100.0,
laa_patched=True,
camera_patched=True,
custom_patched=True,
)
tool._normalize_selected_vanilla_tweaks_output(path)
with open(path, "rb") as handle:
result = handle.read()
self.assertEqual(result[0x0C1ECF:0x0C1ED1], b"\x74\x10")
self.assertEqual(result[0x0C2B25:0x0C2B27], b"\x74\x0B")
self.assertEqual(result[0x3A4869], 0x14)
self.assertEqual(result[0x126:0x128], b"\x0F\x01")
self.assertAlmostEqual(self._read_float(result, 0x4089B4), 1.5708, places=4)
self.assertEqual(self._read_float(result, 0x40FED8), 3000.0)
self.assertEqual(self._read_float(result, 0x467958), 70.0)
self.assertEqual(self._read_float(result, 0x40C448), 20.0)
self.assertEqual(self._read_float(result, 0x4089A4), 50.0)
self.assertEqual(result[0x435D38:0x435D3C], b"12\x00\x00")
for offset, original, _patched in setup_tool_dynamic._CAMERA_REGIONS:
self.assertEqual(result[offset:offset + len(original)], original)
for offset, original, _patched in setup_tool_dynamic._CUSTOM_GLUES_SITES:
self.assertEqual(result[offset], original)
self.assertEqual(result[0x123456:0x12345E], b"KEEPTHIS")
def test_enabled_selections_override_vanilla_input(self):
tool = self._tool(
fov=1.9199,
sound=64,
quickloot=True,
background=True,
farclip=1500,
frill=300,
nameplate=41,
maxcam=100,
laa=True,
camera=True,
custom_glues=True,
)
with tempfile.TemporaryDirectory() as root:
path = self._write_exe(
root,
b"\x90\x90",
b"\x90\x90",
0x14,
1.5707963705,
b"12\x00\x00",
)
tool._normalize_selected_vanilla_tweaks_output(path)
with open(path, "rb") as handle:
result = handle.read()
self.assertEqual(result[0x0C1ECF:0x0C1ED1], b"\x75\x10")
self.assertEqual(result[0x0C2B25:0x0C2B27], b"\x75\x0B")
self.assertEqual(result[0x3A4869], 0x27)
self.assertEqual(result[0x126:0x128], b"\x2F\x01")
self.assertAlmostEqual(self._read_float(result, 0x4089B4), 1.9199, places=4)
self.assertEqual(self._read_float(result, 0x40FED8), 3000.0)
self.assertEqual(self._read_float(result, 0x467958), 300.0)
self.assertEqual(self._read_float(result, 0x40C448), 41.0)
self.assertEqual(self._read_float(result, 0x4089A4), 100.0)
self.assertEqual(result[0x435D38:0x435D3C], b"64\x00\x00")
for offset, _original, patched in setup_tool_dynamic._CAMERA_REGIONS:
self.assertEqual(result[offset:offset + len(patched)], patched)
for offset, _original, patched in setup_tool_dynamic._CUSTOM_GLUES_SITES:
self.assertEqual(result[offset], patched)
self.assertEqual(result[0x123456:0x12345E], b"KEEPTHIS")
def test_unknown_quickloot_bytes_fail_safe(self):
tool = self._tool(
fov=1.9199,
sound=64,
quickloot=True,
background=True,
)
with tempfile.TemporaryDirectory() as root:
path = self._write_exe(
root,
b"\xEB\x10",
b"\x74\x0B",
0x14,
1.5708,
b"12\x00\x00",
)
with self.assertRaisesRegex(RuntimeError, "Unexpected QuickLoot bytes"):
tool._normalize_selected_vanilla_tweaks_output(path)
def test_unknown_custom_glues_bytes_fail_safe_without_source_proof(self):
tool = self._tool(
fov=1.9199,
sound=64,
quickloot=True,
background=True,
custom_glues=False,
)
with tempfile.TemporaryDirectory() as root:
path = self._write_exe(
root,
b"\x74\x10",
b"\x74\x0B",
0x14,
1.5708,
b"12\x00\x00",
)
with open(path, "r+b") as handle:
handle.seek(0x2F113A)
handle.write(b"\xAA")
with self.assertRaisesRegex(RuntimeError, "Unexpected Custom GlueXML bytes"):
tool._normalize_selected_vanilla_tweaks_output(path)
def test_source_preflight_accepts_and_records_foreign_custom_glues(self):
tool = self._tool(
fov=1.9199,
sound=64,
quickloot=True,
background=True,
custom_glues=True,
)
foreign = (0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xF1)
with tempfile.TemporaryDirectory() as root:
source = self._write_exe(
root,
b"\x75\x10",
b"\x75\x0B",
0x27,
1.919862,
b"12\x00\x00",
farclip=3000.0,
frill=300.0,
nameplate=41.0,
maxcam=50.0,
laa_patched=True,
camera_patched=True,
filename="WoW.exe",
)
self._write_custom_state(source, foreign)
preserved = tool._preflight_vanilla_tweaks_source(root)
self.assertEqual(preserved, foreign)
self.assertEqual(tool._vt_preserved_custom_glues, foreign)
def test_blue_moon_and_crossfaction_keep_legacy_output(self):
tool = self._tool(
fov=1.9199,
sound=64,
quickloot=True,
background=True,
crossfaction=True,
bluemoon=True,
)
blue_bytes = bytes.fromhex("a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad")
with tempfile.TemporaryDirectory() as root:
path = self._write_exe(
root,
b"\x74\x10",
b"\x74\x0B",
0x14,
1.5708,
b"12\x00\x00",
crossfaction_byte=0x7A,
bluemoon_bytes=blue_bytes,
)
tool._normalize_selected_vanilla_tweaks_output(path)
with open(path, "rb") as handle:
result = handle.read()
self.assertEqual(result[0x2067DE], 0x7A)
self.assertEqual(
result[0x3E5B83:0x3E5B83 + len(blue_bytes)],
blue_bytes,
)
def test_source_preflight_rejects_unknown_quickloot_before_patcher(self):
tool = self._tool(
fov=1.9199,
sound=64,
quickloot=True,
background=True,
)
with tempfile.TemporaryDirectory() as root:
self._write_exe(
root,
b"\xEB\x10",
b"\x74\x0B",
0x14,
1.5708,
b"12\x00\x00",
filename="WoW.exe",
)
output_exe = os.path.join(root, "WoW_Modernized.exe")
previous_output = b"known-good existing modernized executable"
with open(output_exe, "wb") as handle:
handle.write(previous_output)
patcher = mock.Mock()
with mock.patch.object(WowSetupTool, "run_vanilla_tweaks", new=patcher):
with self.assertRaisesRegex(RuntimeError, "Unexpected QuickLoot bytes"):
tool._run_vanilla_tweaks_transactional(
root,
tweaks_exe="fake-vanilla-tweaks.exe",
modern_cli=True,
)
patcher.assert_not_called()
with open(output_exe, "rb") as handle:
self.assertEqual(handle.read(), previous_output)
self.assertFalse(os.path.exists(output_exe + ".modernization-new"))
self.assertFalse(
any(name.startswith(".modernization-vt-") for name in os.listdir(root))
)
def test_transaction_keeps_existing_output_when_normalization_fails(self):
tool = self._tool(
fov=1.9199,
sound=64,
quickloot=True,
background=True,
)
with tempfile.TemporaryDirectory() as root:
self._write_exe(
root,
b"\x74\x10",
b"\x74\x0B",
0x14,
1.5708,
b"12\x00\x00",
filename="WoW.exe",
)
output_exe = os.path.join(root, "WoW_Modernized.exe")
previous_output = b"known-good existing modernized executable"
with open(output_exe, "wb") as handle:
handle.write(previous_output)
def fake_run(_tool, staging_target, tweaks_exe=None, modern_cli=False):
return self._write_exe(
staging_target,
b"\xEB\x10",
b"\x74\x0B",
0x14,
1.5708,
b"12\x00\x00",
)
with mock.patch.object(WowSetupTool, "run_vanilla_tweaks", new=fake_run):
with self.assertRaisesRegex(RuntimeError, "Unexpected QuickLoot bytes"):
tool._run_vanilla_tweaks_transactional(
root,
tweaks_exe="fake-vanilla-tweaks.exe",
modern_cli=True,
)
with open(output_exe, "rb") as handle:
self.assertEqual(handle.read(), previous_output)
self.assertFalse(os.path.exists(output_exe + ".modernization-new"))
self.assertFalse(
any(name.startswith(".modernization-vt-") for name in os.listdir(root))
)
def test_transaction_restores_foreign_custom_glues_after_patcher(self):
tool = self._tool(
fov=1.5708,
sound=12,
quickloot=False,
background=False,
farclip=777,
frill=70,
nameplate=20,
maxcam=50,
laa=False,
camera=False,
custom_glues=True,
)
foreign = (0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xF1)
with tempfile.TemporaryDirectory() as root:
source = self._write_exe(
root,
b"\x75\x10",
b"\x75\x0B",
0x27,
1.919862,
b"64\x00\x00",
farclip=3000.0,
frill=300.0,
nameplate=41.0,
maxcam=100.0,
laa_patched=True,
camera_patched=True,
filename="WoW.exe",
)
self._write_custom_state(source, foreign)
output_exe = os.path.join(root, "WoW_Modernized.exe")
with open(output_exe, "wb") as handle:
handle.write(b"previous output")
def fake_run(_tool, staging_target, tweaks_exe=None, modern_cli=False):
return self._write_exe(
staging_target,
b"\x75\x10",
b"\x75\x0B",
0x27,
1.919862,
b"64\x00\x00",
farclip=3000.0,
frill=300.0,
nameplate=41.0,
maxcam=100.0,
laa_patched=True,
camera_patched=True,
custom_patched=True,
)
with mock.patch.object(WowSetupTool, "run_vanilla_tweaks", new=fake_run):
tool._run_vanilla_tweaks_transactional(
root,
tweaks_exe="fake-vanilla-tweaks.exe",
modern_cli=True,
)
with open(output_exe, "rb") as handle:
result = handle.read()
self.assertEqual(self._read_custom_state(result), foreign)
self.assertEqual(result[0x0C1ECF:0x0C1ED1], b"\x74\x10")
self.assertEqual(result[0x0C2B25:0x0C2B27], b"\x74\x0B")
self.assertEqual(result[0x3A4869], 0x14)
self.assertEqual(result[0x126:0x128], b"\x0F\x01")
def test_transaction_commits_total_normalized_output_after_validation(self):
tool = self._tool(
fov=1.5708,
sound=12,
quickloot=False,
background=False,
farclip=777,
frill=70,
nameplate=20,
maxcam=50,
laa=False,
camera=False,
custom_glues=False,
)
with tempfile.TemporaryDirectory() as root:
self._write_exe(
root,
b"\x74\x10",
b"\x74\x0B",
0x14,
1.5708,
b"12\x00\x00",
filename="WoW.exe",
)
output_exe = os.path.join(root, "WoW_Modernized.exe")
with open(output_exe, "wb") as handle:
handle.write(b"previous output")
def fake_run(_tool, staging_target, tweaks_exe=None, modern_cli=False):
return self._write_exe(
staging_target,
b"\x75\x10",
b"\x75\x0B",
0x27,
1.5708,
b"12\x00\x00",
farclip=777.0,
frill=70.0,
nameplate=20.0,
maxcam=50.0,
laa_patched=True,
camera_patched=True,
custom_patched=True,
)
with mock.patch.object(WowSetupTool, "run_vanilla_tweaks", new=fake_run):
result_path = tool._run_vanilla_tweaks_transactional(
root,
tweaks_exe="fake-vanilla-tweaks.exe",
modern_cli=True,
)
self.assertEqual(result_path, output_exe)
with open(output_exe, "rb") as handle:
result = handle.read()
self.assertEqual(result[0x0C1ECF:0x0C1ED1], b"\x74\x10")
self.assertEqual(result[0x0C2B25:0x0C2B27], b"\x74\x0B")
self.assertEqual(result[0x3A4869], 0x14)
self.assertEqual(result[0x126:0x128], b"\x0F\x01")
self.assertAlmostEqual(self._read_float(result, 0x4089B4), 1.5708, places=4)
self.assertEqual(self._read_float(result, 0x40FED8), 3000.0)
self.assertEqual(self._read_float(result, 0x467958), 70.0)
self.assertEqual(self._read_float(result, 0x40C448), 20.0)
self.assertEqual(self._read_float(result, 0x4089A4), 50.0)
self.assertEqual(result[0x435D38:0x435D3C], b"12\x00\x00")
for offset, original, _patched in setup_tool_dynamic._CAMERA_REGIONS:
self.assertEqual(result[offset:offset + len(original)], original)
for offset, original, _patched in setup_tool_dynamic._CUSTOM_GLUES_SITES:
self.assertEqual(result[offset], original)
self.assertFalse(os.path.exists(output_exe + ".modernization-new"))
self.assertFalse(
any(name.startswith(".modernization-vt-") for name in os.listdir(root))
)
def test_validation_gate_runs_source_preflight(self):
tool = self._tool(
fov=1.9199,
sound=64,
quickloot=True,
background=True,
)
tool.wow_dir = FakeVar("C:/game")
tool.optional_plugins = {"no1600x1200.dll": FakeVar(False)}
tool.vmmfix_enabled = FakeVar(False)
with mock.patch.object(
tool,
"_preflight_vanilla_tweaks_source",
return_value=None,
) as preflight:
self.assertTrue(tool.validate_plugin_conflicts())
preflight.assert_called_once_with("C:/game")
def test_normalization_policy_forces_one_marker_refresh(self):
tool = self._tool(
fov=1.9199,
sound=64,
quickloot=True,
background=True,
)
old_signature = WowSetupTool._vanilla_tweaks_signature(tool)
new_signature = tool._vanilla_tweaks_signature()
self.assertNotIn("selected_patch_normalization", old_signature)
self.assertEqual(new_signature["selected_patch_normalization"], 4)
tool.core_plugins["SuperWoWhook.dll"].set(False)
self.assertEqual(new_signature, tool._vanilla_tweaks_signature())
if __name__ == "__main__":
unittest.main()
+177
View File
@@ -0,0 +1,177 @@
import unittest
from unittest import mock
import remote_packages
from setup_tool_dynamic import ModernWowSetupTool
class WowPresenceRecoveryTests(unittest.TestCase):
def _tool(self):
tool = ModernWowSetupTool.__new__(ModernWowSetupTool)
tool._report_download_progress = mock.Mock()
tool._warn_offline = mock.Mock()
tool._valid_x86_dll = mock.Mock(return_value=True)
return tool
def test_legacy_managed_wowpresence_is_repaired_offline(self):
tool = self._tool()
error = remote_packages.RemotePackageError("GitHub request failed: offline")
with (
mock.patch.object(
remote_packages,
"wowpresence_install_trust_state",
return_value="managed_unverified",
),
mock.patch.object(
remote_packages,
"install_cached_wowpresence",
) as cached,
mock.patch.object(
remote_packages,
"install_bundled_wowpresence",
) as bundled,
mock.patch.object(
remote_packages,
"ensure_wowpresence_config",
) as ensure_config,
mock.patch.object(
remote_packages,
"cache_installed_wowpresence",
) as seed_cache,
):
tool._recover_wowpresence_offline("C:/WoW", error)
cached.assert_called_once_with(
"C:/WoW",
progress=tool._report_download_progress,
)
bundled.assert_not_called()
ensure_config.assert_not_called()
seed_cache.assert_not_called()
tool._warn_offline.assert_called_once()
def test_legacy_managed_state_is_marked_for_online_refresh(self):
tool = self._tool()
with (
mock.patch.object(
remote_packages,
"wowpresence_install_trust_state",
return_value="managed_unverified",
),
mock.patch.object(
remote_packages,
"_set_managed_manifest_values",
) as update_manifest,
):
state = tool._prepare_wowpresence_managed_state("C:/WoW")
self.assertEqual(state, "managed_unverified")
update_manifest.assert_called_once_with(
"C:/WoW",
remote_packages.WOWPRESENCE_MANAGED_ID,
revision="__legacy_unverified__",
)
def test_unmanaged_wowpresence_is_not_marked_for_refresh(self):
tool = self._tool()
with (
mock.patch.object(
remote_packages,
"wowpresence_install_trust_state",
return_value="unmanaged",
),
mock.patch.object(
remote_packages,
"_set_managed_manifest_values",
) as update_manifest,
):
state = tool._prepare_wowpresence_managed_state("C:/WoW")
self.assertEqual(state, "unmanaged")
update_manifest.assert_not_called()
def test_remote_wowpresence_failure_uses_offline_recovery(self):
tool = self._tool()
tool._prepare_wowpresence_managed_state = mock.Mock()
tool._recover_wowpresence_offline = mock.Mock()
error = remote_packages.RemotePackageError("GitHub request failed: offline")
with mock.patch.object(
remote_packages,
"install_wowpresence",
side_effect=error,
) as install:
result = tool._install_wowpresence_with_fallback("C:/WoW")
self.assertIsNone(result)
tool._prepare_wowpresence_managed_state.assert_called_once_with("C:/WoW")
install.assert_called_once_with(
"C:/WoW",
progress=tool._report_download_progress,
)
tool._recover_wowpresence_offline.assert_called_once_with("C:/WoW", error)
def test_local_wowpresence_failure_is_not_hidden_by_fallback(self):
tool = self._tool()
tool._prepare_wowpresence_managed_state = mock.Mock()
tool._recover_wowpresence_offline = mock.Mock()
error = remote_packages.RemotePackageError(
"Could not prepare C:/WoW/WowPresence.dll for replacement. Close WoW and try again."
)
with mock.patch.object(
remote_packages,
"install_wowpresence",
side_effect=error,
):
with self.assertRaisesRegex(
RuntimeError,
"WowPresence installation failed locally",
):
tool._install_wowpresence_with_fallback("C:/WoW")
tool._recover_wowpresence_offline.assert_not_called()
def test_wowpresence_source_failure_classifier_rejects_local_io(self):
tool = self._tool()
self.assertTrue(
tool._wowpresence_source_failure(
remote_packages.RemotePackageError("GitHub request failed: offline")
)
)
self.assertTrue(
tool._wowpresence_source_failure(
remote_packages.RemotePackageError(
"SHA-256 mismatch for downloaded file (expected a, got b)."
)
)
)
self.assertTrue(
tool._wowpresence_source_failure(
remote_packages.RemotePackageError(
"WowPresence.dll is not a valid Windows PE binary (missing MZ header)."
)
)
)
self.assertFalse(
tool._wowpresence_source_failure(
remote_packages.RemotePackageError(
"Could not prepare C:/WoW/WowPresence.dll for replacement."
)
)
)
self.assertFalse(
tool._wowpresence_source_failure(
remote_packages.RemotePackageError(
"Could not validate WowPresence.dll: permission denied"
)
)
)
if __name__ == "__main__":
unittest.main()