Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a02dc97793 | |||
| bea8a8e0ed | |||
| c9f8b78df7 | |||
| 26dc319155 | |||
| 1b06e33421 | |||
| 85a81d9259 | |||
| c644cb3481 | |||
| 0faf9ce25b | |||
| 0eb62fbf04 | |||
| d59b922f97 | |||
| 5aaf852b79 | |||
| 998b9650d8 | |||
| 776b5f1d0f | |||
| 5ef038041d | |||
| 509a09b4a0 | |||
| 49c7eb5376 | |||
| acd385ac25 | |||
| 48078cc827 | |||
| 4d239a2e30 | |||
| a6c16e4b2b | |||
| a703884a84 | |||
| 514e937d0e | |||
| 4f0b747159 | |||
| 26445a2731 | |||
| 8d4ce2d7c6 | |||
| 7a1a3c9d12 | |||
| 05b0b761c1 | |||
| 29d7f3c174 | |||
| 3e5fbe47dd | |||
| 178f1460a9 | |||
| e385229f4a | |||
| cd8a3e19f9 | |||
| 62b736c205 | |||
| 7841c58a64 | |||
| 99e8aafacf | |||
| f9a0315c1c | |||
| 8cba9f9bfa | |||
| 9b46d98e6a | |||
| d0627a574f | |||
| 315d32d52f | |||
| 881e25ca97 | |||
| 16d2a8d8ec | |||
| 10e4be0fec | |||
| 363eb89fd0 | |||
| 2dbd335d8f | |||
| 1a6a60b643 | |||
| a1c70df8e8 | |||
| 3cda29653b | |||
| 14ca95f553 | |||
| acbe46e5ba | |||
| 81b6cd6a30 | |||
| 94a613a815 | |||
| 50ed6f20fa | |||
| b92e9c2987 | |||
| 0602e012c1 | |||
| a263ec6fb8 | |||
| f51ef200a4 | |||
| 95e1b25f4e | |||
| 31eb2c911c | |||
| c992cee4cf | |||
| 7471b4f12e | |||
| eca8599f0c | |||
| 2338c005b8 | |||
| 13f3c247d0 | |||
| 7c9d69179f | |||
| 655c444353 | |||
| 600719652e | |||
| 8658700a76 | |||
| f8a8678e2e | |||
| e88d2cd28d | |||
| 798408c559 | |||
| 17059d8a76 | |||
| 9569bbd296 | |||
| e5e4dc954a | |||
| b861ad89e2 | |||
| a46bb67d0d | |||
| 41592e7f8c | |||
| b12001584e | |||
| 036ff0c1cb | |||
| 56f0ff9854 | |||
| 3d3eea3c6a | |||
| 7c7ff0d146 | |||
| ae8abe2e16 | |||
| 0b85ec4faf | |||
| 30ab74a4f0 | |||
| b727c928bb | |||
| 1f8c87ae47 | |||
| 3fb218e296 | |||
| 64b7612c90 | |||
| 6c6d6e2b7d | |||
| c8bce690a5 | |||
| 96cc98d52b | |||
| e9c7b3b219 | |||
| 669361aeee | |||
| b0394ac662 | |||
| c140fdfe98 | |||
| 386917c9f0 | |||
| 859fe4037e | |||
| 77ace2d642 | |||
| a42608f635 | |||
| 98325b403c | |||
| 2b7de2613c | |||
| 3e32b7985b | |||
| 9ff415ef3f | |||
| 63aa6328cc | |||
| c9c878dc19 | |||
| 08bd69d186 | |||
| 6fff497457 | |||
| c6a65ab044 | |||
| 4a0f068465 | |||
| 166ae58a9f | |||
| e59bc1fc53 | |||
| 065ff4ee4f | |||
| 34b3affeb1 | |||
| c304cd0542 | |||
| 627e9392b5 | |||
| 74a6dd3891 | |||
| 890949b8db | |||
| 39c3963d5f | |||
| 0416e21b54 | |||
| 90f9247eb1 | |||
| 352330b048 | |||
| 764d177638 | |||
| 943caba91c | |||
| 422b076f19 | |||
| 51e772d959 | |||
| de0515c84c | |||
| a5c6feee95 | |||
| 5f7792b9d9 | |||
| 1e568db8ec | |||
| e1c9ee4fb0 | |||
| 6883fb318d | |||
| 1b80a904b7 | |||
| ad62557e59 | |||
| ae3a23be17 | |||
| 480d20f39e | |||
| d49fb9acc0 | |||
| fa764ba40f | |||
| 023dd7ba1d | |||
| fb9275e13a | |||
| 846dffb59b | |||
| fc8c7eaa8e | |||
| 9c397218a7 | |||
| 098852175e |
@@ -0,0 +1 @@
|
||||
Payload/Interface/Addons/SuperAPI/** text eol=lf
|
||||
+23
-15
@@ -47,8 +47,11 @@ jobs:
|
||||
with:
|
||||
arch: x86
|
||||
|
||||
- name: Prepare bundled remote fallbacks
|
||||
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
|
||||
@@ -63,7 +66,7 @@ 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
|
||||
@@ -81,7 +84,7 @@ jobs:
|
||||
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.2') }}
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') && !(github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v2.3') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: WoW_Modernization_Tool-test
|
||||
@@ -90,33 +93,38 @@ jobs:
|
||||
dist/WoW_Modernization_Tool.zip
|
||||
if-no-files-found: error
|
||||
|
||||
# One-time production promotion for v2.2. The exact commit message keeps
|
||||
# normal main pushes from publishing or modifying the v2.2 release.
|
||||
- name: Create GitHub Release v2.2
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v2.2' }}
|
||||
# One-time production promotion for v2.3. The exact commit message keeps
|
||||
# normal main pushes from publishing or modifying the v2.3 release.
|
||||
# Keep the Git tag aligned with the exact commit used to build the assets.
|
||||
- name: Synchronize Git tag v2.3
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v2.3' }}
|
||||
shell: pwsh
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git tag -f v2.3 "${{ github.sha }}"
|
||||
git push origin refs/tags/v2.3 --force
|
||||
|
||||
- name: Create GitHub Release v2.3
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.head_commit.message == 'release v2.3' }}
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: v2.2
|
||||
tag_name: v2.3
|
||||
target_commitish: ${{ github.sha }}
|
||||
files: |
|
||||
dist/WoW_Modernization_Tool.exe
|
||||
dist/WoW_Modernization_Tool.zip
|
||||
fail_on_unmatched_files: true
|
||||
overwrite_files: true
|
||||
name: "WoW Modernization Tool v2.2"
|
||||
name: "WoW Modernization Tool v2.3"
|
||||
body_path: RELEASE_NOTES.md
|
||||
generate_release_notes: false
|
||||
make_latest: true
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/tags/v2.3' }}
|
||||
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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -17,8 +17,6 @@ Download the latest version from:
|
||||
|
||||
**[GitHub Releases](https://github.com/Dusk-92/Modernization-Tool/releases/latest)**
|
||||
|
||||
Current major release: **v2.2**
|
||||
|
||||
No installation is required. Download and run:
|
||||
|
||||
`WoW_Modernization_Tool.exe`
|
||||
@@ -46,60 +44,58 @@ The launcher shortcut is automatically created:
|
||||
|
||||
Two rendering modes are available:
|
||||
|
||||
### VanillaFixes — DirectX 9
|
||||
### [VanillaFixes](https://github.com/hannesmann/vanillafixes) — DirectX 9
|
||||
|
||||
The default option.
|
||||
|
||||
Uses WoW's native DirectX 9 renderer together with VanillaFixes.
|
||||
|
||||
### VanillaFixes + DXVK — Vulkan
|
||||
### VanillaFixes + [DXVK](https://github.com/doitsujin/dxvk) — Vulkan
|
||||
|
||||
Uses DXVK to translate DirectX 9 to Vulkan.
|
||||
|
||||
The bundled version is currently:
|
||||
|
||||
**DXVK 2.6.1 x86**
|
||||
DXVK is bundled with the tool and can be selected directly from the renderer options.
|
||||
|
||||
Existing custom DirectX wrapper files are backed up rather than blindly deleted when switching renderers.
|
||||
|
||||
---
|
||||
|
||||
# 🧩 Client Plugins
|
||||
# 🧩 Client Plugins & Sources
|
||||
|
||||
## Recommended Core
|
||||
|
||||
The tool can install and configure several recommended client extensions:
|
||||
|
||||
- **Nampower** — improves spell responsiveness and latency handling.
|
||||
- **UnitXP_SP3** — adds networking, targeting, Lua and client improvements.
|
||||
- **SuperWoW** — expands the Vanilla Lua API and improves addon compatibility.
|
||||
- **TransmogFix** — prevents performance drops caused by rapid appearance updates.
|
||||
- **PerfBoost** — provides additional unit rendering/performance controls.
|
||||
- **WeirdPerformance** — lightweight client optimizations aimed at improving FPS and reducing UI memory leaks for smoother gameplay.
|
||||
- **VanillaHelpers** — extends several Vanilla client limits.
|
||||
- **ClassicAPI** — adds newer WoW API functions for compatible addons.
|
||||
- **AuctionQueryThrottle** — removes the fixed delay between Auction House queries.
|
||||
- **[Nampower](https://github.com/brues-code/nampower)** — improves spell responsiveness and latency handling. Settings addon: [NampowerSettings](https://github.com/brues-code/NampowerSettings).
|
||||
- **[UnitXP_SP3](https://github.com/brues-code/UnitXP_SP3)** — adds networking, targeting, Lua and client improvements.
|
||||
- **[SuperWoW](https://github.com/balakethelock/SuperWoW)** — expands the Vanilla Lua API and improves addon compatibility. Its companion API addon is [SuperAPI](https://github.com/balakethelock/SuperAPI).
|
||||
- **[TransmogFix](https://codeberg.org/MarcelineVQ/WeirdUtils)** — prevents performance drops caused by rapid appearance updates.
|
||||
- **[PerfBoost](https://github.com/RetroCro/TurtleWoW-Mods)** — provides additional unit rendering/performance controls. Configuration addon: [PerfBoost Settings](https://gitea.com/avitasia/PerfBoostSettings).
|
||||
- **[WeirdPerformance](https://codeberg.org/Dusk92/WeirdUtils)** — lightweight client optimizations aimed at improving FPS and reducing UI memory leaks for smoother gameplay.
|
||||
- **[VanillaHelpers](https://github.com/isfir/VanillaHelpers)** — extends several Vanilla client limits.
|
||||
- **[ClassicAPI](https://github.com/brues-code/ClassicAPI)** — adds newer WoW API functions for compatible addons.
|
||||
- **[AuctionQueryThrottle](https://github.com/brues-code/AuctionQueryThrottle)** — removes the fixed delay between Auction House queries.
|
||||
|
||||
## Optional Plugins
|
||||
|
||||
Optional client-side improvements include:
|
||||
|
||||
- **VanillaMultiMonitorFix** — improved resolution, refresh-rate and monitor detection.
|
||||
- **Interact** — adds a modern Interact key for NPCs, objects, gathering nodes and loot.
|
||||
- **No1600x1200** — fixes the old Vanilla resolution limitation on some systems.
|
||||
- **BigCursor** — upscales the hardware cursor for better visibility on modern high-resolution displays without sacrificing sharpness.
|
||||
- **CustomAssets** — allows loose game files to be loaded directly from the `Data/` folder and supports custom multi-character patch names without repacking MPQ archives.
|
||||
- **LogSessions** — automatically organizes combat and chat logs into clean per-character, per-day files when you log in.
|
||||
- **MinimapIcons** — adds TBC/WotLK-style minimap tracking icons for NPCs and game objects, with a combined tracking menu and saved preferences.
|
||||
- **PNG Screenshots** — saves screenshots as compressed PNG files instead of the default uncompressed TGA format, using background processing to minimize frame-time impact.
|
||||
- **WorldMarkers** — lets party or raid leaders place up to five animated, Cataclysm-style colored world markers for positioning and tactical planning.
|
||||
- **Discord Rich Presence** — installs and keeps **WowPresence** up to date from [Dusk-92/WowPresence](https://github.com/Dusk-92/WowPresence). The Tool preconfigures the OctoWoW Discord Application ID and lets you choose which character details are shown on Discord, including **Name, Guild, Race, Faction, Class, Level and Zone**. Custom Discord Application IDs are preserved across updates.
|
||||
- **[VanillaMultiMonitorFix](https://github.com/Mates1500/VanillaMultiMonitorFix)** — improved resolution, refresh-rate and monitor detection.
|
||||
- **[Interact](https://github.com/lookino/Interact)** — adds a modern Interact key for NPCs, objects, gathering nodes and loot.
|
||||
- **[No1600x1200](https://github.com/RetroCro/TurtleWoW-Mods#no1600x1200)** — fixes the old Vanilla resolution limitation on some systems.
|
||||
- **[BigCursor](https://codeberg.org/MarcelineVQ/WeirdUtils)** — upscales the hardware cursor for better visibility on modern high-resolution displays without sacrificing sharpness.
|
||||
- **[CustomAssets](https://codeberg.org/MarcelineVQ/WeirdUtils)** — allows loose game files to be loaded directly from the `Data/` folder and supports custom multi-character patch names without repacking MPQ archives.
|
||||
- **[LogSessions](https://codeberg.org/MarcelineVQ/WeirdUtils)** — automatically organizes combat and chat logs into clean per-character, per-day files when you log in.
|
||||
- **[MinimapIcons](https://codeberg.org/MarcelineVQ/WeirdUtils)** — adds TBC/WotLK-style minimap tracking icons for NPCs and game objects, with a combined tracking menu and saved preferences.
|
||||
- **[PNG Screenshots](https://codeberg.org/MarcelineVQ/WeirdUtils)** — saves screenshots as compressed PNG files instead of the default uncompressed TGA format, using background processing to minimize frame-time impact.
|
||||
- **[WorldMarkers](https://codeberg.org/MarcelineVQ/WeirdUtils)** — lets party or raid leaders place up to five animated, Cataclysm-style colored world markers for positioning and tactical planning.
|
||||
- **[Discord Rich Presence](https://github.com/Dusk-92/WowPresence)** — installs and keeps **WowPresence** up to date. The Tool preconfigures the OctoWoW Discord Application ID and lets you choose which character details are shown on Discord, including **Name, Guild, Race, Faction, Class, Level and Zone**. Custom Discord Application IDs are preserved across updates.
|
||||
|
||||
---
|
||||
|
||||
# ⚙️ Vanilla Tweaks
|
||||
|
||||
Modernization Tool integrates the modern **tubtubs/vanilla-tweaks** patcher.
|
||||
Modernization Tool integrates the modern **[tubtubs/vanilla-tweaks](https://github.com/tubtubs/vanilla-tweaks)** patcher.
|
||||
|
||||
Available settings include:
|
||||
|
||||
@@ -117,7 +113,7 @@ Available settings include:
|
||||
- Unlimited AddOn Script Memory
|
||||
- Cross-Faction Resurrection Fix
|
||||
- Custom Glues Patch
|
||||
- Automatic WDB management
|
||||
- [Automatic WDB management](https://github.com/RetroCro/TurtleWoW-Mods#automatically-clear-wdb-folder-every-time-you-launch-turtle-wow)
|
||||
|
||||
When **SuperWoW** already provides a feature, the equivalent vanilla-tweaks patch is automatically skipped to avoid duplicate modifications.
|
||||
|
||||
@@ -127,12 +123,12 @@ When **SuperWoW** already provides a feature, the equivalent vanilla-tweaks patc
|
||||
|
||||
Optional visual modifications are available directly from the tool:
|
||||
|
||||
- **Bluemoon Patch** — restores the rare blue moon effect.
|
||||
- **Darker Nights** — darker and more atmospheric nights.
|
||||
- **Pretty Night Sky** — improved starry night sky.
|
||||
- **Epoch Water** — alternative water textures.
|
||||
- **Fog Pushback** — moves environmental fog farther away.
|
||||
- **Pink Herbs** — makes herb nodes much easier to spot.
|
||||
- **[Bluemoon Patch](https://github.com/tubtubs/vanilla-tweaks)** — restores the rare blue moon effect.
|
||||
- **[Darker Nights](https://projectreforged.github.io/vanilla/downloads/turtle/)** — darker and more atmospheric nights.
|
||||
- **[Pretty Night Sky](https://github.com/RetroCro/TurtleWoW-Mods)** — improved starry night sky.
|
||||
- **[Epoch Water](https://github.com/RetroCro/TurtleWoW-Mods)** — alternative water textures.
|
||||
- **[Fog Pushback](https://github.com/RetroCro/TurtleWoW-Mods)** — moves environmental fog farther away.
|
||||
- **[Pink Herbs](https://github.com/seacrabsam/patch-herb)** — makes herb nodes much easier to spot.
|
||||
|
||||
All visual mods are **disabled by default**.
|
||||
|
||||
@@ -142,9 +138,9 @@ All visual mods are **disabled by default**.
|
||||
|
||||
Optional sound replacements include:
|
||||
|
||||
- **NoErrorSounds** — removes repetitive error/fizzle and interface sounds.
|
||||
- **FishPing** — replaces the fishing bite sound with a clearer ping.
|
||||
- **Warlock Muted Demons** — mutes repetitive Warlock demon voice lines.
|
||||
- **[NoErrorSounds](https://github.com/Macumbafeh/NoErrorSounds)** — removes repetitive error/fizzle and interface sounds.
|
||||
- **[FishPing](https://github.com/notsureawake/FishPing)** — replaces the fishing bite sound with a clearer ping.
|
||||
- **[Warlock Muted Demons](https://github.com/spzilyk/Warlock-Muted-Demons)** — mutes repetitive Warlock demon voice lines.
|
||||
|
||||
All audio mods are **disabled by default**.
|
||||
|
||||
@@ -162,11 +158,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 +200,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 +212,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 +229,9 @@ Your per-installation settings will be restored automatically.
|
||||
|
||||
# 🛡️ Antivirus Notice
|
||||
|
||||
Because Modernization Tool modifies a game executable and installs client-side DLL plugins, some antivirus products may flag the executable or some included components.
|
||||
Because Modernization Tool creates a patched `WoW_Modernized.exe` and installs client-side DLL plugins, some antivirus products may flag the executable or some included components.
|
||||
|
||||
The original `WoW.exe` is not modified in place.
|
||||
|
||||
Always download Modernization Tool from the official GitHub Releases page.
|
||||
|
||||
@@ -247,54 +240,15 @@ Always download Modernization Tool from the official GitHub Releases page.
|
||||
# 🔗 Useful Links
|
||||
|
||||
- **[Latest Release](https://github.com/Dusk-92/Modernization-Tool/releases/latest)**
|
||||
- **[v2.2 Release Notes](https://github.com/Dusk-92/Modernization-Tool/releases/tag/v2.2)**
|
||||
- **[OctoWoW Installation & Modernization Guide](https://octowow.st/forum/viewtopic.php?t=831)**
|
||||
|
||||
---
|
||||
|
||||
# ❤️ Credits
|
||||
|
||||
Modernization Tool brings together work from several community projects and developers.
|
||||
Modernization Tool builds on the work of the community projects and developers linked throughout this README.
|
||||
|
||||
## Rendering & Client Patching
|
||||
|
||||
- [VanillaFixes](https://github.com/hannesmann/vanillafixes)
|
||||
- [DXVK](https://github.com/doitsujin/dxvk)
|
||||
- [vanilla-tweaks](https://github.com/tubtubs/vanilla-tweaks)
|
||||
|
||||
## Core Engine & API Plugins
|
||||
|
||||
- [VanillaHelpers](https://github.com/isfir/VanillaHelpers)
|
||||
- [PerfBoost Settings](https://gitea.com/avitasia/PerfBoostSettings)
|
||||
- [UnitXP_SP3](https://github.com/brues-code/UnitXP_SP3)
|
||||
- [SuperWoW](https://github.com/balakethelock/SuperWoW)
|
||||
- [SuperAPI](https://github.com/balakethelock/SuperAPI)
|
||||
- [ClassicAPI](https://github.com/brues-code/ClassicAPI)
|
||||
- [AuctionQueryThrottle](https://github.com/brues-code/AuctionQueryThrottle)
|
||||
- [Nampower](https://github.com/brues-code/nampower)
|
||||
- [NampowerSettings](https://github.com/brues-code/NampowerSettings)
|
||||
- [No1600x1200](https://github.com/RetroCro/TurtleWoW-Mods#no1600x1200)
|
||||
- [VanillaMultiMonitorFix](https://github.com/Mates1500/VanillaMultiMonitorFix)
|
||||
- [Interact](https://github.com/lookino/Interact)
|
||||
- [WowPresence](https://github.com/Dusk-92/WowPresence)
|
||||
|
||||
## Visual & Audio Mods
|
||||
|
||||
- [Bluemoon Patch via vanilla-tweaks](https://github.com/tubtubs/vanilla-tweaks)
|
||||
- [Darker Nights — Project Reforged](https://projectreforged.github.io/vanilla/downloads/turtle/)
|
||||
- [Pretty Night Sky / Epoch Water / Fog Pushback — RetroCro TurtleWoW Mods](https://github.com/RetroCro/TurtleWoW-Mods)
|
||||
- [Pink Herbs](https://github.com/seacrabsam/patch-herb)
|
||||
- [NoErrorSounds](https://github.com/Macumbafeh/NoErrorSounds)
|
||||
- [FishPing](https://github.com/notsureawake/FishPing)
|
||||
- [Warlock Muted Demons](https://github.com/spzilyk/Warlock-Muted-Demons)
|
||||
- [Automatic WDB management guide](https://github.com/RetroCro/TurtleWoW-Mods#automatically-clear-wdb-folder-every-time-you-launch-turtle-wow)
|
||||
|
||||
## Other Bundled Enhancements
|
||||
|
||||
- [Vanilla AutoLogin](https://github.com/MarcelineVQ/turtle-autologin)
|
||||
- [WeirdUtils](https://codeberg.org/Dusk92/WeirdUtils)
|
||||
|
||||
Additional attribution and source links are available directly in the **Credits** tab of Modernization Tool.
|
||||
Thank you to all upstream authors and contributors whose work makes this project possible. Additional attribution is available directly in the **Credits** tab of Modernization Tool.
|
||||
|
||||
---
|
||||
|
||||
@@ -311,8 +265,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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+24
-23
@@ -1,33 +1,34 @@
|
||||
# 🛠️ WoW Modernization Tool v2.2
|
||||
# 🛠️ WoW Modernization Tool v2.3
|
||||
|
||||
## 🎮 Discord Rich Presence
|
||||
This update focuses on **better compatibility, safer WoW.exe patching, improved recovery, display-scaling support and DXVK configuration**.
|
||||
|
||||
- Added detailed Discord Rich Presence privacy controls.
|
||||
- Choose individually whether Discord can display:
|
||||
- Character Name
|
||||
- Guild
|
||||
- Race
|
||||
- Faction
|
||||
- Class
|
||||
- Level
|
||||
- Zone
|
||||
- Added a `Show character details` option to quickly enable all Discord details.
|
||||
- Updated WowPresence integration for the new privacy system.
|
||||
## ⚙️ Vanilla Tweaks & Compatibility
|
||||
|
||||
## 🔄 Updates & Downloads
|
||||
- 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`.
|
||||
|
||||
- Added smart update checks for remote components.
|
||||
- Unchanged components are no longer downloaded again on every Apply.
|
||||
- Modified or missing managed files are automatically detected and repaired.
|
||||
- Release assets are detected even when an upstream project replaces a file under the same tag.
|
||||
- Improved vanilla-tweaks update detection to avoid unnecessary downloads and repatching.
|
||||
- Improved update checks for GitHub branch-based components.
|
||||
## 🎨 MPQ & Recovery
|
||||
|
||||
## 📚 Documentation
|
||||
- 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.
|
||||
|
||||
- Updated Discord Rich Presence documentation.
|
||||
## 🛡️ Reliability
|
||||
|
||||
## ✅ Updating from v2.1
|
||||
- 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.
|
||||
|
||||
## ✅ Updating from v2.2
|
||||
|
||||
Download the new executable, select your existing WoW folder and click **Apply Setup & Tweaks**.
|
||||
|
||||
|
||||
+26
-18
@@ -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
@@ -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
|
||||
+683
-35
@@ -4,6 +4,7 @@ import os
|
||||
import shutil
|
||||
import stat
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
@@ -33,6 +34,10 @@ class RemotePackageError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class RemoteSourceUnavailable(RemotePackageError):
|
||||
"""Remote source could not provide a usable package."""
|
||||
|
||||
|
||||
def _emit_progress(callback, message, current=None, total=None):
|
||||
if callback is None:
|
||||
return
|
||||
@@ -261,6 +266,265 @@ def _atomic_replace_file(source, target):
|
||||
pass
|
||||
|
||||
|
||||
def _package_cache_dir(target_dir, package_id):
|
||||
safe_id = "".join(
|
||||
ch if ch.isalnum() or ch in ("-", "_", ".") else "_"
|
||||
for ch in str(package_id)
|
||||
)
|
||||
if safe_id in ("", ".", ".."):
|
||||
raise RemotePackageError("Invalid package-cache ID.")
|
||||
return os.path.join(
|
||||
target_dir,
|
||||
".modernization_tool",
|
||||
"package_cache",
|
||||
safe_id,
|
||||
)
|
||||
|
||||
|
||||
def remove_package_cache(target_dir, package_id):
|
||||
"""Remove one tool-owned package cache without touching installed files."""
|
||||
cache_dir = _package_cache_dir(target_dir, package_id)
|
||||
if os.path.isdir(cache_dir):
|
||||
shutil.rmtree(cache_dir, ignore_errors=True)
|
||||
|
||||
parent = os.path.dirname(cache_dir)
|
||||
try:
|
||||
os.rmdir(parent)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _safe_cache_filename(filename):
|
||||
if not isinstance(filename, str) or not filename:
|
||||
raise RemotePackageError("Invalid package-cache filename.")
|
||||
normalized = filename.replace("\\", "/")
|
||||
if (
|
||||
normalized in (".", "..")
|
||||
or "/" in normalized
|
||||
or ":" in normalized
|
||||
or os.path.basename(filename) != filename
|
||||
):
|
||||
raise RemotePackageError(f"Unsafe package-cache filename: {filename}")
|
||||
return filename
|
||||
|
||||
|
||||
def _store_cached_file(target_dir, package_id, source_path, filename, revision=None):
|
||||
"""Persist one validated remote artifact as a local offline fallback."""
|
||||
filename = _safe_cache_filename(filename)
|
||||
if not os.path.isfile(source_path):
|
||||
raise RemotePackageError(f"Package-cache source is missing: {source_path}")
|
||||
|
||||
cache_dir = _package_cache_dir(target_dir, package_id)
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
cached_path = os.path.join(cache_dir, filename)
|
||||
_atomic_replace_file(source_path, cached_path)
|
||||
|
||||
size = os.path.getsize(cached_path)
|
||||
if size <= 0:
|
||||
raise RemotePackageError("Refusing to cache an empty package artifact.")
|
||||
|
||||
metadata = {
|
||||
"filename": filename,
|
||||
"size": size,
|
||||
"sha256": _file_sha256(cached_path),
|
||||
"revision": revision,
|
||||
}
|
||||
metadata_path = os.path.join(cache_dir, "metadata.json")
|
||||
staged = metadata_path + ".new"
|
||||
try:
|
||||
with open(staged, "w", encoding="utf-8", newline="\n") as handle:
|
||||
json.dump(metadata, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
os.replace(staged, metadata_path)
|
||||
finally:
|
||||
if os.path.exists(staged):
|
||||
try:
|
||||
os.remove(staged)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return cached_path
|
||||
|
||||
|
||||
def _store_cached_file_safely(
|
||||
target_dir,
|
||||
package_id,
|
||||
source_path,
|
||||
filename,
|
||||
revision=None,
|
||||
):
|
||||
"""Best-effort cache refresh that can never make a valid install fail."""
|
||||
try:
|
||||
return _store_cached_file(
|
||||
target_dir,
|
||||
package_id,
|
||||
source_path,
|
||||
filename,
|
||||
revision=revision,
|
||||
)
|
||||
except (OSError, RemotePackageError, ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def _load_cached_file(target_dir, package_id, filename=None, expected_revision=None):
|
||||
"""Return a cached artifact only when its recorded integrity still matches."""
|
||||
cache_dir = _package_cache_dir(target_dir, package_id)
|
||||
metadata_path = os.path.join(cache_dir, "metadata.json")
|
||||
try:
|
||||
with open(metadata_path, "r", encoding="utf-8") as handle:
|
||||
metadata = json.load(handle)
|
||||
except (OSError, json.JSONDecodeError, ValueError, TypeError):
|
||||
return None
|
||||
|
||||
if not isinstance(metadata, dict):
|
||||
return None
|
||||
|
||||
recorded_name = metadata.get("filename")
|
||||
try:
|
||||
recorded_name = _safe_cache_filename(recorded_name)
|
||||
except RemotePackageError:
|
||||
return None
|
||||
|
||||
if filename is not None:
|
||||
try:
|
||||
filename = _safe_cache_filename(filename)
|
||||
except RemotePackageError:
|
||||
return None
|
||||
if recorded_name != filename:
|
||||
return None
|
||||
|
||||
revision = metadata.get("revision")
|
||||
if expected_revision is not None and str(revision) != str(expected_revision):
|
||||
return None
|
||||
|
||||
cached_path = os.path.join(cache_dir, recorded_name)
|
||||
if not os.path.isfile(cached_path):
|
||||
return None
|
||||
|
||||
expected_size = metadata.get("size")
|
||||
expected_sha = metadata.get("sha256")
|
||||
if not isinstance(expected_size, int) or expected_size <= 0:
|
||||
return None
|
||||
if (
|
||||
not isinstance(expected_sha, str)
|
||||
or len(expected_sha) != 64
|
||||
or any(ch not in "0123456789abcdefABCDEF" for ch in expected_sha)
|
||||
):
|
||||
return None
|
||||
|
||||
try:
|
||||
if os.path.getsize(cached_path) != expected_size:
|
||||
return None
|
||||
if _file_sha256(cached_path).lower() != expected_sha.lower():
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
return cached_path, metadata
|
||||
|
||||
|
||||
def _current_recorded_package_revision(target_dir, package_id):
|
||||
data = _load_package_state(target_dir, package_id)
|
||||
revision = data.get("revision") if isinstance(data, dict) else None
|
||||
if revision in (None, ""):
|
||||
return None
|
||||
if _package_state_is_current(target_dir, package_id, revision):
|
||||
return str(revision)
|
||||
return None
|
||||
|
||||
|
||||
def _runtime_base_path():
|
||||
return getattr(
|
||||
sys,
|
||||
"_MEIPASS",
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
)
|
||||
|
||||
|
||||
def _load_bundled_remote_fallback(
|
||||
package_id,
|
||||
filename=None,
|
||||
expected_revision=None,
|
||||
):
|
||||
"""Return a release-bundled remote fallback after strict integrity checks."""
|
||||
safe_id = "".join(
|
||||
ch if ch.isalnum() or ch in ("-", "_", ".") else "_"
|
||||
for ch in str(package_id)
|
||||
)
|
||||
if safe_id in ("", ".", ".."):
|
||||
return None
|
||||
|
||||
base = _runtime_base_path()
|
||||
manifest_path = os.path.join(
|
||||
base,
|
||||
"Payload",
|
||||
"Fallback",
|
||||
"remote_fallbacks.json",
|
||||
)
|
||||
try:
|
||||
with open(manifest_path, "r", encoding="utf-8") as handle:
|
||||
manifest = json.load(handle)
|
||||
except (OSError, json.JSONDecodeError, ValueError, TypeError):
|
||||
return None
|
||||
|
||||
fallbacks = manifest.get("fallbacks") if isinstance(manifest, dict) else None
|
||||
if not isinstance(fallbacks, dict):
|
||||
return None
|
||||
record = fallbacks.get(safe_id)
|
||||
if not isinstance(record, dict):
|
||||
return None
|
||||
|
||||
recorded_name = record.get("filename")
|
||||
try:
|
||||
recorded_name = _safe_cache_filename(recorded_name)
|
||||
except RemotePackageError:
|
||||
return None
|
||||
if filename is not None:
|
||||
try:
|
||||
filename = _safe_cache_filename(filename)
|
||||
except RemotePackageError:
|
||||
return None
|
||||
if recorded_name != filename:
|
||||
return None
|
||||
|
||||
revision = record.get("revision")
|
||||
if expected_revision is not None and str(revision) != str(expected_revision):
|
||||
return None
|
||||
|
||||
expected_size = record.get("size")
|
||||
expected_sha = record.get("sha256")
|
||||
if not isinstance(expected_size, int) or expected_size <= 0:
|
||||
return None
|
||||
if (
|
||||
not isinstance(expected_sha, str)
|
||||
or len(expected_sha) != 64
|
||||
or any(ch not in "0123456789abcdefABCDEF" for ch in expected_sha)
|
||||
):
|
||||
return None
|
||||
|
||||
path = os.path.join(
|
||||
base,
|
||||
"Payload",
|
||||
"Fallback",
|
||||
"Remote",
|
||||
safe_id,
|
||||
recorded_name,
|
||||
)
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
try:
|
||||
if os.path.getsize(path) != expected_size:
|
||||
return None
|
||||
if _file_sha256(path) != expected_sha.lower():
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
return path, record
|
||||
|
||||
|
||||
|
||||
def _remove_tree(path):
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
@@ -353,6 +617,31 @@ def _remove_path(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def _cleanup_transaction_backups(target):
|
||||
"""Best-effort cleanup of stale backups once a valid live target exists."""
|
||||
target = os.path.abspath(target)
|
||||
if not os.path.lexists(target):
|
||||
return
|
||||
|
||||
parent = os.path.dirname(target)
|
||||
prefix = os.path.basename(target) + ".modernization-backup-"
|
||||
try:
|
||||
names = os.listdir(parent)
|
||||
except OSError:
|
||||
return
|
||||
|
||||
for name in names:
|
||||
if not name.startswith(prefix):
|
||||
continue
|
||||
backup = os.path.join(parent, name)
|
||||
try:
|
||||
_remove_path(backup)
|
||||
except OSError:
|
||||
# The old file may still be locked by WoW/Windows. Keep it safe
|
||||
# and retry automatically the next time this package is verified.
|
||||
pass
|
||||
|
||||
|
||||
def _transactional_replace_bundle(items, label="component bundle"):
|
||||
"""Replace a group of files/directories as one rollback-safe transaction.
|
||||
|
||||
@@ -469,15 +758,10 @@ def _transactional_replace_bundle(items, label="component bundle"):
|
||||
) from exc
|
||||
|
||||
else:
|
||||
# Commit succeeded. Old copies are no longer needed.
|
||||
# Commit succeeded. Old copies are no longer needed. Clean the current
|
||||
# transaction backup plus any stale backup left by an earlier run.
|
||||
for record in records:
|
||||
if record["backup_created"] and os.path.lexists(record["backup"]):
|
||||
try:
|
||||
_remove_path(record["backup"])
|
||||
except OSError:
|
||||
# A stale backup is harmless; never invalidate a successful
|
||||
# installation just because cleanup was denied.
|
||||
pass
|
||||
_cleanup_transaction_backups(record["target"])
|
||||
|
||||
finally:
|
||||
# Staged paths are safe to remove. Backups are intentionally not
|
||||
@@ -676,6 +960,106 @@ def write_wowpresence_broadcast_flags(target_dir, value):
|
||||
|
||||
|
||||
|
||||
def wowpresence_install_trust_state(target_dir):
|
||||
"""Classify the installed WowPresence pair without trusting PE validity alone.
|
||||
|
||||
Returns one of:
|
||||
- "unmanaged": no Modernization Tool manifest exists;
|
||||
- "managed_verified": recorded hashes match both managed binaries;
|
||||
- "managed_modified": recorded hashes exist but the managed install differs;
|
||||
- "managed_unverified": a legacy/incomplete managed manifest has no usable hashes.
|
||||
"""
|
||||
manifest = _load_managed_manifest_data(
|
||||
target_dir,
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
)
|
||||
if not isinstance(manifest, dict) or not manifest:
|
||||
return "unmanaged"
|
||||
|
||||
required = ("WowPresence.dll", "WowPresence.exe")
|
||||
managed_files = {
|
||||
rel.replace("\\", "/").casefold()
|
||||
for rel in _load_managed_manifest(target_dir, WOWPRESENCE_MANAGED_ID)
|
||||
}
|
||||
if any(name.casefold() not in managed_files for name in required):
|
||||
return "managed_modified"
|
||||
|
||||
saved_hashes = manifest.get("file_sha256")
|
||||
if not isinstance(saved_hashes, dict):
|
||||
return "managed_unverified"
|
||||
|
||||
normalized_hashes = {}
|
||||
for filename in required:
|
||||
expected = saved_hashes.get(filename)
|
||||
if (
|
||||
not isinstance(expected, str)
|
||||
or len(expected.strip()) != 64
|
||||
or any(ch not in "0123456789abcdefABCDEF" for ch in expected.strip())
|
||||
):
|
||||
return "managed_unverified"
|
||||
normalized_hashes[filename] = expected.strip().lower()
|
||||
|
||||
for filename in required:
|
||||
path = os.path.join(target_dir, filename)
|
||||
try:
|
||||
if _file_sha256(path) != normalized_hashes[filename]:
|
||||
return "managed_modified"
|
||||
_verify_x86_pe(path, f"installed {filename}")
|
||||
except (OSError, RemotePackageError):
|
||||
return "managed_modified"
|
||||
|
||||
return "managed_verified"
|
||||
|
||||
|
||||
def cache_installed_wowpresence(target_dir, revision=None):
|
||||
"""Best-effort cache seed from a hash-verified managed WowPresence install."""
|
||||
if wowpresence_install_trust_state(target_dir) != "managed_verified":
|
||||
return False
|
||||
|
||||
manifest = _load_managed_manifest_data(
|
||||
target_dir,
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
)
|
||||
if revision is None:
|
||||
revision = manifest.get("revision")
|
||||
if revision in (None, ""):
|
||||
return False
|
||||
revision = str(revision)
|
||||
|
||||
if _load_cached_file(
|
||||
target_dir,
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
"WowPresence.zip",
|
||||
expected_revision=revision,
|
||||
) is not None:
|
||||
return True
|
||||
|
||||
dll_path = os.path.join(target_dir, "WowPresence.dll")
|
||||
exe_path = os.path.join(target_dir, "WowPresence.exe")
|
||||
fd, zip_path = tempfile.mkstemp(prefix="modernization_wowpresence_seed_", suffix=".zip")
|
||||
os.close(fd)
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
archive.write(dll_path, "WowPresence.dll")
|
||||
archive.write(exe_path, "WowPresence.exe")
|
||||
return (
|
||||
_store_cached_file_safely(
|
||||
target_dir,
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
zip_path,
|
||||
"WowPresence.zip",
|
||||
revision=revision,
|
||||
)
|
||||
is not None
|
||||
)
|
||||
except (OSError, zipfile.BadZipFile):
|
||||
return False
|
||||
finally:
|
||||
try:
|
||||
os.remove(zip_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def install_wowpresence(target_dir, progress=None):
|
||||
"""Install or update WowPresence from its latest stable GitHub release ZIP."""
|
||||
_emit_progress(progress, "Checking WowPresence release...", None, None)
|
||||
@@ -757,6 +1141,7 @@ def install_wowpresence(target_dir, progress=None):
|
||||
package_revision=package_revision,
|
||||
package_digest=current_digest,
|
||||
)
|
||||
cache_installed_wowpresence(target_dir, revision=revision)
|
||||
_emit_progress(
|
||||
progress,
|
||||
f"WowPresence {revision} is already current.",
|
||||
@@ -809,6 +1194,19 @@ def install_wowpresence(target_dir, progress=None):
|
||||
package_digest=package_asset.get("digest"),
|
||||
package_revision=package_revision,
|
||||
)
|
||||
if _store_cached_file_safely(
|
||||
target_dir,
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
zip_path,
|
||||
"WowPresence.zip",
|
||||
revision=revision,
|
||||
) is None:
|
||||
_emit_progress(
|
||||
progress,
|
||||
"WowPresence installed; offline cache could not be refreshed.",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
finally:
|
||||
if zip_path and os.path.exists(zip_path):
|
||||
try:
|
||||
@@ -824,6 +1222,109 @@ def install_wowpresence(target_dir, progress=None):
|
||||
return revision
|
||||
|
||||
|
||||
def _install_wowpresence_fallback_archive(
|
||||
target_dir,
|
||||
zip_path,
|
||||
revision,
|
||||
package_revision,
|
||||
progress=None,
|
||||
):
|
||||
existing_manifest = _load_managed_manifest_data(
|
||||
target_dir,
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
)
|
||||
dlls_entry_preexisting = existing_manifest.get("dlls_entry_preexisting")
|
||||
if not isinstance(dlls_entry_preexisting, bool):
|
||||
if existing_manifest:
|
||||
dlls_entry_preexisting = _managed_backup_exists(
|
||||
target_dir,
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
"WowPresence.dll",
|
||||
)
|
||||
else:
|
||||
dlls_entry_preexisting = _dlls_contains_entry(
|
||||
target_dir,
|
||||
"WowPresence.dll",
|
||||
)
|
||||
|
||||
ensure_wowpresence_config(target_dir)
|
||||
extract_root = tempfile.mkdtemp(prefix="modernization_wowpresence_fallback_")
|
||||
try:
|
||||
_safe_extract(zip_path, extract_root)
|
||||
dll_path = _find_file(extract_root, "WowPresence.dll")
|
||||
exe_path = _find_file(extract_root, "WowPresence.exe")
|
||||
_verify_x86_pe(dll_path, "fallback WowPresence.dll")
|
||||
_verify_x86_pe(exe_path, "fallback WowPresence.exe")
|
||||
|
||||
_install_managed_files_transactional(
|
||||
target_dir,
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
[
|
||||
(dll_path, "WowPresence.dll"),
|
||||
(exe_path, "WowPresence.exe"),
|
||||
],
|
||||
revision=revision,
|
||||
)
|
||||
_set_managed_manifest_values(
|
||||
target_dir,
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
dlls_entry_preexisting=bool(dlls_entry_preexisting),
|
||||
file_sha256={
|
||||
"WowPresence.dll": _file_sha256(dll_path),
|
||||
"WowPresence.exe": _file_sha256(exe_path),
|
||||
},
|
||||
package_digest=None,
|
||||
package_revision=package_revision,
|
||||
)
|
||||
finally:
|
||||
shutil.rmtree(extract_root, ignore_errors=True)
|
||||
|
||||
ensure_wowpresence_config(target_dir)
|
||||
return revision
|
||||
|
||||
|
||||
def install_cached_wowpresence(target_dir, progress=None):
|
||||
"""Install the last validated WowPresence package cached by this WoW install."""
|
||||
cached = _load_cached_file(
|
||||
target_dir,
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
"WowPresence.zip",
|
||||
)
|
||||
if cached is None:
|
||||
raise RemotePackageError("No validated WowPresence offline fallback is cached.")
|
||||
|
||||
zip_path, metadata = cached
|
||||
revision = str(metadata.get("revision") or "cached")
|
||||
_emit_progress(progress, "Loading cached WowPresence fallback...", None, None)
|
||||
return _install_wowpresence_fallback_archive(
|
||||
target_dir,
|
||||
zip_path,
|
||||
revision,
|
||||
f"cache:{metadata.get('sha256', '')}",
|
||||
progress=progress,
|
||||
)
|
||||
|
||||
|
||||
def install_bundled_wowpresence(target_dir, progress=None):
|
||||
"""Install the release-bundled known-good WowPresence fallback."""
|
||||
bundled = _load_bundled_remote_fallback(
|
||||
WOWPRESENCE_MANAGED_ID,
|
||||
"WowPresence.zip",
|
||||
)
|
||||
if bundled is None:
|
||||
raise RemotePackageError("Bundled WowPresence fallback is unavailable.")
|
||||
|
||||
zip_path, metadata = bundled
|
||||
revision = str(metadata.get("revision") or "bundled")
|
||||
_emit_progress(progress, "Loading bundled WowPresence fallback...", None, None)
|
||||
return _install_wowpresence_fallback_archive(
|
||||
target_dir,
|
||||
zip_path,
|
||||
revision,
|
||||
f"bundled:{metadata.get('sha256', '')}",
|
||||
progress=progress,
|
||||
)
|
||||
|
||||
def install_interact(target_dir, progress=None):
|
||||
_emit_progress(progress, "Checking Interact release...", None, None)
|
||||
release = _latest_release("lookino/Interact")
|
||||
@@ -1051,6 +1552,20 @@ def _file_sha256(path):
|
||||
return digest.hexdigest().lower()
|
||||
|
||||
|
||||
def _git_blob_sha1(path):
|
||||
"""Compute the canonical Git blob SHA-1 for exact bundled-tree verification."""
|
||||
size = os.path.getsize(path)
|
||||
digest = hashlib.sha1()
|
||||
digest.update(f"blob {size}\0".encode("ascii"))
|
||||
with open(path, "rb") as handle:
|
||||
while True:
|
||||
chunk = handle.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest().lower()
|
||||
|
||||
|
||||
PACKAGE_STATE_DIR = "package_state"
|
||||
|
||||
|
||||
@@ -1162,7 +1677,17 @@ def _package_state_is_current(target_dir, package_id, revision):
|
||||
current = _snapshot_package_paths(target_dir, paths.keys())
|
||||
except OSError:
|
||||
return False
|
||||
return current == paths
|
||||
|
||||
is_current = current == paths
|
||||
if is_current:
|
||||
# A verified current package no longer needs rollback artifacts from
|
||||
# earlier successful installs. Retry best-effort cleanup here so a
|
||||
# Windows file lock does not leave them around indefinitely.
|
||||
for relative_path in paths:
|
||||
_cleanup_transaction_backups(
|
||||
os.path.join(target_dir, relative_path)
|
||||
)
|
||||
return is_current
|
||||
|
||||
|
||||
def _record_package_state_safely(target_dir, package_id, revision, relative_paths):
|
||||
@@ -1315,6 +1840,21 @@ def managed_mod_is_current(target_dir, mod_id, revision):
|
||||
return str(installed_revision) == str(revision)
|
||||
|
||||
|
||||
def managed_mpq_is_usable(target_dir, mod_id):
|
||||
"""True when a managed single-file MPQ still has a valid MPQ header."""
|
||||
if not managed_mod_is_installed(target_dir, mod_id):
|
||||
return False
|
||||
files = _load_managed_manifest(target_dir, mod_id)
|
||||
if len(files) != 1:
|
||||
return False
|
||||
path = os.path.join(target_dir, files[0])
|
||||
try:
|
||||
_verify_mpq(path)
|
||||
return True
|
||||
except RemotePackageError:
|
||||
return False
|
||||
|
||||
|
||||
def managed_mpq_is_current(target_dir, mod_id, revision):
|
||||
"""Cheap integrity check for an already installed managed MPQ."""
|
||||
if not managed_mod_is_current(target_dir, mod_id, revision):
|
||||
@@ -1616,6 +2156,48 @@ def _migrate_legacy_pink_herbs_patch(target_dir, downloaded_path, progress=None)
|
||||
revision=data.get("revision"),
|
||||
)
|
||||
|
||||
def _download_remote_mpq(
|
||||
url,
|
||||
progress=None,
|
||||
label="Downloading visual mod",
|
||||
timeout=300,
|
||||
):
|
||||
"""Download an MPQ while distinguishing source failures from local I/O."""
|
||||
try:
|
||||
temp_path = _download(
|
||||
url,
|
||||
suffix=".mpq",
|
||||
progress=progress,
|
||||
label=label,
|
||||
timeout=timeout,
|
||||
)
|
||||
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as exc:
|
||||
raise RemoteSourceUnavailable(
|
||||
f"{label}: remote source is unavailable ({exc})."
|
||||
) from exc
|
||||
|
||||
try:
|
||||
with open(temp_path, "rb") as handle:
|
||||
magic = handle.read(3)
|
||||
except OSError:
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
|
||||
if magic != b"MPQ":
|
||||
try:
|
||||
os.remove(temp_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise RemoteSourceUnavailable(
|
||||
f"{label}: remote source returned an invalid MPQ package."
|
||||
)
|
||||
|
||||
return temp_path
|
||||
|
||||
|
||||
def _install_remote_mpq(
|
||||
target_dir,
|
||||
mod_id,
|
||||
@@ -1626,16 +2208,32 @@ def _install_remote_mpq(
|
||||
timeout=300,
|
||||
revision=None,
|
||||
):
|
||||
temp_path = _download(
|
||||
if revision is not None and managed_mpq_is_current(
|
||||
target_dir,
|
||||
mod_id,
|
||||
revision,
|
||||
):
|
||||
_emit_progress(
|
||||
progress,
|
||||
f"{label.replace('Downloading ', '')} is already current.",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
return
|
||||
|
||||
temp_path = _download_remote_mpq(
|
||||
url,
|
||||
suffix=".mpq",
|
||||
progress=progress,
|
||||
label=label,
|
||||
timeout=timeout,
|
||||
)
|
||||
try:
|
||||
_verify_mpq(temp_path)
|
||||
_emit_progress(progress, f"Installing {os.path.basename(destination)}...", None, None)
|
||||
_emit_progress(
|
||||
progress,
|
||||
f"Installing {os.path.basename(destination)}...",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
_install_managed_files(
|
||||
target_dir,
|
||||
mod_id,
|
||||
@@ -1714,26 +2312,68 @@ def install_fog_pushback(target_dir, progress=None):
|
||||
def install_pink_herbs(target_dir, progress=None):
|
||||
mod_id = "visual_pink_herbs"
|
||||
destination = os.path.join("Data", "patch-V.mpq")
|
||||
revision = _branch_head_sha("seacrabsam/patch-herb", "main")
|
||||
target_path = os.path.join(target_dir, destination)
|
||||
installed_revision = _current_recorded_package_revision(target_dir, mod_id)
|
||||
|
||||
try:
|
||||
revision = _branch_head_sha("seacrabsam/patch-herb", "main")
|
||||
except RemotePackageError as revision_error:
|
||||
source_error = RemoteSourceUnavailable(
|
||||
f"Pink Herbs: could not resolve the upstream revision ({revision_error})."
|
||||
)
|
||||
if installed_revision is not None:
|
||||
try:
|
||||
_verify_mpq(target_path)
|
||||
except RemotePackageError:
|
||||
installed_revision = None
|
||||
else:
|
||||
_emit_progress(
|
||||
progress,
|
||||
f"Pink Herbs {installed_revision[:7]} kept; update source unavailable.",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
return f"seacrabsam/patch-herb main@{installed_revision[:7]}"
|
||||
raise source_error from revision_error
|
||||
|
||||
if installed_revision == revision:
|
||||
try:
|
||||
_verify_mpq(target_path)
|
||||
except RemotePackageError:
|
||||
installed_revision = None
|
||||
else:
|
||||
_emit_progress(
|
||||
progress,
|
||||
f"Pink Herbs {revision[:7]} is already current.",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
return f"seacrabsam/patch-herb main@{revision[:7]}"
|
||||
|
||||
try:
|
||||
temp_path = _download_remote_mpq(
|
||||
f"https://raw.githubusercontent.com/seacrabsam/patch-herb/{revision}/patch-H.mpq",
|
||||
progress=progress,
|
||||
label="Downloading Pink Herbs",
|
||||
timeout=300,
|
||||
)
|
||||
except RemoteSourceUnavailable as download_error:
|
||||
if installed_revision is not None:
|
||||
try:
|
||||
_verify_mpq(target_path)
|
||||
except RemotePackageError:
|
||||
installed_revision = None
|
||||
else:
|
||||
_emit_progress(
|
||||
progress,
|
||||
f"Pink Herbs {installed_revision[:7]} kept; latest download unavailable.",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
return f"seacrabsam/patch-herb main@{installed_revision[:7]}"
|
||||
raise
|
||||
|
||||
if _package_state_is_current(target_dir, mod_id, revision):
|
||||
_emit_progress(
|
||||
progress,
|
||||
f"Pink Herbs {revision[:7]} is already current.",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
return f"seacrabsam/patch-herb main@{revision[:7]}"
|
||||
|
||||
temp_path = _download(
|
||||
"https://raw.githubusercontent.com/seacrabsam/patch-herb/main/patch-H.mpq",
|
||||
suffix=".mpq",
|
||||
progress=progress,
|
||||
label="Downloading Pink Herbs",
|
||||
timeout=300,
|
||||
)
|
||||
try:
|
||||
_verify_mpq(temp_path)
|
||||
_migrate_legacy_pink_herbs_patch(target_dir, temp_path, progress=progress)
|
||||
_emit_progress(
|
||||
progress,
|
||||
@@ -1762,8 +2402,15 @@ def install_pink_herbs(target_dir, progress=None):
|
||||
return f"seacrabsam/patch-herb main@{revision[:7]}"
|
||||
|
||||
|
||||
def _download_github_branch_archive(repo, branch, progress=None, label="Downloading sound mod"):
|
||||
url = f"https://codeload.github.com/{repo}/zip/refs/heads/{branch}"
|
||||
def _download_github_branch_archive(
|
||||
repo,
|
||||
branch,
|
||||
progress=None,
|
||||
label="Downloading sound mod",
|
||||
revision=None,
|
||||
):
|
||||
revision = revision or _branch_head_sha(repo, branch)
|
||||
url = f"https://codeload.github.com/{repo}/zip/{revision}"
|
||||
return _download(
|
||||
url,
|
||||
suffix=".zip",
|
||||
@@ -1812,6 +2459,7 @@ def _install_github_sound_pack(target_dir, mod_id, repo, branch, source_folder,
|
||||
branch,
|
||||
progress=progress,
|
||||
label=label,
|
||||
revision=revision,
|
||||
)
|
||||
extract_root = tempfile.mkdtemp(prefix=f"modernization_{mod_id}_")
|
||||
try:
|
||||
@@ -1992,7 +2640,7 @@ def install_no1600x1200(target_dir, progress=None):
|
||||
|
||||
url = (
|
||||
"https://raw.githubusercontent.com/RetroCro/TurtleWoW-Mods/"
|
||||
"refs/heads/main/Archive/DLL%20BACKUP/no1600x1200.dll"
|
||||
f"{revision}/Archive/DLL%20BACKUP/no1600x1200.dll"
|
||||
)
|
||||
temp_path = _download(
|
||||
url,
|
||||
@@ -2198,7 +2846,7 @@ def install_superwow(target_dir, progress=None):
|
||||
# Prepare SuperAPI completely before changing the installed DLL.
|
||||
_emit_progress(progress, "Preparing SuperAPI update...", None, None)
|
||||
superapi_zip = _download(
|
||||
"https://codeload.github.com/balakethelock/SuperAPI/zip/refs/heads/master",
|
||||
f"https://codeload.github.com/balakethelock/SuperAPI/zip/{superapi_revision}",
|
||||
suffix=".zip",
|
||||
progress=progress,
|
||||
label="Downloading SuperAPI addon",
|
||||
|
||||
@@ -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
-118
@@ -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,37 +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():
|
||||
# Hosted static mirrors use explicit revisions and can be
|
||||
# skipped without any network request. Pink Herbs follows a
|
||||
# GitHub branch, so its installer performs a lightweight
|
||||
# branch-SHA check before deciding whether a download is
|
||||
# needed.
|
||||
if key != "pink_herbs":
|
||||
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)
|
||||
|
||||
@@ -2518,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()),
|
||||
@@ -2553,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:
|
||||
@@ -2576,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()),
|
||||
@@ -2586,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")
|
||||
@@ -2601,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")
|
||||
|
||||
+756
-1130
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user