mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 04:17:38 +00:00
Compare commits
453
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d021ae3e3d | ||
|
|
f4862dc817 | ||
|
|
3a6f53f919 | ||
|
|
19b299d3df | ||
|
|
77b304ae68 | ||
|
|
c965e49117 | ||
|
|
ead52e79ce | ||
|
|
7cb6cda101 | ||
|
|
5ab497f2c7 | ||
|
|
205d1fd43b | ||
|
|
950c9deb76 | ||
|
|
e7ccc11309 | ||
|
|
bab31b5f7b | ||
|
|
e0e24bab34 | ||
|
|
a46d823b01 | ||
|
|
38337c841d | ||
|
|
5a0b4f2e31 | ||
|
|
7d9dafbfcf | ||
|
|
79d4b7f44d | ||
|
|
9b07ad15df | ||
|
|
36d55ab305 | ||
|
|
ebe9b9b6de | ||
|
|
4df29722d2 | ||
|
|
086b9f9fbb | ||
|
|
bc2481395d | ||
|
|
daded912c2 | ||
|
|
a0c5c8a30c | ||
|
|
e26f8fb49e | ||
|
|
da9e9b6f55 | ||
|
|
a5e8a6f5b6 | ||
|
|
f191c53c2a | ||
|
|
797fea90ed | ||
|
|
026fe20a89 | ||
|
|
529bacd0cb | ||
|
|
c16c3a90dc | ||
|
|
ab829e98c0 | ||
|
|
602840419b | ||
|
|
40cd25315d | ||
|
|
984dc236ab | ||
|
|
cdc3506cdd | ||
|
|
6347bdd6c5 | ||
|
|
ff4a9ffd5a | ||
|
|
9541dc2949 | ||
|
|
1340cc75c7 | ||
|
|
9c50a90d34 | ||
|
|
1942383494 | ||
|
|
0f37ddc48d | ||
|
|
ac118d5305 | ||
|
|
cf645a30af | ||
|
|
fcd7e52be0 | ||
|
|
886a4cb140 | ||
|
|
e30da939ec | ||
|
|
6e4efb39f7 | ||
|
|
8c21ab4b04 | ||
|
|
7ec3829eb5 | ||
|
|
86bfd881a9 | ||
|
|
cd55ce9139 | ||
|
|
1f8042088c | ||
|
|
a86d5ec48f | ||
|
|
70dadf0449 | ||
|
|
5363434439 | ||
|
|
9bac760195 | ||
|
|
bd02983a08 | ||
|
|
852cf44b7d | ||
|
|
e59f9de5a8 | ||
|
|
59164e3575 | ||
|
|
711c810b72 | ||
|
|
dceec8b74c | ||
|
|
9edce4a9f9 | ||
|
|
4e3a18cdd1 | ||
|
|
fc8fba65cc | ||
|
|
f01a059cc0 | ||
|
|
8d22876c01 | ||
|
|
39a14054fe | ||
|
|
7e1edce462 | ||
|
|
e7b6c39ace | ||
|
|
9027e22e4f | ||
|
|
7add0fc88f | ||
|
|
04ce033907 | ||
|
|
1f3ce9229d | ||
|
|
091d2204a6 | ||
|
|
d93861d746 | ||
|
|
d9827852c7 | ||
|
|
2e75812339 | ||
|
|
1454dbe00c | ||
|
|
259071b647 | ||
|
|
2d887df71c | ||
|
|
317539dbd8 | ||
|
|
10d71371dd | ||
|
|
4959c2c38c | ||
|
|
cece5a4214 | ||
|
|
9daf3960c4 | ||
|
|
39ab9eb1ee | ||
|
|
e8bd35d8e4 | ||
|
|
f4d8f56c59 | ||
|
|
3f33320724 | ||
|
|
492085ac0c | ||
|
|
7b3b61de15 | ||
|
|
77efe8d796 | ||
|
|
8378f3c350 | ||
|
|
1a2f806f16 | ||
|
|
90be617700 | ||
|
|
45b54e89ba | ||
|
|
35b8eaa376 | ||
|
|
f5082cedee | ||
|
|
cca8ff3c55 | ||
|
|
b4256406f5 | ||
|
|
2adabbbd70 | ||
|
|
42ed0d27c7 | ||
|
|
0d19deaee0 | ||
|
|
abd5367533 | ||
|
|
3e83827ae9 | ||
|
|
443c78c453 | ||
|
|
a97909e36c | ||
|
|
89f8932b35 | ||
|
|
829b1cfed3 | ||
|
|
33804bb66d | ||
|
|
80d3e5d8d7 | ||
|
|
bfcd95504e | ||
|
|
bea82687a4 | ||
|
|
63078578b4 | ||
|
|
c7bb90895f | ||
|
|
cd7f830697 | ||
|
|
557dc2f454 | ||
|
|
6c5269e3e6 | ||
|
|
b5aed30a5d | ||
|
|
9755cd3e09 | ||
|
|
604d87b9fc | ||
|
|
6b0a74ec4b | ||
|
|
165ccf3e9c | ||
|
|
ce4f80518f | ||
|
|
3009c6d8d2 | ||
|
|
b83bb5b52b | ||
|
|
f30efb29b1 | ||
|
|
19b6232e43 | ||
|
|
bbac6b9cac | ||
|
|
e48e4e2361 | ||
|
|
09cd9040f7 | ||
|
|
8f79084c57 | ||
|
|
597b16cb74 | ||
|
|
60ef54b4a6 | ||
|
|
0e8b002af9 | ||
|
|
8bc43b455c | ||
|
|
923cf943e3 | ||
|
|
3a06590add | ||
|
|
c1086189f7 | ||
|
|
e827e319be | ||
|
|
d3667797b5 | ||
|
|
e1ae53488b | ||
|
|
b1a58f0d08 | ||
|
|
a61ff06e9a | ||
|
|
3d2ac4d944 | ||
|
|
167815f27b | ||
|
|
80af00f0c7 | ||
|
|
15e1e21c8c | ||
|
|
8318daa74a | ||
|
|
fc4a983644 | ||
|
|
cd5d38d921 | ||
|
|
51b8a47d7a | ||
|
|
8801b8ce35 | ||
|
|
128186288a | ||
|
|
b46b0cd503 | ||
|
|
1e81acf277 | ||
|
|
a0ee633397 | ||
|
|
792cd4c62b | ||
|
|
bdd9e8faad | ||
|
|
b39e95bc66 | ||
|
|
e83307d4c5 | ||
|
|
628b1c347e | ||
|
|
ed8f0ba851 | ||
|
|
15a36827cd | ||
|
|
52e17d05c1 | ||
|
|
00bad65103 | ||
|
|
8e800502e1 | ||
|
|
da7be7a50a | ||
|
|
9ba66e147c | ||
|
|
7614a50aa4 | ||
|
|
53af083339 | ||
|
|
718e5a31fe | ||
|
|
4554c3a5f5 | ||
|
|
7240688008 | ||
|
|
f8e242639b | ||
|
|
81c4aedb06 | ||
|
|
f08e360f10 | ||
|
|
8d1402dc83 | ||
|
|
81eaf24748 | ||
|
|
bac67038ea | ||
|
|
c8880444f4 | ||
|
|
3e4db9279e | ||
|
|
62cabb2acb | ||
|
|
81e1396f81 | ||
|
|
cf5e9c8ee7 | ||
|
|
622478df04 | ||
|
|
362669b071 | ||
|
|
929b6f5c60 | ||
|
|
3751b31cda | ||
|
|
20c4312251 | ||
|
|
7e6bfd70ad | ||
|
|
c1ac5109e5 | ||
|
|
0b7076d330 | ||
|
|
1d3ca3db6e | ||
|
|
f112a13695 | ||
|
|
986f849b65 | ||
|
|
863cb61e8d | ||
|
|
28bc2b3ee2 | ||
|
|
f6ba0ff9a1 | ||
|
|
1e2a36d630 | ||
|
|
2cab0dcb12 | ||
|
|
a2e3eed0de | ||
|
|
9b01cca336 | ||
|
|
1384160fb0 | ||
|
|
116a3b352c | ||
|
|
56871fdebf | ||
|
|
f96e46113e | ||
|
|
0ee594b9e1 | ||
|
|
943597ca0d | ||
|
|
a446e4cf8c | ||
|
|
a53a08be95 | ||
|
|
0668c37156 | ||
|
|
a37b18bdfd | ||
|
|
3d41c8a702 | ||
|
|
5db35619ca | ||
|
|
6b95c03888 | ||
|
|
1a47cb3215 | ||
|
|
d243457ecf | ||
|
|
e84496d036 | ||
|
|
3ce2880c93 | ||
|
|
9f4e45551b | ||
|
|
09492782cc | ||
|
|
b3955f89df | ||
|
|
1645f6513a | ||
|
|
2d2a5b9442 | ||
|
|
9a2de093b9 | ||
|
|
ff57450b7c | ||
|
|
05b978df3c | ||
|
|
e2c789510a | ||
|
|
fd7843b8b3 | ||
|
|
e6f0a077e6 | ||
|
|
bbd78b437f | ||
|
|
d5c6530b80 | ||
|
|
a47f871adb | ||
|
|
b9071a37f9 | ||
|
|
9ab6627d90 | ||
|
|
799ab1d3ce | ||
|
|
77fb074191 | ||
|
|
5c5c21e2ec | ||
|
|
eccf5896cb | ||
|
|
fdae686cc9 | ||
|
|
e75e585551 | ||
|
|
4ea0223f34 | ||
|
|
6f55df19fa | ||
|
|
889a096963 | ||
|
|
90b4691130 | ||
|
|
99de63755b | ||
|
|
7cd891e86c | ||
|
|
6e50577565 | ||
|
|
024c6d2021 | ||
|
|
4b47059788 | ||
|
|
1aa5a07823 | ||
|
|
00f32e0e2f | ||
|
|
5537432662 | ||
|
|
5962ccc83d | ||
|
|
f5acea6060 | ||
|
|
a86f1219c3 | ||
|
|
338c6777d6 | ||
|
|
aeb3bdf07b | ||
|
|
1caf9fde18 | ||
|
|
d496ff2aa7 | ||
|
|
8b96fccfe9 | ||
|
|
9d3bdfb6ad | ||
|
|
42d037f080 | ||
|
|
af4a379311 | ||
|
|
2b9d0e7127 | ||
|
|
a68fb0a6be | ||
|
|
721f32ac77 | ||
|
|
fcdc24471c | ||
|
|
58f5b4f78e | ||
|
|
6be4aac9fb | ||
|
|
a6270469e1 | ||
|
|
a8f73623c4 | ||
|
|
47b258ad95 | ||
|
|
ac6bd552d5 | ||
|
|
2a05baee8d | ||
|
|
80ef0e2988 | ||
|
|
5df5ce00cf | ||
|
|
1297b9522a | ||
|
|
b3188ced2a | ||
|
|
f55893fdf4 | ||
|
|
e0b2e56070 | ||
|
|
20dc9fc962 | ||
|
|
51d226f67d | ||
|
|
eb3c32094c | ||
|
|
26ca1b4e84 | ||
|
|
7331922070 | ||
|
|
27d8f90f03 | ||
|
|
35ef1228eb | ||
|
|
76cc907bd0 | ||
|
|
e9e431e014 | ||
|
|
fbcc1c9ae1 | ||
|
|
142faafb13 | ||
|
|
c243489916 | ||
|
|
917b15640e | ||
|
|
7ccbbaaf68 | ||
|
|
71805db62f | ||
|
|
85619155f8 | ||
|
|
46dc198505 | ||
|
|
7707127d5f | ||
|
|
fa7d71435f | ||
|
|
3467513df9 | ||
|
|
6000df8009 | ||
|
|
fde3beca9d | ||
|
|
452f3c14fc | ||
|
|
6bb12c3c12 | ||
|
|
7a08199ba5 | ||
|
|
a5bb3823fb | ||
|
|
8c941c757d | ||
|
|
2fff9564b2 | ||
|
|
bbefdb847d | ||
|
|
56d2dbbc27 | ||
|
|
0296e0e5a4 | ||
|
|
6812771a89 | ||
|
|
4d835aa674 | ||
|
|
dfbba225d2 | ||
|
|
930b0c6492 | ||
|
|
428c3308c6 | ||
|
|
a33146241e | ||
|
|
c7af8d396e | ||
|
|
a2a2ccb804 | ||
|
|
7a4ab29cb0 | ||
|
|
ef8ea710d4 | ||
|
|
a2d7d4e9e6 | ||
|
|
449841b4eb | ||
|
|
89c705b922 | ||
|
|
bf79ee1d93 | ||
|
|
4f99a1ed95 | ||
|
|
298eff528c | ||
|
|
b8b9da2adb | ||
|
|
40abcabe9d | ||
|
|
108a91a843 | ||
|
|
b4e5f4f2bb | ||
|
|
390eec4749 | ||
|
|
f86893c8ef | ||
|
|
d4ac5dc210 | ||
|
|
b0585f5628 | ||
|
|
6013d8b937 | ||
|
|
27277b11aa | ||
|
|
ccfdb8309c | ||
|
|
be7a8a0dd7 | ||
|
|
f9167bac06 | ||
|
|
7d82ae8e92 | ||
|
|
ed7e7628c7 | ||
|
|
a562573559 | ||
|
|
5aa382bbb1 | ||
|
|
d98d3a9fd5 | ||
|
|
3d91ae0f91 | ||
|
|
fedccf5b95 | ||
|
|
0ba73b8457 | ||
|
|
d9565fbe1f | ||
|
|
61db68dd7f | ||
|
|
f895d38f76 | ||
|
|
804ed61c76 | ||
|
|
aacace1645 | ||
|
|
4bf2c17e0f | ||
|
|
3191c4dcfc | ||
|
|
373dce8202 | ||
|
|
4c7f1c36d6 | ||
|
|
36c367a6d6 | ||
|
|
fe9e7dddcd | ||
|
|
9d8f1b1b5d | ||
|
|
9d1e14410c | ||
|
|
8b06caf708 | ||
|
|
4684088675 | ||
|
|
9a26207df2 | ||
|
|
2adfaff020 | ||
|
|
8a7ab490f4 | ||
|
|
e26f8958e9 | ||
|
|
f0a83f1f31 | ||
|
|
5070c892e2 | ||
|
|
8295ec9442 | ||
|
|
c5b1431073 | ||
|
|
b39c16578b | ||
|
|
7d7e392924 | ||
|
|
14fa610bc8 | ||
|
|
8ca415e9a0 | ||
|
|
becf2f4866 | ||
|
|
28658c2995 | ||
|
|
f5cce25d23 | ||
|
|
1306eaf1b8 | ||
|
|
807afc083f | ||
|
|
c26afcc050 | ||
|
|
a124629669 | ||
|
|
be03d45f0c | ||
|
|
66c878d03e | ||
|
|
3ad87eb2f3 | ||
|
|
a9d2ad73f3 | ||
|
|
db88fca050 | ||
|
|
26af9bd30f | ||
|
|
e7156caa47 | ||
|
|
02ad0ea6ab | ||
|
|
1dee8e5683 | ||
|
|
583f4bb011 | ||
|
|
23ebfbe7ab | ||
|
|
18927551d5 | ||
|
|
2c2f280c51 | ||
|
|
991baa2610 | ||
|
|
e68b690cf1 | ||
|
|
e5690b1c6a | ||
|
|
e91b1457fd | ||
|
|
09c5832ae8 | ||
|
|
4015fee5e2 | ||
|
|
744bfb31d1 | ||
|
|
8706d1d99b | ||
|
|
8637bf05e5 | ||
|
|
802039fd08 | ||
|
|
d14f0b44c1 | ||
|
|
008282d65a | ||
|
|
9a8c314d69 | ||
|
|
4b1bf16ba5 | ||
|
|
5528fb5fa4 | ||
|
|
7ab32df2aa | ||
|
|
8e1429bfac | ||
|
|
9322382596 | ||
|
|
c3cf9e6f3a | ||
|
|
f5d6046523 | ||
|
|
e1ebf6ecce | ||
|
|
698827e9c5 | ||
|
|
60530e32fc | ||
|
|
3cffb71b20 | ||
|
|
7887237a6e | ||
|
|
98af79137b | ||
|
|
622882725f | ||
|
|
0517bb3753 | ||
|
|
fb17488237 | ||
|
|
b1ee8cc183 | ||
|
|
c08ded6db4 | ||
|
|
640f78b2eb | ||
|
|
a51ec40e52 | ||
|
|
2ce5b007af | ||
|
|
be92d721f0 | ||
|
|
b81f672ad7 | ||
|
|
fdbe66b057 | ||
|
|
bd1bd9f5d8 | ||
|
|
b6698ecc7e | ||
|
|
e123a6facc | ||
|
|
695077b6da | ||
|
|
4d744b6638 | ||
|
|
c2a4cbdf27 | ||
|
|
d30ae53fbf | ||
|
|
8f32955823 | ||
|
|
2778643f4f | ||
|
|
3b60ca4094 | ||
|
|
c20be0cef1 | ||
|
|
1ad6b57202 |
@@ -53,6 +53,9 @@ jobs:
|
||||
build:
|
||||
needs: pick-runner
|
||||
runs-on: ${{ needs.pick-runner.outputs.label }}
|
||||
outputs:
|
||||
tag: ${{ steps.ver.outputs.tag }}
|
||||
is_release: ${{ steps.ver.outputs.is_release }}
|
||||
# Windows PowerShell 5.1 is always present; the runner's default `pwsh`
|
||||
# (PowerShell Core) is not installed by default, and `shell: bash` on a
|
||||
# self-hosted box resolves to the WSL launcher in System32.
|
||||
@@ -68,7 +71,7 @@ jobs:
|
||||
# is_release=true when a vX.Y.Z tag is in play (pushed tag,
|
||||
# dispatched-from-tag, or a `tag` input); the embedded addon's
|
||||
# `## Version:` and the compiled CLASSIC_API_VERSION are both
|
||||
# stamped from it at embed/compile time (see cmake/embed_addon.cmake)
|
||||
# stamped from it at embed/compile time (see cmake/embed_resources.cmake)
|
||||
# — no source edit.
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
@@ -97,12 +100,28 @@ jobs:
|
||||
- name: Build
|
||||
run: cmake --build build --config Release
|
||||
|
||||
# Dev builds carry their own symbols. With the PDB sitting next to the DLL
|
||||
# the client's crash handler resolves our frames to file and line — a
|
||||
# tester's log reads `Texture::Mask::PrimIndexed_h+12 (Mask.cpp,246)`
|
||||
# instead of a bare address — and the map resolves `0001:offset` addresses
|
||||
# by hand when there is no dump. Both match THIS build and nothing else,
|
||||
# which is why they travel with it rather than being rebuilt later from the
|
||||
# same commit. Unlike the release bundle these need no off-putting name:
|
||||
# the artifact is behind the Actions UI, so whoever downloads it is already
|
||||
# the audience for symbols.
|
||||
- name: Upload artifact
|
||||
if: steps.ver.outputs.is_release != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ClassicAPI-${{ github.sha }}
|
||||
path: build/Release/ClassicAPI.dll
|
||||
# Shared parent is build/Release, so all three land at the zip root.
|
||||
path: |
|
||||
build/Release/ClassicAPI.dll
|
||||
build/Release/ClassicAPI.pdb
|
||||
build/Release/ClassicAPI.map
|
||||
# A symbol-less artifact looks identical to a good one until someone
|
||||
# needs a stack out of it, so fail the build instead of warning.
|
||||
if-no-files-found: error
|
||||
|
||||
# Bundle both debug artifacts into one zip so a crash from a shipped DLL
|
||||
# can be triaged exactly. The PDB gives WinDbg/x64dbg/VS a line-level
|
||||
|
||||
+1
-1
@@ -47,4 +47,4 @@
|
||||
|
||||
CLAUDE.md
|
||||
|
||||
/AddOns/!!!ClassicAPI/.classicapi-dev
|
||||
/src/embed/AddOns/*/.classicapi-dev
|
||||
|
||||
@@ -7,3 +7,6 @@
|
||||
[submodule "external/tinycbor"]
|
||||
path = external/tinycbor
|
||||
url = https://github.com/intel/tinycbor.git
|
||||
[submodule "external/unordered_dense"]
|
||||
path = external/unordered_dense
|
||||
url = https://github.com/martinus/unordered_dense.git
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
## Interface: 11200
|
||||
## Title: !!!ClassicAPI
|
||||
## Notes: Library to emulate aspects of the modern WoW API on 1.12.1 / Lua 5.0.
|
||||
## Version: DEV
|
||||
## Author: Brues
|
||||
## X-Website: https://github.com/brues-code/ClassicAPI
|
||||
|
||||
locales\locales.xml
|
||||
|
||||
SecureTemplates.xml
|
||||
Compat.lua
|
||||
Util\Print.lua
|
||||
Util\Mixin.lua
|
||||
Util\Color.lua
|
||||
Util\Constants.lua
|
||||
Util\TableUtil.lua
|
||||
Util\MathUtil.lua
|
||||
Util\PixelUtil.lua
|
||||
Util\FrameUtil.lua
|
||||
Util\FormattingUtil.lua
|
||||
Util\TimeUtil.lua
|
||||
Util\CallbackRegistry.lua
|
||||
Util\GlobalCallbackRegistry.lua
|
||||
Util\ColorUtil.lua
|
||||
Util\ItemLocation.lua
|
||||
Util\PlayerLocation.lua
|
||||
Util\EventUtil.lua
|
||||
Util\ItemUtil.lua
|
||||
Util\AddOnCompat.lua
|
||||
Util\FunctionUtil.lua
|
||||
Util\CacheUtil.lua
|
||||
Util\Pools.lua
|
||||
Util\FrameWatcher.lua
|
||||
Util\TimedCallback.lua
|
||||
Util\EquipmentManager.lua
|
||||
Util\PlayerUtil.lua
|
||||
Util\SlashCommandsRegistry.lua
|
||||
Util\LinkUtil.lua
|
||||
Util\TradeSkillLink.lua
|
||||
Util\IconDataProvider.lua
|
||||
Util\GameTooltip.lua
|
||||
Util\Rectangle.lua
|
||||
Util\Vector2D.lua
|
||||
Util\Vector3D.lua
|
||||
Util\Vector4D.lua
|
||||
Util\UIParent.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
Util\AuraUtil.lua
|
||||
Util\SecureUnitMenu.lua
|
||||
Util\TextureUtil.lua
|
||||
Util\MacroOptions.lua
|
||||
Util\SecureStateDriver.lua
|
||||
@@ -1,115 +0,0 @@
|
||||
if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
EventUtil.ContinueOnAddOnLoaded('pfUI', function()
|
||||
-- pfUI\pfUI.lua redeclares the RAID_CLASS_COLORS object at least 14 times (insane!!)
|
||||
-- This stops it from replacing all our ColorMixin values
|
||||
pfUI.UpdateColors = function() end
|
||||
RAID_CLASS_COLORS = setmetatable(RAID_CLASS_COLORS, { __index = function()
|
||||
local unknownColor = CreateColor(0.6, 0.6, 0.6)
|
||||
unknownColor.colorStr = unknownColor:GenerateHexColor()
|
||||
return unknownColor
|
||||
end})
|
||||
|
||||
-- Compatibility with OLDER forks of pfUI. Our DLL adds the modern
|
||||
-- HookScript widget method; older pfUI forks branch on
|
||||
-- `if button.HookScript` and take a modern code path they were never
|
||||
-- written for on their pfActionBar buttons, which breaks them. While
|
||||
-- the actionbar module builds those buttons, temporarily wrap
|
||||
-- CreateFrame to shadow HookScript with a falsy field so those forks
|
||||
-- fall back to their vanilla SetScript path. The wrapper is removed as
|
||||
-- soon as the actionbar module finishes loading so it affects nothing
|
||||
-- else. A pfUI is trusted to handle HookScript itself if EITHER signal
|
||||
-- is present: the maintained "brues" X-Website tag OR the
|
||||
-- `pfUI.handlesHookScript` capability flag. Without the website the
|
||||
-- flag is required; if neither is present we apply the shim.
|
||||
local website = GetAddOnMetadata("pfUI", "X-Website")
|
||||
local hasBruesWebsite = website and strfind(website, 'brues')
|
||||
if not hasBruesWebsite and not pfUI.handlesHookScript then
|
||||
local _createFrame = CreateFrame
|
||||
local HookedCreateFrame = function(frameType, name, parent, template)
|
||||
local frame = _createFrame(frameType, name, parent, template)
|
||||
if frameType == "Button" and strfind(name or "", "pfActionBar") then
|
||||
frame.HookScript = false
|
||||
end
|
||||
return frame
|
||||
end
|
||||
hooksecurefunc(pfUI, 'LoadModule', function(frame, moduleName)
|
||||
if moduleName == "updatenotify" then
|
||||
CreateFrame = HookedCreateFrame
|
||||
elseif moduleName == "actionbar" then
|
||||
CreateFrame = _createFrame
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if C_AddOns.DoesAddOnExist("SpecialTalentUI") then
|
||||
EventUtil.ContinueOnAddOnLoaded("SpecialTalentUI", CAPI_ApplyStandardColorGlobals)
|
||||
end
|
||||
|
||||
-- ShaguTweaks libpredict register's TBC events that ClassicAPI backports.
|
||||
if C_AddOns.DoesAddOnExist("ShaguTweaks") then
|
||||
EventUtil.ContinueOnAddOnLoaded("ShaguTweaks", function()
|
||||
local libp = ShaguTweaks.libpredict
|
||||
if libp then
|
||||
libp.sender:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
|
||||
libp.sender:UnregisterEvent("UNIT_SPELLCAST_START")
|
||||
libp.sender:UnregisterEvent("UNIT_SPELLCAST_STOP")
|
||||
libp.sender:UnregisterEvent("UNIT_SPELLCAST_FAILED")
|
||||
libp.sender:UnregisterEvent("UNIT_SPELLCAST_INTERRUPTED")
|
||||
libp.sender:UnregisterEvent("UNIT_SPELLCAST_SENT")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Guard UPDATE_MOUSEOVER_UNIT handlers (issue #12).
|
||||
function CAPI_MouseoverClearedCompat(frame)
|
||||
if not frame then return end
|
||||
local originalOnEvent = frame:GetScript("OnEvent")
|
||||
frame:SetScript("OnEvent", function()
|
||||
if event == "UPDATE_MOUSEOVER_UNIT" and not UnitExists("mouseover") then
|
||||
return
|
||||
end
|
||||
|
||||
if originalOnEvent then
|
||||
originalOnEvent()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
CAPI_MouseoverClearedCompat(GameTooltip)
|
||||
|
||||
if C_AddOns.DoesAddOnExist("Puppeteer") then
|
||||
EventUtil.ContinueOnAddOnLoaded("Puppeteer", function()
|
||||
CAPI_MouseoverClearedCompat(PTEnemyUpdater)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Cartographer_Notes gates a WorldMap release-spirit hook behind an
|
||||
-- `if lua51` probe, treating a 5.1-capable Lua as a modern client that has
|
||||
-- the WorldMapDeathRelease frame. Our 5.1 syntax backport makes the probe
|
||||
-- pass, so Cartographer takes the modern branch and hooks a frame 1.12's
|
||||
-- WorldMap never had (WorldMapDeathRelease:SetScript in OnEnable) -> nil
|
||||
-- index. Provide an inert, hidden stub so the hook succeeds and does nothing
|
||||
-- -- the same outcome vanilla had, where the branch never ran. The embedded
|
||||
-- !!!ClassicAPI addon loads first, so the frame exists before Cartographer's
|
||||
-- OnEnable.
|
||||
if C_AddOns.DoesAddOnExist("Cartographer") and not WorldMapDeathRelease then
|
||||
CreateFrame("Button", "WorldMapDeathRelease", WorldMapButton or UIParent):Hide()
|
||||
end
|
||||
|
||||
-- ModernSpellBook (Spellbook\MSB_Spellbook.lua) re-hooks its "show all ranks"
|
||||
-- checkbox by calling HookScript as a GLOBAL -- HookScript(frame, "OnClick",
|
||||
-- handler). That global never existed on any client: HookScript has always
|
||||
-- been the widget method frame:HookScript, which our DLL provides. MSB even
|
||||
-- guards the call with `if checkbox.HookScript` (the method IS present), then
|
||||
-- calls the global (nil) -> "attempt to call global 'HookScript'". Define the
|
||||
-- global so the call resolves, forwarding to the method. Gated on the addon
|
||||
-- being present so the global namespace stays clean otherwise.
|
||||
if C_AddOns.DoesAddOnExist("ModernSpellBook") and not HookScript then
|
||||
function HookScript(frame, script, handler)
|
||||
if frame and frame.HookScript then
|
||||
frame:HookScript(script, handler)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,128 +0,0 @@
|
||||
-- AuraUtil: iterate / find auras without allocating a table per aura.
|
||||
--
|
||||
-- Backport of FrameXML's AuraUtil, the helper library that sits on top of
|
||||
-- C_UnitAuras. Its whole value on 1.12 is the zero-allocation path: modern
|
||||
-- addons that scan many units' auras every frame call AuraUtil.ForEachAura (with
|
||||
-- usePackedAura false) instead of C_UnitAuras.GetAuraDataByIndex, so no AuraData
|
||||
-- table is built per aura -- which matters far more under Lua 5.0's mark-sweep
|
||||
-- GC than it does on retail's generational GC. See ClassicAPI issue #30.
|
||||
--
|
||||
-- Ported from the anniversary source (Blizzard_FrameXMLUtil/AuraUtil.lua),
|
||||
-- keeping the exact public shapes:
|
||||
-- * ForEachAura(unit, filter, batchSize, func, usePackedAura)
|
||||
-- * FindAura(predicate, unit, filter, arg1, arg2, arg3)
|
||||
-- * FindAuraByName(auraName, unit, filter)
|
||||
-- * UnpackAuraData(auraData)
|
||||
--
|
||||
-- Two deliberate differences from that source:
|
||||
-- 1. Retail's ForEachAura/FindAura batch through GetAuraSlots / GetAuraDataBySlot
|
||||
-- (continuation tokens) -- 1.12 has no slot-batching API, so the bodies here
|
||||
-- iterate by index (1, 2, ...) until the accessor returns no aura. Same public
|
||||
-- contract, different internals.
|
||||
-- 2. The non-packed ForEachAura and FindAura source each aura from the positional
|
||||
-- C_UnitAuras.UnitAura (no table), so both are zero-allocation here -- FindAura
|
||||
-- in particular, where the retail version allocates a table per aura scanned.
|
||||
--
|
||||
-- The comparator / dispel-priority / border helpers (DefaultAuraCompare,
|
||||
-- UnitFrameDebuffComparator, ShouldDisplayDebuff, SetAuraBorder*, IsPriorityDebuff)
|
||||
-- are NOT ported: they depend on isPriorityAura / auraInstanceID / the aura
|
||||
-- visualization pipeline / secure-call machinery, none of which exist on 1.12.
|
||||
|
||||
local UnitAura = C_UnitAuras.UnitAura
|
||||
|
||||
AuraUtil = AuraUtil or {};
|
||||
|
||||
-- Returns the AuraData table's fields in the classic `UnitAura` positional order,
|
||||
-- for code written against that shape. Retail appends `unpack(auraData.points)`;
|
||||
-- vanilla auras carry no points, so the tuple ends at timeMod (15 values). Field
|
||||
-- #13 is `isFromPlayerOrPlayerPet` (the table's field) -- note the positional
|
||||
-- C_UnitAuras.UnitAura reports the higher-fidelity `castByPlayer` at #13 instead.
|
||||
function AuraUtil.UnpackAuraData(auraData)
|
||||
if not auraData then
|
||||
return nil;
|
||||
end
|
||||
return auraData.name,
|
||||
auraData.icon,
|
||||
auraData.applications,
|
||||
auraData.dispelName,
|
||||
auraData.duration,
|
||||
auraData.expirationTime,
|
||||
auraData.sourceUnit,
|
||||
auraData.isStealable,
|
||||
auraData.nameplateShowPersonal,
|
||||
auraData.spellId,
|
||||
auraData.canApplyAura,
|
||||
auraData.isBossAura,
|
||||
auraData.isFromPlayerOrPlayerPet,
|
||||
auraData.nameplateShowAll,
|
||||
auraData.timeMod;
|
||||
end
|
||||
|
||||
-- Calls `func` for each aura on `unit` matching `filter`, in index order, until
|
||||
-- `func` returns a truthy value or the auras run out. Non-packed (default) passes
|
||||
-- the 15 positional `UnitAura` values (no table); `usePackedAura` passes the
|
||||
-- AuraData table from GetAuraDataByIndex. `batchSize <= 0` does nothing; any other
|
||||
-- value (or nil) visits every matching aura -- `batchSize` is retail's slot-batch
|
||||
-- hint, not a cap on how many auras are visited.
|
||||
function AuraUtil.ForEachAura(unit, filter, batchSize, func, usePackedAura)
|
||||
if batchSize and batchSize <= 0 then
|
||||
return;
|
||||
end
|
||||
local index = 1;
|
||||
while true do
|
||||
local done;
|
||||
if usePackedAura then
|
||||
local auraData = C_UnitAuras.GetAuraDataByIndex(unit, index, filter);
|
||||
if auraData == nil then
|
||||
return;
|
||||
end
|
||||
done = func(auraData);
|
||||
else
|
||||
local name, icon, count, dispelType, duration, expirationTime, source,
|
||||
isStealable, nameplateShowPersonal, spellId, canApplyAura,
|
||||
isBossDebuff, castByPlayer, nameplateShowAll, timeMod =
|
||||
UnitAura(unit, index, filter);
|
||||
if name == nil then
|
||||
return;
|
||||
end
|
||||
done = func(name, icon, count, dispelType, duration, expirationTime,
|
||||
source, isStealable, nameplateShowPersonal, spellId, canApplyAura,
|
||||
isBossDebuff, castByPlayer, nameplateShowAll, timeMod);
|
||||
end
|
||||
if done then
|
||||
return;
|
||||
end
|
||||
index = index + 1;
|
||||
end
|
||||
end
|
||||
|
||||
-- Recursive worker: `...` is the current aura's positional values (nil when the
|
||||
-- index is past the last aura). Tail-recurses to the next index. Lua 5.0 has
|
||||
-- proper tail calls, so depth is constant regardless of aura count.
|
||||
local function FindAuraRecurse(predicate, unit, filter, index, arg1, arg2, arg3, ...)
|
||||
if ... == nil then
|
||||
return nil; -- past the last aura -> not found
|
||||
end
|
||||
if predicate(arg1, arg2, arg3, ...) then
|
||||
return ...;
|
||||
end
|
||||
index = index + 1;
|
||||
return FindAuraRecurse(predicate, unit, filter, index, arg1, arg2, arg3,
|
||||
UnitAura(unit, index, filter));
|
||||
end
|
||||
|
||||
-- Returns the positional values of the first aura for which `predicate` returns
|
||||
-- truthy, or nil. `predicate` receives up to three caller args followed by the
|
||||
-- aura's positional values: predicate(arg1, arg2, arg3, name, icon, ...). Sources
|
||||
-- auras from the positional accessor, so no table is allocated while scanning.
|
||||
function AuraUtil.FindAura(predicate, unit, filter, arg1, arg2, arg3)
|
||||
return FindAuraRecurse(predicate, unit, filter, 1, arg1, arg2, arg3,
|
||||
UnitAura(unit, 1, filter));
|
||||
end
|
||||
|
||||
-- Returns the positional values of the first aura whose name matches `auraName`,
|
||||
-- or nil. Names are localized and not unique -- this finds the first match.
|
||||
function AuraUtil.FindAuraByName(auraName, unit, filter)
|
||||
return AuraUtil.UnpackAuraData(
|
||||
C_UnitAuras.GetAuraDataBySpellName(unit, auraName, filter));
|
||||
end
|
||||
@@ -1,98 +0,0 @@
|
||||
ColorMixin = ColorMixin or {}
|
||||
|
||||
function CreateColor(r, g, b, a)
|
||||
local color = CreateFromMixins(ColorMixin)
|
||||
color:OnLoad(r, g, b, a)
|
||||
return color
|
||||
end
|
||||
|
||||
function ColorMixin:OnLoad(r, g, b, a)
|
||||
self:SetRGBA(r, g, b, a)
|
||||
end
|
||||
|
||||
function ColorMixin:IsRGBEqualTo(otherColor)
|
||||
return self.r == otherColor.r
|
||||
and self.g == otherColor.g
|
||||
and self.b == otherColor.b
|
||||
end
|
||||
|
||||
function ColorMixin:IsEqualTo(otherColor)
|
||||
return self:IsRGBEqualTo(otherColor) and self.a == otherColor.a
|
||||
end
|
||||
|
||||
function ColorMixin:GetRGB()
|
||||
return self.r, self.g, self.b
|
||||
end
|
||||
|
||||
function ColorMixin:GetHSL()
|
||||
local r, g, b, a = self.r, self.g, self.b, self.a
|
||||
local h, s, l = C_ColorUtil.ConvertHSVToHSL(C_ColorUtil.ConvertRGBToHSV(r, g, b))
|
||||
return h, s, l, a or 1
|
||||
end
|
||||
|
||||
function ColorMixin:GetRGBAsBytes()
|
||||
return Round(self.r * 255), Round(self.g * 255), Round(self.b * 255)
|
||||
end
|
||||
|
||||
function ColorMixin:GetRGBA()
|
||||
return self.r, self.g, self.b, self.a
|
||||
end
|
||||
|
||||
function ColorMixin:GetRGBAAsBytes()
|
||||
return Round(self.r * 255), Round(self.g * 255), Round(self.b * 255), Round((self.a or 1) * 255)
|
||||
end
|
||||
|
||||
function ColorMixin:SetRGBA(r, g, b, a)
|
||||
self.r = r
|
||||
self.g = g
|
||||
self.b = b
|
||||
self.a = a or 1
|
||||
end
|
||||
|
||||
function ColorMixin:SetRGB(r, g, b)
|
||||
self:SetRGBA(r, g, b, nil)
|
||||
end
|
||||
|
||||
function ColorMixin:GenerateHexColor()
|
||||
return C_ColorUtil.GenerateTextColorCode(self)
|
||||
end
|
||||
|
||||
function ColorMixin:GenerateHexColorNoAlpha()
|
||||
return string.format("%.2X%.2X%.2X", self:GetRGBAsBytes())
|
||||
end
|
||||
|
||||
function ColorMixin:GenerateHexColorMarkup()
|
||||
return "|c"..self:GenerateHexColor()
|
||||
end
|
||||
|
||||
function ColorMixin:WrapTextInColorCode(text)
|
||||
return C_ColorUtil.WrapTextInColor(text, self)
|
||||
end
|
||||
|
||||
function WrapTextInColorCode(text, colorHexString)
|
||||
return C_ColorUtil.WrapTextInColorCode(text, colorHexString)
|
||||
end
|
||||
|
||||
function WrapTextInColor(text, color)
|
||||
return C_ColorUtil.WrapTextInColor(text, color)
|
||||
end
|
||||
|
||||
function ColorMixin:WrapTextInColorTableCode(text)
|
||||
return WrapTextInColorCode(text, self:GenerateHexColor())
|
||||
end
|
||||
|
||||
for _, v in pairs(ITEM_QUALITY_COLORS) do
|
||||
v.color = CreateColor(v.r, v.g, v.b, 1)
|
||||
end
|
||||
|
||||
function CAPI_ApplyStandardColorGlobals()
|
||||
local envTbl = _G
|
||||
for _, dbColor in ipairs(C_UIColor.GetColors()) do
|
||||
if not envTbl[dbColor.baseTag] or not envTbl[dbColor.baseTag].GetRGB then
|
||||
local color = CreateColor(dbColor.color.r, dbColor.color.g, dbColor.color.b, dbColor.color.a)
|
||||
envTbl[dbColor.baseTag] = color
|
||||
envTbl[dbColor.baseTag.."_CODE"] = color:GenerateHexColorMarkup()
|
||||
end
|
||||
end
|
||||
end
|
||||
CAPI_ApplyStandardColorGlobals()
|
||||
@@ -1,140 +0,0 @@
|
||||
|
||||
|
||||
-- Item location bitflags (Using tonumber because lua 5.0 doesn't support hexadecimal literals. 0x)
|
||||
ITEM_INVENTORY_LOCATION_PLAYER = tonumber("0x00100000", 16);
|
||||
ITEM_INVENTORY_LOCATION_BAGS = tonumber("0x00200000", 16);
|
||||
ITEM_INVENTORY_LOCATION_BANK = tonumber("0x00400000", 16);
|
||||
ITEM_INVENTORY_BAG_BIT_OFFSET = 8; -- Number of bits that the bag index in GetInventoryItemsForSlot gets shifted to the left.
|
||||
ITEM_INVENTORY_BANK_BAG_OFFSET = 4;
|
||||
|
||||
-- Inventory slots
|
||||
INVSLOT_AMMO = 0;
|
||||
INVSLOT_HEAD = 1; INVSLOT_FIRST_EQUIPPED = INVSLOT_HEAD;
|
||||
INVSLOT_NECK = 2;
|
||||
INVSLOT_SHOULDER = 3;
|
||||
INVSLOT_BODY = 4;
|
||||
INVSLOT_CHEST = 5;
|
||||
INVSLOT_WAIST = 6;
|
||||
INVSLOT_LEGS = 7;
|
||||
INVSLOT_FEET = 8;
|
||||
INVSLOT_WRIST = 9;
|
||||
INVSLOT_HAND = 10;
|
||||
INVSLOT_FINGER1 = 11;
|
||||
INVSLOT_FINGER2 = 12;
|
||||
INVSLOT_TRINKET1 = 13;
|
||||
INVSLOT_TRINKET2 = 14;
|
||||
INVSLOT_BACK = 15;
|
||||
INVSLOT_MAINHAND = 16;
|
||||
INVSLOT_OFFHAND = 17;
|
||||
INVSLOT_RANGED = 18;
|
||||
INVSLOT_TABARD = 19; INVSLOT_LAST_EQUIPPED = INVSLOT_TABARD;
|
||||
|
||||
INVSLOTS_EQUIPABLE_IN_COMBAT = {
|
||||
[INVSLOT_MAINHAND] = true,
|
||||
[INVSLOT_OFFHAND] = true,
|
||||
[INVSLOT_RANGED] = true,
|
||||
}
|
||||
|
||||
-- Item IDs
|
||||
HEARTHSTONE_ITEM_ID = 6948;
|
||||
|
||||
--
|
||||
-- Equipment Set
|
||||
--
|
||||
MAX_EQUIPMENT_SETS_PER_PLAYER = 10;
|
||||
EQUIPMENT_SET_EMPTY_SLOT = 0;
|
||||
EQUIPMENT_SET_IGNORED_SLOT = 1;
|
||||
EQUIPMENT_SET_ITEM_MISSING = -1;
|
||||
|
||||
--
|
||||
-- Totems
|
||||
--
|
||||
|
||||
MAX_TOTEMS = 4;
|
||||
|
||||
FIRE_TOTEM_SLOT = 1;
|
||||
EARTH_TOTEM_SLOT = 2;
|
||||
WATER_TOTEM_SLOT = 3;
|
||||
AIR_TOTEM_SLOT = 4;
|
||||
|
||||
STANDARD_TOTEM_PRIORITIES = {1, 2, 3, 4};
|
||||
|
||||
SHAMAN_TOTEM_PRIORITIES = {
|
||||
EARTH_TOTEM_SLOT,
|
||||
FIRE_TOTEM_SLOT,
|
||||
WATER_TOTEM_SLOT,
|
||||
AIR_TOTEM_SLOT,
|
||||
};
|
||||
|
||||
-- Druid Forms.
|
||||
CAT_FORM = 1;
|
||||
BEAR_FORM = 5;
|
||||
|
||||
--
|
||||
-- Class
|
||||
--
|
||||
CLASS_SORT_ORDER = {
|
||||
"WARRIOR",
|
||||
"PALADIN",
|
||||
"PRIEST",
|
||||
"SHAMAN",
|
||||
"DRUID",
|
||||
"ROGUE",
|
||||
"MAGE",
|
||||
"WARLOCK",
|
||||
"HUNTER",
|
||||
};
|
||||
MAX_CLASSES = table.getn(CLASS_SORT_ORDER);
|
||||
|
||||
LOCALIZED_CLASS_NAMES_MALE = {};
|
||||
LOCALIZED_CLASS_NAMES_FEMALE = {};
|
||||
FillLocalizedClassList(LOCALIZED_CLASS_NAMES_MALE, false);
|
||||
FillLocalizedClassList(LOCALIZED_CLASS_NAMES_FEMALE, true);
|
||||
|
||||
--
|
||||
-- Inventory
|
||||
--
|
||||
BAG_ITEM_QUALITY_COLORS =
|
||||
{
|
||||
[LE_ITEM_QUALITY_COMMON] = COMMON_GRAY_COLOR,
|
||||
[LE_ITEM_QUALITY_UNCOMMON] = UNCOMMON_GREEN_COLOR,
|
||||
[LE_ITEM_QUALITY_RARE] = RARE_BLUE_COLOR,
|
||||
[LE_ITEM_QUALITY_EPIC] = EPIC_PURPLE_COLOR,
|
||||
[LE_ITEM_QUALITY_LEGENDARY] = LEGENDARY_ORANGE_COLOR,
|
||||
[LE_ITEM_QUALITY_ARTIFACT] = ARTIFACT_GOLD_COLOR,
|
||||
};
|
||||
|
||||
-- faction
|
||||
PLAYER_FACTION_GROUP = { [0] = "Horde", [1] = "Alliance", Horde = 0, Alliance = 1 };
|
||||
|
||||
FACTION_LABELS = {
|
||||
[0] = FACTION_HORDE,
|
||||
[1] = FACTION_ALLIANCE,
|
||||
};
|
||||
|
||||
FACTION_LABELS_FROM_STRING = {
|
||||
["Horde"] = FACTION_HORDE,
|
||||
["Alliance"] = FACTION_ALLIANCE,
|
||||
}
|
||||
|
||||
PLAYER_FACTION_COLORS =
|
||||
{
|
||||
[0] = PLAYER_FACTION_COLOR_HORDE,
|
||||
[1] = PLAYER_FACTION_COLOR_ALLIANCE,
|
||||
};
|
||||
|
||||
--
|
||||
-- Glyph
|
||||
--
|
||||
INSCRIPTION_AVAILABLE = false;
|
||||
|
||||
-- PaperDollFrame.lua
|
||||
EQUIPPED_FIRST = 1;
|
||||
EQUIPPED_LAST = 19;
|
||||
|
||||
BASE_MOVEMENT_SPEED = 7;
|
||||
|
||||
UPPER_LEFT_VERTEX = 1;
|
||||
LOWER_LEFT_VERTEX = 2;
|
||||
UPPER_RIGHT_VERTEX = 3;
|
||||
LOWER_RIGHT_VERTEX = 4;
|
||||
@@ -1,64 +0,0 @@
|
||||
-- Backport of FrameXML/FrameUtil.lua (core subset) to vanilla 1.12.
|
||||
--
|
||||
-- Only the dependency-light, commonly-called helpers are ported. Omitted:
|
||||
-- - the fade/flash functions (UIFrameFade*/UIFrameFlash*) are already
|
||||
-- native in 1.12.
|
||||
-- - RegisterFrameForUnitEvents (no RegisterUnitEvent in 1.12), and the
|
||||
-- scale/layout/secure/EventRegistry helpers (rely on APIs absent here).
|
||||
|
||||
FrameUtil = FrameUtil or {};
|
||||
|
||||
function FrameUtil.RegisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.UnregisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:UnregisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.GetRootParent(frame)
|
||||
local parent = frame:GetParent();
|
||||
while parent do
|
||||
local nextParent = parent:GetParent();
|
||||
if not nextParent then
|
||||
break;
|
||||
end
|
||||
parent = nextParent;
|
||||
end
|
||||
return parent;
|
||||
end
|
||||
|
||||
-- Reparents while preserving the frame's render layering.
|
||||
function FrameUtil.SetParentMaintainRenderLayering(frame, parent)
|
||||
local origStrata = frame:GetFrameStrata();
|
||||
local origFrameLevel = frame:GetFrameLevel();
|
||||
frame:SetParent(parent);
|
||||
frame:SetFrameStrata(origStrata);
|
||||
frame:SetFrameLevel(origFrameLevel);
|
||||
end
|
||||
|
||||
function DoesAncestryInclude(ancestry, frame)
|
||||
if ancestry then
|
||||
local currentFrame = frame;
|
||||
while currentFrame do
|
||||
if currentFrame == ancestry then
|
||||
return true;
|
||||
end
|
||||
currentFrame = currentFrame:GetParent();
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function DoesAncestryIncludeAny(ancestry, frames)
|
||||
for _, frame in ipairs(frames) do
|
||||
if DoesAncestryInclude(ancestry, frame) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
@@ -1,101 +0,0 @@
|
||||
-- Backport of 3.3.5 FrameXML/GlobalCallbackRegistry.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - 3.3.5 ref-counts WoW frame events via a SetAttribute/OnAttributeChanged
|
||||
-- dance because attribute mutations are taint-safe in the secure-template
|
||||
-- model. 1.12 has neither secure templates nor SetAttribute on frames,
|
||||
-- so we keep an ordinary {[event]=count} table and Register/Unregister
|
||||
-- on 0<->1 transitions.
|
||||
|
||||
EventRegistry = CreateFromMixins(CallbackRegistryMixin)
|
||||
|
||||
function EventRegistry:OnLoad()
|
||||
CallbackRegistryMixin.OnLoad(self)
|
||||
self:SetUndefinedEventsAllowed(true)
|
||||
|
||||
self.eventCounts = {}
|
||||
self.frameEventFrame = CreateFrame("Frame")
|
||||
self.frameEventFrame.registry = self
|
||||
self.frameEventFrame:SetScript("OnEvent", function()
|
||||
-- 1.12 globals: `event` is the event name; `arg1..argN` are payload.
|
||||
-- We don't repack them here; CallbackRegistryMixin:TriggerEvent uses
|
||||
-- Lua-5.0 varargs, so we forward through a small wrapper.
|
||||
self:DispatchGameEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
|
||||
end)
|
||||
|
||||
-- Stop delivering events once logout begins: a callback firing during the
|
||||
-- engine's teardown can touch state that's already gone. A dedicated frame
|
||||
-- owns PLAYER_LOGOUT so this fires regardless of what consumers registered
|
||||
-- — folding the check into frameEventFrame's OnEvent would depend on the
|
||||
-- ref-counted event set still holding PLAYER_LOGOUT at logout time.
|
||||
self.logoutFrame = CreateFrame("Frame")
|
||||
self.logoutFrame:RegisterEvent("PLAYER_LOGOUT")
|
||||
self.logoutFrame:SetScript("OnEvent", function()
|
||||
self.frameEventFrame:UnregisterAllEvents()
|
||||
self.frameEventFrame:SetScript("OnEvent", nil)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Helper so we can forward up to nine event args (the most any vanilla
|
||||
-- event currently emits) into TriggerEvent's vararg machinery.
|
||||
function EventRegistry:DispatchGameEvent(event, ...)
|
||||
self:TriggerEvent(event, unpack(arg))
|
||||
end
|
||||
|
||||
function EventRegistry:RegisterFrameEvent(frameEvent)
|
||||
local n = (self.eventCounts[frameEvent] or 0) + 1
|
||||
self.eventCounts[frameEvent] = n
|
||||
if n == 1 then
|
||||
self.frameEventFrame:RegisterEvent(frameEvent)
|
||||
end
|
||||
end
|
||||
|
||||
function EventRegistry:UnregisterFrameEvent(frameEvent)
|
||||
local n = self.eventCounts[frameEvent] or 0
|
||||
if n > 0 then
|
||||
n = n - 1
|
||||
self.eventCounts[frameEvent] = n
|
||||
if n == 0 then
|
||||
self.frameEventFrame:UnregisterEvent(frameEvent)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function EventRegistry:RegisterFrameEventAndCallback(frameEvent, ...)
|
||||
self:RegisterFrameEvent(frameEvent)
|
||||
return self:RegisterCallback(frameEvent, unpack(arg))
|
||||
end
|
||||
|
||||
local function CreateCallbackHandle(cbr, cbrHandle, frameEvent)
|
||||
local handle = {
|
||||
Unregister = function()
|
||||
cbr:UnregisterFrameEvent(frameEvent)
|
||||
cbrHandle:Unregister()
|
||||
end,
|
||||
}
|
||||
return handle
|
||||
end
|
||||
|
||||
function EventRegistry:RegisterFrameEventAndCallbackWithHandle(frameEvent, ...)
|
||||
self:RegisterFrameEvent(frameEvent)
|
||||
local cbrHandle = self:RegisterCallbackWithHandle(frameEvent, unpack(arg))
|
||||
return CreateCallbackHandle(self, cbrHandle, frameEvent)
|
||||
end
|
||||
|
||||
function EventRegistry:UnregisterFrameEventAndCallback(frameEvent, ...)
|
||||
self:UnregisterFrameEvent(frameEvent)
|
||||
self:UnregisterCallback(frameEvent, unpack(arg))
|
||||
end
|
||||
|
||||
function EventRegistry:GetEventCounts(...)
|
||||
local counts = {}
|
||||
for i = 1, arg.n do
|
||||
local frameEvent = arg[i]
|
||||
local count = self.eventCounts[frameEvent] or "?"
|
||||
table.insert(counts, frameEvent .. " : " .. tostring(count))
|
||||
end
|
||||
|
||||
return table.concat(counts, "\n")
|
||||
end
|
||||
|
||||
EventRegistry:OnLoad()
|
||||
@@ -1,222 +0,0 @@
|
||||
-- SecureStateDriver: automatic frame state / visibility from macro conditionals.
|
||||
--
|
||||
-- Backports FrameXML's SecureStateDriverManager. A registered driver re-runs
|
||||
-- SecureCmdOptionParse (Util/MacroOptions.lua) on a 0.2s poll (plus an instant
|
||||
-- rescan on the events below) and applies the result:
|
||||
-- * RegisterStateDriver(frame, "visibility", "[combat] hide; show") -> the
|
||||
-- value "show"/"hide" drives frame:Show()/Hide();
|
||||
-- * RegisterStateDriver / RegisterAttributeDriver for any other state ->
|
||||
-- frame:SetAttribute("state-"..state, value), so an OnAttributeChanged
|
||||
-- handler reacts to it;
|
||||
-- * RegisterUnitWatch(frame [, asState]) -> shows/hides (or sets
|
||||
-- "state-unitexists") as the frame's `unit` attribute comes and goes.
|
||||
--
|
||||
-- Base: 3.3.5 FrameXML/SecureStateDriver.lua, with the anniversary refinements
|
||||
-- grafted in (the resolveDriver() consolidation, tonumber() coercion of driver
|
||||
-- values, the literal string 'nil' -> nil, and the "statehidden" attribute).
|
||||
--
|
||||
-- Three deliberate deviations from upstream, all because 1.12 has no taint and
|
||||
-- our attribute system fires OnAttributeChanged SYNCHRONOUSLY:
|
||||
-- 1. No attribute-bootstrap manager. Upstream drives itself through
|
||||
-- SecureStateDriverManager:SetAttribute("setframe"/"setstate"/...) -- a
|
||||
-- taint-boundary trick. Our SetAttribute has a recursion guard (a nested
|
||||
-- SetAttribute stores the value but does NOT fire the handler, see
|
||||
-- src/frame/Attributes.cpp), so doing the registration-time resolve from
|
||||
-- inside that handler would silently swallow the target frame's own
|
||||
-- OnAttributeChanged. Instead the public Register* functions write plain
|
||||
-- local tables and resolve immediately at top level. Public signatures are
|
||||
-- unchanged from retail.
|
||||
-- 2. GetTime()-deadline throttle instead of the OnUpdate `elapsed` arg (1.12
|
||||
-- handlers take no parameters).
|
||||
-- 3. UpdateUnitWatch guards its "statehidden"/"state-unitexists" writes on a
|
||||
-- real change, so a watched frame's handler is not fired at 5 Hz.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- SecureButton_* (minimal -- no taint, no modifier/button suffix machinery)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- SecureButton_GetAttribute and SecureButton_GetUnit are defined in
|
||||
-- SecureTemplates.lua (loaded earlier via SecureTemplates.xml).
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Private state
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local secureAttributeDrivers = {}; -- [frame][attribute] = optionsString
|
||||
local unitExistsWatchers = {}; -- [frame] = doState (boolean)
|
||||
local unitExistsCache = setmetatable({}, {
|
||||
__index = function(t, k)
|
||||
local v = (UnitExists(k) and true) or false;
|
||||
t[k] = v;
|
||||
return v;
|
||||
end,
|
||||
});
|
||||
|
||||
local STATE_DRIVER_UPDATE_THROTTLE = 0.2;
|
||||
local nextUpdate = 0;
|
||||
|
||||
local pairs = pairs;
|
||||
|
||||
function UnitWatchRegistered(frame)
|
||||
return unitExistsWatchers[frame] ~= nil;
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Resolvers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function UpdateUnitWatch(frame, doState)
|
||||
local unit = SecureButton_GetUnit(frame);
|
||||
local exists = (unit and unitExistsCache[unit]) or false;
|
||||
if doState then
|
||||
if frame:GetAttribute("state-unitexists") ~= exists then
|
||||
frame:SetAttribute("state-unitexists", exists);
|
||||
end
|
||||
else
|
||||
if exists then
|
||||
frame:Show();
|
||||
else
|
||||
frame:Hide();
|
||||
end
|
||||
local hidden = (not exists) or nil;
|
||||
if frame:GetAttribute("statehidden") ~= hidden then
|
||||
frame:SetAttribute("statehidden", hidden);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function resolveDriver(frame, attribute, values)
|
||||
local newValue = SecureCmdOptionParse(values);
|
||||
|
||||
if attribute == "state-visibility" then
|
||||
if newValue == "show" then
|
||||
frame:Show();
|
||||
if frame:GetAttribute("statehidden") ~= nil then
|
||||
frame:SetAttribute("statehidden", nil);
|
||||
end
|
||||
elseif newValue == "hide" then
|
||||
frame:Hide();
|
||||
if frame:GetAttribute("statehidden") ~= true then
|
||||
frame:SetAttribute("statehidden", true);
|
||||
end
|
||||
end
|
||||
elseif newValue then
|
||||
if newValue == "nil" then
|
||||
newValue = nil;
|
||||
else
|
||||
newValue = tonumber(newValue) or newValue;
|
||||
end
|
||||
if frame:GetAttribute(attribute) ~= newValue then
|
||||
frame:SetAttribute(attribute, newValue);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Manager frame
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
SecureStateDriverManager = CreateFrame("Frame", "SecureStateDriverManager");
|
||||
SecureStateDriverManager:Hide(); -- hidden -> no OnUpdate until a driver exists
|
||||
|
||||
SecureStateDriverManager:SetScript("OnUpdate", function()
|
||||
local now = GetTime();
|
||||
if now < nextUpdate then
|
||||
return;
|
||||
end
|
||||
nextUpdate = now + STATE_DRIVER_UPDATE_THROTTLE;
|
||||
|
||||
for frame, drivers in pairs(secureAttributeDrivers) do
|
||||
for attribute, values in pairs(drivers) do
|
||||
resolveDriver(frame, attribute, values);
|
||||
end
|
||||
end
|
||||
|
||||
for k in pairs(unitExistsCache) do
|
||||
unitExistsCache[k] = nil;
|
||||
end
|
||||
for frame, doState in pairs(unitExistsWatchers) do
|
||||
UpdateUnitWatch(frame, doState);
|
||||
end
|
||||
end);
|
||||
|
||||
SecureStateDriverManager:SetScript("OnEvent", function()
|
||||
nextUpdate = 0; -- force a rescan on the next frame
|
||||
end);
|
||||
|
||||
SecureStateDriverManager:SetScript("OnAttributeChanged", function()
|
||||
if arg1 == "updatetime" and tonumber(arg2) then
|
||||
STATE_DRIVER_UPDATE_THROTTLE = tonumber(arg2);
|
||||
end
|
||||
end);
|
||||
|
||||
-- Events that warrant an early rescan. Each is guarded so a name this client
|
||||
-- doesn't define (e.g. UPDATE_STEALTH) is simply skipped.
|
||||
local rescanEvents = {
|
||||
"MODIFIER_STATE_CHANGED", -- ClassicAPI-fired
|
||||
"PLAYER_FOCUS_CHANGED", -- ClassicAPI-fired
|
||||
"UPDATE_SHAPESHIFT_FORM", -- ClassicAPI-fired (singular)
|
||||
"UPDATE_SHAPESHIFT_FORMS", -- native 1.12 (plural)
|
||||
"ACTIONBAR_PAGE_CHANGED",
|
||||
"UPDATE_BONUS_ACTIONBAR",
|
||||
"PLAYER_ENTERING_WORLD",
|
||||
"UPDATE_STEALTH",
|
||||
"PLAYER_TARGET_CHANGED",
|
||||
"PLAYER_REGEN_DISABLED",
|
||||
"PLAYER_REGEN_ENABLED",
|
||||
"UNIT_PET",
|
||||
"RAID_ROSTER_UPDATE",
|
||||
"PARTY_MEMBERS_CHANGED",
|
||||
};
|
||||
for _, event in ipairs(rescanEvents) do
|
||||
if C_EventUtils.IsEventValid(event) then
|
||||
SecureStateDriverManager:RegisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Public API
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function RegisterAttributeDriver(frame, attribute, values)
|
||||
if attribute and values and string.sub(attribute, 1, 1) ~= "_" then
|
||||
local drivers = secureAttributeDrivers[frame];
|
||||
if not drivers then
|
||||
drivers = {};
|
||||
secureAttributeDrivers[frame] = drivers;
|
||||
end
|
||||
drivers[attribute] = values;
|
||||
SecureStateDriverManager:Show();
|
||||
resolveDriver(frame, attribute, values);
|
||||
end
|
||||
end
|
||||
|
||||
function UnregisterAttributeDriver(frame, attribute)
|
||||
if attribute then
|
||||
local drivers = secureAttributeDrivers[frame];
|
||||
if drivers then
|
||||
drivers[attribute] = nil;
|
||||
end
|
||||
else
|
||||
secureAttributeDrivers[frame] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
function RegisterStateDriver(frame, state, values)
|
||||
RegisterAttributeDriver(frame, "state-" .. state, values);
|
||||
end
|
||||
|
||||
function UnregisterStateDriver(frame, state)
|
||||
UnregisterAttributeDriver(frame, state and ("state-" .. state));
|
||||
end
|
||||
|
||||
function RegisterUnitWatch(frame, asState)
|
||||
local doState = (asState and true) or false;
|
||||
unitExistsWatchers[frame] = doState;
|
||||
SecureStateDriverManager:Show();
|
||||
UpdateUnitWatch(frame, doState);
|
||||
end
|
||||
|
||||
function UnregisterUnitWatch(frame)
|
||||
unitExistsWatchers[frame] = nil;
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
|
||||
function RegisterNewSlashCommand(callback, command, commandAlias)
|
||||
local name = string.upper(command);
|
||||
_G["SLASH_"..name.."1"] = "/"..command;
|
||||
_G["SLASH_"..name.."2"] = "/"..commandAlias;
|
||||
SlashCmdList[name] = callback;
|
||||
end
|
||||
|
||||
SlashCmdList["FOCUS"] = function(msg)
|
||||
if ( msg == "" ) then
|
||||
FocusUnit();
|
||||
else
|
||||
local action, target = SecureCmdOptionParse(msg);
|
||||
if ( action ) then
|
||||
if ( not target or target == "focus" ) then
|
||||
target = action;
|
||||
end
|
||||
FocusUnit(target);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["CLEARFOCUS"] = function(msg)
|
||||
if ( SecureCmdOptionParse(msg) ) then
|
||||
ClearFocus();
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["EQUIP_SET"] = function(msg)
|
||||
local set = SecureCmdOptionParse(msg);
|
||||
if ( set and set ~= "" ) then
|
||||
C_EquipmentSet.UseEquipmentSet(C_EquipmentSet.GetEquipmentSetID(set))
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["CLICK"] = function(msg)
|
||||
local action = SecureCmdOptionParse(msg);
|
||||
if ( action and action ~= "" ) then
|
||||
local name, mouseButton = string.match(action, "([^%s]+)%s+([^%s]+)");
|
||||
if ( not name ) then
|
||||
name = action;
|
||||
end
|
||||
local button = GetClickFrame(name);
|
||||
if ( button and button:IsObjectType("Button") ) then
|
||||
button:Click(mouseButton);
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,89 +0,0 @@
|
||||
if GetLocale() ~= "deDE" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Fokus setzen";
|
||||
BINDING_NAME_TARGETFOCUS = "Fokus anvisieren";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Seid Ihr sicher, dass Ihr das Ausrüstungsset \"%s\" löschen wollt?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Möchtet Ihr das Ausrüstungsset \"%s\" speichern?";
|
||||
DEBUFF_SYMBOL_CURSE = "Fl";
|
||||
DEBUFF_SYMBOL_DISEASE = "Kr";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
DEBUFF_SYMBOL_POISON = "Gi";
|
||||
DEBUG_FRAMESTACK = "Frame Stack";
|
||||
EQUIPMENT_MANAGER = "Ausrüstungsmanager";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Diesen Platz ignorieren";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "Der Ausrüstungsmanager ist abgeschaltet.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(%d fehlen)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "In Beuteln platzieren";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Diesen Platz einbeziehen";
|
||||
EQUIPMENT_SETS = "Ausrüstungs-Sets: |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "Ein Ausrüstungsset mit diesem Namen gibt es bereits.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "Ihr könnt keine weiteren Ausrüstungssets erstellen.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Spezialisierung zuweisen:";
|
||||
EQUIPMENT_SET_EDIT = "Name/Icon ändern";
|
||||
EQUIPMENT_SET_SETTINGS = "Einstellungen";
|
||||
EQUIPSET_EQUIP = "Anlegen";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Ausrüstungstausch fehlgeschlagen. Inventar ist voll.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Einige Gegenstände des Sets '%s' können nicht gewechselt werden, da Ihr Euch im Kampf befindet.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Mindestens ein Teil des Sets '%s' fehlt und kann nicht ausgerüstet werden.";
|
||||
CLEAR_FOCUS = "Fokus löschen";
|
||||
SET_FOCUS = "Fokus setzen";
|
||||
FOCUSTARGET = "Ziel als Fokus festlegen";
|
||||
GEARSETS_POPUP_TEXT = "Setnamen eingeben (Max. 16 Zeichen):";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4IGegenstand:Gegenstände;";
|
||||
ITEMS_EQUIPPED = "%d |4Gegenstand:Gegenstände; ausgerüstet";
|
||||
ITEMS_IN_INVENTORY = "%d |4Gegenstand:Gegenstände; im Inventar";
|
||||
ITEM_SLOTS_IGNORED = "%d |4Platz:Plätze; ignoriert";
|
||||
ITEM_MISSING = "%s fehlt";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4Gegenstand fehlt:Gegenstände fehlen;";
|
||||
TARGETFOCUS = "Fokussierte Einheit anvisieren";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "K";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "G";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "Alle Symbole";
|
||||
ICON_FILTER_ITEM = "Gegenstände";
|
||||
ICON_FILTER_SPELL = "Zauber";
|
||||
ICON_SELECTION_TITLE_CURRENT = "Aktuell ausgewählt";
|
||||
ITEM_DELTA_DESCRIPTION = "Wenn Ihr diesen Gegenstand ersetzt, werden sich folgende Werte ändern:";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Gesundheit";
|
||||
ITEM_MOD_AGILITY_SHORT = "Beweglichkeit";
|
||||
ITEM_MOD_STRENGTH_SHORT = "Stärke";
|
||||
ITEM_MOD_INTELLECT_SHORT = "Intelligenz";
|
||||
ITEM_MOD_SPIRIT_SHORT = "Willenskraft";
|
||||
ITEM_MOD_STAMINA_SHORT = "Ausdauer";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Angriffskraft";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Distanzangriffskraft";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Kritischer Trefferwert (Nah)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Kritischer Trefferwert (Distanz)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Trefferwert (Nah)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Trefferwert (Distanz)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Trefferwert (Zauber)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Kritischer Trefferwert (Zauber)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Bonusschaden";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Bonusheilung";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Manaregeneration";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Verteidigung";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Ausweichen";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parieren";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Blocken";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Blocken";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Schaden pro Sekunde";
|
||||
LARGE_NUMBER_SEPERATOR = ".";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Ausrüstungsmanager";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Neues Set";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Notizen für %s eintragen:";
|
||||
SET_NOTE = "Notiz eintragen";
|
||||
SILVER_AMOUNT_SYMBOL = "S";
|
||||
SLASH_CLEARFOCUS1 = "/fokusaufheben";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLICK1 = "/klick";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_EQUIP_SET1 = "/Ausrüstungsset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/fokus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Hier klicken, um einen Link zu Eurem Beruf zu erstellen.";
|
||||
@@ -1,91 +0,0 @@
|
||||
BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Are you sure you want to delete the equipment set %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Would you like to save the equipment set \"%s\"?";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:2:3:0:1|t";
|
||||
DEBUFF_SYMBOL_CURSE = "Cu";
|
||||
DEBUFF_SYMBOL_DISEASE = "Di";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
DEBUFF_SYMBOL_POISON = "Po";
|
||||
DEBUG_FRAMESTACK = "Frame Stack";
|
||||
EQUIPMENT_MANAGER = "Equipment Manager";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Ignore This Slot";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "The Equipment Manager is disabled.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(%d missing)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Place In Bags";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Include This Slot";
|
||||
EQUIPMENT_SETS = "Equipment Sets: |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "An equipment set with that name already exists.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "You cannot create any more new equipment sets.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Assign to Specialization:";
|
||||
EQUIPMENT_SET_EDIT = "Change Name/Icon";
|
||||
EQUIPMENT_SET_SETTINGS = "Settings";
|
||||
EQUIPSET_EQUIP = "Equip";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Equipment swap failed. Inventory is full.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Some items from set %s cannot be swapped because you are in combat.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "One or more items from set %s were missing and could not be equipped.";
|
||||
CLEAR_FOCUS = "Clear Focus";
|
||||
SET_FOCUS = "Set Focus";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
GEARSETS_POPUP_TEXT = "Enter Set Name (Max 16 Characters):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "All Icons";
|
||||
ICON_FILTER_ITEM = "Items";
|
||||
ICON_FILTER_SPELL = "Spells";
|
||||
ICON_SELECTION_TITLE_CURRENT = "Currently Selected";
|
||||
ITEM_DELTA_DESCRIPTION = "If you replace this item, the following stat changes will occur:";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Health";
|
||||
ITEM_MOD_AGILITY_SHORT = "Agility";
|
||||
ITEM_MOD_STRENGTH_SHORT = "Strength";
|
||||
ITEM_MOD_INTELLECT_SHORT = "Intellect";
|
||||
ITEM_MOD_SPIRIT_SHORT = "Spirit";
|
||||
ITEM_MOD_STAMINA_SHORT = "Stamina";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Attack Power";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Ranged Attack Power";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Critical Strike (Melee)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Critical Strike (Ranged)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Hit (Melee)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Hit (Ranged)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Hit (Spell)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Critical Strike (Spell)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Spell Damage";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Spell Healing";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Mana Regeneration";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Defense";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Dodge";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parry";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Block";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Block Value";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Damage Per Second";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Equipment Manager";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "New Set";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Set Notes for %s:";
|
||||
SET_NOTE = "Set Note";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:1:2:0:1|t";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
|
||||
@@ -1,89 +0,0 @@
|
||||
if GetLocale() ~= "esES" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Establecer objetivo";
|
||||
BINDING_NAME_TARGETFOCUS = "Seleccionar enfocado";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "¿Seguro que quieres eliminar el equipamiento %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "¿Quieres guardar el conjunto de equipamiento \"%s\"?";
|
||||
DEBUFF_SYMBOL_CURSE = "Mal";
|
||||
DEBUFF_SYMBOL_DISEASE = "En";
|
||||
DEBUFF_SYMBOL_MAGIC = "Mag";
|
||||
DEBUFF_SYMBOL_POISON = "Ve";
|
||||
DEBUG_FRAMESTACK = "Trama de pila";
|
||||
EQUIPMENT_MANAGER = "Gestor de equipamiento";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Ignorar esta casilla";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "El Gestor de equipamiento está desactivado.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(falta %d)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Colocar en bolsas";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Incluir esta casilla";
|
||||
EQUIPMENT_SETS = "Conjuntos de equipamiento: |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "Ya existe un conjunto de equipamiento con ese nombre.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "Ya no puedes crear más equipamientos.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Asignar a especialización:";
|
||||
EQUIPMENT_SET_EDIT = "Cambiar nombre/icono";
|
||||
EQUIPMENT_SET_SETTINGS = "Config.";
|
||||
EQUIPSET_EQUIP = "Equipar";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "No se ha podido cambiar el equipamiento. Inventario lleno.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Algunos objetos del equipo %s no se cambiarán porque estás en combate.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Falta uno o más objetos del equipo %s y no se ha podido equipar.";
|
||||
CLEAR_FOCUS = "Quitar foco";
|
||||
SET_FOCUS = "Establecer foco";
|
||||
FOCUSTARGET = "Establecer objetivo";
|
||||
GEARSETS_POPUP_TEXT = "Nombra este equipamiento (Máx. 16 carac.):";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objeto:objetos;";
|
||||
ITEMS_EQUIPPED = "Equipado: %d |4objeto:objetos;";
|
||||
ITEMS_IN_INVENTORY = "En inventario: %d |4objeto:objetos;";
|
||||
ITEM_SLOTS_IGNORED = "Ignorado: %d |4casilla:casillas;";
|
||||
ITEM_MISSING = "Falta: %s";
|
||||
ITEMS_NOT_IN_INVENTORY = "Faltan: %d |4objeto:objetos;";
|
||||
TARGETFOCUS = "Fijar unidad objetivo";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "o";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "Todos los iconos";
|
||||
ICON_FILTER_ITEM = "Objetos";
|
||||
ICON_FILTER_SPELL = "Hechizos";
|
||||
ICON_SELECTION_TITLE_CURRENT = "Selección actual";
|
||||
ITEM_DELTA_DESCRIPTION = "Si reemplazas este objeto, se producirán los siguientes cambios en las estadísticas:";
|
||||
ITEM_MOD_MANA_SHORT = "Maná";
|
||||
ITEM_MOD_HEALTH_SHORT = "Salud";
|
||||
ITEM_MOD_AGILITY_SHORT = "agilidad";
|
||||
ITEM_MOD_STRENGTH_SHORT = "fuerza";
|
||||
ITEM_MOD_INTELLECT_SHORT = "intelecto";
|
||||
ITEM_MOD_SPIRIT_SHORT = "espíritu";
|
||||
ITEM_MOD_STAMINA_SHORT = "aguante";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Poder de ataque";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Poder de ataque a distancia";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Golpe crítico (cuerpo a cuerpo)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Golpe crítico (a distancia)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Golpe (cuerpo a cuerpo)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Golpe (a distancia)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Golpe (hechizo)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Golpe crítico (hechizo)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Bon. daño";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Bon. sanac.";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Regeneración de maná";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Defensa";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Esquiva";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parada";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Bloqueo";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Valor de bloqueo";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "daño por segundo";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Gestor de equipamiento";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Nuevo conjunto";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Establecer nota para %s:";
|
||||
SET_NOTE = "Establecer nota";
|
||||
SILVER_AMOUNT_SYMBOL = "p";
|
||||
SLASH_CLEARFOCUS1 = "/quitarenfoque";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLICK1 = "/clic";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_EQUIP_SET1 = "/conjequip";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/enfoque";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Haz clic aquí para crear un vínculo a tu profesión.";
|
||||
@@ -1,89 +0,0 @@
|
||||
if GetLocale() ~= "frFR" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Définir cible";
|
||||
BINDING_NAME_TARGETFOCUS = "Reprendre cible";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Voulez-vous vraiment supprimer l'ensemble d'équipement %s ?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Voulez-vous sauvegarder l'ensemble d'équipement %s ?";
|
||||
DEBUFF_SYMBOL_CURSE = "Mn";
|
||||
DEBUFF_SYMBOL_DISEASE = "Md";
|
||||
DEBUFF_SYMBOL_MAGIC = "Mg";
|
||||
DEBUFF_SYMBOL_POISON = "Po";
|
||||
DEBUG_FRAMESTACK = "Empilement des cadres";
|
||||
EQUIPMENT_MANAGER = "Gestionnaire d'équipement";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Ignorer cet emplacement";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "Le Gestionnaire d'équipement est désactivé.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(objet manquant : %d)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Mettre en sac";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Inclure cet emplacement";
|
||||
EQUIPMENT_SETS = "Ensembles d'équipement : |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "Un ensemble d'équipement portant ce nom existe déjà.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "Vous ne pouvez plus créer de nouveaux ensembles d'équipement.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Assigner à la spécialisation :";
|
||||
EQUIPMENT_SET_EDIT = "Modifier le nom/l'icône";
|
||||
EQUIPMENT_SET_SETTINGS = "Réglages";
|
||||
EQUIPSET_EQUIP = "Equiper";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Échec de l'échange d'équipement. L'inventaire est plein.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Certains objets de l'ensemble %s ne peuvent pas être échangés car vous êtes en combat.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Une ou plusieurs pièces de l'ensemble %s manquaient et n'ont pu être équipées.";
|
||||
CLEAR_FOCUS = "Effacer la focalisation";
|
||||
SET_FOCUS = "Définir la focalisation";
|
||||
FOCUSTARGET = "Définir la cible de focalisation";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objet:objets;";
|
||||
ITEMS_EQUIPPED = "%d |4objet:objets; équipés";
|
||||
ITEMS_IN_INVENTORY = "%d |4objet:objets; dans l'inventaire";
|
||||
ITEM_SLOTS_IGNORED = "%d |4emplacement ignoré:emplacements ignorés;";
|
||||
ITEM_MISSING = "Objet manquant : %s";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4objet:objets; manquants";
|
||||
TARGETFOCUS = "Cibler l'unité focalisée";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
GEARSETS_POPUP_TEXT = "Nom de l'ensemble (16 caractères max.) :";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "o";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "Toutes les icônes";
|
||||
ICON_FILTER_ITEM = "Objets";
|
||||
ICON_FILTER_SPELL = "Sorts";
|
||||
ICON_SELECTION_TITLE_CURRENT = "Sélection actuelle";
|
||||
ITEM_DELTA_DESCRIPTION = "Si vous remplacez cet objet, les caractéristiques suivantes seront modifiées :";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Vie";
|
||||
ITEM_MOD_AGILITY_SHORT = "Agilité";
|
||||
ITEM_MOD_STRENGTH_SHORT = "Force";
|
||||
ITEM_MOD_INTELLECT_SHORT = "Intelligence";
|
||||
ITEM_MOD_SPIRIT_SHORT = "Esprit";
|
||||
ITEM_MOD_STAMINA_SHORT = "Endurance";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Puissance d'attaque";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Puissance d'attaque à distance";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Score de critique (mêlée)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Score de critique (distance)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Toucher (mêlée)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Toucher (distance)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Toucher (sorts)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Score de critique (sorts)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Bonus aux dégâts";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Bonus aux soins";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Régénération du mana";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Défense";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Esquive";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parade";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Blocage";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Valeur de blocage";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Dégâts par seconde";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Gestionnaire d'équipement";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "Nouvel ensemble";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Écrire une note pour %s :";
|
||||
SET_NOTE = "Écrire une note";
|
||||
SILVER_AMOUNT_SYMBOL = "a";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/annulerfocal";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/cliquer";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/équipens";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focal";
|
||||
LINK_TRADESKILL_TOOLTIP = "Cliquez ici pour créer un lien vers votre profession.";
|
||||
@@ -1,89 +0,0 @@
|
||||
if GetLocale() ~= "zhCN" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "焦点目标";
|
||||
BINDING_NAME_TARGETFOCUS = "目标焦点";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你确认要删除装备方案%s吗?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "你想要保存装备方案\"%s\"吗?";
|
||||
DEBUFF_SYMBOL_CURSE = "诅";
|
||||
DEBUFF_SYMBOL_DISEASE = "疾";
|
||||
DEBUFF_SYMBOL_MAGIC = "魔";
|
||||
DEBUFF_SYMBOL_POISON = "毒";
|
||||
DEBUG_FRAMESTACK = "框架栈";
|
||||
EQUIPMENT_MANAGER = "装备管理";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "忽略此栏位";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "装备管理已禁用。";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(缺少%d)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "放入背包";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "包括此栏位";
|
||||
EQUIPMENT_SETS = "装备配置方案:|cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "一个重名的装备方案已经存在。";
|
||||
EQUIPMENT_SETS_TOO_MANY = "你不能再创建新的装备方案了。";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "指定专精:";
|
||||
EQUIPMENT_SET_EDIT = "修改名称/图标";
|
||||
EQUIPMENT_SET_SETTINGS = "设置";
|
||||
EQUIPSET_EQUIP = "装备";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "装备更换失败。背包已满。";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "装备方案%s中的某些物品不能在战斗中切换。";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "未找到套装方案%s中的一件或多件物品,无法装备。";
|
||||
CLEAR_FOCUS = "清除焦点";
|
||||
SET_FOCUS = "设置焦点";
|
||||
FOCUSTARGET = "设定焦点目标";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "已装备%d件物品";
|
||||
ITEMS_IN_INVENTORY = "背包中有%d件物品";
|
||||
ITEM_SLOTS_IGNORED = "忽略%d个栏位";
|
||||
ITEM_MISSING = "%s缺失";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
TARGETFOCUS = "选中焦点目标";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "铜";
|
||||
GEARSETS_POPUP_TEXT = "输入方案名称(最多16个字符):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "金";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "所有图标";
|
||||
ICON_FILTER_ITEM = "物品";
|
||||
ICON_FILTER_SPELL = "法术";
|
||||
ICON_SELECTION_TITLE_CURRENT = "当前已选择";
|
||||
ITEM_DELTA_DESCRIPTION = "如果你替换该物品,将会产生以下的属性变更:";
|
||||
ITEM_MOD_MANA_SHORT = "法力值";
|
||||
ITEM_MOD_HEALTH_SHORT = "生命值";
|
||||
ITEM_MOD_AGILITY_SHORT = "敏捷";
|
||||
ITEM_MOD_STRENGTH_SHORT = "力量";
|
||||
ITEM_MOD_INTELLECT_SHORT = "智力";
|
||||
ITEM_MOD_SPIRIT_SHORT = "精神";
|
||||
ITEM_MOD_STAMINA_SHORT = "耐力";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "攻击强度";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "远程攻击强度";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "爆击(近战)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "爆击(远程)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "命中(近战)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "命中(远程)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "命中(法术)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "爆击(法术)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "伤害加成";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "治疗加成";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "法力回复";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "防御";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "躲闪";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "招架";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "格挡";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "格挡值";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒伤害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "装备管理";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "新的方案";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "为%s设置备注:";
|
||||
SET_NOTE = "设置备注";
|
||||
SILVER_AMOUNT_SYMBOL = "银";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "点击这里以创建一个链接到你的专业的快捷方式。";
|
||||
@@ -1,89 +0,0 @@
|
||||
if GetLocale() ~= "zhTW" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "鎖定目標";
|
||||
BINDING_NAME_TARGETFOCUS = "選定鎖定目標";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你確定要刪除裝備設定:%s嗎?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "你想要儲存裝備設定「%s」嗎?";
|
||||
DEBUFF_SYMBOL_CURSE = "咒";
|
||||
DEBUFF_SYMBOL_DISEASE = "疾";
|
||||
DEBUFF_SYMBOL_MAGIC = "魔";
|
||||
DEBUFF_SYMBOL_POISON = "毒";
|
||||
DEBUG_FRAMESTACK = "框架堆疊";
|
||||
EQUIPMENT_MANAGER = "裝備管理員";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "忽略此欄位";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "裝備管理員已關閉。";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d件%s|cffff0000(缺少%d件)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "放進背包";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "包含此欄位";
|
||||
EQUIPMENT_SETS = "裝備設定:|cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "您所選擇的名稱已經有裝備設定使用了。";
|
||||
EQUIPMENT_SETS_TOO_MANY = "你無法製造更多的新裝備設定。";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "指定專精:";
|
||||
EQUIPMENT_SET_EDIT = "改變名稱/圖示";
|
||||
EQUIPMENT_SET_SETTINGS = "設定";
|
||||
EQUIPSET_EQUIP = "裝備";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "裝備切換失敗。物品欄已滿。";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "%s設定中的某些物品必須等到你脫離戰鬥後才能更換。";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "%s設定中的一個或多個物品未尋獲,因此無法裝備。";
|
||||
CLEAR_FOCUS = "清除專注目標";
|
||||
SET_FOCUS = "設定專注目標";
|
||||
FOCUSTARGET = "設定鎖定目標";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "裝備%d件物品";
|
||||
ITEMS_IN_INVENTORY = "%d件物品在物品欄中";
|
||||
ITEM_SLOTS_IGNORED = "忽略%d個欄位";
|
||||
ITEM_MISSING = "缺少%s物品";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
TARGETFOCUS = "選定鎖定單位";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "銅";
|
||||
GEARSETS_POPUP_TEXT = "輸入設定名稱(最多16個字元):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "金";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "所有圖示";
|
||||
ICON_FILTER_ITEM = "物品";
|
||||
ICON_FILTER_SPELL = "法術";
|
||||
ICON_SELECTION_TITLE_CURRENT = "目前選擇";
|
||||
ITEM_DELTA_DESCRIPTION = "若你更換此裝備,下列屬性將會改變:";
|
||||
ITEM_MOD_MANA_SHORT = "法力";
|
||||
ITEM_MOD_HEALTH_SHORT = "生命力";
|
||||
ITEM_MOD_AGILITY_SHORT = "敏捷";
|
||||
ITEM_MOD_STRENGTH_SHORT = "力量";
|
||||
ITEM_MOD_INTELLECT_SHORT = "智力";
|
||||
ITEM_MOD_SPIRIT_SHORT = "精神";
|
||||
ITEM_MOD_STAMINA_SHORT = "耐力";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "攻擊強度";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "遠程攻擊強度";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "致命一擊(近戰)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "致命一擊(遠程)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "命中(近戰)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "命中(遠程)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "命中(法術)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "致命一擊(法術)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "傷害加成";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "治療加成";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "法力恢復";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "防禦";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "閃躲";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "招架";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "格擋";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "格擋值";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒傷害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "裝備管理員";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "新設定";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "設定%s的註記:";
|
||||
SET_NOTE = "設定註記";
|
||||
SILVER_AMOUNT_SYMBOL = "銀";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/清除專注目標";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/點擊";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/裝備套裝";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/專注目標";
|
||||
LINK_TRADESKILL_TOOLTIP = "點擊這裡以製造一個你專業技能的連結。";
|
||||
@@ -1,217 +0,0 @@
|
||||
-- AoE Loot Test
|
||||
--
|
||||
-- /aoeloot opens a picker window listing every item in every lootable
|
||||
-- corpse within click range. Clicking a row hands the (guid, itemID)
|
||||
-- pair to C_Loot.LootUnitItem and re-scans so the picker refreshes
|
||||
-- with whatever's left.
|
||||
--
|
||||
-- Known visual: each scan opens / closes loot windows in rapid
|
||||
-- succession, so the Blizzard LootFrame flickers during the scan. This
|
||||
-- addon intentionally doesn't suppress that — the goal is to exercise
|
||||
-- the C++ surface, not to ship polished UI.
|
||||
|
||||
local MAX_ROWS = 24
|
||||
local RESCAN_DELAY = 0.3 -- seconds; lets LootUnitItem complete before re-scan
|
||||
|
||||
-- ---------- UI scaffolding ----------
|
||||
|
||||
-- Frame geometry. Height is recomputed dynamically per scan so the
|
||||
-- window only takes up as much screen real estate as the result count
|
||||
-- needs — see `ResizeForRows`.
|
||||
local FRAME_WIDTH = 320
|
||||
local HEADER_HEIGHT = 64 -- title + status + insets; minimum frame height
|
||||
local ROW_HEIGHT = 22
|
||||
|
||||
local frame = CreateFrame("Frame", "AoELootTestFrame", UIParent)
|
||||
frame:SetWidth(FRAME_WIDTH)
|
||||
frame:SetHeight(HEADER_HEIGHT)
|
||||
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
})
|
||||
frame:SetMovable(true)
|
||||
frame:EnableMouse(true)
|
||||
frame:RegisterForDrag("LeftButton")
|
||||
-- Vanilla 1.12 fires script handlers with the frame set as the global
|
||||
-- `this`, not as a `self` arg to the function. Passing `frame.StartMoving`
|
||||
-- directly (the modern shortcut) breaks because the C method has no
|
||||
-- `self` to operate on. Wrap in closures so `this:method()` resolves
|
||||
-- correctly inside the handler.
|
||||
frame:SetScript("OnDragStart", function() this:StartMoving() end)
|
||||
frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
||||
frame:Hide()
|
||||
|
||||
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
title:SetPoint("TOP", frame, "TOP", 0, -16)
|
||||
title:SetText("AoE Loot")
|
||||
|
||||
local status = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
||||
status:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -36)
|
||||
status:SetText("")
|
||||
|
||||
local closeBtn = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
|
||||
closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6)
|
||||
|
||||
-- ---------- Row pool ----------
|
||||
|
||||
local rows = {}
|
||||
|
||||
local function CreateRow(parent, idx)
|
||||
local row = CreateFrame("Button", nil, parent)
|
||||
row:SetWidth(290)
|
||||
row:SetHeight(20)
|
||||
row:SetPoint("TOPLEFT", parent, "TOPLEFT", 15,
|
||||
-54 - (idx - 1) * ROW_HEIGHT)
|
||||
|
||||
local icon = row:CreateTexture(nil, "ARTWORK")
|
||||
icon:SetWidth(18); icon:SetHeight(18)
|
||||
icon:SetPoint("LEFT", row, "LEFT", 0, 0)
|
||||
row.icon = icon
|
||||
|
||||
local text = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
text:SetPoint("LEFT", icon, "RIGHT", 6, 0)
|
||||
text:SetPoint("RIGHT", row, "RIGHT", 0, 0)
|
||||
text:SetJustifyH("LEFT")
|
||||
row.text = text
|
||||
|
||||
local hl = row:CreateTexture(nil, "HIGHLIGHT")
|
||||
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
||||
hl:SetBlendMode("ADD")
|
||||
hl:SetAllPoints(row)
|
||||
|
||||
-- Tooltip on hover. The engine's native `SetHyperlink` does a
|
||||
-- literal `SStrCmpI` of the input's first 5 chars against the
|
||||
-- string `"item:"` at `.rdata` VA `0x00842D60` — anything that
|
||||
-- starts with `|c...` (full link with color) or `|Hitem` (full
|
||||
-- link without color) fails the prefix check with "Unknown link
|
||||
-- type". Only the bare payload form survives.
|
||||
--
|
||||
-- We extract the payload from the engine's full link rather than
|
||||
-- composing `item:ID:0:0:0` ourselves, so per-instance fields
|
||||
-- (enchant / random-suffix / unique) survive round-trip and
|
||||
-- "Stringy Wolf Meat of the Bear" tooltips correctly instead of
|
||||
-- collapsing to the un-suffixed itemID.
|
||||
row:SetScript("OnEnter", function()
|
||||
if this.payload then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetHyperlink(this.payload)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
end)
|
||||
row:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
row:Hide()
|
||||
return row
|
||||
end
|
||||
|
||||
for i = 1, MAX_ROWS do
|
||||
rows[i] = CreateRow(frame, i)
|
||||
end
|
||||
|
||||
-- ---------- Render ----------
|
||||
|
||||
-- Tail of a GUID string — last 4 hex chars of the low dword. Lets the
|
||||
-- user tell apart two rows of "Stringy Wolf Meat x1" coming from
|
||||
-- different corpses, without occupying a whole column with the full
|
||||
-- 16-digit GUID.
|
||||
local function ShortGuidSuffix(guid)
|
||||
return "[.." .. string.sub(guid, -4) .. "]"
|
||||
end
|
||||
|
||||
-- Extract the `item:ID:enchant:suffix:unique` payload out of the
|
||||
-- engine's full link string (`|cff...|Hitem:...|h[Name]|h|r`).
|
||||
-- Vanilla `SetHyperlink` requires the input to start literally with
|
||||
-- `"item:"` — so we can't pass the full link, but we also can't drop
|
||||
-- the per-instance fields (random suffix is what distinguishes "of
|
||||
-- the Bear" from "of the Eagle" on otherwise identical itemIDs).
|
||||
-- Returns the payload, or nil if extraction fails.
|
||||
local function PayloadFromLink(link)
|
||||
if not link then return nil end
|
||||
return string.match(link, "|H(item:[^|]+)|h")
|
||||
end
|
||||
|
||||
local function ClearRows()
|
||||
for i = 1, MAX_ROWS do
|
||||
rows[i]:Hide()
|
||||
rows[i]:SetScript("OnClick", nil)
|
||||
rows[i].payload = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function PopulateRows(results)
|
||||
local idx = 0
|
||||
for _, corpse in ipairs(results) do
|
||||
for _, item in ipairs(corpse.items) do
|
||||
idx = idx + 1
|
||||
if idx > MAX_ROWS then break end
|
||||
local row = rows[idx]
|
||||
local name, _, _, _, _, _, _, _, icon = GetItemInfo(item.itemID)
|
||||
row.icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
local label = name or ("Item #" .. item.itemID)
|
||||
if item.count and item.count > 1 then
|
||||
label = label .. " x" .. item.count
|
||||
end
|
||||
label = label .. " " .. ShortGuidSuffix(corpse.guid)
|
||||
row.text:SetText(label)
|
||||
row.payload = PayloadFromLink(item.link) -- for OnEnter tooltip
|
||||
local guid, itemID = corpse.guid, item.itemID
|
||||
row:SetScript("OnClick", function()
|
||||
C_Loot.LootUnitItem(guid, itemID)
|
||||
C_Timer.After(RESCAN_DELAY, function()
|
||||
if frame:IsShown() then C_Loot.ScanNearbyLoot() end
|
||||
end)
|
||||
end)
|
||||
row:Show()
|
||||
end
|
||||
if idx > MAX_ROWS then break end
|
||||
end
|
||||
for i = idx + 1, MAX_ROWS do rows[i]:Hide() end
|
||||
local shown
|
||||
if idx == 0 then
|
||||
status:SetText("Nothing lootable in range.")
|
||||
shown = 0
|
||||
elseif idx > MAX_ROWS then
|
||||
status:SetText(MAX_ROWS .. "+ items (showing first " .. MAX_ROWS .. ")")
|
||||
shown = MAX_ROWS
|
||||
else
|
||||
status:SetText(idx .. " item(s) available")
|
||||
shown = idx
|
||||
end
|
||||
frame:SetHeight(HEADER_HEIGHT + shown * ROW_HEIGHT)
|
||||
end
|
||||
|
||||
-- ---------- Scan trigger ----------
|
||||
|
||||
local function StartScan()
|
||||
status:SetText("Scanning...")
|
||||
ClearRows()
|
||||
frame:SetHeight(HEADER_HEIGHT) -- collapse while we wait for results
|
||||
if not C_Loot.ScanNearbyLoot() then
|
||||
status:SetText("Scan busy or not in world.")
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------- Event wiring ----------
|
||||
|
||||
local evt = CreateFrame("Frame")
|
||||
evt:RegisterEvent("LOOT_SCAN_COMPLETED")
|
||||
evt:SetScript("OnEvent", function()
|
||||
-- Race guard: a delayed scan may complete after the user closed
|
||||
-- the picker. Just drop the result rather than re-showing.
|
||||
if not frame:IsShown() then return end
|
||||
PopulateRows(C_Loot.GetLastScanResults())
|
||||
end)
|
||||
|
||||
-- ---------- Slash command ----------
|
||||
|
||||
RegisterNewSlashCommand(function()
|
||||
if frame:IsShown() then
|
||||
StartScan() -- already open → just refresh
|
||||
else
|
||||
frame:Show()
|
||||
StartScan()
|
||||
end
|
||||
end, "aoeloot", "aoe")
|
||||
@@ -1,7 +0,0 @@
|
||||
## Interface: 11200
|
||||
## Title: AoE Loot Test
|
||||
## Notes: Test bed for ClassicAPI's C_Loot.ScanNearbyLoot / LootUnitItem. /aoeloot
|
||||
## Dependencies: !!!ClassicAPI
|
||||
## Version: DEV
|
||||
|
||||
AoELootTest.lua
|
||||
@@ -1,335 +0,0 @@
|
||||
------------------------------------------------------------------------------
|
||||
-- TableInspector.lua
|
||||
--
|
||||
-- Vanilla 1.12 backport of the modern Blizzard_TableInspector UX,
|
||||
-- scoped to the "minimal" feature set: one navigable window with a
|
||||
-- breadcrumb showing the current path, a scrollable list of (key,
|
||||
-- value) rows for the current table, and click-to-drill / Back-to-pop
|
||||
-- navigation through sub-tables.
|
||||
--
|
||||
-- Trade-offs vs the modern source:
|
||||
-- * No DataProvider / ScrollBox infrastructure — we use the same
|
||||
-- fixed-row + slider pattern EventTraceFrame uses (vanilla
|
||||
-- FauxScrollFrame works too but we already validated the slider
|
||||
-- approach in this addon).
|
||||
-- * No anchor / attribute providers, no live polling — open once
|
||||
-- against the current snapshot.
|
||||
-- * No multi-tab or pinning — one window, navigate via Back.
|
||||
--
|
||||
-- API:
|
||||
-- TableInspector_Show(tbl, label) -- open with `tbl` as root, `label` shown in breadcrumb
|
||||
-- TableInspector_Push(tbl, label) -- drill in (called by row click)
|
||||
-- TableInspector_Back() -- pop one level
|
||||
-- TableInspector_Refresh() -- re-read the current level's live table (scroll kept)
|
||||
-- TableInspector_Close() -- hide the window
|
||||
-- /tinspect <luaexpr> -- evaluate `<luaexpr>` as Lua and open if result is a table
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
TABLE_INSPECTOR_ROW_HEIGHT = 16;
|
||||
TABLE_INSPECTOR_NUM_ROWS = 22;
|
||||
|
||||
local _TableInspectorFrame;
|
||||
|
||||
-- Navigation stack — `{ {tbl=..., label=...}, ... }`. The last entry
|
||||
-- is the active view. Push when drilling in, pop on Back.
|
||||
local _navStack = {};
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Key + value formatting
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Returns three lists: numeric keys (sorted ascending), string keys
|
||||
-- (sorted), and other-type keys (booleans, tables-as-keys, etc.).
|
||||
-- The other-type bucket stays unsorted — there's no canonical order
|
||||
-- across types and these are rare in practice.
|
||||
local function _SortedKeys(tbl)
|
||||
local nums, strs, others = {}, {}, {};
|
||||
for k in pairs(tbl) do
|
||||
local t = type(k);
|
||||
if (t == "number") then
|
||||
table.insert(nums, k);
|
||||
elseif (t == "string") then
|
||||
table.insert(strs, k);
|
||||
else
|
||||
table.insert(others, k);
|
||||
end
|
||||
end
|
||||
table.sort(nums);
|
||||
table.sort(strs);
|
||||
return nums, strs, others;
|
||||
end
|
||||
|
||||
-- Returns (displayText, valueType, isDrillable). `isDrillable` is true
|
||||
-- only for tables (the only type that can be navigated into).
|
||||
local function _FormatValue(v)
|
||||
local t = type(v);
|
||||
if (t == "string") then
|
||||
-- Truncate very long strings to keep rows scannable; user can
|
||||
-- always tooltip-hover for the full content if needed.
|
||||
if (string.len(v) > 80) then
|
||||
return string.format('"%s..."', string.sub(v, 1, 77)), "string", false;
|
||||
end
|
||||
return string.format('"%s"', v), "string", false;
|
||||
elseif (t == "number") then
|
||||
return tostring(v), "number", false;
|
||||
elseif (t == "boolean") then
|
||||
return v and "true" or "false", "boolean", false;
|
||||
elseif (t == "table") then
|
||||
return tostring(v), "table", true;
|
||||
elseif (t == "function") then
|
||||
return tostring(v), "function", false;
|
||||
elseif (t == "userdata") then
|
||||
return tostring(v), "userdata", false;
|
||||
end
|
||||
return tostring(v), t, false;
|
||||
end
|
||||
|
||||
local _TYPE_COLORS = {
|
||||
["string"] = { 1.00, 0.75, 0.40 },
|
||||
["number"] = { 0.60, 0.85, 1.00 },
|
||||
["boolean"] = { 0.70, 0.70, 1.00 },
|
||||
["table"] = { 0.60, 1.00, 0.60 },
|
||||
["function"] = { 1.00, 0.60, 0.60 },
|
||||
["userdata"] = { 1.00, 0.60, 0.60 },
|
||||
};
|
||||
|
||||
local function _ValueColor(t)
|
||||
local c = _TYPE_COLORS[t];
|
||||
if (c) then return c[1], c[2], c[3]; end
|
||||
return 1, 1, 1;
|
||||
end
|
||||
|
||||
local function _FormatKey(k)
|
||||
local t = type(k);
|
||||
if (t == "string") then return k; end
|
||||
if (t == "number") then return string.format("[%d]", k); end
|
||||
return string.format("[%s]", tostring(k));
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- View state — manages the flattened (key, value) list for the active
|
||||
-- table. Rebuilt on push / pop, not on every Update tick.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Cached array of (key, value) tuples for the active table, in the
|
||||
-- sort order returned by _SortedKeys. Re-derived on every navigation
|
||||
-- change so the rows can be rendered by simple `entries[scroll + i]`
|
||||
-- indexing instead of walking `pairs()` mid-frame.
|
||||
local _entries = {};
|
||||
|
||||
local function _RebuildEntries()
|
||||
_entries = {};
|
||||
local top = _navStack[table.getn(_navStack)];
|
||||
if (not top or type(top.tbl) ~= "table") then return; end
|
||||
local nums, strs, others = _SortedKeys(top.tbl);
|
||||
for i = 1, table.getn(nums) do
|
||||
local k = nums[i];
|
||||
table.insert(_entries, { key = k, value = top.tbl[k] });
|
||||
end
|
||||
for i = 1, table.getn(strs) do
|
||||
local k = strs[i];
|
||||
table.insert(_entries, { key = k, value = top.tbl[k] });
|
||||
end
|
||||
for i = 1, table.getn(others) do
|
||||
local k = others[i];
|
||||
table.insert(_entries, { key = k, value = top.tbl[k] });
|
||||
end
|
||||
end
|
||||
|
||||
local function _Breadcrumb()
|
||||
local parts = {};
|
||||
for i = 1, table.getn(_navStack) do
|
||||
table.insert(parts, _navStack[i].label or "?");
|
||||
end
|
||||
return table.concat(parts, " > ");
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Public API
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
function TableInspector_Show(tbl, label)
|
||||
if (type(tbl) ~= "table") then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
"|cffff8888TableInspector: argument is not a table|r");
|
||||
return;
|
||||
end
|
||||
_navStack = { { tbl = tbl, label = label or tostring(tbl) } };
|
||||
_RebuildEntries();
|
||||
if (_TableInspectorFrame) then
|
||||
_TableInspectorFrame:Show();
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
end
|
||||
|
||||
function TableInspector_Push(tbl, label)
|
||||
if (type(tbl) ~= "table") then return; end
|
||||
table.insert(_navStack, { tbl = tbl, label = label or tostring(tbl) });
|
||||
_RebuildEntries();
|
||||
-- Reset scroll to top so the user starts at the beginning of the
|
||||
-- newly-drilled table.
|
||||
local scroll = getglobal("TableInspectorFrameScroll");
|
||||
if (scroll) then scroll:SetValue(0); end
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
|
||||
function TableInspector_Back()
|
||||
if (table.getn(_navStack) <= 1) then
|
||||
TableInspector_Close();
|
||||
return;
|
||||
end
|
||||
table.remove(_navStack);
|
||||
_RebuildEntries();
|
||||
local scroll = getglobal("TableInspectorFrameScroll");
|
||||
if (scroll) then scroll:SetValue(0); end
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
|
||||
function TableInspector_Refresh()
|
||||
local top = _navStack[table.getn(_navStack)];
|
||||
if (not top or type(top.tbl) ~= "table") then return; end
|
||||
_RebuildEntries();
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
|
||||
function TableInspector_Close()
|
||||
if (_TableInspectorFrame) then _TableInspectorFrame:Hide(); end
|
||||
_navStack = {};
|
||||
_entries = {};
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Frame scripts (called from TableInspector XML in DebugTools.xml)
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
function TableInspectorFrame_OnLoad(self)
|
||||
self.rows = {};
|
||||
_TableInspectorFrame = self;
|
||||
|
||||
self:EnableMouse(true);
|
||||
self:EnableMouseWheel(true);
|
||||
self:SetScript("OnMouseWheel", function()
|
||||
TableInspectorFrame_OnMouseWheel(this, arg1);
|
||||
end);
|
||||
|
||||
for i = 1, TABLE_INSPECTOR_NUM_ROWS do
|
||||
local row = CreateFrame("Button", "TableInspectorFrameRow" .. i, self,
|
||||
"TableInspectorRowTemplate");
|
||||
row:SetPoint("TOPLEFT", 12, -(56 + (i - 1) * TABLE_INSPECTOR_ROW_HEIGHT));
|
||||
row:SetPoint("RIGHT", -28, 0);
|
||||
row.rowIndex = i;
|
||||
table.insert(self.rows, row);
|
||||
end
|
||||
end
|
||||
|
||||
function TableInspectorFrame_Update()
|
||||
if (not _TableInspectorFrame or not _TableInspectorFrame.rows) then return; end
|
||||
|
||||
local scroll = getglobal("TableInspectorFrameScroll");
|
||||
if (not scroll) then return; end
|
||||
|
||||
local total = table.getn(_entries);
|
||||
local visible = TABLE_INSPECTOR_NUM_ROWS;
|
||||
local maxOffset = math.max(0, total - visible);
|
||||
scroll:SetMinMaxValues(0, maxOffset);
|
||||
local offset = math.floor(scroll:GetValue() + 0.5);
|
||||
if (offset > maxOffset) then offset = maxOffset; end
|
||||
if (offset < 0) then offset = 0; end
|
||||
|
||||
-- Update breadcrumb each tick — cheap (string concat over a tiny
|
||||
-- nav stack) and avoids a separate "breadcrumb changed" signal.
|
||||
local crumb = getglobal("TableInspectorFrameBreadcrumb");
|
||||
if (crumb) then
|
||||
crumb:SetText(_Breadcrumb() .. string.format(" (%d entries)", total));
|
||||
end
|
||||
|
||||
for i = 1, visible do
|
||||
local row = _TableInspectorFrame.rows[i];
|
||||
local entry = _entries[offset + i];
|
||||
if (entry) then
|
||||
local keyFS = getglobal(row:GetName() .. "Key");
|
||||
local valFS = getglobal(row:GetName() .. "Value");
|
||||
keyFS:SetText(_FormatKey(entry.key));
|
||||
local disp, vt, drillable = _FormatValue(entry.value);
|
||||
valFS:SetText(disp);
|
||||
valFS:SetTextColor(_ValueColor(vt));
|
||||
row.entryKey = entry.key;
|
||||
row.entryValue = entry.value;
|
||||
row.drillable = drillable;
|
||||
row:Show();
|
||||
else
|
||||
row.entryKey = nil;
|
||||
row.entryValue = nil;
|
||||
row.drillable = false;
|
||||
row:Hide();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TableInspectorFrame_OnMouseWheel(self, delta)
|
||||
local scroll = getglobal("TableInspectorFrameScroll");
|
||||
if (not scroll) then return; end
|
||||
local mn, mx = scroll:GetMinMaxValues();
|
||||
local v = scroll:GetValue() - delta * 3;
|
||||
if (v < mn) then v = mn; end
|
||||
if (v > mx) then v = mx; end
|
||||
scroll:SetValue(v);
|
||||
end
|
||||
|
||||
function TableInspectorRow_OnClick(self)
|
||||
if (not self.drillable or type(self.entryValue) ~= "table") then return; end
|
||||
TableInspector_Push(self.entryValue, _FormatKey(self.entryKey));
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Slash command — /tinspect <luaexpr>
|
||||
--
|
||||
-- Evaluates `<luaexpr>` in a fresh chunk with `return ` prefixed, so
|
||||
-- bare expressions (`_G`, `GameTooltip`, `MyAddOn.foo`) work without
|
||||
-- the user having to write `return ...` themselves.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local function _ChatErr(msg)
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff8888TableInspector:|r " .. msg);
|
||||
end
|
||||
|
||||
function TableInspector_HandleSlashCmd(msg)
|
||||
if (not msg) then msg = ""; end
|
||||
msg = string.gsub(msg, "^%s*(.-)%s*$", "%1"); -- trim
|
||||
if (msg == "" or msg == "help") then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
"|cff88ff88/tinspect|r <expr> -- open inspector on the result of <expr>");
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" example: /tinspect _G");
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" example: /tinspect ChatTypeInfo");
|
||||
return;
|
||||
end
|
||||
if (msg == "close") then
|
||||
TableInspector_Close();
|
||||
return;
|
||||
end
|
||||
local chunk, parseErr = loadstring("return " .. msg);
|
||||
if (not chunk) then
|
||||
_ChatErr("parse error: " .. tostring(parseErr));
|
||||
return;
|
||||
end
|
||||
-- Run in the global environment so the user's expression sees
|
||||
-- the usual addon globals. setfenv on the loadstring chunk
|
||||
-- defaults to that already in vanilla, but be explicit.
|
||||
setfenv(chunk, getfenv(0));
|
||||
local ok, result = pcall(chunk);
|
||||
if (not ok) then
|
||||
_ChatErr("eval error: " .. tostring(result));
|
||||
return;
|
||||
end
|
||||
if (type(result) ~= "table") then
|
||||
_ChatErr(string.format(
|
||||
"result is %s (%s), expected table",
|
||||
type(result), tostring(result)));
|
||||
return;
|
||||
end
|
||||
TableInspector_Show(result, msg);
|
||||
end
|
||||
|
||||
SLASH_TINSPECT1 = "/tinspect";
|
||||
SLASH_TINSPECT2 = "/tins";
|
||||
SlashCmdList["TINSPECT"] = TableInspector_HandleSlashCmd;
|
||||
+54
-61
@@ -2,6 +2,21 @@ cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(ClassicAPI LANGUAGES CXX RC)
|
||||
|
||||
# WoW.exe is x86, so the injected DLL must be x86 too — there is no valid
|
||||
# 64-bit configure of this project. Fail here rather than let one through:
|
||||
# a 64-bit build's first visible symptom is a static_assert deep in a
|
||||
# struct-layout check, which reads as an unrelated code fault. An IDE that
|
||||
# configures on its own (VS Code's CMake Tools picks an architecture from a
|
||||
# scanned kit) can flip an existing build directory this way, which is what
|
||||
# CMakePresets.json's `win32` preset exists to prevent.
|
||||
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
message(FATAL_ERROR
|
||||
"ClassicAPI builds 32-bit only. This configure uses "
|
||||
"${CMAKE_SIZEOF_VOID_P}-byte pointers. Delete the build directory, "
|
||||
"because CMake cannot switch platform in place. Then configure with "
|
||||
"`cmake --preset win32` or `cmake -B build -A Win32`.")
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
|
||||
@@ -30,7 +45,7 @@ set(TINYCBOR_SOURCES
|
||||
# always pass locally. CI release builds pass `-DCLASSICAPI_TAG=vX.Y.Z`,
|
||||
# which drives BOTH the compiled `CLASSIC_API_VERSION` int AND the
|
||||
# embedded addon's `## Version:` toc line (substituted in
|
||||
# embed_addon.cmake). The on-disk source toc stays `## Version: DEV`;
|
||||
# embed_resources.cmake). The on-disk source toc stays `## Version: DEV`;
|
||||
# only the embedded copy is stamped, so a release build needs nothing
|
||||
# but the flag — no source edit.
|
||||
set(CLASSICAPI_VERSION_VALUE 99999999)
|
||||
@@ -58,55 +73,24 @@ elseif(CLASSICAPI_TAG)
|
||||
)
|
||||
endif()
|
||||
|
||||
# Bundle `AddOns/!!!ClassicAPI/` into a generated header so the DLL can
|
||||
# surface the addon to the engine even when the user doesn't have it
|
||||
# installed on disk. The file-read hook in `src/addons/Embedded.cpp`
|
||||
# returns these buffers when the engine asks for paths under
|
||||
# `Interface\AddOns\!!!ClassicAPI\`.
|
||||
set(EMBEDDED_CLASSICAPI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/AddOns/!!!ClassicAPI")
|
||||
set(EMBEDDED_CLASSICAPI_HEADER "${CMAKE_BINARY_DIR}/generated/embedded_classicapi.h")
|
||||
file(GLOB_RECURSE EMBEDDED_CLASSICAPI_FILES CONFIGURE_DEPENDS
|
||||
"${EMBEDDED_CLASSICAPI_SRC}/*")
|
||||
# The `.classicapi-dev` marker is a disk-only developer signal — keep it
|
||||
# out of the embed inputs (embed_addon.cmake filters it too).
|
||||
list(FILTER EMBEDDED_CLASSICAPI_FILES EXCLUDE REGEX "/\\.classicapi-dev$")
|
||||
add_custom_command(
|
||||
OUTPUT "${EMBEDDED_CLASSICAPI_HEADER}"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DSRC_DIR=${EMBEDDED_CLASSICAPI_SRC}
|
||||
-DOUT_HEADER=${EMBEDDED_CLASSICAPI_HEADER}
|
||||
-DADDON_NAME=ClassicAPI
|
||||
-DADDON_VERSION=${CLASSICAPI_TOC_VERSION}
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_addon.cmake"
|
||||
DEPENDS
|
||||
${EMBEDDED_CLASSICAPI_FILES}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_addon.cmake"
|
||||
COMMENT "Embedding !!!ClassicAPI addon into ${EMBEDDED_CLASSICAPI_HEADER}"
|
||||
VERBATIM)
|
||||
|
||||
# Bundle `src/bindings/Bindings.xml` (our TARGETING-group additions)
|
||||
# into a generated header. `src/bindings/Inject.cpp` extracts the
|
||||
# inner `<Binding>` elements from the embedded buffer and splices them
|
||||
# into the engine's `Interface\FrameXML\Bindings.xml` at load time.
|
||||
set(EMBEDDED_BINDINGS_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml")
|
||||
set(EMBEDDED_BINDINGS_HEADER "${CMAKE_BINARY_DIR}/generated/embedded_bindings.h")
|
||||
add_custom_command(
|
||||
OUTPUT "${EMBEDDED_BINDINGS_HEADER}"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DINPUT_FILE=${EMBEDDED_BINDINGS_SRC}
|
||||
-DOUT_HEADER=${EMBEDDED_BINDINGS_HEADER}
|
||||
-DSYMBOL=kEmbeddedBindingsXml
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_file.cmake"
|
||||
DEPENDS
|
||||
"${EMBEDDED_BINDINGS_SRC}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_file.cmake"
|
||||
COMMENT "Embedding bindings XML into ${EMBEDDED_BINDINGS_HEADER}"
|
||||
VERBATIM)
|
||||
|
||||
add_library(${PROJECT_NAME} SHARED
|
||||
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES}
|
||||
${EMBEDDED_CLASSICAPI_HEADER}
|
||||
${EMBEDDED_BINDINGS_HEADER})
|
||||
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
|
||||
|
||||
# Embed the addons under `src/embed/AddOns/` and `src/bindings/Bindings.xml` as RCDATA
|
||||
# resources. The file-read hook in `src/addons/Embedded.cpp` serves the
|
||||
# addon files when the engine asks for paths under
|
||||
# `Interface\AddOns\<addon>\`, so the addons load even when the user
|
||||
# doesn't have them on disk. `src/bindings/Inject.cpp` splices the bindings
|
||||
# XML (our TARGETING-group additions) into the engine's
|
||||
# `Interface\FrameXML\Bindings.xml` at load time.
|
||||
include(cmake/embed_resources.cmake)
|
||||
embed_resources(
|
||||
TARGET ${PROJECT_NAME}
|
||||
ADDONS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/src/embed/AddOns"
|
||||
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools"
|
||||
ADDON_VERSION "${CLASSICAPI_TOC_VERSION}"
|
||||
BINDINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml"
|
||||
OUT_DIR "${CMAKE_BINARY_DIR}/generated")
|
||||
|
||||
target_compile_definitions(
|
||||
${PROJECT_NAME}
|
||||
@@ -114,8 +98,7 @@ target_compile_definitions(
|
||||
|
||||
# Stamp the PE VERSIONINFO resource from the same version components, so the
|
||||
# DLL's file/product version (Explorer -> Properties -> Details) matches the
|
||||
# CLASSIC_API_VERSION Lua global. The .rsrc section the resource lands in is
|
||||
# untouched by the .rdata/.pdata section merges below.
|
||||
# CLASSIC_API_VERSION Lua global.
|
||||
set(CLASSICAPI_VER_STRING
|
||||
"${CLASSICAPI_VER_MAJOR}.${CLASSICAPI_VER_MINOR}.${CLASSICAPI_VER_PATCH}.0")
|
||||
set(CLASSICAPI_VERSION_RC "${CMAKE_BINARY_DIR}/generated/version.rc")
|
||||
@@ -125,6 +108,12 @@ configure_file(
|
||||
@ONLY)
|
||||
target_sources(${PROJECT_NAME} PRIVATE "${CLASSICAPI_VERSION_RC}")
|
||||
|
||||
# The .def exports `Load` (VanillaFixes' post-injection entry point)
|
||||
# undecorated so its GetProcAddress(module, "Load") resolves. See the
|
||||
# loader-lock rationale in src/DllMain.cpp. Listed as a source so CMake emits
|
||||
# a quoted /DEF: on every generator and relinks when the file changes.
|
||||
target_sources(${PROJECT_NAME} PRIVATE src/ClassicAPI.def)
|
||||
|
||||
# ole32: COM (CoInitializeEx / CoCreateInstance / CoTaskMemFree) for the
|
||||
# SAPI text-to-speech backend in src/voicechat/Tts.cpp. The SAPI GUIDs are
|
||||
# instantiated in that TU via <initguid.h>, so no sapi.lib is needed.
|
||||
@@ -137,16 +126,19 @@ target_include_directories(
|
||||
${CMAKE_BINARY_DIR}/generated
|
||||
external/minhook/include
|
||||
external
|
||||
external/tinycbor/src)
|
||||
external/tinycbor/src
|
||||
external/unordered_dense/include)
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "ClassicAPI")
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT")
|
||||
# /Zi retains full debug info (source file + line numbers, locals, types) in a
|
||||
# separate PDB WITHOUT disabling optimization — the DLL stays a fully optimized
|
||||
# /O2 release build; the symbols just live in ClassicAPI.pdb on the side. Feeds
|
||||
# the linker's /DEBUG below.
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT /O2 /Ob2 /DNDEBUG /Zi")
|
||||
# /O1 optimizes for size: the code is mostly short glue around engine calls,
|
||||
# and /O1 made the DLL 280 KB smaller than /O2 /Ob2 (link-time code generation,
|
||||
# /GL + /LTCG, made it larger again). /Zi retains full debug info (source file +
|
||||
# line numbers, locals, types) in a separate PDB WITHOUT disabling
|
||||
# optimization; the symbols just live in ClassicAPI.pdb on the side. Feeds the
|
||||
# linker's /DEBUG below.
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT /O1 /DNDEBUG /Zi")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS
|
||||
# Two debug artifacts for triaging crashes from a SHIPPED DLL. Both are
|
||||
# published as release artifacts, match this exact build, and have no effect
|
||||
@@ -163,8 +155,9 @@ set(CMAKE_SHARED_LINKER_FLAGS
|
||||
# identical-COMDAT fold); they are listed explicitly below so they stay ON
|
||||
# and the shipped DLL does not grow. (/OPT:ICF folds identical functions, so
|
||||
# a few map symbols share an address — harmless.)
|
||||
# /DEF exports `Load` (VanillaFixes' post-injection entry point) undecorated
|
||||
# so its GetProcAddress(module, "Load") resolves. See src/ClassicAPI.def and
|
||||
# the loader-lock rationale in src/DllMain.cpp.
|
||||
"/OPT:REF /OPT:ICF /MERGE:.rdata=.text /MERGE:.pdata=.text /SECTION:.text,EWR /NODEFAULTLIB:MSVCRT /MAP /DEBUG /DEF:${CMAKE_CURRENT_SOURCE_DIR}/src/ClassicAPI.def"
|
||||
# The sections keep the standard layout: .text is read + execute only and
|
||||
# constants stay in a read-only .rdata. Nothing writes into this DLL's own
|
||||
# code; engine patches make their target writable themselves
|
||||
# (Common::PatchBytes).
|
||||
"/OPT:REF /OPT:ICF /NODEFAULTLIB:MSVCRT /MAP /DEBUG"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 3,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 21,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "win32",
|
||||
"displayName": "Win32 (x86)",
|
||||
"description": "The only supported configuration: WoW.exe is 32-bit, so the DLL must be 32-bit too. No generator is named, so CMake picks the newest installed Visual Studio, matching the documented command line.",
|
||||
"binaryDir": "${sourceDir}/build",
|
||||
"architecture": "Win32"
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "release",
|
||||
"displayName": "Release",
|
||||
"configurePreset": "win32",
|
||||
"configuration": "Release"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,26 +15,28 @@ The headline features. Each one takes effect as soon as the DLL loads, so your
|
||||
existing addons benefit without any code changes. The complete per-function
|
||||
reference is in **[docs/API.md](docs/API.md)**.
|
||||
|
||||
### Lua 5.1 compatibility
|
||||
|
||||
The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
|
||||
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| [Syntax](docs/API.md#lua-51-syntax) | Compiles the Lua 5.1 length (`#`), modulo (`%`), `...`-expression, `0x` hex-literal, and leveled long bracket (`[=[ ]=]`) syntax that vanilla's Lua 5.0 rejects, by rewriting addon source before it compiles. Each addon file also receives its `(name, table)` through `...` (`local name, tbl = ...`). |
|
||||
| [String methods](docs/API.md#string-methods-supper-sformat) | Every string value accepts method calls — `("asd"):upper()`, `("%d gold"):format(n)`, `msg:match("^!(%w+)")` — resolving through the `string` table, the way Lua 5.1 works. Vanilla's Lua 5.0 errors with `attempt to index a string value`. Works on literals and variables, in-world and on the login screen, and inside coroutines. |
|
||||
| [Script-handler arguments](docs/API.md#setmodernscriptargsenable--getmodernscriptargs) | Frame-script handlers receive their values as positional arguments — `OnMouseWheel(self, delta)`, `OnClick(self, button)`, `OnEvent(self, event, ...)`, etc. — the way 5.1+ clients do, so modern addon ports work unmodified. The `this` / `arg1` globals stay set. A handler that declares no parameters is unaffected. A handler that declared a parameter and expected nil now receives its real value. On by default. `SetModernScriptArgs(false)` restores exact vanilla dispatch. |
|
||||
|
||||
### Modern client behaviors
|
||||
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) as icons in FontStrings, chat, and tooltips, the way 4.3.4+ clients do. Vanilla 1.12 shows the raw escape as literal text instead. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| Tooltip line cap | Lifts `GameTooltip`'s hard 30-line limit to 60 for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). Stat-heavy tooltips and comparison blocks (e.g. pfUI's eqcompare) no longer have their extra lines silently dropped. Done in pure C++ by growing the engine's FontString pool at tooltip-creation time. |
|
||||
| [Event-driven nameplates](docs/API.md#nameplate) | The modern `C_NamePlate` API, driven by real events instead of vanilla frame-scraping. `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` fire as plates appear and vanish, `nameplate1`..`nameplateN` tokens resolve with every `UnitX` function (and fire `UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "nameplateN"`), and `C_NamePlate.GetNamePlateForUnit` / `GetNamePlates` hand back the live frames. |
|
||||
| [Focus target](docs/API.md#focus) | A sticky focus unit, like later clients. `FocusUnit("target")` pins it and `ClearFocus()` drops it, with `PLAYER_FOCUS_CHANGED` on every change. The `focus` / `focustarget` tokens resolve with every `UnitX` function and fire unit events (`UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "focus"`), and the predefined `FOCUSTARGET` / `TARGETFOCUS` keybinds are ready to set. |
|
||||
| [Retail-like `/reload`](docs/API.md#reload-picks-up-new-addons-and-new-files) | `/reload` picks up addon changes made while the game runs, like a modern client. A new folder under `Interface\AddOns\` registers and loads as a normal addon, new files added to an existing addon's TOC load, a newly installed addon's first SavedVariables save survives `/reload`, `##` metadata edits (`## SavedVariables:`, `## Dependencies:`, `## Title:`, …) take effect, and a deleted addon folder drops from the addon list. A stock client needs a full restart for all of these. |
|
||||
| Lua 5.1 code | Addon code written for Lua 5.1 runs on the 1.12 Lua 5.0 VM. It covers the [5.1 syntax](docs/API.md#lua-51-syntax) (`#`, `%`, `...` expressions, `0x` hex, `[=[ ]=]` long brackets), [method calls on strings](docs/API.md#string-methods-supper-sformat) (`msg:match("^!(%w+)")`), [up to 60 upvalues](docs/API.md#upvalue-limit) for each function, and [new `for` loop variables](docs/API.md#for-loop-variables-per-pass) on each pass. Each addon file also gets its `(name, table)` through `...`. |
|
||||
| [Modern script handlers](docs/API.md#setmodernscriptargsenable--getmodernscriptargs) | Frame scripts get their values as arguments, for example `OnClick(self, button, down)` and `OnEvent(self, event, ...)`. [XML script bodies](docs/API.md#xml-script-bodies-see-self-and-their-arguments) such as `<OnLoad>self:Init()</OnLoad>` see `self` and their arguments by name. The `this` / `arg1` globals stay set. `SetModernScriptArgs(false)` restores the vanilla behavior. |
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| [Soft targeting](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key and soft targets. Bind **Interact With Target** to a key to interact with the nearest NPC, corpse, mailbox, chest or gathering node in front of you. The [`softenemy` and `softfriend`](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) tokens point at the nearest enemy and friendly unit without a change of target. With no target, harmful spells go to `softenemy` and helpful spells go to `softfriend`. `PLAYER_SOFT_*_CHANGED` events fire when the units change. |
|
||||
| [Animations](docs/API.md#animation) | Animation groups on frames, textures and font strings, made in Lua or XML: `CreateAnimationGroup`, looping, smoothing, scripts and templates, with `Alpha`, `Translation`, `Scale`, `Rotation` and `Path` applied. |
|
||||
| Tooltip line cap | Removes `GameTooltip`'s 30-line limit for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). A tooltip adds a new line pair when it needs one, so long tooltips and comparison blocks (for example, pfUI's eqcompare) keep all their lines. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. `parentArray="Name"` adds the object to a list on its parent. An `<Anchor>` takes `x` and `y` offsets and `relativeKey="$parent.Key"`. `<OnEnter inherit="prepend">` runs the template's handler and then its own, and `inherit="append"` runs them in the other order. |
|
||||
| [Macro conditions](docs/API.md#more-commands-with-conditions) | `[conditions]` and `@unit` work on 38 slash commands, from `/cast` and `/use` to `/target`, `/follow`, `/cancelaura` and the `/pet` family. With `/cast [@mouseover,harm][] Fireball`, the spell goes to the mouseover unit. With no mouseover unit, it goes to your target. `@cursor` places a ground-target spell or item under the mouse and `@player` places it at your feet. A character name works where a unit token does (`[target=Feral]`). Addons read the same rules through `SecureCmdOptionParse`. |
|
||||
| [`#showtooltip`](docs/API.md#showtooltip-and-show) | A macro button shows the spell or item that its directive resolves to, conditions included: icon, tooltip, cooldown, range, usable state and auto-repeat glow. Action-bar addons get this with no code of their own. Addons with their own macro parser can publish through `C_Macro.SetMacroDisplay`. |
|
||||
| [Event-driven nameplates](docs/API.md#nameplate) | The modern `C_NamePlate` API, driven by real events. `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` fire as plates appear and vanish, `nameplate1`..`nameplateN` tokens resolve with every `UnitX` function (and fire `UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "nameplateN"`), and `C_NamePlate.GetNamePlateForUnit` / `GetNamePlates` hand back the live frames. |
|
||||
| [Focus target](docs/API.md#focus) | A sticky focus unit. `FocusUnit("target")` pins it and `ClearFocus()` drops it, with `PLAYER_FOCUS_CHANGED` on every change. The `focus` / `focustarget` tokens resolve with every `UnitX` function and fire unit events (`UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "focus"`), and the predefined `FOCUSTARGET` / `TARGETFOCUS` keybinds are ready to set. |
|
||||
| [Combat log events](docs/API.md#combat_log_event_unfiltered--combat_log_event-events) | `COMBAT_LOG_EVENT_UNFILTERED` and `CombatLogGetCurrentEventInfo` give each combat event with GUIDs and `COMBATLOG_OBJECT_*` flags. The values do not depend on the client language, so damage meters do not need to parse chat text. `COMBAT_LOG_EVENT` fires for the events that pass the `CombatLogAddFilter` filters. |
|
||||
| [Retail-like `/reload`](docs/API.md#reload-picks-up-new-addons-and-new-files) | `/reload` picks up addon changes made while the game runs. A new folder under `Interface\AddOns\` registers and loads as a normal addon, new files added to an existing addon's TOC load, a newly installed addon's first SavedVariables save survives `/reload`, `##` metadata edits (`## SavedVariables:`, `## Dependencies:`, `## Title:`, …) take effect, and a deleted addon folder drops from the addon list. |
|
||||
| [Multi-flavor & conditional TOC](docs/API.md#conditional-and-multi-flavor-toc-loading) | Loads modern multi-flavor addons that ship one folder. Selects a version-specific TOC (`<Name>_ClassicAPI.toc` or `<Name>_Turtle.toc`) and the matching keybinding file (`Bindings_ClassicAPI.xml` / `Bindings_Turtle.xml`), accepts a comma-separated `## Interface:` version list (compatible when it includes the client version `11200`), and honors per-line `[AllowLoadGameType]` / `[AllowLoadTextLocale]` conditions and `[Family]` / `[Game]` / `[TextLocale]` path variables inside a TOC. |
|
||||
| [Launch switches](docs/API.md#launch) | Options you add to the shortcut that starts the game. `-config <name>` reads and writes `WTF\<name>` in place of `WTF\Config.wtf`, so one install can hold several settings profiles. `-gluescript` and `-gluescriptFile` run Lua at the login and character-select screens, every time those screens appear. `-gamescript` and `-gamescriptFile` run it one time, after you enter the world. Each script runs after the interface has loaded, so it can call interface functions and use frames. |
|
||||
| [Addon memory and CPU usage](docs/API.md#profiling) | Addon managers and performance tools can show how much Lua memory and CPU time each addon uses. `GetAddOnMemoryUsage` gives the memory of each addon. When `scriptProfile` is `1`, the CPU functions give the time of each addon, function, frame, and event. With profiling off, the CPU functions cost nothing. |
|
||||
| [Any texture size](docs/API.md#texture-size-and-shape) | Textures load at any size and any shape. Width and height do not need to be powers of two, and there is no fixed upper limit. The only limit is the maximum texture size of your graphics card. This applies to every texture path: `SetTexture`, sprite sheets, inline textures, tooltips, and masks. The client allocates memory for a large texture only when an addon loads one, so small textures cost nothing extra. |
|
||||
|
||||
Smaller behavior fixes, such as frame alpha and `SetParent` frame levels, are
|
||||
in **[docs/API.md](docs/API.md)** with the API that they change.
|
||||
|
||||
## Full API reference
|
||||
|
||||
@@ -45,67 +47,95 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
<details>
|
||||
<summary><b>In-game Lua calls</b> — 550+ functions across ~60 namespaces</summary>
|
||||
|
||||
In the Frame rows, a name that starts with `:` is a method of the widget that the row names, for example `:SetShown` is `frame:SetShown`.
|
||||
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
|
||||
| [AuraUtil](docs/API.md#aurautil) | `AuraUtil.FindAura`, `AuraUtil.FindAuraByName`, `AuraUtil.ForEachAura`, `AuraUtil.UnpackAuraData` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `HasBonusActionBar` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnInterfaceVersion`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded`, `C_AddOns.LoadAddOn` |
|
||||
| [APIDocumentation](docs/API.md#apidocumentation) | `/classicapi` in-game browser, `C_APIDocumentation.GetSystem`, `C_APIDocumentation.GetSystems` |
|
||||
| [Animation](docs/API.md#animation) | `region:CreateAnimationGroup`, `region:GetAnimationGroups`, `region:StopAnimating`, `SetMaxAnimFramerate`, `GetMaxAnimFramerate`, `AnimationGroup` and `Animation` / `Alpha` / `Translation` / `Scale` / `Rotation` / `Path` / `ControlPoint` methods |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem`, `CanCancelAuction` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `GetBindingByKey`, `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatEvent`, `GetCurrentChatGUID`, `IsChatAFK`, `IsChatDND`, `SendSystemMessage` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [ClassColor](docs/API.md#classcolor) | `C_ClassColor.GetClassColor` |
|
||||
| [ColorUtil](docs/API.md#colorutil) | `C_ColorUtil.ConvertRGBToHSV`, `C_ColorUtil.ConvertHSVToRGB`, `C_ColorUtil.ConvertHSVToHSL`, `C_ColorUtil.ConvertHSLToHSV`, `C_ColorUtil.ConvertHSLToRGB`, `C_ColorUtil.GenerateTextColorCode`, `C_ColorUtil.WrapTextInColor`, `C_ColorUtil.WrapTextInColorCode` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown`, `StartAttack`, `StopAttack` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [CombatLog](docs/API.md#combatlog) | `CombatLogGetCurrentEventInfo`, `COMBATLOG_OBJECT_*` (constants) |
|
||||
| [Console](docs/API.md#console) | `CalculateStringEditDistance`, `ConsoleAddMessage`, `ConsoleEcho`, `ConsoleExec`, `ConsoleGetAllCommands`, `ConsoleGetColorFromType`, `ConsoleGetFontHeight`, `ConsoleIsActive`, `ConsolePrintAllMatchingCommands`, `SetConsoleKey` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVarBitfield`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarInfo`, `C_CVar.RemoveTempCVar`, `C_CVar.SetCVar`, `C_CVar.SetCVarBitfield`, `C_CVar.SetTempCVar`, `GetCVar`, `GetCVarDefault` |
|
||||
| [Cursor](docs/API.md#cursor) | `CursorHasMacro`, `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerFiltered`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBagPortraitTexture`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetItemSearch`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown`, `SetBagPortraitTexture` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
|
||||
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetAccountExpansionLevel`, `GetClassicExpansionLevel`, `GetExpansionLevel`, `GetMaxPlayerLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByID`, `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.IsFactionActive`, `C_Reputation.IsFactionActiveByID`, `C_Reputation.IsFactionInactive`, `C_Reputation.SetFactionActive`, `C_Reputation.SetFactionActiveByID`, `C_Reputation.SetFactionInactive`, `C_Reputation.SetFactionInactiveByID`, `C_Reputation.SetSelectedFaction`, `C_Reputation.SetSelectedFactionByID`, `C_Reputation.SetWatchedFactionByID`, `C_Reputation.ToggleFactionAtWar`, `C_Reputation.ToggleFactionAtWarByID`, `CollapseAllFactionHeaders`, `ExpandAllFactionHeaders`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `fontstring:GetStringHeight`, `fontstring:GetUnboundedStringWidth`, `fontstring:GetWrappedWidth`, `fontstring:GetNumLines`, `fontstring:GetLineHeight`, `fontstring:IsTruncated`, `fontstring:SetMaxLines`, `fontstring:GetMaxLines`, `fontstring:SetFormattedText`, `texture:SetRotation`, `texture:GetRotation`, `texture:SetVertexOffset`, `texture:GetVertexOffset`, `texture:SetColorTexture`, `texture:SetMask`, `frame:CreateMaskTexture`, `texture:AddMaskTexture`, `texture:RemoveMaskTexture`, `texture:GetNumMaskTextures`, `texture:GetMaskTexture`, `fontstring:SetRotation`, `fontstring:GetRotation`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs`, `SecureCmdOptionParse`, `RegisterStateDriver`, `UnregisterStateDriver`, `RegisterAttributeDriver`, `UnregisterAttributeDriver`, `RegisterUnitWatch`, `UnregisterUnitWatch`, `UnitWatchRegistered`, `SecureButton_GetAttribute`, `SecureButton_GetUnit`, `PreClick` (script), `PostClick` (script), `GetClickFrame` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [Frame: Region](docs/API.md#regionsetpointpoint-one-argument-form) | `:SetPoint("point")` (one-arg form), `:SetSize` / `:GetSize`, `ignoreRect` on `:GetSize` / `:GetWidth` / `:GetHeight`, `:GetRect`, `:GetScaledRect`, `:IsRectValid`, `:GetPointByName`, `:ClearPoint`, `:GetScale` / `:GetEffectiveScale` (on textures and font strings), `:GetDebugName`, `:IsMouseOver`, `:IsDragging`, `:IsProtected` / `:CanChangeProtectedState` |
|
||||
| [Frame: Frame](docs/API.md#createframeframetype--name-parent-template-id) | `CreateFrame` template lists and `id`, `GetMouseFoci`, `:SetShown`, `:SetClampRectInsets` / `:GetClampRectInsets`, `:SetResizeBounds`, `:SetClipsChildren` / `:DoesClipChildren`, `:GetBoundsRect`, `:GetEffectiveAlpha`, `:SetAlphaGradient` / `:ClearAlphaGradient` / `:HasAlphaGradient`, `:DesaturateHierarchy`, `:HookScript`, `:IsEventRegistered`, `:RegisterUnitEvent`, `:SetPropagateKeyboardInput` / `:GetPropagateKeyboardInput`, `:SetDontSavePosition` / `:GetDontSavePosition`, `:SetAttribute`, `:SetAttributeNoHandler`, `:ClearAttribute`, `:GetAttribute`, `:CanChangeAttribute`, `:AllowAttributeChanges` |
|
||||
| [Frame: Button](docs/API.md#buttonsetnormalfontobjectfont--buttongetnormalfontobject) | `:SetEnabled`, `:SetFormattedText`, `:SetNormalFontObject` / `:GetNormalFontObject`, `:SetMotionScriptsWhileDisabled` / `:GetMotionScriptsWhileDisabled`, `:RegisterForClicks("AnyUp"/"AnyDown")`, `:RegisterForMouse`, `GetClickFrame` |
|
||||
| [Frame: FontString](docs/API.md#fontstringgetstringheight) | `:GetStringHeight`, `:GetUnboundedStringWidth`, `:GetWrappedWidth`, `:GetNumLines`, `:GetLineHeight`, `:GetFontHeight` / `:SetFontHeight` (also on fonts), `:GetFieldSize`, `:IsTruncated`, `:SetMaxLines` / `:GetMaxLines`, `:SetWordWrap` / `:CanWordWrap`, `:SetIndentedWordWrap` / `:GetIndentedWordWrap` (also on fonts, edit boxes, message frames, and SimpleHTML), `:SetFormattedText`, `:SetRotation` / `:GetRotation` |
|
||||
| [Frame: Texture](docs/API.md#texturesetrotationangle--cx-cy) | `:SetRotation` / `:GetRotation`, `:SetVertexOffset` / `:GetVertexOffset`, `:SetColorTexture`, `:SetAtlas` / `:GetAtlas`, `:ResetTexCoord`, `:SetSpriteSheetCell`, `:SetDesaturation` / `:GetDesaturation`, `IsDesaturateSupported`, `:SetMask`, `frame:CreateMaskTexture`, `:AddMaskTexture` / `:RemoveMaskTexture`, `:GetNumMaskTextures`, `:GetMaskTexture` |
|
||||
| [Frame: EditBox](docs/API.md#editboxsetcursorpositionposition) | `:SetCursorPosition` / `:GetCursorPosition`, `:GetUTF8CursorPosition`, `:ClearHighlightText`, `:SetHighlightColor` / `:GetHighlightColor`, `:HasFocus`, `GetCurrentKeyBoardFocus`, `:HasText`, `:ClearHistory`, `:Enable` / `:Disable` / `:IsEnabled`, `:SetCountInvisibleLetters` / `:IsCountInvisibleLetters`, `:IsInIMECompositionMode` |
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
|
||||
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
|
||||
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
|
||||
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
|
||||
| [FriendList](docs/API.md#friendlist) | `AddOrRemoveFriend`, `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject` (scripts) |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:AddTexture`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetMinimumWidth`, `GameTooltip:GetOwner`, `GameTooltip:GetPadding`, `GameTooltip:GetSpell`, `GameTooltip:GetUnit`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:IsUnit`, `GameTooltip:SetAnchorType`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetFrameStack`, `GameTooltip:SetHyperlink` (full links, `spell:`, `quest:`), `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject`, `OnTooltipSetFrameStack` (scripts) |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetPoiForUiMapID`, `C_GossipInfo.GetPoiInfo`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests`, `GetNumGossipOptions` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemClassInfo`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemClassInfo`, `GetItemIcon`, `GetItemSubClassInfo`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `GetMouseButtonName`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo`, `IsActiveBattlefieldArena` |
|
||||
| [Item: C_Item](docs/API.md#item) | `C_Item.DoesItemExist` / `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemClassInfo`, `C_Item.GetItemCount`, `C_Item.GetItemData` / `C_Item.GetItemDataByID`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon` / `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType` / `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize` / `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName` / `C_Item.GetItemNameByID`, `C_Item.GetItemQuality` / `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice` / `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID` / `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemTempEnchantInfo`, `C_Item.GetItemUniqueness` / `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached` / `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.IsUsableItem`, `C_Item.ItemHasRange`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockItem`, `C_Item.UnlockAllItems`, `C_Item.PickupItem`, `C_Item.RequestLoadItemData` / `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName` |
|
||||
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
|
||||
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `IsDualWielding`, `SetInventoryPortraitTexture` |
|
||||
| [Item: item IDs](docs/API.md#getitemid--companions-to-the-engines-getitemlink-family) | `GetAuctionItemID`, `GetAuctionSellItemID`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeTargetItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `string.gmatch`, `string.gsub` (table replacement), `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroBody`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `GetRunningMacro`, `GetRunningMacroButton`, `RunMacroText`, `SetMacroItem`, `SetMacroSpell`, `StopMacro` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
|
||||
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredAreaIDsAtPosition`, `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems`, `GetBuybackItemLink` |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names), `DressUpModel:UndressSlot`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature`, `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`) |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
| [PartyInfo](docs/API.md#partyinfo) | `InviteUnit`, `PromoteToLeader`, `UninviteUnit` |
|
||||
| [Pet](docs/API.md#pet) | `IsPetAttackAction`, `IsAtStableMaster` |
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `GetSheathState`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsOutdoors`, `IsStealthed`, `IsSwimming` |
|
||||
| [System](docs/API.md#system) | `CopyToClipboard`, `GetPhysicalScreenSize` |
|
||||
| [Profiling](docs/API.md#profiling) | `GetAddOnCPUUsage`, `GetAddOnMemoryUsage`, `GetEventCPUUsage`, `GetFrameCPUUsage`, `GetFunctionCPUUsage`, `GetScriptCPUUsage`, `ResetCPUUsage`, `UpdateAddOnCPUUsage`, `UpdateAddOnMemoryUsage` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetInfo`, `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestLogTitle`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogCompletionText`, `GetQuestLogLeaderBoardID` (Turtle WoW only: `C_QuestLog.GetAllCompletedQuestIDs`, `C_QuestLog.IsQuestFlaggedCompleted`, `GetQuestsCompleted`, `QueryQuestsCompleted`) |
|
||||
| [Sound](docs/API.md#sound) | `C_Sound.GetRecentSoundFiles`, `C_Sound.GetSoundScaledVolume`, `C_Sound.IsPlaying`, `C_Sound.PlayItemSound`, `C_Sound.PlaySound`, `C_Sound.PlaySoundWithOptions`, `C_Sound.PlayVocalErrorSound`, `MuteSoundFile`, `PlaySound`, `PlaySoundFile`, `StopSound`, `UnmuteSoundFile` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CancelUnitBuff`, `CastSpellByID`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetCritChance`, `GetRangedCritChance`, `GetSpellBaseCooldown`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsAttackSpell`, `IsAutoRepeatSpell`, `IsConsumableSpell`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.ContainsAnyDisenchantSpell`, `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetNumSpellBookSkillLines`, `C_SpellBook.GetPlayerSpellsByAura`, `C_SpellBook.GetSkillLineIndexByID`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemCastCount`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownDuration`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownInfo`, `C_SpellBook.GetSpellBookItemSkillLineIndex`, `C_SpellBook.GetSpellBookSkillLineInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsClassTalentSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftForm`, `GetShapeshiftFormID`, `GetSheathState`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsLoggingOut`, `IsMounted`, `IsOutdoors`, `IsPlayerInWorld`, `IsStealthed`, `IsSwimming` |
|
||||
| [SwingTimer](docs/API.md#swingtimer) | `C_SwingTimer.EnableRangeCheck`, `C_SwingTimer.IsTargetWithinSwingRange` |
|
||||
| [System](docs/API.md#system) | `CopyToClipboard`, `FlashClientIcon`, `GetPhysicalScreenSize` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetDirectionFinished`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer`, `InteractUnit`, `C_PlayerInteractionManager.InteractUnit`, `IsTargetLoose` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes`, `C_TaxiMap.GetTaxiPaths`, `C_TaxiMap.GetTaxiPathWaypoints`, `C_TaxiMap.GetTaxiRoute` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTimeCached` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTickTime`, `GetTimeCached` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
|
||||
| [Trainer](docs/API.md#trainer) | `GetTrainerServiceItemLink` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByAuraInstanceID`, `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
|
||||
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
|
||||
|
||||
@@ -117,9 +147,7 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
Registered on the **glue** Lua state (the engine that runs the login,
|
||||
realm-select, and character-select screens). The persistence entries
|
||||
in the first row are *glue-only* — they exist to support GlueXML
|
||||
patches that need a small persistence surface across sessions, since
|
||||
vanilla 1.12 glue ships no general-purpose persistence API beyond
|
||||
`GetSavedAccountName`/`SetSavedAccountName` (saturated by autologin).
|
||||
patches that need a small persistence surface across sessions.
|
||||
The rest of the table is in-game calls that we also mirror onto
|
||||
the glue state because GlueXML had no way to reach them otherwise.
|
||||
|
||||
@@ -127,7 +155,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
|-------|-------|
|
||||
| [Account](docs/API.md#account) | `SaveAccount`, `DeleteAccount`, `GetSavedAccounts`, `LoginWithSavedAccount` (passwords encrypted in Windows Credential Manager, scoped per realmlist; plaintext never returned to Lua) |
|
||||
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool`, `C_CVar.SetTempCVar`, `C_CVar.RemoveTempCVar` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsFirstLoadThisSession`, `C_Glue.IsOnGlueScreen` |
|
||||
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
|
||||
| State | `IsLoggedIn` |
|
||||
@@ -135,17 +163,82 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Macros</b> — engine-level parsing extensions</summary>
|
||||
<summary><b>Macros</b> — forms a macro body accepts</summary>
|
||||
|
||||
Engine-level extensions to macro parsing and dispatch — no new Lua
|
||||
functions, just behavior the stock 1.12 engine didn't have. See the
|
||||
[Macros section in the Lua reference](docs/API.md#macros) for details.
|
||||
The DLL parses these forms, so an action-bar addon needs no code for them.
|
||||
See the [Macros section in the Lua reference](docs/API.md#macros) for
|
||||
details.
|
||||
|
||||
| Form | What it does |
|
||||
|------|--------------|
|
||||
| `/cast <spellID>` | `/cast 5019` casts Shoot if known; macro slot tags correctly for action-bar UI |
|
||||
| `CastSpellByName("<spellID>")` | Same — numeric strings resolve through the engine's name resolver |
|
||||
| `CastSpellNoToggle("<name>")` in a macro | Engine's macro parser now recognizes it as a primary-spell line, so the macro slot in an addon like pfUI highlights when its spell is auto-repeating or its self-aura is active |
|
||||
| `#showtooltip [conditions] Value` | First line of a macro. Sets what the button shows. Bare `#showtooltip` follows the first `/cast` or `/use` line instead. `#show` is the same form. |
|
||||
| `[conditions]` and `@unit` | Accepted by 42 commands. The slash-command list below names them. |
|
||||
| `!Name` | Starts an ability but never turns it off. Use it for auto-repeat shots and for the self-buffs (stance, aspect, seal, form, tracking). |
|
||||
| A line that starts with `#` | A comment. The line never reaches chat. |
|
||||
| `/cast <spellID>` | For a spell you know, `/cast 5019` casts it by ID. The macro slot also tags correctly for an action-bar addon. |
|
||||
| `CastSpellByName("<spellID>")` | A numeric string resolves through the engine name resolver, the same as `/cast`. |
|
||||
| `CastSpellNoToggle("<name>")` in a macro | The engine macro parser reads this as a primary-spell line. The macro slot in an addon such as pfUI then highlights while the spell is on auto-repeat or its self-aura is active. |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Slash commands</b> — <code>[conditions]</code> on 42 commands</summary>
|
||||
|
||||
The bundled addon registers these commands. Each one takes the same
|
||||
`[conditions]` and `@unit` syntax as `/cast`. Each client language has its
|
||||
own command names next to the English ones. See
|
||||
[More commands with `[conditions]`](docs/API.md#more-commands-with-conditions)
|
||||
for what each command does, and
|
||||
[`/castsequence`](docs/API.md#castsequence) and
|
||||
[`/castrandom`](docs/API.md#castrandom-and-userandom) for the list forms.
|
||||
|
||||
| Group | Commands |
|
||||
|-------|----------|
|
||||
| Casting | `/cast`, `/use`, `/castsequence`, `/castrandom`, `/userandom`, `/stopcasting`, `/stopmacro`, `/cancelaura`, `/cancelform`, `/dismount` |
|
||||
| Targeting | `/target`, `/targetexact`, `/cleartarget`, `/targetlasttarget`, `/targetlastenemy`, `/targetenemy`, `/targetfriend`, `/targetenemyplayer`, `/targetfriendplayer`, `/targetparty`, `/targetraid`, `/assist`, `/follow`, `/focus`, `/clearfocus`, `/startattack`, `/stopattack` |
|
||||
| Equipment | `/equip`, `/equipslot`, `/equipset` |
|
||||
| Action bars | `/changeactionbar`, `/swapactionbar`, `/click` |
|
||||
| Pet | `/petattack`, `/petfollow`, `/petstay`, `/petpassive`, `/petdefensive`, `/petaggressive`, `/petautocaston`, `/petautocastoff`, `/petautocasttoggle` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>CVars</b> — new settings, set with <code>/console name value</code></summary>
|
||||
|
||||
Each setting is saved in `WTF\Config.wtf`. Read and write them with `GetCVar`
|
||||
/ `SetCVar` or `/console`. Click a group name for the full rules. To change
|
||||
the most common settings and the player cache in the game, open the game
|
||||
menu and click AddOn Options, then ClassicAPI.
|
||||
|
||||
| Group | CVar | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
|
||||
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
|
||||
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
|
||||
| [Profiling](docs/API.md#scriptprofile) | `scriptProfile` | `0` | `1` turns on CPU profiling for the `*CPUUsage` functions. The change starts after `/reload`. |
|
||||
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | `SoftTargetInteract` | `3` | Turns `softinteract` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
|
||||
| | `SoftTargetInteractArc` | `0` | Where the object can be: `0` straight ahead of the camera, `1` within 30° of it, `2` any direction. |
|
||||
| | `SoftTargetInteractRange` | `10` | Maximum distance in yards. |
|
||||
| | `SoftTargetInteractRangeIsHard` | `0` | `1` always applies the range. `0` also picks an object past it that you can use now. |
|
||||
| | `SoftTargetIconInteract` | `0` | `1` shows an icon over the soft-interact object. |
|
||||
| | `SoftTargetIconGameObject` | `0` | `1` also shows that icon over game objects. |
|
||||
| [Soft enemy / friend](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) | `SoftTargetEnemy` / `SoftTargetFriend` | `1` / `0` | Turns `softenemy` / `softfriend` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
|
||||
| | `SoftTargetEnemyArc` / `SoftTargetFriendArc` | `0` | The same values as `SoftTargetInteractArc`. |
|
||||
| | `SoftTargetEnemyRange` / `SoftTargetFriendRange` | `45` | Maximum distance in yards. |
|
||||
| | `SoftTargetMatchLocked` | `1` | `1` uses your target as the soft unit when it qualifies. `0` always uses the nearest unit. |
|
||||
| | `SoftTargetWithLocked` | `1` | `0` turns soft enemy and soft friend off while you have a locked target. |
|
||||
| | `SoftTargetForce` | `1` | A loose target follows `softenemy` (`1`) or `softfriend` (`2`). `0` keeps it. |
|
||||
| | `SoftTargetIconEnemy` / `SoftTargetIconFriend` | `0` | `1` shows an icon over `softenemy` / `softfriend`. |
|
||||
| | `SoftTargetLowPriorityIcons` | `0` | `1` also shows the normal pointer as an icon. |
|
||||
| | `SoftTargetNameplateSize` | `19` | Size of all soft-target icons. |
|
||||
| [Cooldown](docs/API.md#createframecooldown--name-parent-template) | `countdownForCooldowns` | `0` | `1` shows the time left as numbers on cooldown frames. |
|
||||
| [NamePlate](docs/API.md#nameplate) | `nameplateNotSelectedAlpha` | `0.5` | Alpha of a non-target default nameplate, `0` to `1`. |
|
||||
| | `nameplateSelectedAlpha` | `1` | Alpha of the target's default nameplate, `0` to `1`. |
|
||||
| [Text to speech](docs/API.md#tts-cvars) | `ttsVoice` | `0` | The voice that `C_VoiceChat.SpeakText` uses. |
|
||||
| | `ttsSpeed` | `0` | Speech rate, `-10` to `10`. |
|
||||
| | `ttsVolume` | `100` | Speech volume, `0` to `100`. |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -161,6 +254,10 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `ExportInterfaceFiles code` | Extracts Blizzard's UI source (`.lua`/`.xml`/`.toc`/`.xsd`) from the MPQs to `BlizzardInterfaceCode\` |
|
||||
| `ExportInterfaceFiles art` | Extracts Blizzard's UI art (`.blp`/`.tga`) from the MPQs to `BlizzardInterfaceArt\` |
|
||||
| `ExportDBCFiles` | Extracts the client's `.dbc` tables from the MPQs to `DBFilesClient\` |
|
||||
| `ExportSoundFiles [subpath]` | Extracts the client's sound files from the MPQs to `BlizzardSound\`; the optional subpath narrows it (the full tree is ~1 GB) |
|
||||
| `pitchLimit degrees` | Sets how far the camera can tilt up and down (default 89) |
|
||||
| `AppendLogToFile [filename] [numLines]` | Writes the console output to a text file (default `ConsoleLogs\Log<Timestamp>.txt`) |
|
||||
| Tab / Ctrl+Tab | Tab completes CVar names after `set`, `cvar_reset` and `cvar_default`; Ctrl+Tab lists every command that contains the typed text |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -171,6 +268,11 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|-------|---------|
|
||||
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
|
||||
| `BAG_UPDATE_DELAYED` | *(none)* |
|
||||
| `CHAT_MSG_*` | adds `lineID, guid` as `arg11, arg12` after the existing ten ([details](docs/API.md#chat_msg_-arguments-lineid-arg11-and-guid-arg12)) |
|
||||
| `COMBAT_LOG_EVENT` | `timestamp, subevent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...` ([details](docs/API.md#combat_log_event_unfiltered--combat_log_event-events)) |
|
||||
| `COMBAT_LOG_EVENT_UNFILTERED` | same as `COMBAT_LOG_EVENT` |
|
||||
| `CURSOR_CHANGED` | `isDefault, newCursorType, oldCursorType, oldCursorVirtualID` |
|
||||
| `DYNAMIC_GOSSIP_POI_UPDATED` | *(none)* |
|
||||
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
|
||||
| `EQUIPMENT_SWAP_PENDING` | `setID` |
|
||||
| `EQUIPMENT_SWAP_FINISHED` | `success, setID` |
|
||||
@@ -178,29 +280,43 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `GLOBAL_MOUSE_DOWN` | `button` |
|
||||
| `GLOBAL_MOUSE_UP` | `button` |
|
||||
| `HEARTHSTONE_BOUND` | *(none)* |
|
||||
| `INCOMING_RESURRECT_CHANGED` | `unitTarget` |
|
||||
| `INVENTORY_SEARCH_UPDATE` | *(none)* |
|
||||
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
|
||||
| `LEARNED_SPELL_IN_SKILL_LINE` | `spellID, skillLineIndex, isGuildPerkSpell` |
|
||||
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex, playerIndex` |
|
||||
| `LOOT_HISTORY_ROLL_COMPLETE` | `itemIndex` |
|
||||
| `LOOT_SCAN_COMPLETED` | *(none)* |
|
||||
| `LOSS_OF_CONTROL_ADDED` | `eventIndex` |
|
||||
| `LOSS_OF_CONTROL_UPDATE` | `unitToken` (always `"player"`) |
|
||||
| `MAP_EXPLORATION_UPDATED` | *(none)* |
|
||||
| `MODIFIER_STATE_CHANGED` | `keyName, down` |
|
||||
| `NAME_PLATE_CREATED` | `nameplateFrame` |
|
||||
| `NAME_PLATE_UNIT_ADDED` | `unitToken` ("nameplateN") |
|
||||
| `NAME_PLATE_UNIT_REMOVED` | `unitToken` ("nameplateN") |
|
||||
| `PLAYER_AVG_ITEM_LEVEL_READY` | *(none)* |
|
||||
| `PLAYER_EQUIPMENT_CHANGED` | `equipmentSlot, hasCurrent` |
|
||||
| `PLAYER_FOCUS_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_ENEMY_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_FRIEND_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_INTERACT_CHANGED` | `oldTarget, newTarget` |
|
||||
| `PLAYER_STARTED_LOOKING` | *(none)* |
|
||||
| `PLAYER_STOPPED_LOOKING` | *(none)* |
|
||||
| `PLAYER_STARTED_MOVING` | *(none)* |
|
||||
| `PLAYER_STOPPED_MOVING` | *(none)* |
|
||||
| `PLAYER_STARTED_TURNING` | *(none)* |
|
||||
| `PLAYER_STOPPED_TURNING` | *(none)* |
|
||||
| `PLAYER_SWING` | `swingDuration, swingType` |
|
||||
| `PLAYER_SWING_RANGE_UPDATE` | `swingType, isInRange, checksRange` |
|
||||
| `PLAYER_TOTEM_UPDATE` | `totemSlot` |
|
||||
| `QUEST_ACCEPTED` | `questLogIndex, questID` |
|
||||
| `QUEST_DATA_LOAD_RESULT` | `questID, success` |
|
||||
| `QUEST_QUERY_COMPLETE` | *(none)* (Turtle WoW only) |
|
||||
| `QUEST_REMOVED` | `questID` |
|
||||
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
|
||||
| `SELF_RES_SPELL_CHANGED` | *(none)* |
|
||||
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
|
||||
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
|
||||
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_START` | `unit, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_STOP` | `unit, castGUID, spellID, spellName, rank` |
|
||||
@@ -214,12 +330,17 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `UNIT_SPELLCAST_CHANNEL_STOP` | `unit, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_RETICLE_TARGET` | `"player", "", spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_RETICLE_CLEAR` | `"player", "", spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_INTERRUPTIBLE` | `unit` |
|
||||
| `UNIT_SPELLCAST_NOT_INTERRUPTIBLE` | `unit` |
|
||||
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
|
||||
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
|
||||
| `USER_WAYPOINT_UPDATED` | *(none)* |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_STARTED` | `numConsumers, utteranceID, durationMS, destination` |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FINISHED` | `numConsumers, utteranceID, destination` |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FAILED` | `status, utteranceID, destination` |
|
||||
| `VOICE_CHAT_TTS_VOICES_UPDATE` | *(none)* |
|
||||
| `WEAPON_SLOT_CHANGED` | *(none)* |
|
||||
| `WEAR_EQUIPMENT_SET` | `setID` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -228,7 +349,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|
||||
| Group | Constants |
|
||||
|-------|-----------|
|
||||
| Version | `CLASSIC_API_VERSION` |
|
||||
| Version | `CLASSIC_API_VERSION`, `INTERFACE_VERSION` |
|
||||
| Expansion | `LE_EXPANSION_LEVEL_CURRENT`, `LE_EXPANSION_CLASSIC` … `LE_EXPANSION_MIDNIGHT` |
|
||||
| Item quality | `LE_ITEM_QUALITY_POOR` … `LE_ITEM_QUALITY_WOWTOKEN` |
|
||||
| Unit stat | `LE_UNIT_STAT_STRENGTH` … `LE_UNIT_STAT_SPIRIT` |
|
||||
@@ -237,26 +358,31 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Inventory type | `Enum.InventoryType.Index*Type` (0–34, e.g. `IndexHeadType`=1 … `IndexRelicType`=28) |
|
||||
| Item class | `Enum.ItemClass.{Consumable,Container,Weapon,Gem,Armor,Reagent,Projectile,Tradegoods,ItemEnhancement,Recipe,Quiver,Questitem,Key,Miscellaneous,…}` (0–19) |
|
||||
| Item quality | `Enum.ItemQuality.{Poor,Common,Uncommon,Rare,Epic,Legendary,Artifact}` (0–6) |
|
||||
| Cursor type | `Enum.UICursorType.{Default,Item,Money,Spell,PetAction,Merchant,Macro,Pet,…}` (0–20; this client yields 0–5, 7 and 9) |
|
||||
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
|
||||
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
|
||||
| Player swing type | `Enum.PlayerSwingType.{MainHand,OffHand,Ranged}` (0–2) |
|
||||
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Unit tokens</b> — <code>nameplateN</code>, <code>focus</code>, <code>markN</code></summary>
|
||||
<summary><b>Unit tokens</b> — <code>nameplateN</code>, <code>focus</code>, <code>markN</code>, <code>softinteract</code>, <code>softenemy</code>, <code>softfriend</code>, <code>anyinteract</code>, <code>anyenemy</code>, <code>anyfriend</code></summary>
|
||||
|
||||
| Token | Resolves to |
|
||||
|-------|-------------|
|
||||
| `nameplate1`..`nameplateN` | Unit behind the Nth visible nameplate, in creation-order. Works with every `UnitX` function — `UnitName`, `UnitGUID`, `UnitClass`, `UnitHealth`, etc. Suffix chains (`nameplate1target`, `nameplate1targettarget`) compose. See [NamePlate / Unit tokens](docs/API.md#unit-tokens-nameplaten). |
|
||||
| `focus` / `focustarget` | Sticky target set via [`FocusUnit`](docs/API.md#focusunitunit), cleared via [`ClearFocus`](docs/API.md#clearfocus). Same `UnitX` coverage as `nameplateN`. Fires [`PLAYER_FOCUS_CHANGED`](docs/API.md#player_focus_changed-event) on transition. See [Focus](docs/API.md#focus). |
|
||||
| `mark1`..`mark8` | Unit currently wearing the Nth raid-target marker (`mark1` = star … `mark8` = skull). Same `UnitX` coverage as `nameplateN`, and fires unit events (`UNIT_HEALTH`, `UNIT_AURA`, …) with `arg1 == "markN"`. `UnitExists("markN")` is `false` when the marker is unset, out of range, or on a non-unit. Suffix chains (`mark1target`) compose. See [Unit tokens (`markN`)](docs/API.md#unit-tokens-markn). |
|
||||
| `softinteract` / `anyinteract` / `anyleftinteract` | The object that the Interact Key uses. `softinteract` is the nearest object in front of you that you can interact with. `anyinteract` is your target when you can interact with it, else `softinteract`. Both can be a game object or a corpse, which only `UnitGUID` reads. See [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract). |
|
||||
| `softenemy` / `softfriend` / `anyenemy` / `anyfriend` | The nearest enemy and the nearest friendly unit, or your target when it qualifies. `anyenemy` and `anyfriend` use your target first. See [Soft enemy and soft friend](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend). |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Bindings</b> — direct-action and override binding families</summary>
|
||||
|
||||
ClassicAPI backports the later-client direct-action and temporary override
|
||||
ClassicAPI backports the direct-action and temporary override
|
||||
binding families. Permanent bindings use the standard `SPELL`, `ITEM`,
|
||||
`MACRO`, and `CLICK` command strings and can be saved normally. Overrides are
|
||||
session-only, frame-owned, and support the usual priority flag.
|
||||
@@ -292,22 +418,30 @@ Download the prebuilt `ClassicAPI.dll` from the
|
||||
3. Add `ClassicAPI.dll` to `dlls.txt`.
|
||||
4. Launch the game with `VanillaFixes.exe`.
|
||||
|
||||
The bundled `!!!ClassicAPI` addon ships *inside* the DLL — no separate
|
||||
addon download or install step needed.
|
||||
The `!!!ClassicAPI` and `ClassicAPI_DebugTools` addons are *inside* the
|
||||
DLL. You do not download or install them. The `src/embed/AddOns/` folder
|
||||
in this repository is source code for the DLL. Do not copy it into your
|
||||
game.
|
||||
|
||||
## Bundled addon: !!!ClassicAPI
|
||||
|
||||
The Lua-side companion library lives in
|
||||
[`AddOns/!!!ClassicAPI/`](AddOns/!!!ClassicAPI/). It's a 1.12.1 /
|
||||
[`src/embed/AddOns/!!!ClassicAPI/`](src/embed/AddOns/!!!ClassicAPI/). It's a 1.12.1 /
|
||||
Lua 5.0 backport of the modern Blizzard helpers that aren't engine
|
||||
functions but that consumer code still expects to find as globals —
|
||||
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
|
||||
`Item` / `ItemLocation`, `MathUtil`(`Lerp` / `Clamp` / `CreateCounter`),
|
||||
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), and `EventUtil`
|
||||
(`ContinueOnAddOnLoaded` etc.).
|
||||
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), `EventUtil`
|
||||
(`ContinueOnAddOnLoaded` and more), `DataProvider`, `CircularBuffer`,
|
||||
`EasingUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
|
||||
`GridLayoutUtil`, and the layout frame templates (`VerticalLayoutFrame`,
|
||||
`HorizontalLayoutFrame`, `ResizeLayoutFrame`, `GridLayoutFrame`,
|
||||
`StaticGridLayoutFrame`). It also holds the conditional slash
|
||||
commands and `SecureCmdOptionParse`, the parser that reads `[conditions]`
|
||||
and `@unit` for them.
|
||||
|
||||
**You don't have to install the addon manually** — the DLL embeds
|
||||
the contents of [`AddOns/!!!ClassicAPI/`](AddOns/!!!ClassicAPI/) and
|
||||
the contents of [`src/embed/AddOns/!!!ClassicAPI/`](src/embed/AddOns/!!!ClassicAPI/) and
|
||||
registers them with the engine as a synthetic addon on startup, so
|
||||
the library is always available when the DLL is loaded. It fires
|
||||
`ADDON_LOADED`, supports `SavedVariables`, and is resolvable by name —
|
||||
@@ -324,45 +458,29 @@ can't be switched off. Being hidden means it's omitted from the
|
||||
index-based `GetNumAddOns()` / `GetAddOnInfo(i)` enumeration, but every
|
||||
by-name lookup and dependency reference still resolves normally.
|
||||
|
||||
If you drop the folder into your `Interface/AddOns/` directory, **that
|
||||
copy's files win** — the DLL's embedded version only serves files when
|
||||
the engine's normal scan doesn't already have a newer entry under that
|
||||
name. Useful for editing the Lua locally without rebuilding the DLL.
|
||||
The hidden + force-enabled treatment applies either way (it's keyed to
|
||||
the addon name, not to which copy serves the files), so the local copy
|
||||
is also absent from the AddOns list. The dispatch is transparent:
|
||||
addons consuming `Mixin`, `ColorMixin`, `TableUtil`, etc. behave
|
||||
identically in both cases.
|
||||
For development, you can put a copy of the addon folder in
|
||||
`Interface/AddOns/` and add an empty `.classicapi-dev` file to it. The
|
||||
game then reads the Lua files from that copy, so you can edit them
|
||||
without a DLL rebuild. Without this file, the game uses the copy with the
|
||||
newer `## Version:`. The copy is hidden and always enabled, the same as
|
||||
the embedded addon.
|
||||
|
||||
## Bundled addon: DebugTools
|
||||
## Debug tools
|
||||
|
||||
A 1.12.1 / Lua 5.0 backport of Blizzard's `Blizzard_DebugTools` addon
|
||||
(originally shipped with the 3.0 client) lives in [`AddOns/DebugTools/`](AddOns/DebugTools/).
|
||||
It's an independent addon — it doesn't use anything ClassicAPI adds, and
|
||||
ClassicAPI works fine without it. It's bundled here because it's the
|
||||
natural companion for testing and debugging anything written against the
|
||||
1.12 Lua surface (with or without the ClassicAPI extensions).
|
||||
ClassicAPI contains a backport of Blizzard's `Blizzard_DebugTools` addon,
|
||||
with the name `ClassicAPI_DebugTools`. The DLL contains this addon, so you do
|
||||
not install it. The addon is not in the AddOns list, and you cannot disable it.
|
||||
|
||||
Slash commands provided:
|
||||
The slash commands are always available. The tools load the first time
|
||||
that you use one of these commands:
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/dump <expr>` | Pretty-print any Lua value, including tables and multi-return tuples. The right tool for inspecting return values from `GetSpellInfo`, `C_Item.GetItemInfoInstant`, etc. |
|
||||
| `/etrace` | Event tracer window. `/etrace start`, `/etrace stop`, `/etrace add EVENT`, etc. |
|
||||
| `/framestack` (or `/fstack`) | Tooltip showing the frame hierarchy under the mouse cursor. |
|
||||
| `/luaerrors` (or `/scripterrors`) | Lua error display window. |
|
||||
|
||||
Lua globals provided (backports of 3.3.5 helpers that don't exist in
|
||||
1.12):
|
||||
|
||||
| Global | Purpose |
|
||||
|--------|---------|
|
||||
| `print(...)` | Backport of the 3.3.5 `print` — concats varargs with `" "` and pushes to `DEFAULT_CHAT_FRAME`. Routes through `setprinthandler`'s handler; falls back via `geterrorhandler` if the handler errors. |
|
||||
| `setprinthandler(func)` / `getprinthandler()` | Install / query a custom print handler. Useful for redirecting print output in tests. |
|
||||
| `tostringall(...)` | Apply `tostring()` to every vararg, preserving the count. Lua 5.0-compatible (uses `arg.n` since `select` doesn't exist in 5.0). |
|
||||
|
||||
To install: copy [`AddOns/DebugTools/`](AddOns/DebugTools/) into your
|
||||
`Interface/AddOns/` directory like any other addon.
|
||||
| `/dump <expr>` | Shows a Lua value in the chat frame. It shows tables and all return values. Use it to examine the return values of `GetSpellInfo`, `C_Item.GetItemInfoInstant`, and other functions. |
|
||||
| `/etrace` | Opens the event trace window. Other forms: `/etrace start`, `/etrace stop`, `/etrace add EVENT`, and more. |
|
||||
| `/framestack [showHidden [showRegions]]` (or `/fstack`) | Shows a tooltip with a list of the frames under the cursor. A `-->` mark shows one frame, with its anchors and the file and line that made it. Left Alt and Right Alt move the mark. Ctrl opens the marked frame in the table inspector. Shift shows its texture files. |
|
||||
| `/tinspect [expr]` (or `/tins`) | Opens a table inspector on the result of `expr`. With no table, it opens on the frame under the cursor. The window shows the anchors and the fields of the table, grouped by type. You can edit numbers, strings and booleans, filter the lines, and open a child table or frame. |
|
||||
| `/luaerrors` (or `/scripterrors`) | Opens the Lua error window. |
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
@@ -5,23 +5,25 @@ Candidates for new DLL functions, prioritized. All are things the
|
||||
addon polyfills badly, hackily, or not at all, and where engine access is the
|
||||
right answer.
|
||||
|
||||
## ~~1. `C_QuestLog.IsQuestFlaggedCompleted(questID)`~~ — NOT FEASIBLE in 1.12
|
||||
## ~~1. `C_QuestLog.IsQuestFlaggedCompleted(questID)`~~ — DONE (Turtle-lineage servers)
|
||||
|
||||
The original TODO note ("engine maintains a completed-quests bitfield in
|
||||
client memory") was based on 3.3.5 / WotLK semantics. Vanilla 1.12 has no
|
||||
equivalent: neither `QueryQuestsCompleted` nor `GetQuestsCompleted`
|
||||
exists, the protocol has no `CMSG_QUERY_QUESTS_COMPLETED`, and the
|
||||
client doesn't track historical completions. The polyfill at
|
||||
`Util/C_QuestLog.lua:5-8` works on Frostmourne (3.3.5a) where those
|
||||
functions exist; on the 1.12 Octo client they're absent.
|
||||
Stock 1.12 has no completed-quests query and keeps no quest history, so
|
||||
this exists only where the server offers the list another way. Turtle WoW
|
||||
and its forks answer the player command `.queststatuses` with `TWQUEST`
|
||||
guild addon messages from the player, split at 2000 characters, the last
|
||||
message always shorter (probed: 2003, 2004, 0) - an exact end marker.
|
||||
|
||||
Servers that want to expose this in 1.12 (notably Turtle WoW) do it
|
||||
via the `CHAT_MSG_ADDON` channel — the addon sends `.queststatus` over
|
||||
guild/whisper chat and the server replies with addon-channel messages
|
||||
the addon parses. Fundamentally async, server-specific, and requires
|
||||
SavedVariables-style persistence across sessions to amortize the
|
||||
round-trip — all of which belong on the Lua side, not in this DLL.
|
||||
There's nothing the DLL can usefully add here.
|
||||
[src/quest/Completed.cpp](src/quest/Completed.cpp) does it all in the DLL,
|
||||
Turtle clients only: it sends the request itself (on
|
||||
SMSG_LOGIN_VERIFY_WORLD, after each turn-in, and from
|
||||
`QueryQuestsCompleted`), reads every answer at the packet funnel (sender
|
||||
must be the player), and keeps the live list behind `GetQuestsCompleted`,
|
||||
`C_QuestLog.IsQuestFlaggedCompleted` and
|
||||
`C_QuestLog.GetAllCompletedQuestIDs`; `QUEST_QUERY_COMPLETE` fires per
|
||||
answer. No disk cache: the first answer lands one round trip after
|
||||
PLAYER_ENTERING_WORLD, because the server sends the player object (which
|
||||
fires PLAYER_LOGIN) before it reads any request. Replaces the closed Lua
|
||||
collector of PR #86.
|
||||
|
||||
## ~~2. `C_Spell.GetSpellDescription(spellID)`~~ — DONE
|
||||
|
||||
@@ -455,8 +457,7 @@ the other spell registrations in [src/spell/Info.cpp](src/spell/Info.cpp).
|
||||
## ~~26. `C_Item.IsEquippableItem(item)`~~ — DONE
|
||||
|
||||
Cache-record read of `m_inventoryType` at `OFF_ITEMSTATS_INVENTORY_TYPE`.
|
||||
Non-zero = equippable. Lives in `item/Equipment.cpp` alongside
|
||||
`OffhandHasWeapon` (which uses the same field) and `IsEquippedItem`.
|
||||
Non-zero = equippable. Lives in `item/Equipment.cpp` alongside `IsEquippedItem`.
|
||||
Synchronous; returns `false` on cache miss without firing a load.
|
||||
Namespaced under `C_Item` to match Classic Era 1.15.x.
|
||||
|
||||
@@ -951,88 +952,15 @@ See [src/unit/Tooltip.cpp](src/unit/Tooltip.cpp) and the
|
||||
`FUN_SCRIPT_GAMETOOLTIP_SET_UNIT_*` block in
|
||||
[src/Offsets.h](src/Offsets.h).
|
||||
|
||||
## 46. `GameTooltip:SetFrameStack([showHidden])` — deferred (Lua-side fix in place)
|
||||
## ~~46. `GameTooltip:SetFrameStack([showHidden])`~~ — DONE
|
||||
|
||||
Renders a tooltip listing the chain of frames at the mouse cursor,
|
||||
sorted by strata + level. The 3.0+ debug-tooling primitive used by
|
||||
Blizzard's `Blizzard_DebugTools` and every layout-debugging addon.
|
||||
|
||||
### Current state in this project
|
||||
|
||||
Our bundled `DebugTools/` backport ships its own `FrameStackTooltip`
|
||||
in [DebugTools/DebugTools.lua](DebugTools/DebugTools.lua), driven
|
||||
by `_CollectFramesUnderMouse`. As of the EnumerateFrames switch,
|
||||
the iterator walks the engine's own frame list — fast (typically
|
||||
~hundreds of entries) and includes anonymous frames the engine
|
||||
sees but a `_G` walk would miss.
|
||||
|
||||
What's still missing relative to the real engine `SetFrameStack`:
|
||||
|
||||
- **Anonymous-frame display name.** We currently show
|
||||
`<anonymous>` for frames where `frame:GetName()` returns nil.
|
||||
The engine's method shows the frame's pointer + the Lua-side
|
||||
registry-table address. To get there we'd need a Lua-callable
|
||||
shim that exposes a numeric handle per frame (vftable + Lua-ref
|
||||
lookup), which is non-trivial.
|
||||
- **Single-tooltip semantics.** Modern addons can call
|
||||
`GameTooltip:SetFrameStack(...)` directly on any tooltip;
|
||||
ours requires the `FrameStackTooltip` we ship in `DebugTools/`.
|
||||
Most consumers (`Blizzard_DebugTools` ports, layout addons via
|
||||
`/framestack`) don't care, but a strict polyfill would.
|
||||
|
||||
### C++ port from 3.3.5 — kept as a research note
|
||||
|
||||
What `Script_GameTooltip_SetFrameStack`
|
||||
actually does in 3.3.5 (Frostmourne client, registration entry at
|
||||
VA 0x00AD2CF0 → function VA 0x00626560 → inner builder
|
||||
`FUN_006230d0`):
|
||||
|
||||
- Pulls cursor (in frame space) via `FUN_0047BFF0` from a render-
|
||||
state global at `[DAT_00B499A8 + 0x1224 / +0x1228]`.
|
||||
- Walks an intrusive linked list at `[DAT_00B499A8 + 0xCD4]`. Each
|
||||
frame node reads:
|
||||
- `[+0x00]` — vftable; `vftable[+0x10]` = "is the frame visible
|
||||
enough to hit-test" predicate.
|
||||
- `[+0x0E0]` (`node[0x38]`) = strata; gates `(strata != 0 ||
|
||||
showHidden)`.
|
||||
- `[+0x0D0]..[+0x0DC]` (`node[0x34..0x37]`) = frame rect for
|
||||
hit-test.
|
||||
- `[+0x0D8]` (`node[0x36]`) flags, bit 2 → visible.
|
||||
- `[+0x288]` (`node[0xA2]`) = next pointer.
|
||||
- Per-frame name via `vftable[+0x04]` (a `GetDebugName()`-style
|
||||
virtual). If the frame is Lua-side (`vftable[+0x10]` returns
|
||||
type code 5), an extra `lua_rawgeti(L, REGISTRY, frame->luaRef)`
|
||||
pulls the Lua-table reference for the `"%s: %p (%s)"` format;
|
||||
otherwise it's `"%s: %p"`.
|
||||
- Builds into a scratch sort array at `DAT_00C5D370` (entry stride
|
||||
`0x410`), `qsort` against comparator `FUN_0061A5B0`.
|
||||
- Header line via `FUN_00819D40("DEBUG_FRAMESTACK", ...)` (Lua
|
||||
`GetGlobalString`) then per-entry `FUN_0061FEC0` (tooltip
|
||||
`AddLine` equivalent, takes color codes from a 4-byte-stride
|
||||
table at `0xAD2DC0` / `0xAD2DB8` / `0xAD2DB0` for
|
||||
visible/hidden/special states).
|
||||
|
||||
For a 1.12 port we'd need to derive:
|
||||
|
||||
- The frame-manager global (3.3.5's `DAT_00B499A8` — different VA
|
||||
in 1.12, different field offsets likely).
|
||||
- Frame struct offsets for rect, strata, flags, next, name vftable
|
||||
slot — the layout shifted between 1.12 and 3.3.5 (same way unit
|
||||
field offsets did, per the `OFF_UNIT_FIELD_HEALTH` discovery in
|
||||
CLAUDE.md).
|
||||
- AddTooltipLine engine address (1.12 has it — every
|
||||
`GameTooltip:SetX` method calls it internally).
|
||||
- Lua-side reference lookup helper (whatever 1.12 uses to map
|
||||
frame → Lua table; the `FrameScript_GetObject` chain at
|
||||
`0x6F3740` is part of this).
|
||||
|
||||
### Recommendation
|
||||
|
||||
The Lua-side iterator switch is done. The C++ port stays deferred
|
||||
unless we need the engine frame list for something else
|
||||
(`GetMouseFocus` polyfill, anonymous-frame inspector with extra
|
||||
fields beyond what Lua exposes, single-tooltip strict polyfill,
|
||||
etc.) that would amortize the RE cost.
|
||||
A mirror of 4.3.4's CGTooltip::SetFrameStack in
|
||||
[src/tooltip/FrameStack.cpp](src/tooltip/FrameStack.cpp), plus the
|
||||
`OnTooltipSetFrameStack` script in Tooltip::SetEvents. Every input exists in
|
||||
1.12: the CSimpleTop frame list, the NDC cursor + NDCToDDC, CLayoutFrame
|
||||
GetRect, the frame's visible / enabled-input / strata / level fields,
|
||||
FrameStrataToString and the two default tooltip colors (all in Offsets.h).
|
||||
The `/fstack` UI itself is addon Lua and can now call the method.
|
||||
|
||||
## ~~47. `IsPlayerSpell(spellID)`~~ — DONE
|
||||
|
||||
@@ -1120,51 +1048,50 @@ spell-API functions use. Returns `(1, nil)` / `(nil, 1)` /
|
||||
`(nil, nil)` per legacy convention. `C_Spell.IsSpellUsable` is the
|
||||
modern shape — same logic, returns proper booleans.
|
||||
|
||||
**Two engine-cache approaches investigated and discarded:**
|
||||
**Reworked for issue #51 (2026-09-12) — it now IS the engine's
|
||||
verdict.** `IsUsableAction` reads the per-slot cache
|
||||
(`0x00BC6B60` usable / `0x00BC67A0` noMana, reader `FUN_004E5230`),
|
||||
which the recompute `FUN_004E5050` fills per slot. For a player spell
|
||||
slot that recompute is one call: `FUN_SPELL_IS_USABLE(record,
|
||||
&noMana)` (`0x006E3D60`). For a pet spell slot it is `FUN_PET_ACTIONS_
|
||||
USABLE()` then the pet's power vs `FUN_GET_SPELL_COST(spellID, 0)`.
|
||||
`Spell::Usable` now runs exactly those two paths live (plus a
|
||||
knowledge gate on `VAR_PLAYER_SPELL_BITMAP`, since the helper is also
|
||||
fed item on-use spells and never checks knowledge). Verified check
|
||||
list of the helper is on `FUN_SPELL_IS_USABLE` in `Offsets.h`.
|
||||
|
||||
**The first version hand-rolled five checks instead** (known, alive,
|
||||
off cooldown, power, reagents) and diverged from `IsUsableAction`
|
||||
exactly where the helper does more or less — the issue #51 repro
|
||||
(Warrior, Whirlwind):
|
||||
|
||||
| State | `IsUsableAction` | old `IsUsableSpell` |
|
||||
|---|---|---|
|
||||
| Berserker, 0 rage, on CD | `nil, 1` | `nil, nil` (we tested cooldown first — the engine never does) |
|
||||
| Battle Stance, 25 rage | `nil, nil` | `1, nil` (we never tested stance) |
|
||||
|
||||
Cooldown is deliberately NOT part of usability: FrameXML greys
|
||||
(`IsUsableAction`) and swipes (`GetActionCooldown`) as two separate
|
||||
states, and the engine helper has no cooldown branch. Rule #1 case:
|
||||
`Item::Usable` had been calling the helper all along.
|
||||
|
||||
**Two engine-cache approaches investigated and discarded before the
|
||||
first version** (still true, kept for the record):
|
||||
|
||||
1. **Per-spell cache at `0x004F0F40` returning fields at +0x564
|
||||
/ +0x568** — initially looked promising (the action-bar
|
||||
dispatcher at `0x004E5BA0` reads these), but the writers
|
||||
(`0x004EFF17` cluster) showed they're parsed config integers,
|
||||
not real-time usability bools. False trail.
|
||||
not real-time usability bools. False trail. (Later identified as
|
||||
the macro entry's cached primary spell — see the `#showtooltip`
|
||||
notes.)
|
||||
2. **Action-bar per-slot caches at `0x00BC67A0` (noMana) and
|
||||
`0x00BC6B60` (usable)** — these ARE real-time, but only updated
|
||||
for the 120 action-bar slots. Spells off the bar stay
|
||||
uninitialized. Action-bar-only is the wrong abstraction since
|
||||
modern `IsUsableSpell(spellID)` is action-bar-agnostic.
|
||||
|
||||
Settled on a **manual five-check approach**:
|
||||
1. Player knows the spell (`VAR_PLAYER_SPELL_BITMAP` lookup —
|
||||
covers profession recipes, talents, racials, etc.).
|
||||
2. Player is alive (`HEALTH > 0`).
|
||||
3. Spell is not on cooldown (`FUN_SPELL_QUERY_COOLDOWN` —
|
||||
the engine helper `Script_GetSpellCooldown` calls internally
|
||||
after slot resolution, queried with `bookType=0` for player).
|
||||
4. Player has ≥ `ManaCost` of the spell's `PowerType` —
|
||||
only this failure flips `noMana=true`.
|
||||
5. Player has all required reagents in bags
|
||||
(`Spell.dbc` Reagent[8] / ReagentCount[8] at `+0x110` /
|
||||
`+0x130`, walked via `Item::Location::ResolveBag`).
|
||||
|
||||
**Not checked** (deliberate, different concerns): silence, GCD,
|
||||
stance/form, range, target type, line-of-sight, casting state.
|
||||
|
||||
**Verified on Turtle WoW:**
|
||||
- Mana check: Renew rank 3 (cost 105) reports usable at 144 mana
|
||||
and unusable at 39 mana, transitioning exactly at the cost
|
||||
boundary.
|
||||
- Reagent check: SHIPPED BUT UNVERIFIED — the Reagent[8] /
|
||||
ReagentCount[8] offsets at `+0x110` / `+0x130` are the
|
||||
CMaNGOS-documented vanilla layout, and we know spell records
|
||||
match CMaNGOS docs (PowerType=+0x7C and ManaCost=+0x80 both
|
||||
match), so high confidence — but no in-game verification yet.
|
||||
Should test with a reagent spell (e.g., Mage Teleport spell 3565
|
||||
needs Rune of Teleportation 17031). Failure mode if offsets are
|
||||
wrong: reagent check becomes a no-op (always passes).
|
||||
- Cooldown check: SHIPPED BUT UNVERIFIED in this implementation —
|
||||
the helper signature was traced via `Script_GetSpellCooldown` at
|
||||
`0x004B40A0` but not exercised in-game. Should test with a
|
||||
cooldown'd spell.
|
||||
modern `IsUsableSpell(spellID)` is action-bar-agnostic. The fix
|
||||
above sidesteps this by calling what FILLS the cache, not the
|
||||
cache.
|
||||
|
||||
**Important offset correction along the way.** Initial implementation
|
||||
read POWER1 at descriptor `+0x5C` based on the CMaNGOS-documented
|
||||
@@ -1539,20 +1466,12 @@ it on a struct pointer to scan layouts, or from Lua via
|
||||
`_classicapi_HexDump(label, addr, len)` to inspect engine
|
||||
globals without a per-investigation probe module.
|
||||
|
||||
## ~~56. `OffhandHasWeapon()`~~ — DONE
|
||||
## ~~56. `OffhandHasWeapon()`~~ — DROPPED
|
||||
|
||||
Shipped as `src/item/Equipment.cpp`. Resolves the off-hand
|
||||
equipment slot (slot 17) via `Item::Location::ResolveEquipmentSlot`,
|
||||
reads the cached `ItemStats_C` record's `m_inventoryType` at
|
||||
`+0x2C`, and returns true iff it's `INVTYPE_WEAPON` (13) or
|
||||
`INVTYPE_WEAPONOFFHAND` (22). Shields, holdables (tomes/orbs),
|
||||
empty slots, and uncached items all return false.
|
||||
|
||||
No async load is fired — modern API behavior matches; if the
|
||||
off-hand item isn't cached the function silently returns false
|
||||
until something else warms the cache. Typically only matters on
|
||||
first login before the player's own gear is in the cache, which
|
||||
in practice is loaded eagerly anyway.
|
||||
Vanilla already ships it (`0x004C9570`, same body as 3.3.5/4.3.4:
|
||||
off-hand slot item class 2 → `1`, else `nil`). Our override returned
|
||||
booleans and tested inventory type instead; removed so the engine's
|
||||
own global stands.
|
||||
|
||||
## 57. `IsFlying()` — TESTED, DROPPED
|
||||
|
||||
@@ -1741,7 +1660,14 @@ is a thin wrapper. If they're inlined, we'd either need to skip
|
||||
the clear ourselves (call after some no-op state setup) or
|
||||
manually call the line-emission primitives.
|
||||
|
||||
## ~~61. `GameTooltip:GetItem()` / `GetSpell()` / `GetUnit()`~~ — `GetItem` + `GetSpell` DONE; `GetUnit` skipped
|
||||
## ~~61. `GameTooltip:GetItem()` / `GetSpell()` / `GetUnit()`~~ — DONE
|
||||
|
||||
`GetUnit` shipped later in [src/unit/Tooltip.cpp](src/unit/Tooltip.cpp):
|
||||
`SetUnit` is re-registered in front of the engine's (no MinHook) to
|
||||
stage the token, and the token binds in the unit-builder co-hook that
|
||||
`Tooltip::SetEvents` already owned. The builder has three callers:
|
||||
SetUnit, the world-mouseover setter (→ `"mouseover"`), and an in-place
|
||||
refresh (keeps the token). The notes below are the original skip.
|
||||
|
||||
Modern query methods that return what the tooltip is currently
|
||||
showing:
|
||||
@@ -1847,10 +1773,17 @@ are supported.
|
||||
|
||||
### Unhookable globals
|
||||
|
||||
Modern WoW rejects `hooksecurefunc("<core lua/secure fn>", ...)` outright
|
||||
to keep callers from clobbering language primitives or breaking taint
|
||||
propagation. We mirror that with a name blacklist in `Script_HookSecureFunc`
|
||||
that fires `lua_error` with `"hooksecurefunc: function is unhookable"`
|
||||
Patch 11.0.0 (The War Within, 2024-07-23) made `hooksecurefunc` refuse a
|
||||
fixed set of names with a "Cannot hook function" error, to keep callers
|
||||
from clobbering language primitives or breaking taint propagation. No
|
||||
earlier client has the restriction (3.3.5, 4.3.4 and 5.4.8 carry no such
|
||||
string), so 11.0's published list is the reference — and it is NOT the
|
||||
engine's base-library table (`0x00811E28`, 36 entries, catalogued in
|
||||
`BlizzardScriptAPI.md` §3): 11.0 leaves `tostring`/`error`/`loadstring`
|
||||
hookable and includes secure-family names that are not base functions. We
|
||||
mirror the 11.0 list verbatim with a name blacklist in
|
||||
`Script_HookSecureFunc` that fires `lua_error` with
|
||||
`"hooksecurefunc: function is unhookable"`
|
||||
when the two-arg form targets one of: `getfenv`, `getmetatable`,
|
||||
`hooksecurefunc`, `ipairs`, `issecurevalue`, `issecurevariable`, `next`,
|
||||
`pairs`, `pcall`, `pcallwithenv`, `rawget`, `rawset`, `scrub`,
|
||||
@@ -2012,8 +1945,8 @@ current item/spell/unit. Approach:
|
||||
4. Each match reveals the offset for that state field.
|
||||
|
||||
Once located, `GetItem()`, `GetSpell()`, `GetUnit()` are trivial
|
||||
deref+push. Implementation roughly the same as `OffhandHasWeapon`
|
||||
or `IsEquippedItem` patterns.
|
||||
deref+push. Implementation roughly the same as the `IsEquippedItem`
|
||||
pattern.
|
||||
|
||||
### Risks
|
||||
|
||||
@@ -3827,9 +3760,10 @@ private namespace table **only** when BOTH hold, else `nil`:
|
||||
boolean flag; approach (a) from the notes below — no parser hook).
|
||||
|
||||
The two paths stay separate, as required:
|
||||
- **Internal** `__addonns` (the `local name, ns = ...` preamble) stays
|
||||
**ungated** — an addon always gets its own namespace. Still in
|
||||
`luasyntax/Transpile.cpp`.
|
||||
- **Internal**: each addon file is called with `(name, ns)` as real
|
||||
arguments, as 4.3.4 does (`Script_RunAddonFile` in
|
||||
`luasyntax/Transpile.cpp`) — **ungated**, an addon always gets its own
|
||||
namespace. (Replaced the `__addonns` global preamble.)
|
||||
- **Public** `C_AddOns.GetAddOnLocalTable` carries the gate, in
|
||||
`addons/LocalTable.cpp`.
|
||||
|
||||
@@ -3854,3 +3788,350 @@ Not shipped by default: no addon opts in yet. The embedded `!!!ClassicAPI`
|
||||
addon could add `## AllowAddOnTableAccess: 1` to its `.toc` to expose its own
|
||||
namespace as a worked example, but that's a product/security decision left to
|
||||
the maintainer.
|
||||
|
||||
## 100. Self-documenting API + `/classicapi` browser — SHIPPED (first slice)
|
||||
|
||||
`docs/API.md` is 18.7k lines and its anchors are signature slugs, so a
|
||||
signature edit breaks the 281 internal + 88 README links into it. Blizzard
|
||||
solved the same problem by shipping the API as DATA
|
||||
(`Blizzard_APIDocumentationGenerated`: one table per system with typed
|
||||
`Functions` / `Events` / `Tables`) plus a browser addon
|
||||
(`Blizzard_APIDocumentation`, the `/api` command). warcraft.wiki.gg's
|
||||
Arguments/Returns blocks are those same tables rendered by a bot — so one set
|
||||
of descriptors feeds the in-game browser, a page generator, and a drift check.
|
||||
|
||||
**Shipped in this round**
|
||||
|
||||
- `namespace Game::Doc` in [src/Game.h](src/Game.h) — `Field` +
|
||||
`Req`/`Opt`/`Vararg`, `FieldList`, `Function`, `Method`, `Structure`,
|
||||
`Event`. Constant-initialized `const` objects; no heap, no dynamic init.
|
||||
- A trailing descriptor argument on all six `Game::Lua::Register*`, and on
|
||||
`Event::Custom::AutoReserve`. Zero changes inside any `Script_*`.
|
||||
- [src/api/Documentation.cpp](src/api/Documentation.cpp) — records every
|
||||
registration on the first pass of each Lua state, derives systems, and
|
||||
exports `C_APIDocumentation.GetSystems()` / `GetSystem(name)` lazily in
|
||||
Blizzard's exact table shape, plus `_classicapi_UndocumentedAPI()`.
|
||||
- `src/embed/AddOns/!!!ClassicAPI/APIDocumentation/` — the 8-file port of
|
||||
`Blizzard_APIDocumentation`, driven by `/classicapi` (short `/capi`).
|
||||
Named for what it documents: the browser holds only ClassicAPI's own
|
||||
surface, so `/api` would have over-promised.
|
||||
- `Util/LinkUtil.lua` — the `SetItemRef` → `LinkUtil.ProcessLink` dispatch,
|
||||
wiring up a `RegisterLinkHandler` registry that had been dead code. Any
|
||||
future link type now registers a handler instead of stacking another
|
||||
`SetItemRef` override. Also fixed `assertsafe` being undefined there.
|
||||
- Documented: the whole `C_Spell` / `C_SpellBook` surface, the spell globals
|
||||
(`SpellGlobals`), the two SpellBook enums, the 13 `UNIT_SPELLCAST_*`
|
||||
events, `GameTooltipAPI` (frame-method example), and `C_Glue` (glue and
|
||||
mixed-environment example).
|
||||
|
||||
**The remaining sweep.** `_classicapi_UndocumentedAPI()` is the worklist —
|
||||
~700 registrations across ~46 namespaces. Do it a namespace at a time; the
|
||||
descriptor goes beside the registration and must be written against the
|
||||
`Script_*` BODY, not against `docs/API.md` (which is already stale in
|
||||
places — `IsHarmfulSpell` / `IsHelpfulSpell` describe an `AttributesEx` bit
|
||||
read that the code does not do; it walks the effects' implicit targets).
|
||||
`tools/New-ApiDocSkeletons.ps1` emits paste-ready skeletons from the 207
|
||||
single-line `Usage:` strings + the API.md headings, with `// TODO` on every
|
||||
guess — a starting point to verify, never to paste blind.
|
||||
|
||||
When the sweep finishes, delete the `= nullptr` / `= 0` defaults from the six
|
||||
registrar parameters: a registration without a descriptor then fails to
|
||||
compile, which is the whole point of putting the descriptor at the call site.
|
||||
|
||||
**Then, not before:** an `ExportAPIDocumentation` console command (glue-
|
||||
registered like `ExportInterfaceFiles`, writing one Blizzard-format `.lua`
|
||||
per system plus `Undocumented.txt`), a CI diff of the committed copies to
|
||||
catch descriptor drift, and a Markdown/wiki generator to replace the
|
||||
hand-maintained `docs/API.md` — the reason the descriptors exist at all.
|
||||
|
||||
## 101. `Frame:SetOnUpdateMode(mode)` + `Enum.OnUpdateMode` — medium
|
||||
|
||||
```
|
||||
Frame:SetOnUpdateMode(onUpdateMode) -- Enum.OnUpdateMode
|
||||
0 Disabled no OnUpdate, visible or not
|
||||
1 RunWhenVisible the vanilla behaviour (default)
|
||||
2 RunWhenVisibleOnce run once while visible; resets to Disabled BEFORE running
|
||||
3 RunOnce run once regardless of visibility; resets to Disabled BEFORE running
|
||||
4 RunAlways run regardless of visibility
|
||||
```
|
||||
|
||||
"Resets before running" is load-bearing: the handler re-arms by calling
|
||||
`SetOnUpdateMode` again from inside itself.
|
||||
|
||||
**Engine (verified 2026-09-15).** The per-frame dispatcher is `FUN_0076B2C0`,
|
||||
`__thiscall(frame, float elapsed)` at vtable `+0x38`, present in every frame
|
||||
vtable with subclass overrides chaining into it (same shape as the click
|
||||
vmethod `FUN_00779540`):
|
||||
|
||||
```c
|
||||
if (frame[0x128] != 0) // the OnUpdate slot
|
||||
FUN_007026F0(frame, frame + 0x128, "%f", elapsed); // fmt @0x00835160
|
||||
```
|
||||
|
||||
The walk is `FUN_00765650`: nine strata buckets of an intrusive list
|
||||
(`frame+0x310` = next, saved into the bucket before each call so a handler may
|
||||
unlink itself), calling `vtable+0x38` per node — with **no visibility check in
|
||||
the loop and none in the dispatcher**. Visibility is therefore *list
|
||||
membership*, not a test, and that list is the per-strata render list, so a
|
||||
hidden frame is simply absent. Contention: a literal scan of every DLL in
|
||||
`dll\` + `dll_local\` finds no reference to `FUN_0076B2C0` or `FUN_00765650`.
|
||||
|
||||
**Design.** Modes 0/1/2 need only a co-hook on `FUN_0076B2C0` — mode 0 returns
|
||||
without calling the original, mode 2 sets Disabled then calls it, mode 1 is the
|
||||
engine unchanged. Guard the detour on an empty mode map so non-users pay a
|
||||
compare.
|
||||
|
||||
Modes 3/4 cannot come from that hook: the frame is not in the walk's list while
|
||||
hidden, and forcing membership would put it in the RENDER list (it would try to
|
||||
draw). Drive them from `Tick::WorldTick` instead — an already-owned hook, so no
|
||||
second MinHook — calling the engine's own `FUN_0076B2C0(frame, elapsed)` so the
|
||||
handler sees exactly the fire it normally would, with a per-tick mark set by the
|
||||
hook so a *visible* `RunAlways` frame is not dispatched twice.
|
||||
|
||||
**Open question before writing it:** the `elapsed` for the self-driven modes.
|
||||
The walk receives it from its caller; cleanest is to capture it in the hook (one
|
||||
value per pass, shared by every frame) and reuse it, falling back to a
|
||||
`Time::Clock` delta when nothing visible ran that frame. Needs checking whether
|
||||
`Tick::WorldTick` (`FUN_0066FD50`, the world subsystem) runs before or after the
|
||||
UI pass, which decides whether the captured value is this frame's or last
|
||||
frame's.
|
||||
|
||||
**Cost:** one new MinHook (uncontended, guarded to ~zero when unused), one
|
||||
WorldTick subscriber, a frame-keyed mode map, `Enum.OnUpdateMode`, and
|
||||
`SetOnUpdateMode` / `GetOnUpdateMode`.
|
||||
|
||||
**Value, honestly.** `RunAlways` is the only genuinely new capability — a hidden
|
||||
frame currently cannot run OnUpdate at all, which is why addons keep a
|
||||
permanently-shown dummy frame for their tickers. `Disabled` duplicates
|
||||
`SetScript("OnUpdate", nil)` and `RunOnce` overlaps `C_Timer.After(0, …)`, so
|
||||
the rest is API parity. Worth it for the one capability plus a clean enum, not
|
||||
as a performance feature.
|
||||
|
||||
## 102. Texture tiling: `SetHorizTile` / `SetVertTile` (+ `SetNonBlocking`) — medium
|
||||
|
||||
The only texture methods 4.3.4 has that 1.12 + ClassicAPI lack
|
||||
(`wowdecomp15595/Engine/Source/Frame/CSimpleTexture_Scripts.cpp`): six
|
||||
setter/getter pairs on `m_textureFlags`. Getters answer 1 or nil.
|
||||
|
||||
| Methods | 4.3.4 flag | Effect |
|
||||
|---|---|---|
|
||||
| `SetHorizTile` / `GetHorizTile` | `0x4` | repeat on U |
|
||||
| `SetVertTile` / `GetVertTile` | `0x8` | repeat on V |
|
||||
| `SetNonBlocking` / `GetNonBlocking` | `0x1` | skip drawing an unloaded image this frame instead of force-loading it |
|
||||
|
||||
**How 4.3.4 tiles.** `CRenderBatch::QueueTexture` (`CRenderBatch.cpp:164`):
|
||||
while the dirty flag `0x2` is set, every non-zero U texcoord becomes
|
||||
`rectWidthInPixels / gxTex->m_width` (V likewise with height), using
|
||||
`DDCToNDCWidth(aspect × 1024 × (right − left)) / m_layoutScale` from the drawn
|
||||
corners. A layout change (`CSimpleRender.cpp:2391`) sets `0x2` again when either
|
||||
tile flag is on. Repeat only shows if the image was loaded with wrap.
|
||||
|
||||
**Wrap on 1.12.** `CSimpleTexture::SetTexture` `0x770200` already takes
|
||||
`(name, wrap, filter, shader)` (benilla-re `ui/texture#settexture`); the Lua
|
||||
`SetTexture` always passes wrap 0. 4.3.4's Lua `SetTexture(file, tile)` reads
|
||||
arg 3 as wrap for both axes — add that. To verify: vanilla has one wrap flag,
|
||||
4.3.4 has `wrapU`/`wrapV`; check whether `CGxTexFlags` `0x58A980` can wrap one
|
||||
axis alone.
|
||||
|
||||
**Where to hook.** The rescale belongs where the drawn corners are built — the
|
||||
texture `OnFrameSizeChanged` co-hook `0x770670` the animation system already
|
||||
owns (`src/animation/Region.cpp`).
|
||||
|
||||
**XML.** `<Texture horizTile= vertTile= nonBlocking=>`; 4.3.4's loader also
|
||||
turns wrap on when `<TexCoords>` leave 0..1 and errors
|
||||
`Texture %s: Invalid TexCoords value (horizTile is on)` (`CSimpleRender.cpp:3024`).
|
||||
|
||||
**NonBlocking.** Only meaningful if 1.12 has a lazy texture-load path (4.3.4:
|
||||
`TextureForceLoadForDiskOnly` vs `NotifyDelayedLoad`). Not checked — if 1.12
|
||||
always loads synchronously it is a stored flag with no effect.
|
||||
|
||||
## ~~103. FontString `SetWordWrap` / `CanWordWrap` / `GetFieldSize`~~ — DONE
|
||||
|
||||
The only FontString methods 4.3.4 has that 1.12 + ClassicAPI lacked
|
||||
(`CSimpleFontString_Scripts.cpp`). Code: `src/fontstring/WordWrap.cpp`,
|
||||
`GetFieldSize` in `src/fontstring/Metrics.cpp`.
|
||||
|
||||
- **SetWordWrap / CanWordWrap.** 4.3.4 style bit `0x40` (set = no wrap). The
|
||||
setter is 1.12's own `SetNonSpaceWrap` (`0x79E9F0`, identical body on bit
|
||||
`0x1000`) with `0x40` inverted. `TextBlockCreate` `0x44D420` already maps
|
||||
`0x40` to text flag `0x2` exactly as 4.3.4's `ConvertStringFlags`, and 1.12's
|
||||
layout `0x5CDC20` / line count `0x5C2070` stop after one line on it.
|
||||
Verified in-game: one line, `...` at the width (the fit `0x771EC0`), lines
|
||||
after `\n` dropped, no width = the whole line. A change must lay the string
|
||||
out again (`FontString::Layout::Invalidate`, as `SetText`), not only rebuild
|
||||
it: a text-sized string kept its two-line height and the fit cut at a whole
|
||||
word with no `...`.
|
||||
- **GetFieldSize.** 4.3.4 returns a constant 8191. 1.12's truth: a string with a
|
||||
fixed text size `+0xE8` (XML `bytes="n"`) keeps `n − 1` bytes (`SetText`'s
|
||||
`SStrCopy(text, src, n)`, `0x771E0F`); without one the buffer grows to any
|
||||
length, so it returns infinity.
|
||||
|
||||
## ~~104. XML `<KeyValues>`~~ — DONE
|
||||
|
||||
Shipped in a2e3eed (`mixin=` and `method=` followed in 2cab0dc). Code:
|
||||
`src/xml/KeyValues.cpp`; the texture / font string hooks in
|
||||
`src/xml/ParentKey.cpp` call it directly (no `RegionLoad` fan-out), and
|
||||
`src/animation/Xml.cpp` reads it for groups and animations. Notes below
|
||||
are the original plan.
|
||||
|
||||
Not in 4.3.4 (no decomp); the contract comes from retail's `UI.xsd`
|
||||
(`_classic_era_/BlizzardInterfaceCode/.../Blizzard_SharedXML/UI.xsd`) and
|
||||
`XMLUtilDocumentation.lua`. Blizzard's classic-era UI uses it in 356 files.
|
||||
|
||||
- `<KeyValues><KeyValue key= value= type= keyType=/></KeyValues>`; `type` and
|
||||
`keyType` are `nil` / `boolean` / `number` / `string` (default) / `global`
|
||||
(`_G[value]`). Sets `object[key] = value` on the Lua wrapper.
|
||||
- A LayoutField: frames, textures, font strings. Also a child of
|
||||
`AnimationGroup` and `Animation`.
|
||||
- `C_XMLUtil.GetTemplateInfo(name).keyValues` is a list of
|
||||
`{key, keyType, type, value}` (all strings); ours always returns `{}`
|
||||
(`src/xml/Templates.cpp`).
|
||||
|
||||
**Ordering.** `NineSlicePanelMixin:OnLoad` reads `self:GetParent().layoutType`
|
||||
— a parent's KeyValue must be set before its child frames load. 1.12's frame
|
||||
load (benilla-re `ui/frames#loading-from-xml`): `0x769820` loads the inherited
|
||||
node first, then attributes, `Layers`, `Scripts`; `0x76A060` then loads the
|
||||
child `<Frames>` and the load ends (OnLoad). So the existing
|
||||
`Xml::FrameLoad::AutoSubscribe` (after `0x769820`, as `<Attributes>` uses) is
|
||||
the right point: template values first, the node's own win, before children
|
||||
and OnLoad.
|
||||
|
||||
**Plan.**
|
||||
1. Textures / font strings: `ParentKey.cpp` owns the `0x76FE20` / `0x770F40`
|
||||
loader hooks and calls `Animation::Xml::LoadAnimations` from them by hand.
|
||||
Turn them into a shared `Xml::RegionLoad::AutoSubscribe` fan-out (like
|
||||
`FrameLoad`) and subscribe ParentKey, Animations and KeyValues.
|
||||
2. `Xml::PushTypedValue(L, type, value)` shared with `<Attributes>`
|
||||
(`Frame::Attributes::LoadXmlAttributes`, engine `FUN_XML_ATTR_TO_BOOL` /
|
||||
`FUN_SSTR_TO_FLOAT`), plus `global`.
|
||||
3. `src/xml/KeyValues.cpp` for frames + regions; the animation loader
|
||||
(`src/animation/Xml.cpp`) reads it for groups and animations.
|
||||
4. Fill `GetTemplateInfo`'s `keyValues` from the template node.
|
||||
|
||||
**Unverifiable without a retail binary** — follow `<Attributes>` and say so in
|
||||
the code: a `boolean` value other than true/false, a `number` that fails to
|
||||
parse, and whether `GetTemplateInfo` includes inherited templates' entries
|
||||
(plan: the node's own only).
|
||||
|
||||
## 105. Button methods — small to medium
|
||||
|
||||
Button methods later clients have that 1.12 + ClassicAPI lack. 4.3.4 adds three
|
||||
over 1.12 (`CSimpleButton_Scripts.cpp`); retail's `SimpleButtonAPIDocumentation`
|
||||
(`_classic_era_` install) lists the rest. `Get/SetNormalFontObject` are done
|
||||
(`src/frame/Modern.cpp`).
|
||||
|
||||
**From 4.3.4 (decomp available).**
|
||||
- **DONE** (`src/frame/MotionScriptsWhileDisabled.cpp`, verified in-game
|
||||
2026-10-19). `SetMotionScriptsWhileDisabled(enabled)` / `GetMotionScriptsWhileDisabled()`
|
||||
— the one real new capability: tooltips on disabled buttons. 4.3.4
|
||||
`CSimpleButton::OnLayerCursorEnter/Exit` (`CSimpleButton.cpp:827/844`): an
|
||||
enabled button runs the frame's enter/leave and the label refresh; a disabled
|
||||
one does nothing unless the flag is set, then shows `m_hoverFrame` and runs
|
||||
ONLY the OnEnter/OnLeave script (no highlight, no label refresh). 1.12 is
|
||||
4.3.4 without the flag: `0x779490` (slot 19) / `0x7794E0` (slot 20) skip the
|
||||
frame's enter/leave (`0x76B6A0` / `0x76B6F0`, scripts + highlight) and the
|
||||
label refresh `0x779810` for state 0 (benilla-re `ui/button#entering-and-leaving`).
|
||||
Needs: a per-button flag (side map, reset on reload), co-hooks on the two
|
||||
slots, a way to run just the script (find the script runner inside
|
||||
`0x76B6A0`), what 1.12 has for `m_hoverFrame`, and the XML attribute
|
||||
`motionScriptsWhileDisabled` (4.3.4 `m_buttonFlags` bits 0x4 set / 0x8 value,
|
||||
`CSimpleButton.cpp:1058`). Default false.
|
||||
- **DONE** (`src/frame/FormattedText.cpp`, shared with the font string's;
|
||||
verified in-game 2026-10-19).
|
||||
`button:SetFormattedText(fmt, ...)` — 4.3.4 formats into a 4096-byte buffer
|
||||
and calls the button's `SetText`. We have it on font strings only
|
||||
(`src/fontstring/Metrics.cpp`, engine `str_format` under pcall); register the
|
||||
same on the Button registry with the button's own SetText.
|
||||
|
||||
**From later clients (contract from retail docs only — no decomp).**
|
||||
- **DONE** (`src/frame/SetEnabled.cpp`). `SetEnabled(enabled)` — `Enable()` /
|
||||
`Disable()` by a bool, through the button's Enable vmethod as they do.
|
||||
- `SetNormalAtlas` / `SetPushedAtlas` / `SetDisabledAtlas` /
|
||||
`SetHighlightAtlas(atlas[, blendMode])` — the state texture made or reused
|
||||
through `SetNormalTexture` etc., then `Texture::Atlas`'s `SetAtlas`.
|
||||
- `ClearNormalTexture` / `ClearPushedTexture` / `ClearDisabledTexture` /
|
||||
`ClearHighlightTexture` — remove the state texture. Check what 1.12's
|
||||
`SetNormalTexture(nil)` does first (`CSimpleButton::SetStateTexture`
|
||||
`0x778FD0` deletes the old one).
|
||||
- **DONE** (`src/frame/MouseRegistration.cpp`). `RegisterForMouse(buttons...)`
|
||||
— which mouse buttons fire OnMouseDown/Up. 1.12 filters nothing: the base
|
||||
frame pair 0x76BF70 / 0x76C040 fire both for any button through the runner,
|
||||
where `Frame::ClickEvents`' gate now refuses unregistered ones.
|
||||
|
||||
## 106. Script-object API sweep — remaining methods, globals and scripts
|
||||
|
||||
A name-level diff, 2026-10-19: every 4.3.4 method table
|
||||
(`wowdecomp15595/Engine/Source/Frame/*_Scripts.cpp`) and retail's
|
||||
`Blizzard_APIDocumentationGenerated/Simple*APIDocumentation.lua` (`_classic_era_`
|
||||
install) against 1.12 (`docs/BlizzardScriptAPI.md`) and what `src/` registers.
|
||||
Texture (#102), FontString (#103 done) and Button (#105) have their own items.
|
||||
Each entry still needs the usual check of how the engine does it before building.
|
||||
|
||||
### From 4.3.4 (decomp available)
|
||||
|
||||
- **DONE** (`src/slider/Enable.cpp`). **Slider** `Enable` / `Disable` / `IsEnabled`: frame flag `0x400`, read by the
|
||||
thumb-drag path (`CSimpleSlider.cpp:317`); `Disable` runs OnDisable unless
|
||||
loading (we have OnEnable/OnDisable in `src/frame/EnableScripts.cpp`).
|
||||
- **DONE** (`src/frame/MinMaxChanged.cpp`). **OnMinMaxChanged** (Slider, StatusBar): `SetMinMaxValues` runs it with
|
||||
`(min, max)` when the range changes (`CSimpleStatusBar.cpp:39/141`). 1.12 has
|
||||
no such script.
|
||||
- **DONE** (`src/statusbar/Fill.cpp`, `src/statusbar/BarTexture.cpp`; XML
|
||||
`reverseFill` / `rotatesTexture` / `fillStyle` attributes not backported).
|
||||
**StatusBar** `SetReverseFill` / `GetReverseFill`, `SetRotatesTexture` /
|
||||
`GetRotatesTexture` (`CSimpleStatusBar.cpp:135`, the vertical texcoord path
|
||||
at `:285`). Retail adds `SetFillStyle` (STANDARD / CENTER / REVERSE),
|
||||
`SetColorFill`, `SetStatusBarDesaturated` / `Desaturation`.
|
||||
- **DONE** (`src/scrollingmessageframe/`, retail's shapes).
|
||||
**ScrollingMessageFrame** `GetMessageInfo`, `SetScrollOffset` (1.12's
|
||||
`SetScrollFromBottom`, 0x00793070, under its later name, clamped as
|
||||
retail), `GetScrollOffset`, `GetMaxScrollRange`, `ScrollByAmount`,
|
||||
`AddOnDisplayRefreshedCallback`, `SetInsertMode` / `GetInsertMode` (TOP
|
||||
through a redirect of the layout's one line-anchor call, 0x00788975),
|
||||
`BackFillMessage` (writes 4.3.4's addToTop slot of the ring, the free one
|
||||
before the oldest; a full frame drops it, as retail's CircularBuffer
|
||||
`PushBack` does — 4.3.4 overwrites the oldest there).
|
||||
- **DONE** (`src/scrollingmessageframe/MessageInfo.cpp`).
|
||||
**ScrollingMessageFrame** `RemoveMessagesByPredicate` (retail's shape; it
|
||||
covers 4.3.4's `RemoveMessagesByAccessID` / `RemoveMessagesByExtraData`,
|
||||
whose int fields our retail-shaped records do not have). The compaction
|
||||
is 4.3.4's `RemoveLines` loop over 1.12's ring, then the engine's own
|
||||
layout (0x00788750) at the kept scroll offset and re-arm (0x00788B80),
|
||||
or Clear (0x007882B0) when nothing is kept.
|
||||
- **DONE** (`src/editbox/Focus.cpp`, `src/animation/Framerate.cpp`).
|
||||
**Globals** (`SimpleFrameScript_Scripts.cpp`): `GetCurrentKeyBoardFocus()`
|
||||
(the focused edit box's wrapper, or nil); `SetMaxAnimFramerate(fps)` /
|
||||
`GetMaxAnimFramerate()` — 4.3.4's `CSimpleAnim::s_frameInterval`, the 1/30 s
|
||||
throttle our animation port already has.
|
||||
- Not worth it: Frame `EnableJoystick` / `IsJoystickEnabled`; the stereo-depth
|
||||
set `SetDepth` / `GetDepth` / `GetEffectiveDepth` / `IgnoreDepth` /
|
||||
`IsIgnoringDepth`; `GetText(token[, gender[, ordinal]])` — gendered
|
||||
global-string lookup, little use on 1.12.
|
||||
|
||||
### From later clients (retail docs only — contract, no decomp)
|
||||
|
||||
Likely worth it (small, ported code uses them):
|
||||
- Object: `GetParentKey` / `SetParentKey` / `ClearParentKey`, `GetDebugName`.
|
||||
- Region: `SetIgnoreParentAlpha` / `IsIgnoringParentAlpha` (4.3.4 has it
|
||||
internally: its texture XML load calls `SetIgnoreParentAlpha` from
|
||||
`m_regionFlags 0x10/0x20`, `CSimpleRender.cpp`),
|
||||
`SetIgnoreParentScale` / `IsIgnoringParentScale`.
|
||||
- ScriptRegion: `GetScaledRect`, `IsRectValid`, `Intersects`.
|
||||
- Frame: `GetResizeBounds` (we have `SetResizeBounds`), `SetFixedFrameStrata` /
|
||||
`SetFixedFrameLevel` / `HasFixed*`, `GetHighestFrameLevel` /
|
||||
`GetRaisedFrameLevel`, `SetUsingParentLevel`, `ClearAttributes`,
|
||||
`IsObjectLoaded`, `RotateTextures` (on `Texture::Transform`), `AbortDrag`.
|
||||
- Slider: `SetObeyStepOnDrag`, `SetStepsPerPage`, `IsDraggingThumb`,
|
||||
`SetEnabled` (also EditBox `SetEnabled`).
|
||||
- FontString / Font: `SetTextScale` /
|
||||
`GetTextScale`, `ClearText`, `SetTextToFit`,
|
||||
`GetUnboundedStringWidthForText`, `FindCharacterIndexAtCoordinate`.
|
||||
- Texture: `GetTextureFilePath`, `ClearVertexOffsets`.
|
||||
- SimpleHTML `GetContentHeight`; ColorSelect alpha (`SetColorAlpha` + its
|
||||
textures).
|
||||
|
||||
Larger, decide separately: mouse-motion / click split and propagation
|
||||
(`SetMouseMotionEnabled`, `SetMouseClickEnabled`, `SetPropagateMouseClicks`,
|
||||
`SetPassThroughButtons`); `SetClipsChildren` (scissoring); `CreateLine` (the
|
||||
Line region type); texture slicing (`SetTextureSliceMargins` / `Mode`); Model
|
||||
camera / transform / alpha (~40 methods); `RegisterEventCallback`; EditBox
|
||||
secure-text and byte-limit options; MessageFrame fade power and message IDs.
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
# embed_addon.cmake — bundle an addon directory into a generated C++ header.
|
||||
#
|
||||
# Invoked from CMakeLists.txt via `cmake -P` as a custom command. Reads every
|
||||
# file under SRC_DIR recursively and emits OUT_HEADER containing:
|
||||
#
|
||||
# namespace Addons::Embedded {
|
||||
# struct File {
|
||||
# const char *path;
|
||||
# const unsigned char *data;
|
||||
# size_t size;
|
||||
# };
|
||||
# extern const File kFiles[];
|
||||
# extern const size_t kFileCount;
|
||||
# }
|
||||
#
|
||||
# Paths are normalized to lowercase backslash-form relative to
|
||||
# `Interface\AddOns\` so they match what the engine's TOC parser
|
||||
# constructs when it asks for the addon's files (see
|
||||
# `s_Interface_AddOns__s__s_toc_00853840` in the binary). The
|
||||
# manifest is hash-table-friendly for fast lookup but the lookup
|
||||
# walks the array linearly — fine for 25 files.
|
||||
#
|
||||
# Usage:
|
||||
# cmake -DSRC_DIR=... -DOUT_HEADER=... -DADDON_NAME=... -P embed_addon.cmake
|
||||
|
||||
if(NOT SRC_DIR)
|
||||
message(FATAL_ERROR "embed_addon.cmake: SRC_DIR required")
|
||||
endif()
|
||||
if(NOT OUT_HEADER)
|
||||
message(FATAL_ERROR "embed_addon.cmake: OUT_HEADER required")
|
||||
endif()
|
||||
if(NOT ADDON_NAME)
|
||||
message(FATAL_ERROR "embed_addon.cmake: ADDON_NAME required")
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE INPUT_FILES RELATIVE "${SRC_DIR}" "${SRC_DIR}/*")
|
||||
# Never embed the developer marker — it must reflect on-disk presence
|
||||
# only (see src/addons/Embedded.cpp DiskHasDevMarker), and it is
|
||||
# gitignored so it only exists in a developer working tree anyway.
|
||||
list(FILTER INPUT_FILES EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(SORT INPUT_FILES) # deterministic codegen
|
||||
|
||||
set(BODY "")
|
||||
set(MANIFEST "")
|
||||
set(INDEX 0)
|
||||
foreach(REL_PATH ${INPUT_FILES})
|
||||
set(ABS_PATH "${SRC_DIR}/${REL_PATH}")
|
||||
|
||||
# Stamp the addon's `## Version:` line from the build flag rather
|
||||
# than the committed source (which stays `DEV`). This is what makes
|
||||
# a release build a pure function of `-DCLASSICAPI_TAG` — the source
|
||||
# toc is never edited. Only the `.toc` file is rewritten, in-memory,
|
||||
# before hex-encoding; every other file is embedded verbatim.
|
||||
if(ADDON_VERSION AND REL_PATH MATCHES "\\.toc$")
|
||||
file(READ "${ABS_PATH}" TOC_TEXT)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${ADDON_VERSION}" TOC_TEXT "${TOC_TEXT}")
|
||||
# Round-trip through a temp file to hex-encode the modified text
|
||||
# (portable to CMake 3.10; avoids requiring string(HEX), 3.18+).
|
||||
set(TOC_TMP "${OUT_HEADER}.toc.tmp")
|
||||
file(WRITE "${TOC_TMP}" "${TOC_TEXT}")
|
||||
file(READ "${TOC_TMP}" CONTENT_HEX HEX)
|
||||
file(REMOVE "${TOC_TMP}")
|
||||
else()
|
||||
file(READ "${ABS_PATH}" CONTENT_HEX HEX)
|
||||
endif()
|
||||
|
||||
string(LENGTH "${CONTENT_HEX}" HEX_LEN)
|
||||
math(EXPR FILE_SIZE "${HEX_LEN} / 2")
|
||||
|
||||
# Convert hex string to comma-separated byte literals. 16 bytes per line
|
||||
# keeps the generated header readable.
|
||||
set(BYTES "")
|
||||
set(COL 0)
|
||||
set(POS 0)
|
||||
while(POS LESS HEX_LEN)
|
||||
string(SUBSTRING "${CONTENT_HEX}" ${POS} 2 BYTE)
|
||||
if(COL EQUAL 0)
|
||||
string(APPEND BYTES " ")
|
||||
endif()
|
||||
string(APPEND BYTES "0x${BYTE},")
|
||||
math(EXPR POS "${POS} + 2")
|
||||
math(EXPR COL "${COL} + 1")
|
||||
if(COL EQUAL 16)
|
||||
string(APPEND BYTES "\n")
|
||||
set(COL 0)
|
||||
else()
|
||||
string(APPEND BYTES " ")
|
||||
endif()
|
||||
endwhile()
|
||||
if(NOT COL EQUAL 0)
|
||||
string(APPEND BYTES "\n")
|
||||
endif()
|
||||
|
||||
# Normalize path to engine-style: `<AddonName>\Sub\Path.lua` with
|
||||
# backslashes. Engine constructs full path as
|
||||
# `Interface\AddOns\<AddonName>\<file>` — we strip the
|
||||
# `Interface\AddOns\<AddonName>\` prefix at lookup time so storing
|
||||
# just the relative form keeps the manifest small.
|
||||
string(REPLACE "/" "\\\\" REL_PATH_BS "${REL_PATH}")
|
||||
|
||||
string(APPEND BODY "static const unsigned char kData_${INDEX}[] = {\n${BYTES}};\n\n")
|
||||
string(APPEND MANIFEST " { \"${REL_PATH_BS}\", kData_${INDEX}, ${FILE_SIZE} },\n")
|
||||
|
||||
math(EXPR INDEX "${INDEX} + 1")
|
||||
endforeach()
|
||||
|
||||
set(HEADER_CONTENT "// Auto-generated by embed_addon.cmake — do not edit.
|
||||
// Source: ${SRC_DIR}
|
||||
//
|
||||
// Addon \"${ADDON_NAME}\" embedded as a fallback for users who don't have
|
||||
// it installed on disk. See src/addons/Embedded.cpp for the file-read
|
||||
// hook that surfaces these buffers to the engine.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Addons::Embedded {
|
||||
|
||||
namespace ${ADDON_NAME}Files {
|
||||
|
||||
${BODY}static const struct File {
|
||||
const char *path;
|
||||
const unsigned char *data;
|
||||
size_t size;
|
||||
} kFiles[] = {
|
||||
${MANIFEST}};
|
||||
|
||||
static constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace ${ADDON_NAME}Files
|
||||
|
||||
} // namespace Addons::Embedded
|
||||
")
|
||||
|
||||
file(WRITE "${OUT_HEADER}" "${HEADER_CONTENT}")
|
||||
message(STATUS "embed_addon: wrote ${OUT_HEADER} (${INDEX} files)")
|
||||
@@ -1,62 +0,0 @@
|
||||
# embed_file.cmake — bundle a single file into a generated C++ header.
|
||||
#
|
||||
# Invoked from CMakeLists.txt via `cmake -P` as a custom command. Reads
|
||||
# INPUT_FILE and emits OUT_HEADER containing a constexpr byte array
|
||||
# named SYMBOL plus a `SYMBOL ## Size` size constant. Both live at file
|
||||
# scope; the header is meant to be included exactly once.
|
||||
#
|
||||
# Usage:
|
||||
# cmake -DINPUT_FILE=... -DOUT_HEADER=... -DSYMBOL=... -P embed_file.cmake
|
||||
|
||||
if(NOT INPUT_FILE)
|
||||
message(FATAL_ERROR "embed_file.cmake: INPUT_FILE required")
|
||||
endif()
|
||||
if(NOT OUT_HEADER)
|
||||
message(FATAL_ERROR "embed_file.cmake: OUT_HEADER required")
|
||||
endif()
|
||||
if(NOT SYMBOL)
|
||||
message(FATAL_ERROR "embed_file.cmake: SYMBOL required")
|
||||
endif()
|
||||
|
||||
file(READ "${INPUT_FILE}" CONTENT_HEX HEX)
|
||||
string(LENGTH "${CONTENT_HEX}" HEX_LEN)
|
||||
math(EXPR FILE_SIZE "${HEX_LEN} / 2")
|
||||
|
||||
# Convert hex string to comma-separated byte literals. 16 bytes per
|
||||
# line keeps the generated header readable.
|
||||
set(BYTES "")
|
||||
set(COL 0)
|
||||
set(POS 0)
|
||||
while(POS LESS HEX_LEN)
|
||||
string(SUBSTRING "${CONTENT_HEX}" ${POS} 2 BYTE)
|
||||
if(COL EQUAL 0)
|
||||
string(APPEND BYTES " ")
|
||||
endif()
|
||||
string(APPEND BYTES "0x${BYTE},")
|
||||
math(EXPR POS "${POS} + 2")
|
||||
math(EXPR COL "${COL} + 1")
|
||||
if(COL EQUAL 16)
|
||||
string(APPEND BYTES "\n")
|
||||
set(COL 0)
|
||||
else()
|
||||
string(APPEND BYTES " ")
|
||||
endif()
|
||||
endwhile()
|
||||
if(NOT COL EQUAL 0)
|
||||
string(APPEND BYTES "\n")
|
||||
endif()
|
||||
|
||||
set(HEADER_CONTENT "// Auto-generated by embed_file.cmake — do not edit.
|
||||
// Source: ${INPUT_FILE}
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
static constexpr unsigned char ${SYMBOL}[] = {
|
||||
${BYTES}};
|
||||
static constexpr size_t ${SYMBOL}Size = sizeof(${SYMBOL});
|
||||
")
|
||||
|
||||
file(WRITE "${OUT_HEADER}" "${HEADER_CONTENT}")
|
||||
message(STATUS "embed_file: wrote ${OUT_HEADER} (${FILE_SIZE} bytes)")
|
||||
@@ -0,0 +1,152 @@
|
||||
# embed_resources.cmake — embed the addons and the bindings XML as Win32
|
||||
# RCDATA resources.
|
||||
#
|
||||
# Runs at configure time. It writes two files into OUT_DIR:
|
||||
#
|
||||
# embedded.rc one `<id> RCDATA "<path>"` line per file; rc.exe
|
||||
# reads each file's gzipped copy from disk.
|
||||
# embedded_resources.h the IDs the DLL looks up at runtime through
|
||||
# src/embed/Resource.h:
|
||||
#
|
||||
# namespace Embed::Ids { constexpr int kBindingsXml = ...; }
|
||||
# namespace Addons::Embedded::Manifest {
|
||||
# constexpr const char *kAddons[] = { ... }; // in ADDONS order
|
||||
# constexpr size_t kAddonCount = ...;
|
||||
# struct File { const char *path; int resourceId; };
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
#
|
||||
# File paths are relative to ADDONS_ROOT, in backslash form
|
||||
# (`!!!ClassicAPI\Util\Print.lua`) — the suffix the engine builds after
|
||||
# `Interface\AddOns\`. The DLL compares them ignoring case.
|
||||
#
|
||||
# Each addon's `.toc` is the one file that is not embedded verbatim: its
|
||||
# `## Version:` line is stamped with ADDON_VERSION into a copy under
|
||||
# OUT_DIR, so a release build is a pure function of `-DCLASSICAPI_TAG` and
|
||||
# the source tocs stay `DEV`. The developer marker `.classicapi-dev` is
|
||||
# never embedded: it must reflect on-disk presence only (see
|
||||
# DiskHasDevMarker in src/addons/Embedded.cpp).
|
||||
#
|
||||
# Every resource is gzipped at build time (gzip_file.cmake) into
|
||||
# `OUT_DIR/embed/<path>.gz`, and the DLL inflates it with the engine's zlib on
|
||||
# first use (src/embed/Resource.cpp). The addon text compresses to about a
|
||||
# quarter of its size.
|
||||
#
|
||||
# Rebuild triggers: adding or removing a file reconfigures through the
|
||||
# CONFIGURE_DEPENDS glob; editing a toc reconfigures through
|
||||
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-gzips it through its
|
||||
# custom command, which recompiles the .rc through its OBJECT_DEPENDS.
|
||||
#
|
||||
# Usage:
|
||||
# embed_resources(TARGET <t> ADDONS_ROOT <dir> ADDONS <folder names...>
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file> OUT_DIR <dir>)
|
||||
|
||||
set(_EMBED_GZIP_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/gzip_file.cmake")
|
||||
|
||||
# Add the build-time command that gzips `src` into `gz`.
|
||||
function(_embed_gzip src gz)
|
||||
add_custom_command(
|
||||
OUTPUT "${gz}"
|
||||
COMMAND "${CMAKE_COMMAND}" "-DIN=${src}" "-DOUT=${gz}"
|
||||
-P "${_EMBED_GZIP_SCRIPT}"
|
||||
DEPENDS "${src}" "${_EMBED_GZIP_SCRIPT}"
|
||||
VERBATIM)
|
||||
endfunction()
|
||||
|
||||
# Write `content` to `path` only when it differs, so an unchanged
|
||||
# reconfigure does not touch the file and trigger a rebuild.
|
||||
function(_embed_write_if_changed path content)
|
||||
set(tmp "${path}.tmp")
|
||||
file(WRITE "${tmp}" "${content}")
|
||||
configure_file("${tmp}" "${path}" COPYONLY)
|
||||
file(REMOVE "${tmp}")
|
||||
endfunction()
|
||||
|
||||
function(embed_resources)
|
||||
cmake_parse_arguments(E "" "TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;OUT_DIR"
|
||||
"ADDONS" ${ARGN})
|
||||
|
||||
set(rc "// Generated by cmake/embed_resources.cmake - do not edit.\n\n")
|
||||
set(addon_names "")
|
||||
set(manifest "")
|
||||
set(inputs "")
|
||||
set(id 1)
|
||||
|
||||
set(gz "${E_OUT_DIR}/embed/Bindings.xml.gz")
|
||||
_embed_gzip("${E_BINDINGS_XML}" "${gz}")
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
set(bindings_id ${id})
|
||||
math(EXPR id "${id} + 1")
|
||||
|
||||
set(files "")
|
||||
foreach(addon ${E_ADDONS})
|
||||
string(APPEND addon_names " \"${addon}\",\n")
|
||||
file(GLOB_RECURSE addon_files CONFIGURE_DEPENDS RELATIVE
|
||||
"${E_ADDONS_ROOT}" "${E_ADDONS_ROOT}/${addon}/*")
|
||||
list(FILTER addon_files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(APPEND files ${addon_files})
|
||||
endforeach()
|
||||
list(SORT files) # deterministic IDs
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDONS_ROOT}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
|
||||
set(stamped "${E_OUT_DIR}/embed/${rel}")
|
||||
_embed_write_if_changed("${stamped}" "${toc}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
set(gz "${E_OUT_DIR}/embed/${rel}.gz")
|
||||
_embed_gzip("${abs}" "${gz}")
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id} },\n")
|
||||
math(EXPR id "${id} + 1")
|
||||
endforeach()
|
||||
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded.rc" "${rc}")
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded_resources.h"
|
||||
"// Generated by cmake/embed_resources.cmake — do not edit.
|
||||
// Resource IDs for the RCDATA entries in embedded.rc. Load them through
|
||||
// src/embed/Resource.h.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Embed::Ids {
|
||||
constexpr int kBindingsXml = ${bindings_id};
|
||||
} // namespace Embed::Ids
|
||||
|
||||
namespace Addons::Embedded::Manifest {
|
||||
|
||||
// Registration order: the first one registers, and so loads, first.
|
||||
constexpr const char *kAddons[] = {
|
||||
${addon_names}};
|
||||
|
||||
constexpr size_t kAddonCount = sizeof(kAddons) / sizeof(kAddons[0]);
|
||||
|
||||
struct File {
|
||||
const char *path;
|
||||
int resourceId;
|
||||
};
|
||||
|
||||
constexpr File kFiles[] = {
|
||||
${manifest}};
|
||||
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Addons::Embedded::Manifest
|
||||
")
|
||||
|
||||
add_custom_target(${E_TARGET}_embed DEPENDS ${inputs})
|
||||
add_dependencies(${E_TARGET} ${E_TARGET}_embed)
|
||||
target_sources(${E_TARGET} PRIVATE "${E_OUT_DIR}/embedded.rc")
|
||||
set_source_files_properties("${E_OUT_DIR}/embedded.rc" PROPERTIES
|
||||
OBJECT_DEPENDS "${inputs}")
|
||||
endfunction()
|
||||
@@ -0,0 +1,8 @@
|
||||
# gzip_file.cmake — gzip one file at build time, for embed_resources.cmake.
|
||||
#
|
||||
# cmake -DIN=<file> -DOUT=<file.gz> -P gzip_file.cmake
|
||||
#
|
||||
# The raw format holds the one file's bytes with no archive entry around
|
||||
# them, so OUT is a plain gzip stream of IN.
|
||||
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${IN}" FORMAT raw
|
||||
COMPRESSION GZip COMPRESSION_LEVEL 9)
|
||||
+11594
-2978
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,8 @@ ImageBase `0x00400000`. All VAs in this doc use that base.
|
||||
|
||||
## How Blizzard wires Lua
|
||||
|
||||
There are two registries.
|
||||
There are two Blizzard registries, plus the Lua standard libraries, which are
|
||||
bound a third way (§3) and are NOT in the batch counts below.
|
||||
|
||||
### 1. Global functions — `FrameScript_RegisterFunction` at `0x00704120`
|
||||
|
||||
@@ -57,6 +58,62 @@ C3 ret
|
||||
23 tables found, **481 methods total**. The runtime dispatcher at `0x005368D0`
|
||||
resolves `frame:method(...)` by looking the name up in the matching context.
|
||||
|
||||
### 3. Lua standard libraries — `luaL_openlib` tables
|
||||
|
||||
The Lua 5.0 standard libraries, and Blizzard's replacements inside them, are
|
||||
bound by `luaL_openlib` walking one `luaL_reg { char *name; lua_CFunction
|
||||
func; }[]` table per library when the Lua state is created (`FUN_00703B80`,
|
||||
called immediately BEFORE `FUN_LOAD_SCRIPT_FUNCTIONS`). They never pass
|
||||
through `FrameScript_RegisterFunction`, so none of them appear in the 54
|
||||
batches above or in the loop scan behind `raw_globals.txt` — which is how
|
||||
`debugprofilestart` / `debugprofilestop` (stock since 1.7.0) went
|
||||
uncatalogued here until 2026-09. **Before declaring a global absent, check
|
||||
these tables or `/dump` the name in game.**
|
||||
|
||||
**Base library** — table `[0x00811E28]`, 36 entries, `.rdata`. Blizzard's
|
||||
own base library: the stock 5.0 set minus `dofile`/`loadfile`/`require`/
|
||||
`print` (`print` is a ClassicAPI backport), plus the `debug*` /
|
||||
`*errorhandler` / `date` / `time` additions.
|
||||
|
||||
```
|
||||
0x00702770 setglobal 0x00702DF0 type
|
||||
0x007027B0 getglobal 0x00702E30 tonumber
|
||||
0x007027E0 debugload 0x00702F40 tostring
|
||||
0x007027F0 debuginfo 0x007030A0 unpack
|
||||
0x00702800 debugprint 0x007030F0 rawequal
|
||||
0x00702810 debugdump 0x00703130 rawget
|
||||
0x00702820 debugbreak 0x00703160 rawset
|
||||
0x00702830 debugtimestamp 0x007031A0 assert
|
||||
0x00702840 debugprofilestart 0x00703200 gcinfo
|
||||
0x00702860 debugprofilestop 0x00703250 collectgarbage
|
||||
0x00702900 seterrorhandler 0x00703280 loadstring
|
||||
0x00702950 geterrorhandler 0x007032F0 pcall
|
||||
0x00702970 error 0x00703340 xpcall
|
||||
0x007029F0 getmetatable 0x007033A0 date
|
||||
0x00702A40 setmetatable 0x007035A0 time
|
||||
0x00702AC0 getfenv 0x00703760 debugstack
|
||||
0x00702BE0 setfenv
|
||||
0x00702C90 next
|
||||
0x00702CD0 ipairs
|
||||
0x00702DA0 pairs
|
||||
```
|
||||
|
||||
`debugprofilestart` stores `rdtsc` in `0x00CEEB58` (u64);
|
||||
`debugprofilestop` pushes `|rdtsc − start| / (ticksPerSecond / 1000)` as a
|
||||
double, where `ticksPerSecond` is `FUN_0042C020` = `(int64)(1000.0 /
|
||||
[0x008332C8])` — a TSC scale calibrated at boot (the image holds `1.0`). So:
|
||||
milliseconds with a sub-microsecond fraction, from the CPU counter, unrelated
|
||||
to the engine's millisecond tick and to `GetTime()`.
|
||||
|
||||
The `table`, `string` and `math` libraries have the same shape (the `table`
|
||||
library's `luaL_reg` is at `0x00822D70` — see `Offsets.h`
|
||||
`FUN_LUA_TABLE_INSERT`) and are not enumerated here. The short global
|
||||
aliases (`tinsert`, `getn`, `strlen`, `format`, `floor`, …) are not registered
|
||||
at all: an embedded Lua chunk at `.data 0x008722E8`, run by the same state
|
||||
init, assigns them from the library tables (`tinsert = tab.insert`). A module
|
||||
that registers over a library function must therefore re-bind its alias —
|
||||
`Game::Lua::RegisterGlobalAlias`.
|
||||
|
||||
## Scope
|
||||
|
||||
The 54 global-function batches split into two phases:
|
||||
|
||||
+200
-116
@@ -69,30 +69,37 @@ The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
earlier `0xFF` alpha force at emit was a misdiagnosis of a bubble that built
|
||||
while faded out; it froze icon-line text fully opaque while everything
|
||||
around it faded.
|
||||
- **Outlined fonts** — outline ink extends past the glyph advances; the
|
||||
engine itself grows line height by exactly 4.0 / 2.0 pen px for thick / thin
|
||||
outline (`face+0x180` bits 3 / 0, `.rdata` constants). `OutlineInkPen` reads
|
||||
the same flags and constants, and half the ink leads each icon, half trails
|
||||
— threaded through the shared advance helper so the emitter advance, the
|
||||
justify pre-shift, and the wrap-stepper probes all agree. Without it, coins
|
||||
clipped into outlined digits the moment the exact K removed the old
|
||||
miscalibration's accidental slack (`968335d`).
|
||||
- **Size, spacing and alignment are 4.3.4's rules** (decompiled source in
|
||||
`C:\Git\wowdecomp15595`; 3.3.5 has the same). See "Icon geometry" below.
|
||||
Every hand-tuned constant this module once carried (a 0.18 × font lead/trail
|
||||
pad, a 0.6 × font vertical centre, a −1px region nudge, a size multiplier)
|
||||
is gone.
|
||||
- **Measure** — three cold FontString-level co-hooks make measure match the
|
||||
drawn result: width (`FUN_00772890`), height (`FUN_007729B0`), and wrap
|
||||
(`FUN_005C7260`). `GetStringWidth`, `fontstring:GetStringHeight`, wrap
|
||||
breaks, and line height all count the icon.
|
||||
- **Tall icons** — an icon taller than the font grows its line height, so it
|
||||
does not overlap its neighbors, and it centers in the grown line. A hyperlink
|
||||
- **Tall icons** — a line holding an icon taller than the font moves itself
|
||||
and the lines after it down by the overflow, as 4.3.4 does, so it does not
|
||||
overlap its neighbors; the icon aligns by the line's vertical justification
|
||||
(below). A hyperlink
|
||||
that contains a tall icon is hoverable across the whole icon
|
||||
(`49934b0`, `b9d0705`).
|
||||
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
|
||||
- **Editboxes** — an editbox keeps the raw markup, so the caret stays aligned
|
||||
(two complementary signals, below).
|
||||
- **Edit boxes** — render the icons in their text, as 4.3.4 does; the caret,
|
||||
the mouse and the arrow keys treat an icon as one character, Ctrl+C
|
||||
drops icons (4.3.4's 0x1500 strip mask), and a multi-line box's caret,
|
||||
selection and click rows follow the lines a tall icon moves (below;
|
||||
4.3.4's own rows do not). Typed markup stays literal by the engine's own
|
||||
input escape (below).
|
||||
- **Payload** — the full
|
||||
`|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`:
|
||||
size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
|
||||
TGA.
|
||||
- **Anti-spoof** — chat strips player-injected `|T` icons.
|
||||
- **Anti-spoof** — chat neutralizes a player-injected `|T` the way the edit
|
||||
box neutralizes a typed one (the `|` is doubled).
|
||||
- **Raid markers** — `{rt1}`/`{skull}`/… become icons in the CHAT_MSG text and,
|
||||
separately, in the speech bubble after its escape strip (4.3.4's
|
||||
`CGChatBubbleFrame::ParseRaidIcons`).
|
||||
|
||||
Every fs-level measure path now counts icons: width (`FUN_00772890`), height
|
||||
(`FUN_007729B0`), wrap (`FUN_005C7260`), and the substring measure
|
||||
@@ -171,7 +178,7 @@ SetTexture `+0x104`). Selector map (shared across GL/D3D backends): 7=blend,
|
||||
0x10=depthtest, 0x14=cullface, 0x17-0x1e=texture bind (8 units),
|
||||
0x1f-0x26=colorop/alphaop preset (unit 0-7).
|
||||
|
||||
## SOLVED — positioning, measure/wrap, editbox exclusion, texcoords
|
||||
## SOLVED — positioning, measure/wrap, edit boxes, texcoords
|
||||
|
||||
The primitive draws at arbitrary coords; the rest is knowing WHERE and WHEN.
|
||||
Everything is in [src/text/InlineTexture.cpp](../src/text/InlineTexture.cpp);
|
||||
@@ -196,8 +203,7 @@ frame → renderNode [frame+0x118] → group [renderNode+0x18]
|
||||
- The **emitter** `FUN_005ccbe0(line, text, len, colorState, penXYZ, pageMask,
|
||||
linkState)` builds one wrapped line's glyph quads. `this` (the "line"/node the
|
||||
icon list keys on) owns batches `[+0xa0+page*4]` and origin `[+0x70]`. **Note
|
||||
the render node the emitter sees has NO back-pointer up to the frame** — the
|
||||
editbox discriminator had to be found another way (see below).
|
||||
the render node the emitter sees has NO back-pointer up to the frame.**
|
||||
|
||||
### Positioning — segmented delegation (co-hook the emitter)
|
||||
|
||||
@@ -224,14 +230,27 @@ We do NOT reimplement the emitter's intricate glyph vertex math. Instead, in the
|
||||
does NOT reset it between left-justified lines, so leaving it mutated
|
||||
cascade-shifts every following line).
|
||||
|
||||
### FontString pipe-doubling — the escape arrives as `||T…||t`
|
||||
### The span rule — `|T…|t` only, `||` is always a literal pipe
|
||||
|
||||
The FontString text sanitizer **doubles any pipe that doesn't begin a recognized
|
||||
escape** so it renders as a literal `|`. 1.12 doesn't know `|T`, so a caller's
|
||||
`|Tpath:h|t` reaches the emitter as `||Tpath:h||t` (verified: emitter text length
|
||||
+2, pipes doubled). The detector therefore matches BOTH the doubled form (the
|
||||
real-world case) and a clean `|T` (should one ever arrive undoubled). Normal
|
||||
doubled pipes (`|| `) never match because the next char must be `T`.
|
||||
One scanner, `IconSpanAt`, decides what a span is, with 4.3.4's
|
||||
`GxuDetermineQuotedCode` CODE_TEXTURESTART rule: after `|T` (or `|A`), the
|
||||
FIRST `|` must be followed by `t` (`a`), and an empty `|T|t` is literal.
|
||||
Anything else is not a span and draws as text. Every walker steps over `||`
|
||||
as a pair, because `||` is the escaped pipe in both engines.
|
||||
|
||||
That is what keeps typed markup literal everywhere — chat display, edit
|
||||
boxes, macros: the edit box stores a typed `|` as `||` (`0x77c254`; Paste
|
||||
goes through the same `InsertChar`, as in 4.3.4), so a typed `|Tpath|t` is
|
||||
`||Tpath||t` in every buffer it reaches.
|
||||
|
||||
(An earlier version matched a "doubled" `||T…||t` form too, on the belief
|
||||
that the FontString sanitizer doubled unknown escapes. Stock 1.12 has no
|
||||
such sanitizer — not SetText `0x771d80`, its grammar pass `0x7716f0`,
|
||||
FitText `0x771ec0`, nor the edit box's RebuildLabel `0x77d4d0`. The `||T`
|
||||
seen in chat lines was typed text, and our own `{rt1}` expansion emitted it
|
||||
on purpose to survive the bubble's escape strip. Matching it turned every
|
||||
typed `|T` into an icon, which is why the edit box needed the suppression
|
||||
gates the span rule replaced.)
|
||||
|
||||
### bit-3 batch-clear mode
|
||||
|
||||
@@ -258,15 +277,76 @@ per build — on the first wrapped line, detected by `text == [node+0x48]` (the
|
||||
node's text start) — and icons ACCUMULATE across lines. Multiple icons per line
|
||||
fall out of the segmentation loop naturally.
|
||||
|
||||
### Vertical centring — font-relative
|
||||
### Icon geometry — 4.3.4's rules, ported
|
||||
|
||||
`penXYZ[1]` sits near the text TOP, so the icon centre = `penY + fontHeight *
|
||||
g_centerFrac` (0.6, calibrated in-game across chat + pfUI's bubble). Font height
|
||||
comes from the engine's own `FUN_005c6fa0(flag, [node+0x1c])` (the emitter's
|
||||
glyph-sizing call), so centring holds across font sizes without a fixed pixel
|
||||
nudge. A TALL icon (height > font) centres in its GROWN line (see the tall-icon
|
||||
sections in Current design) — the string-height co-hook adds the overflow, so
|
||||
the extra room sits half above and half below the text.
|
||||
**Size** (`ParseEmbeddedTexture`, 0x00617BA0 in 4.3.4). A WRITTEN size is not
|
||||
pixels: it multiplies by `fontPx / (14 × ddcWidthScale)`, the reduction of the
|
||||
engine's `windowPx / (aspectComp × 1024) × fontHeight / NDCToDDCWidth(14 /
|
||||
(aspectComp × 1024))` (the call list of the real apwow.exe function confirms
|
||||
`CoordinateGetAspectCompensation` + `NDCToDDCWidth`). So sizes are relative to
|
||||
a 14-unit font and follow the text's size, UI scale and resolution. 1.12 holds
|
||||
ddcWidthScale in the same Coordinate static (`VAR_COORD_DDC_WIDTH_SCALE`,
|
||||
0x00832A44 — the four statics sit at 0x00832A40 with 4.3.4's exact initial
|
||||
values). On top of that, every size multiplies by the fontstring's layout
|
||||
scale (`fs+0x7C`) — 3.3.5's `CGxString::m_scale`, applied again although the
|
||||
font height already includes it. 4.3.4 dropped this factor; 3.3.5's was kept
|
||||
after a side-by-side look in game. Every wrap gets the same scale, as 4.3.4
|
||||
passes `m_layoutScale` to every `CalcWrapPoint`: the draw builder through the
|
||||
node's owner, and the three fs-level callers (the height getter, the
|
||||
display-text truncation inside the rebuild, the break array) through a
|
||||
fontstring scope their co-hooks set (`WrapLayoutScale`). Size cases: height ≤ 0 → font height; width absent → the height;
|
||||
width > 0 → width × scale, or a RATIO of the height when the height came from
|
||||
the font (`:0:2` is twice as wide); width ≤ 0 → the font height. Offsets are
|
||||
screen pixels and move only the drawn texture (+x right, +y UP), never the
|
||||
layout. `ResolveIconSize` is the one implementation.
|
||||
|
||||
**Spacing** (`InitializeTextLine`, case CODE_TEXTURESTART). An icon starts at
|
||||
the previous glyph's INK edge — bearing + cell width, clamped to ≥ 0 and
|
||||
capped at the glyph's own advance (`FUN_TEXT_GLYPH_BASE_ADVANCE`) — plus
|
||||
the face's outline ink (4 × scale thick / 2 × scale thin, `face+0x180` bits 3 /
|
||||
0; the same constants the 1.12 emitter grows its line by); with no glyph
|
||||
since the line start or the last icon it starts at the pen, no gap. The next
|
||||
glyph starts at `floor(iconRight + 0.5)` with no kerning against the icon.
|
||||
1.12's emitter doesn't report its last glyph's position (its pen is lazy), so
|
||||
`drawRun` replays the run's placements with the emitter's own helpers — the
|
||||
pair advance, the snap rounding (`FADD 0.5; __ftol`), `FUN_TEXT_GLYPH_DESC`
|
||||
(`CGxFont::NewCodeDesc`) and `FUN_TEXT_GLYPH_BEARING` — and floors the cell
|
||||
width in snap mode exactly as the emitter draws the quad. Measures count an
|
||||
icon as its width plus the outline gap when a glyph precedes it
|
||||
(`SumIconAdvances`); the engine measures width alone, but counting the gap
|
||||
keeps measure and render in step for tooltips and right-justified text. The
|
||||
advance cap is ClassicAPI's: every measure counts the preceding glyph by its
|
||||
advance, so an ink edge past the advance (a glyph cell wider than its step —
|
||||
seen on "[" in chat) pushed each following icon and the rest of the line past
|
||||
the measured width, and wrapped chat lines overran the frame edge.
|
||||
|
||||
**Vertical** (same case + `CreateGeometry`). By the node's vertical justify
|
||||
(`OFF_TEXT_NODE_JUSTIFY_V`, node+0x50: 0 top / 1 middle / 2 bottom — the
|
||||
branch of the origin finalize `FUN_005cdf70`): top → the icon's top on the line
|
||||
top; bottom → its bottom on the baseline; middle → centred on the font height
|
||||
below the line top. The baseline is `penXYZ[1]` and the line top is the draw
|
||||
builder's `linkState[1]` (baseline + font height + spacing), 4.3.4's
|
||||
m_baseLineHeight / m_lineHeight.
|
||||
|
||||
**Tall lines** (`CGxString::InitializeText` + `InitializeViewTranslation` +
|
||||
`GxuFontGetWrappedTextHeight`), mirrored step for step:
|
||||
- A line whose tallest icon reaches past the font height moves down by that
|
||||
overflow, and every later line with it: all of it before the line for
|
||||
bottom justify, all after for top, `floor(overflow / 2)` before and after
|
||||
for middle. The emitter co-hook moves the builder's pen y and link band
|
||||
(`penXYZ[1]` = its `local_48`, `linkState[1]/[3]` = its `local_74[0]/[2]`),
|
||||
which the builder lowers per line, so the move carries forward.
|
||||
- The node's summed overflow (`m_extraLineHeight`) joins the text height the
|
||||
origin finalize justifies by: the `FUN_TEXT_NODE_FINALIZE_ORIGIN` co-hook
|
||||
raises the node position y (`+0x24`) by half / all of it for middle /
|
||||
bottom justify for the call, so the engine's own math and rounding produce
|
||||
4.3.4's origin. It also runs on every reposition (`FUN_005ccb10`).
|
||||
- `GetStringHeight` adds the same per-line overflows, summed. The wrap
|
||||
stepper reports each line's overflow during the engine's own height pass,
|
||||
so the lines are the engine's breaks; the sum is kept per fontstring while
|
||||
the engine serves its cached height.
|
||||
A link's hit band grows toward where its icon reaches (below for top, above
|
||||
for bottom, half each way for middle); 4.3.4 keeps the band at the text.
|
||||
|
||||
### Measure / wrap — co-hook the tokenizer
|
||||
|
||||
@@ -299,7 +379,7 @@ funnel for exactly 4 callers:
|
||||
|
||||
- `Script_GetStringWidth` (0x0079E510)
|
||||
- the GameTooltip auto-size (0x00530640)
|
||||
- the editbox caret positioner (0x0077DE70)
|
||||
- the editbox selection-piece placer (0x0077DE70, a whole last line's width)
|
||||
- the layout effective-width vmethod (`FUN_00772930` = fs+0x24 vtbl +0x1C,
|
||||
which feeds auto-width fontstring layout)
|
||||
|
||||
@@ -308,13 +388,9 @@ the measure core `FUN_0044D670`, then divides the pen-space result by
|
||||
`fs+0x7C`.
|
||||
|
||||
The co-hook lives in `src/text/InlineTexture.cpp`. It calls the original,
|
||||
then adds `SumIconAdvances(text) / K` when two gates pass:
|
||||
|
||||
- `InlineInterceptActive(text)` is true — the **same predicate** the tokenizer
|
||||
hook stands down on, factored into one function so measure and render can
|
||||
never disagree.
|
||||
- The fs is not an editbox: `fs+0x120 & 0x1000` is clear (the fs-level bit
|
||||
`FUN_0044D670` translates into the gxu editbox flag 0x40).
|
||||
then adds the icons' advances (`SumIconAdvances`) whenever the feature is
|
||||
on — the same condition the tokenizer hook intercepts on, so measure and
|
||||
render never disagree.
|
||||
|
||||
The icon sum is computed in TRUE PEN PX — the exact numbers the emitter
|
||||
reserves — and bridged into the getter's return space by the font height
|
||||
@@ -355,8 +431,7 @@ measure `FUN_005C2070`, the chars-that-fit counter `FUN_005C21C0` (ellipsis),
|
||||
and the break-array computer `FUN_005C2430` (behind `FUN_00772B60`).
|
||||
|
||||
The co-hook shrinks the `wrapWidth` argument by a **minimal feasible
|
||||
shrink**, gated on the same `InlineInterceptActive` predicate plus the flags
|
||||
editbox bit 0x40 the dispatcher already carries. The stepper reports each
|
||||
shrink** whenever the feature is on. The stepper reports each
|
||||
probe's measured (icons-at-zero) line width in `outWidth`, which gives a
|
||||
direct feasibility check: the line renders inside the frame iff its icons
|
||||
fit in the shrink plus the spare the break left (`iconSum ≤ s + (probeW −
|
||||
@@ -410,87 +485,81 @@ line through our co-hooked tokenizer (`FUN_005c2810`) and wrap-stepper
|
||||
(`FUN_005c7260`). So the recorded rect rides the same icon-aware position
|
||||
accounting the wrap fix installs; the icon lands inside the hover region for free,
|
||||
and hovering it fires the frame's `OnHyperlinkEnter`. This is a consequence of the
|
||||
wrap fix, not a separate feature. (Ghidra caveat: the decompile of the
|
||||
`FUN_005cd310` call args is mangled — a pointer-as-float + an uninitialized temp —
|
||||
so this is confirmed by the call path plus in-game test, not the byte-level
|
||||
formula.)
|
||||
wrap fix, not a separate feature. (`FUN_005cd310` itself, from benilla
|
||||
font/string "Hyperlink regions", difftested: it multiplies the rect's y fields
|
||||
`+0x08`/`+0x10` by 1/H and its x fields `+0x0c`/`+0x14` by 1/W and appends the
|
||||
32-byte record to the array at `+0x7c`. Ghidra's own decompile of the call
|
||||
args is mangled.)
|
||||
|
||||
**Still icon-blind (accepted residuals):**
|
||||
|
||||
- Sub-pixel only for a string-final icon: the width hooks report its INK edge
|
||||
(advance minus the trail half-pad/ink — `TrailingIconTrimPen`), matching the
|
||||
engine's own last-glyph convention (the gxu width loop ends on ink, not
|
||||
advance — the source of the old −0.8px measurement, whose previous-glyph
|
||||
advance treatment is semantically CORRECT once an icon renders after it).
|
||||
- Sub-pixel only for an icon after a glyph: layout starts it at that glyph's
|
||||
INK edge while measures count the glyph's advance, so the two differ by the
|
||||
glyph's right bearing — the same discrepancy 4.3.4 has between its own
|
||||
layout and measure loops. A string-final icon measures exactly (the gxu
|
||||
width loop ends on the last glyph's ink, which is where the icon starts).
|
||||
What remains is the icon region's ±0.5px placement rounding and the base
|
||||
loop's ~0 contribution for the payload-0 icon token.
|
||||
- An fs whose `fs+0xFC` cache was filled while the feature was toggled
|
||||
differently (`_classicapi_InlineTexEnable`) keeps its old base width until
|
||||
the next `SetText`/font change re-dirties it. Debug-toggle-only.
|
||||
|
||||
**Editbox measure stays raw (by design).** The caret/measure path reads the
|
||||
focused editbox's INPUT buffer in place, and the tokenizer stands down on it
|
||||
per-editbox (`TextInFocusedEditbox` pointer-range test — not a global focus
|
||||
check), so caret math measures the raw `|T…|t` literal the editbox renders.
|
||||
The width hook mirrors that with the `fs+0x120 & 0x1000` editable gate. One
|
||||
marginal wobble: the focused single-line chat editbox's display fontstring
|
||||
carries editable=0 (only multi-line editors set the bit), so the caret
|
||||
positioner's line-boundary branch (multi-line-only in practice) can see an
|
||||
icon-adjusted `FUN_00772890` on it. Closing that costs a per-token content
|
||||
compare, which is not worth it.
|
||||
### Edit boxes — 4.3.4's model, no special case
|
||||
|
||||
### Editbox exclusion — two COMPLEMENTARY signals (bit 6 + focus global)
|
||||
4.3.4's edit box renders `|T…|t` in its text: no frame code ever sets the
|
||||
FontString style bit 0x10000 that `ConvertStringFlags` maps to the gxu
|
||||
texture-off flag 0x1000. Its cursor treats the span as one markup token
|
||||
(`UpdateTextInfo` / `GetNumToLen`), and its `CopyToClipboard` strips with
|
||||
0x1500 — colours, links AND textures. Typed markup never renders because a
|
||||
typed `|` is stored as `||` (`CSimpleEditBox.cpp`, `ch == '|'`). 1.12 does
|
||||
the same input escape, so ClassicAPI's edit boxes need no special case:
|
||||
|
||||
Editable text must show raw, editable `|T…|t` markup, not rendered icons — you
|
||||
edit macros, tweak icon fields, copy/paste texture strings. **This is
|
||||
retail-correct, verified in-game on a modern client:** typing
|
||||
`|TInterface\Icons\INV_Misc_Coin_01:16|t` into the chat entry box OR a macro body
|
||||
shows it **raw** (no icon) — Blizzard neutralizes player-*typed* escape sequences
|
||||
as anti-abuse — while addon/system text (`AddMessage`, `GetCoinTextureString`)
|
||||
renders. Retail's discriminator is the text's SOURCE (trusted vs player-typed),
|
||||
not editbox-vs-display; a shift-clicked item link renders in the chat box because
|
||||
it's inserted through the trusted path, but a typed `|T`/`|c` does not. Our
|
||||
editbox-raw + display-renders split reproduces the same observable outcome. (One
|
||||
mechanism-level divergence we accept: our editbox-raw uses the GLOBAL focus signal
|
||||
below, so while an editbox is focused OTHER display text that rebuilds also
|
||||
briefly shows raw; retail suppresses per-source. Cosmetic, and a per-editbox fix
|
||||
would need hot-path text-buffer matching — not worth it.) 1.12's tokenizer,
|
||||
unlike 4.3.4's, has no per-render texture-disable bit (4.3.4 uses tokenizer flag
|
||||
**0x1000** to kill only `|T`, **0x800** to kill all escapes), and editbox text
|
||||
nodes are NOT distinguishable by class from the render node — the emitter can't
|
||||
reach the owning frame. It takes **two signals together**, not one (an earlier
|
||||
version of this note wrongly called the focus global a redundant "belt-and-
|
||||
suspenders fallback" — removing it renders the chat/copy boxes' `|T` as icons):
|
||||
- **Render** — the label FontString (`RebuildLabel` `0x77d4d0`,
|
||||
`SetText(buffer + scroll, 0)`, no pipe doubling) renders like any other,
|
||||
and its regions parent to the edit box, so a ScrollFrame clips them.
|
||||
- **Cursor** — the class array (`0x77ba90`) tokenizes through our tokenizer
|
||||
hook, so a span is one class-6 letter: Left/Right cross it in one step,
|
||||
Backspace deletes it whole, and an insert can never land inside it.
|
||||
Class 6 is also the safe class — a skip class (0/4) as the last token
|
||||
hangs the step loop (benilla ui/editboxmarkup "Faults"). 4.3.4 joins an
|
||||
icon with the following character into one step; ours is its own step.
|
||||
- **Caret x** — `FUN_0077DA80` measures the line through
|
||||
`FUN_00772AE0`, whose co-hook adds the icons.
|
||||
- **Rows (multi-line)** — `PlaceCaret` (`0x77da80`), `PlaceSelectionPiece`
|
||||
(`0x77de70`) and `GetIndexAtPoint` (`0x77d0d0`) step rows of one height
|
||||
(font height + spacing, pixel-aligned), as 4.3.4's `UpdateVisibleCursor`,
|
||||
`UpdateHighlightArea` and `ConvertCoordinateToIndex` do — while the render
|
||||
moves a tall-icon line and every line after it down by the overflow
|
||||
(`InitializeText`; the emitter co-hook) and the box grows to match (the
|
||||
string-height co-hook). 4.3.4 has that fault too; `EditBox::IconRows`
|
||||
(`src/editbox/IconRows.cpp`) gives each placer the render's move, from
|
||||
`Text::InlineTexture::LineIconShift` over the box's own line table: the
|
||||
click's y is moved onto the uniform row of the icon-aware row under it
|
||||
before the engine maps it; a selection piece is re-anchored after the
|
||||
engine placed it, with the engine's own x; the caret likewise, then
|
||||
`OnCursorChanged` fired once with the moved y through
|
||||
`FUN_FRAME_RUN_SCRIPT_VARIADIC` exactly as `PlaceCaret` fires it (its own
|
||||
fire muted for the call by emptying the slot), so
|
||||
`ScrollingEditBoxMixin:ScrollCursorIntoView` scrolls to the real caret.
|
||||
- **Click → cursor, single-line window** — `GetIndexAtPoint` (`0x77d0d0`),
|
||||
`RebuildLabel` and `MeasureWindow` count characters with
|
||||
`FUN_FONTSTRING_FIT_TEXT` (`0x772bc0` → `0x44d860` → `0x5c6c50`), whose
|
||||
kernel counts an icon as one character of no width. Its co-hook mirrors
|
||||
4.3.4's `GxuFontGetMaxCharsWithinWidth`: the run before each icon is fitted
|
||||
by the original with the budget the earlier icons left, and an icon fits
|
||||
when the icon-aware width through its end does.
|
||||
- **Ctrl+C** — `CopySelection` (`0x77e1d0`) pushes 0x500; patched to 4.3.4's
|
||||
0x1500, and the `GxuStripControlCodes` (`0x5c2590`) co-hook honours 0x1000
|
||||
by dropping spans and stray `|t`. Paste re-escapes every `|`
|
||||
(`InsertChar` per character), as in 4.3.4.
|
||||
|
||||
- **Node flag `[node+0x5c]` bit 6 (0x40) = EDITABLE** — catches the **macro
|
||||
editor** (`MacroFrameText`, flags `0x4D`), which builds its layout once,
|
||||
**un-focused**, so the focus global never sees it. But single-line inputs
|
||||
(chat entry box, `pfChatCopyBox`) have bit 6 **CLEAR** — their nodes are
|
||||
`0x0D`/`0x20D`, identical to display FontStrings — so bit 6 can't catch them.
|
||||
- **Focused-editbox global `DAT_00cf4dc8`** (`InputFocused()`; holds the editbox
|
||||
with keyboard focus, cleared by `ClearFocus` = `FUN_0077e410`) — catches those
|
||||
single-line inputs. It's GLOBAL (true UI-wide, not per-node), which is why,
|
||||
while an editbox is focused, other display text that *rebuilds* also briefly
|
||||
renders raw. That's an accepted tradeoff: there is no per-node signal for
|
||||
single-line inputs, and when you're in an editbox seeing raw markup is what you
|
||||
want anyway.
|
||||
|
||||
**Why the tokenizer's bit-6 gate is load-bearing for RENDER, not just measure:**
|
||||
when the emitter suppresses an editbox it delegates to the ORIGINAL emitter,
|
||||
which **re-enters the tokenizer** to lay out glyphs (`FUN_005ccbe0` →
|
||||
`FUN_005c2810`). On that render path `flags` is the node's own flags, so bit
|
||||
0x40 makes the tokenizer stand down and the `|T` draws as literal text. Drop the
|
||||
bit-6 gate and the (un-focused) macro editor draws **BLANK** — the tokenizer eats
|
||||
the span as a zero-width token and the original emitter renders nothing (verified
|
||||
in-game). Focused single-line inputs get the same "don't intercept" via
|
||||
`Suppressed()` → `InputFocused()`. So both the emitter and the tokenizer consult
|
||||
`Suppressed() || bit-6`; the two must agree or you get blank-vs-raw mismatches.
|
||||
|
||||
(Dead end: **monochrome render flag `DAT_00c2b9dc`** (`FUN_005c8b70` sets it per
|
||||
group) — un-set at MacroFrameText's build time and flickered as a flush gate.
|
||||
Dropped. The editbox class vtable is `0x0081c8c0`, its script-slot resolver
|
||||
`FUN_0077a310` with focus slots at `+0x438`/`+0x440` — useful if a frame-level
|
||||
hook is ever needed.)
|
||||
(History: before the span rule, every typed `|T` reached the emitter as a
|
||||
matching `||T`, so edit boxes were special-cased with three gates — node
|
||||
flag bit 6, a focused-buffer pointer test, and a content compare of the
|
||||
render line against the focused input — plus a re-entry bracket so the
|
||||
delegated raw layout did not draw blank. All removed. Node flag bit 6 is
|
||||
NON-SPACE-WRAP, not "editable"; the gate only worked because the macro
|
||||
editor sets it.)
|
||||
|
||||
### Descriptor fields + the vertical flip
|
||||
|
||||
@@ -544,9 +613,7 @@ backend note.)
|
||||
- **Still icon-blind** (accepted): the hyperlink hit-test past a tall icon
|
||||
OUTSIDE the link on the same line. The substring measure `FUN_00772AE0` was
|
||||
hooked once its real consumer surfaced (the tooltip auto-size measures
|
||||
wrapped SEGMENTS with it), and the trailing-icon width residual is trimmed
|
||||
to sub-pixel (`TrailingIconTrimPen` — a string-final icon reports its ink
|
||||
edge, not its advance, per the engine's own last-glyph convention).
|
||||
wrapped SEGMENTS with it).
|
||||
|
||||
## Goal & spec
|
||||
|
||||
@@ -556,9 +623,26 @@ Render the modern inline-texture escape (warcraft.wiki.gg/wiki/UI_escape_sequenc
|
||||
|Tpath:height[:width[:offsetX:offsetY[:texWidth:texHeight:left:right:top:bottom[:rV:gV:bV]]]]|t
|
||||
```
|
||||
|
||||
Atlas markup (`|A:atlas:…|a`) is Legion+ (needs an atlas DB 1.12 lacks) — out
|
||||
of scope. FileDataID paths are Legion+ — 1.12 resolves texture PATH strings
|
||||
only.
|
||||
Atlas markup is also supported:
|
||||
|
||||
```
|
||||
|A:atlasName:height:width[:offsetX:offsetY]|a
|
||||
```
|
||||
|
||||
It shares every scanner, measure, wrap and placement stage with `|T` — only the
|
||||
payload parser differs. `ParseAtlasIcon` resolves the name through
|
||||
`Texture::Atlas` and fills the SAME `IconDesc`, because an atlas is exactly the
|
||||
"path + normalized sub-rect" that struct already carries for a `|T` span with
|
||||
texcoords. A `height`/`width` of `0` resolves to the atlas's own pixel size at
|
||||
PARSE time (not the line font height `|T` falls back to), which is what keeps the
|
||||
rest of the pipeline marker-agnostic. Tiling atlases draw one quad, so
|
||||
`tilesHorizontally` / `tilesVertically` are not honored here.
|
||||
|
||||
Adding any further marker means touching `IconStartLen`, `FindIconClose`,
|
||||
`InlineSpanLen`, the ghost guard in `FlushLayout` — and, mandatory,
|
||||
`chat/IconFilter.cpp`, or the marker becomes a chat-spoof vector.
|
||||
|
||||
FileDataID paths are Legion+ — 1.12 resolves texture PATH strings only.
|
||||
|
||||
## Confirmed: 1.12 has ZERO inline-texture support
|
||||
|
||||
|
||||
@@ -213,6 +213,8 @@ filenames in Lua 5.0 by behaviour.
|
||||
|--------------|--------------------------|---------------------------------------------------|
|
||||
| `0x006F3140` | `setobj` / TValue copy | 16-byte memcpy |
|
||||
| `0x006F58B0` | `equalobj` | called by `lua_equal` |
|
||||
| `0x006F6050` | `luaD_precall` | frame setup for a call; Lua closure: reads Proto (`+0xc` of the closure), `is_vararg` at Proto `+0x46` → `adjust_varargs`, `numparams` `+0x45`, `maxstacksize` `+0x47`; C closure: reserves 0x14 slots and calls the C function |
|
||||
| `0x006F6200` | `adjust_varargs` | stock 5.0: pads missing fixed params with nil, then `luaH_new(L, nvar, 1)` + `luaH_setnum` per extra arg + sets field `"n"` and pushes the table. So EVERY call to a vararg Lua function allocates one table, whether the body uses `arg` or `...` — keep vararg helpers off per-frame paths |
|
||||
| `0x006F65A0` | `luaD_call` | the actual call dispatcher |
|
||||
| `0x006F6960` | `luaD_pcall` | protected call dispatcher |
|
||||
| `0x006F7340` | `luaD_growstack` (or checkstack) | invoked when stack needs space |
|
||||
|
||||
@@ -1259,4 +1259,42 @@
|
||||
2: 0x00706040 CreateFont
|
||||
3: 0x007060B0 CreateFrame
|
||||
|
||||
=== luaL_openlib base library Table 0x00811E28 Count=36 (not a FrameScript_RegisterFunction batch; see BlizzardScriptAPI.md §3) ===
|
||||
0: 0x00702770 setglobal
|
||||
1: 0x007027B0 getglobal
|
||||
2: 0x007027E0 debugload
|
||||
3: 0x007027F0 debuginfo
|
||||
4: 0x00702800 debugprint
|
||||
5: 0x00702810 debugdump
|
||||
6: 0x00702820 debugbreak
|
||||
7: 0x00702830 debugtimestamp
|
||||
8: 0x00702840 debugprofilestart
|
||||
9: 0x00702860 debugprofilestop
|
||||
10: 0x00702900 seterrorhandler
|
||||
11: 0x00702950 geterrorhandler
|
||||
12: 0x00702970 error
|
||||
13: 0x007029F0 getmetatable
|
||||
14: 0x00702A40 setmetatable
|
||||
15: 0x00702AC0 getfenv
|
||||
16: 0x00702BE0 setfenv
|
||||
17: 0x00702C90 next
|
||||
18: 0x00702CD0 ipairs
|
||||
19: 0x00702DA0 pairs
|
||||
20: 0x00702DF0 type
|
||||
21: 0x00702E30 tonumber
|
||||
22: 0x00702F40 tostring
|
||||
23: 0x007030A0 unpack
|
||||
24: 0x007030F0 rawequal
|
||||
25: 0x00703130 rawget
|
||||
26: 0x00703160 rawset
|
||||
27: 0x007031A0 assert
|
||||
28: 0x00703200 gcinfo
|
||||
29: 0x00703250 collectgarbage
|
||||
30: 0x00703280 loadstring
|
||||
31: 0x007032F0 pcall
|
||||
32: 0x00703340 xpcall
|
||||
33: 0x007033A0 date
|
||||
34: 0x007035A0 time
|
||||
35: 0x00703760 debugstack
|
||||
|
||||
|
||||
|
||||
+1
Submodule external/unordered_dense added at ddd49116e6
@@ -14,6 +14,7 @@
|
||||
#include "Common.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
|
||||
namespace Common {
|
||||
@@ -31,4 +32,74 @@ bool PatchBytes(void *dst, const void *src, size_t len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RedirectCall(uintptr_t site, uintptr_t expected, const void *target) {
|
||||
const auto *code = reinterpret_cast<const uint8_t *>(site);
|
||||
if (code[0] != 0xE8)
|
||||
return false;
|
||||
const auto callee = [site](int32_t rel) { return site + 5 + static_cast<uintptr_t>(rel); };
|
||||
int32_t rel = 0;
|
||||
std::memcpy(&rel, code + 1, sizeof rel);
|
||||
const uintptr_t to = reinterpret_cast<uintptr_t>(target);
|
||||
if (callee(rel) == to)
|
||||
return true;
|
||||
if (callee(rel) != expected)
|
||||
return false;
|
||||
rel = static_cast<int32_t>(to - (site + 5));
|
||||
return PatchBytes(reinterpret_cast<uint8_t *>(site) + 1, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target) {
|
||||
const auto *code = reinterpret_cast<const uint8_t *>(site);
|
||||
if (code[0] != 0x0F || (code[1] & 0xF0) != 0x80)
|
||||
return false;
|
||||
const auto dest = [site](int32_t rel) { return site + 6 + static_cast<uintptr_t>(rel); };
|
||||
int32_t rel = 0;
|
||||
std::memcpy(&rel, code + 2, sizeof rel);
|
||||
const uintptr_t to = reinterpret_cast<uintptr_t>(target);
|
||||
if (dest(rel) == to)
|
||||
return true;
|
||||
if (dest(rel) != expected)
|
||||
return false;
|
||||
rel = static_cast<int32_t>(to - (site + 6));
|
||||
return PatchBytes(reinterpret_cast<uint8_t *>(site) + 2, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
bool ReplaceWithCall(uintptr_t site, const uint8_t *original, size_t len, const void *target) {
|
||||
uint8_t code[64];
|
||||
if (len < 5 || len > sizeof code)
|
||||
return false;
|
||||
code[0] = 0xE8;
|
||||
const int32_t rel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(target) - (site + 5));
|
||||
std::memcpy(code + 1, &rel, sizeof rel);
|
||||
std::memset(code + 5, 0x90, len - 5);
|
||||
const void *at = reinterpret_cast<const void *>(site);
|
||||
if (std::memcmp(at, code, len) == 0)
|
||||
return true;
|
||||
if (std::memcmp(at, original, len) != 0)
|
||||
return false;
|
||||
return PatchBytes(reinterpret_cast<void *>(site), code, len);
|
||||
}
|
||||
|
||||
void BoundedCopy(char *dst, const char *src, size_t n) {
|
||||
if (dst == nullptr || n == 0)
|
||||
return;
|
||||
size_t i = 0;
|
||||
for (; src != nullptr && src[i] != '\0' && i + 1 < n; ++i)
|
||||
dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
void EnsureParentDirs(const char *relPath) {
|
||||
std::string accum;
|
||||
for (const char *p = relPath; *p != '\0'; ++p) {
|
||||
if (*p == '\\' || *p == '/') {
|
||||
if (!accum.empty())
|
||||
CreateDirectoryA(accum.c_str(), nullptr);
|
||||
accum.push_back('\\');
|
||||
} else {
|
||||
accum.push_back(*p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -11,8 +11,38 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Common {
|
||||
|
||||
bool PatchBytes(void *dst, const void *src, size_t len);
|
||||
|
||||
// Points the `E8 rel32` call at `site` to `target` when it still calls
|
||||
// `expected`; true when the call now reaches `target`. A call that reaches
|
||||
// anything else (another DLL's patch, a different build) is left alone.
|
||||
// Idempotent, so it can run on every registration pass.
|
||||
bool RedirectCall(uintptr_t site, uintptr_t expected, const void *target);
|
||||
|
||||
// RedirectCall for a `0F 8x rel32` conditional jump: points the taken branch
|
||||
// at `site` to `target` when it still reaches `expected`. Same contract.
|
||||
bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target);
|
||||
|
||||
// Writes `call target` plus NOP padding over the `len` bytes (5 to 64) of
|
||||
// code at `site` when they still hold `original`; true when the site
|
||||
// now calls `target`. A site holding anything else (another DLL's patch, a
|
||||
// different build) is left alone. Idempotent, like RedirectCall.
|
||||
bool ReplaceWithCall(uintptr_t site, const uint8_t *original, size_t len, const void *target);
|
||||
|
||||
// Bounded, always-terminated string copy: writes at most `n - 1`
|
||||
// characters plus the terminator, and treats a null `src` as empty. `n`
|
||||
// is the size of `dst`. Stands in for `strncpy`, which MSVC deprecates
|
||||
// (C4996) and which does not terminate on truncation.
|
||||
void BoundedCopy(char *dst, const char *src, size_t n);
|
||||
|
||||
// Creates every parent directory of `relPath` (backslash- or slash-separated,
|
||||
// e.g. "BlizzardInterfaceCode\FrameXML\Foo.lua"), relative to the working
|
||||
// directory. Directories that already exist are a no-op.
|
||||
void EnsureParentDirs(const char *relPath);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
+13
-4
@@ -15,6 +15,8 @@
|
||||
#include "Game.h"
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "config/CommandLine.h"
|
||||
#include "config/FileSwitch.h"
|
||||
#include "event/Custom.h"
|
||||
#include "player/NameCache.h"
|
||||
#include "text/InlineTexture.h"
|
||||
@@ -80,13 +82,11 @@ static void __stdcall LoadGlueScriptFunctions_h() {
|
||||
// Every Lua-side `frame:RegisterEvent(...)` is a chance to claim a slot
|
||||
// for any custom event still waiting. By this point the engine's table
|
||||
// is fully populated and SuperWoWhook / other DLLs have done their
|
||||
// post-rebuild writes, so the table state is settled and our backwards
|
||||
// walk finds genuine NULL slots near the tail.
|
||||
// post-rebuild writes, so the table state is settled and our walk from the
|
||||
// low end finds genuinely free slots.
|
||||
static void __fastcall FrameRegisterEvent_h(void *frame, void *edx,
|
||||
const char *eventName) {
|
||||
Event::Custom::RetryClaims();
|
||||
// Throttled-internally; no-op when the cache or scan toggle is off.
|
||||
Player::NameCache::Tick();
|
||||
FrameRegisterEvent_o(frame, edx, eventName);
|
||||
}
|
||||
|
||||
@@ -207,6 +207,15 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
|
||||
if (reason == DLL_PROCESS_ATTACH) {
|
||||
DisableThreadLibraryCalls(hModule);
|
||||
|
||||
// The ONE thing that has to happen this early, and the only modules this
|
||||
// file names directly. The engine parses its launch switches and reads
|
||||
// its config file during boot, several calls before VanillaFixes invokes
|
||||
// `Load`, so our switches cannot be wired from the normal install path.
|
||||
// Each is a single rel32 store, not a hook, so it carries none of the
|
||||
// loader-lock cost described above.
|
||||
Config::CommandLine::Install();
|
||||
Config::FileSwitch::Install();
|
||||
|
||||
// Install nothing here (loader lock — see the block comment above).
|
||||
//
|
||||
// Under VanillaFixes, `Load` is the ONLY install trigger. VfPatcher.dll
|
||||
|
||||
+289
-54
@@ -14,6 +14,7 @@
|
||||
#include "Game.h"
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "api/Documentation.h"
|
||||
|
||||
namespace Game {
|
||||
|
||||
@@ -23,6 +24,32 @@ void *ResolveUnitToken(const char *token) {
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
float PixelToInternal(float px) {
|
||||
const float mul = Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (div == 0.0f)
|
||||
return 0.0f;
|
||||
return px * mul / (div * static_cast<float>(Offsets::UI_COORD_SCALE_UNIT));
|
||||
}
|
||||
|
||||
double InternalToPixel(float internal) {
|
||||
const float mul = Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (mul == 0.0f)
|
||||
return 0.0;
|
||||
return static_cast<double>(internal) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
|
||||
}
|
||||
|
||||
bool StringToFlag(const char *text) {
|
||||
using StringToFlag_t = int(__fastcall *)(const char *text);
|
||||
return reinterpret_cast<StringToFlag_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(text) != 0;
|
||||
}
|
||||
|
||||
int StrCmpI(const char *a, const char *b, int n) {
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
return reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I)(a, b, n);
|
||||
}
|
||||
|
||||
namespace Lua {
|
||||
// Each entry binds a typed function pointer in `Game::Lua::` to the
|
||||
// corresponding raw VA in `Offsets`. The X-macro keeps the column-aligned
|
||||
@@ -44,8 +71,11 @@ namespace Lua {
|
||||
F(NewTable, lua_newtable, LUA_NEW_TABLE) \
|
||||
F(GetTable, lua_gettable, LUA_GET_TABLE) \
|
||||
F(RawGet, lua_rawget, LUA_RAW_GET) \
|
||||
F(RawGetI, lua_rawgeti, LUA_RAWGETI) \
|
||||
F(SetTable, lua_settable, LUA_SET_TABLE) \
|
||||
F(RawSet, lua_rawset, LUA_RAW_SET) \
|
||||
F(GetMetatable,lua_getmetatable,LUA_GET_METATABLE)\
|
||||
F(SetMetatable,lua_setmetatable,LUA_SET_METATABLE)\
|
||||
F(Insert, lua_insert, LUA_INSERT) \
|
||||
F(Remove, lua_remove, LUA_REMOVE) \
|
||||
F(GetTop, lua_gettop, LUA_GET_TOP) \
|
||||
@@ -54,6 +84,7 @@ namespace Lua {
|
||||
F(PCall, lua_pcall, LUA_PCALL) \
|
||||
F(Next, lua_next, LUA_NEXT) \
|
||||
F(Type, lua_type, LUA_TYPE) \
|
||||
F(TypeName, lua_typename, LUA_TYPE_NAME) \
|
||||
F(Error, lua_error, LUA_ERROR) \
|
||||
F(ToPointer, lua_topointer, LUA_TO_POINTER) \
|
||||
F(ToThread, lua_tothread, LUA_TO_THREAD) \
|
||||
@@ -64,7 +95,10 @@ namespace Lua {
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
|
||||
F(SetN, luaL_setn, LUAL_SETN) \
|
||||
F(CheckStack, lua_checkstack, LUA_CHECK_STACK)
|
||||
F(Ref, luaL_ref, LUA_REF_REF) \
|
||||
F(Unref, luaL_unref, LUA_REF_UNREF) \
|
||||
F(CheckStack, lua_checkstack, LUA_CHECK_STACK) \
|
||||
F(Concat, lua_concat, LUA_CONCAT)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
@@ -98,26 +132,114 @@ void *ResolveObject(void *L, int idx) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void RegisterGlobalFunction(const char *name, CFunction func) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
int ObjectLuaRef(void *object) {
|
||||
using ScriptRegister_t = void(__thiscall *)(void *object, void *nameOrNull);
|
||||
if (Game::Read<int>(object, Offsets::OFF_COBJECT_LUA_REFCOUNT) == 0)
|
||||
reinterpret_cast<ScriptRegister_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER)(object, nullptr);
|
||||
return Game::Read<int>(object, Offsets::OFF_COBJECT_LUA_REF);
|
||||
}
|
||||
|
||||
// Aliased to the same engine entry; the engine reads VAR_LUA_STATE
|
||||
// internally to decide which state to write to, and the glue hook
|
||||
// runs while that pointer is set to the glue state. Wrapped as a
|
||||
// named function (rather than `using RegisterGlueFunction = ...`) so
|
||||
// callers express intent at the call site.
|
||||
void RegisterGlueFunction(const char *name, CFunction func) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
void PushObject(void *L, void *object) {
|
||||
RawGetI(L, REGISTRY_INDEX, ObjectLuaRef(object));
|
||||
}
|
||||
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count) {
|
||||
auto fn = reinterpret_cast<RegisterFrameMethods_t>(Offsets::FUN_REGISTER_FRAME_METHODS);
|
||||
fn(table, count, context);
|
||||
bool ToFlag(void *L, int index, bool defaultValue) {
|
||||
// Returns a full int in EAX on every path (Ghidra's `bool` is wrong).
|
||||
using ToFlag_t = int(__fastcall *)(void *L, int index, int defaultValue);
|
||||
return reinterpret_cast<ToFlag_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, index,
|
||||
defaultValue ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
const char *ObjectName(void *object) {
|
||||
using GetName_t = const char *(__thiscall *)(void *);
|
||||
const void *vtable = Game::Read<const void *>(object, 0);
|
||||
return Game::Read<GetName_t>(vtable, Offsets::VTBL_SCRIPTOBJECT_GET_NAME)(object);
|
||||
}
|
||||
|
||||
const char *ObjectDisplayName(void *object) {
|
||||
const char *name = ObjectName(object);
|
||||
return name != nullptr ? name
|
||||
: reinterpret_cast<const char *>(Offsets::VAR_STR_UNNAMED);
|
||||
}
|
||||
|
||||
namespace {
|
||||
using FrameScriptIsA_t = char(__thiscall *)(void *self, int typeId);
|
||||
|
||||
const char *EngineString(uintptr_t address) {
|
||||
return reinterpret_cast<const char *>(address);
|
||||
}
|
||||
|
||||
// Shared body: the engine's prologue with N accepted type ids.
|
||||
void *ResolveChecked(void *L, int idx, const uintptr_t *typeIdVars, int count,
|
||||
bool raiseError) {
|
||||
if (Type(L, idx) != TYPE_TABLE) {
|
||||
if (raiseError)
|
||||
Error(L, EngineString(Offsets::STR_FS_THIS_NON_TABLE));
|
||||
return nullptr;
|
||||
}
|
||||
void *obj = ResolveObject(L, idx);
|
||||
if (obj == nullptr) {
|
||||
if (raiseError)
|
||||
Error(L, EngineString(Offsets::STR_FS_THIS_NON_OBJECT));
|
||||
return nullptr;
|
||||
}
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (IsA(obj, typeIdVars[i]))
|
||||
return obj;
|
||||
}
|
||||
if (raiseError)
|
||||
Error(L, EngineString(Offsets::STR_FS_WRONG_OBJECT_TYPE));
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int FrameScriptTypeId(uintptr_t typeIdVar) {
|
||||
auto &typeId = Game::Ref<int>(typeIdVar);
|
||||
if (typeId == 0) {
|
||||
auto &counter = Game::Ref<int>(Offsets::VAR_FRAMESCRIPT_TYPE_ID_COUNTER);
|
||||
counter += 1;
|
||||
typeId = counter;
|
||||
}
|
||||
return typeId;
|
||||
}
|
||||
|
||||
bool IsA(void *object, uintptr_t typeIdVar) {
|
||||
auto **vtable = *reinterpret_cast<void ***>(object);
|
||||
auto isA = reinterpret_cast<FrameScriptIsA_t>(
|
||||
vtable[Offsets::OFF_VMT_FRAMESCRIPT_ISA / sizeof(void *)]);
|
||||
return isA(object, FrameScriptTypeId(typeIdVar)) != 0;
|
||||
}
|
||||
|
||||
void *ResolveTypedObject(void *L, int idx, uintptr_t typeIdVar, bool raiseError) {
|
||||
return ResolveChecked(L, idx, &typeIdVar, 1, raiseError);
|
||||
}
|
||||
|
||||
void *ResolveTypedObjectAny(void *L, int idx, const uintptr_t *typeIdVars,
|
||||
int count, bool raiseError) {
|
||||
return ResolveChecked(L, idx, typeIdVars, count, raiseError);
|
||||
}
|
||||
|
||||
void *ResolveTooltip(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_GAMETOOLTIP_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveFrame(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_FRAME_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveRegion(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_REGION_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveTexture(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_TEXTURE_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveFontString(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_FONTSTRING_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveEditBox(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_EDITBOX_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveModel(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_MODEL_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
|
||||
// Looks up `_G[name]`. If absent, creates a fresh table and binds it.
|
||||
@@ -135,11 +257,110 @@ void EnsureGlobalTable(void *L, const char *name) {
|
||||
Insert(L, -2); // [tbl, name, tbl]
|
||||
SetTable(L, GLOBALS_INDEX); // _G[name] = tbl; pops k+v. [tbl]
|
||||
}
|
||||
|
||||
// Same, one level down: ensures `parent[name]` is a table, with `parent`
|
||||
// on top of the stack. Leaves `[parent, sub]`. Raw access, so a namespace
|
||||
// carrying a metatable can't divert the read or the write.
|
||||
void EnsureSubTable(void *L, const char *name) {
|
||||
PushString(L, name); // [parent, name]
|
||||
RawGet(L, -2); // [parent, parent[name]]
|
||||
if (Type(L, -1) == TYPE_TABLE)
|
||||
return;
|
||||
SetTop(L, -2); // pop the non-table. [parent]
|
||||
NewTable(L); // [parent, sub]
|
||||
PushString(L, name); // [parent, sub, name]
|
||||
PushValue(L, -2); // [parent, sub, name, sub]
|
||||
RawSet(L, -4); // parent[name] = sub. [parent, sub]
|
||||
}
|
||||
|
||||
// The escape-hatch namespace. Registering a name is not the same as owning
|
||||
// it: the engine's registrars write `_G`, FrameXML and every addon load
|
||||
// after that, and whoever writes last wins with no error and no way for the
|
||||
// loser to notice. `GetServerTime` is the case on record — a client whose
|
||||
// GameTime.lua declares its own replaced ours on every login.
|
||||
//
|
||||
// So every name the registrars below bind is also bound under
|
||||
// `_G.ClassicAPI`, which nothing else writes. Two properties earn their
|
||||
// keep, and both depend on doing this HERE rather than from Lua:
|
||||
//
|
||||
// - It runs at registration, so the mirror captures OUR function. A
|
||||
// Lua-side `ClassicAPI.X = X` in the embedded addon would run after
|
||||
// FrameXML and faithfully copy the clobbered version.
|
||||
// - It mirrors by VALUE, reading the name back instead of making a
|
||||
// second closure over the same C function. So `ClassicAPI.X == X`
|
||||
// holds until something replaces the global, and the pair doubles as
|
||||
// a clobber check for addons and for us.
|
||||
//
|
||||
// Not covered: frame methods (`obj:Method()` lives in the per-frame-type
|
||||
// registry, not `_G`) and `RegisterIntegerEnum` values (a mirrored number
|
||||
// is a copy, not a reference).
|
||||
constexpr const char *kMirrorTable = "ClassicAPI";
|
||||
|
||||
void MirrorRegistration(const char *tableName, const char *name) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = GetTop(L);
|
||||
|
||||
// Read back whatever the registration actually bound.
|
||||
if (tableName != nullptr) {
|
||||
EnsureGlobalTable(L, tableName); // [src]
|
||||
PushString(L, name); // [src, name]
|
||||
RawGet(L, -2); // [src, value]
|
||||
} else {
|
||||
PushString(L, name); // [name]
|
||||
GetTable(L, GLOBALS_INDEX); // [value]
|
||||
}
|
||||
if (Type(L, -1) != TYPE_FUNCTION) {
|
||||
SetTop(L, top); // registration didn't land; nothing to mirror
|
||||
return;
|
||||
}
|
||||
const int valueIdx = GetTop(L); // absolute, so the pushes below can't skew it
|
||||
|
||||
EnsureGlobalTable(L, kMirrorTable); // [..., mirror]
|
||||
if (tableName != nullptr)
|
||||
EnsureSubTable(L, tableName); // [..., mirror, sub]
|
||||
PushString(L, name);
|
||||
PushValue(L, valueIdx);
|
||||
RawSet(L, -3); // target[name] = value
|
||||
SetTop(L, top);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void RegisterGlobalFunction(const char *name, CFunction func, const Doc::Function *doc) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
MirrorRegistration(nullptr, name);
|
||||
Api::Documentation::RecordGlobal(name, doc);
|
||||
}
|
||||
|
||||
// Aliased to the same engine entry; the engine reads VAR_LUA_STATE
|
||||
// internally to decide which state to write to, and the glue hook
|
||||
// runs while that pointer is set to the glue state. Wrapped as a
|
||||
// named function (rather than `using RegisterGlueFunction = ...`) so
|
||||
// callers express intent at the call site.
|
||||
void RegisterGlueFunction(const char *name, CFunction func, const Doc::Function *doc) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
// `State()` is the glue state here for the same reason the engine
|
||||
// registrar writes there, so the mirror lands on the glue `_G`.
|
||||
MirrorRegistration(nullptr, name);
|
||||
Api::Documentation::RecordGlobal(name, doc);
|
||||
}
|
||||
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
|
||||
const Doc::Method *docs, int docCount) {
|
||||
auto fn = reinterpret_cast<RegisterFrameMethods_t>(Offsets::FUN_REGISTER_FRAME_METHODS);
|
||||
fn(table, count, context);
|
||||
Api::Documentation::RecordFrameMethods(context, table, count, docs, docCount);
|
||||
}
|
||||
|
||||
// Registers `func` at `_G[tableName][methodName]`. If the namespace
|
||||
// doesn't already exist, creates an empty table for it.
|
||||
void RegisterTableFunction(const char *tableName, const char *methodName, CFunction func) {
|
||||
void RegisterTableFunction(const char *tableName, const char *methodName, CFunction func,
|
||||
const Doc::Function *doc) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
@@ -148,10 +369,41 @@ void RegisterTableFunction(const char *tableName, const char *methodName, CFunct
|
||||
PushCClosure(L, func, 0); // [tbl, methodName, closure]
|
||||
SetTable(L, -3); // tbl[m]=c; pops k+v. [tbl]
|
||||
SetTop(L, -2); // pop tbl. []
|
||||
MirrorRegistration(tableName, methodName);
|
||||
Api::Documentation::RecordTable(tableName, methodName, doc);
|
||||
}
|
||||
|
||||
// `_G[alias] = _G[tableName][methodName]`, by VALUE. The engine binds its
|
||||
// short aliases this way (`tinsert = tab.insert` in the compat snippet it
|
||||
// runs at Lua init), so a module that registers over a library function has
|
||||
// to re-bind the alias to the same closure — that keeps `tinsert ==
|
||||
// table.insert` true, as the engine left it, instead of minting a second
|
||||
// closure. A plain raw write, exactly what the snippet's assignment did. No-op
|
||||
// if the source is not a function (nothing was registered to alias).
|
||||
void RegisterGlobalAlias(const char *alias, const char *tableName, const char *methodName,
|
||||
const Doc::Function *doc) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = GetTop(L);
|
||||
EnsureGlobalTable(L, tableName); // [tbl]
|
||||
PushString(L, methodName); // [tbl, m]
|
||||
RawGet(L, -2); // [tbl, value]
|
||||
if (Type(L, -1) != TYPE_FUNCTION) {
|
||||
SetTop(L, top);
|
||||
return;
|
||||
}
|
||||
PushString(L, alias); // [tbl, value, alias]
|
||||
PushValue(L, -2); // [tbl, value, alias, value]
|
||||
RawSet(L, GLOBALS_INDEX); // _G[alias] = value. [tbl, value]
|
||||
SetTop(L, top);
|
||||
MirrorRegistration(nullptr, alias);
|
||||
Api::Documentation::RecordGlobal(alias, doc);
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count) {
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
@@ -165,6 +417,7 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
}
|
||||
SetTable(L, -3); // parentTbl[sub] = subTbl
|
||||
SetTop(L, -2); // pop parentTbl
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
}
|
||||
|
||||
void SetFieldNumber(void *L, const char *key, double value) {
|
||||
@@ -185,6 +438,12 @@ void SetFieldBool(void *L, const char *key, bool value) {
|
||||
SetTable(L, -3);
|
||||
}
|
||||
|
||||
void SetFieldNil(void *L, const char *key) {
|
||||
PushString(L, key);
|
||||
PushNil(L);
|
||||
SetTable(L, -3);
|
||||
}
|
||||
|
||||
void SetGlobalNumber(void *L, const char *name, double value) {
|
||||
PushString(L, name);
|
||||
PushNumber(L, value);
|
||||
@@ -229,45 +488,14 @@ void PushLocalizedString(void *L, const char *globalName, const char *fallback)
|
||||
}
|
||||
}
|
||||
|
||||
// In-place `string.gsub(stack[-1], pattern, replacement)` — replaces
|
||||
// the string at the top of the stack with the substitution result.
|
||||
// Discards gsub's second return (the substitution count) by
|
||||
// requesting only one return from `Call`.
|
||||
static void GsubInPlace(void *L, const char *pattern, const char *replacement) {
|
||||
PushString(L, "string");
|
||||
GetTable(L, GLOBALS_INDEX);
|
||||
PushString(L, "gsub");
|
||||
GetTable(L, -2); // [..., str, string, gsub]
|
||||
Remove(L, -2); // [..., str, gsub]
|
||||
Insert(L, -2); // [..., gsub, str]
|
||||
PushString(L, pattern);
|
||||
PushString(L, replacement);
|
||||
Call(L, 3, 1); // [..., result]
|
||||
}
|
||||
|
||||
void PushLocalizedFormatInt(void *L, const char *globalName,
|
||||
const char *fallback, int n) {
|
||||
// Resolve `string.format` from _G.
|
||||
PushString(L, "format");
|
||||
GetTable(L, GLOBALS_INDEX);
|
||||
PushLocalizedString(L, globalName, fallback);
|
||||
|
||||
// Expand WoW's `|4singular:plural;` (and 3-form
|
||||
// `|4nom:gen_sing:plural;`) pluralization template by piggybacking
|
||||
// on Lua's `string.gsub`. The 2- vs 3-form distinction is locale
|
||||
// grammar (Slavic counting has a separate genitive-singular form
|
||||
// for 2..4); we collapse to nominative/plural since vanilla has
|
||||
// no proper locale-aware counting rules. 3-form pattern runs
|
||||
// first because it's more specific. Vanilla's `string.format`
|
||||
// doesn't process `|4` natively, so without this the template
|
||||
// would render literally.
|
||||
const char *picker3 = (n == 1) ? "%1" : "%3";
|
||||
const char *picker2 = (n == 1) ? "%1" : "%2";
|
||||
GsubInPlace(L, "|4([^:;]+):([^:;]+):([^;]+);", picker3);
|
||||
GsubInPlace(L, "|4([^:;]+):([^;]+);", picker2);
|
||||
|
||||
PushNumber(L, static_cast<double>(n));
|
||||
Call(L, 2, 1); // format(expandedFmt, n) → string
|
||||
Call(L, 2, 1); // format(fmt, n) → string
|
||||
}
|
||||
} // namespace Lua
|
||||
|
||||
@@ -275,7 +503,7 @@ namespace Console {
|
||||
namespace {
|
||||
using RegisterCommand_t = int(__fastcall *)(const char *name, void *handler,
|
||||
int category, const char *description);
|
||||
using Write_t = void(__fastcall *)(const char *line, int colorFlag);
|
||||
using Write_t = void(__fastcall *)(const char *line, int color);
|
||||
} // namespace
|
||||
|
||||
void RegisterCommand(const char *name, CommandHandler handler, int category,
|
||||
@@ -284,10 +512,13 @@ void RegisterCommand(const char *name, CommandHandler handler, int category,
|
||||
fn(name, reinterpret_cast<void *>(handler), category, description);
|
||||
}
|
||||
|
||||
void Write(const char *line) {
|
||||
void Write(const char *line, Color color) {
|
||||
auto fn = reinterpret_cast<Write_t>(Offsets::FUN_CONSOLE_WRITE);
|
||||
fn(line, 0);
|
||||
fn(line, color);
|
||||
}
|
||||
|
||||
void(__cdecl *const Printf)(const char *format, ...) =
|
||||
reinterpret_cast<void(__cdecl *)(const char *, ...)>(Offsets::FUN_CONSOLE_PRINTF);
|
||||
} // namespace Console
|
||||
|
||||
namespace {
|
||||
@@ -302,8 +533,10 @@ ModuleAutoRegister::ModuleAutoRegister(Fn f) : fn(f), next(g_moduleHead) {
|
||||
}
|
||||
|
||||
void RunModuleRegistrations() {
|
||||
Api::Documentation::BeginPass(Api::Documentation::ENV_GAME);
|
||||
for (auto *node = g_moduleHead; node != nullptr; node = node->next)
|
||||
node->fn();
|
||||
Api::Documentation::EndPass();
|
||||
}
|
||||
|
||||
GlueModuleAutoRegister::GlueModuleAutoRegister(Fn f)
|
||||
@@ -312,8 +545,10 @@ GlueModuleAutoRegister::GlueModuleAutoRegister(Fn f)
|
||||
}
|
||||
|
||||
void RunGlueModuleRegistrations() {
|
||||
Api::Documentation::BeginPass(Api::Documentation::ENV_GLUE);
|
||||
for (auto *node = g_glueModuleHead; node != nullptr; node = node->next)
|
||||
node->fn();
|
||||
Api::Documentation::EndPass();
|
||||
}
|
||||
|
||||
ReloadAutoRegister::ReloadAutoRegister(Fn f) : fn(f), next(g_reloadHead) {
|
||||
|
||||
+317
-16
@@ -13,6 +13,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Game {
|
||||
@@ -57,6 +58,22 @@ inline T &Ref(uintptr_t address) {
|
||||
return *reinterpret_cast<T *>(address);
|
||||
}
|
||||
|
||||
// Calls `fn(object)` for each object of one of a frame's intrusive lists,
|
||||
// OFF_FRAME_REGION_LIST or OFF_FRAME_CHILD_LIST: node+4 next, node+8 object,
|
||||
// a NULL or low-bit-1 link ends it - the walk the engine's own frame code
|
||||
// does (the alpha setter, the bounds walk 0x00786F80).
|
||||
template <typename Fn>
|
||||
void ForEachFrameListObject(void *frame, uintptr_t listOffset, Fn fn) {
|
||||
uintptr_t link = Read<uintptr_t>(frame, listOffset);
|
||||
while (link != 0 && (link & 1) == 0) {
|
||||
void *obj = *reinterpret_cast<void *const *>(link + 8);
|
||||
const uintptr_t next = *reinterpret_cast<const uintptr_t *>(link + 4);
|
||||
if (obj != nullptr)
|
||||
fn(obj);
|
||||
link = next;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Engine primitives ------------------------------------------------------
|
||||
|
||||
// Resolve a unit token (`"player"`, `"target"`, `"party3"`, `"nameplate1"`,
|
||||
@@ -70,6 +87,15 @@ inline T &Ref(uintptr_t address) {
|
||||
// across a dozen modules.
|
||||
void *ResolveUnitToken(const char *token);
|
||||
|
||||
// UI pixels <-> internal layout (anchor) units. Script_SetPoint converts its
|
||||
// pixel offsets with `px * [VAR_COORD_DDC_WIDTH_SCALE] / ([VAR_COORD_ASPECT_COMPENSATION]
|
||||
// * UI_COORD_SCALE_UNIT)`; Script_GetLeft and Script_GetStringWidth push the
|
||||
// inverse. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// (OFF_REGION_EFFECTIVE_SCALE) first. Both return 0 while the scale globals
|
||||
// are unset, so an early caller gets a zero offset rather than a wild one.
|
||||
float PixelToInternal(float px);
|
||||
double InternalToPixel(float internal);
|
||||
|
||||
using FrameScript_Initialize_t = bool(__fastcall *)();
|
||||
using LoadScriptFunctions_t = void(__fastcall *)();
|
||||
|
||||
@@ -78,6 +104,126 @@ using LoadScriptFunctions_t = void(__fastcall *)();
|
||||
// registering its 109 glue functions. See FUN_LOAD_GLUE_SCRIPT_FUNCTIONS.
|
||||
using LoadGlueScriptFunctions_t = void(__stdcall *)();
|
||||
|
||||
// The engine's text-to-flag reader (FUN_XML_ATTR_TO_BOOL, 4.3.4's
|
||||
// `StringToBOOL(const char *)`), which XML boolean attributes go through:
|
||||
// true for text starting 1-9/T/Y/t/y or equal to "on"/"enabled" (any case),
|
||||
// false for anything else, including null.
|
||||
bool StringToFlag(const char *text);
|
||||
|
||||
// Storm's case-insensitive compare, SStrCmpI (FUN_SSTR_CMP_I) — the one the
|
||||
// engine uses for names, tokens and tags: at most `n` characters, ASCII case
|
||||
// folded, <0 / 0 / >0 like strcmp. The default `n` is Storm's "whole string".
|
||||
// Always call it through here: the engine function is `__stdcall` (RET 0xC),
|
||||
// and a `__cdecl` declaration drifts ESP by 12 per call.
|
||||
int StrCmpI(const char *a, const char *b, int n = 0x7FFFFFFF);
|
||||
|
||||
// --- API documentation descriptors -----------------------------------------
|
||||
//
|
||||
// Every Lua registration (see the `Register*` functions in `Game::Lua`) can
|
||||
// carry a descriptor: the signature the function presents to Lua — typed
|
||||
// arguments and returns, nilability, defaults — plus a one-sentence summary.
|
||||
// The descriptors are the single source of truth for the API's SHAPE.
|
||||
// `src/api/Documentation.cpp` records each registration on the first pass of
|
||||
// each Lua state and exports the result on demand as Blizzard
|
||||
// `APIDocumentation` tables (`C_APIDocumentation.GetSystems` / `GetSystem`),
|
||||
// which the embedded addon's `/api` browser renders. Prose beyond one
|
||||
// sentence stays in the docs; a descriptor is shape, not manual.
|
||||
//
|
||||
// Rules:
|
||||
// - One descriptor per REGISTERED NAME, not per C function. The same
|
||||
// `Script_*` bound twice with different return conventions
|
||||
// (`IsUsableSpell` → 1/nil pairs, `C_Spell.IsSpellUsable` → booleans)
|
||||
// gets two descriptors; a pair with the same shape may share one.
|
||||
// - `type` uses Blizzard's spellings — "number", "string", "bool",
|
||||
// "table", "luaIndex", "UnitToken", "SpellIdentifier", … — or the Name
|
||||
// of a `Structure` / enumeration declared elsewhere, which the browser
|
||||
// renders as a link.
|
||||
// - `Function::system` names the System a GLOBAL belongs to; convention
|
||||
// `<Area>Globals` ("SpellGlobals", "ItemGlobals"). Table functions
|
||||
// derive theirs from the table ("C_Spell" → System "Spell", Namespace
|
||||
// "C_Spell"); frame methods from the registry. A global's system must
|
||||
// not reuse a namespaced System's Name — the browser would prefix it.
|
||||
// - Descriptors are `const` objects with constant initializers (string
|
||||
// literals + file-scope arrays): no heap, no dynamic init.
|
||||
// - Summaries are user-facing text: one sentence, plain words, present
|
||||
// tense, no patch versions.
|
||||
namespace Doc {
|
||||
|
||||
struct Field {
|
||||
const char *name;
|
||||
const char *type;
|
||||
bool nilable;
|
||||
const char *defaultValue; // Lua literal ("false", "0", "\"player\""), or nullptr
|
||||
const char *doc; // one sentence, or nullptr
|
||||
};
|
||||
|
||||
// Required argument / always-present return.
|
||||
constexpr Field Req(const char *name, const char *type, const char *doc = nullptr) {
|
||||
return {name, type, false, nullptr, doc};
|
||||
}
|
||||
// Optional: nilable when no default is given, otherwise non-nil with that
|
||||
// default (Blizzard marks defaulted arguments `Nilable = false` + `Default`).
|
||||
constexpr Field Opt(const char *name, const char *type, const char *dflt = nullptr,
|
||||
const char *doc = nullptr) {
|
||||
return {name, type, dflt == nullptr, dflt, doc};
|
||||
}
|
||||
// Variable arguments / returns.
|
||||
constexpr Field Vararg(const char *type, const char *doc = nullptr) {
|
||||
return {"...", type, true, nullptr, doc};
|
||||
}
|
||||
|
||||
// A view over a file-scope `const Field[]`; `{}` means none.
|
||||
struct FieldList {
|
||||
const Field *data = nullptr;
|
||||
int count = 0;
|
||||
constexpr FieldList() = default;
|
||||
template <size_t N>
|
||||
constexpr FieldList(const Field (&a)[N]) : data(a), count(static_cast<int>(N)) {}
|
||||
};
|
||||
|
||||
// A registered function. Positional init: `{summary, args, rets}` for a
|
||||
// table function, `{summary, args, rets, "XGlobals"}` for a global, and
|
||||
// `{summary, args, rets, nullptr, true}` for a namespaced ClassicAPI
|
||||
// extension.
|
||||
struct Function {
|
||||
const char *summary;
|
||||
FieldList args;
|
||||
FieldList rets;
|
||||
const char *system; // globals / glue / aliases only; see the rules above
|
||||
bool extension; // a ClassicAPI-original API, not a Blizzard one
|
||||
};
|
||||
|
||||
// Pairs a frame method with its descriptor BY NAME (the engine's method
|
||||
// tables are name-keyed; `SetShown` exists on four registries with four
|
||||
// function pointers). Order is free; unmatched names are reported.
|
||||
struct Method {
|
||||
const char *name;
|
||||
const Function *doc;
|
||||
};
|
||||
|
||||
// A table shape a function returns or accepts → `Tables{Type="Structure"}`
|
||||
// in `system`. Self-registering: nothing binds a structure to Lua, so it
|
||||
// chains onto a static list at construction. Declare it next to the
|
||||
// function that returns it.
|
||||
struct Structure {
|
||||
Structure(const char *name, const char *system, FieldList fields,
|
||||
const char *summary = nullptr);
|
||||
const char *name;
|
||||
const char *system;
|
||||
FieldList fields;
|
||||
const char *summary;
|
||||
const Structure *next;
|
||||
};
|
||||
|
||||
// A custom event's payload, attached to its `Event::Custom::AutoReserve`.
|
||||
struct Event {
|
||||
const char *system;
|
||||
const char *summary;
|
||||
FieldList payload;
|
||||
};
|
||||
|
||||
} // namespace Doc
|
||||
|
||||
namespace Lua {
|
||||
using CFunction = int(__fastcall *)(void *L);
|
||||
|
||||
@@ -121,8 +267,17 @@ using lua_pushcclosure_t = void(__fastcall *)(void *L, CFunction fn, int upvals)
|
||||
using lua_newtable_t = void(__fastcall *)(void *L);
|
||||
using lua_gettable_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_rawget_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pushes t[n] for the table at idx, raw (no metamethods). With idx =
|
||||
// REGISTRY_INDEX it pushes a value stored under an integer ref.
|
||||
using lua_rawgeti_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
using lua_settable_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_rawset_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pushes the metatable of the value at idx and returns 1, or pushes
|
||||
// nothing and returns 0 when it has none.
|
||||
using lua_getmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
// Pops the table at the top and installs it as the metatable of the value
|
||||
// at idx; returns 1.
|
||||
using lua_setmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_insert_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_remove_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_gettop_t = int(__fastcall *)(void *L);
|
||||
@@ -131,6 +286,7 @@ using lua_call_t = void(__fastcall *)(void *L, int nargs, int nresults);
|
||||
using lua_pcall_t = int(__fastcall *)(void *L, int nargs, int nresults, int errfunc);
|
||||
using lua_next_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_type_t = int(__fastcall *)(void *L, int index);
|
||||
using lua_typename_t = const char *(__fastcall *)(void *L, int type);
|
||||
// The 1.12 `lua_error` wrapper at `0x6F4940` is actually variadic —
|
||||
// it does `lua_pushvfstring(L, fmt, args)` then prepends `luaL_where`
|
||||
// and throws. Existing callers pass a single literal (no `%`), which
|
||||
@@ -153,7 +309,16 @@ using lua_resume_t = int(__fastcall *)(void *L, int nargs);
|
||||
using lua_yield_t = int(__fastcall *)(void *L, int nresults);
|
||||
using luaL_argerror_t = void(__fastcall *)(void *L, int narg, const char *msg);
|
||||
using luaL_setn_t = void(__fastcall *)(void *L, int t, int n);
|
||||
// Pops the top value into the table at `t` under a fresh integer key and
|
||||
// returns the key (LUA_REFNIL, -1, for a nil). With REGISTRY_INDEX it anchors
|
||||
// a value across calls; read it back with RawGetI(L, REGISTRY_INDEX, ref).
|
||||
using luaL_ref_t = int(__fastcall *)(void *L, int t);
|
||||
// Releases a key returned by luaL_ref; ignores LUA_REFNIL / LUA_NOREF.
|
||||
using luaL_unref_t = void(__fastcall *)(void *L, int t, int ref);
|
||||
using lua_checkstack_t = int(__fastcall *)(void *L, int size);
|
||||
// Pops the top `n` values and pushes their `..` concatenation; n == 0 pushes
|
||||
// "" and n == 1 leaves the value untouched.
|
||||
using lua_concat_t = void(__fastcall *)(void *L, int n);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -169,6 +334,13 @@ extern const lua_pushboolean_t PushBoolean;
|
||||
// engine's `lua_pushboolean` ABI wants `int` (any non-zero = true), so
|
||||
// this just funnels through the raw binding with a single conversion.
|
||||
inline void PushBool(void *L, bool b) { PushBoolean(L, static_cast<int>(b)); }
|
||||
// The engine's script-flag argument reader (FUN_LUA_TO_BOOLEAN_LOOSE, 4.3.4's
|
||||
// `StringToBOOL(L, index, default)`), which every engine method taking an
|
||||
// optional flag uses: nil is false, a boolean is itself, a number is its
|
||||
// truncated int (so 0.5 is false), a string reads as StringToFlag except that
|
||||
// text it does not recognize is `defaultValue`, and any other type is
|
||||
// `defaultValue`.
|
||||
bool ToFlag(void *L, int index, bool defaultValue = false);
|
||||
extern const lua_pushstring_t PushString;
|
||||
extern const lua_pushlstring_t PushLString;
|
||||
extern const lua_pushvalue_t PushValue;
|
||||
@@ -176,8 +348,11 @@ extern const lua_pushcclosure_t PushCClosure;
|
||||
extern const lua_newtable_t NewTable;
|
||||
extern const lua_gettable_t GetTable;
|
||||
extern const lua_rawget_t RawGet;
|
||||
extern const lua_rawgeti_t RawGetI;
|
||||
extern const lua_settable_t SetTable;
|
||||
extern const lua_rawset_t RawSet;
|
||||
extern const lua_getmetatable_t GetMetatable;
|
||||
extern const lua_setmetatable_t SetMetatable;
|
||||
extern const lua_insert_t Insert;
|
||||
extern const lua_remove_t Remove;
|
||||
extern const lua_gettop_t GetTop;
|
||||
@@ -186,6 +361,7 @@ extern const lua_call_t Call;
|
||||
extern const lua_pcall_t PCall;
|
||||
extern const lua_next_t Next;
|
||||
extern const lua_type_t Type;
|
||||
extern const lua_typename_t TypeName;
|
||||
extern const lua_error_t Error;
|
||||
extern const lua_topointer_t ToPointer;
|
||||
extern const lua_tothread_t ToThread;
|
||||
@@ -196,7 +372,10 @@ extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
extern const luaL_setn_t SetN;
|
||||
extern const luaL_ref_t Ref;
|
||||
extern const luaL_unref_t Unref;
|
||||
extern const lua_checkstack_t CheckStack;
|
||||
extern const lua_concat_t Concat;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
@@ -212,9 +391,88 @@ void *State();
|
||||
// the `self` slot of any `frame:method(...)` invocation.
|
||||
void *ResolveObject(void *L, int idx);
|
||||
|
||||
// The inverse: the object's Lua-registry ref, created on first use exactly as
|
||||
// the engine does it (GetMouseFocus and the script runners register an
|
||||
// object with FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER when its refcount is 0).
|
||||
int ObjectLuaRef(void *object);
|
||||
// Pushes the object's Lua table (registry[ObjectLuaRef(object)]).
|
||||
void PushObject(void *L, void *object);
|
||||
|
||||
// The object's global name (VTBL_SCRIPTOBJECT_GET_NAME), or nullptr when it
|
||||
// has none.
|
||||
const char *ObjectName(void *object);
|
||||
|
||||
// How the engine names an object in its frame-script usage errors:
|
||||
// `GetName() ?: "<unnamed>"`.
|
||||
const char *ObjectDisplayName(void *object);
|
||||
|
||||
// Same resolve, then the type gate every engine frame method applies to its
|
||||
// `self` before touching it: `lua_type == table`, resolve to the object, then
|
||||
// the `IsA(typeId)` vmethod at vtable+0x10. `ResolveObject` alone is not
|
||||
// enough for a method that casts the result — it hands back a
|
||||
// `CFrameScriptObject *` for ANY frame, so `GameTooltip.SetSpellByID(button)`
|
||||
// would reach the tooltip builder with a Button. Frame methods are ordinary
|
||||
// first-class Lua values (the per-type dispatcher is an `__index` handler
|
||||
// that returns the method closure), so that call is reachable from plain Lua
|
||||
// and the gate is not optional.
|
||||
//
|
||||
// `typeIdVar` is the class's `VAR_*_LUA_TYPE_ID`, which must match the
|
||||
// registry the method was registered on. The id is assigned lazily by
|
||||
// whichever method of the class runs first, so these mirror that assignment
|
||||
// and work even before any stock method of the type has been called.
|
||||
//
|
||||
// Raises the engine's own three errors and returns null. Pass
|
||||
// `raiseError = false` for an optional object ARGUMENT, where the caller
|
||||
// wants a silent null rather than a "this" error naming the wrong slot.
|
||||
// Returns null either way, so callers always check.
|
||||
void *ResolveTypedObject(void *L, int idx, uintptr_t typeIdVar,
|
||||
bool raiseError = true);
|
||||
|
||||
// For a method registered on SEVERAL registries, where any of those types is
|
||||
// a legitimate `self`. Accepts the object if `IsA` passes for any of the
|
||||
// listed ids — which is exactly what being in all of those registries means,
|
||||
// and avoids having to pin down the shared base class.
|
||||
void *ResolveTypedObjectAny(void *L, int idx, const uintptr_t *typeIdVars,
|
||||
int count, bool raiseError = true);
|
||||
|
||||
// The class's live type id, assigning it if the engine has not yet. Needed
|
||||
// directly only when calling a type-specific vtable slot.
|
||||
int FrameScriptTypeId(uintptr_t typeIdVar);
|
||||
|
||||
// The object's `IsA(typeId)` vmethod for a class's `VAR_*_LUA_TYPE_ID`.
|
||||
bool IsA(void *object, uintptr_t typeIdVar);
|
||||
|
||||
// Per-type sugar over `ResolveTypedObject`, so a call site cannot pair a
|
||||
// method with the wrong class's id. `idx` defaults to the `self` slot.
|
||||
void *ResolveTooltip(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveFrame(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveRegion(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveTexture(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveFontString(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveEditBox(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveModel(void *L, int idx = 1, bool raiseError = true);
|
||||
|
||||
// Every name these registrars bind is ALSO bound under `_G.ClassicAPI` —
|
||||
// `ClassicAPI.GetSpellInfo`, `ClassicAPI.C_Item.IsBound` — as an escape
|
||||
// hatch for names something else replaces later, and by value so
|
||||
// `ClassicAPI.X == X` until that happens. Automatic; a new module needs no
|
||||
// extra call. See `MirrorRegistration` in Game.cpp for why it lives here
|
||||
// and what it does not cover.
|
||||
//
|
||||
// Every registrar also RECORDS the registration for the API documentation
|
||||
// (`src/api/Documentation.cpp`): kind, table, name, registry, and which Lua
|
||||
// state's pass it ran in (in-game / glue / both). Only the FIRST pass of
|
||||
// each state records — later passes (every `/reload`) are no-ops — and a
|
||||
// registration made outside a `Run*Registrations` pass is not recorded at
|
||||
// all. The trailing `doc` argument is the descriptor (see `Game::Doc`
|
||||
// above); pass `nullptr` for a not-yet-documented registration and
|
||||
// `_classicapi_UndocumentedAPI()` lists it. The defaults exist only until
|
||||
// the sweep finishes; then they go and a missing descriptor stops compiling.
|
||||
|
||||
// Registers a single global Lua function (e.g. `GetSpellInfo`). The function
|
||||
// must use the WoW Lua C function ABI: `int __fastcall(void *L)`.
|
||||
void RegisterGlobalFunction(const char *name, CFunction func);
|
||||
void RegisterGlobalFunction(const char *name, CFunction func,
|
||||
const Doc::Function *doc = nullptr);
|
||||
|
||||
// Glue-state equivalent of `RegisterGlobalFunction`. Identical wire
|
||||
// (calls `FrameScript_RegisterFunction`); the engine routes the
|
||||
@@ -224,7 +482,8 @@ void RegisterGlobalFunction(const char *name, CFunction func);
|
||||
// its own 109 glue functions, and `VAR_LUA_STATE` still points at the
|
||||
// glue state. Calling outside that window would silently target the
|
||||
// wrong state.
|
||||
void RegisterGlueFunction(const char *name, CFunction func);
|
||||
void RegisterGlueFunction(const char *name, CFunction func,
|
||||
const Doc::Function *doc = nullptr);
|
||||
|
||||
// Frame-method registration entry: { name, func } pairs walked by the engine's
|
||||
// per-frame-type method-table iterator. Layout matches what the engine expects
|
||||
@@ -236,15 +495,26 @@ struct FrameMethodEntry {
|
||||
|
||||
// Registers a batch of methods on a per-frame-type registry (e.g.
|
||||
// GameTooltipMethodRegistry for `tooltip:Foo()` calls). `context` is the
|
||||
// registry address — see Offsets::VAR_*_METHOD_REGISTRY.
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count);
|
||||
// registry address — see Offsets::VAR_*_METHOD_REGISTRY. `docs` pairs
|
||||
// descriptors to the entries by NAME (any order, any subset).
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
|
||||
const Doc::Method *docs = nullptr, int docCount = 0);
|
||||
|
||||
// Registers `func` at `_G[tableName][methodName]`, creating the namespace
|
||||
// table if it doesn't already exist. This is how modern WoW C_*-style APIs
|
||||
// are bound — the engine has no built-in support for table-bound Lua
|
||||
// functions, so we manipulate the globals table directly via the Lua C API.
|
||||
void RegisterTableFunction(const char *tableName, const char *methodName,
|
||||
CFunction func);
|
||||
CFunction func, const Doc::Function *doc = nullptr);
|
||||
|
||||
// Binds `_G[alias]` to the SAME closure as `_G[tableName][methodName]`, by
|
||||
// value — the way the engine's own Lua-init snippet binds its short aliases
|
||||
// (`tinsert = table.insert`, `strlen = string.len`, …). Use it after
|
||||
// registering over a library function whose alias the snippet has already
|
||||
// captured, so the alias follows the replacement and `alias == table.method`
|
||||
// stays true. Mirrored under `_G.ClassicAPI` like every other registration.
|
||||
void RegisterGlobalAlias(const char *alias, const char *tableName, const char *methodName,
|
||||
const Doc::Function *doc = nullptr);
|
||||
|
||||
// Key/value pair for `RegisterIntegerEnum`. `key` becomes a field name
|
||||
// (PascalCase, matching Blizzard's `Enum.*` naming) and `value` is the
|
||||
@@ -257,8 +527,11 @@ struct EnumIntegerEntry {
|
||||
// Registers `_G[parent][sub] = { entries }` as an integer-valued enum
|
||||
// table, creating `_G[parent]` if needed. Used for Blizzard-style
|
||||
// `Enum.AddOnSecurityStatus = { Secure=0, Insecure=1, ... }` shapes.
|
||||
// `docSystem` is the documentation System the enumeration belongs to
|
||||
// ("SpellBook", "Item", …); the entries themselves are the documentation.
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count);
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem = nullptr);
|
||||
|
||||
// Set `t[key] = value` on the table currently at stack[-1] (the most
|
||||
// common shape used when populating a struct-style table mid-build).
|
||||
@@ -268,6 +541,10 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
void SetFieldNumber(void *L, const char *key, double value);
|
||||
void SetFieldString(void *L, const char *key, const char *value);
|
||||
void SetFieldBool(void *L, const char *key, bool value);
|
||||
// `t[key] = nil` — clears a field a reused (caller-supplied) table may still
|
||||
// hold from an earlier fill. Not for fresh tables: Lua 5.0 inserts a node for
|
||||
// a missing key even when the value is nil.
|
||||
void SetFieldNil(void *L, const char *key);
|
||||
|
||||
// Sets `_G[name] = value` via `lua_rawset` on the globals
|
||||
// pseudo-index. `RawSet` over `SetTable` because the globals table has
|
||||
@@ -295,6 +572,8 @@ void PushLocalizedString(void *L, const char *globalName, const char *fallback);
|
||||
// `ITEM_SLOTS_IGNORED = "%d slot(s) ignored"`, etc.). Callers that
|
||||
// need string or float args, or multiple args, can build their own
|
||||
// using `PushLocalizedString` + manual `string.format` invocation.
|
||||
// A `|4singular:plural;` in the string stays; the engine's grammar pass
|
||||
// (Text::Grammar) expands it when the text is set.
|
||||
void PushLocalizedFormatInt(void *L, const char *globalName,
|
||||
const char *fallback, int n);
|
||||
|
||||
@@ -322,16 +601,17 @@ bool CallGlobalString(void *L, const char *name, const char *arg);
|
||||
// section in CLAUDE.md for the underlying offsets and ABI.
|
||||
namespace Console {
|
||||
|
||||
// Console-command handler ABI: `args` is the command-line text after
|
||||
// the command name (empty string when invoked bare). The engine
|
||||
// ignores the return value; return 1 by convention.
|
||||
using CommandHandler = int(__fastcall *)(void *unused, const char *args);
|
||||
// Console-command handler ABI — 4.3.4's `COMMANDHANDLER(command,
|
||||
// arguments)`. `command` is the name the command was invoked by, which
|
||||
// is how one handler serves several names: every CVar is registered with
|
||||
// the shared FUN_CONSOLE_CVAR_COMMAND_HANDLER, which looks the CVar up by
|
||||
// `command`. `args` is the text after the name (empty string when
|
||||
// invoked bare). The engine ignores the return value; return 1.
|
||||
using CommandHandler = int(__fastcall *)(const char *command, const char *args);
|
||||
|
||||
// Help-grouping categories — the engine's `help` enumerates these nine
|
||||
// (in this order). Cosmetic: the value only affects which group `help`
|
||||
// lists the command under; the engine doesn't validate it. Values are
|
||||
// sequential table indices (graphics=1 confirmed via FUN_0066F6C0, the
|
||||
// rest follow the displayed order).
|
||||
// Help-grouping categories — 4.3.4's `CATEGORY` enum, which matches the
|
||||
// nine-entry table the 1.12 `help` command lists (VAR_CONSOLE_CATEGORY_TABLE).
|
||||
// Cosmetic: only `help` grouping reads it; the registrar doesn't validate it.
|
||||
enum Category {
|
||||
CATEGORY_DEBUG = 0,
|
||||
CATEGORY_GRAPHICS = 1,
|
||||
@@ -353,9 +633,30 @@ enum Category {
|
||||
void RegisterCommand(const char *name, CommandHandler handler, int category,
|
||||
const char *description);
|
||||
|
||||
// Line colour types — 4.3.4's `COLOR_T`, indexing the nine-entry
|
||||
// VAR_CONSOLE_COLORS table. The engine's `help` writes WARNING (yellow),
|
||||
// its failures ERROR (red), and its plain output DEFAULT.
|
||||
enum Color {
|
||||
COLOR_DEFAULT = 0,
|
||||
COLOR_INPUT = 1,
|
||||
COLOR_ECHO = 2,
|
||||
COLOR_ERROR = 3,
|
||||
COLOR_WARNING = 4,
|
||||
COLOR_GLOBAL = 5,
|
||||
COLOR_ADMIN = 6,
|
||||
COLOR_HIGHLIGHT = 7,
|
||||
COLOR_BACKGROUND = 8,
|
||||
};
|
||||
|
||||
// Writes a line to the console output buffer. No-ops cleanly when the
|
||||
// console isn't active, so it's always safe to call.
|
||||
void Write(const char *line);
|
||||
void Write(const char *line, Color color = COLOR_DEFAULT);
|
||||
|
||||
// The engine's own `ConsolePrintf` (FUN_CONSOLE_PRINTF): formats with
|
||||
// Storm's printf into a 1024-byte buffer, then writes it in
|
||||
// COLOR_DEFAULT. Exposed as the engine's entry itself rather than a
|
||||
// wrapper, because a cdecl varargs call cannot be forwarded.
|
||||
extern void(__cdecl *const Printf)(const char *format, ...);
|
||||
|
||||
} // namespace Console
|
||||
|
||||
|
||||
+99
-110
@@ -11,7 +11,10 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "HookSecureFunc.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
@@ -19,14 +22,19 @@ namespace HookSecureFunc {
|
||||
|
||||
namespace {
|
||||
|
||||
// Names that must not be hooked when targeting `_G`. Hooking any of these
|
||||
// would either break taint propagation (the "secure" functions) or replace
|
||||
// core Lua language primitives whose behavior the engine depends on
|
||||
// internally — e.g. `pairs` is called from C in the iterator path; if Lua
|
||||
// `pairs` is replaced with a wrapper closure, the engine still gets the
|
||||
// original via its own table-method dispatch, but Lua-side iteration goes
|
||||
// through our wrapper, and the two diverging is a footgun. Modern WoW
|
||||
// rejects the hook outright; we mirror that. Sorted alphabetically.
|
||||
// Names that must not be hooked when targeting `_G`. This is the client's
|
||||
// own list, VERBATIM: patch 11.0.0 (The War Within, 2024-07-23) made
|
||||
// `hooksecurefunc` refuse exactly these 23 names with a "Cannot hook
|
||||
// function" error — the taint-relevant primitives (the "secure" family plus
|
||||
// the Lua primitives secure code paths run through). It is deliberately NOT
|
||||
// "every base-library function": 11.0 leaves `tostring`, `error`,
|
||||
// `loadstring`, `assert`, `collectgarbage`, … hookable, and includes names
|
||||
// that are not base-library functions at all. So do not derive this from
|
||||
// the engine's `luaopen_base` table (`0x00811E28`, 36 entries — see
|
||||
// BlizzardScriptAPI.md §3); that is a different set with a different
|
||||
// purpose. Names with no 1.12 counterpart (`wipe` is `table.wipe` here; the
|
||||
// secure family does not exist) stay for parity of the error an addon
|
||||
// sees. Sorted alphabetically.
|
||||
const char *const kUnhookableNames[] = {
|
||||
"getfenv", "getmetatable", "hooksecurefunc", "ipairs", "issecurevalue",
|
||||
"issecurevariable", "next", "pairs", "pcall", "pcallwithenv", "rawget",
|
||||
@@ -45,138 +53,113 @@ bool IsUnhookable(const char *name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Wrapper closure body — called when the hooked function is invoked.
|
||||
// Stack contains the wrapper's args (`[arg1..argN]`); the closure's
|
||||
// upvalues hold `(orig, callback)`.
|
||||
// upvalues hold `(orig, callback)`. 4.3.4's FrameScript_HookWrapper, less
|
||||
// its taint bookkeeping (1.12 has no taint):
|
||||
//
|
||||
// Modern `hooksecurefunc` semantics: original runs first, callback
|
||||
// runs after with the same args (returns ignored), original's
|
||||
// results propagate to the caller. No cap on return count. A buggy
|
||||
// callback must NOT prevent the original's results from reaching
|
||||
// the caller — that's why step (3) uses pcall.
|
||||
//
|
||||
// Stack-shuffling mirrors 3.3.5's `FUN_00817050` so the args sit at
|
||||
// the top for both calls without copying them twice; results sink
|
||||
// to the bottom and end up as the wrapper's natural return frame.
|
||||
int __fastcall HookWrapper(void *L) {
|
||||
// 1. orig(...) with lua_call — an error in the original propagates to
|
||||
// the caller, as it would unhooked.
|
||||
// 2. Results sink below the args, so they end up as the wrapper's
|
||||
// return frame without being copied again.
|
||||
// 3. callback(...) through lua_pcall with the engine's frame-script
|
||||
// message handler as errfunc: a broken hook is REPORTED the way any
|
||||
// script error is (the error frame / geterrorhandler), and the
|
||||
// original's results still reach the caller.
|
||||
int __fastcall Wrapper(void *L) {
|
||||
const int nargs = Game::Lua::GetTop(L);
|
||||
|
||||
// (1) orig(...) — pops orig + nargs args (copies), pushes results.
|
||||
// Stack after: `[arg1..argN, res1..resM]`.
|
||||
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(1));
|
||||
// (1) Stack after: `[arg1..argN, res1..resM]`.
|
||||
for (int i = 1; i <= nargs; ++i)
|
||||
Game::Lua::PushValue(L, i);
|
||||
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(1));
|
||||
Game::Lua::Insert(L, nargs + 1);
|
||||
Game::Lua::Call(L, nargs, Game::Lua::MULTRET);
|
||||
const int nresults = Game::Lua::GetTop(L) - nargs;
|
||||
|
||||
// (2) Shuffle results below the saved args. Each `Insert(L, 1)`
|
||||
// moves the current top down to position 1; running it
|
||||
// nresults times rotates the results block beneath the args.
|
||||
// (2) Each `Insert(L, 1)` moves the top value to the bottom; nresults
|
||||
// of them rotate the results block beneath the args.
|
||||
// Stack after: `[res1..resM, arg1..argN]`.
|
||||
for (int i = 0; i < nresults; ++i)
|
||||
Game::Lua::Insert(L, 1);
|
||||
|
||||
// (3) callback(...) via pcall — errors are caught and dropped so
|
||||
// the original's return values always reach the caller. This
|
||||
// is the load-bearing safety property of hooksecurefunc on
|
||||
// 3.x; on 1.12 it just means a buggy hook can't break the
|
||||
// hooked path. errfunc=0 → no traceback (3.3.5 uses
|
||||
// `registry["_TRACEBACK"]`, not a 1.12 convention; we'd be
|
||||
// discarding the formatted message anyway).
|
||||
// (3) Stack: `[res1..resM, errfunc, callback, arg1..argN]`. The
|
||||
// handler ref is the one every engine script pcall passes; 0 (not
|
||||
// yet set) falls back to no handler rather than a bad index.
|
||||
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(2));
|
||||
Game::Lua::Insert(L, nresults + 1);
|
||||
if (Game::Lua::PCall(L, nargs, 0, 0) != 0) {
|
||||
// pcall failure leaves the error message on top; drop it.
|
||||
Game::Lua::Remove(L, -1);
|
||||
int errIdx = 0;
|
||||
const int errRef = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
|
||||
if (errRef > 0) {
|
||||
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, errRef);
|
||||
Game::Lua::Insert(L, nresults + 1);
|
||||
errIdx = nresults + 1;
|
||||
}
|
||||
Game::Lua::PCall(L, nargs, 0, errIdx);
|
||||
|
||||
// Stack: `[res1..resM]` — return the original's results verbatim.
|
||||
// Drop the handler and any error value — `[res1..resM]` remains.
|
||||
Game::Lua::SetTop(L, nresults);
|
||||
return nresults;
|
||||
}
|
||||
|
||||
// `hooksecurefunc(name, callback)` — hooks a global by name, target
|
||||
// table is `_G`.
|
||||
// `hooksecurefunc(table, name, callback)` — hooks `table[name]`.
|
||||
//
|
||||
// In both cases the original function runs first and its results
|
||||
// propagate; `callback` runs after with the same args (returns
|
||||
// discarded). The "secure" label refers to taint propagation in
|
||||
// 3.x+; on 1.12 it's just a post-hook with return preservation.
|
||||
void PushWrapper(void *L) {
|
||||
Game::Lua::PushCClosure(L, &Wrapper, 2);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// `hooksecurefunc([table,] "function", hookfunc)` — replaces
|
||||
// `table[name]` (default `_G`) with a wrapper that runs the original, then
|
||||
// `hookfunc` with the same args, and returns the original's results.
|
||||
// 4.3.4's Script_hooksecurefunc, less taint:
|
||||
// * a non-table first argument means `_G` is inserted at index 1, so
|
||||
// both forms share one index layout;
|
||||
// * the name only has to pass lua_isstring, so a number works (and is
|
||||
// looked up as its string, the same as lua_tostring makes it);
|
||||
// * the original is read with lua_gettable (inherited methods resolve
|
||||
// through __index) but the wrapper is written with lua_rawset, so a
|
||||
// target's __newindex never runs;
|
||||
// * the error texts are 4.3.4's.
|
||||
// The one addition is the 11.0 unhookable-name check for the `_G` form.
|
||||
int __fastcall Script_HookSecureFunc(void *L) {
|
||||
int targetIdx, nameIdx, callbackIdx;
|
||||
const int t1 = Game::Lua::Type(L, 1);
|
||||
const bool implicitGlobals = Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE;
|
||||
if (implicitGlobals) {
|
||||
Game::Lua::PushValue(L, Game::Lua::GLOBALS_INDEX);
|
||||
Game::Lua::Insert(L, 1);
|
||||
}
|
||||
|
||||
if (t1 == Game::Lua::TYPE_TABLE) {
|
||||
// Three-arg form: (table, name, callback)
|
||||
targetIdx = 1;
|
||||
nameIdx = 2;
|
||||
callbackIdx = 3;
|
||||
} else if (t1 == Game::Lua::TYPE_STRING) {
|
||||
// Two-arg form: (name, callback) — target = _G via pseudo-
|
||||
// index. Don't push _G with `lua_pushvalue(GLOBALS_INDEX)`:
|
||||
// it crashes (the pushvalue → setobj2s chain dereferences a
|
||||
// bad TValue pointer for that pseudo-index). `lua_gettable` /
|
||||
// `lua_settable` accept GLOBALS_INDEX as the table arg
|
||||
// directly, so we use it that way without pushing.
|
||||
targetIdx = Game::Lua::GLOBALS_INDEX;
|
||||
nameIdx = 1;
|
||||
callbackIdx = 2;
|
||||
} else {
|
||||
Game::Lua::Error(L,
|
||||
"hooksecurefunc(table, name, callback) or hooksecurefunc(name, callback)");
|
||||
if (!Game::Lua::IsString(L, 2) ||
|
||||
Game::Lua::Type(L, 3) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Error(L, "Usage: hooksecurefunc([table,] \"function\", hookfunc)");
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::SetTop(L, 3); // [t, name, hookfunc]
|
||||
// Converts a number name to its string in place; the string stays on
|
||||
// the stack, so the pointer outlives every use below.
|
||||
const char *name = Game::Lua::ToString(L, 2);
|
||||
|
||||
// Block the hook when targeting `_G` implicitly and the name is in the
|
||||
// unhookable set. Not applied to the three-arg form (table targets):
|
||||
// hooking `MyLib.pairs` is fine even if Lua's global `pairs` is not,
|
||||
// and a caller who passes `_G` explicitly is opting in deliberately.
|
||||
if (implicitGlobals && IsUnhookable(name)) {
|
||||
Game::Lua::Error(L, "hooksecurefunc: function is unhookable");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Game::Lua::Type(L, nameIdx) != Game::Lua::TYPE_STRING) {
|
||||
Game::Lua::Error(L, "hooksecurefunc: name must be a string");
|
||||
return 0;
|
||||
}
|
||||
if (Game::Lua::Type(L, callbackIdx) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Error(L, "hooksecurefunc: callback must be a function");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Block the hook when targeting `_G` and the name is in the unhookable
|
||||
// set. We don't apply this to the three-arg form (table targets):
|
||||
// hooking `MyLib.pairs` is fine even if Lua's global `pairs` is not —
|
||||
// the blacklist is specifically about `_G[name]`. A user who passes
|
||||
// `_G` explicitly as the table target is opting in deliberately and we
|
||||
// don't second-guess it.
|
||||
if (targetIdx == Game::Lua::GLOBALS_INDEX) {
|
||||
const char *name = Game::Lua::ToString(L, nameIdx);
|
||||
if (IsUnhookable(name)) {
|
||||
Game::Lua::Error(L,
|
||||
"hooksecurefunc: function is unhookable");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch target[name] — the original function. `gettable` uses
|
||||
// metamethods, so frame methods (resolved via the frame
|
||||
// metatable's __index) come back as callable closures, exactly
|
||||
// what we want to wrap.
|
||||
Game::Lua::PushValue(L, nameIdx);
|
||||
Game::Lua::GetTable(L, targetIdx);
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::GetTable(L, 1); // [t, name, hookfunc, orig]
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Error(L, "hooksecurefunc: target field is not a function");
|
||||
Game::Lua::Error(L, "hooksecurefunc(): %s is not a function", name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Push callback as the second upvalue; orig is already on top
|
||||
// from the GetTable above.
|
||||
Game::Lua::PushValue(L, callbackIdx);
|
||||
|
||||
// Build the wrapper as a C closure with (orig, callback) as the
|
||||
// two upvalues. PushCClosure pops the upvalues from the stack
|
||||
// and pushes the closure.
|
||||
Game::Lua::PushCClosure(L, &HookWrapper, 2);
|
||||
|
||||
// Install: target[name] = wrapper. SetTable expects the value
|
||||
// on top and the key just below; push them in that order.
|
||||
Game::Lua::PushValue(L, nameIdx);
|
||||
Game::Lua::PushValue(L, -2); // duplicate the closure
|
||||
Game::Lua::SetTable(L, targetIdx);
|
||||
|
||||
Game::Lua::Insert(L, 3); // [t, name, orig, hookfunc]
|
||||
PushWrapper(L); // [t, name, wrapper]
|
||||
Game::Lua::RawSet(L, 1); // t[name] = wrapper
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -185,7 +168,13 @@ int __fastcall Script_HookSecureFunc(void *L) {
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("hooksecurefunc", &Script_HookSecureFunc);
|
||||
}
|
||||
// 4.3.4 registers it from FrameScript_Initialize, which runs for the glue
|
||||
// state (Client.cpp) as well as the world (GameUI.cpp).
|
||||
static void RegisterGlueFunctions() {
|
||||
Game::Lua::RegisterGlueFunction("hooksecurefunc", &Script_HookSecureFunc);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
static const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlueFunctions};
|
||||
|
||||
} // namespace HookSecureFunc
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The one hook wrapper `hooksecurefunc` and `Frame:HookScript` both install —
|
||||
// 4.3.4 uses its FrameScript_HookWrapper for both, so a hooked function and a
|
||||
// hooked script behave the same way.
|
||||
namespace HookSecureFunc {
|
||||
|
||||
// The wrapper closure body. Upvalue 1 = the original, upvalue 2 = the hook.
|
||||
// Runs the original with the wrapper's args (its errors propagate), then the
|
||||
// hook with the same args under the engine's script error handler, and
|
||||
// returns the original's results. Exposed so readers can recognize the
|
||||
// closure by its C function.
|
||||
int __fastcall Wrapper(void *L);
|
||||
|
||||
// Pops the hook (top) and the original (below it) and pushes the wrapper
|
||||
// closure over them.
|
||||
void PushWrapper(void *L);
|
||||
|
||||
} // namespace HookSecureFunc
|
||||
+6320
-745
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `IsItemAction(slot)` / `HasBonusActionBar()` — the action-bar predicates
|
||||
// from the newer clients' ActionBarFrame_Scripts, read off the same engine
|
||||
// state the 1.12 readers use.
|
||||
//
|
||||
// IsItemAction 1 when the 1-based slot holds an item, else nil; a
|
||||
// non-number slot is a usage error. The item kind is
|
||||
// the 0x80000000 tag in VAR_ACTION_TABLE.
|
||||
// HasBonusActionBar 1 while a bonus bar is active, else nil: the offset
|
||||
// GetBonusBarOffset returns is nonzero.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::Bar {
|
||||
|
||||
namespace {
|
||||
|
||||
int PushFlag(void *L, bool on) {
|
||||
if (on)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsItemAction(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: IsItemAction(slot)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return PushFlag(L, false);
|
||||
const auto *table = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE));
|
||||
return PushFlag(L, (table[slot0] & Offsets::ACTION_TYPE_MASK) ==
|
||||
Offsets::ACTION_TYPE_ITEM_BY_ID);
|
||||
}
|
||||
|
||||
int __fastcall Script_HasBonusActionBar(void *L) {
|
||||
return PushFlag(L, Game::Read<uint32_t>(Offsets::VAR_BONUS_BAR_OFFSET) != 0);
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kSlotArgs[] = {
|
||||
Game::Doc::Req("slot", "number", "The action slot, 1 to 120."),
|
||||
};
|
||||
const Game::Doc::Field kIsItemRets[] = {
|
||||
Game::Doc::Opt("isItem", "number", nullptr, "1 if the slot holds an item, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kIsItemAction{
|
||||
"Returns 1 if an action slot holds an item.",
|
||||
kSlotArgs, kIsItemRets, "ActionBar"};
|
||||
|
||||
const Game::Doc::Field kHasBonusRets[] = {
|
||||
Game::Doc::Opt("hasBonusBar", "number", nullptr,
|
||||
"1 if a bonus action bar is active, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kHasBonusActionBar{
|
||||
"Returns 1 if a bonus action bar is active, for example in a stance or form.",
|
||||
{}, kHasBonusRets, "ActionBar"};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsItemAction", &Script_IsItemAction, &kIsItemAction);
|
||||
Game::Lua::RegisterGlobalFunction("HasBonusActionBar", &Script_HasBonusActionBar,
|
||||
&kHasBonusActionBar);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Action::Bar
|
||||
+38
-55
@@ -12,41 +12,37 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetActionInfo(slot)` — modern-WoW-style action descriptor lookup.
|
||||
// Returns `actionType, id, subType` for the slot, or nil if empty.
|
||||
// Returns `actionType, id, subType` for the slot, or nothing if the slot
|
||||
// holds nothing the descriptor can describe (4.3.4 returns nothing there
|
||||
// too, rather than a nil).
|
||||
//
|
||||
// The vanilla 1.12.1 engine stores action descriptors as packed
|
||||
// uint32s in `VAR_ACTION_TABLE` (`0x00BC6980`, 120 slots). Top 4 bits
|
||||
// are the type tag; the `0x40000000` tag is ambiguous between
|
||||
// macros and bag-items and is disambiguated by checking the per-
|
||||
// character macro-slot map at `VAR_MACRO_SLOT_MAP` (36 entries).
|
||||
// are the type tag — the server's ACTION_BUTTON_* values shifted into the
|
||||
// top byte: 0x00 spell, 0x40 macro, 0x80 item.
|
||||
//
|
||||
// empty slot: nil
|
||||
// spell action: "spell", spellID, "spell"
|
||||
// macro: "macro", macroIndex (1-based)
|
||||
// bag item: "item", nil (see note below)
|
||||
// item-by-id: "item", itemID
|
||||
// item: "item", itemID
|
||||
// equipment set: "equipmentset", setName
|
||||
//
|
||||
// The subType is always "spell" for spell-type entries — the engine
|
||||
// helper hardcodes pet=0 for entries on this table. Pet-bar actions
|
||||
// live in a separate table; that's `PetHasActionBar` + the pet-action
|
||||
// helpers, not this function.
|
||||
//
|
||||
// The macro discriminator is the same trick SuperWoWhook uses in its
|
||||
// modified `GetActionText`: walk the 36-entry macro-slot map and check
|
||||
// whether `entry & 0xBFFFFFFF` matches one of those macro IDs. If yes,
|
||||
// the slot holds a macro; if no, it's a bag-item.
|
||||
//
|
||||
// **Bag-item itemID note.** For real bag items (top-4 bits = `0x4`
|
||||
// and the payload isn't in the macro-slot map), the engine stores a
|
||||
// bag-instance key, not the itemID. Resolving to itemID requires
|
||||
// walking the per-character action-item hash table at `[0x00BDCC54]`
|
||||
// and reading a struct field whose offset we haven't fully derived
|
||||
// yet. For now we report the action *type* ("item") but return nil
|
||||
// for the id. Callers that need it can fall back to
|
||||
// `GetActionTexture` + Lua-side item lookups.
|
||||
// A 0x40 entry is a macro only while its macroID is in the macro-slot map
|
||||
// (`FUN_ACTION_IS_MACRO`, which every engine reader gates its macro branch
|
||||
// on). One that is not — a macro deleted out from under its button — is
|
||||
// nothing, as in 4.3.4: its IsItem and IsMacro are both false, so it falls
|
||||
// through to an empty return. There is no bag-item tag: items are always
|
||||
// by-itemID (0x80), and an earlier reading of the 0x40 "miss" as a bag item
|
||||
// was wrong.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Slot.h"
|
||||
#include "equipmentset/Data.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -62,19 +58,6 @@ uint32_t ReadEntry(int slot0) {
|
||||
return base[slot0];
|
||||
}
|
||||
|
||||
// Returns 1-based macro slot index if `key` matches any entry in the
|
||||
// macro-slot map, or 0 if no match. `key` is the action entry with the
|
||||
// `0x40000000` type bit stripped (i.e. `entry & 0xBFFFFFFF`).
|
||||
int FindMacroSlot(uint32_t key) {
|
||||
auto *map = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MACRO_SLOT_MAP));
|
||||
for (int i = 0; i < Offsets::MACRO_SLOT_MAP_COUNT; ++i) {
|
||||
if (map[i] == key)
|
||||
return i + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetActionInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetActionInfo(slot)");
|
||||
@@ -82,16 +65,12 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
}
|
||||
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
|
||||
const uint32_t entry = ReadEntry(slot0);
|
||||
if (entry == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (entry == 0)
|
||||
return 0;
|
||||
|
||||
const uint32_t type = entry & Offsets::ACTION_TYPE_MASK;
|
||||
|
||||
@@ -104,10 +83,8 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
static_cast<uintptr_t>(Offsets::FUN_ACTION_SLOT_TO_SPELL));
|
||||
uint32_t petFlag = 0;
|
||||
const uint32_t spellID = fn(static_cast<uint32_t>(slot0), &petFlag);
|
||||
if (spellID == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (spellID == 0)
|
||||
return 0;
|
||||
(void)petFlag; // hardcoded 0 in the engine helper for this table
|
||||
Game::Lua::PushString(L, "spell");
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
@@ -117,18 +94,13 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
|
||||
if (type == Offsets::ACTION_TYPE_BAG_OR_MACRO) {
|
||||
const uint32_t key = entry & Offsets::ACTION_PAYLOAD_MASK_BAG_OR_MACRO;
|
||||
const int macroSlot = FindMacroSlot(key);
|
||||
const int macroSlot = Action::Slot::MacroSlotForID(key);
|
||||
if (macroSlot > 0) {
|
||||
Game::Lua::PushString(L, "macro");
|
||||
Game::Lua::PushNumber(L, static_cast<double>(macroSlot));
|
||||
return 2;
|
||||
}
|
||||
// Not a macro — it's a bag item. We know the type but extracting
|
||||
// the itemID from the bag-instance hash needs more reversing;
|
||||
// see the file-level comment.
|
||||
Game::Lua::PushString(L, "item");
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
return 0; // a macro button whose macro is gone — see the file comment
|
||||
}
|
||||
|
||||
if (type == Offsets::ACTION_TYPE_ITEM_BY_ID) {
|
||||
@@ -138,9 +110,20 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Unknown type tag — surface as nil so callers don't get garbage.
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
if (type == Offsets::ACTION_TYPE_EQUIPMENT_SET) {
|
||||
// 3.3.5 `Script_GetActionInfo` (`FUN_005a8f10`): the type and the
|
||||
// set's name.
|
||||
const EquipmentSet::Set *set = EquipmentSet::Data::FindByID(
|
||||
entry & ~static_cast<uint32_t>(Offsets::ACTION_TYPE_EQUIPMENT_SET));
|
||||
if (set == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, "equipmentset");
|
||||
Game::Lua::PushString(L, set->name.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Unknown type tag — nothing, so callers don't get garbage.
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetActionCooldown` / `GetActionCount` / `IsConsumableAction` for macro
|
||||
// slots whose `#showtooltip` / `#show` resolved to an ITEM. A spell
|
||||
// resolution needs none of this — the engine reads a macro slot's spell from
|
||||
// its primary-spell cache, which `Macro::ShowTooltip` writes — but the engine
|
||||
// has no per-macro item field, so the item forms of these three are answered
|
||||
// here, each with the engine's own helper for the item-by-ID slot case:
|
||||
//
|
||||
// - cooldown → `FUN_ITEM_QUERY_COOLDOWN` via `Item::Cooldown::PushCooldown`
|
||||
// (the same triple `GetContainerItemCooldown` returns);
|
||||
// - count → `FUN_ACTION_ITEM_COUNT`, the value `UseAction` caches per
|
||||
// item-by-ID slot;
|
||||
// - consumable → the rule of the inner `IsConsumableAction` (`0x004E5250`):
|
||||
// ammo / thrown, or any on-use spell slot with negative
|
||||
// charges.
|
||||
//
|
||||
// Registered in front of the engine globals (last registration wins) and a
|
||||
// strict superset of them: every other slot tail-calls the engine function
|
||||
// with the untouched stack.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Cooldown.h"
|
||||
#include "item/Record.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::ItemState {
|
||||
|
||||
namespace {
|
||||
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
using ItemCount_t = uint32_t(__fastcall *)(uint32_t itemID);
|
||||
|
||||
int Engine(uintptr_t fn, void *L) {
|
||||
return reinterpret_cast<ScriptFn_t>(fn)(L);
|
||||
}
|
||||
|
||||
// True (and fills `info`) when stack[1] is a slot whose macro resolved to an item.
|
||||
bool ItemTarget(void *L, Macro::ShowTooltip::Info *info) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return false;
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
return Macro::ShowTooltip::ForSlot(slot0, info) &&
|
||||
info->target == Macro::ShowTooltip::Target::Item;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetActionCooldown(void *L) {
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (!ItemTarget(L, &info))
|
||||
return Engine(Offsets::FUN_SCRIPT_GET_ACTION_COOLDOWN, L);
|
||||
Item::Cooldown::PushCooldown(L, info.itemID);
|
||||
return 3;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetActionCount(void *L) {
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (!ItemTarget(L, &info))
|
||||
return Engine(Offsets::FUN_SCRIPT_GET_ACTION_COUNT, L);
|
||||
const uint32_t count = reinterpret_cast<ItemCount_t>(Offsets::FUN_ACTION_ITEM_COUNT)(
|
||||
static_cast<uint32_t>(info.itemID));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(count));
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool IsConsumable(int itemID) {
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const uint32_t invType = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
if (invType == Offsets::INVTYPE_AMMO || invType == Offsets::INVTYPE_THROWN)
|
||||
return true;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i) {
|
||||
const uint32_t spellID = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_ID + i * 4);
|
||||
const uint32_t trigger = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_TRIGGER + i * 4);
|
||||
const int32_t charges = Game::Read<int32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_CHARGES + i * 4);
|
||||
if (spellID != 0 && trigger == Offsets::ITEM_SPELLTRIGGER_ON_USE && charges < 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsConsumableAction(void *L) {
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (!ItemTarget(L, &info))
|
||||
return Engine(Offsets::FUN_SCRIPT_IS_CONSUMABLE_ACTION, L);
|
||||
if (IsConsumable(info.itemID))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetActionCooldown", &Script_GetActionCooldown);
|
||||
Game::Lua::RegisterGlobalFunction("GetActionCount", &Script_GetActionCount);
|
||||
Game::Lua::RegisterGlobalFunction("IsConsumableAction", &Script_IsConsumableAction);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::ItemState
|
||||
@@ -0,0 +1,54 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Dropping a spell on a slot that already holds it — 4.3.4's
|
||||
// CGActionBar::PutActionInSlot:
|
||||
//
|
||||
// else if (CGActionBar::IsSpell(slot) && CGActionBar::GetSpell(slot, petSpell) == spellId)
|
||||
// CGGameUI::DropCursorSpell();
|
||||
//
|
||||
// 1.12's FUN_ACTION_PLACE runs the same compare without the IsSpell gate,
|
||||
// and FUN_ACTION_SLOT_TO_SPELL resolves a macro slot to the macro's spell.
|
||||
// So a spell dropped on a `/cast <that spell>` macro was taken for a
|
||||
// duplicate: the cursor cleared and the macro stayed. The gate is one call
|
||||
// redirect: a slot that is not a spell action has no spell to match.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::Place {
|
||||
|
||||
namespace {
|
||||
|
||||
using SlotToSpell_t = uint32_t(__fastcall *)(uint32_t slot0, uint32_t *outPetFlag);
|
||||
|
||||
uint32_t __fastcall SpellSlotSpell(uint32_t slot0, uint32_t *outPetFlag) {
|
||||
const uint32_t action = reinterpret_cast<const uint32_t *>(Offsets::VAR_ACTION_TABLE)[slot0];
|
||||
if (action == 0 || (action & Offsets::ACTION_TYPE_MASK) != Offsets::ACTION_TYPE_SPELL)
|
||||
return 0;
|
||||
return reinterpret_cast<SlotToSpell_t>(Offsets::FUN_ACTION_SLOT_TO_SPELL)(slot0, outPetFlag);
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Common::RedirectCall(Offsets::CALL_ACTION_PLACE_SAME_SPELL, Offsets::FUN_ACTION_SLOT_TO_SPELL,
|
||||
reinterpret_cast<const void *>(&SpellSlotSpell));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::Place
|
||||
@@ -0,0 +1,110 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under
|
||||
// the terms of the GNU General Public License as published by the Free Software
|
||||
// Foundation, either version 3 of the License, or (at your option) any later
|
||||
// version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The range answer of an action button whose macro is aimed at a unit.
|
||||
//
|
||||
// 4.3.4 keeps that unit on the macro (MACRONODE::m_target: the `[@unit]` of
|
||||
// the clause its display resolved from, or the target SetMacroSpell /
|
||||
// SetMacroItem named), and CGActionBar::HasValidTarget range-checks the slot's
|
||||
// spell against it in place of the current target. 1.12's slot range function
|
||||
// FUN_ACTION_SLOT_RANGE already takes the unit as a GUID for its range check,
|
||||
// and always passes 0, the selection. `Macro::ShowTooltip` keeps the unit (its
|
||||
// `Info::unit`), and this answers for those slots with that GUID, in the
|
||||
// engine's order: the range check, then the implicit-target test, where the
|
||||
// unit standing in for the selection must exist. Every other slot goes to the
|
||||
// engine function unchanged.
|
||||
//
|
||||
// The function's one caller, Script_IsActionInRange, is redirected rather than
|
||||
// the function hooked.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Slot.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::Range {
|
||||
|
||||
namespace {
|
||||
|
||||
using SlotRange_t = int(__fastcall *)(int slot0, int *inRange);
|
||||
using SlotToSpell_t = int(__fastcall *)(int slot0, uint32_t *outPetFlag);
|
||||
using CheckRange_t = uint8_t(__fastcall *)(const void *caster, const uint8_t *record,
|
||||
uint32_t guidLo, uint32_t guidHi, uint32_t report);
|
||||
|
||||
// The first implicit targets the engine answers for: case 0 of its table.
|
||||
bool TakesUnitTarget(int32_t implicitTarget) {
|
||||
switch (implicitTarget) {
|
||||
case 6: case 21: case 23: case 25: case 26: case 35: case 45: case 57: case 61:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t MacroUnit(int slot0) {
|
||||
const uint32_t macroID = Action::Slot::MacroIDForSlot(slot0);
|
||||
Macro::ShowTooltip::Info info{};
|
||||
// Passive: this runs inside a Lua call, from a button's OnUpdate.
|
||||
if (macroID == 0 || !Macro::ShowTooltip::LookupPassive(macroID, &info))
|
||||
return 0;
|
||||
return info.unit;
|
||||
}
|
||||
|
||||
int __fastcall SlotRange_h(int slot0, int *inRange) {
|
||||
const auto original = reinterpret_cast<SlotRange_t>(Offsets::FUN_ACTION_SLOT_RANGE);
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return original(slot0, inRange);
|
||||
const uint64_t unit = MacroUnit(slot0);
|
||||
if (unit == 0)
|
||||
return original(slot0, inRange);
|
||||
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
uint32_t pet = 0;
|
||||
const int spellID =
|
||||
reinterpret_cast<SlotToSpell_t>(Offsets::FUN_ACTION_SLOT_TO_SPELL)(slot0, &pet);
|
||||
const uint8_t *record = ::Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr || pet != 0)
|
||||
return 0;
|
||||
if (!reinterpret_cast<CheckRange_t>(Offsets::FUN_CHECK_SPELL_TARGET_RANGE)(
|
||||
player, record, static_cast<uint32_t>(unit), static_cast<uint32_t>(unit >> 32), 0)) {
|
||||
*inRange = 0;
|
||||
return 1;
|
||||
}
|
||||
const int32_t implicitTarget =
|
||||
Game::Read<int32_t>(record, Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
|
||||
if (!TakesUnitTarget(implicitTarget) || Object::ByGuid(Offsets::TYPEMASK_UNIT, unit) == nullptr)
|
||||
return 0;
|
||||
*inRange = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Common::RedirectCall(Offsets::CALL_IS_ACTION_IN_RANGE_SLOT_RANGE,
|
||||
Offsets::FUN_ACTION_SLOT_RANGE,
|
||||
reinterpret_cast<const void *>(&SlotRange_h));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::Range
|
||||
@@ -0,0 +1,42 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "action/Slot.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Action::Slot {
|
||||
|
||||
int MacroSlotForID(uint32_t macroID) {
|
||||
if (macroID == 0)
|
||||
return 0;
|
||||
// The engine's own scan: the 0-based slot, or 0xFFFFFFFF on a miss.
|
||||
using IdToSlot_t = uint32_t(__fastcall *)(int macroID);
|
||||
const uint32_t slot0 = reinterpret_cast<IdToSlot_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_MACRO_ID_TO_SLOT))(static_cast<int>(macroID));
|
||||
return slot0 == 0xFFFFFFFFu ? 0 : static_cast<int>(slot0) + 1;
|
||||
}
|
||||
|
||||
uint32_t MacroIDForSlot(int slot0) {
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
auto *table = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE));
|
||||
const uint32_t entry = table[slot0];
|
||||
if (entry == 0 || (entry & Offsets::ACTION_TYPE_MASK) != Offsets::ACTION_TYPE_BAG_OR_MACRO)
|
||||
return 0;
|
||||
const uint32_t key = entry & Offsets::ACTION_PAYLOAD_MASK_BAG_OR_MACRO;
|
||||
return MacroSlotForID(key) != 0 ? key : 0;
|
||||
}
|
||||
|
||||
} // namespace Action::Slot
|
||||
@@ -0,0 +1,36 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Action-slot ↔ macro decoding shared by every module that has to answer
|
||||
// "which macro sits in this action slot" (`GetActionInfo`, the
|
||||
// `#showtooltip` display overrides). The action table stores macroIDs under
|
||||
// the `0x40000000` tag; the macro-slot map says which of those ids still
|
||||
// name a macro (the rest are buttons whose macro is gone). Pure C — no Lua
|
||||
// stack.
|
||||
|
||||
namespace Action::Slot {
|
||||
|
||||
// 1-based macro slot holding `macroID` — the same linear scan of
|
||||
// `VAR_MACRO_SLOT_MAP` the engine's `FUN_MACRO_ID_TO_SLOT` does — or 0 when
|
||||
// no macro has that id (0 is never a macroID).
|
||||
int MacroSlotForID(uint32_t macroID);
|
||||
|
||||
// The macroID in action slot `slot0` (0-based, `< ACTION_TABLE_MAX_SLOTS`),
|
||||
// or 0 when the slot is empty, isn't `0x40000000`-tagged, or its macro is gone.
|
||||
uint32_t MacroIDForSlot(int slot0);
|
||||
|
||||
} // namespace Action::Slot
|
||||
@@ -0,0 +1,37 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The `ActionButtonUseKeyDown` cvar — registered by 4.3.4's engine
|
||||
// (GameUI_CVar.cpp, "Activate the action button on a keydown") and read only
|
||||
// by FrameXML's action-button key handlers, which this project's addon ports
|
||||
// (Util/ActionButton.lua). The engine half is the registration. The default
|
||||
// is 0, keeping 1.12's action buttons firing on key release; 4.3.4 shipped 1.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cvar/Factory.h"
|
||||
|
||||
namespace Action::UseKeyDown {
|
||||
|
||||
namespace {
|
||||
|
||||
void Register() {
|
||||
CVar::Factory::Register("ActionButtonUseKeyDown", "Activate the action button on a keydown",
|
||||
Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::UseKeyDown
|
||||
+202
-180
@@ -11,31 +11,47 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Embedded `!!!ClassicAPI` addon fallback. When the user doesn't have
|
||||
// the addon installed on disk, this module makes the engine think it
|
||||
// does — without writing anything to the filesystem.
|
||||
// Embedded addons. When the user doesn't have an addon installed on disk,
|
||||
// this module makes the engine think they do — without writing anything
|
||||
// to the filesystem. The addons are `Manifest::kAddons` (the `ADDONS`
|
||||
// list in CMakeLists.txt): `!!!ClassicAPI`, which loads at login, and
|
||||
// `ClassicAPI_DebugTools`, which is `## LoadOnDemand` behind the
|
||||
// `/dump` / `/fstack` / `/etrace` stubs in `!!!ClassicAPI` — retail's
|
||||
// chat-frame + Blizzard_DebugTools split. (The `Blizzard_` name is not
|
||||
// available to us: the engine moves loose `Interface\AddOns\Blizzard*`
|
||||
// folders aside at startup, `MoveAsideLooseInterface`, and refuses a
|
||||
// `Blizzard_*` TOC without a signature.)
|
||||
//
|
||||
// How:
|
||||
//
|
||||
// 1. The CMake build embeds every file under `AddOns/!!!ClassicAPI/`
|
||||
// into a generated header (`embedded_classicapi.h`) as a byte
|
||||
// array per file plus a `{path, data, size}` manifest.
|
||||
// 1. The CMake build embeds every file of every listed addon as an
|
||||
// RCDATA resource, plus a generated `{path, resourceId}` manifest
|
||||
// (`embedded_resources.h`).
|
||||
//
|
||||
// 2. We hook the engine's file-read function at `FUN_FILE_READ`.
|
||||
// When the engine asks for a path under
|
||||
// `Interface\AddOns\!!!ClassicAPI\`, we allocate a Storm buffer
|
||||
// `Interface\AddOns\<embedded addon>\`, we allocate a Storm buffer
|
||||
// via the same allocator the engine itself uses, copy the
|
||||
// embedded content in, and hand it back. The caller's normal
|
||||
// `SMemFree` reclaims it on completion — no special lifetime.
|
||||
//
|
||||
// 3. We post-hook the engine's `FUN_ADDON_INIT`. After the engine's
|
||||
// own `Interface\AddOns\` scan finishes, we call the TOC parser
|
||||
// directly with `"!!!ClassicAPI"`. The parser opens its TOC via
|
||||
// our hooked read function and registers the addon as a normal
|
||||
// entry. The TOC parser is dedup-safe: if the user already has
|
||||
// the addon on disk, the engine's scan picked it up, the
|
||||
// hash-table lookup hits, and our call is a no-op. So the
|
||||
// conditional "only if not on disk" behavior is free.
|
||||
// 3. We hook the exists-check `FUN_FILE_EXISTS` so it answers the same
|
||||
// way for the same paths. Without it every embedded file reads fine
|
||||
// but reports as missing — the per-addon loader would skip an
|
||||
// embedded `Bindings.xml`, and `Xml::ScriptPath` would never find an
|
||||
// embedded `<Script file="sub\x.lua">`.
|
||||
//
|
||||
// 4. We pre-hook the engine's `FUN_ADDON_SCAN_FULL` and call the TOC
|
||||
// parser with each embedded name before the scan runs. The registry
|
||||
// is empty there (the login init resets it just before), so our
|
||||
// entries are the first records and the load pass — which walks the
|
||||
// records in registration order — loads `!!!ClassicAPI` first. The
|
||||
// scan's own `## LoadWith:` pass then sees them like any disk addon.
|
||||
// The parser is dedup-safe: when the user also has an addon on disk,
|
||||
// the scan's call for that folder hits the hash table and is a no-op.
|
||||
//
|
||||
// Every embedded addon is hidden from the addon list and force-enabled,
|
||||
// the way Blizzard's own addons are always available and never listed.
|
||||
//
|
||||
// Same pattern WeirdUtils.dll uses (we reverse-engineered it from
|
||||
// `0x10015A74` while figuring this out). The addon load pass then
|
||||
@@ -43,10 +59,11 @@
|
||||
// `Compat.lua`, `Util\Print.lua`, etc. all surface from
|
||||
// embedded buffers.
|
||||
|
||||
#include "embedded_classicapi.h"
|
||||
#include "embedded_resources.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Embedded.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/FlavorBindings.h"
|
||||
#include "addons/FlavorToc.h"
|
||||
@@ -54,6 +71,7 @@
|
||||
#include "addons/Toc.h"
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "bindings/Inject.h"
|
||||
#include "embed/Resource.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -63,23 +81,23 @@ namespace Addons::Embedded {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kAddonName = "!!!ClassicAPI";
|
||||
constexpr const char *kAddonTocFile = "!!!ClassicAPI.toc";
|
||||
using Manifest::kAddonCount;
|
||||
using Manifest::kAddons;
|
||||
|
||||
// Developer marker. When this file exists in the on-disk addon folder,
|
||||
// the disk copy wins unconditionally (see `DiskHasDevMarker` /
|
||||
// `DecideSource`). It is gitignored, excluded from the embed, and the
|
||||
// release ships only the DLL — so it can only exist where a developer
|
||||
// deliberately created it, never in a normal user's install.
|
||||
// Developer marker. When this file exists in an on-disk addon folder,
|
||||
// the disk copy of that addon wins unconditionally (see
|
||||
// `DiskHasDevMarker` / `DecideSource`). It is gitignored, excluded from
|
||||
// the embed, and the release ships only the DLL — so it can only exist
|
||||
// where a developer deliberately created it, never in a normal user's
|
||||
// install.
|
||||
constexpr const char *kDevMarkerFile = ".classicapi-dev";
|
||||
|
||||
// `Interface\AddOns\!!!ClassicAPI\` — the prefix the engine
|
||||
// constructs for any of our addon's files. Comparing case-
|
||||
// `Interface\AddOns\` — the prefix the engine constructs for any addon
|
||||
// file; the manifest paths are relative to it. Comparing case-
|
||||
// insensitively because the TOC parser uses `Interface\AddOns\%s\…`
|
||||
// where %s comes from the on-disk directory name, and Windows file
|
||||
// paths are case-insensitive.
|
||||
constexpr const char *kAddonPathPrefix = "Interface\\AddOns\\!!!ClassicAPI\\";
|
||||
constexpr size_t kAddonPathPrefixLen = 30; // length of the literal above
|
||||
constexpr const char *kAddOnsPrefix = "Interface\\AddOns\\";
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
@@ -95,37 +113,63 @@ bool PathEqualsCI(const char *a, const char *b) {
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
// Strip the `Interface\AddOns\!!!ClassicAPI\` prefix and return the
|
||||
// suffix (e.g. `Util\Print.lua`) on a match, NULL otherwise.
|
||||
const char *StripAddonPrefix(const char *path) {
|
||||
if (path == nullptr) return nullptr;
|
||||
const char *p = path;
|
||||
const char *q = kAddonPathPrefix;
|
||||
while (*q) {
|
||||
if (*p == '\0') return nullptr;
|
||||
if (NormalizeChar(*p) != NormalizeChar(*q)) return nullptr;
|
||||
++p; ++q;
|
||||
// `path` with the case-insensitive `prefix` stripped, NULL when it does
|
||||
// not start with it.
|
||||
const char *StripPrefixCI(const char *path, const char *prefix) {
|
||||
while (*prefix) {
|
||||
if (*path == '\0') return nullptr;
|
||||
if (NormalizeChar(*path) != NormalizeChar(*prefix)) return nullptr;
|
||||
++path; ++prefix;
|
||||
}
|
||||
return p;
|
||||
return path;
|
||||
}
|
||||
|
||||
const ClassicAPIFiles::File *LookupEmbedded(const char *suffix) {
|
||||
for (size_t i = 0; i < ClassicAPIFiles::kFileCount; ++i) {
|
||||
if (PathEqualsCI(suffix, ClassicAPIFiles::kFiles[i].path))
|
||||
return &ClassicAPIFiles::kFiles[i];
|
||||
int IndexOfAddon(const char *name) {
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
if (PathEqualsCI(name, kAddons[i]))
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
return nullptr;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The embedded addon `path` lies in (`Interface\AddOns\<addon>\…`), or -1.
|
||||
// `*relPath` gets the manifest-relative part (`<addon>\Util\Print.lua`).
|
||||
int MatchAddonPath(const char *path, const char **relPath) {
|
||||
if (path == nullptr) return -1;
|
||||
const char *rel = StripPrefixCI(path, kAddOnsPrefix);
|
||||
if (rel == nullptr) return -1;
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
const char *rest = StripPrefixCI(rel, kAddons[i]);
|
||||
if (rest != nullptr && NormalizeChar(*rest) == '\\') {
|
||||
*relPath = rel;
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The embedded bytes for manifest path `relPath`; `data` is NULL when it
|
||||
// is not embedded.
|
||||
Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
for (size_t i = 0; i < Manifest::kFileCount; ++i) {
|
||||
if (PathEqualsCI(relPath, Manifest::kFiles[i].path))
|
||||
return Embed::Resource(Manifest::kFiles[i].resourceId);
|
||||
}
|
||||
return {nullptr, 0};
|
||||
}
|
||||
|
||||
// Engine file I/O + Storm allocator — see addons/EngineIO.h for the shapes and
|
||||
// the __stdcall / ESP-drift hazard. A buffer we SMemAlloc here is freed cleanly
|
||||
// by the caller's standard SMemFree in turn.
|
||||
using AddOns::EngineIO::FileExistsFn;
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemAllocFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
|
||||
// The trampoline to the original FUN_FILE_READ (installed by the co-hook below).
|
||||
// The trampolines to the original FUN_FILE_READ / FUN_FILE_EXISTS (installed
|
||||
// by the co-hooks below).
|
||||
FileReadFn FileRead_o = nullptr;
|
||||
FileExistsFn FileExists_o = nullptr;
|
||||
|
||||
// Extracts the value of the `## Version: X` line from a TOC byte
|
||||
// buffer. Writes into `out` (size `outSize`) and returns true on
|
||||
@@ -137,7 +181,7 @@ bool ExtractTocVersion(const char *content, size_t size,
|
||||
size_t n = 0;
|
||||
if (outSize == 0)
|
||||
return false;
|
||||
if (!AddOns::Toc::FindValue(content, size, "## Version:", &v, &n) || n == 0) {
|
||||
if (!AddOns::Toc::FindValue(content, size, "Version", &v, &n) || n == 0) {
|
||||
out[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
@@ -176,79 +220,63 @@ int CompareVersions(const char *a, const char *b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Pre-extracted embedded TOC version, populated lazily on first
|
||||
// FileRead_h call under our prefix. Empty until populated.
|
||||
char g_embeddedVersion[64] = "";
|
||||
|
||||
// Which source wins the embed-vs-disk comparison, decided once on
|
||||
// first relevant file read.
|
||||
// Which source wins the embed-vs-disk comparison, decided per addon once
|
||||
// on its first file read.
|
||||
enum class Source { Undecided, Disk, Embedded };
|
||||
Source g_source = Source::Undecided;
|
||||
Source g_source[kAddonCount] = {};
|
||||
|
||||
void EnsureEmbeddedVersionExtracted() {
|
||||
if (g_embeddedVersion[0] != '\0') return;
|
||||
for (size_t i = 0; i < ClassicAPIFiles::kFileCount; ++i) {
|
||||
if (PathEqualsCI(kAddonTocFile, ClassicAPIFiles::kFiles[i].path)) {
|
||||
ExtractTocVersion(
|
||||
reinterpret_cast<const char *>(ClassicAPIFiles::kFiles[i].data),
|
||||
ClassicAPIFiles::kFiles[i].size,
|
||||
g_embeddedVersion, sizeof(g_embeddedVersion));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// True iff the on-disk addon folder contains the `.classicapi-dev`
|
||||
// marker. Read via the ORIGINAL `FUN_FILE_READ` (bypassing our embed
|
||||
// True iff addon `addon`'s on-disk folder contains the `.classicapi-dev`
|
||||
// marker. Asked of the ORIGINAL `FUN_FILE_EXISTS` (bypassing our embed
|
||||
// hook), so it reflects a real file on disk / in an MPQ — never an
|
||||
// embedded copy. That makes it a clean "is this a developer working
|
||||
// tree" signal: the marker is gitignored + excluded from the embed and
|
||||
// the release ships only the DLL, so a normal user's install can't
|
||||
// carry it.
|
||||
bool DiskHasDevMarker() {
|
||||
bool DiskHasDevMarker(int addon) {
|
||||
char fullPath[256];
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s",
|
||||
kAddonPathPrefix, kDevMarkerFile);
|
||||
void *buf = nullptr;
|
||||
size_t size = 0;
|
||||
const int ok = FileRead_o(0, fullPath, &buf, &size, 1, 1, 0);
|
||||
if (ok == 0 || buf == nullptr)
|
||||
return false;
|
||||
auto SMemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
SMemFree(buf, __FILE__, __LINE__, 0);
|
||||
return true;
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s\\%s", kAddOnsPrefix,
|
||||
kAddons[addon], kDevMarkerFile);
|
||||
return FileExists_o(fullPath, 1) != 0;
|
||||
}
|
||||
|
||||
// Decide whether disk or embedded should serve all reads for this
|
||||
// addon. Called on the first FileRead_h call with a matching prefix.
|
||||
// The `.classicapi-dev` marker forces disk unconditionally (explicit
|
||||
// developer override). Otherwise reads the on-disk TOC via the original
|
||||
// `FUN_FILE_READ` (bypassing our hook), parses its version, compares to
|
||||
// the embedded version, and caches the newer. If disk has no TOC at
|
||||
// all, embedded wins by default.
|
||||
void DecideSource() {
|
||||
if (g_source != Source::Undecided) return;
|
||||
EnsureEmbeddedVersionExtracted();
|
||||
// Decide whether disk or embedded should serve all reads for addon
|
||||
// `addon`. Called on the first FileRead_h / FileExists_h call under its
|
||||
// folder. The `.classicapi-dev` marker forces disk unconditionally
|
||||
// (explicit developer override). Otherwise reads the on-disk TOC via the
|
||||
// original `FUN_FILE_READ` (bypassing our hook), parses its version,
|
||||
// compares to the embedded version, and caches the newer. If disk has no
|
||||
// TOC at all, embedded wins by default.
|
||||
void DecideSource(int addon) {
|
||||
if (g_source[addon] != Source::Undecided) return;
|
||||
|
||||
// Explicit developer override: disk wins regardless of version. This
|
||||
// is how a dev keeps disk precedence when running local Lua against a
|
||||
// *released* DLL (where the embedded version would otherwise be newer
|
||||
// and win). Dev intent lives in this marker, decoupled from the toc
|
||||
// version — so users never get silently shadowed by a stray copy.
|
||||
if (DiskHasDevMarker()) {
|
||||
g_source = Source::Disk;
|
||||
if (DiskHasDevMarker(addon)) {
|
||||
g_source[addon] = Source::Disk;
|
||||
return;
|
||||
}
|
||||
|
||||
char embeddedVersion[64] = "";
|
||||
char tocRelPath[256];
|
||||
std::snprintf(tocRelPath, sizeof(tocRelPath), "%s\\%s.toc",
|
||||
kAddons[addon], kAddons[addon]);
|
||||
const Embed::Bytes toc = LookupEmbedded(tocRelPath);
|
||||
if (toc.data != nullptr)
|
||||
ExtractTocVersion(reinterpret_cast<const char *>(toc.data), toc.size,
|
||||
embeddedVersion, sizeof(embeddedVersion));
|
||||
|
||||
char fullPath[256];
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s",
|
||||
kAddonPathPrefix, kAddonTocFile);
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s", kAddOnsPrefix,
|
||||
tocRelPath);
|
||||
|
||||
void *diskBuf = nullptr;
|
||||
size_t diskSize = 0;
|
||||
const int ok = FileRead_o(0, fullPath, &diskBuf, &diskSize, 1, 1, 0);
|
||||
if (ok == 0 || diskBuf == nullptr) {
|
||||
g_source = Source::Embedded;
|
||||
g_source[addon] = Source::Embedded;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -265,12 +293,12 @@ void DecideSource() {
|
||||
// Missing or unparseable disk version → assume it's older than
|
||||
// anything we ship, embedded wins.
|
||||
if (diskVersion[0] == '\0') {
|
||||
g_source = Source::Embedded;
|
||||
g_source[addon] = Source::Embedded;
|
||||
return;
|
||||
}
|
||||
|
||||
const int cmp = CompareVersions(g_embeddedVersion, diskVersion);
|
||||
g_source = (cmp > 0) ? Source::Embedded : Source::Disk;
|
||||
const int cmp = CompareVersions(embeddedVersion, diskVersion);
|
||||
g_source[addon] = (cmp > 0) ? Source::Embedded : Source::Disk;
|
||||
}
|
||||
|
||||
int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
@@ -284,9 +312,10 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *suffix = StripAddonPrefix(path);
|
||||
if (suffix == nullptr) {
|
||||
// Not our embedded addon. If this is a base-TOC read for a multi-flavor
|
||||
const char *relPath = nullptr;
|
||||
const int addon = MatchAddonPath(path, &relPath);
|
||||
if (addon < 0) {
|
||||
// Not an embedded addon. If this is a base-TOC read for a multi-flavor
|
||||
// addon that ships only `<Name>_Turtle.toc` / `_ClassicAPI.toc`, serve
|
||||
// that in place of the missing `<Name>.toc` so it registers and loads
|
||||
// (see addons/FlavorToc.h).
|
||||
@@ -315,9 +344,9 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
return result;
|
||||
}
|
||||
|
||||
DecideSource();
|
||||
DecideSource(addon);
|
||||
|
||||
if (g_source == Source::Disk) {
|
||||
if (g_source[addon] == Source::Disk) {
|
||||
// Disk version is at least as new — serve disk. If disk
|
||||
// doesn't have this specific file (e.g. embedded has files
|
||||
// disk doesn't), fall through and try embedded.
|
||||
@@ -329,8 +358,8 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
// Source::Embedded path, OR Source::Disk-but-file-missing fallback.
|
||||
// Look up the embedded version and synthesize a Storm buffer the
|
||||
// caller can free normally.
|
||||
const auto *entry = LookupEmbedded(suffix);
|
||||
if (entry == nullptr) {
|
||||
const Embed::Bytes entry = LookupEmbedded(relPath);
|
||||
if (entry.data == nullptr) {
|
||||
// Not in our embedded set either — let the engine try disk
|
||||
// one more time (returns 0 if that also fails). Covers
|
||||
// pathological cases like an empty disk install + embedded
|
||||
@@ -341,115 +370,99 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(
|
||||
Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
const size_t totalSize = entry->size + extraBytes;
|
||||
const size_t totalSize = entry.size + extraBytes;
|
||||
void *buf = SMemAlloc(totalSize, __FILE__, __LINE__, 0);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry->data, entry->size);
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
std::memset(static_cast<uint8_t *>(buf) + entry->size, 0, extraBytes);
|
||||
std::memset(static_cast<uint8_t *>(buf) + entry.size, 0, extraBytes);
|
||||
}
|
||||
if (outBuf != nullptr) *outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = entry->size;
|
||||
if (outSize != nullptr) *outSize = entry.size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `FUN_ADDON_INIT` — `__fastcall(accountName)`. We post-hook it: let
|
||||
// the engine's normal `Interface\AddOns\` scan run first, then call
|
||||
// the TOC parser with `"!!!ClassicAPI"`. Dedup-safe — see file header.
|
||||
using AddonInit_t = void(__fastcall *)(const char *accountName);
|
||||
AddonInit_t AddonInit_o = nullptr;
|
||||
// The exists-check counterpart of FileRead_h: the same source decision and
|
||||
// the same fallbacks, so a path reports as existing exactly when FileRead_h
|
||||
// would serve it. Other paths go to FlavorBindings (flavor-only addon
|
||||
// Bindings.xml), then to the original.
|
||||
int __stdcall FileExists_h(const char *path, int mode) {
|
||||
const char *relPath = nullptr;
|
||||
const int addon = MatchAddonPath(path, &relPath);
|
||||
if (addon < 0) {
|
||||
if (FileExists_o(path, mode) != 0)
|
||||
return 1;
|
||||
return AddOns::FlavorBindings::TryExists(path, mode, FileExists_o) ? 1 : 0;
|
||||
}
|
||||
|
||||
DecideSource(addon);
|
||||
if (g_source[addon] == Source::Disk && FileExists_o(path, mode) != 0)
|
||||
return 1;
|
||||
if (LookupEmbedded(relPath).data != nullptr)
|
||||
return 1;
|
||||
return FileExists_o(path, mode);
|
||||
}
|
||||
|
||||
// `FUN_ADDON_SCAN_FULL` — `void __cdecl()`. We pre-hook it: register the
|
||||
// embedded addons into the empty registry, then let the engine's scan
|
||||
// run. Dedup-safe — see file header.
|
||||
using AddonScan_t = void(__cdecl *)();
|
||||
AddonScan_t AddonScan_o = nullptr;
|
||||
|
||||
using TocParser_t = void(__fastcall *)(const char *name);
|
||||
using ListInsert_t = void(__thiscall *)(void *listCtrl, void *entry,
|
||||
int position, int anchor);
|
||||
|
||||
// `FUN_TOC_PARSER` appends new entries to the *tail* of the addon
|
||||
// registry's linked list, and the engine's addon-load pass at
|
||||
// `FUN_0051F600` walks that list in *insertion order* (NOT
|
||||
// alphabetical order — the alphabetical sort only applies to the
|
||||
// flat array `GetAddOnInfo(i)` reads from). So our post-hook
|
||||
// injection lands at the very end of the load order, after every
|
||||
// disk-scanned addon. Addons that consume our globals from their
|
||||
// main chunk (idTip's `FrameWatcher`, etc.) would see `nil` and
|
||||
// hard-error during their boot.
|
||||
//
|
||||
// Fix: walk the linked list, find our entry by name, and re-link
|
||||
// it to the head via `FUN_INTRUSIVE_LIST_INSERT(position=1)`. The
|
||||
// engine's own insert helper handles remove-then-insert atomically,
|
||||
// so it's safe to call on an entry that's already in the list.
|
||||
//
|
||||
// The list is walked via the shared `Addons::ForEachEntry`
|
||||
// (`addons/Registry.h`); the callback stops the walk once it finds and
|
||||
// re-links our entry, since the re-link invalidates the current
|
||||
// next-pointer.
|
||||
//
|
||||
// `entry + OFF_ADDON_ENTRY_FILTER_OUT` is the "filter-out" byte. The
|
||||
// flat display-array builder `FUN_0051da70` walks the linked list and
|
||||
// copies an entry into `GetNumAddOns`/`GetAddOnInfo`'s array ONLY when
|
||||
// this byte is 0, so setting it hides `!!!ClassicAPI` from the
|
||||
// this byte is 0, so setting it hides an embedded addon from the
|
||||
// character-select AddOns list — there's no checkbox, so it can't be
|
||||
// toggled off. The builder only ever *writes* this byte on the
|
||||
// secure/SMSG path (`entry+0x28 != 0`); our entry is non-secure, so a
|
||||
// manual `1` here is stable across rebuilds. Hiding does NOT stop
|
||||
// loading (the load pass `FUN_0051f600` never reads `+0x29`) and does
|
||||
// NOT break dependency resolution or by-name queries (those use the
|
||||
// addon hash table, which keeps the entry regardless).
|
||||
// addon hash table, which keeps the entry regardless — `LoadAddOn` of a
|
||||
// hidden load-on-demand entry still works).
|
||||
//
|
||||
// DefaultState (`+0x2b`) needs no write: the entry constructor
|
||||
// `FUN_00520550` stores 1 there, our tocs have no `## DefaultState:`, and
|
||||
// the resolver co-hook below forces enabled anyway.
|
||||
|
||||
// `entry+0x2b` — DefaultState (`## DefaultState: enabled/disabled`).
|
||||
// The enable resolver `FUN_ADDON_ENABLE_RESOLVE` falls back to this
|
||||
// when the player has no per-character override. Belt-and-suspenders
|
||||
// alongside the resolver co-hook below (which forces enabled anyway).
|
||||
constexpr int OFF_ADDON_ENTRY_DEFAULT_STATE = 0x2b;
|
||||
|
||||
// Post-registration finalize for the embedded entry: move it to the
|
||||
// head of the load-order list (so consumers that read our globals from
|
||||
// their main chunk see them — see the block comment above the offsets),
|
||||
// hide it from the addon list, and mark it default-enabled. Combined
|
||||
// with the `FUN_ADDON_ENABLE_RESOLVE` co-hook, `!!!ClassicAPI` always
|
||||
// loads and never appears as a toggleable entry.
|
||||
void FinalizeEmbeddedEntry() {
|
||||
Addons::ForEachEntry([](uintptr_t entry) -> bool {
|
||||
// Post-registration finalize for the embedded entries: hide them from the
|
||||
// addon list. Combined with the `FUN_ADDON_ENABLE_RESOLVE` co-hook, an
|
||||
// embedded addon is always loadable and never appears as a toggleable
|
||||
// entry.
|
||||
void FinalizeEmbeddedEntries() {
|
||||
Addons::ForEachEntry([](uintptr_t entry) {
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_NAME_PTR);
|
||||
if (name == nullptr || std::strcmp(name, kAddonName) != 0)
|
||||
return true; // keep walking
|
||||
|
||||
// Hide from the character-select AddOns list and default it to
|
||||
// enabled, then move it to the head of the load order.
|
||||
*reinterpret_cast<uint8_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_FILTER_OUT) = 1;
|
||||
*reinterpret_cast<uint8_t *>(entry + OFF_ADDON_ENTRY_DEFAULT_STATE) = 1;
|
||||
|
||||
auto fn = reinterpret_cast<ListInsert_t>(
|
||||
Offsets::FUN_INTRUSIVE_LIST_INSERT);
|
||||
fn(reinterpret_cast<void *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL)),
|
||||
reinterpret_cast<void *>(entry),
|
||||
/*position=*/ 1, /*anchor=*/ 0);
|
||||
return false; // stop — the re-link invalidated our next-pointer
|
||||
if (name != nullptr && IndexOfAddon(name) >= 0)
|
||||
*reinterpret_cast<uint8_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_FILTER_OUT) = 1;
|
||||
});
|
||||
}
|
||||
|
||||
void __fastcall AddonInit_h(const char *accountName) {
|
||||
AddonInit_o(accountName);
|
||||
void __cdecl AddonScan_h() {
|
||||
auto TocParser = reinterpret_cast<TocParser_t>(Offsets::FUN_TOC_PARSER);
|
||||
TocParser(kAddonName);
|
||||
FinalizeEmbeddedEntry();
|
||||
for (size_t i = 0; i < kAddonCount; ++i)
|
||||
TocParser(kAddons[i]);
|
||||
FinalizeEmbeddedEntries();
|
||||
AddonScan_o();
|
||||
}
|
||||
|
||||
// `FUN_ADDON_ENABLE_RESOLVE` — per-character enable resolver. We
|
||||
// co-hook it to force-enable `!!!ClassicAPI` regardless of DefaultState
|
||||
// or any stale WTF disable-override, so hiding it from the addon list
|
||||
// can never leave it stuck off. Returns 2 ("enabled for all"); the
|
||||
// load-pass gate only tests `!= 0`. Every other addon falls through to
|
||||
// the original untouched. See the Offsets.h note for the ABI.
|
||||
// co-hook it to force-enable the embedded addons regardless of
|
||||
// DefaultState or any stale WTF disable-override, so hiding them from
|
||||
// the addon list can never leave one stuck off. Returns 2 ("enabled for
|
||||
// all"); the load-pass gate only tests `!= 0`. Every other addon falls
|
||||
// through to the original untouched. See the Offsets.h note for the ABI.
|
||||
using AddonEnableResolve_t =
|
||||
uint32_t(__fastcall *)(const char *name, const char *account, char resolveAll);
|
||||
AddonEnableResolve_t AddonEnableResolve_o = nullptr;
|
||||
|
||||
uint32_t __fastcall AddonEnableResolve_h(const char *name, const char *account,
|
||||
char resolveAll) {
|
||||
if (name != nullptr && PathEqualsCI(name, kAddonName))
|
||||
if (name != nullptr && IndexOfAddon(name) >= 0)
|
||||
return 2;
|
||||
return AddonEnableResolve_o(name, account, resolveAll);
|
||||
}
|
||||
@@ -459,10 +472,15 @@ const Game::HookAutoRegister _hookFileRead{
|
||||
reinterpret_cast<void *>(&FileRead_h),
|
||||
reinterpret_cast<void **>(&FileRead_o)};
|
||||
|
||||
const Game::HookAutoRegister _hookAddonInit{
|
||||
Offsets::FUN_ADDON_INIT,
|
||||
reinterpret_cast<void *>(&AddonInit_h),
|
||||
reinterpret_cast<void **>(&AddonInit_o)};
|
||||
const Game::HookAutoRegister _hookFileExists{
|
||||
Offsets::FUN_FILE_EXISTS,
|
||||
reinterpret_cast<void *>(&FileExists_h),
|
||||
reinterpret_cast<void **>(&FileExists_o)};
|
||||
|
||||
const Game::HookAutoRegister _hookAddonScan{
|
||||
Offsets::FUN_ADDON_SCAN_FULL,
|
||||
reinterpret_cast<void *>(&AddonScan_h),
|
||||
reinterpret_cast<void **>(&AddonScan_o)};
|
||||
|
||||
const Game::HookAutoRegister _hookEnableResolve{
|
||||
Offsets::FUN_ADDON_ENABLE_RESOLVE,
|
||||
@@ -471,4 +489,8 @@ const Game::HookAutoRegister _hookEnableResolve{
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsEmbedded(const char *name) {
|
||||
return name != nullptr && IndexOfAddon(name) >= 0;
|
||||
}
|
||||
|
||||
} // namespace Addons::Embedded
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Addons::Embedded {
|
||||
|
||||
// True when `name` is one of the addons embedded in the DLL (compared
|
||||
// ignoring case). Their registry entries are owned by `Addons::Embedded`:
|
||||
// registered first, hidden from the addon list, force-enabled.
|
||||
bool IsEmbedded(const char *name);
|
||||
|
||||
} // namespace Addons::Embedded
|
||||
@@ -15,8 +15,6 @@
|
||||
#include "addons/FlavorBindings.h"
|
||||
#include "addons/Toc.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstddef>
|
||||
@@ -72,40 +70,24 @@ int FlavorOrder(const char *(&order)[2]) {
|
||||
return count;
|
||||
}
|
||||
|
||||
// Engine file-exists probe FUN_00648a30 — __stdcall(path, mode) (RET 8).
|
||||
using EngineIO::FileExistsFn;
|
||||
FileExistsFn g_origExists = nullptr;
|
||||
} // namespace
|
||||
|
||||
// Co-hook the exists-check so `Interface\AddOns\<Name>\Bindings.xml` reports as
|
||||
// existing when the base file is absent but a flavor variant is present — so
|
||||
// the loader (`FUN_004b6f70`, then our TryHandle redirect) runs for a
|
||||
// flavor-only addon. Every non-Bindings path passes straight through.
|
||||
int __stdcall FileExists_h(const char *path, int mode) {
|
||||
bool TryExists(const char *path, int mode, FileExistsFn orig) {
|
||||
const size_t prefixLen = AddonBindingsPrefix(path);
|
||||
if (prefixLen == NPOS)
|
||||
return g_origExists(path, mode);
|
||||
|
||||
if (g_origExists(path, mode) != 0)
|
||||
return 1; // the plain Bindings.xml exists
|
||||
return false;
|
||||
|
||||
const char *order[2];
|
||||
const int count = FlavorOrder(order);
|
||||
char cand[512];
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (BuildFlavorPath(path, prefixLen, order[i], cand, sizeof cand) &&
|
||||
g_origExists(cand, mode) != 0)
|
||||
return 1;
|
||||
orig(cand, mode) != 0)
|
||||
return true;
|
||||
}
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookExists{
|
||||
Offsets::FUN_FILE_EXISTS,
|
||||
reinterpret_cast<void *>(&FileExists_h),
|
||||
reinterpret_cast<void **>(&g_origExists)};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn orig) {
|
||||
const size_t prefixLen = AddonBindingsPrefix(path);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
namespace AddOns::FlavorBindings {
|
||||
|
||||
using EngineIO::FileExistsFn;
|
||||
using EngineIO::FileReadFn;
|
||||
|
||||
// Flavor-specific addon Bindings selection — the Bindings.xml analog of
|
||||
@@ -36,10 +37,10 @@ using EngineIO::FileReadFn;
|
||||
// place if one exists (a selected flavor wins over a plain `Bindings.xml`,
|
||||
// matching FlavorToc). Probe order `Bindings_Turtle.xml` (only when
|
||||
// Turtle::Detected) then `Bindings_ClassicAPI.xml`.
|
||||
// 2. A co-hook on the exists-check `FUN_00648a30` (inside FlavorBindings.cpp):
|
||||
// report `Bindings.xml` as existing when the base is absent but a flavor
|
||||
// variant is present — otherwise the loader is never called for a
|
||||
// flavor-only addon.
|
||||
// 2. TryExists, called from the exists-check `FUN_FILE_EXISTS` hook: report
|
||||
// `Bindings.xml` as existing when the base is absent but a flavor variant
|
||||
// is present — otherwise the loader is never called for a flavor-only
|
||||
// addon.
|
||||
//
|
||||
// Only `_Turtle` / `_ClassicAPI` are selected — never `_Vanilla` / `_Classic`
|
||||
// (which target the 1.15 Classic Era build; its bindings reference handlers
|
||||
@@ -52,4 +53,9 @@ using EngineIO::FileReadFn;
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn orig);
|
||||
|
||||
// True when `path` is an addon `…\AddOns\<Name>\Bindings.xml` and a flavor
|
||||
// variant exists through `orig` (the original exists-check). The caller asks
|
||||
// `orig` about the base path itself; this answers only the flavor case.
|
||||
bool TryExists(const char *path, int mode, FileExistsFn orig);
|
||||
|
||||
} // namespace AddOns::FlavorBindings
|
||||
|
||||
+72
-51
@@ -26,6 +26,8 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/LoadState.h"
|
||||
#include "addons/Registry.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -34,6 +36,7 @@ namespace AddOns::Info {
|
||||
namespace {
|
||||
|
||||
using GetByIndex_t = void *(__fastcall *)(int idx0Based);
|
||||
using GetAddOnCount_t = int(__fastcall *)();
|
||||
using FieldByName_t = const char *(__fastcall *)(const char *name);
|
||||
using EntryByteFn_t = uint8_t(__fastcall *)(const uint8_t *entry);
|
||||
using GetSecurityIndex_t = uint32_t(__fastcall *)(const uint8_t *entry);
|
||||
@@ -42,9 +45,8 @@ using CanLoadFn_t = uint8_t(__fastcall *)(const uint8_t *entry, char fullCheck,
|
||||
const char *accountName);
|
||||
using GetAccountName_t = const char *(*)();
|
||||
// Hash-lookup by name → the matched record's RequiredDeps descriptor
|
||||
// (`record + 0x48`), NULL on miss. Accepts an entry pointer too (its
|
||||
// first bytes are the inline name). We use it only to recover the
|
||||
// record base for string input; see `Script_GetAddOnOptionalDependencies`.
|
||||
// (`record + 0x48`), NULL on miss. We use it only to recover the
|
||||
// record base; see `Script_GetAddOnOptionalDependencies`.
|
||||
using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry);
|
||||
|
||||
// Resolves the user's arg to a `const uint8_t *` that the engine's
|
||||
@@ -52,8 +54,7 @@ using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry);
|
||||
// a NUL-terminated string starting at the input pointer and case-
|
||||
// insensitively match it against the registry, so either of these
|
||||
// shapes works:
|
||||
// - an AddOnEntry pointer from `GetByIndex` (the entry's first
|
||||
// 12 bytes ARE the inline name buffer)
|
||||
// - the name pointer `GetByIndex` returns (`[entry+0x14]`)
|
||||
// - a raw `const char *` to a Lua string
|
||||
//
|
||||
// Mirrors `Script_GetAddOnInfo`'s own input handling exactly — it
|
||||
@@ -251,19 +252,22 @@ int __fastcall Script_GetAddOnSecurity(void *L) {
|
||||
|
||||
// `C_AddOns.IsAddOnLoaded(indexOrName)` → `(loadedOrLoading, loaded)`.
|
||||
//
|
||||
// Modern WoW distinguishes "load-in-progress" from "fully loaded" —
|
||||
// matters for LoD addons whose load is split across multiple
|
||||
// `LoadAddOn` callbacks. Vanilla 1.12's `FUN_0051F240` is fully
|
||||
// synchronous: the loaded byte at `entry+0x18` flips from 0 → 1
|
||||
// inside a single call, so the "loading" state is never observable
|
||||
// from Lua. We return the same boolean twice — `loadedOrLoading`
|
||||
// and `loaded` are identical here.
|
||||
// Modern WoW distinguishes "load-in-progress" from "fully loaded", and
|
||||
// the two really are distinguishable here: the engine's loaded byte
|
||||
// (`entry+0x18`) is set at the START of a load, before the addon's files
|
||||
// run, so the byte alone answers `loadedOrLoading`, not `loaded`. The
|
||||
// window is reachable from Lua — an addon's own file-scope code, and any
|
||||
// dependency pulled in during it, run while the byte already reads set.
|
||||
//
|
||||
// So `loadedOrLoading` is the engine's byte, and `loaded` additionally
|
||||
// requires the addon not to be mid-load, which `AddOns::LoadState`
|
||||
// tracks from the loader itself.
|
||||
//
|
||||
// Resolves both numeric indices (via `FUN_ADDON_GET_BY_INDEX`) and
|
||||
// string names. `IS_LOADED` accepts an entry pointer as its name arg
|
||||
// because the entry's first 12 bytes are the inline NUL-terminated
|
||||
// directory name (same as the other per-field accessors), so we can
|
||||
// feed it either an entry or a Lua-side string transparently.
|
||||
// string names. `FUN_ADDON_GET_BY_INDEX` returns the entry's name
|
||||
// pointer, so `IS_LOADED` (like the other per-field accessors) takes
|
||||
// either that or a Lua-side string transparently — and that same
|
||||
// pointer is the name `LoadState` matches on.
|
||||
int __fastcall Script_IsAddOnLoaded(void *L) {
|
||||
const uint8_t *entry = ResolveAddOnName(L);
|
||||
if (entry == nullptr) {
|
||||
@@ -273,8 +277,11 @@ int __fastcall Script_IsAddOnLoaded(void *L) {
|
||||
}
|
||||
auto isLoaded = reinterpret_cast<EntryByteFn_t>(
|
||||
Offsets::FUN_ADDON_IS_LOADED);
|
||||
const bool loaded = isLoaded(entry) != 0;
|
||||
Game::Lua::PushBool(L, loaded);
|
||||
const bool loadedOrLoading = isLoaded(entry) != 0;
|
||||
const bool loaded =
|
||||
loadedOrLoading &&
|
||||
!AddOns::LoadState::IsLoading(reinterpret_cast<const char *>(entry));
|
||||
Game::Lua::PushBool(L, loadedOrLoading);
|
||||
Game::Lua::PushBool(L, loaded);
|
||||
return 2;
|
||||
}
|
||||
@@ -303,46 +310,20 @@ int __fastcall Script_DoesAddOnExist(void *L) {
|
||||
// `C_AddOns.GetAddOnOptionalDependencies(indexOrName)` — the addon's
|
||||
// `## OptionalDeps:` names as multiple return values, in declared order.
|
||||
// The exact shape of the stock `GetAddOnDependencies`, but reading the
|
||||
// *optional* dep array instead of the required one. No optional deps →
|
||||
// returns nothing.
|
||||
// *optional* dep array instead of the required one. No optional deps or
|
||||
// an unknown name → returns nothing.
|
||||
//
|
||||
// Vanilla parses `## OptionalDeps:` in the TOC parser `FUN_0051c9b0`
|
||||
// into a `{cap, count, data, quantum}` name-array descriptor at
|
||||
// `record+0x38` (count@+0x3C, data@+0x40), but ships no getter for it.
|
||||
//
|
||||
// Arg/error handling mirrors stock `Script_GetAddOnDependencies`
|
||||
// (`0x0048E5E0`): numeric out-of-range or a non-string/non-number arg
|
||||
// raises the usage error; an unknown *name* string returns nothing
|
||||
// (no error), matching the engine's own not-found path.
|
||||
//
|
||||
// Resolution mirrors stock `Script_GetAddOnDependencies` exactly: the
|
||||
// arg is first turned into a name-usable pointer (`ResolveAddOnName` —
|
||||
// numeric via `FUN_ADDON_GET_BY_INDEX`, string is the raw string), then
|
||||
// routed through the by-name hash lookup `FUN_ADDON_RESOLVE_REQ_DEPS`
|
||||
// to reach the deps-bearing record. `FUN_ADDON_GET_BY_INDEX` returns a
|
||||
// *name pointer*, NOT the record — reading dep offsets off it lands in
|
||||
// string memory and crashes (a numeric-path caller like pfUI's addon
|
||||
// list hit exactly that). The resolver returns the RequiredDeps
|
||||
// descriptor at `record+0x48`; subtract to recover the base. An unknown
|
||||
// name yields NULL → return nothing (no error), like stock.
|
||||
// Like stock, `lua_checkstack(count)` before the pushes — a C function
|
||||
// is only guaranteed LUA_MINSTACK (20) free slots.
|
||||
int __fastcall Script_GetAddOnOptionalDependencies(void *L) {
|
||||
static constexpr const char *kUsage =
|
||||
"Usage: C_AddOns.GetAddOnOptionalDependencies(index or \"name\")";
|
||||
|
||||
// NULL ⇒ out-of-range numeric index or a non-string/non-number arg;
|
||||
// stock raises the usage error in both cases.
|
||||
const uint8_t *nameOrEntry = ResolveAddOnName(L);
|
||||
if (nameOrEntry == nullptr) {
|
||||
Game::Lua::Error(L, "%s", kUsage);
|
||||
const uint8_t *record = Addons::ResolveRecord(
|
||||
L, "Usage: C_AddOns.GetAddOnOptionalDependencies(index or \"name\")");
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveReqDeps_t>(
|
||||
Offsets::FUN_ADDON_RESOLVE_REQ_DEPS);
|
||||
uint8_t *reqDesc = resolve(reinterpret_cast<const char *>(nameOrEntry));
|
||||
if (reqDesc == nullptr)
|
||||
return 0; // unknown addon name — no error, push nothing
|
||||
const uint8_t *record = reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC;
|
||||
|
||||
const uint32_t count = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ADDON_OPTIONALDEPS_COUNT);
|
||||
@@ -352,11 +333,30 @@ int __fastcall Script_GetAddOnOptionalDependencies(void *L) {
|
||||
record + Offsets::OFF_ADDON_OPTIONALDEPS_ARRAY);
|
||||
if (names == nullptr)
|
||||
return 0;
|
||||
Game::Lua::CheckStack(L, static_cast<int>(count));
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
Game::Lua::PushString(L, names[i]); // NULL → pushnil, per engine
|
||||
return static_cast<int>(count);
|
||||
}
|
||||
|
||||
// `C_AddOns.GetAddOnInterfaceVersion(indexOrName)` — the `## Interface:`
|
||||
// number the engine parsed into `record+0x1C`, the same value its
|
||||
// loadability check compares against the client version. A comma-list
|
||||
// is already reduced to retail's pick (closest value at or below the
|
||||
// client version) by `AddOns::TocRewrite` before the parser sees it.
|
||||
// nil for an unknown name.
|
||||
int __fastcall Script_GetAddOnInterfaceVersion(void *L) {
|
||||
const uint8_t *record = Addons::ResolveRecord(
|
||||
L, "Usage: C_AddOns.GetAddOnInterfaceVersion(index or \"name\")");
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(*reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ADDON_INTERFACE_VERSION)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `Enum.AddOnSecurityStatus` — what `C_AddOns.GetAddOnSecurity`
|
||||
// returns. Slots 0..2 match the engine's own ordering at
|
||||
// `VAR_ADDON_SECURITY_TABLE`; slot 3 is our addition for
|
||||
@@ -368,6 +368,18 @@ constexpr Game::Lua::EnumIntegerEntry kAddOnSecurityStatusEntries[] = {
|
||||
{"NotAvailable", 3},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kInterfaceVersionArgs[] = {
|
||||
Game::Doc::Req("name", "uiAddon", "An addon index or folder name."),
|
||||
};
|
||||
const Game::Doc::Field kInterfaceVersionRets[] = {
|
||||
Game::Doc::Opt("interfaceVersion", "number", nullptr,
|
||||
"The ## Interface: value from the TOC, or nil for an unknown name."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnInterfaceVersion{
|
||||
"Returns the Interface directive of an addon TOC. For a comma-separated list, "
|
||||
"returns the closest value at or below the client interface version.",
|
||||
kInterfaceVersionArgs, kInterfaceVersionRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
@@ -385,8 +397,17 @@ static void RegisterLuaFunctions() {
|
||||
&Script_DoesAddOnExist);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "IsAddOnLoaded",
|
||||
&Script_IsAddOnLoaded);
|
||||
// The stock global, mirrored into the namespace — the engine's own
|
||||
// handler already has the Lua C ABI and the modern return shape.
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_AddOns", "LoadAddOn",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_LOAD_ADDON)));
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnOptionalDependencies",
|
||||
&Script_GetAddOnOptionalDependencies);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnInterfaceVersion",
|
||||
&Script_GetAddOnInterfaceVersion,
|
||||
&kGetAddOnInterfaceVersion);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AddOnSecurityStatus",
|
||||
kAddOnSecurityStatusEntries,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Tracks in-progress addon loads so `C_AddOns.IsAddOnLoaded` can report
|
||||
// `loadedOrLoading` and `loaded` separately. See `LoadState.h` for why the
|
||||
// engine's own byte can't answer both.
|
||||
//
|
||||
// Loads nest — an addon's dependencies load inside its own call — so this
|
||||
// keeps a stack rather than a single name. The name is copied rather than
|
||||
// stored by pointer: `LoadAddOn` passes a Lua string, which does not
|
||||
// outlive the call.
|
||||
|
||||
#include "addons/LoadState.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace AddOns::LoadState {
|
||||
|
||||
namespace {
|
||||
|
||||
// Dependency chains are shallow; anything deeper simply isn't tracked
|
||||
// (the depth counter still balances, so the stack can't desync).
|
||||
constexpr int kMaxDepth = 16;
|
||||
constexpr int kMaxName = 64;
|
||||
|
||||
char g_stack[kMaxDepth][kMaxName];
|
||||
int g_depth = 0;
|
||||
|
||||
bool EqualsIgnoreCase(const char *a, const char *b) {
|
||||
for (; *a != '\0' && *b != '\0'; ++a, ++b) {
|
||||
char ca = *a;
|
||||
char cb = *b;
|
||||
if (ca >= 'A' && ca <= 'Z')
|
||||
ca = static_cast<char>(ca - 'A' + 'a');
|
||||
if (cb >= 'A' && cb <= 'Z')
|
||||
cb = static_cast<char>(cb - 'A' + 'a');
|
||||
if (ca != cb)
|
||||
return false;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
using LoadOne_t = unsigned int(__fastcall *)(const char *name, char flag,
|
||||
void *progressCtx);
|
||||
LoadOne_t s_loadOne_o = nullptr;
|
||||
|
||||
unsigned int __fastcall LoadOne_h(const char *name, char flag, void *progressCtx) {
|
||||
const int slot = g_depth;
|
||||
if (slot >= 0 && slot < kMaxDepth)
|
||||
Common::BoundedCopy(g_stack[slot], name, kMaxName);
|
||||
++g_depth;
|
||||
|
||||
const unsigned int result = s_loadOne_o(name, flag, progressCtx);
|
||||
|
||||
--g_depth;
|
||||
if (g_depth < 0)
|
||||
g_depth = 0; // defensive: never let the stack run negative
|
||||
return result;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookLoadOne{
|
||||
Offsets::FUN_ADDON_LOAD_ONE,
|
||||
reinterpret_cast<void *>(&LoadOne_h),
|
||||
reinterpret_cast<void **>(&s_loadOne_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsLoading(const char *name) {
|
||||
if (name == nullptr || *name == '\0')
|
||||
return false;
|
||||
const int depth = (g_depth < kMaxDepth) ? g_depth : kMaxDepth;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
if (EqualsIgnoreCase(g_stack[i], name))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace AddOns::LoadState
|
||||
@@ -0,0 +1,33 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// `AddOns::LoadState` — tracks which addons are mid-load.
|
||||
//
|
||||
// The engine's own "loaded" byte (`entry+0x18`) is set at the START of a
|
||||
// load, before the addon's files run, so on its own it can't tell
|
||||
// "loading" from "loaded". That distinction is observable: an addon's
|
||||
// file-scope code, and every dependency pulled in during it, execute
|
||||
// while the byte already reads set.
|
||||
//
|
||||
// A co-hook on the per-addon loader keeps a stack of the names currently
|
||||
// being loaded, which is the missing half of
|
||||
// `C_AddOns.IsAddOnLoaded`'s `(loadedOrLoading, loaded)` pair.
|
||||
namespace AddOns::LoadState {
|
||||
|
||||
// True while `name` is inside the engine's per-addon load call. Matching
|
||||
// is case-insensitive, as addon-name matching is everywhere else.
|
||||
bool IsLoading(const char *name);
|
||||
|
||||
} // namespace AddOns::LoadState
|
||||
@@ -12,17 +12,17 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_AddOns.GetAddOnLocalTable(name)` — the public, gated view of an addon's
|
||||
// private namespace table (the `local _, addonTable = ...` second vararg the
|
||||
// transpiler injects; see luasyntax/AddonNamespace.h).
|
||||
// private namespace table (the `local _, addonTable = ...` second vararg each
|
||||
// addon file is called with; see luasyntax/AddonNamespace.h).
|
||||
//
|
||||
// Retail added this in 11.0 alongside the `## AllowAddOnTableAccess` TOC
|
||||
// directive: one addon can read another's local table ONLY when the target
|
||||
// opts in. We mirror that gate:
|
||||
// 1. the named addon is currently LOADED, and
|
||||
// 2. its `.toc` declares `## AllowAddOnTableAccess: 1`.
|
||||
// Either check failing returns nil. The internal preamble path (an addon
|
||||
// receiving its OWN table) is a different, ungated function (`__addonns`) —
|
||||
// an addon never needs permission for its own namespace.
|
||||
// Either check failing returns nil. An addon receiving its OWN table is a
|
||||
// different, ungated path (its files are called with it at load) — an addon
|
||||
// never needs permission for its own namespace.
|
||||
//
|
||||
// Directive spelling verified against the 1.15.8 Classic Era binary (both the
|
||||
// `AllowAddOnTableAccess:` TOC key and the `GetAddOnLocalTable` name are
|
||||
@@ -77,7 +77,7 @@ bool TocAllowsTableAccess(const char *name) {
|
||||
size_t n = 0;
|
||||
bool allowed = false;
|
||||
if (AddOns::Toc::FindValue(static_cast<const char *>(buf), size,
|
||||
"## AllowAddOnTableAccess:", &v, &n)) {
|
||||
"AllowAddOnTableAccess", &v, &n)) {
|
||||
int value = 0;
|
||||
bool anyDigit = false;
|
||||
for (size_t k = 0; k < n && v[k] >= '0' && v[k] <= '9'; ++k) {
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `UpdateAddOnMemoryUsage()` / `GetAddOnMemoryUsage(index or "name")` —
|
||||
// 3.3.5's `Script_UpdateAddOnMemoryUsage` (0x00511510) and
|
||||
// `Script_GetAddOnMemoryUsage` (0x00511530) over `Gc::Tag`.
|
||||
//
|
||||
// An addon's tag is its registry name pointer `[entry+0x14]`: the exact
|
||||
// pointer `FUN_ADDON_LOAD_ONE` installs as the context word while the
|
||||
// addon's files run (and the one 3.3.5's `GetAddOnName` returns), so the
|
||||
// lookup is a pointer match like 3.3.5's `luaC_getsize`. Secure addons run
|
||||
// under context 0, are never tagged, and read 0 — as in 3.3.5.
|
||||
//
|
||||
// 3.3.5's `LoadAddOn` also pins `lua_gctag` to the addon for its whole
|
||||
// file / Bindings / SavedVariables window, so objects made there stay the
|
||||
// addon's even when its code switches the context word (calling into another
|
||||
// addon's function or table makes that addon's word current). The 1.12 loader
|
||||
// has the same window; two 5-byte call patches open and close a `Gc::Tag` pin
|
||||
// at its edges (SITE_ADDON_LOAD_WINDOW_OPEN / _CLOSE).
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Registry.h"
|
||||
#include "gc/Tag.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace AddOns::MemoryUsage {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uintptr_t addrExecContextDepth = Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH;
|
||||
|
||||
// The loader's own context value: the name for an insecure addon, 0 for a
|
||||
// secure one (3.3.5's `addon->m_status ? addonName : nullptr`). A 0 pin means
|
||||
// "no override", so a secure addon's window keeps following the context word.
|
||||
void __fastcall OpenLoadWindow(const uint8_t *entry) {
|
||||
const bool insecure = Game::Read<uint32_t>(entry, Offsets::OFF_ADDON_ENTRY_STATUS) != 0;
|
||||
Gc::Tag::PushPin(insecure ? Game::Read<const char *>(entry, Offsets::OFF_ADDON_ENTRY_NAME_PTR)
|
||||
: nullptr);
|
||||
}
|
||||
|
||||
void CloseLoadWindow() { Gc::Tag::PopPin(); }
|
||||
|
||||
// Stands in for `xor eax,eax; lea ecx,[ebp-0x18]` at the window's open.
|
||||
// EBX = the entry; EBP is the loader's frame.
|
||||
__declspec(naked) void LoadWindowOpenThunk() {
|
||||
__asm {
|
||||
pushad
|
||||
mov ecx, ebx
|
||||
call OpenLoadWindow
|
||||
popad
|
||||
xor eax, eax
|
||||
lea ecx, [ebp - 18h]
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Stands in for `mov eax,[VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH]` at the close.
|
||||
__declspec(naked) void LoadWindowCloseThunk() {
|
||||
__asm {
|
||||
pushad
|
||||
call CloseLoadWindow
|
||||
popad
|
||||
mov eax, addrExecContextDepth
|
||||
mov eax, [eax]
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
void PatchLoadWindow() {
|
||||
static const uint8_t kOpen[5] = {0x33, 0xC0, 0x8D, 0x4D, 0xE8};
|
||||
static const uint8_t kClose[5] = {0xA1, 0xC4, 0xEA, 0xCE, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_ADDON_LOAD_WINDOW_OPEN, kOpen, sizeof kOpen,
|
||||
reinterpret_cast<const void *>(&LoadWindowOpenThunk));
|
||||
Common::ReplaceWithCall(Offsets::SITE_ADDON_LOAD_WINDOW_CLOSE, kClose, sizeof kClose,
|
||||
reinterpret_cast<const void *>(&LoadWindowCloseThunk));
|
||||
}
|
||||
|
||||
int __fastcall Script_UpdateAddOnMemoryUsage(void * /*L*/) {
|
||||
Gc::Tag::UpdateUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetAddOnMemoryUsage(void *L) {
|
||||
const uint8_t *record =
|
||||
Addons::ResolveRecord(L, "Usage: GetAddOnMemoryUsage(index or \"name\")");
|
||||
const char *tag =
|
||||
record != nullptr
|
||||
? Game::Read<const char *>(record, Offsets::OFF_ADDON_ENTRY_NAME_PTR)
|
||||
: nullptr;
|
||||
const uint64_t bytes = tag != nullptr ? Gc::Tag::Usage(tag) : 0;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(bytes) * (1.0 / 1024.0));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `_classicapi_LuaHeapAudit()` → objectBytes, nblocks, taggedObjects.
|
||||
// Walks every collectable object the engine can reach — the main list,
|
||||
// the userdata list and the string table, the three lists `luaC_sweep`
|
||||
// sweeps — summing `Gc::Tag::SizeOf`. `objectBytes` should sit just under
|
||||
// `nblocks` (the engine's own byte count): the gap is what is not a GC
|
||||
// object (string-table buckets, the main thread and its stack, buffers).
|
||||
// A gap that grows with the heap means a size formula is wrong.
|
||||
uint64_t SumList(const uint8_t *object) {
|
||||
uint64_t total = 0;
|
||||
while (object != nullptr) {
|
||||
total += Gc::Tag::SizeOf(object);
|
||||
object = Game::Read<const uint8_t *>(object, Offsets::OFF_LUA_GCHEADER_NEXT);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int __fastcall Script_LuaHeapAudit(void *L) {
|
||||
const uint8_t *g = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_G);
|
||||
uint64_t bytes = SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTGC)) +
|
||||
SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTUDATA));
|
||||
const auto buckets =
|
||||
Game::Read<const uint8_t *const *>(g, Offsets::OFF_LUA_G_STRT_HASH);
|
||||
const int size = Game::Read<int>(g, Offsets::OFF_LUA_G_STRT_SIZE);
|
||||
for (int i = 0; i < size; ++i)
|
||||
bytes += SumList(buckets[i]);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(bytes));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Game::Read<uint32_t>(g, Offsets::OFF_LUA_G_NBLOCKS)));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Gc::Tag::TaggedCount()));
|
||||
return 3;
|
||||
}
|
||||
|
||||
// `_classicapi_GcTagOf(value)` → the owner tag of a table / function /
|
||||
// userdata / thread: the addon's name when the tag is an addon's, else the
|
||||
// raw context word as "0x…" (e.g. RunScript's), nil when untagged.
|
||||
int __fastcall Script_GcTagOf(void *L) {
|
||||
const char *tag = Gc::Tag::Of(Game::Lua::ToPointer(L, 1));
|
||||
if (tag == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const char *name = nullptr;
|
||||
Addons::ForEachEntry([&](uintptr_t entry) {
|
||||
if (Game::Read<const char *>(entry + Offsets::OFF_ADDON_ENTRY_NAME_PTR) != tag)
|
||||
return true;
|
||||
name = tag;
|
||||
return false;
|
||||
});
|
||||
if (name != nullptr) {
|
||||
Game::Lua::PushString(L, name);
|
||||
} else {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof buf, "0x%08X", static_cast<unsigned>(reinterpret_cast<uintptr_t>(tag)));
|
||||
Game::Lua::PushString(L, buf);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Function kUpdateAddOnMemoryUsage{
|
||||
"Recounts the memory each addon uses; GetAddOnMemoryUsage returns the result of the "
|
||||
"last recount.",
|
||||
{}, {}, "AddOnsGlobals"};
|
||||
|
||||
const Game::Doc::Field kGetArgs[] = {
|
||||
Game::Doc::Req("name", "uiAddon", "An addon index or folder name."),
|
||||
};
|
||||
const Game::Doc::Field kGetRets[] = {
|
||||
Game::Doc::Req("memory", "number",
|
||||
"Kilobytes of Lua memory the addon's code allocated, as of the last "
|
||||
"UpdateAddOnMemoryUsage."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnMemoryUsage{
|
||||
"Returns the Lua memory an addon uses, in kilobytes, as of the last "
|
||||
"UpdateAddOnMemoryUsage.",
|
||||
kGetArgs, kGetRets, "AddOnsGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
// Each pass runs before the load pass; the patch is idempotent.
|
||||
PatchLoadWindow();
|
||||
Game::Lua::RegisterGlobalFunction("UpdateAddOnMemoryUsage", &Script_UpdateAddOnMemoryUsage,
|
||||
&kUpdateAddOnMemoryUsage);
|
||||
Game::Lua::RegisterGlobalFunction("GetAddOnMemoryUsage", &Script_GetAddOnMemoryUsage,
|
||||
&kGetAddOnMemoryUsage);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_LuaHeapAudit", &Script_LuaHeapAudit);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_GcTagOf", &Script_GcTagOf);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace AddOns::MemoryUsage
|
||||
@@ -0,0 +1,63 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "addons/Registry.h"
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace Addons {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetByIndex_t = void *(__fastcall *)(int idx0Based);
|
||||
using GetAddOnCount_t = int(__fastcall *)();
|
||||
using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *name);
|
||||
|
||||
} // namespace
|
||||
|
||||
// Either input is routed through the by-name hash lookup
|
||||
// `FUN_ADDON_RESOLVE_REQ_DEPS` to reach the record.
|
||||
// `FUN_ADDON_GET_BY_INDEX` returns a *name pointer*, NOT the record —
|
||||
// reading record offsets off it lands in string memory and crashes (a
|
||||
// numeric-path caller like pfUI's addon list hit exactly that). The
|
||||
// resolver returns the RequiredDeps descriptor at `record+0x48`;
|
||||
// subtract to recover the base.
|
||||
const uint8_t *ResolveRecord(void *L, const char *usage) {
|
||||
const char *name;
|
||||
if (Game::Lua::IsNumber(L, 1)) {
|
||||
const int idx = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
auto byIndex = reinterpret_cast<GetByIndex_t>(Offsets::FUN_ADDON_GET_BY_INDEX);
|
||||
name = static_cast<const char *>(byIndex(idx));
|
||||
if (name == nullptr) {
|
||||
auto count = reinterpret_cast<GetAddOnCount_t>(Offsets::FUN_ADDON_GET_COUNT);
|
||||
Game::Lua::Error(L, reinterpret_cast<const char *>(static_cast<uintptr_t>(
|
||||
Offsets::VAR_ADDON_INDEX_RANGE_ERROR_FMT)),
|
||||
count());
|
||||
return nullptr;
|
||||
}
|
||||
} else if (Game::Lua::IsString(L, 1)) {
|
||||
name = Game::Lua::ToString(L, 1);
|
||||
} else {
|
||||
Game::Lua::Error(L, usage);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveReqDeps_t>(
|
||||
Offsets::FUN_ADDON_RESOLVE_REQ_DEPS);
|
||||
uint8_t *reqDesc = resolve(name);
|
||||
if (reqDesc == nullptr)
|
||||
return nullptr;
|
||||
return reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC;
|
||||
}
|
||||
|
||||
} // namespace Addons
|
||||
+13
-4
@@ -27,10 +27,10 @@ namespace Addons {
|
||||
// engine's own load pass (`FUN_0051F600`) and scan tail loop use.
|
||||
//
|
||||
// `fn` may return `void` (always continue) or `bool` (return `false` to STOP
|
||||
// early). Early stop is load-bearing for a visitor that mutates the list —
|
||||
// e.g. re-linking an entry to the head invalidates the current next-pointer,
|
||||
// so the walk must not follow it. Shared by `Addons::Embedded` (find + re-link
|
||||
// the embedded entry) and `Addons::Rescan` (snapshot / diff / fix-up passes).
|
||||
// early, e.g. once a by-name search finds its entry). A visitor must not
|
||||
// unlink the entry it is given and then continue: the walk reads that
|
||||
// entry's next-pointer after the call. Shared by `Addons::Embedded` (find the
|
||||
// embedded entry) and `Addons::Rescan` (snapshot / diff / fix-up passes).
|
||||
template <typename Fn> void ForEachEntry(Fn fn) {
|
||||
const int linkOffset = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL));
|
||||
@@ -47,4 +47,13 @@ template <typename Fn> void ForEachEntry(Fn fn) {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolves Lua arg 1 (index or name) to the addon's registry record, with
|
||||
// the arg handling of stock `Script_GetAddOnDependencies` (`0x0048E5E0`) —
|
||||
// the same shape 3.3.5 gives every `(index or "name")` addon global:
|
||||
// `lua_isnumber` first (so a numeric string like `"3"` is an index), an
|
||||
// out-of-range index raises the engine's own range error with the addon
|
||||
// count, anything else non-string raises `usage`, and an unknown *name*
|
||||
// returns nullptr with no error.
|
||||
const uint8_t *ResolveRecord(void *L, const char *usage);
|
||||
|
||||
} // namespace Addons
|
||||
|
||||
+32
-76
@@ -16,16 +16,15 @@
|
||||
// a stock 1.12 client, each fixed here with the engine's own machinery:
|
||||
//
|
||||
// 1. FILE VISIBILITY. Every relative-path read resolves through a
|
||||
// loose-file hash index built ONCE at boot (see Offsets.h,
|
||||
// `FUN_VFS_INDEX_SUBTREE`); files created after boot — a new
|
||||
// addon's TOC, a new .lua in an existing addon, a freshly written
|
||||
// SavedVariables file — are invisible until restart. The boot
|
||||
// indexer is dedup-safe and works on subtrees, so we re-run it per
|
||||
// /reload on the only two subtrees whose contents can change what
|
||||
// a /reload loads: `Interface\AddOns` and `WTF\Account`. New files
|
||||
// in EXISTING addons need nothing more — the per-addon loader
|
||||
// `FUN_0051F240` re-reads the TOC from disk on every load pass, so
|
||||
// once the files are visible they load.
|
||||
// loose-file hash index (see Offsets.h, `FUN_VFS_INDEX_CLEAR`);
|
||||
// a file created after the index was built — a new addon's TOC, a
|
||||
// new .lua in an existing addon — is invisible until it is rebuilt.
|
||||
// We call the engine's own clear, the same one the engine calls
|
||||
// after writing a new SavedVariables file: the next lookup rebuilds
|
||||
// the index from disk as boot does, so new files appear and deleted
|
||||
// ones drop out. New files in EXISTING addons need nothing more —
|
||||
// the per-addon loader `FUN_0051F240` re-reads the TOC from disk on
|
||||
// every load pass, so once the files are visible they load.
|
||||
//
|
||||
// 2. REGISTRY MEMBERSHIP. The addon registry is built once at login;
|
||||
// new folders are never walked. We replay the login scan's disk
|
||||
@@ -62,14 +61,14 @@
|
||||
// bytes cleared), and the load pass hasn't — nothing holds entry state.
|
||||
//
|
||||
// Excluded from the evict path (everything else is fair game):
|
||||
// - `!!!ClassicAPI` — embedded; `Addons::Embedded` owns its entry
|
||||
// (head re-link + filter byte);
|
||||
// - the embedded addons — `Addons::Embedded` owns their entries
|
||||
// (registered first + filter byte);
|
||||
// - `## Secure:` / SMSG-managed entries — the parser cannot restore
|
||||
// packet-delivered state (see OFF_ADDON_ENTRY_SECURE).
|
||||
//
|
||||
// NOTE the surgical discipline. A previous attempt re-invoked the
|
||||
// login-only teardown+rescan (`FUN_ADDON_INIT`) mid-/reload and
|
||||
// corrupted the registry (duplicate loads + Lua memory explosion on the
|
||||
// login-only teardown+rescan (`LoadAddOnsForAccount`, `0x0051C740`)
|
||||
// mid-/reload and corrupted the registry (duplicate loads + Lua memory explosion on the
|
||||
// NEXT login). This module never runs bulk teardown; the only entry
|
||||
// mutation beyond what a login scan produces is the per-entry evict
|
||||
// above, which is (a) the engine's own complete destructor, (b) always
|
||||
@@ -78,13 +77,13 @@
|
||||
// registration gets. New entries come from the same parser call
|
||||
// `Addons::Embedded` has exercised every login.
|
||||
//
|
||||
// Thread note: the loose-file index has no lock, but the engine itself
|
||||
// builds it lazily on first access and we mutate it only during the
|
||||
// reload loading screen on the main thread — the same context in which
|
||||
// the engine does all its own file work.
|
||||
// Thread note: the loose-file index has no lock in 1.12. We clear it on
|
||||
// the main thread during the reload loading screen — the same context
|
||||
// in which the engine's own unload pass clears it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Embedded.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Registry.h"
|
||||
|
||||
@@ -102,11 +101,7 @@ namespace {
|
||||
|
||||
// ── Engine entry points (all verified by disassembly; see Offsets.h) ──
|
||||
|
||||
using FindOpen_t = void *(__stdcall *)(const char *dirPath);
|
||||
using FindClose_t = void(__stdcall *)(void *findBlock);
|
||||
using IndexSubtree_t = void(__fastcall *)(const char *basePath,
|
||||
const char *relSubdir,
|
||||
void *findHandle);
|
||||
using IndexClear_t = void(__cdecl *)();
|
||||
using ScanDiskDirs_t = int(__fastcall *)(const char *basePath,
|
||||
const char *pattern, void *callback,
|
||||
void *userParam, int includeHidden);
|
||||
@@ -163,53 +158,13 @@ uintptr_t ResolveEntryByName(const char *name) {
|
||||
|
||||
// ── Step 1: loose-file index refresh ──────────────────────────────────
|
||||
|
||||
// Register new on-disk files under `relSubdir` with the frozen index,
|
||||
// exactly as the boot walk would have (the indexer skips every file
|
||||
// already present). No-op when the directory doesn't exist —
|
||||
// `FUN_VFS_FIND_OPEN` requires an existing directory and returns NULL.
|
||||
void ReindexSubtree(const char *base, const char *relSubdir) {
|
||||
char dir[260];
|
||||
std::snprintf(dir, sizeof(dir), "%s\\%s", base, relSubdir);
|
||||
auto findOpen = reinterpret_cast<FindOpen_t>(Offsets::FUN_VFS_FIND_OPEN);
|
||||
auto findClose = reinterpret_cast<FindClose_t>(Offsets::FUN_VFS_FIND_CLOSE);
|
||||
void *handle = findOpen(dir);
|
||||
if (handle == nullptr)
|
||||
return;
|
||||
auto index =
|
||||
reinterpret_cast<IndexSubtree_t>(Offsets::FUN_VFS_INDEX_SUBTREE);
|
||||
index(base, relSubdir, handle);
|
||||
findClose(handle);
|
||||
}
|
||||
|
||||
// Drop the loose-file index; the next file lookup rebuilds it from disk.
|
||||
void RefreshLooseFileIndex() {
|
||||
// The boot walk's base choice (`FUN_00646EA0`): the recorded game
|
||||
// dir if it opens as a directory, else ".". Index keys are relative
|
||||
// to this, so it must match or the new keys would never be hit.
|
||||
auto findOpen = reinterpret_cast<FindOpen_t>(Offsets::FUN_VFS_FIND_OPEN);
|
||||
auto findClose = reinterpret_cast<FindClose_t>(Offsets::FUN_VFS_FIND_CLOSE);
|
||||
const char *base =
|
||||
reinterpret_cast<const char *>(Offsets::VAR_VFS_BASE_PATH);
|
||||
void *probe = findOpen(base);
|
||||
if (probe != nullptr)
|
||||
findClose(probe);
|
||||
else
|
||||
base = ".";
|
||||
|
||||
// The only subtrees whose post-boot changes affect what a /reload
|
||||
// loads: addon content, and the SavedVariables the unload pass
|
||||
// wrote moments ago (a first-ever SV file is otherwise invisible —
|
||||
// the stock client loses a freshly installed addon's settings on
|
||||
// /reload for exactly this reason).
|
||||
ReindexSubtree(base, "Interface\\AddOns");
|
||||
ReindexSubtree(base, "WTF\\Account");
|
||||
reinterpret_cast<IndexClear_t>(Offsets::FUN_VFS_INDEX_CLEAR)();
|
||||
}
|
||||
|
||||
// ── Step 1.5: `##` metadata refresh (evict + re-register on change) ──
|
||||
|
||||
// The embedded addon's name — its entry is owned by `Addons::Embedded`
|
||||
// (head re-link + filter byte) and never refreshed here.
|
||||
constexpr const char *kEmbeddedAddon = "!!!ClassicAPI";
|
||||
|
||||
// name → FNV-1a hash of the addon's `##` lines, as last parsed.
|
||||
// Per-process on purpose: the registry persists across /reload, and a
|
||||
// stale hash after a full re-login scan only causes one harmless
|
||||
@@ -263,11 +218,12 @@ uint32_t HashTocMetadata(const char *buf, size_t size) {
|
||||
return h;
|
||||
}
|
||||
|
||||
// True when the entry never takes the evict path: the embedded addon
|
||||
// (owned by `Addons::Embedded`) and SMSG-managed secure entries (the
|
||||
// parser cannot restore packet-delivered state).
|
||||
// True when the entry never takes the evict path: the embedded addons
|
||||
// (owned by `Addons::Embedded` — registered first + filter byte) and
|
||||
// SMSG-managed secure entries (the parser cannot restore packet-delivered
|
||||
// state).
|
||||
bool IsRefreshExempt(uintptr_t entry, const char *name) {
|
||||
return name == nullptr || std::strcmp(name, kEmbeddedAddon) == 0 ||
|
||||
return name == nullptr || Addons::Embedded::IsEmbedded(name) ||
|
||||
*reinterpret_cast<const uint8_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_SECURE) != 0;
|
||||
}
|
||||
@@ -415,7 +371,7 @@ void FixupReverseLoadWith(const std::vector<uintptr_t> &added) {
|
||||
// `FUN_0051DA70` phase 2, mirrored (never call that function itself —
|
||||
// its phase 1 consumes an SMSG_ADDON_INFO packet): rebuild the
|
||||
// `GetNumAddOns`/`GetAddOnInfo(i)` name-pointer array from the linked
|
||||
// list, skipping filtered entries (keeps `!!!ClassicAPI` hidden), then
|
||||
// list, skipping filtered entries (keeps the embedded addons hidden), then
|
||||
// sort with the engine's own comparator to restore alphabetical order.
|
||||
void RebuildDisplayArray() {
|
||||
auto desc = reinterpret_cast<Desc *>(
|
||||
@@ -437,13 +393,13 @@ void RebuildDisplayArray() {
|
||||
}
|
||||
|
||||
void Run() {
|
||||
// Registry populated (login scan ran) and loose index built — both
|
||||
// always true by the first in-world LoadScriptFunctions, but these
|
||||
// are the states the steps below mutate, so gate explicitly.
|
||||
// Registry populated (login scan ran) — always true by the first
|
||||
// in-world LoadScriptFunctions, but it is the state the steps below
|
||||
// mutate, so gate explicitly. No gate on the index being built: the
|
||||
// unload pass clears it whenever it wrote a new SV file, and the
|
||||
// clear below is safe either way.
|
||||
if (*reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_INITIALIZED)) == 0 ||
|
||||
*reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_VFS_INDEX_READY)) == 0)
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_INITIALIZED)) == 0)
|
||||
return;
|
||||
|
||||
RefreshLooseFileIndex();
|
||||
|
||||
@@ -35,18 +35,19 @@
|
||||
// The engine STILL runs its own SavedVariables load at the normal step, right
|
||||
// after the files. Left alone, that re-load would overwrite any value the
|
||||
// addon wrote to its SavedVariable at file scope before ADDON_LOADED. So we
|
||||
// also co-hook FUN_LUA_LOAD_FILE and suppress that one redundant re-load: each
|
||||
// path we pre-load is remembered, and the engine's imminent load of that exact
|
||||
// path is skipped once. This makes the behavior match true
|
||||
// LoadSavedVariablesFirst — file-scope reads AND writes both survive. Our own
|
||||
// early load calls the trampoline directly, so it is never self-suppressed.
|
||||
// FUN_LUA_LOAD_FILE loads FILES only (not RunScript string execution), so the
|
||||
// extra hook is a cold, addon-load-time path.
|
||||
// also suppress that one redundant re-load, through Script::LoadFile's shared
|
||||
// FUN_LUA_LOAD_FILE hook: each path we pre-load is remembered, and the
|
||||
// engine's imminent load of that exact path is skipped once. This makes the
|
||||
// behavior match true LoadSavedVariablesFirst — file-scope reads AND writes
|
||||
// both survive. Our own early load calls Script::LoadFile::Original, so it is
|
||||
// never self-suppressed. FUN_LUA_LOAD_FILE loads FILES only (not RunScript
|
||||
// string execution), so this is a cold, addon-load-time path.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "script/LoadFile.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -67,14 +68,12 @@ using AddOns::EngineIO::FileExistsFn;
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
using LoadTocFilesFn = uint32_t(__fastcall *)(char *tocPath, int *a2, int *a3);
|
||||
using LuaLoadFileFn = uint32_t(__fastcall *)(const char *path, void *a2, void *a3);
|
||||
using NameFn = const char *(*)(); // no-arg readers (return in EAX)
|
||||
|
||||
LoadTocFilesFn g_orig = nullptr;
|
||||
LuaLoadFileFn g_origLuaLoad = nullptr;
|
||||
|
||||
// Full SV paths we pre-loaded whose imminent engine re-load must be skipped
|
||||
// once. Populated in LoadSavedVarsEarly, drained by LuaLoad_h. Only ever holds
|
||||
// once. Populated in LoadSavedVarsEarly, drained by SuppressReload. Only ever holds
|
||||
// a handful of entries (one addon's SV files, briefly). Main-thread only.
|
||||
std::vector<std::string> g_pendingSuppress;
|
||||
|
||||
@@ -148,18 +147,18 @@ SvFlags ReadSvFlags(const char *tocPath) {
|
||||
|
||||
const char *v = nullptr;
|
||||
size_t n = 0;
|
||||
if (AddOns::Toc::FindValue(b, size, "## LoadSavedVariablesFirst:", &v, &n))
|
||||
if (AddOns::Toc::FindValue(b, size, "LoadSavedVariablesFirst", &v, &n))
|
||||
f.first = NonzeroFlag(v, n);
|
||||
// Gate each SV file on the DECLARATION, not mere file existence — mirroring
|
||||
// the engine, which loads the account file iff `## SavedVariables` is
|
||||
// declared and the per-char file iff `## SavedVariablesPerCharacter` is.
|
||||
// (The trailing `:` keeps "## SavedVariables:" from matching the
|
||||
// "## SavedVariablesPerCharacter:" line.) Otherwise a stale SV file from a
|
||||
// (FindValue requires `:` right after the key, so "SavedVariables" never
|
||||
// matches the "## SavedVariablesPerCharacter:" line.) Otherwise a stale SV file from a
|
||||
// scope the addon no longer declares would be executed early — Lua the
|
||||
// engine never runs — and queued for a suppression the engine never drains.
|
||||
f.account = AddOns::Toc::FindValue(b, size, "## SavedVariables:", &v, &n) && n > 0;
|
||||
f.account = AddOns::Toc::FindValue(b, size, "SavedVariables", &v, &n) && n > 0;
|
||||
f.perChar =
|
||||
AddOns::Toc::FindValue(b, size, "## SavedVariablesPerCharacter:", &v, &n) && n > 0;
|
||||
AddOns::Toc::FindValue(b, size, "SavedVariablesPerCharacter", &v, &n) && n > 0;
|
||||
|
||||
reinterpret_cast<SMemFreeFn>(static_cast<uintptr_t>(Offsets::FUN_STORM_SMEM_FREE))(
|
||||
buf, __FILE__, __LINE__, 0);
|
||||
@@ -173,12 +172,10 @@ bool FileExists(const char *path) {
|
||||
|
||||
// Load a SavedVariables file we found, then mark its path so the engine's own
|
||||
// re-load of it (right after the addon's files) is suppressed — preserving any
|
||||
// file-scope write. Calls the trampoline directly so this load is never itself
|
||||
// suppressed by our LuaLoad_h hook.
|
||||
// file-scope write. Calls the engine loader directly so this load is never
|
||||
// itself suppressed by SuppressReload.
|
||||
void RunLuaFile(const char *path) {
|
||||
if (g_origLuaLoad == nullptr)
|
||||
return;
|
||||
g_origLuaLoad(path, nullptr, nullptr);
|
||||
Script::LoadFile::Original(path, nullptr, nullptr);
|
||||
g_pendingSuppress.emplace_back(path);
|
||||
}
|
||||
|
||||
@@ -241,18 +238,16 @@ uint32_t __fastcall LoadTocFiles_h(char *tocPath, int *a2, int *a3) {
|
||||
// Skip the engine's one redundant SavedVariables re-load per pre-loaded path.
|
||||
// Every other file load passes straight through. Match is by full path
|
||||
// (case-insensitive); a path is suppressed exactly once, so nothing else is
|
||||
// affected. Our own early load bypasses this via the trampoline.
|
||||
uint32_t __fastcall LuaLoad_h(const char *path, void *a2, void *a3) {
|
||||
if (path != nullptr) {
|
||||
for (size_t i = 0; i < g_pendingSuppress.size(); ++i) {
|
||||
if (SamePathCI(g_pendingSuppress[i].c_str(), path)) {
|
||||
g_pendingSuppress.erase(g_pendingSuppress.begin() +
|
||||
static_cast<std::ptrdiff_t>(i));
|
||||
return 0; // suppress — the addon's file-scope SV writes survive
|
||||
}
|
||||
// affected. Our own early load bypasses this through Original.
|
||||
bool SuppressReload(const char *path) {
|
||||
for (size_t i = 0; i < g_pendingSuppress.size(); ++i) {
|
||||
if (SamePathCI(g_pendingSuppress[i].c_str(), path)) {
|
||||
g_pendingSuppress.erase(g_pendingSuppress.begin() +
|
||||
static_cast<std::ptrdiff_t>(i));
|
||||
return true; // suppress — the addon's file-scope SV writes survive
|
||||
}
|
||||
}
|
||||
return g_origLuaLoad(path, a2, a3);
|
||||
return false;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{
|
||||
@@ -260,10 +255,7 @@ const Game::HookAutoRegister _hook{
|
||||
reinterpret_cast<void *>(&LoadTocFiles_h),
|
||||
reinterpret_cast<void **>(&g_orig)};
|
||||
|
||||
const Game::HookAutoRegister _hookLua{
|
||||
Offsets::FUN_LUA_LOAD_FILE,
|
||||
reinterpret_cast<void *>(&LuaLoad_h),
|
||||
reinterpret_cast<void **>(&g_origLuaLoad)};
|
||||
const Script::LoadFile::AutoSuppress _suppress{&SuppressReload};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
+50
-27
@@ -33,35 +33,58 @@ inline bool EqCI(const char *s, size_t n, const char *lit) {
|
||||
return lit[n] == '\0';
|
||||
}
|
||||
|
||||
// Scans an in-memory TOC buffer for `directive` (the full line prefix, e.g.
|
||||
// "## Version:") at a line start, case-insensitively, and returns the value
|
||||
// that follows via out-params: [*valStart, *valStart + *valLen), trimmed of
|
||||
// leading and trailing spaces/tabs and excluding the CR/LF/EOF terminator.
|
||||
// Returns false (out-params untouched) when the directive is absent. Pure —
|
||||
// reads no files; the caller owns the buffer (via FUN_FILE_READ, or an embedded
|
||||
// copy). Shared by addons/LocalTable.cpp (AllowAddOnTableAccess) and
|
||||
// addons/Embedded.cpp (Version) so the line-scan lives in one place.
|
||||
inline bool FindValue(const char *buf, size_t size, const char *directive,
|
||||
// Scans an in-memory TOC buffer for the `## <key>:` directive and returns the
|
||||
// first one's value via out-params: [*valStart, *valStart + *valLen), trimmed
|
||||
// of leading and trailing spaces/tabs and excluding the CR/LF/EOF terminator.
|
||||
// `key` is the bare name ("Version", "SavedVariables"). Returns false
|
||||
// (out-params untouched) when the directive is absent.
|
||||
//
|
||||
// Matches lines the way the engine's TOC parser `FUN_TOC_PARSER` (0x0051C9B0)
|
||||
// does, so a directive the engine reads is never one we miss: a UTF-8 BOM at
|
||||
// the start of the file is skipped; lines break at CR or LF; a line counts
|
||||
// when it starts with "##", then any number of spaces, then `key`
|
||||
// case-insensitively, IMMEDIATELY followed by ':' (the engine compares
|
||||
// against literals like "SavedVariables:", which is also why
|
||||
// "SavedVariables" never matches a "SavedVariablesPerCharacter:" line).
|
||||
//
|
||||
// Pure — reads no files; the caller owns the buffer (via FUN_FILE_READ, or an
|
||||
// embedded copy). Shared by every addon module that reads a TOC directive so
|
||||
// the line-scan lives in one place.
|
||||
inline bool FindValue(const char *buf, size_t size, const char *key,
|
||||
const char **valStart, size_t *valLen) {
|
||||
const size_t dlen = std::strlen(directive);
|
||||
if (dlen == 0)
|
||||
const size_t klen = std::strlen(key);
|
||||
if (klen == 0)
|
||||
return false;
|
||||
for (size_t i = 0; i + dlen <= size; ++i) {
|
||||
const bool atLineStart = (i == 0) || buf[i - 1] == '\n';
|
||||
if (!atLineStart || _strnicmp(buf + i, directive, dlen) != 0)
|
||||
continue;
|
||||
const char *p = buf + i + dlen;
|
||||
const char *end = buf + size;
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
const char *v = p;
|
||||
while (p < end && *p != '\r' && *p != '\n')
|
||||
++p;
|
||||
while (p > v && (p[-1] == ' ' || p[-1] == '\t'))
|
||||
--p;
|
||||
*valStart = v;
|
||||
*valLen = static_cast<size_t>(p - v);
|
||||
return true;
|
||||
const char *end = buf + size;
|
||||
const char *line = buf;
|
||||
if (size >= 3 && static_cast<unsigned char>(buf[0]) == 0xEF &&
|
||||
static_cast<unsigned char>(buf[1]) == 0xBB &&
|
||||
static_cast<unsigned char>(buf[2]) == 0xBF)
|
||||
line += 3;
|
||||
while (line < end) {
|
||||
const char *eol = line;
|
||||
while (eol < end && *eol != '\r' && *eol != '\n')
|
||||
++eol;
|
||||
const char *p = line;
|
||||
if (eol - p >= 2 && p[0] == '#' && p[1] == '#') {
|
||||
p += 2;
|
||||
while (p < eol && *p == ' ')
|
||||
++p;
|
||||
if (static_cast<size_t>(eol - p) > klen && EqCI(p, klen, key) &&
|
||||
p[klen] == ':') {
|
||||
p += klen + 1;
|
||||
while (p < eol && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
const char *v = p;
|
||||
p = eol;
|
||||
while (p > v && (p[-1] == ' ' || p[-1] == '\t'))
|
||||
--p;
|
||||
*valStart = v;
|
||||
*valLen = static_cast<size_t>(p - v);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
line = eol + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+18
-13
@@ -78,13 +78,18 @@ uint32_t ClientInterfaceVersion() {
|
||||
static_cast<uintptr_t>(Offsets::FUN_ADDON_CLIENT_INTERFACE_VERSION))();
|
||||
}
|
||||
|
||||
// If `line` is a `## Interface:` directive whose value is a comma-list that
|
||||
// contains the client version but does NOT lead with it, append a normalized
|
||||
// `<prefix>: <clientVersion>` line to `out` and return true. Vanilla's parser
|
||||
// reads only the first number, so without this a later 11200 is never seen and
|
||||
// the addon is marked out of date. Returns false for any other line, and for
|
||||
// an interface line that needs no change (single value, no client version, or
|
||||
// client version already first) — the caller then copies the line verbatim.
|
||||
// If `line` is a `## Interface:` directive whose value is a comma-list, pick
|
||||
// the retail value — the closest entry at or below the client version — and,
|
||||
// when that is not already the first entry, append a normalized
|
||||
// `<prefix>: <value>` line to `out` and return true. Vanilla's parser reads
|
||||
// only the first number, so without this a later 11200 is never seen and the
|
||||
// addon is marked out of date. Loadability stays an exact match on the client
|
||||
// version, and the pick equals it iff it is in the list, so this only changes
|
||||
// the stored value (what GetAddOnInterfaceVersion reads) for lists that were
|
||||
// out of date anyway. Returns false for any other line, and for an interface
|
||||
// line that needs no change (single value, nothing at or below the client
|
||||
// version, or the pick already first) — the caller then copies the line
|
||||
// verbatim.
|
||||
bool TryRewriteInterfaceLine(const char *line, size_t len, std::string &out) {
|
||||
size_t s = 0;
|
||||
while (s < len && IsSpace(line[s])) ++s;
|
||||
@@ -103,8 +108,8 @@ bool TryRewriteInterfaceLine(const char *line, size_t len, std::string &out) {
|
||||
const size_t prefixEnd = p; // "## Interface:" (author spacing preserved)
|
||||
|
||||
const uint32_t client = ClientInterfaceVersion();
|
||||
long first = -1;
|
||||
bool hasComma = false, hasClient = false;
|
||||
long first = -1, best = -1;
|
||||
bool hasComma = false;
|
||||
size_t q = p;
|
||||
while (q < len) {
|
||||
while (q < len && (IsSpace(line[q]) || line[q] == ',')) {
|
||||
@@ -120,17 +125,17 @@ bool TryRewriteInterfaceLine(const char *line, size_t len, std::string &out) {
|
||||
}
|
||||
if (digit) {
|
||||
if (first < 0) first = v;
|
||||
if (static_cast<uint32_t>(v) == client) hasClient = true;
|
||||
if (static_cast<uint32_t>(v) <= client && v > best) best = v;
|
||||
} else if (q < len) {
|
||||
++q; // skip a stray non-digit so the scan always advances
|
||||
}
|
||||
}
|
||||
if (!hasComma || !hasClient || first == static_cast<long>(client))
|
||||
return false; // single value / no client version / already first
|
||||
if (!hasComma || best < 0 || first == best)
|
||||
return false; // single value / nothing at or below client / already first
|
||||
|
||||
out.append(line, prefixEnd);
|
||||
out.push_back(' ');
|
||||
out += std::to_string(client);
|
||||
out += std::to_string(best);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,12 +59,13 @@ namespace AddOns::TocRewrite {
|
||||
// Multi-flavor `## Interface:` list. Retail lets a TOC list several
|
||||
// interface versions (`## Interface: 120100, 50504, 11200`). Vanilla's
|
||||
// parser is a plain atoi that reads only the FIRST number, so it never
|
||||
// sees a later 11200 and marks the addon out of date. When the client
|
||||
// version (FUN_ADDON_CLIENT_INTERFACE_VERSION, 11200) appears anywhere in
|
||||
// the list but is not first, the `## Interface:` line is rewritten to that
|
||||
// single value so the engine marks the addon compatible. A list WITHOUT
|
||||
// the client version is left unchanged — it still parses to its first
|
||||
// value and stays out of date, exactly as before.
|
||||
// sees a later 11200 and marks the addon out of date. The line is rewritten
|
||||
// to retail's pick — the closest value at or below the client version
|
||||
// (FUN_ADDON_CLIENT_INTERFACE_VERSION, 11200) — when that is not already
|
||||
// first. If 11200 is listed the pick is 11200 and the addon loads; if not,
|
||||
// the addon stays out of date exactly as before, but the stored value
|
||||
// (`GetAddOnInterfaceVersion`) matches retail. A list with nothing at or
|
||||
// below the client version is left unchanged.
|
||||
//
|
||||
// Called from the FUN_FILE_READ hook after the read succeeds. Does
|
||||
// nothing unless `path` is an addon `.toc` read AND its content contains a
|
||||
|
||||
@@ -0,0 +1,715 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 4.3.4's CSimpleAnim and its Alpha, Translation, Scale and Rotation
|
||||
// subclasses (wowdecomp15595 CSimpleAnim.cpp, CSimple*Anim.cpp and their
|
||||
// _Scripts.cpp), on Animation::Object, and the group's CreateAnimation.
|
||||
// Path and its control points are in Path.cpp.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
#include "animation/Xml.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
const char *const kAnimScripts[] = {"onload", "onplay", "onpause", "onstop", "onfinished", "onupdate"};
|
||||
|
||||
Class &ClassOf(AnimKind kind) {
|
||||
switch (kind) {
|
||||
case KIND_ALPHA: return Anim::s_alphaClass;
|
||||
case KIND_TRANSLATION: return Anim::s_translationClass;
|
||||
case KIND_SCALE: return Anim::s_scaleClass;
|
||||
case KIND_ROTATION: return Anim::s_rotationClass;
|
||||
case KIND_PATH: return Anim::s_pathClass;
|
||||
default: return Anim::s_class;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AnimKind Anim::KindOf(const char *typeName) {
|
||||
if (typeName == nullptr || *typeName == '\0')
|
||||
return KIND_ANIMATION;
|
||||
if (Ascii::EqualCI(typeName, "Alpha"))
|
||||
return KIND_ALPHA;
|
||||
if (Ascii::EqualCI(typeName, "Translation"))
|
||||
return KIND_TRANSLATION;
|
||||
if (Ascii::EqualCI(typeName, "Scale"))
|
||||
return KIND_SCALE;
|
||||
if (Ascii::EqualCI(typeName, "Rotation"))
|
||||
return KIND_ROTATION;
|
||||
if (Ascii::EqualCI(typeName, "Path"))
|
||||
return KIND_PATH;
|
||||
return KIND_ANIMATION;
|
||||
}
|
||||
|
||||
// ---- Construction ---------------------------------------------------------------
|
||||
|
||||
// 4.3.4's constructor links the animation into the group's list; it joins an
|
||||
// order list only once it has an order (NotifyAnimAdded, SortAnimations).
|
||||
Anim::Anim(Group *group, AnimKind kind, const char *name) : Object(ClassOf(kind), group, name), kind(kind) {
|
||||
if (group != nullptr)
|
||||
group->animations.push_back(this);
|
||||
}
|
||||
|
||||
// ~CSimplePathAnim: a path's control points go with it.
|
||||
Anim::~Anim() {
|
||||
for (ControlPoint *point : controlPoints)
|
||||
delete point;
|
||||
}
|
||||
|
||||
// ---- Playback --------------------------------------------------------------------
|
||||
|
||||
bool Anim::Play() {
|
||||
if (targetName[0] != '\0')
|
||||
target = Region::FindRegionByName(targetName);
|
||||
if (playState != 1) {
|
||||
playState = 1;
|
||||
RunScript(A_ONPLAY, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Anim::Pause() {
|
||||
GetGroup()->Pause();
|
||||
if (playState == 1) {
|
||||
playState = 2;
|
||||
RunScript(A_ONPAUSE, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Anim::Stop(bool requested) {
|
||||
if (playState == 0)
|
||||
return;
|
||||
loopState = STATE_NONE;
|
||||
playState = 0;
|
||||
elapsed = 0.0f;
|
||||
progressWithDelay = 0.0f;
|
||||
progress = 0.0f;
|
||||
if (HasScript(A_ONSTOP)) {
|
||||
Game::Lua::PushBool(Game::Lua::State(), requested);
|
||||
RunScript(A_ONSTOP, 1);
|
||||
}
|
||||
}
|
||||
|
||||
bool Anim::IsDelaying() const {
|
||||
if (progressWithDelay <= 0.0f || progressWithDelay >= 1.0f)
|
||||
return false;
|
||||
const float total = endDelay + duration + startDelay;
|
||||
if (std::fabs(total) < 0.00000023841858f)
|
||||
return false;
|
||||
if (loopState == STATE_REVERSE) {
|
||||
if (endDelay > elapsed)
|
||||
return true;
|
||||
if (endDelay + duration > elapsed)
|
||||
return false;
|
||||
} else {
|
||||
if (startDelay > elapsed)
|
||||
return true;
|
||||
if (duration + startDelay > elapsed)
|
||||
return false;
|
||||
}
|
||||
return total > elapsed;
|
||||
}
|
||||
|
||||
float Anim::UpdateProgress(float elapsedSec) {
|
||||
const float prevElapsed = elapsed;
|
||||
elapsed += elapsedSec;
|
||||
const float total = orderPadding + startDelay + endDelay + duration;
|
||||
float now;
|
||||
if (total > elapsed) {
|
||||
now = elapsed;
|
||||
progressWithDelay = now / total;
|
||||
if (progressWithDelay > 1.0f)
|
||||
progressWithDelay = 1.0f;
|
||||
if (std::fabs(duration) < 0.00000023841858f) {
|
||||
progress = 1.0f;
|
||||
} else {
|
||||
frameElapsed += elapsedSec;
|
||||
if ((GetGroup()->flags & GROUP_IGNORE_THROTTLE) || frameElapsed >= g_frameInterval) {
|
||||
const float delay = loopState == STATE_REVERSE ? orderPadding + endDelay : startDelay;
|
||||
progress = (now - delay) / duration;
|
||||
if (progress < 0.0f)
|
||||
progress = 0.0f;
|
||||
else if (progress >= 1.0f)
|
||||
progress = 1.0f;
|
||||
frameElapsed = 0.0f;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
now = total;
|
||||
progressWithDelay = 1.0f;
|
||||
progress = 1.0f;
|
||||
}
|
||||
float p = loopState == STATE_REVERSE ? 1.0f - progress : progress;
|
||||
if (hasSmoother) {
|
||||
// CSimpleAnimSCurveSmoother::GetSmoothProgress.
|
||||
if (std::fabs(smoothIn) >= 0.001f && std::fabs(smoothOut) >= 0.001f)
|
||||
p = 0.5f - std::cos(p * 3.1415927f) * 0.5f;
|
||||
else if (std::fabs(smoothIn) >= 0.001f)
|
||||
p = 1.0f - std::cos(p * 1.5707964f);
|
||||
else if (std::fabs(smoothOut) >= 0.001f)
|
||||
p = std::cos((p + 1.0f) * 1.5707964f) * -1.0f;
|
||||
}
|
||||
smoothProgress = p;
|
||||
return now - prevElapsed;
|
||||
}
|
||||
|
||||
bool Anim::OnUpdate(float elapsedSec, float &consumed) {
|
||||
consumed = 0.0f;
|
||||
if (progressWithDelay >= 1.0f) {
|
||||
Apply(smoothProgress);
|
||||
return true;
|
||||
}
|
||||
if (playState == 0)
|
||||
return true;
|
||||
if (playState == 2) {
|
||||
Apply(smoothProgress);
|
||||
return false;
|
||||
}
|
||||
consumed = UpdateProgress(elapsedSec);
|
||||
if (HasScript(A_ONUPDATE)) {
|
||||
Game::Lua::PushNumber(Game::Lua::State(), elapsedSec);
|
||||
RunScript(A_ONUPDATE, 1);
|
||||
}
|
||||
Apply(smoothProgress);
|
||||
if (progressWithDelay < 1.0f)
|
||||
return false;
|
||||
RunScript(A_ONFINISHED, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Anim::Apply(float p) {
|
||||
void *region = GetRegionParent();
|
||||
if (region == nullptr)
|
||||
return;
|
||||
switch (kind) {
|
||||
case KIND_ALPHA:
|
||||
Region::AddAnimAlpha(region, target, static_cast<int16_t>(change * p));
|
||||
break;
|
||||
case KIND_TRANSLATION:
|
||||
Region::AddAnimTranslation(region, target, offsetX * p, offsetY * p);
|
||||
break;
|
||||
case KIND_SCALE:
|
||||
Region::AddAnimScale(region, target, originPoint, originX, originY, scaleChangeX * p, scaleChangeY * p);
|
||||
break;
|
||||
case KIND_ROTATION:
|
||||
Region::AddAnimRotation(region, target, originPoint, originX, originY, radians * p);
|
||||
break;
|
||||
case KIND_PATH:
|
||||
ApplyPath(p);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Settings ----------------------------------------------------------------------
|
||||
|
||||
void Anim::SetStartDelay(float delay, bool notify) {
|
||||
startDelay = delay < 0.0f ? 0.0f : delay;
|
||||
if (notify && parent != nullptr)
|
||||
GetGroup()->ComputeDuration();
|
||||
}
|
||||
|
||||
void Anim::SetEndDelay(float delay, bool notify) {
|
||||
endDelay = delay < 0.0f ? 0.0f : delay;
|
||||
if (notify && parent != nullptr)
|
||||
GetGroup()->ComputeDuration();
|
||||
}
|
||||
|
||||
void Anim::SetDuration(float value, bool notify) {
|
||||
duration = value;
|
||||
if (notify && parent != nullptr)
|
||||
GetGroup()->ComputeDuration();
|
||||
}
|
||||
|
||||
void Anim::SetOrder(int value, bool notify, bool wrap) {
|
||||
const int oldOrder = order;
|
||||
if (wrap)
|
||||
order = value < 0 || value > 99 ? 0 : value;
|
||||
else
|
||||
order = value < 0 ? 0 : (value > 99 ? 99 : value);
|
||||
// 4.3.4 compares the old order with the argument, not the clamped value.
|
||||
if (notify && parent != nullptr && oldOrder != value)
|
||||
GetGroup()->NotifyOrderChanged(this, oldOrder);
|
||||
}
|
||||
|
||||
void Anim::SetSmoothing(float in, float out) {
|
||||
if ((in > 0.0f && in <= 1.0f) || (out > 0.0f && out <= 1.0f)) {
|
||||
hasSmoother = true;
|
||||
smoothIn = in < 0.0f ? 0.0f : (in < 1.0f ? in : 1.0f);
|
||||
smoothOut = out < 0.0f ? 0.0f : (out < 1.0f ? out : 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
bool Anim::GetSmoothing(float &in, float &out) const {
|
||||
in = hasSmoother ? smoothIn : 0.0f;
|
||||
out = hasSmoother ? smoothOut : 0.0f;
|
||||
return hasSmoother;
|
||||
}
|
||||
|
||||
bool Anim::ChangeParent(Group *group) {
|
||||
if (group == nullptr || parent == group)
|
||||
return false;
|
||||
GetGroup()->NotifyAnimRemoved(this);
|
||||
parent = group;
|
||||
group->NotifyAnimAdded(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Anim::PostLoadXML() {
|
||||
flags &= ~1;
|
||||
if (kind == KIND_PATH) {
|
||||
// CSimplePathAnim::PostLoadXML: the points' orders and positions.
|
||||
BuildSpline();
|
||||
UpdateControlPointPositions();
|
||||
}
|
||||
RunScript(A_ONLOAD, 0);
|
||||
}
|
||||
|
||||
// ---- Lua methods ----------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
using StringToPoint_t = int(__fastcall *)(const char *name, int *point);
|
||||
using PointToString_t = const char *(__fastcall *)(int point);
|
||||
|
||||
Anim *Self(void *L) { return static_cast<Anim *>(Resolve(L, 1, Anim::s_class)); }
|
||||
Anim *SelfOf(void *L, Class &cls) { return static_cast<Anim *>(Resolve(L, 1, cls)); }
|
||||
|
||||
// Usage error when argument `idx` isn't a number; the number otherwise.
|
||||
bool NumberArg(void *L, int idx, Anim *anim, const char *usage) {
|
||||
if (Game::Lua::IsNumber(L, idx))
|
||||
return true;
|
||||
Game::Lua::Error(L, usage, anim->DisplayName());
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_Play(void *L) {
|
||||
if (Anim *a = Self(L))
|
||||
a->Play();
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Pause(void *L) {
|
||||
if (Anim *a = Self(L))
|
||||
a->Pause();
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Stop(void *L) {
|
||||
if (Anim *a = Self(L))
|
||||
a->Stop(true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
template <typename Fn> int PushBoolOf(void *L, Fn fn) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, fn(a));
|
||||
return 1;
|
||||
}
|
||||
template <typename Fn> int PushNumberOf(void *L, Fn fn) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, fn(a));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsDone(void *L) { return PushBoolOf(L, [](Anim *a) { return a->progressWithDelay >= 1.0f; }); }
|
||||
int __fastcall Script_IsPlaying(void *L) { return PushBoolOf(L, [](Anim *a) { return a->playState == 1; }); }
|
||||
int __fastcall Script_IsPaused(void *L) { return PushBoolOf(L, [](Anim *a) { return a->playState == 2; }); }
|
||||
int __fastcall Script_IsStopped(void *L) { return PushBoolOf(L, [](Anim *a) { return a->playState == 0; }); }
|
||||
int __fastcall Script_IsDelaying(void *L) { return PushBoolOf(L, [](Anim *a) { return a->IsDelaying(); }); }
|
||||
int __fastcall Script_GetElapsed(void *L) { return PushNumberOf(L, [](Anim *a) { return a->elapsed; }); }
|
||||
int __fastcall Script_GetStartDelay(void *L) { return PushNumberOf(L, [](Anim *a) { return a->startDelay; }); }
|
||||
int __fastcall Script_GetEndDelay(void *L) { return PushNumberOf(L, [](Anim *a) { return a->endDelay; }); }
|
||||
int __fastcall Script_GetDuration(void *L) { return PushNumberOf(L, [](Anim *a) { return a->duration; }); }
|
||||
int __fastcall Script_GetSmoothProgress(void *L) {
|
||||
return PushNumberOf(L, [](Anim *a) { return a->smoothProgress; });
|
||||
}
|
||||
int __fastcall Script_GetProgress(void *L) { return PushNumberOf(L, [](Anim *a) { return a->progress; }); }
|
||||
int __fastcall Script_GetProgressWithDelay(void *L) {
|
||||
return PushNumberOf(L, [](Anim *a) { return a->progressWithDelay; });
|
||||
}
|
||||
int __fastcall Script_GetOrder(void *L) {
|
||||
return PushNumberOf(L, [](Anim *a) { return static_cast<double>(a->order + 1); });
|
||||
}
|
||||
|
||||
int __fastcall Script_SetStartDelay(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetStartDelay(delaySec)"))
|
||||
a->SetStartDelay(static_cast<float>(Game::Lua::ToNumber(L, 2)), true);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetEndDelay(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetEndDelay(delaySec)"))
|
||||
a->SetEndDelay(static_cast<float>(Game::Lua::ToNumber(L, 2)), true);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetDuration(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetDuration(durationSec)"))
|
||||
a->SetDuration(static_cast<float>(Game::Lua::ToNumber(L, 2)), true);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetSmoothProgress(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetSmoothProgress(smoothProgress)"))
|
||||
a->smoothProgress = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetOrder(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetOrder(order)"))
|
||||
a->SetOrder(static_cast<int>(Game::Lua::ToNumber(L, 2)) - 1, true, false);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetSmoothing(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetSmoothing(\"smoothingType\")", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
float in, out;
|
||||
if (StringToSmoothWeights(Game::Lua::ToString(L, 2), in, out)) {
|
||||
a->SetSmoothing(in, out);
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::Error(L, "%s:SetSmoothing(): Unknown smoothing type specified", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetSmoothing(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
float in, out;
|
||||
a->GetSmoothing(in, out);
|
||||
Game::Lua::PushString(L, SmoothWeightsToString(in, out));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetParent(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_NIL) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Cannot set a 'nil' parent for animations", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
Group *group = nullptr;
|
||||
if (Game::Lua::IsString(L, 2)) {
|
||||
// CScriptObject::GetScriptObjectByName(name, AnimationGroup).
|
||||
Game::Lua::PushString(L, Game::Lua::ToString(L, 2));
|
||||
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
|
||||
void *object = Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE ? Game::Lua::ResolveObject(L, -1) : nullptr;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (object != nullptr && IsOurs(object) &&
|
||||
Game::Lua::IsA(object, reinterpret_cast<uintptr_t>(&Group::s_class.typeId)))
|
||||
group = static_cast<Group *>(object);
|
||||
} else if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_TABLE) {
|
||||
void *object = Game::Lua::ResolveObject(L, 2);
|
||||
if (object == nullptr) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Couldn't find 'this' in parent object", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
if (!IsOurs(object) || !Game::Lua::IsA(object, reinterpret_cast<uintptr_t>(&Group::s_class.typeId))) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Wrong parent object type, expected AnimationGroup", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
group = static_cast<Group *>(object);
|
||||
}
|
||||
if (group == nullptr) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Couldn't find animation group named '%s'", a->DisplayName(),
|
||||
Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : "");
|
||||
return 0;
|
||||
}
|
||||
const int oldOrder = a->order;
|
||||
if (Game::Lua::IsNumber(L, 3)) {
|
||||
a->SetOrder(static_cast<int>(Game::Lua::ToNumber(L, 3)) - 1, false, false);
|
||||
} else {
|
||||
int value = a->order;
|
||||
if (value == -1)
|
||||
value = group->GetMaxAnimOrder() + 1;
|
||||
a->SetOrder(value, false, true);
|
||||
}
|
||||
if (!a->ChangeParent(group) && oldOrder != a->order)
|
||||
group->NotifyOrderChanged(a, oldOrder);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetRegionParent(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
void *region = a->GetRegionParent();
|
||||
if (region != nullptr)
|
||||
Game::Lua::PushObject(L, region);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Alpha ----
|
||||
|
||||
int __fastcall Script_SetChange(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr || !NumberArg(L, 2, a, "Usage: %s:SetChange(change)"))
|
||||
return 0;
|
||||
// CSimpleAlphaAnim::SetChange.
|
||||
const float change = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
const float value = change < -1.0f ? -1.0f : (change >= 1.0f ? 1.0f : change);
|
||||
a->change = static_cast<int16_t>(value * 255.0f);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetChange(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->change * (1.0f / 255.0f));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Translation ----
|
||||
|
||||
int __fastcall Script_SetOffset(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_translationClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetOffset(x, y)", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
const float x = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 2)));
|
||||
const float y = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 3)));
|
||||
// CSimpleTranslationAnim::SetOffset: a near-zero offset is zero.
|
||||
if (x * x + y * y <= 0.00000023841858f) {
|
||||
a->offsetX = 0.0f;
|
||||
a->offsetY = 0.0f;
|
||||
} else {
|
||||
a->offsetX = x;
|
||||
a->offsetY = y;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetOffset(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_translationClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(a->offsetX));
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(a->offsetY));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// ---- Scale and Rotation origin (ReadOrigin) ----
|
||||
|
||||
Anim *OriginSelf(void *L) {
|
||||
Anim *a = static_cast<Anim *>(Resolve(L, 1, Anim::s_class));
|
||||
if (a != nullptr && a->kind != KIND_SCALE && a->kind != KIND_ROTATION) {
|
||||
// The type test of 4.3.4's Scale / Rotation SetOrigin.
|
||||
Game::Lua::Error(L, "Wrong object type for member function");
|
||||
return nullptr;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
int __fastcall Script_SetOrigin(void *L) {
|
||||
Anim *a = OriginSelf(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2) || !Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetOrigin(point, offsetX, offsetY)", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
int point = Offsets::FRAMEPOINT_CENTER;
|
||||
if (!reinterpret_cast<StringToPoint_t>(Offsets::FUN_STRING_TO_FRAME_POINT)(Game::Lua::ToString(L, 2), &point))
|
||||
point = Offsets::FRAMEPOINT_CENTER;
|
||||
a->originPoint = point;
|
||||
a->originX = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 3)));
|
||||
a->originY = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 4)));
|
||||
return 0;
|
||||
}
|
||||
// 4.3.4 pushes the stored offsets as they are, in layout units.
|
||||
int __fastcall Script_GetOrigin(void *L) {
|
||||
Anim *a = OriginSelf(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, reinterpret_cast<PointToString_t>(Offsets::FUN_FRAME_POINT_TO_STRING)(a->originPoint));
|
||||
Game::Lua::PushNumber(L, a->originX);
|
||||
Game::Lua::PushNumber(L, a->originY);
|
||||
return 3;
|
||||
}
|
||||
|
||||
// ---- Scale ----
|
||||
|
||||
int __fastcall Script_SetScale(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetScale(x, y)", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
// CSimpleScaleAnim::SetScale.
|
||||
const float x = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
const float y = static_cast<float>(Game::Lua::ToNumber(L, 3));
|
||||
a->scaleChangeX = 1.0f - (x >= 0.001f ? x : 0.001f);
|
||||
a->scaleChangeY = 1.0f - (y >= 0.001f ? y : 0.001f);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetScale(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeX);
|
||||
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeY);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// ---- Rotation ----
|
||||
|
||||
int __fastcall Script_SetDegrees(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_rotationClass);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetDegrees(degrees)"))
|
||||
a->radians = static_cast<float>(Game::Lua::ToNumber(L, 2) * 0.017453292f);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetDegrees(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_rotationClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->radians * 57.29578f);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_SetRadians(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_rotationClass);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetRadians(radians)"))
|
||||
a->radians = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetRadians(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_rotationClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->radians);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kAnimMethods[] = {
|
||||
{"GetObjectType", &Script_GetObjectType},
|
||||
{"IsObjectType", &Script_IsObjectType},
|
||||
{"GetName", &Script_GetName},
|
||||
{"GetParent", &Script_GetParent},
|
||||
{"Play", &Script_Play},
|
||||
{"Pause", &Script_Pause},
|
||||
{"Stop", &Script_Stop},
|
||||
{"IsDone", &Script_IsDone},
|
||||
{"IsPlaying", &Script_IsPlaying},
|
||||
{"IsPaused", &Script_IsPaused},
|
||||
{"IsStopped", &Script_IsStopped},
|
||||
{"IsDelaying", &Script_IsDelaying},
|
||||
{"GetElapsed", &Script_GetElapsed},
|
||||
{"SetStartDelay", &Script_SetStartDelay},
|
||||
{"GetStartDelay", &Script_GetStartDelay},
|
||||
{"SetEndDelay", &Script_SetEndDelay},
|
||||
{"GetEndDelay", &Script_GetEndDelay},
|
||||
{"SetDuration", &Script_SetDuration},
|
||||
{"GetDuration", &Script_GetDuration},
|
||||
{"GetSmoothProgress", &Script_GetSmoothProgress},
|
||||
{"SetSmoothProgress", &Script_SetSmoothProgress},
|
||||
{"GetProgress", &Script_GetProgress},
|
||||
{"GetProgressWithDelay", &Script_GetProgressWithDelay},
|
||||
{"SetOrder", &Script_SetOrder},
|
||||
{"GetOrder", &Script_GetOrder},
|
||||
{"SetSmoothing", &Script_SetSmoothing},
|
||||
{"GetSmoothing", &Script_GetSmoothing},
|
||||
{"SetParent", &Script_SetParent},
|
||||
{"GetRegionParent", &Script_GetRegionParent},
|
||||
{"HasScript", &Script_HasScript},
|
||||
{"GetScript", &Script_GetScript},
|
||||
{"SetScript", &Script_SetScript},
|
||||
{"HookScript", &Script_HookScript},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kAlphaMethods[] = {
|
||||
{"SetChange", &Script_SetChange},
|
||||
{"GetChange", &Script_GetChange},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kTranslationMethods[] = {
|
||||
{"SetOffset", &Script_SetOffset},
|
||||
{"GetOffset", &Script_GetOffset},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kScaleMethods[] = {
|
||||
{"SetOrigin", &Script_SetOrigin},
|
||||
{"GetOrigin", &Script_GetOrigin},
|
||||
{"SetScale", &Script_SetScale},
|
||||
{"GetScale", &Script_GetScale},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kRotationMethods[] = {
|
||||
{"SetOrigin", &Script_SetOrigin}, {"GetOrigin", &Script_GetOrigin}, {"SetDegrees", &Script_SetDegrees},
|
||||
{"GetDegrees", &Script_GetDegrees}, {"SetRadians", &Script_SetRadians}, {"GetRadians", &Script_GetRadians},
|
||||
};
|
||||
|
||||
template <size_t N> constexpr int Count(const Game::Lua::FrameMethodEntry (&)[N]) { return static_cast<int>(N); }
|
||||
|
||||
} // namespace
|
||||
|
||||
Class Anim::s_class{"Animation", nullptr, kAnimScripts, 6, kAnimMethods, Count(kAnimMethods), 0, 0};
|
||||
Class Anim::s_alphaClass{"Alpha", &Anim::s_class, nullptr, 0, kAlphaMethods, Count(kAlphaMethods), 0, 0};
|
||||
Class Anim::s_translationClass{"Translation", &Anim::s_class, nullptr, 0, kTranslationMethods,
|
||||
Count(kTranslationMethods), 0, 0};
|
||||
Class Anim::s_scaleClass{"Scale", &Anim::s_class, nullptr, 0, kScaleMethods, Count(kScaleMethods), 0, 0};
|
||||
Class Anim::s_rotationClass{"Rotation", &Anim::s_class, nullptr, 0, kRotationMethods, Count(kRotationMethods), 0, 0};
|
||||
|
||||
// ---- AnimationGroup:CreateAnimation ------------------------------------------------------
|
||||
|
||||
int __fastcall Script_CreateAnimation(void *L) {
|
||||
Group *group = static_cast<Group *>(Resolve(L, 1, Group::s_class));
|
||||
if (group == nullptr)
|
||||
return 0;
|
||||
const char *type = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : "Animation";
|
||||
const char *name = Game::Lua::IsString(L, 3) ? Game::Lua::ToString(L, 3) : nullptr;
|
||||
const void *templateNode = nullptr;
|
||||
if (Game::Lua::Type(L, 4) == Game::Lua::TYPE_STRING) {
|
||||
const char *inherits = Game::Lua::ToString(L, 4);
|
||||
templateNode = Xml::Template(inherits);
|
||||
if (templateNode == nullptr) {
|
||||
Game::Lua::Error(L, "%s:CreateAnimation(): Couldn't find inherited node \"%s\"", group->DisplayName(),
|
||||
inherits);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Anim *anim = new Anim(group, Anim::KindOf(type), name);
|
||||
if (templateNode != nullptr) {
|
||||
Xml::LoadAnim(anim, templateNode, nullptr);
|
||||
anim->PostLoadXML();
|
||||
}
|
||||
if (anim->order == -1)
|
||||
anim->SetOrder(group->GetMaxAnimOrder(), false, true);
|
||||
group->NotifyAnimAdded(anim);
|
||||
Push(L, anim);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,213 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "animation/Object.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
// 4.3.4's CSimpleAnimGroup and CSimpleAnim (and its Alpha, Translation,
|
||||
// Scale, Rotation and Path subclasses, and Path's CSimpleControlPoint), ported from the decompiled client
|
||||
// (wowdecomp15595 Engine/Source/Frame/CSimpleAnimGroup.cpp, CSimpleAnim.cpp).
|
||||
// Field and function names follow it; the comments say where 1.12 differs.
|
||||
namespace Animation {
|
||||
|
||||
class Anim;
|
||||
class ControlPoint;
|
||||
|
||||
enum LoopType { LOOP_NONE = 0, LOOP_REPEAT = 1, LOOP_BOUNCE = 2 };
|
||||
enum LoopState { STATE_NONE = 0, STATE_FORWARD = 1, STATE_REVERSE = 2 };
|
||||
|
||||
// m_flags.
|
||||
constexpr uint8_t GROUP_LOADING = 0x01;
|
||||
constexpr uint8_t GROUP_PENDING_FINISH = 0x02;
|
||||
constexpr uint8_t GROUP_PAUSED = 0x04;
|
||||
constexpr uint8_t GROUP_STOPPING = 0x08;
|
||||
constexpr uint8_t GROUP_IGNORE_THROTTLE = 0x10;
|
||||
|
||||
// Script indexes (Class::scriptNames order).
|
||||
enum GroupScript { G_ONLOAD, G_ONPLAY, G_ONPAUSE, G_ONSTOP, G_ONFINISHED, G_ONUPDATE, G_ONLOOP };
|
||||
enum AnimScript { A_ONLOAD, A_ONPLAY, A_ONPAUSE, A_ONSTOP, A_ONFINISHED, A_ONUPDATE };
|
||||
|
||||
class Group : public Object {
|
||||
public:
|
||||
static Class s_class;
|
||||
|
||||
// `region`: the 1.12 region (frame, texture or font string) it animates.
|
||||
Group(void *region, const char *name);
|
||||
~Group();
|
||||
|
||||
void *RegionObject() const { return parent; }
|
||||
|
||||
bool IsDone() const;
|
||||
bool IsPlaying() const { return loopState != STATE_NONE && !(flags & GROUP_PAUSED); }
|
||||
bool Play(bool reverse);
|
||||
void Pause();
|
||||
void Stop(bool requested);
|
||||
void Finish();
|
||||
bool OnUpdate(float elapsedSec);
|
||||
int GetMaxAnimOrder() const;
|
||||
bool HasAlphaAnim() const;
|
||||
bool HasPositionAnim() const;
|
||||
|
||||
void NotifyAnimAdded(Anim *anim);
|
||||
void NotifyOrderChanged(Anim *anim, int oldOrder);
|
||||
void NotifyAnimRemoved(Anim *anim);
|
||||
void SortAnimations();
|
||||
void ComputeDuration();
|
||||
// XML: 4.3.4's PostLoadXML - every animation's, then sort, then OnLoad.
|
||||
void PostLoadXML();
|
||||
|
||||
LoopType loopType = LOOP_NONE;
|
||||
float duration = 0.0f;
|
||||
float initialOffsetX = 0.0f, initialOffsetY = 0.0f; // layout units
|
||||
LoopState loopState = STATE_NONE;
|
||||
uint8_t flags = 0;
|
||||
int currentOrder = -1;
|
||||
float elapsed = 0.0f;
|
||||
float progress = 0.0f;
|
||||
std::vector<Anim *> animations; // m_animations, creation order
|
||||
std::vector<std::vector<Anim *>> animLists; // m_animLists, by order
|
||||
|
||||
private:
|
||||
std::vector<Anim *> *FindAnimList(int order);
|
||||
std::vector<Anim *> *RegisterAnimList(int order);
|
||||
bool AddAnimListNode(std::vector<Anim *> *list, Anim *anim);
|
||||
bool RemoveAnimListNode(std::vector<Anim *> *list, Anim *anim);
|
||||
bool AdvanceLoopState(float elapsedSec, float remaining);
|
||||
bool DoGroupUpdateWork(float elapsedSec);
|
||||
void ResetAnims();
|
||||
};
|
||||
|
||||
enum AnimKind { KIND_ANIMATION, KIND_ALPHA, KIND_TRANSLATION, KIND_SCALE, KIND_ROTATION, KIND_PATH };
|
||||
|
||||
class Anim : public Object {
|
||||
public:
|
||||
static Class s_class; // "Animation"
|
||||
static Class s_alphaClass; // "Alpha"
|
||||
static Class s_translationClass; // "Translation"
|
||||
static Class s_scaleClass; // "Scale"
|
||||
static Class s_rotationClass; // "Rotation"
|
||||
static Class s_pathClass; // "Path"
|
||||
// The class of a CreateAnimation / XML type name; Animation for any
|
||||
// other (4.3.4's CreateAnimation falls back to it).
|
||||
static AnimKind KindOf(const char *typeName);
|
||||
|
||||
Anim(Group *group, AnimKind kind, const char *name);
|
||||
~Anim();
|
||||
|
||||
Group *GetGroup() const { return static_cast<Group *>(parent); }
|
||||
void *GetRegionParent() const { return parent != nullptr ? GetGroup()->RegionObject() : nullptr; }
|
||||
|
||||
bool Play();
|
||||
void Pause();
|
||||
void Stop(bool requested);
|
||||
bool IsDelaying() const;
|
||||
bool OnUpdate(float elapsedSec, float &consumed);
|
||||
void Apply(float progress);
|
||||
void SetStartDelay(float delay, bool notify);
|
||||
void SetEndDelay(float delay, bool notify);
|
||||
void SetDuration(float value, bool notify);
|
||||
void SetOrder(int value, bool notify, bool wrap);
|
||||
void SetSmoothing(float in, float out);
|
||||
bool GetSmoothing(float &in, float &out) const;
|
||||
bool ChangeParent(Group *group);
|
||||
// XML: 4.3.4's PostLoadXML - OnLoad.
|
||||
void PostLoadXML();
|
||||
|
||||
AnimKind kind;
|
||||
float startDelay = 0.0f;
|
||||
float endDelay = 0.0f;
|
||||
float duration = 0.0f;
|
||||
float frameElapsed = 0.0f;
|
||||
float elapsed = 0.0f;
|
||||
float progressWithDelay = 0.0f;
|
||||
float progress = 0.0f;
|
||||
float smoothProgress = 0.0f;
|
||||
float orderPadding = 0.0f;
|
||||
int order = -1;
|
||||
uint8_t flags = 0;
|
||||
int playState = 0; // 0 stopped, 1 playing, 2 paused
|
||||
int loopState = STATE_NONE;
|
||||
void *target = nullptr; // m_target, resolved from targetName on Play
|
||||
char targetName[128] = {}; // m_targetName
|
||||
bool hasSmoother = false;
|
||||
float smoothIn = 0.0f, smoothOut = 0.0f;
|
||||
|
||||
// Alpha: m_change, -255..255.
|
||||
int16_t change = 0;
|
||||
// Translation: the offset; Scale: the scale and origin; Rotation: the
|
||||
// angle and origin. Offsets in layout units, as 4.3.4 keeps them.
|
||||
float offsetX = 0.0f, offsetY = 0.0f;
|
||||
float scaleChangeX = 0.0f, scaleChangeY = 0.0f; // m_scaleChange: 1 - scale
|
||||
float radians = 0.0f;
|
||||
int originPoint = 4; // FRAMEPOINT_CENTER
|
||||
float originX = 0.0f, originY = 0.0f;
|
||||
|
||||
// Path (CSimplePathAnim, Path.cpp): the control points it owns in link
|
||||
// order (m_controlPoints), the same sorted by order (m_sortedPoints),
|
||||
// and m_curve (false NONE, true SMOOTH).
|
||||
std::vector<ControlPoint *> controlPoints;
|
||||
std::vector<ControlPoint *> sortedPoints;
|
||||
bool curve = false;
|
||||
void BuildSpline();
|
||||
void UpdateControlPointPositions();
|
||||
int FindControlPoint(int order) const;
|
||||
int GetMaxControlPointOrder() const;
|
||||
int RegisterSplineControlPoint(ControlPoint *point);
|
||||
void NotifyControlPointAdded(ControlPoint *point);
|
||||
void NotifyControlPointRemoved(ControlPoint *point);
|
||||
void NotifyControlPointOrderChanged(ControlPoint *point);
|
||||
void ApplyPath(float progress);
|
||||
|
||||
private:
|
||||
float UpdateProgress(float elapsedSec);
|
||||
};
|
||||
|
||||
// CSimpleControlPoint: a point of a Path, an offset from where the region
|
||||
// is, in layout units.
|
||||
class ControlPoint : public Object {
|
||||
public:
|
||||
static Class s_class; // "ControlPoint"
|
||||
|
||||
// Links the point at the end of the path's points.
|
||||
ControlPoint(Anim *path, const char *name);
|
||||
~ControlPoint();
|
||||
|
||||
Anim *GetPath() const { return static_cast<Anim *>(parent); }
|
||||
bool ChangeParent(Anim *path);
|
||||
void SetOffset(float x, float y);
|
||||
void SetOrder(int value, bool notify, bool wrap);
|
||||
|
||||
float offsetX = 0.0f, offsetY = 0.0f;
|
||||
float position = 0.0f; // m_position: the path progress the point is at
|
||||
int order = -1;
|
||||
};
|
||||
|
||||
// 4.3.4's s_frameInterval: progress steps at most this often unless the
|
||||
// group ignores the framerate throttle. 1/30 s until SetMaxAnimFramerate
|
||||
// (Framerate.cpp) changes it.
|
||||
extern float g_frameInterval;
|
||||
|
||||
// AnimationGroup:CreateAnimation (Anim.cpp, where the animation classes are).
|
||||
int __fastcall Script_CreateAnimation(void *L);
|
||||
|
||||
const char *LoopTypeToString(LoopType type);
|
||||
bool StringToLoopType(const char *text, LoopType &type);
|
||||
const char *LoopStateToString(int state);
|
||||
bool StringToSmoothWeights(const char *text, float &in, float &out);
|
||||
const char *SmoothWeightsToString(float in, float out);
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,61 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
// `SetMaxAnimFramerate(framesPerSec)` / `GetMaxAnimFramerate()` - 4.3.4's
|
||||
// globals over CSimpleAnim::s_frameInterval (SimpleFrameScript_Scripts.cpp),
|
||||
// the throttle that Anim::UpdateProgress steps progress by. The setter stores
|
||||
// 1 / fps unchecked, as 4.3.4 does: 0 stores infinity, so progress stops
|
||||
// stepping until the animation ends.
|
||||
|
||||
#include "Classes.h"
|
||||
#include "Game.h"
|
||||
|
||||
namespace Animation {
|
||||
|
||||
float g_frameInterval = 1.0f / 30.0f;
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_SetMaxAnimFramerate(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: SetMaxFramerate(framesPerSec)");
|
||||
return 0;
|
||||
}
|
||||
g_frameInterval = 1.0f / static_cast<float>(Game::Lua::ToNumber(L, 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMaxAnimFramerate(void *L) {
|
||||
Game::Lua::PushNumber(L, 1.0f / g_frameInterval);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kSetArgs[] = {Game::Doc::Req("framesPerSec", "number")};
|
||||
const Game::Doc::Field kGetRets[] = {Game::Doc::Req("framesPerSec", "number")};
|
||||
|
||||
const Game::Doc::Function kSet{"Sets how many times per second animations step their progress.", kSetArgs, {},
|
||||
"AnimationGlobals"};
|
||||
const Game::Doc::Function kGet{"Returns how many times per second animations step their progress.", {}, kGetRets,
|
||||
"AnimationGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("SetMaxAnimFramerate", &Script_SetMaxAnimFramerate, &kSet);
|
||||
Game::Lua::RegisterGlobalFunction("GetMaxAnimFramerate", &Script_GetMaxAnimFramerate, &kGet);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,672 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 4.3.4's CSimpleAnimGroup (wowdecomp15595 CSimpleAnimGroup.cpp and
|
||||
// CSimpleAnimGroup_Scripts.cpp), on Animation::Object.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
const char *const kGroupScripts[] = {"onload", "onplay", "onpause", "onstop", "onfinished", "onupdate", "onloop"};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- Construction ---------------------------------------------------------------
|
||||
|
||||
Group::Group(void *region, const char *name) : Object(s_class, region, name) {
|
||||
if (region != nullptr)
|
||||
Region::RegisterGroup(region, this);
|
||||
}
|
||||
|
||||
Group::~Group() {
|
||||
for (Anim *anim : animations)
|
||||
delete anim;
|
||||
}
|
||||
|
||||
// ---- State --------------------------------------------------------------------
|
||||
|
||||
bool Group::IsDone() const {
|
||||
if (duration <= 0.0001f)
|
||||
return true;
|
||||
if (loopType == LOOP_REPEAT || loopType == LOOP_BOUNCE)
|
||||
return false;
|
||||
if (loopState == STATE_FORWARD || loopState == STATE_REVERSE)
|
||||
return false;
|
||||
return progress >= 1.0f;
|
||||
}
|
||||
|
||||
void Group::Finish() {
|
||||
if (!IsDone() && loopState != STATE_NONE)
|
||||
flags |= GROUP_PENDING_FINISH;
|
||||
}
|
||||
|
||||
int Group::GetMaxAnimOrder() const {
|
||||
if (animLists.empty())
|
||||
return -1;
|
||||
return animLists.back().front()->order;
|
||||
}
|
||||
|
||||
bool Group::HasAlphaAnim() const {
|
||||
for (const Anim *anim : animations)
|
||||
if (anim->kind == KIND_ALPHA)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Group::HasPositionAnim() const {
|
||||
for (const Anim *anim : animations)
|
||||
if (anim->kind != KIND_ALPHA)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Order lists ----------------------------------------------------------------
|
||||
|
||||
std::vector<Anim *> *Group::FindAnimList(int order) {
|
||||
if (order == -1)
|
||||
return nullptr;
|
||||
int low = 0;
|
||||
int high = static_cast<int>(animLists.size()) - 1;
|
||||
while (low <= high) {
|
||||
const int middle = static_cast<int>(static_cast<unsigned>(high + low) >> 1);
|
||||
std::vector<Anim *> &list = animLists[middle];
|
||||
if (!list.empty()) {
|
||||
const int listOrder = list.front()->order;
|
||||
if (listOrder < order)
|
||||
low = middle + 1;
|
||||
else if (listOrder > order)
|
||||
high = middle - 1;
|
||||
else
|
||||
return &list;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<Anim *> *Group::RegisterAnimList(int order) {
|
||||
if (order == -1)
|
||||
return nullptr;
|
||||
const size_t count = animLists.size();
|
||||
if (count == 0) {
|
||||
animLists.emplace_back();
|
||||
return &animLists.back();
|
||||
}
|
||||
size_t index = count;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
std::vector<Anim *> &list = animLists[i];
|
||||
if (list.empty())
|
||||
return &list;
|
||||
const int listOrder = list.front()->order;
|
||||
if (order == listOrder)
|
||||
return &list;
|
||||
if (order < listOrder) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
animLists.insert(animLists.begin() + static_cast<ptrdiff_t>(index), std::vector<Anim *>());
|
||||
return &animLists[index];
|
||||
}
|
||||
|
||||
bool Group::AddAnimListNode(std::vector<Anim *> *list, Anim *anim) {
|
||||
if (list == nullptr)
|
||||
return false;
|
||||
list->push_back(anim);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Group::RemoveAnimListNode(std::vector<Anim *> *list, Anim *anim) {
|
||||
if (list == nullptr)
|
||||
return false;
|
||||
const auto it = std::find(list->begin(), list->end(), anim);
|
||||
if (it == list->end())
|
||||
return false;
|
||||
list->erase(it);
|
||||
if (list->empty())
|
||||
SortAnimations();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Group::ComputeDuration() {
|
||||
duration = 0.0f;
|
||||
for (std::vector<Anim *> &list : animLists) {
|
||||
float span = 0.0f;
|
||||
for (const Anim *anim : list) {
|
||||
const float total = anim->duration + anim->endDelay + anim->startDelay;
|
||||
if (total > span)
|
||||
span = total;
|
||||
}
|
||||
for (Anim *anim : list)
|
||||
anim->orderPadding = span - (anim->duration + anim->endDelay + anim->startDelay);
|
||||
duration += span;
|
||||
}
|
||||
}
|
||||
|
||||
void Group::NotifyAnimAdded(Anim *anim) {
|
||||
if (anim == nullptr)
|
||||
return;
|
||||
// m_animations.LinkToTail: an animation already in the list moves to its tail.
|
||||
animations.erase(std::remove(animations.begin(), animations.end(), anim), animations.end());
|
||||
animations.push_back(anim);
|
||||
if (AddAnimListNode(RegisterAnimList(anim->order), anim))
|
||||
ComputeDuration();
|
||||
}
|
||||
|
||||
void Group::NotifyOrderChanged(Anim *anim, int oldOrder) {
|
||||
if (anim == nullptr)
|
||||
return;
|
||||
if (RemoveAnimListNode(FindAnimList(oldOrder), anim))
|
||||
if (AddAnimListNode(RegisterAnimList(anim->order), anim))
|
||||
ComputeDuration();
|
||||
}
|
||||
|
||||
void Group::NotifyAnimRemoved(Anim *anim) {
|
||||
if (anim == nullptr)
|
||||
return;
|
||||
animations.erase(std::remove(animations.begin(), animations.end(), anim), animations.end());
|
||||
std::vector<Anim *> *list = FindAnimList(anim->order);
|
||||
if (RemoveAnimListNode(list, anim)) {
|
||||
// RemoveAnimListNode sorts when the list emptied; the list may be gone.
|
||||
ComputeDuration();
|
||||
}
|
||||
}
|
||||
|
||||
void Group::SortAnimations() {
|
||||
if (loopState == STATE_NONE || (flags & GROUP_PAUSED)) {
|
||||
loopState = STATE_NONE;
|
||||
flags &= 0xF0;
|
||||
currentOrder = -1;
|
||||
elapsed = 0.0f;
|
||||
progress = 0.0f;
|
||||
} else {
|
||||
Stop(false);
|
||||
}
|
||||
if (animations.empty()) {
|
||||
animLists.clear();
|
||||
return;
|
||||
}
|
||||
for (std::vector<Anim *> &list : animLists)
|
||||
list.clear();
|
||||
int nextOrder = 0;
|
||||
for (Anim *anim : animations) {
|
||||
int order = anim->order;
|
||||
if (order == -1) {
|
||||
order = nextOrder++;
|
||||
anim->order = order < 0 ? 0 : order;
|
||||
} else {
|
||||
nextOrder = order + 1;
|
||||
}
|
||||
if (nextOrder > 99)
|
||||
nextOrder = 0;
|
||||
AddAnimListNode(RegisterAnimList(order), anim);
|
||||
}
|
||||
while (!animLists.empty() && animLists.back().empty())
|
||||
animLists.pop_back();
|
||||
}
|
||||
|
||||
void Group::ResetAnims() {
|
||||
for (Anim *anim : animations) {
|
||||
anim->loopState = loopState > 0 && loopState <= 2 ? loopState : 0;
|
||||
anim->playState = 0;
|
||||
anim->elapsed = 0.0f;
|
||||
anim->progressWithDelay = 0.0f;
|
||||
anim->progress = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Playback --------------------------------------------------------------------
|
||||
|
||||
void Group::Pause() {
|
||||
if (loopState == STATE_NONE || (flags & GROUP_PAUSED))
|
||||
return;
|
||||
flags |= GROUP_PAUSED;
|
||||
const std::vector<Anim *> list = animLists[currentOrder];
|
||||
for (Anim *anim : list)
|
||||
anim->Pause();
|
||||
RunScript(G_ONPAUSE, 0);
|
||||
}
|
||||
|
||||
void Group::Stop(bool requested) {
|
||||
if (loopState == STATE_NONE || (flags & GROUP_STOPPING))
|
||||
return;
|
||||
flags |= GROUP_STOPPING;
|
||||
const std::vector<Anim *> list = animLists[currentOrder];
|
||||
for (Anim *anim : list)
|
||||
anim->Stop(requested);
|
||||
Region::NotifyEnd(RegionObject(), this);
|
||||
flags &= 0xF0;
|
||||
loopState = STATE_NONE;
|
||||
currentOrder = -1;
|
||||
elapsed = 0.0f;
|
||||
progress = 0.0f;
|
||||
if (HasScript(G_ONSTOP)) {
|
||||
Game::Lua::PushBool(Game::Lua::State(), requested);
|
||||
RunScript(G_ONSTOP, 1);
|
||||
}
|
||||
}
|
||||
|
||||
bool Group::AdvanceLoopState(float /*elapsedSec*/, float /*remaining*/) {
|
||||
const int count = static_cast<int>(animLists.size());
|
||||
const bool reverse = loopState == STATE_REVERSE;
|
||||
const int step = reverse ? -1 : 1;
|
||||
const int first = reverse ? count - 1 : 0;
|
||||
const int end = reverse ? -1 : count;
|
||||
|
||||
currentOrder += step;
|
||||
if (currentOrder == end) {
|
||||
if (loopType == LOOP_NONE || (flags & GROUP_PENDING_FINISH))
|
||||
return true;
|
||||
if (loopType == LOOP_BOUNCE) {
|
||||
loopState = static_cast<LoopState>(!reverse + 1);
|
||||
currentOrder -= step;
|
||||
} else {
|
||||
currentOrder = first;
|
||||
}
|
||||
elapsed = 0.0f;
|
||||
progress = 0.0f;
|
||||
ResetAnims();
|
||||
if (HasScript(G_ONLOOP)) {
|
||||
Game::Lua::PushString(Game::Lua::State(), LoopStateToString(loopState));
|
||||
RunScript(G_ONLOOP, 1);
|
||||
}
|
||||
if (loopState == STATE_NONE || (flags & GROUP_PAUSED))
|
||||
return true;
|
||||
}
|
||||
const std::vector<Anim *> list = animLists[currentOrder];
|
||||
for (Anim *anim : list)
|
||||
anim->Play();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Group::DoGroupUpdateWork(float elapsedSec) {
|
||||
float remaining = elapsedSec < 0.0f ? 0.0f : (elapsedSec >= 60.0f ? 60.0f : elapsedSec);
|
||||
while (true) {
|
||||
for (Anim *anim : animLists[currentOrder]) {
|
||||
void *region = anim->GetRegionParent();
|
||||
if (region != nullptr)
|
||||
Region::PreOnAnimUpdate(region);
|
||||
}
|
||||
for (int i = 0; i < currentOrder; ++i)
|
||||
for (Anim *anim : animLists[i])
|
||||
anim->Apply(1.0f);
|
||||
|
||||
float maxConsumed = 0.0f;
|
||||
bool finished = true;
|
||||
const std::vector<Anim *> list = animLists[currentOrder];
|
||||
for (Anim *anim : list) {
|
||||
float consumed;
|
||||
finished = anim->OnUpdate(remaining, consumed) && finished;
|
||||
if (consumed > maxConsumed)
|
||||
maxConsumed = consumed;
|
||||
}
|
||||
|
||||
if (!(flags & GROUP_PAUSED) && loopState != STATE_NONE) {
|
||||
remaining -= maxConsumed;
|
||||
elapsed += maxConsumed;
|
||||
progress = elapsed < duration ? elapsed / duration : 1.0f;
|
||||
if (finished && AdvanceLoopState(elapsedSec, remaining))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (remaining <= 0.0001f || IsDone() || loopState == STATE_NONE || (flags & GROUP_PAUSED))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Group::OnUpdate(float elapsedSec) {
|
||||
if (IsDone() || loopState == STATE_NONE)
|
||||
return true;
|
||||
const bool done = DoGroupUpdateWork(elapsedSec);
|
||||
if (HasScript(G_ONUPDATE)) {
|
||||
Game::Lua::PushNumber(Game::Lua::State(), elapsedSec);
|
||||
RunScript(G_ONUPDATE, 1);
|
||||
}
|
||||
if (done) {
|
||||
Region::NotifyEnd(RegionObject(), this);
|
||||
const bool requested = (flags & GROUP_PENDING_FINISH) != 0;
|
||||
loopState = STATE_NONE;
|
||||
flags &= 0xF0;
|
||||
currentOrder = -1;
|
||||
elapsed = 0.0f;
|
||||
progress = 0.0f;
|
||||
if (HasScript(G_ONFINISHED)) {
|
||||
Game::Lua::PushBool(Game::Lua::State(), requested);
|
||||
RunScript(G_ONFINISHED, 1);
|
||||
}
|
||||
}
|
||||
return IsDone();
|
||||
}
|
||||
|
||||
bool Group::Play(bool reverse) {
|
||||
if (animations.empty())
|
||||
return false;
|
||||
if (loopState != STATE_NONE && !(flags & GROUP_PAUSED))
|
||||
return true;
|
||||
if (flags & GROUP_PAUSED) {
|
||||
flags &= ~GROUP_PAUSED;
|
||||
} else {
|
||||
flags &= 0xF0;
|
||||
loopState = STATE_NONE;
|
||||
currentOrder = reverse ? 0x7FFF : -1;
|
||||
elapsed = 0.0f;
|
||||
progress = 0.0f;
|
||||
}
|
||||
RunScript(G_ONPLAY, 0);
|
||||
const int count = static_cast<int>(animLists.size());
|
||||
if (count > 0) {
|
||||
if (currentOrder < 0 || currentOrder >= count) {
|
||||
if (currentOrder < 0) {
|
||||
loopState = STATE_FORWARD;
|
||||
currentOrder = 0;
|
||||
} else {
|
||||
loopState = STATE_REVERSE;
|
||||
currentOrder = count - 1;
|
||||
}
|
||||
ResetAnims();
|
||||
}
|
||||
const std::vector<Anim *> list = animLists[currentOrder];
|
||||
for (Anim *anim : list)
|
||||
anim->Play();
|
||||
Region::NotifyBegin(RegionObject(), this);
|
||||
}
|
||||
OnUpdate(0.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Group::PostLoadXML() {
|
||||
flags &= ~GROUP_LOADING;
|
||||
const std::vector<Anim *> list = animations;
|
||||
for (Anim *anim : list)
|
||||
anim->PostLoadXML();
|
||||
SortAnimations();
|
||||
ComputeDuration();
|
||||
RunScript(G_ONLOAD, 0);
|
||||
}
|
||||
|
||||
// ---- Strings ----------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
struct Named {
|
||||
int value;
|
||||
const char *name;
|
||||
};
|
||||
const Named kLoopTypes[] = {{LOOP_NONE, "NONE"}, {LOOP_REPEAT, "REPEAT"}, {LOOP_BOUNCE, "BOUNCE"}};
|
||||
const Named kLoopStates[] = {{STATE_NONE, "NONE"}, {STATE_FORWARD, "FORWARD"}, {STATE_REVERSE, "REVERSE"}};
|
||||
|
||||
struct Weights {
|
||||
float in, out;
|
||||
const char *name;
|
||||
};
|
||||
const Weights kSmoothWeights[] = {
|
||||
{0.0f, 0.0f, "NONE"}, {1.0f, 0.0f, "IN"}, {0.0f, 1.0f, "OUT"}, {1.0f, 1.0f, "IN_OUT"}, {1.0f, 1.0f, "OUT_IN"},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *LoopTypeToString(LoopType type) {
|
||||
for (const Named &n : kLoopTypes)
|
||||
if (n.value == type)
|
||||
return n.name;
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
bool StringToLoopType(const char *text, LoopType &type) {
|
||||
for (const Named &n : kLoopTypes)
|
||||
if (text != nullptr && Ascii::EqualCI(n.name, text)) {
|
||||
type = static_cast<LoopType>(n.value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *LoopStateToString(int state) {
|
||||
for (const Named &n : kLoopStates)
|
||||
if (n.value == state)
|
||||
return n.name;
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
bool StringToSmoothWeights(const char *text, float &in, float &out) {
|
||||
for (const Weights &w : kSmoothWeights)
|
||||
if (text != nullptr && Ascii::EqualCI(w.name, text)) {
|
||||
in = w.in;
|
||||
out = w.out;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *SmoothWeightsToString(float in, float out) {
|
||||
for (const Weights &w : kSmoothWeights)
|
||||
if (std::fabs(in - w.in) < 0.001f && std::fabs(out - w.out) < 0.001f)
|
||||
return w.name;
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// ---- Lua methods (CSimpleAnimGroup_Scripts.cpp) -------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
Group *Self(void *L) { return static_cast<Group *>(Resolve(L, 1, Group::s_class)); }
|
||||
|
||||
|
||||
int __fastcall Script_Play(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->Play(Game::Lua::ToFlag(L, 2));
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Pause(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->Pause();
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Stop(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->Stop(true);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Finish(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->Finish();
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetProgress(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, g->elapsed / g->duration);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_IsDone(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, g->IsDone());
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_IsPlaying(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, g->IsPlaying());
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_IsPaused(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, (g->flags & GROUP_PAUSED) != 0);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_IsPendingFinish(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, (g->flags & GROUP_PENDING_FINISH) != 0);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_GetDuration(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, g->duration);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_SetLooping(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetLooping(\"loopType\")", g->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
LoopType type;
|
||||
if (StringToLoopType(Game::Lua::ToString(L, 2), type)) {
|
||||
g->loopType = type;
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::Error(L, "%s:SetLooping(): Unknown loop type specified", g->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetLooping(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, LoopTypeToString(g->loopType));
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_GetLoopState(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, LoopStateToString(g->loopState));
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_GetMaxOrder(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, g->GetMaxAnimOrder() + 1);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_SetInitialOffset(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetInitialOffset(x, y)", g->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
const float x = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 2)));
|
||||
const float y = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 3)));
|
||||
// 4.3.4's SetInitialOffset: a near-zero offset is zero.
|
||||
if (x * x + y * y <= 0.00000023841858f) {
|
||||
g->initialOffsetX = 0.0f;
|
||||
g->initialOffsetY = 0.0f;
|
||||
} else {
|
||||
g->initialOffsetX = x;
|
||||
g->initialOffsetY = y;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetInitialOffset(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(g->initialOffsetX));
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(g->initialOffsetY));
|
||||
return 2;
|
||||
}
|
||||
int __fastcall Script_GetAnimations(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
const int count = static_cast<int>(g->animations.size());
|
||||
if (!Game::Lua::CheckStack(L, count)) {
|
||||
Game::Lua::Error(L, "%s:GetAnimations(): Stack overflow", g->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
for (Anim *anim : g->animations)
|
||||
Push(L, anim);
|
||||
return count;
|
||||
}
|
||||
int __fastcall Script_SetIgnoreFramerateThrottle(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
if (Game::Lua::ToFlag(L, 2))
|
||||
g->flags |= GROUP_IGNORE_THROTTLE;
|
||||
else
|
||||
g->flags &= ~GROUP_IGNORE_THROTTLE;
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetIgnoreFramerateThrottle(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
if (g->flags & GROUP_IGNORE_THROTTLE)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"GetObjectType", &Script_GetObjectType},
|
||||
{"IsObjectType", &Script_IsObjectType},
|
||||
{"GetName", &Script_GetName},
|
||||
{"GetParent", &Script_GetParent},
|
||||
{"Play", &Script_Play},
|
||||
{"Pause", &Script_Pause},
|
||||
{"Stop", &Script_Stop},
|
||||
{"Finish", &Script_Finish},
|
||||
{"GetProgress", &Script_GetProgress},
|
||||
{"IsDone", &Script_IsDone},
|
||||
{"IsPlaying", &Script_IsPlaying},
|
||||
{"IsPaused", &Script_IsPaused},
|
||||
{"IsPendingFinish", &Script_IsPendingFinish},
|
||||
{"GetDuration", &Script_GetDuration},
|
||||
{"SetLooping", &Script_SetLooping},
|
||||
{"GetLooping", &Script_GetLooping},
|
||||
{"GetLoopState", &Script_GetLoopState},
|
||||
{"GetMaxOrder", &Script_GetMaxOrder},
|
||||
{"SetInitialOffset", &Script_SetInitialOffset},
|
||||
{"GetInitialOffset", &Script_GetInitialOffset},
|
||||
{"GetAnimations", &Script_GetAnimations},
|
||||
{"CreateAnimation", &Script_CreateAnimation},
|
||||
{"HasScript", &Script_HasScript},
|
||||
{"GetScript", &Script_GetScript},
|
||||
{"SetScript", &Script_SetScript},
|
||||
{"HookScript", &Script_HookScript},
|
||||
{"SetIgnoreFramerateThrottle", &Script_SetIgnoreFramerateThrottle},
|
||||
{"GetIgnoreFramerateThrottle", &Script_GetIgnoreFramerateThrottle},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Class Group::s_class{"AnimationGroup", nullptr, kGroupScripts, 7, kMethods,
|
||||
static_cast<int>(sizeof(kMethods) / sizeof(kMethods[0])), 0, 0};
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,338 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "animation/Object.h"
|
||||
|
||||
#include "HookSecureFunc.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
#include "frame/ScriptArgs.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
using Construct_t = void(__fastcall *)(void *object);
|
||||
using Destruct_t = void(__fastcall *)(void *object);
|
||||
using Lock_t = void(__fastcall *)(void *object, void *edx, const char *name);
|
||||
using Unlock_t = void(__fastcall *)(void *object, void *edx, const char *name);
|
||||
using BaseResolve_t = uint32_t *(__fastcall *)(void *object, void *edx, const char *name);
|
||||
using Release_t = void(__stdcall *)(uint32_t *slot);
|
||||
using GetName_t = const char *(__fastcall *)(const void *object);
|
||||
|
||||
// 4.3.4's CScriptObject type name, which every class's IsA(name) accepts.
|
||||
constexpr const char *kObjectTypeName = "Object";
|
||||
|
||||
Object *Self(void *object) { return static_cast<Object *>(object); }
|
||||
|
||||
// ---- The vtable (the engine calls these on our objects) --------------------
|
||||
|
||||
void *__fastcall V_Delete(Object *object, void * /*edx*/, unsigned flags);
|
||||
|
||||
const char *__fastcall V_GetName(Object *object) { return object->Name(); }
|
||||
|
||||
// vtable+0x08: push the method `name` of the object's class or a base.
|
||||
int __fastcall V_FindMethod(Object *object, void * /*edx*/, void *L, const char *name) {
|
||||
for (const Class *c = object->cls; c != nullptr; c = c->base) {
|
||||
if (c->methodsRef <= 0)
|
||||
continue;
|
||||
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, c->methodsRef);
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::RawGet(L, -2);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_NIL) {
|
||||
Game::Lua::Remove(L, -2);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::SetTop(L, -3);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// vtable+0x0C: 4.3.4's GetScriptByName - the base OnEvent first.
|
||||
uint32_t *__fastcall V_ResolveScript(Object *object, void * /*edx*/, const char *name) {
|
||||
uint32_t *slot =
|
||||
reinterpret_cast<BaseResolve_t>(Offsets::FUN_SCRIPT_OBJECT_BASE_RESOLVE_SCRIPT)(object, nullptr, name);
|
||||
return slot != nullptr ? slot : object->ScriptSlot(name);
|
||||
}
|
||||
|
||||
int __fastcall V_IsA(Object *object, void * /*edx*/, int typeId) {
|
||||
for (const Class *c = object->cls; c != nullptr; c = c->base)
|
||||
if (c->typeId != 0 && c->typeId == typeId)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// vtable+0x14 differs per engine class; nothing calls it on ours.
|
||||
int __fastcall V_Unused(Object * /*object*/) { return 0; }
|
||||
|
||||
int __fastcall V_IsAName(Object *object, void * /*edx*/, const char *typeName) {
|
||||
if (typeName == nullptr)
|
||||
return 0;
|
||||
for (const Class *c = object->cls; c != nullptr; c = c->base)
|
||||
if (Ascii::EqualCI(typeName, c->typeName))
|
||||
return 1;
|
||||
return Ascii::EqualCI(typeName, kObjectTypeName) ? 1 : 0;
|
||||
}
|
||||
|
||||
const char *__fastcall V_GetObjectTypeName(Object *object) { return object->cls->typeName; }
|
||||
|
||||
void *g_vtable[] = {
|
||||
reinterpret_cast<void *>(&V_Delete), reinterpret_cast<void *>(&V_GetName),
|
||||
reinterpret_cast<void *>(&V_FindMethod), reinterpret_cast<void *>(&V_ResolveScript),
|
||||
reinterpret_cast<void *>(&V_IsA), reinterpret_cast<void *>(&V_Unused),
|
||||
reinterpret_cast<void *>(&V_IsAName), reinterpret_cast<void *>(&V_GetObjectTypeName),
|
||||
};
|
||||
|
||||
// Slot 0, the engine's deleting destructor. The engine never deletes our
|
||||
// objects - their owners (a region, a group) do, by their concrete type.
|
||||
void *__fastcall V_Delete(Object *object, void * /*edx*/, unsigned /*flags*/) { return object; }
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- Object -------------------------------------------------------------------
|
||||
|
||||
Object::Object(const Class &cls, void *parent, const char *name) : cls(&cls), parent(parent) {
|
||||
reinterpret_cast<Construct_t>(Offsets::FUN_SCRIPT_OBJECT_CONSTRUCT)(this);
|
||||
vtable = g_vtable;
|
||||
if (name != nullptr && *name != '\0')
|
||||
this->name = CreateName(name, parent);
|
||||
reinterpret_cast<Lock_t>(Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER)(this, nullptr, Name());
|
||||
}
|
||||
|
||||
bool Object::s_detached = false;
|
||||
|
||||
Object::~Object() {
|
||||
if (s_detached)
|
||||
return;
|
||||
for (auto &slot : scripts)
|
||||
reinterpret_cast<Release_t>(Offsets::FUN_SCRIPT_RELEASE)(slot);
|
||||
while (holders > 1)
|
||||
reinterpret_cast<Unlock_t>(Offsets::FUN_SCRIPT_OBJECT_UNLOCK)(this, nullptr, nullptr);
|
||||
if (holders > 0)
|
||||
reinterpret_cast<Unlock_t>(Offsets::FUN_SCRIPT_OBJECT_UNLOCK)(this, nullptr, Name());
|
||||
reinterpret_cast<Destruct_t>(Offsets::FUN_SCRIPT_OBJECT_DESTRUCT)(this);
|
||||
}
|
||||
|
||||
uint32_t *Object::ScriptSlot(const char *scriptName) {
|
||||
if (scriptName == nullptr)
|
||||
return nullptr;
|
||||
for (const Class *c = cls; c != nullptr; c = c->base) {
|
||||
for (int i = 0; i < c->scriptCount; ++i)
|
||||
if (Ascii::EqualCI(scriptName, c->scriptNames[i]))
|
||||
return scripts[i];
|
||||
if (c->scriptCount > 0)
|
||||
break; // only one class in a chain declares the scripts
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Object::RunScript(int index, int nargs) {
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int first = Game::Lua::GetTop(L) - nargs + 1;
|
||||
if (HasScript(index))
|
||||
Frame::ScriptArgs::RunSlotWithValues(L, scripts[index], this, first, nargs);
|
||||
Game::Lua::SetTop(L, first - 1);
|
||||
}
|
||||
|
||||
// ---- Helpers --------------------------------------------------------------------
|
||||
|
||||
int TypeId(Class &cls) { return Game::Lua::FrameScriptTypeId(reinterpret_cast<uintptr_t>(&cls.typeId)); }
|
||||
|
||||
Object *Resolve(void *L, int idx, Class &cls) {
|
||||
return static_cast<Object *>(Game::Lua::ResolveTypedObject(L, idx, reinterpret_cast<uintptr_t>(&cls.typeId)));
|
||||
}
|
||||
|
||||
void Push(void *L, Object *object) { Game::Lua::PushObject(L, object); }
|
||||
|
||||
bool IsOurs(const void *object) { return object != nullptr && Game::Read<void **>(object, 0) == g_vtable; }
|
||||
|
||||
std::string CreateName(const char *source, const void *parent) {
|
||||
constexpr const char kParent[] = "$parent";
|
||||
constexpr size_t kParentLen = sizeof(kParent) - 1;
|
||||
if (std::strlen(source) < kParentLen || !Ascii::EqualCI(std::string(source, kParentLen).c_str(), kParent))
|
||||
return source;
|
||||
std::string name = "Top";
|
||||
for (const void *p = parent; p != nullptr;) {
|
||||
const char *pname = IsOurs(p) ? static_cast<const Object *>(p)->Name()
|
||||
: Game::Read<GetName_t>(Game::Read<void *>(p, 0),
|
||||
Offsets::VTBL_SCRIPTOBJECT_GET_NAME)(p);
|
||||
if (pname != nullptr && *pname != '\0') {
|
||||
name = pname;
|
||||
break;
|
||||
}
|
||||
p = IsOurs(p) ? static_cast<const Object *>(p)->parent : Game::Read<void *>(p, Offsets::OFF_REGION_PARENT);
|
||||
}
|
||||
return name + (source + kParentLen);
|
||||
}
|
||||
|
||||
void RegisterClass(Class &cls) {
|
||||
void *L = Game::Lua::State();
|
||||
TypeId(cls);
|
||||
Game::Lua::NewTable(L);
|
||||
for (int i = 0; i < cls.methodCount; ++i) {
|
||||
Game::Lua::PushString(L, cls.methods[i].name);
|
||||
Game::Lua::PushCClosure(L, cls.methods[i].func, 0);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
cls.methodsRef = Game::Lua::Ref(L, Game::Lua::REGISTRY_INDEX);
|
||||
}
|
||||
|
||||
// ---- The base methods -----------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// The object at self, of any of our classes.
|
||||
Object *AnySelf(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE)
|
||||
return nullptr;
|
||||
void *object = Game::Lua::ResolveObject(L, 1);
|
||||
return IsOurs(object) ? Self(object) : nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int __fastcall Script_GetObjectType(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, object->cls->typeName);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsObjectType(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:IsObjectType(\"TYPE\")", object->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
if (V_IsAName(object, nullptr, Game::Lua::ToString(L, 2)))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetName(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, object->Name());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetParent(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
if (object->parent != nullptr)
|
||||
Game::Lua::PushObject(L, object->parent);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 4.3.4's HasScript: 1 when the class has the script, else nil.
|
||||
int __fastcall Script_HasScript(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:HasScript(\"type\")", object->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
if (V_ResolveScript(object, nullptr, Game::Lua::ToString(L, 2)) != nullptr)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The engine's Script GetScript (0x00774780) on our object: the slot's
|
||||
// context becomes current, then the handler (nil for none) is pushed.
|
||||
int __fastcall Script_GetScript(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:GetScript(\"type\")", object->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
const uint32_t *slot = V_ResolveScript(object, nullptr, Game::Lua::ToString(L, 2));
|
||||
if (slot == nullptr) {
|
||||
Game::Lua::Error(L, "%s doesn't have a \"%s\" script", object->DisplayName(), Game::Lua::ToString(L, 2));
|
||||
return 0;
|
||||
}
|
||||
if (slot[1] != 0 && Game::Read<int>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH) != 0)
|
||||
Game::Ref<uint32_t>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT) = slot[1];
|
||||
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, static_cast<int>(slot[0]));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The engine's Script SetScript (0x007748D0) on our object: release the old
|
||||
// handler, store a ref of the new one (0 for nil) with the current context.
|
||||
int __fastcall Script_SetScript(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
const int handlerType = Game::Lua::Type(L, 3);
|
||||
if (!Game::Lua::IsString(L, 2) ||
|
||||
(handlerType != Game::Lua::TYPE_FUNCTION && handlerType != Game::Lua::TYPE_NIL &&
|
||||
handlerType != -1 /* none */)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetScript(\"type\", function)", object->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
uint32_t *slot = V_ResolveScript(object, nullptr, Game::Lua::ToString(L, 2));
|
||||
if (slot == nullptr) {
|
||||
Game::Lua::Error(L, "%s doesn't have a \"%s\" script", object->DisplayName(), Game::Lua::ToString(L, 2));
|
||||
return 0;
|
||||
}
|
||||
reinterpret_cast<Release_t>(Offsets::FUN_SCRIPT_RELEASE)(slot);
|
||||
Game::Lua::SetTop(L, 3);
|
||||
const int ref = Game::Lua::Ref(L, Game::Lua::REGISTRY_INDEX);
|
||||
slot[0] = ref > 0 ? static_cast<uint32_t>(ref) : 0;
|
||||
slot[1] = Game::Read<uint32_t>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Frame HookScript's (frame/Modern.cpp) on our object: no handler yet -> the
|
||||
// hook is the handler; else the hooksecurefunc wrapper over (old, hook).
|
||||
int __fastcall Script_HookScript(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2) || Game::Lua::Type(L, 3) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Error(L, "Usage: %s:HookScript(\"type\", function)", object->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::SetTop(L, 3); // (self, type, hook)
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::PushCClosure(L, &Script_GetScript, 0);
|
||||
Game::Lua::Insert(L, -3);
|
||||
Game::Lua::Call(L, 2, 1); // (self, type, hook, old)
|
||||
if (Game::Lua::Type(L, 4) == Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Insert(L, 3); // (self, type, old, hook)
|
||||
HookSecureFunc::PushWrapper(L); // (self, type, wrapper)
|
||||
} else {
|
||||
Game::Lua::SetTop(L, 3);
|
||||
}
|
||||
return Script_SetScript(L);
|
||||
}
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,123 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Animation groups and animations as real 1.12 script objects.
|
||||
//
|
||||
// 4.3.4's CSimpleAnimGroup and CSimpleAnim are CScriptObjects: the engine
|
||||
// hands them to Lua as the same kind of table as a frame (`t[0]` = the
|
||||
// object, metatable `__framescript_meta`), finds their methods through the
|
||||
// object's vtable, resolves `self` with the object's IsA, and stores their
|
||||
// scripts in {handler, context} slots it finds through the object's script
|
||||
// resolver. 1.12 has the same machinery (benilla-re ui/methods, ui/script):
|
||||
// - ConstructScriptObject (0x00701AE0) builds the 0x24-byte header below;
|
||||
// DestructScriptObject (0x00701B40) releases it;
|
||||
// - LockScriptObject / UnlockScriptObject (0x00701BD0 / 0x00701CD0) make and
|
||||
// free the object's table and its name global;
|
||||
// - `__framescript_meta.__index` (0x007020B0) calls vtable+0x08 (L, name)
|
||||
// to push a method; ResolveTypedObject's type test calls vtable+0x10;
|
||||
// - SetScriptObjectScript (0x007025C0) compiles an XML script body into
|
||||
// the slot vtable+0x0C returns, and FrameScriptReleaseScript (0x00702670)
|
||||
// frees a slot.
|
||||
// So an animation object is an engine script object whose vtable is ours.
|
||||
// It has no C++ virtual functions - its first bytes are the engine header -
|
||||
// and its class (Class) carries what differs per type.
|
||||
namespace Animation {
|
||||
|
||||
// The 1.12 script-object header, as ConstructScriptObject fills it.
|
||||
struct ScriptObjectHeader {
|
||||
void **vtable;
|
||||
int32_t holders; // +0x04, LockScriptObject's count
|
||||
int32_t luaRef; // +0x08, the table's registry ref (-2 when none)
|
||||
uint32_t onEvent[2]; // +0x0C, the base OnEvent slot
|
||||
uint32_t events[4]; // +0x14, the event index array
|
||||
};
|
||||
static_assert(sizeof(ScriptObjectHeader) == 0x24, "the engine's script-object header");
|
||||
|
||||
struct Class {
|
||||
const char *typeName; // GetObjectType, IsObjectType
|
||||
const Class *base;
|
||||
// The class's own script names, lower case; a slot per name.
|
||||
const char *const *scriptNames;
|
||||
int scriptCount;
|
||||
const Game::Lua::FrameMethodEntry *methods;
|
||||
int methodCount;
|
||||
int typeId; // from the engine's type counter, on first use
|
||||
int methodsRef; // registry ref of the method table, per Lua state
|
||||
};
|
||||
|
||||
constexpr int kMaxScripts = 8;
|
||||
|
||||
class Object : public ScriptObjectHeader {
|
||||
public:
|
||||
const Class *cls;
|
||||
// The script object this one hangs from: a group's region, an
|
||||
// animation's group (GetParent, "$parent").
|
||||
void *parent;
|
||||
std::string name;
|
||||
uint32_t scripts[kMaxScripts][2] = {}; // {handler ref, context}
|
||||
|
||||
// The object's script slot for `name` (any case), or nullptr.
|
||||
uint32_t *ScriptSlot(const char *name);
|
||||
bool HasScript(int index) const { return scripts[index][0] != 0; }
|
||||
// Runs script `index` with `nargs` values pushed on top of the stack,
|
||||
// and pops them.
|
||||
void RunScript(int index, int nargs);
|
||||
|
||||
const char *Name() const { return name.empty() ? nullptr : name.c_str(); }
|
||||
// While set, a destroyed object leaves the engine alone: its table and
|
||||
// handlers belonged to a Lua state that is gone (/reload leftovers).
|
||||
static bool s_detached;
|
||||
const char *DisplayName() const { return name.empty() ? "<unnamed>" : name.c_str(); }
|
||||
|
||||
protected:
|
||||
// Registers the object's table, under its name when it has one ("$parent"
|
||||
// resolved from `parent`).
|
||||
Object(const Class &cls, void *parent, const char *name);
|
||||
~Object();
|
||||
};
|
||||
|
||||
// The class's type id (the engine's counter, assigned on first use).
|
||||
int TypeId(Class &cls);
|
||||
// The object at `idx` when it is of `cls` or a class derived from it, else
|
||||
// raises the engine's errors (as every method does) and returns nullptr.
|
||||
Object *Resolve(void *L, int idx, Class &cls);
|
||||
// Pushes the object's table.
|
||||
void Push(void *L, Object *object);
|
||||
// Whether a script object is one of ours.
|
||||
bool IsOurs(const void *object);
|
||||
// 4.3.4's CScriptObject::CreateName: "$parent" at the start of `source`
|
||||
// becomes the nearest named ancestor's name, "Top" when none.
|
||||
std::string CreateName(const char *source, const void *parent);
|
||||
|
||||
// Builds every class's method table in the current Lua state.
|
||||
void RegisterClass(Class &cls);
|
||||
|
||||
// The base methods every class has (4.3.4's CScriptObject methods), and the
|
||||
// script methods groups and animations share.
|
||||
int __fastcall Script_GetObjectType(void *L);
|
||||
int __fastcall Script_IsObjectType(void *L);
|
||||
int __fastcall Script_GetName(void *L);
|
||||
int __fastcall Script_GetParent(void *L);
|
||||
int __fastcall Script_HasScript(void *L);
|
||||
int __fastcall Script_GetScript(void *L);
|
||||
int __fastcall Script_SetScript(void *L);
|
||||
int __fastcall Script_HookScript(void *L);
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,501 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 4.3.4's CSimplePathAnim and CSimpleControlPoint (wowdecomp15595
|
||||
// CSimplePathAnim.cpp, CSimpleControlPoint.cpp and their _Scripts.cpp).
|
||||
//
|
||||
// A Path moves the region through its control points: each point is an
|
||||
// offset from where the region is, the points are spread evenly over the
|
||||
// animation's progress (point i of n at (i + 1) / n), and the path starts
|
||||
// at offset 0. Between points the offset is a straight line, or with the
|
||||
// SMOOTH curve a Catmull-Rom spline. The offset goes through the same
|
||||
// AddAnimTranslation as a Translation.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
#include "animation/Xml.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "baselib/Ascii.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
// ---- CSimpleControlPoint ------------------------------------------------------------
|
||||
|
||||
ControlPoint::ControlPoint(Anim *path, const char *name) : Object(s_class, path, name) {
|
||||
if (path != nullptr)
|
||||
path->controlPoints.push_back(this);
|
||||
}
|
||||
|
||||
ControlPoint::~ControlPoint() = default;
|
||||
|
||||
void ControlPoint::SetOffset(float x, float y) {
|
||||
if (x * x + y * y <= 0.00000023841858f) {
|
||||
offsetX = 0.0f;
|
||||
offsetY = 0.0f;
|
||||
} else {
|
||||
offsetX = x;
|
||||
offsetY = y;
|
||||
}
|
||||
}
|
||||
|
||||
void ControlPoint::SetOrder(int value, bool notify, bool wrap) {
|
||||
const int oldOrder = order;
|
||||
if (wrap)
|
||||
order = value < 0 || value > 99 ? 0 : value;
|
||||
else
|
||||
order = value < 0 ? 0 : (value > 99 ? 99 : value);
|
||||
if (parent != nullptr && notify && oldOrder != order)
|
||||
GetPath()->NotifyControlPointOrderChanged(this);
|
||||
}
|
||||
|
||||
bool ControlPoint::ChangeParent(Anim *path) {
|
||||
if (path == nullptr || GetPath() == path)
|
||||
return false;
|
||||
// LinkToTail unlinks the point from the old path's list first.
|
||||
Anim *old = GetPath();
|
||||
if (old != nullptr)
|
||||
old->controlPoints.erase(std::remove(old->controlPoints.begin(), old->controlPoints.end(), this),
|
||||
old->controlPoints.end());
|
||||
path->controlPoints.push_back(this);
|
||||
if (old != nullptr)
|
||||
old->NotifyControlPointRemoved(this);
|
||||
parent = path;
|
||||
path->NotifyControlPointAdded(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- CSimplePathAnim -------------------------------------------------------------------
|
||||
|
||||
int Anim::GetMaxControlPointOrder() const { return sortedPoints.empty() ? -1 : sortedPoints.back()->order; }
|
||||
|
||||
int Anim::FindControlPoint(int value) const {
|
||||
if (value == -1)
|
||||
return -1;
|
||||
int low = 0;
|
||||
int high = static_cast<int>(sortedPoints.size()) - 1;
|
||||
while (low <= high) {
|
||||
const int middle = static_cast<int>(static_cast<unsigned>(high + low) >> 1);
|
||||
const ControlPoint *point = sortedPoints[middle];
|
||||
if (point == nullptr)
|
||||
break; // 4.3.4 spins here; the array has no holes after BuildSpline
|
||||
if (point->order < value)
|
||||
low = middle + 1;
|
||||
else if (point->order > value)
|
||||
high = middle - 1;
|
||||
else
|
||||
return middle;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void Anim::UpdateControlPointPositions() {
|
||||
const size_t count = sortedPoints.size();
|
||||
if (count == 0)
|
||||
return;
|
||||
const float step = 1.0f / static_cast<float>(count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
float position = static_cast<float>(i + 1) * step;
|
||||
position = position < 0.0f ? 0.0f : (position >= 1.0f ? 1.0f : position);
|
||||
sortedPoints[i]->position = position;
|
||||
}
|
||||
}
|
||||
|
||||
int Anim::RegisterSplineControlPoint(ControlPoint *point) {
|
||||
const int count = static_cast<int>(sortedPoints.size());
|
||||
if (count == 0) {
|
||||
sortedPoints.push_back(point);
|
||||
return 0;
|
||||
}
|
||||
int index = count;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
ControlPoint *existing = sortedPoints[i];
|
||||
if (existing == nullptr) {
|
||||
sortedPoints[i] = point;
|
||||
return i;
|
||||
}
|
||||
if (point->order == existing->order)
|
||||
return i;
|
||||
if (point->order < existing->order) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
sortedPoints.insert(sortedPoints.begin() + index, point);
|
||||
return index;
|
||||
}
|
||||
|
||||
void Anim::BuildSpline() {
|
||||
Stop(false);
|
||||
if (controlPoints.empty()) {
|
||||
sortedPoints.clear();
|
||||
return;
|
||||
}
|
||||
std::fill(sortedPoints.begin(), sortedPoints.end(), nullptr);
|
||||
// Points take their order in link order; a point without one, or with
|
||||
// one below the next free order, takes the next free order.
|
||||
int nextOrder = 0;
|
||||
for (ControlPoint *point : controlPoints) {
|
||||
int value = point->order;
|
||||
if (value == -1 || value < nextOrder) {
|
||||
value = nextOrder++;
|
||||
point->order = value < 0 ? 0 : value;
|
||||
} else {
|
||||
nextOrder = value + 1;
|
||||
}
|
||||
if (nextOrder > 99)
|
||||
nextOrder = 0;
|
||||
RegisterSplineControlPoint(point);
|
||||
}
|
||||
while (!sortedPoints.empty() && sortedPoints.back() == nullptr)
|
||||
sortedPoints.pop_back();
|
||||
}
|
||||
|
||||
void Anim::NotifyControlPointAdded(ControlPoint *point) {
|
||||
if (point == nullptr)
|
||||
return;
|
||||
const int index = FindControlPoint(point->order);
|
||||
if (index < 0) {
|
||||
if (RegisterSplineControlPoint(point) >= 0)
|
||||
UpdateControlPointPositions();
|
||||
return;
|
||||
}
|
||||
// The order is taken: the point goes before the one that has it and
|
||||
// takes the order; BuildSpline moves the rest up.
|
||||
ControlPoint *existing = sortedPoints[index];
|
||||
controlPoints.erase(std::remove(controlPoints.begin(), controlPoints.end(), point), controlPoints.end());
|
||||
controlPoints.insert(std::find(controlPoints.begin(), controlPoints.end(), existing), point);
|
||||
BuildSpline();
|
||||
UpdateControlPointPositions();
|
||||
}
|
||||
|
||||
void Anim::NotifyControlPointRemoved(ControlPoint *point) {
|
||||
if (point != nullptr && FindControlPoint(point->order) >= 0) {
|
||||
BuildSpline();
|
||||
UpdateControlPointPositions();
|
||||
}
|
||||
}
|
||||
|
||||
void Anim::NotifyControlPointOrderChanged(ControlPoint *point) {
|
||||
if (point == nullptr)
|
||||
return;
|
||||
const int index = FindControlPoint(point->order);
|
||||
if (index >= 0 && sortedPoints[index] != point) {
|
||||
ControlPoint *existing = sortedPoints[index];
|
||||
controlPoints.erase(std::remove(controlPoints.begin(), controlPoints.end(), point), controlPoints.end());
|
||||
controlPoints.insert(std::find(controlPoints.begin(), controlPoints.end(), existing), point);
|
||||
}
|
||||
BuildSpline();
|
||||
UpdateControlPointPositions();
|
||||
}
|
||||
|
||||
void Anim::ApplyPath(float progress) {
|
||||
const int count = static_cast<int>(sortedPoints.size());
|
||||
if (count == 0)
|
||||
return;
|
||||
void *region = GetRegionParent();
|
||||
if (region == nullptr)
|
||||
return;
|
||||
|
||||
const float t = std::fabs(progress);
|
||||
// The segment: 4.3.4 converts count * t with the 2^16 + x float trick,
|
||||
// which rounds to 1/128 before it truncates. Kept, so a progress just
|
||||
// under a point's lands on the same segment.
|
||||
const float scaled = static_cast<float>(count) * t;
|
||||
const float biased = std::fabs(scaled) + 65536.0f;
|
||||
int32_t bits;
|
||||
std::memcpy(&bits, &biased, sizeof(bits));
|
||||
uint32_t scaledBits;
|
||||
std::memcpy(&scaledBits, &scaled, sizeof(scaledBits));
|
||||
const int32_t sign = static_cast<int32_t>(scaledBits >> 31);
|
||||
const int index = static_cast<int16_t>((((bits >> 7) & 0x7FFF) ^ sign) - sign);
|
||||
|
||||
float x = 0.0f, y = 0.0f;
|
||||
const auto offsetOf = [this](int i, float &ox, float &oy) {
|
||||
ox = sortedPoints[i]->offsetX;
|
||||
oy = sortedPoints[i]->offsetY;
|
||||
};
|
||||
if (static_cast<unsigned>(index) >= static_cast<unsigned>(count)) {
|
||||
offsetOf(count - 1, x, y);
|
||||
} else if (curve && count > 1) {
|
||||
// Catmull-Rom through p1 -> p2; the start is offset 0.
|
||||
float p0x = 0.0f, p0y = 0.0f, p1x = 0.0f, p1y = 0.0f, p2x, p2y, p3x, p3y, t0, t1;
|
||||
if (index != 0) {
|
||||
offsetOf(index - 1, p1x, p1y);
|
||||
t0 = sortedPoints[index - 1]->position;
|
||||
offsetOf(index, p2x, p2y);
|
||||
t1 = sortedPoints[index]->position;
|
||||
if (index != 1)
|
||||
offsetOf(index - 2, p0x, p0y);
|
||||
if (index == count - 1) {
|
||||
p3x = p2x * 2.0f - p1x;
|
||||
p3y = p2y * 2.0f - p1y;
|
||||
} else {
|
||||
offsetOf(index + 1, p3x, p3y);
|
||||
}
|
||||
} else {
|
||||
t0 = 0.0f;
|
||||
offsetOf(0, p2x, p2y);
|
||||
t1 = sortedPoints[0]->position;
|
||||
p0x = -p2x;
|
||||
p0y = -p2y;
|
||||
offsetOf(1, p3x, p3y);
|
||||
}
|
||||
const float f = (t - t0) / (t1 - t0);
|
||||
const float f2 = f * f;
|
||||
const float f3 = f2 * f;
|
||||
const float ax = -p0x + p1x * 3.0f - p2x * 3.0f + p3x;
|
||||
const float ay = -p0y + p1y * 3.0f - p2y * 3.0f + p3y;
|
||||
const float bx = p0x * 2.0f - p1x * 5.0f + p2x * 4.0f - p3x;
|
||||
const float by = p0y * 2.0f - p1y * 5.0f + p2y * 4.0f - p3y;
|
||||
const float cx = -p0x + p2x;
|
||||
const float cy = -p0y + p2y;
|
||||
x = (cx * f + p1x * 2.0f + bx * f2 + ax * f3) * 0.5f;
|
||||
y = (cy * f + p1y * 2.0f + by * f2 + ay * f3) * 0.5f;
|
||||
} else {
|
||||
float ax = 0.0f, ay = 0.0f, bx, by, t0;
|
||||
if (index != 0) {
|
||||
offsetOf(index - 1, ax, ay);
|
||||
t0 = sortedPoints[index - 1]->position;
|
||||
} else {
|
||||
t0 = 0.0f;
|
||||
}
|
||||
offsetOf(index, bx, by);
|
||||
const float t1 = sortedPoints[index]->position;
|
||||
const float f = (t - t0) / (t1 - t0);
|
||||
x = ax + (bx - ax) * f;
|
||||
y = f * (by - ay) + ay;
|
||||
}
|
||||
|
||||
if (progress >= 0.0f)
|
||||
Region::AddAnimTranslation(region, target, x, y);
|
||||
else
|
||||
Region::AddAnimTranslation(region, target, -x, -y);
|
||||
}
|
||||
|
||||
// ---- Lua methods ---------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
Anim *PathSelf(void *L) { return static_cast<Anim *>(Resolve(L, 1, Anim::s_pathClass)); }
|
||||
ControlPoint *PointSelf(void *L) { return static_cast<ControlPoint *>(Resolve(L, 1, ControlPoint::s_class)); }
|
||||
|
||||
// LoadXML.cpp's s_curveTypes.
|
||||
bool StringToCurve(const char *text, bool &curve) {
|
||||
if (Ascii::EqualCI(text, "NONE")) {
|
||||
curve = false;
|
||||
return true;
|
||||
}
|
||||
if (Ascii::EqualCI(text, "SMOOTH")) {
|
||||
curve = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetCurve(void *L) {
|
||||
Anim *path = PathSelf(L);
|
||||
if (path == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetCurve(\"curveType\")", path->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
if (!StringToCurve(Game::Lua::ToString(L, 2), path->curve))
|
||||
Game::Lua::Error(L, "%s:SetCurve(): Unknown curve type specified", path->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCurve(void *L) {
|
||||
Anim *path = PathSelf(L);
|
||||
if (path == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, path->curve ? "SMOOTH" : "NONE");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMaxOrder(void *L) {
|
||||
Anim *path = PathSelf(L);
|
||||
if (path == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, path->GetMaxControlPointOrder() + 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetControlPoints(void *L) {
|
||||
Anim *path = PathSelf(L);
|
||||
if (path == nullptr)
|
||||
return 0;
|
||||
const int count = static_cast<int>(path->sortedPoints.size());
|
||||
if (count == 0)
|
||||
return 0;
|
||||
if (!Game::Lua::CheckStack(L, count)) {
|
||||
Game::Lua::Error(L, "%s:GetControlPoints(): Stack overflow", path->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
for (ControlPoint *point : path->sortedPoints)
|
||||
Push(L, point);
|
||||
return count;
|
||||
}
|
||||
|
||||
int __fastcall Script_CreateControlPoint(void *L) {
|
||||
Anim *path = PathSelf(L);
|
||||
if (path == nullptr)
|
||||
return 0;
|
||||
const char *name = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
const void *templateNode = nullptr;
|
||||
if (Game::Lua::Type(L, 3) == Game::Lua::TYPE_STRING) {
|
||||
const char *inherits = Game::Lua::ToString(L, 3);
|
||||
templateNode = Xml::Template(inherits);
|
||||
if (templateNode == nullptr) {
|
||||
Game::Lua::Error(L, "%s:CreateControlPoint(): Couldn't find inherited node \"%s\"", path->DisplayName(),
|
||||
inherits);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
ControlPoint *point = new ControlPoint(path, name);
|
||||
if (templateNode != nullptr)
|
||||
Xml::LoadControlPoint(point, templateNode, nullptr);
|
||||
if (Game::Lua::IsNumber(L, 4))
|
||||
point->SetOrder(static_cast<int>(Game::Lua::ToNumber(L, 4)) - 1, false, false);
|
||||
// The decompiled CreateControlPoint passes the highest order itself,
|
||||
// which takes the last point's place and reverses points made one after
|
||||
// another; one past it, as SetParent does, puts the point last.
|
||||
if (point->order == -1)
|
||||
point->SetOrder(path->GetMaxControlPointOrder() + 1, false, true);
|
||||
path->NotifyControlPointAdded(point);
|
||||
Push(L, point);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_PointSetParent(void *L) {
|
||||
ControlPoint *point = PointSelf(L);
|
||||
if (point == nullptr)
|
||||
return 0;
|
||||
if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_NIL) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Cannot set a 'nil' parent for control points", point->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
const uintptr_t pathType = reinterpret_cast<uintptr_t>(&Anim::s_pathClass.typeId);
|
||||
Anim *path = nullptr;
|
||||
if (Game::Lua::IsString(L, 2)) {
|
||||
// CScriptObject::GetScriptObjectByName(name, Path).
|
||||
Game::Lua::PushString(L, Game::Lua::ToString(L, 2));
|
||||
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
|
||||
void *object = Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE ? Game::Lua::ResolveObject(L, -1) : nullptr;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (object != nullptr && IsOurs(object) && Game::Lua::IsA(object, pathType))
|
||||
path = static_cast<Anim *>(object);
|
||||
} else if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_TABLE) {
|
||||
void *object = Game::Lua::ResolveObject(L, 2);
|
||||
if (object == nullptr) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Couldn't find 'this' in parent object", point->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
if (!IsOurs(object) || !Game::Lua::IsA(object, pathType)) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Wrong parent object type, expected Path", point->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
path = static_cast<Anim *>(object);
|
||||
}
|
||||
if (path == nullptr) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Couldn't find Path named '%s'", point->DisplayName(),
|
||||
Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : "");
|
||||
return 0;
|
||||
}
|
||||
const int oldOrder = point->order;
|
||||
if (Game::Lua::IsNumber(L, 3)) {
|
||||
point->SetOrder(static_cast<int>(Game::Lua::ToNumber(L, 3)) - 1, false, false);
|
||||
} else {
|
||||
int value = point->order;
|
||||
if (value == -1)
|
||||
value = path->GetMaxControlPointOrder() + 1;
|
||||
point->SetOrder(value, false, true);
|
||||
}
|
||||
if (!point->ChangeParent(path) && oldOrder != point->order)
|
||||
path->NotifyControlPointOrderChanged(point);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_PointSetOffset(void *L) {
|
||||
ControlPoint *point = PointSelf(L);
|
||||
if (point == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetOffset(x, y)", point->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
point->SetOffset(Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 2))),
|
||||
Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 3))));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_PointGetOffset(void *L) {
|
||||
ControlPoint *point = PointSelf(L);
|
||||
if (point == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(point->offsetX));
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(point->offsetY));
|
||||
return 2;
|
||||
}
|
||||
|
||||
int __fastcall Script_PointSetOrder(void *L) {
|
||||
ControlPoint *point = PointSelf(L);
|
||||
if (point == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetOrder(order)", point->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
point->SetOrder(static_cast<int>(Game::Lua::ToNumber(L, 2)) - 1, true, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_PointGetOrder(void *L) {
|
||||
ControlPoint *point = PointSelf(L);
|
||||
if (point == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, point->order + 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kPathMethods[] = {
|
||||
{"SetCurve", &Script_SetCurve},
|
||||
{"GetCurve", &Script_GetCurve},
|
||||
{"GetControlPoints", &Script_GetControlPoints},
|
||||
{"CreateControlPoint", &Script_CreateControlPoint},
|
||||
{"GetMaxOrder", &Script_GetMaxOrder},
|
||||
};
|
||||
|
||||
// CScriptObject's methods, then CSimpleControlPoint's.
|
||||
const Game::Lua::FrameMethodEntry kPointMethods[] = {
|
||||
{"GetObjectType", &Script_GetObjectType}, {"IsObjectType", &Script_IsObjectType},
|
||||
{"GetName", &Script_GetName}, {"GetParent", &Script_GetParent},
|
||||
{"SetParent", &Script_PointSetParent}, {"SetOffset", &Script_PointSetOffset},
|
||||
{"GetOffset", &Script_PointGetOffset}, {"SetOrder", &Script_PointSetOrder},
|
||||
{"GetOrder", &Script_PointGetOrder},
|
||||
};
|
||||
|
||||
template <size_t N> constexpr int Count(const Game::Lua::FrameMethodEntry (&)[N]) { return static_cast<int>(N); }
|
||||
|
||||
} // namespace
|
||||
|
||||
Class Anim::s_pathClass{"Path", &Anim::s_class, nullptr, 0, kPathMethods, Count(kPathMethods), 0, 0};
|
||||
Class ControlPoint::s_class{"ControlPoint", nullptr, nullptr, 0, kPointMethods, Count(kPointMethods), 0, 0};
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,833 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The region side of the animation system, and the region methods
|
||||
// CreateAnimationGroup / GetAnimationGroups / StopAnimating.
|
||||
//
|
||||
// 4.3.4 (wowdecomp15595 CScriptRegion.cpp, CSimpleFrame.cpp, CSimpleRegion.cpp,
|
||||
// CSimpleRender.cpp, CLayoutFrame.cpp):
|
||||
// - a region owns its groups (m_animGroups) and lists the playing ones
|
||||
// (m_playingAnimGroups);
|
||||
// - CSimpleFrame::OnLayerUpdate runs OnUpdate, then every group of the
|
||||
// frame, then every group of each of its regions;
|
||||
// - when a group starts, the region stores the state its animations change
|
||||
// (AnimActivated: a frame its own alpha, a texture or font string its
|
||||
// vertex alpha), unless an earlier group already did; a frame does this
|
||||
// for every frame and region below it too;
|
||||
// - every update first puts that state back (PreOnAnimUpdate), then the
|
||||
// animations add to it (AddAnimAlpha: a frame's own alpha becomes
|
||||
// change + its drawn alpha; a region's vertex alpha change + its own);
|
||||
// - when the last group with that kind of animation on the region or any
|
||||
// ancestor ends, the stored state goes back (AnimDeactivated).
|
||||
// So, as in 4.3.4, GetAlpha returns the animated alpha while it plays.
|
||||
//
|
||||
// Position animations (Translation, Scale, Rotation) move only what
|
||||
// is DRAWN, never a frame's rect: a frame hands them to every texture and font
|
||||
// string below it. A texture moves the four corners its draw record points at
|
||||
// (+0xD4, TL BL TR BR, the order of 4.3.4's m_positions); a font string moves
|
||||
// its text block's string position, and is turned and scaled about its origin
|
||||
// when its glyphs are baked (4.3.4 bakes them again whenever the string's
|
||||
// rotation or scale changes; so does this, through the engine's node
|
||||
// invalidate). The glyphs are scaled, not drawn at a larger font size: one
|
||||
// size per update would fill the glyph cache. Their stored base is what the engine
|
||||
// itself lays out (4.3.4's StoreAnimPosition): a texture's corners from its
|
||||
// rect through the engine's corner store, a font string's inset x layout
|
||||
// scale + its rect's min corner. When the layout moves a region mid-animation,
|
||||
// the co-hooks on the two OnFrameSizeChanged callbacks move the base and keep
|
||||
// the animation's offset, as 4.3.4's own OnFrameSizeChanged legs do.
|
||||
//
|
||||
// 1.12's regions have no room for these, so they are kept here, keyed by the
|
||||
// region. A frame's own alpha is Frame::Alpha's (3.x's m_alpha), set through
|
||||
// the frame's vtable setter as SetAlpha sets it; a region's vertex alpha is the
|
||||
// engine's region color (FUN_REGION_GET_COLOR / FUN_FONTSTRING_SET_COLOR).
|
||||
// The groups update from a co-hook of CSimpleFrame::OnFrameUpdate, which the
|
||||
// engine calls for every shown frame after OnUpdate, so a hidden frame's
|
||||
// groups wait, as in 4.3.4. Groups die with their frame (Frame::Destruction).
|
||||
|
||||
#include "animation/Region.h"
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Xml.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Alpha.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "texture/Transform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Animation::Region {
|
||||
|
||||
namespace {
|
||||
|
||||
struct State {
|
||||
std::vector<Group *> groups; // m_animGroups (owned)
|
||||
std::vector<Group *> playing; // m_playingAnimGroups
|
||||
};
|
||||
|
||||
// CAnimationData.
|
||||
struct AnimData {
|
||||
bool hasAlpha = false;
|
||||
uint8_t alpha = 0xFF;
|
||||
bool hasPosition = false;
|
||||
// m_position[4]: a texture's four corners {x, y, z}; a font string uses
|
||||
// the first as its string position.
|
||||
float position[12] = {};
|
||||
// A font string's animated rotation and scale (4.3.4's m_rotation /
|
||||
// m_scale over the animation base), and what its glyphs were last baked
|
||||
// with.
|
||||
float rotation = 0.0f;
|
||||
float scale = 1.0f;
|
||||
float bakedRotation = 0.0f;
|
||||
float bakedScale = 1.0f;
|
||||
};
|
||||
|
||||
std::unordered_map<void *, State> g_states;
|
||||
std::unordered_map<void *, AnimData> g_data;
|
||||
// Font strings whose rotation or scale an update reset: checked against what
|
||||
// they were baked with once the update has run.
|
||||
std::vector<void *> g_touchedStrings;
|
||||
// Regions (not frames) with groups: their frame's update has to look at its
|
||||
// regions.
|
||||
int g_regionsWithGroups = 0;
|
||||
|
||||
using FrameUpdate_t = void(__fastcall *)(void *frame, void *edx, float elapsed);
|
||||
using SetFrameAlpha_t = void(__fastcall *)(void *frame, void *edx, uint32_t alpha);
|
||||
using GetColor_t = void(__fastcall *)(void *region, void *edx, uint32_t *bgra);
|
||||
using SetColor_t = void(__fastcall *)(void *region, void *edx, const uint32_t *bgra);
|
||||
using StoreCorners_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
using Crop_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
using SetStringPos_t = void(__fastcall *)(void *block, const float *position);
|
||||
using NdcToDdc_t = void(__fastcall *)(float *outX, float *outY, float x, float y);
|
||||
using SizeChanged_t = void(__fastcall *)(void *layout, void *edx, const float *oldRect);
|
||||
using InvalidateNode_t = void(__fastcall *)(void *node);
|
||||
|
||||
FrameUpdate_t g_origFrameUpdate = nullptr;
|
||||
SizeChanged_t g_origTextureSizeChanged = nullptr;
|
||||
SizeChanged_t g_origFontStringSizeChanged = nullptr;
|
||||
|
||||
bool IsFrame(void *region) { return Game::Lua::IsA(region, Offsets::VAR_FRAME_LUA_TYPE_ID); }
|
||||
bool IsTexture(void *region) { return Game::Lua::IsA(region, Offsets::VAR_TEXTURE_LUA_TYPE_ID); }
|
||||
bool IsFontString(void *region) { return Game::Lua::IsA(region, Offsets::VAR_FONTSTRING_LUA_TYPE_ID); }
|
||||
void *ParentOf(void *region) { return Game::Read<void *>(region, Offsets::OFF_REGION_PARENT); }
|
||||
|
||||
State *Find(void *region) {
|
||||
const auto it = g_states.find(region);
|
||||
return it != g_states.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
AnimData *DataOf(void *region) {
|
||||
const auto it = g_data.find(region);
|
||||
return it != g_data.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
template <typename Fn> void ForEachChild(void *frame, Fn fn) {
|
||||
Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_CHILD_LIST, fn);
|
||||
Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_REGION_LIST, fn);
|
||||
}
|
||||
|
||||
// ---- The state alpha animations change -----------------------------------------
|
||||
|
||||
uint8_t OwnAlpha(void *frame) { return Frame::Alpha::OwnAlpha(frame); }
|
||||
|
||||
void SetOwnAlpha(void *frame, uint8_t alpha) {
|
||||
void **vtable = *static_cast<void ***>(frame);
|
||||
reinterpret_cast<SetFrameAlpha_t>(vtable[Offsets::VTBL_FRAME_SET_FRAME_ALPHA / sizeof(void *)])(frame, nullptr,
|
||||
alpha);
|
||||
}
|
||||
|
||||
uint8_t DrawnParentAlpha(void *frame) {
|
||||
void *parent = ParentOf(frame);
|
||||
return parent != nullptr ? Game::Read<uint8_t>(parent, Offsets::OFF_FRAME_ALPHA) : 0xFF;
|
||||
}
|
||||
|
||||
uint32_t VertexColor(void *region) {
|
||||
uint32_t bgra = 0xFFFFFFFF;
|
||||
reinterpret_cast<GetColor_t>(Offsets::FUN_REGION_GET_COLOR)(region, nullptr, &bgra);
|
||||
return bgra;
|
||||
}
|
||||
|
||||
void SetVertexAlpha(void *region, uint8_t alpha) {
|
||||
const uint32_t bgra = (VertexColor(region) & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
|
||||
reinterpret_cast<SetColor_t>(Offsets::FUN_FONTSTRING_SET_COLOR)(region, nullptr, &bgra);
|
||||
}
|
||||
|
||||
// ---- The state position animations change --------------------------------------
|
||||
|
||||
float *Corners(void *texture) { return Game::Ptr<float>(texture, Offsets::OFF_SIMPLETEXTURE_CORNERS); }
|
||||
|
||||
// CSimpleTexture::SetPosition(m_rect, m_positions): the corners from the rect,
|
||||
// written the way the texture's layout callback (FUN_TEXTURE_ON_FRAME_SIZE_CHANGED)
|
||||
// writes them - cropped when SetTexCoordModifiesRect is on - so the corner
|
||||
// store's co-hooks (Texture::Transform) shape them as they do on a layout.
|
||||
void StoreTextureCorners(void *texture) {
|
||||
float rect[4];
|
||||
std::memcpy(rect, Game::Ptr<float>(texture, Offsets::OFF_REGION_RECT), sizeof(rect));
|
||||
if (Game::Read<int>(texture, Offsets::OFF_REGION_TEXCOORD_MODIFIES_RECT) != 0)
|
||||
reinterpret_cast<Crop_t>(Offsets::FUN_REGION_TEXCOORD_CROP)(texture, nullptr, rect);
|
||||
reinterpret_cast<StoreCorners_t>(Offsets::FUN_REGION_STORE_CORNERS)(texture, nullptr, rect);
|
||||
}
|
||||
|
||||
void *StringBlock(void *fontString) { return Game::Read<void *>(fontString, Offsets::OFF_FONTSTRING_TEXT_BLOCK); }
|
||||
|
||||
// Where the engine lays the string out: inset x layout scale + the rect's min
|
||||
// corner (the position FUN_FONTSTRING_ON_FRAME_SIZE_CHANGED and SetTextOffset
|
||||
// pass), as 4.3.4's StoreAnimPosition computes it.
|
||||
void StringBase(void *fontString, float out[3]) {
|
||||
const float *rect = Game::Ptr<const float>(fontString, Offsets::OFF_REGION_RECT);
|
||||
const float scale = Game::Read<float>(fontString, Offsets::OFF_LAYOUT_SCALE);
|
||||
out[0] = Game::Read<float>(fontString, Offsets::OFF_FONTSTRING_INSET_X) * scale + rect[1];
|
||||
out[1] = Game::Read<float>(fontString, Offsets::OFF_FONTSTRING_INSET_Y) * scale + rect[0];
|
||||
out[2] = 0.0f;
|
||||
}
|
||||
|
||||
// TextBlockGetStringPos: the string node's position, back in DDC.
|
||||
bool GetStringPos(void *block, float out[3]) {
|
||||
const void *node = Game::Read<void *>(block, Offsets::OFF_TEXTBLOCK_NODE);
|
||||
if (node == nullptr)
|
||||
return false;
|
||||
const float *position = Game::Ptr<const float>(node, Offsets::OFF_TEXT_NODE_POSITION);
|
||||
reinterpret_cast<NdcToDdc_t>(Offsets::FUN_COORD_NDC_TO_DDC)(&out[0], &out[1], position[0], position[1]);
|
||||
out[2] = position[2];
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetStringPos(void *block, const float position[3]) {
|
||||
reinterpret_cast<SetStringPos_t>(Offsets::FUN_TEXTBLOCK_SET_STRING_POS)(block, position);
|
||||
}
|
||||
|
||||
// CSimpleTexture / CSimpleFontString::StoreAnimPosition.
|
||||
void StoreAnimPosition(void *region, AnimData &data) {
|
||||
if (IsTexture(region)) {
|
||||
StoreTextureCorners(region);
|
||||
std::memcpy(data.position, Corners(region), sizeof(data.position));
|
||||
data.hasPosition = true;
|
||||
} else if (IsFontString(region)) {
|
||||
StringBase(region, data.position);
|
||||
data.hasPosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
void *StringNode(void *fontString) {
|
||||
void *block = StringBlock(fontString);
|
||||
return block != nullptr ? Game::Read<void *>(block, Offsets::OFF_TEXTBLOCK_NODE) : nullptr;
|
||||
}
|
||||
|
||||
// GxuFontStringInvalidate after GxuFontSetRotation / GxuFontSetScale: the
|
||||
// glyphs are baked again on the next paint, where FontStringBaked turns and
|
||||
// scales them. Only when the values differ from the baked ones.
|
||||
void RebakeIfChanged(void *fontString, const AnimData &data) {
|
||||
if (data.rotation == data.bakedRotation && data.scale == data.bakedScale)
|
||||
return;
|
||||
if (void *node = StringNode(fontString))
|
||||
reinterpret_cast<InvalidateNode_t>(Offsets::FUN_TEXT_NODE_INVALIDATE)(node);
|
||||
}
|
||||
|
||||
// AnimData_ResetPositionData: the region goes back to where the layout puts it.
|
||||
void ResetPosition(void *region, AnimData &data) {
|
||||
data.hasPosition = false;
|
||||
if (IsTexture(region)) {
|
||||
StoreTextureCorners(region);
|
||||
} else if (IsFontString(region)) {
|
||||
// 4.3.4 leaves the string where the last update put it until the next
|
||||
// layout; it goes back now, as the corners of a texture do.
|
||||
if (void *block = StringBlock(region)) {
|
||||
float base[3];
|
||||
StringBase(region, base);
|
||||
SetStringPos(block, base);
|
||||
}
|
||||
data.rotation = 0.0f;
|
||||
data.scale = 1.0f;
|
||||
RebakeIfChanged(region, data);
|
||||
}
|
||||
}
|
||||
|
||||
// CSimpleFrame / CSimpleRegion::AnimActivated.
|
||||
void AnimActivated(void *region, bool alpha, bool position) {
|
||||
const bool frame = IsFrame(region);
|
||||
AnimData &data = g_data[region]; // AnimData_Init
|
||||
if (alpha) {
|
||||
data.hasAlpha = true;
|
||||
data.alpha = frame ? OwnAlpha(region) : static_cast<uint8_t>(VertexColor(region) >> 24);
|
||||
}
|
||||
// A frame stores no position of its own, but it is marked, so the end of
|
||||
// its last position group resets everything below it (NotifyEnd reads
|
||||
// the mark), as the end of an alpha group does.
|
||||
if (position) {
|
||||
if (frame)
|
||||
data.hasPosition = true;
|
||||
else
|
||||
StoreAnimPosition(region, data);
|
||||
}
|
||||
if (frame)
|
||||
ForEachChild(region, [alpha, position](void *child) { AnimActivated(child, alpha, position); });
|
||||
}
|
||||
|
||||
// CSimpleFrame / CSimpleRegion::AnimDeactivated.
|
||||
void AnimDeactivated(void *region, bool alpha, bool position) {
|
||||
const bool frame = IsFrame(region);
|
||||
AnimData *data = DataOf(region);
|
||||
if (alpha) {
|
||||
const uint8_t stored = data != nullptr ? data->alpha : 0xFF; // AnimData_GetAlpha
|
||||
if (frame) {
|
||||
if (stored != OwnAlpha(region))
|
||||
SetOwnAlpha(region, stored);
|
||||
} else {
|
||||
SetVertexAlpha(region, stored);
|
||||
}
|
||||
if (data != nullptr) {
|
||||
data->hasAlpha = false;
|
||||
data->alpha = 0xFF;
|
||||
}
|
||||
}
|
||||
if (position && data != nullptr) {
|
||||
if (frame)
|
||||
data->hasPosition = false;
|
||||
else
|
||||
ResetPosition(region, *data);
|
||||
}
|
||||
if (frame)
|
||||
ForEachChild(region, [alpha, position](void *child) { AnimDeactivated(child, alpha, position); });
|
||||
}
|
||||
|
||||
// CScriptRegion::HasActiveAlphaAnim / HasActivePositionAnim: a playing group
|
||||
// with that kind of animation on the region or an ancestor.
|
||||
template <typename Has> bool HasActiveAnim(void *region, Has has) {
|
||||
for (void *r = region; r != nullptr; r = ParentOf(r)) {
|
||||
const State *state = Find(r);
|
||||
if (state == nullptr)
|
||||
continue;
|
||||
for (const Group *group : state->playing)
|
||||
if (has(group))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- The update ----------------------------------------------------------------------
|
||||
|
||||
void UpdateGroups(void *region, float elapsed) {
|
||||
State *state = Find(region);
|
||||
if (state == nullptr)
|
||||
return;
|
||||
const std::vector<Group *> groups = state->groups;
|
||||
for (Group *group : groups)
|
||||
group->OnUpdate(elapsed);
|
||||
}
|
||||
|
||||
void __fastcall FrameUpdate_h(void *frame, void *edx, float elapsed) {
|
||||
g_origFrameUpdate(frame, edx, elapsed);
|
||||
if (g_states.empty())
|
||||
return;
|
||||
UpdateGroups(frame, elapsed);
|
||||
if (g_regionsWithGroups > 0)
|
||||
Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_REGION_LIST,
|
||||
[elapsed](void *region) { UpdateGroups(region, elapsed); });
|
||||
// The font strings this update turned or scaled, or put back.
|
||||
for (void *fontString : g_touchedStrings)
|
||||
if (const AnimData *data = DataOf(fontString))
|
||||
RebakeIfChanged(fontString, *data);
|
||||
g_touchedStrings.clear();
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _frameUpdateHook{Offsets::FUN_FRAME_ON_UPDATE, reinterpret_cast<void *>(&FrameUpdate_h),
|
||||
reinterpret_cast<void **>(&g_origFrameUpdate)};
|
||||
|
||||
// ---- A layout during a position animation -----------------------------------------
|
||||
//
|
||||
// The layout writes the region's unanimated position. As in 4.3.4's
|
||||
// OnFrameSizeChanged legs, the stored base becomes that new position and the
|
||||
// drawn position keeps the animation's offset from it.
|
||||
|
||||
AnimData *PositionData(void *region) {
|
||||
if (g_data.empty())
|
||||
return nullptr;
|
||||
AnimData *data = DataOf(region);
|
||||
return data != nullptr && data->hasPosition ? data : nullptr;
|
||||
}
|
||||
|
||||
void __fastcall TextureSizeChanged_h(void *layout, void *edx, const float *oldRect) {
|
||||
void *texture = static_cast<char *>(layout) - Offsets::OFF_REGION_ANCHOR;
|
||||
AnimData *data = PositionData(texture);
|
||||
if (data == nullptr) {
|
||||
g_origTextureSizeChanged(layout, edx, oldRect);
|
||||
return;
|
||||
}
|
||||
float animated[12];
|
||||
std::memcpy(animated, Corners(texture), sizeof(animated));
|
||||
g_origTextureSizeChanged(layout, edx, oldRect);
|
||||
float *corners = Corners(texture);
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
const float base = corners[i];
|
||||
corners[i] = animated[i] + (base - data->position[i]);
|
||||
data->position[i] = base;
|
||||
}
|
||||
}
|
||||
|
||||
void __fastcall FontStringSizeChanged_h(void *layout, void *edx, const float *oldRect) {
|
||||
void *fontString = static_cast<char *>(layout) - Offsets::OFF_REGION_ANCHOR;
|
||||
AnimData *data = PositionData(fontString);
|
||||
if (data == nullptr) {
|
||||
g_origFontStringSizeChanged(layout, edx, oldRect);
|
||||
return;
|
||||
}
|
||||
float animated[3];
|
||||
void *before = StringBlock(fontString);
|
||||
const bool hadString = before != nullptr && GetStringPos(before, animated);
|
||||
g_origFontStringSizeChanged(layout, edx, oldRect);
|
||||
float base[3];
|
||||
StringBase(fontString, base);
|
||||
void *block = StringBlock(fontString);
|
||||
if (hadString && block != nullptr) {
|
||||
float position[3];
|
||||
for (int i = 0; i < 3; ++i)
|
||||
position[i] = animated[i] + (base[i] - data->position[i]);
|
||||
SetStringPos(block, position);
|
||||
}
|
||||
std::memcpy(data->position, base, sizeof(base));
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _textureSizeHook{Offsets::FUN_TEXTURE_ON_FRAME_SIZE_CHANGED,
|
||||
reinterpret_cast<void *>(&TextureSizeChanged_h),
|
||||
reinterpret_cast<void **>(&g_origTextureSizeChanged)};
|
||||
const Game::HookAutoRegister _fontStringSizeHook{Offsets::FUN_FONTSTRING_ON_FRAME_SIZE_CHANGED,
|
||||
reinterpret_cast<void *>(&FontStringSizeChanged_h),
|
||||
reinterpret_cast<void **>(&g_origFontStringSizeChanged)};
|
||||
|
||||
// ---- Lifetime ------------------------------------------------------------------------
|
||||
|
||||
void DestroyGroupsOf(void *region) {
|
||||
const auto it = g_states.find(region);
|
||||
if (it != g_states.end()) {
|
||||
if (!it->second.groups.empty() && !IsFrame(region))
|
||||
--g_regionsWithGroups;
|
||||
const std::vector<Group *> groups = std::move(it->second.groups);
|
||||
g_states.erase(it);
|
||||
for (Group *group : groups)
|
||||
delete group;
|
||||
}
|
||||
g_data.erase(region);
|
||||
}
|
||||
|
||||
// The frame and its regions die here; its child frames run their own
|
||||
// destructors.
|
||||
void OnFrameDestroyed(void *frame) {
|
||||
if (g_states.empty() && g_data.empty())
|
||||
return;
|
||||
Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_REGION_LIST, [](void *region) { DestroyGroupsOf(region); });
|
||||
DestroyGroupsOf(frame);
|
||||
}
|
||||
|
||||
const Frame::Destruction::AutoSubscribe _destructionSub{&OnFrameDestroyed};
|
||||
|
||||
// Groups on frames that outlived a /reload: their tables and handlers were
|
||||
// in the old Lua state, so they are dropped without touching it.
|
||||
void DropLeftovers() {
|
||||
Object::s_detached = true;
|
||||
for (auto &entry : g_states)
|
||||
for (Group *group : entry.second.groups)
|
||||
delete group;
|
||||
Object::s_detached = false;
|
||||
g_states.clear();
|
||||
g_data.clear();
|
||||
g_regionsWithGroups = 0;
|
||||
}
|
||||
|
||||
// ---- Region methods (CScriptRegion_Scripts.cpp) -------------------------------------------
|
||||
|
||||
void StopAnimating(void *region) {
|
||||
if (State *state = Find(region)) {
|
||||
const std::vector<Group *> playing = state->playing;
|
||||
for (Group *group : playing)
|
||||
group->Stop(true);
|
||||
}
|
||||
// CSimpleFrame::StopAnimating: and everything below it.
|
||||
if (IsFrame(region))
|
||||
ForEachChild(region, [](void *child) { StopAnimating(child); });
|
||||
}
|
||||
|
||||
int __fastcall Script_CreateAnimationGroup(void *L) {
|
||||
void *region = Game::Lua::ResolveRegion(L);
|
||||
if (region == nullptr)
|
||||
return 0;
|
||||
const char *name = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
const void *templateNode = nullptr;
|
||||
if (Game::Lua::Type(L, 3) == Game::Lua::TYPE_STRING) {
|
||||
const char *inherits = Game::Lua::ToString(L, 3);
|
||||
templateNode = Xml::Template(inherits);
|
||||
if (templateNode == nullptr) {
|
||||
Game::Lua::Error(L, "%s:CreateAnimationGroup(): Couldn't find inherited node \"%s\"",
|
||||
Game::Lua::ObjectDisplayName(region), inherits);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Group *group = new Group(region, name);
|
||||
if (templateNode != nullptr) {
|
||||
Xml::LoadGroup(group, templateNode, nullptr);
|
||||
group->PostLoadXML();
|
||||
}
|
||||
Push(L, group);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetAnimationGroups(void *L) {
|
||||
void *region = Game::Lua::ResolveRegion(L);
|
||||
if (region == nullptr)
|
||||
return 0;
|
||||
const State *state = Find(region);
|
||||
if (state == nullptr)
|
||||
return 0;
|
||||
const int count = static_cast<int>(state->groups.size());
|
||||
if (!Game::Lua::CheckStack(L, count)) {
|
||||
Game::Lua::Error(L, "%s:GetAnimationGroups(): Stack overflow", Game::Lua::ObjectDisplayName(region));
|
||||
return 0;
|
||||
}
|
||||
for (Group *group : state->groups)
|
||||
Push(L, group);
|
||||
return count;
|
||||
}
|
||||
|
||||
int __fastcall Script_StopAnimating(void *L) {
|
||||
void *region = Game::Lua::ResolveRegion(L);
|
||||
if (region != nullptr)
|
||||
StopAnimating(region);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kRegionMethods[] = {
|
||||
{"CreateAnimationGroup", &Script_CreateAnimationGroup},
|
||||
{"GetAnimationGroups", &Script_GetAnimationGroups},
|
||||
{"StopAnimating", &Script_StopAnimating},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kCreateArgs[] = {
|
||||
Game::Doc::Opt("name", "string", nullptr, "The group's name; \"$parent\" is the region's."),
|
||||
Game::Doc::Opt("templateName", "string", nullptr, "A virtual <AnimationGroup> to inherit."),
|
||||
};
|
||||
const Game::Doc::Field kCreateRets[] = {
|
||||
Game::Doc::Req("animationGroup", "AnimationGroup"),
|
||||
};
|
||||
const Game::Doc::Field kGroupsRets[] = {
|
||||
Game::Doc::Vararg("AnimationGroup", "Every animation group of the region, in creation order."),
|
||||
};
|
||||
const Game::Doc::Function kCreateAnimationGroup{"Creates an animation group on the region.", kCreateArgs,
|
||||
kCreateRets};
|
||||
const Game::Doc::Function kGetAnimationGroups{"Returns the region's animation groups.", {}, kGroupsRets};
|
||||
const Game::Doc::Function kStopAnimating{
|
||||
"Stops every playing animation group of the region and, for a frame, of everything inside it.", {}, {}};
|
||||
const Game::Doc::Method kRegionDocs[] = {
|
||||
{"CreateAnimationGroup", &kCreateAnimationGroup},
|
||||
{"GetAnimationGroups", &kGetAnimationGroups},
|
||||
{"StopAnimating", &kStopAnimating},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
DropLeftovers();
|
||||
RegisterClass(Group::s_class);
|
||||
RegisterClass(Anim::s_class);
|
||||
RegisterClass(Anim::s_alphaClass);
|
||||
RegisterClass(Anim::s_translationClass);
|
||||
RegisterClass(Anim::s_scaleClass);
|
||||
RegisterClass(Anim::s_rotationClass);
|
||||
RegisterClass(Anim::s_pathClass);
|
||||
RegisterClass(ControlPoint::s_class);
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), kRegionMethods,
|
||||
static_cast<int>(sizeof(kRegionMethods) / sizeof(kRegionMethods[0])),
|
||||
kRegionDocs, static_cast<int>(sizeof(kRegionDocs) / sizeof(kRegionDocs[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- The CScriptRegion animation members ---------------------------------------------
|
||||
|
||||
void RegisterGroup(void *region, Group *group) {
|
||||
State &state = g_states[region];
|
||||
if (state.groups.empty() && !IsFrame(region))
|
||||
++g_regionsWithGroups;
|
||||
state.groups.push_back(group);
|
||||
}
|
||||
|
||||
void NotifyBegin(void *region, Group *group) {
|
||||
if (group == nullptr)
|
||||
return;
|
||||
State &state = g_states[region];
|
||||
const AnimData *data = DataOf(region);
|
||||
const bool alpha = !(data != nullptr && data->hasAlpha) && group->HasAlphaAnim();
|
||||
const bool position = !(data != nullptr && data->hasPosition) && group->HasPositionAnim();
|
||||
AnimActivated(region, alpha, position);
|
||||
state.playing.push_back(group);
|
||||
}
|
||||
|
||||
void NotifyEnd(void *region, Group *group) {
|
||||
State *state = Find(region);
|
||||
if (group == nullptr || state == nullptr)
|
||||
return;
|
||||
state->playing.erase(std::remove(state->playing.begin(), state->playing.end(), group), state->playing.end());
|
||||
const AnimData *data = DataOf(region);
|
||||
const bool alpha = data != nullptr && data->hasAlpha &&
|
||||
!HasActiveAnim(region, [](const Group *g) { return g->HasAlphaAnim(); });
|
||||
const bool position = data != nullptr && data->hasPosition &&
|
||||
!HasActiveAnim(region, [](const Group *g) { return g->HasPositionAnim(); });
|
||||
AnimDeactivated(region, alpha, position);
|
||||
}
|
||||
|
||||
void PreOnAnimUpdate(void *region) {
|
||||
const bool frame = IsFrame(region);
|
||||
const AnimData *data = DataOf(region);
|
||||
if (data != nullptr && data->hasAlpha) {
|
||||
if (frame) {
|
||||
if (data->alpha != OwnAlpha(region))
|
||||
SetOwnAlpha(region, data->alpha);
|
||||
} else {
|
||||
SetVertexAlpha(region, data->alpha);
|
||||
}
|
||||
}
|
||||
if (data != nullptr && data->hasPosition) {
|
||||
if (IsTexture(region)) {
|
||||
std::memcpy(Corners(region), data->position, sizeof(data->position));
|
||||
} else if (IsFontString(region)) {
|
||||
if (void *block = StringBlock(region))
|
||||
SetStringPos(block, data->position);
|
||||
// CSimpleFontString::PreOnAnimUpdate: back to the base rotation
|
||||
// and scale.
|
||||
AnimData *own = DataOf(region);
|
||||
own->rotation = 0.0f;
|
||||
own->scale = 1.0f;
|
||||
g_touchedStrings.push_back(region);
|
||||
}
|
||||
}
|
||||
if (frame)
|
||||
ForEachChild(region, [](void *child) { PreOnAnimUpdate(child); });
|
||||
}
|
||||
|
||||
void AddAnimTranslation(void *region, void *target, float x, float y) {
|
||||
if (IsFrame(region)) {
|
||||
// CSimpleFrame::AddAnimTranslation: the frame itself does not move;
|
||||
// everything below it does, or only the target below it.
|
||||
if (target == region)
|
||||
target = nullptr;
|
||||
ForEachChild(region, [target, x, y](void *child) { AddAnimTranslation(child, target, x, y); });
|
||||
return;
|
||||
}
|
||||
if (target != nullptr && target != region)
|
||||
return;
|
||||
// Only a region whose base is stored: PreOnAnimUpdate puts it back before
|
||||
// the next add, so nothing builds up.
|
||||
const AnimData *data = DataOf(region);
|
||||
if (data == nullptr || !data->hasPosition)
|
||||
return;
|
||||
if (IsTexture(region)) {
|
||||
// ApplyTranslationAnim(m_positions, translation).
|
||||
float *corners = Corners(region);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
corners[i * 3 + 0] += x;
|
||||
corners[i * 3 + 1] += y;
|
||||
}
|
||||
} else if (void *block = StringBlock(region)) {
|
||||
float position[3];
|
||||
if (!GetStringPos(block, position))
|
||||
return;
|
||||
position[0] += x;
|
||||
position[1] += y;
|
||||
SetStringPos(block, position);
|
||||
}
|
||||
}
|
||||
|
||||
void AddAnimAlpha(void *region, void *target, int16_t alpha) {
|
||||
if (IsFrame(region)) {
|
||||
// CSimpleFrame::AddAnimAlpha: a target below the frame takes it.
|
||||
if (target != nullptr && target != region) {
|
||||
ForEachChild(region, [target, alpha](void *child) { AddAnimAlpha(child, target, alpha); });
|
||||
return;
|
||||
}
|
||||
const uint8_t own = OwnAlpha(region);
|
||||
const int value = alpha + static_cast<uint8_t>(own * DrawnParentAlpha(region) / 255);
|
||||
const uint8_t newAlpha = static_cast<uint8_t>(value > 255 ? 255 : (value < 0 ? 0 : value));
|
||||
if (newAlpha != own)
|
||||
SetOwnAlpha(region, newAlpha);
|
||||
return;
|
||||
}
|
||||
// CSimpleRegion::AddAnimAlpha.
|
||||
if (target != nullptr && target != region)
|
||||
return;
|
||||
const int value = alpha + static_cast<int>(VertexColor(region) >> 24);
|
||||
SetVertexAlpha(region, static_cast<uint8_t>(value > 255 ? 255 : (value < 0 ? 0 : value)));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The corners (TL, BL, TR, BR) as 2D points.
|
||||
float CornerX(const float *corners, int i) { return corners[i * 3 + 0]; }
|
||||
float CornerY(const float *corners, int i) { return corners[i * 3 + 1]; }
|
||||
|
||||
// GetOriginOffset (4.3.4 CSimpleRegion.cpp): the origin point on the drawn
|
||||
// quad, then the offset along the quad's bottom edge direction, so the
|
||||
// offset turns with a quad that is already turned.
|
||||
void GetOriginOffset(int origin, const float *c, float offsetX, float offsetY, float &x, float &y) {
|
||||
const auto mid = [c](int a, int b, float &mx, float &my) {
|
||||
mx = (CornerX(c, b) - CornerX(c, a)) * 0.5f + CornerX(c, a);
|
||||
my = (CornerY(c, b) - CornerY(c, a)) * 0.5f + CornerY(c, a);
|
||||
};
|
||||
switch (origin) {
|
||||
case Offsets::FRAMEPOINT_TOPLEFT: x = CornerX(c, 0); y = CornerY(c, 0); break;
|
||||
case Offsets::FRAMEPOINT_TOP: mid(0, 2, x, y); break;
|
||||
case Offsets::FRAMEPOINT_TOPRIGHT: x = CornerX(c, 2); y = CornerY(c, 2); break;
|
||||
case Offsets::FRAMEPOINT_LEFT: mid(0, 1, x, y); break;
|
||||
case Offsets::FRAMEPOINT_CENTER: mid(0, 3, x, y); break;
|
||||
case Offsets::FRAMEPOINT_RIGHT: mid(2, 3, x, y); break;
|
||||
case Offsets::FRAMEPOINT_BOTTOMLEFT: x = CornerX(c, 1); y = CornerY(c, 1); break;
|
||||
case Offsets::FRAMEPOINT_BOTTOM: mid(1, 3, x, y); break;
|
||||
case Offsets::FRAMEPOINT_BOTTOMRIGHT: x = CornerX(c, 3); y = CornerY(c, 3); break;
|
||||
default: x = 0.0f; y = 0.0f; break;
|
||||
}
|
||||
if (offsetX * offsetX + offsetY * offsetY > 0.00000011920929f) {
|
||||
float dx = CornerX(c, 3) - CornerX(c, 1);
|
||||
float dy = CornerY(c, 3) - CornerY(c, 1);
|
||||
const float length = dy * dy + dx * dx;
|
||||
if (length > 0.00000023841858f) {
|
||||
const float scale = 1.0f / std::sqrt(length);
|
||||
dx *= scale;
|
||||
dy *= scale;
|
||||
}
|
||||
x = offsetX * dx - offsetY * dy + x;
|
||||
y = offsetY * dx + offsetX * dy + y;
|
||||
}
|
||||
}
|
||||
|
||||
// The font string that takes a position animation, or nullptr.
|
||||
AnimData *AnimatedString(void *region, void *target) {
|
||||
if (target != nullptr && target != region)
|
||||
return nullptr;
|
||||
AnimData *data = DataOf(region);
|
||||
if (data == nullptr || !data->hasPosition || !IsFontString(region))
|
||||
return nullptr;
|
||||
return data;
|
||||
}
|
||||
|
||||
// The texture that takes a position animation (the target filter of the
|
||||
// region overrides), or nullptr.
|
||||
float *AnimatedCorners(void *region, void *target) {
|
||||
if (target != nullptr && target != region)
|
||||
return nullptr;
|
||||
const AnimData *data = DataOf(region);
|
||||
if (data == nullptr || !data->hasPosition || !IsTexture(region))
|
||||
return nullptr;
|
||||
return Corners(region);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AddAnimScale(void *region, void *target, int origin, float offsetX, float offsetY, float x, float y) {
|
||||
if (IsFrame(region)) {
|
||||
// CSimpleFrame::AddAnimScale.
|
||||
if (target == region)
|
||||
target = nullptr;
|
||||
ForEachChild(region, [=](void *child) { AddAnimScale(child, target, origin, offsetX, offsetY, x, y); });
|
||||
return;
|
||||
}
|
||||
// CSimpleFontString::AddAnimScale: the text grows from its origin by the
|
||||
// x scale; the last scale animation of the update wins.
|
||||
if (AnimData *data = AnimatedString(region, target)) {
|
||||
data->scale = 1.0f - x;
|
||||
return;
|
||||
}
|
||||
// CSimpleTexture::AddAnimScale -> ApplyScaleAnim.
|
||||
float *c = AnimatedCorners(region, target);
|
||||
if (c == nullptr)
|
||||
return;
|
||||
float cx, cy;
|
||||
GetOriginOffset(origin, c, offsetX, offsetY, cx, cy);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
c[i * 3 + 0] = (cx - c[i * 3 + 0]) * x + c[i * 3 + 0];
|
||||
c[i * 3 + 1] = (cy - c[i * 3 + 1]) * y + c[i * 3 + 1];
|
||||
}
|
||||
}
|
||||
|
||||
void AddAnimRotation(void *region, void *target, int origin, float offsetX, float offsetY, float radians) {
|
||||
if (IsFrame(region)) {
|
||||
// CSimpleFrame::AddAnimRotation.
|
||||
if (target == region)
|
||||
target = nullptr;
|
||||
ForEachChild(region, [=](void *child) { AddAnimRotation(child, target, origin, offsetX, offsetY, radians); });
|
||||
return;
|
||||
}
|
||||
// CSimpleFontString::AddAnimRotation: the text turns about its origin;
|
||||
// the last rotation animation of the update wins.
|
||||
if (AnimData *data = AnimatedString(region, target)) {
|
||||
data->rotation = radians;
|
||||
return;
|
||||
}
|
||||
// CSimpleTexture::AddAnimRotation -> ApplyRotationAnim.
|
||||
float *c = AnimatedCorners(region, target);
|
||||
if (c == nullptr)
|
||||
return;
|
||||
float cx, cy;
|
||||
GetOriginOffset(origin, c, offsetX, offsetY, cx, cy);
|
||||
const float cs = std::cos(radians);
|
||||
const float sn = std::sin(radians);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const float x = c[i * 3 + 0] - cx;
|
||||
const float y = c[i * 3 + 1] - cy;
|
||||
c[i * 3 + 0] = x * cs - y * sn + cx;
|
||||
c[i * 3 + 1] = x * sn + y * cs + cy;
|
||||
}
|
||||
}
|
||||
|
||||
void FontStringBaked(void *node, void *fontString) {
|
||||
if (g_data.empty())
|
||||
return;
|
||||
AnimData *data = DataOf(fontString);
|
||||
if (data == nullptr)
|
||||
return;
|
||||
if (data->rotation != 0.0f || data->scale != 1.0f)
|
||||
Texture::Transform::ScaleRotateFontStringNode(node, data->scale, data->rotation);
|
||||
data->bakedRotation = data->rotation;
|
||||
data->bakedScale = data->scale;
|
||||
}
|
||||
|
||||
float *TextureBaseCorners(void *texture) {
|
||||
if (g_data.empty())
|
||||
return nullptr;
|
||||
AnimData *data = DataOf(texture);
|
||||
return data != nullptr && data->hasPosition && IsTexture(texture) ? data->position : nullptr;
|
||||
}
|
||||
|
||||
bool StringAnimated(void *fontString) {
|
||||
if (g_data.empty())
|
||||
return false;
|
||||
const AnimData *data = DataOf(fontString);
|
||||
return data != nullptr && (data->rotation != 0.0f || data->scale != 1.0f || data->bakedRotation != 0.0f ||
|
||||
data->bakedScale != 1.0f);
|
||||
}
|
||||
|
||||
void *FindRegionByName(const char *name) {
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr || name == nullptr)
|
||||
return nullptr;
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
|
||||
void *object = Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE ? Game::Lua::ResolveObject(L, -1) : nullptr;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (object == nullptr || IsOurs(object) || !Game::Lua::IsA(object, Offsets::VAR_REGION_LUA_TYPE_ID))
|
||||
return nullptr;
|
||||
return object;
|
||||
}
|
||||
|
||||
} // namespace Animation::Region
|
||||
@@ -0,0 +1,62 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// The region side of the animation system: 4.3.4's CScriptRegion animation
|
||||
// members (m_animGroups, m_playingAnimGroups, the CLayoutFrame animation
|
||||
// data) and their virtuals on CSimpleFrame and CSimpleRegion, for 1.12's
|
||||
// frames, textures and font strings, which have no room for them.
|
||||
namespace Animation {
|
||||
class Group;
|
||||
}
|
||||
|
||||
namespace Animation::Region {
|
||||
|
||||
// CScriptRegion::RegisterAnimGroup - the region owns the group from now on.
|
||||
void RegisterGroup(void *region, Group *group);
|
||||
// CScriptRegion::NotifyAnimBegin / NotifyAnimEnd.
|
||||
void NotifyBegin(void *region, Group *group);
|
||||
void NotifyEnd(void *region, Group *group);
|
||||
// CScriptRegion::PreOnAnimUpdate: put the animated state back to its base
|
||||
// before the animations apply themselves for this update.
|
||||
void PreOnAnimUpdate(void *region);
|
||||
// CScriptRegion::AddAnimAlpha(target, alpha) on the group's region.
|
||||
void AddAnimAlpha(void *region, void *target, int16_t alpha);
|
||||
// CScriptRegion::AddAnimTranslation(target, translation), in layout units:
|
||||
// moves the drawn textures and font strings, never a rect.
|
||||
void AddAnimTranslation(void *region, void *target, float x, float y);
|
||||
// CScriptRegion::AddAnimScale(target, origin, offset, scale): `x`/`y` are the
|
||||
// share of the way to the origin (1 - scale at full progress).
|
||||
void AddAnimScale(void *region, void *target, int origin, float offsetX, float offsetY, float x, float y);
|
||||
// CScriptRegion::AddAnimRotation(target, origin, offset, radians),
|
||||
// counter-clockwise.
|
||||
void AddAnimRotation(void *region, void *target, int origin, float offsetX, float offsetY, float radians);
|
||||
// A font string's glyphs were just baked (the draw builder): turn and scale
|
||||
// them by its animation, and remember what they were baked with.
|
||||
void FontStringBaked(void *node, void *fontString);
|
||||
// The stored corners (TL BL TR BR {x, y, z}) of a texture a position group
|
||||
// is playing on, or nullptr. Each update draws the animation on top of them,
|
||||
// so a change to the texture's own shape (Texture::Transform) goes here, not
|
||||
// to the drawn corners.
|
||||
float *TextureBaseCorners(void *texture);
|
||||
// Whether the font string's glyphs carry an animation's rotation or scale:
|
||||
// then a change to its own rotation has to bake them again.
|
||||
bool StringAnimated(void *fontString);
|
||||
// CScriptObject::GetScriptObjectByName for a region (Animation:Play's
|
||||
// target lookup): the region named `name`, or nullptr.
|
||||
void *FindRegionByName(const char *name);
|
||||
|
||||
} // namespace Animation::Region
|
||||
@@ -0,0 +1,377 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 4.3.4's animation XML (CScriptRegion::LoadXML_Animations,
|
||||
// CSimpleAnimGroup::LoadXML, CSimpleAnim::LoadXML and the subclasses',
|
||||
// LoadXML_AnimOrigin; the attributes BinaryXML.cpp parses), on 1.12 nodes.
|
||||
// A script body compiles through the engine's own SetScriptObjectScript, so
|
||||
// it is named "<object>:<script>" and handled like a frame's.
|
||||
|
||||
#include "animation/Xml.h"
|
||||
|
||||
#include "animation/Classes.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
#include "xml/KeyValues.h"
|
||||
#include "xml/Mixin.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace Animation::Xml {
|
||||
|
||||
namespace {
|
||||
|
||||
using SetScriptText_t = bool(__fastcall *)(void *object, void *edx, const char *name, const char *text);
|
||||
using StringToPoint_t = int(__fastcall *)(const char *name, int *point);
|
||||
|
||||
const void *FindChild(const void *node, const char *tag) {
|
||||
for (const void *c = ::Xml::FirstChild(node); c != nullptr; c = ::Xml::NextSibling(c))
|
||||
if (::Xml::TagIs(c, tag))
|
||||
return c;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool FloatAttr(const void *node, const char *name, float &out) {
|
||||
const char *v = ::Xml::Attr(node, name);
|
||||
if (v == nullptr || *v == '\0')
|
||||
return false;
|
||||
out = static_cast<float>(std::atof(v));
|
||||
return true;
|
||||
}
|
||||
|
||||
// object[parentKey] = child, as 4.3.4's LoadXML does for both.
|
||||
void BindParentKey(void *object, Object *child, const char *key) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (::Xml::PushWrapper(L, object)) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Push(L, child);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// <Scripts>: each element's body, its `function` global, or its `method`
|
||||
// field on the object.
|
||||
void LoadScripts(Object *object, const void *node) {
|
||||
const void *scripts = FindChild(node, "Scripts");
|
||||
if (scripts == nullptr)
|
||||
return;
|
||||
for (const void *s = ::Xml::FirstChild(scripts); s != nullptr; s = ::Xml::NextSibling(s)) {
|
||||
const char *script = ::Xml::Tag(s);
|
||||
if (script == nullptr)
|
||||
continue;
|
||||
const char *function = ::Xml::Attr(s, "function");
|
||||
const char *method = ::Xml::Attr(s, "method");
|
||||
const bool byFunction = function != nullptr && *function != '\0';
|
||||
if (byFunction || (method != nullptr && *method != '\0')) {
|
||||
uint32_t *slot = object->ScriptSlot(script);
|
||||
void *L = Game::Lua::State();
|
||||
if (slot == nullptr || L == nullptr)
|
||||
continue;
|
||||
if (byFunction) {
|
||||
Game::Lua::PushString(L, function);
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
} else {
|
||||
Push(L, object);
|
||||
Game::Lua::PushString(L, method);
|
||||
Game::Lua::GetTable(L, -2);
|
||||
Game::Lua::Remove(L, -2);
|
||||
}
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::SetTop(L, -2);
|
||||
continue;
|
||||
}
|
||||
reinterpret_cast<void(__stdcall *)(uint32_t *)>(Offsets::FUN_SCRIPT_RELEASE)(slot);
|
||||
slot[0] = static_cast<uint32_t>(Game::Lua::Ref(L, Game::Lua::REGISTRY_INDEX));
|
||||
slot[1] = 0;
|
||||
continue;
|
||||
}
|
||||
const char *body = Game::Read<const char *>(s, Offsets::OFF_XML_NODE_TEXT);
|
||||
if (body != nullptr && *body != '\0')
|
||||
reinterpret_cast<SetScriptText_t>(Offsets::FUN_SCRIPT_OBJECT_SET_SCRIPT_TEXT)(object, nullptr, script,
|
||||
body);
|
||||
}
|
||||
}
|
||||
|
||||
// <KeyValues> on a group or an animation, as on later clients' schema.
|
||||
void LoadKeyValues(Object *object, const void *node, void *status) {
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Push(L, object);
|
||||
::Xml::KeyValues::Apply(L, top + 1, node, status, object->DisplayName());
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// LoadXML_AnimOrigin: <Origin point="..."><Offset x= y=/></Origin>.
|
||||
void LoadOrigin(Anim *anim, const void *node) {
|
||||
const void *origin = FindChild(node, "Origin");
|
||||
if (origin == nullptr)
|
||||
return;
|
||||
int point = Offsets::FRAMEPOINT_CENTER;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
const char *pointName = ::Xml::Attr(origin, "point");
|
||||
if (pointName != nullptr &&
|
||||
reinterpret_cast<StringToPoint_t>(Offsets::FUN_STRING_TO_FRAME_POINT)(pointName, &point)) {
|
||||
if (const void *offset = FindChild(origin, "Offset")) {
|
||||
const void *abs = FindChild(offset, "AbsDimension");
|
||||
const void *dim = abs != nullptr ? abs : offset;
|
||||
FloatAttr(dim, "x", x);
|
||||
FloatAttr(dim, "y", y);
|
||||
}
|
||||
} else {
|
||||
point = Offsets::FRAMEPOINT_CENTER;
|
||||
}
|
||||
anim->originPoint = point;
|
||||
anim->originX = Game::PixelToInternal(x);
|
||||
anim->originY = Game::PixelToInternal(y);
|
||||
}
|
||||
|
||||
void LoadAnimChildren(Group *group, const void *node, void *status) {
|
||||
for (const void *child = ::Xml::FirstChild(node); child != nullptr; child = ::Xml::NextSibling(child)) {
|
||||
const char *tag = ::Xml::Tag(child);
|
||||
if (tag == nullptr || ::Xml::TagIs(child, "Scripts"))
|
||||
continue;
|
||||
// 4.3.4 knows Animation, Translation, Rotation, Scale, Path and Alpha.
|
||||
if (!::Xml::TagIs(child, "Animation") && !::Xml::TagIs(child, "Translation") &&
|
||||
!::Xml::TagIs(child, "Rotation") && !::Xml::TagIs(child, "Scale") && !::Xml::TagIs(child, "Alpha") &&
|
||||
!::Xml::TagIs(child, "Path"))
|
||||
continue;
|
||||
Anim *anim = new Anim(group, Anim::KindOf(tag), ::Xml::Attr(child, "name"));
|
||||
anim->flags |= 1;
|
||||
LoadAnim(anim, child, status);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const void *Template(const char *name) { return ::Xml::Template(name); }
|
||||
|
||||
namespace {
|
||||
|
||||
// LoadGroup re-enters itself for each inherited template; the mixins of the
|
||||
// whole chain go in once, from the outermost call, before any node's
|
||||
// KeyValues or scripts (see Xml::Mixin).
|
||||
int g_groupDepth = 0;
|
||||
|
||||
void LoadGroupNode(Group *group, const void *node, void *status);
|
||||
|
||||
} // namespace
|
||||
|
||||
void LoadGroup(Group *group, const void *node, void *status) {
|
||||
if (g_groupDepth == 0) {
|
||||
if (void *L = Game::Lua::State()) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Push(L, group);
|
||||
::Xml::Mixin::ApplyChain(L, top + 1, node, status, group->DisplayName(), false);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
}
|
||||
++g_groupDepth;
|
||||
LoadGroupNode(group, node, status);
|
||||
--g_groupDepth;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void LoadGroupNode(Group *group, const void *node, void *status) {
|
||||
group->flags |= GROUP_LOADING;
|
||||
if (const char *inherits = ::Xml::Attr(node, "inherits"); inherits != nullptr && *inherits != '\0') {
|
||||
const void *base = ::Xml::Template(inherits);
|
||||
if (base == nullptr)
|
||||
::Xml::Warn(status, "Couldn't find inherited node: %s", inherits);
|
||||
else
|
||||
LoadGroupNode(group, base, status);
|
||||
}
|
||||
BindParentKey(group->RegionObject(), group, ::Xml::Attr(node, "parentKey"));
|
||||
if (const char *looping = ::Xml::Attr(node, "looping"); looping != nullptr && *looping != '\0') {
|
||||
LoopType type;
|
||||
if (StringToLoopType(looping, type))
|
||||
group->loopType = type;
|
||||
else
|
||||
::Xml::Warn(status, "AnimationGroup %s: Invalid looping value: %s", group->DisplayName(), looping);
|
||||
}
|
||||
float x = 0.0f, y = 0.0f;
|
||||
FloatAttr(node, "initialOffsetX", x);
|
||||
FloatAttr(node, "initialOffsetY", y);
|
||||
x = Game::PixelToInternal(x);
|
||||
y = Game::PixelToInternal(y);
|
||||
if (x * x + y * y <= 0.00000023841858f) {
|
||||
group->initialOffsetX = 0.0f;
|
||||
group->initialOffsetY = 0.0f;
|
||||
} else {
|
||||
group->initialOffsetX = x;
|
||||
group->initialOffsetY = y;
|
||||
}
|
||||
LoadScripts(group, node);
|
||||
LoadKeyValues(group, node, status);
|
||||
LoadAnimChildren(group, node, status);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
// The subclass reads its values, then CSimpleAnim::LoadXML (inherits
|
||||
// first, then the shared attributes) - the order 4.3.4's overrides use.
|
||||
float x = 0.0f, y = 0.0f;
|
||||
switch (anim->kind) {
|
||||
case KIND_ALPHA:
|
||||
break; // CSimpleAlphaAnim::LoadXML reads `change` after the base.
|
||||
case KIND_TRANSLATION: {
|
||||
// CSimpleTranslationAnim::LoadXML sets the offset, 0 for a missing axis.
|
||||
FloatAttr(node, "offsetX", x);
|
||||
FloatAttr(node, "offsetY", y);
|
||||
const float ox = Game::PixelToInternal(x), oy = Game::PixelToInternal(y);
|
||||
const bool zero = ox * ox + oy * oy <= 0.00000023841858f;
|
||||
anim->offsetX = zero ? 0.0f : ox;
|
||||
anim->offsetY = zero ? 0.0f : oy;
|
||||
break;
|
||||
}
|
||||
case KIND_ROTATION:
|
||||
if (FloatAttr(node, "degrees", x))
|
||||
anim->radians = x * 0.017453292f;
|
||||
if (FloatAttr(node, "radians", y))
|
||||
anim->radians = y;
|
||||
break;
|
||||
case KIND_SCALE: {
|
||||
float sx = 1.0f, sy = 1.0f;
|
||||
if (FloatAttr(node, "scaleX", sx) && sx < 0.001f)
|
||||
::Xml::Warn(status, "%s: Invalid scaleX value: %s. Value must be at least 0.001.", anim->DisplayName(),
|
||||
::Xml::Attr(node, "scaleX"));
|
||||
if (FloatAttr(node, "scaleY", sy) && sy < 0.001f)
|
||||
::Xml::Warn(status, "%s: Invalid scaleY value: %s. Value must be at least 0.001.", anim->DisplayName(),
|
||||
::Xml::Attr(node, "scaleY"));
|
||||
anim->scaleChangeX = 1.0f - (sx >= 0.001f ? sx : 0.001f);
|
||||
anim->scaleChangeY = 1.0f - (sy >= 0.001f ? sy : 0.001f);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// CSimpleAnim::LoadXML.
|
||||
anim->flags |= 1;
|
||||
if (const char *inherits = ::Xml::Attr(node, "inherits"); inherits != nullptr && *inherits != '\0') {
|
||||
const void *base = ::Xml::Template(inherits);
|
||||
if (base == nullptr)
|
||||
::Xml::Warn(status, "Couldn't find inherited node: %s", inherits);
|
||||
else
|
||||
LoadAnim(anim, base, status);
|
||||
}
|
||||
BindParentKey(anim->GetGroup(), anim, ::Xml::Attr(node, "parentKey"));
|
||||
if (const char *target = ::Xml::Attr(node, "target"); target != nullptr && *target != '\0') {
|
||||
const std::string name = CreateName(target, anim);
|
||||
if (name.size() + 1 > sizeof(anim->targetName))
|
||||
::Xml::Warn(status, "target key too long: %s", name.c_str());
|
||||
std::snprintf(anim->targetName, sizeof(anim->targetName), "%s", name.c_str());
|
||||
}
|
||||
if (FloatAttr(node, "startDelay", x))
|
||||
anim->startDelay = x < 0.0f ? 0.0f : x;
|
||||
if (FloatAttr(node, "endDelay", x))
|
||||
anim->endDelay = x < 0.0f ? 0.0f : x;
|
||||
if (FloatAttr(node, "duration", x))
|
||||
anim->duration = x;
|
||||
if (const char *smoothing = ::Xml::Attr(node, "smoothing"); smoothing != nullptr && *smoothing != '\0') {
|
||||
float in, out;
|
||||
if (StringToSmoothWeights(smoothing, in, out))
|
||||
anim->SetSmoothing(in, out);
|
||||
else
|
||||
::Xml::Warn(status, "%s: Invalid smoothing value: %s", anim->DisplayName(), smoothing);
|
||||
}
|
||||
if (const char *order = ::Xml::Attr(node, "order"); order != nullptr && *order != '\0') {
|
||||
const int value = std::atoi(order) - 1;
|
||||
if (value < 0 || value > 99)
|
||||
::Xml::Warn(status, "%s: Invalid order value: %s. Order must be between 1 and 100.", anim->DisplayName(),
|
||||
order);
|
||||
anim->order = value < 0 ? 0 : (value > 99 ? 99 : value);
|
||||
}
|
||||
LoadScripts(anim, node);
|
||||
LoadKeyValues(anim, node, status);
|
||||
|
||||
// The subclasses' reads after the base.
|
||||
// CSimpleAlphaAnim::LoadXML sets the change, 0 when it is missing.
|
||||
if (anim->kind == KIND_ALPHA) {
|
||||
x = 0.0f;
|
||||
if (FloatAttr(node, "change", x) && (x < -1.0f || x > 1.0f)) {
|
||||
::Xml::Warn(status, "%s: Invalid change value: %s. Value must be between -1 and 1, inclusive.",
|
||||
anim->DisplayName(), ::Xml::Attr(node, "change"));
|
||||
x = 0.0f;
|
||||
}
|
||||
anim->change = static_cast<int16_t>((x < -1.0f ? -1.0f : (x >= 1.0f ? 1.0f : x)) * 255.0f);
|
||||
}
|
||||
if (anim->kind == KIND_SCALE || anim->kind == KIND_ROTATION)
|
||||
LoadOrigin(anim, node);
|
||||
// CSimplePathAnim::LoadXML: the curve, then <ControlPoints>.
|
||||
if (anim->kind == KIND_PATH) {
|
||||
if (const char *curve = ::Xml::Attr(node, "curve"); curve != nullptr && *curve != '\0') {
|
||||
if (Ascii::EqualCI(curve, "NONE"))
|
||||
anim->curve = false;
|
||||
else if (Ascii::EqualCI(curve, "SMOOTH"))
|
||||
anim->curve = true;
|
||||
else
|
||||
::Xml::Warn(status, "Path %s: Invalid curve value: %s", anim->DisplayName(), curve);
|
||||
}
|
||||
if (const void *points = FindChild(node, "ControlPoints")) {
|
||||
for (const void *child = ::Xml::FirstChild(points); child != nullptr; child = ::Xml::NextSibling(child)) {
|
||||
if (!::Xml::TagIs(child, "ControlPoint")) {
|
||||
::Xml::Warn(status, "Path %s: Error loading Origin element", anim->DisplayName());
|
||||
continue;
|
||||
}
|
||||
LoadControlPoint(new ControlPoint(anim, ::Xml::Attr(child, "name")), child, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LoadControlPoint(ControlPoint *point, const void *node, void *status) {
|
||||
// CSimpleControlPoint::LoadXML: inherits first, then parentKey (on the
|
||||
// path), then the offset - 0 on an axis the node does not give.
|
||||
if (const char *inherits = ::Xml::Attr(node, "inherits"); inherits != nullptr && *inherits != '\0') {
|
||||
const void *base = ::Xml::Template(inherits);
|
||||
if (base == nullptr)
|
||||
::Xml::Warn(status, "Couldn't find inherited node: %s", inherits);
|
||||
else
|
||||
LoadControlPoint(point, base, status);
|
||||
}
|
||||
BindParentKey(point->GetPath(), point, ::Xml::Attr(node, "parentKey"));
|
||||
float x = 0.0f, y = 0.0f;
|
||||
FloatAttr(node, "offsetX", x);
|
||||
FloatAttr(node, "offsetY", y);
|
||||
point->SetOffset(Game::PixelToInternal(x), Game::PixelToInternal(y));
|
||||
}
|
||||
|
||||
void LoadAnimations(void *region, const void *node, void *status) {
|
||||
const void *animations = FindChild(node, "Animations");
|
||||
if (animations == nullptr)
|
||||
return;
|
||||
for (const void *g = ::Xml::FirstChild(animations); g != nullptr; g = ::Xml::NextSibling(g)) {
|
||||
if (!::Xml::TagIs(g, "AnimationGroup"))
|
||||
continue;
|
||||
Group *group = new Group(region, ::Xml::Attr(g, "name"));
|
||||
LoadGroup(group, g, status);
|
||||
group->PostLoadXML();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Animation::Xml
|
||||
@@ -0,0 +1,42 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// <Animations>, <AnimationGroup> and the animation elements: 4.3.4's
|
||||
// CScriptRegion::LoadXML_AnimGroup, CSimpleAnimGroup::LoadXML and
|
||||
// CSimpleAnim::LoadXML (and the subclasses'), on 1.12's XML nodes. A
|
||||
// virtual top-level <AnimationGroup> or animation element is a template:
|
||||
// 1.12's file loader registers every virtual named top-level node
|
||||
// (RegisterXMLTemplate), whatever its tag, so `inherits` resolves through
|
||||
// Xml::Template as 4.3.4's XMLHashNodeRef does.
|
||||
namespace Animation {
|
||||
class Group;
|
||||
class Anim;
|
||||
class ControlPoint;
|
||||
} // namespace Animation
|
||||
|
||||
namespace Animation::Xml {
|
||||
|
||||
// The <Animations> child of a region's node, if any: each <AnimationGroup>
|
||||
// becomes a group of `region`, loaded and post-loaded (OnLoad runs).
|
||||
void LoadAnimations(void *region, const void *node, void *status);
|
||||
// CSimpleAnimGroup::LoadXML / CSimpleAnim::LoadXML of one node, its
|
||||
// `inherits` first.
|
||||
void LoadGroup(Group *group, const void *node, void *status);
|
||||
void LoadAnim(Anim *anim, const void *node, void *status);
|
||||
void LoadControlPoint(ControlPoint *point, const void *node, void *status);
|
||||
// The template node `name` resolves to, or nullptr.
|
||||
const void *Template(const char *name);
|
||||
|
||||
} // namespace Animation::Xml
|
||||
@@ -0,0 +1,693 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// API documentation: the record of every Lua registration plus its
|
||||
// descriptor, exported on demand as Blizzard `APIDocumentation` tables.
|
||||
//
|
||||
// C_APIDocumentation.GetSystems() -> sorted array of system Names
|
||||
// C_APIDocumentation.GetSystem(name) -> one System table (Blizzard shape), or nil
|
||||
// _classicapi_UndocumentedAPI() -> array of registrations lacking a
|
||||
// descriptor, and its count
|
||||
//
|
||||
// The registrars in Game.cpp call `Record*` (see Documentation.h). Recording
|
||||
// runs once per Lua state — the first in-game pass and the first glue pass —
|
||||
// because the set of registrations is static: every module registers the
|
||||
// same names on every `/reload`. The recorded list is never materialized
|
||||
// into Lua wholesale; `GetSystem` builds ONE system's table per call, which
|
||||
// is how Blizzard's browser loads too (its generated data addon is
|
||||
// LoadOnDemand). Eager export would create thousands of tables on every
|
||||
// `/reload` for a feature used from a slash command.
|
||||
//
|
||||
// System grouping is derived, never authored, except for two strings:
|
||||
// - `C_Spell.X` -> System "Spell", Namespace "C_Spell" (strip `C_`)
|
||||
// - `table.insert` -> System "table", Namespace "table"
|
||||
// - a global -> `doc->system` ("SpellGlobals"); a global whose
|
||||
// descriptor names no system has no home and is
|
||||
// listed by the coverage function
|
||||
// - a frame method -> Blizzard's widget system for its registry
|
||||
// ("SimpleTextureAPI", Type "ScriptObject")
|
||||
// - an enum / structure /
|
||||
// event -> the System named in its declaration
|
||||
// A system's `Environment` is "All" when its functions were recorded in both
|
||||
// Lua states, else "Game" or "Glue"; a function whose own environment is
|
||||
// narrower than its system's gets a Documentation line saying so.
|
||||
|
||||
#include "api/Documentation.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Api::Documentation {
|
||||
|
||||
using Game::Doc::Field;
|
||||
using Game::Doc::FieldList;
|
||||
using Game::Doc::Function;
|
||||
using Game::Doc::Method;
|
||||
using Game::Doc::Structure;
|
||||
// NOT `using Game::Doc::Event` — that name would shadow the global `Event`
|
||||
// namespace for the whole file, so `Event::Custom::…` would resolve to the
|
||||
// type. Same trap as `Time::Tick` vs the `Tick` namespace (see CLAUDE.md).
|
||||
using EventDoc = Game::Doc::Event;
|
||||
|
||||
// Head of the self-registering `Structure` list. Constant-initialized, so it
|
||||
// is valid before any `Structure` constructor runs, whatever the cross-TU
|
||||
// static-init order.
|
||||
const Structure *g_structureHead = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
enum Kind : uint8_t {
|
||||
KIND_GLOBAL, // RegisterGlobalFunction / RegisterGlueFunction / RegisterGlobalAlias
|
||||
KIND_TABLE, // RegisterTableFunction
|
||||
KIND_FRAME_METHOD, // RegisterFrameMethods, one per entry
|
||||
KIND_ENUM, // RegisterIntegerEnum
|
||||
KIND_ORPHAN_METHOD_DOC // a Doc::Method naming no entry in its table
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
Kind kind;
|
||||
unsigned env; // Env bits the registration was recorded in
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum")
|
||||
const char *name; // registered name / method name / enum sub-name
|
||||
const void *registry; // frame methods only
|
||||
const Function *doc; // nullptr = undocumented
|
||||
const char *enumSystem;
|
||||
const Game::Lua::EnumIntegerEntry *enumEntries;
|
||||
int enumCount;
|
||||
};
|
||||
|
||||
std::vector<Entry> g_entries;
|
||||
Env g_pass = ENV_NONE;
|
||||
unsigned g_sealed = 0;
|
||||
|
||||
constexpr const char *kDebugPrefix = "_classicapi_";
|
||||
|
||||
bool IsDebugName(const char *name) {
|
||||
return std::strncmp(name, kDebugPrefix, std::strlen(kDebugPrefix)) == 0;
|
||||
}
|
||||
|
||||
bool SameText(const char *a, const char *b) {
|
||||
if (a == nullptr || b == nullptr)
|
||||
return a == b;
|
||||
return std::strcmp(a, b) == 0;
|
||||
}
|
||||
|
||||
bool SameTextNoCase(const char *a, const char *b) {
|
||||
if (a == nullptr || b == nullptr)
|
||||
return a == b;
|
||||
return _stricmp(a, b) == 0;
|
||||
}
|
||||
|
||||
Entry *Find(Kind kind, const char *table, const char *name, const void *registry) {
|
||||
for (Entry &e : g_entries) {
|
||||
if (e.kind == kind && e.registry == registry && SameText(e.table, table) &&
|
||||
SameText(e.name, name))
|
||||
return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Record(Kind kind, const char *table, const char *name, const void *registry,
|
||||
const Function *doc) {
|
||||
if (g_pass == ENV_NONE || name == nullptr)
|
||||
return;
|
||||
if (Entry *e = Find(kind, table, name, registry)) {
|
||||
e->env |= g_pass;
|
||||
if (e->doc == nullptr)
|
||||
e->doc = doc;
|
||||
return;
|
||||
}
|
||||
Entry e{};
|
||||
e.kind = kind;
|
||||
e.env = g_pass;
|
||||
e.table = table;
|
||||
e.name = name;
|
||||
e.registry = registry;
|
||||
e.doc = doc;
|
||||
g_entries.push_back(e);
|
||||
}
|
||||
|
||||
// ---- System resolution ------------------------------------------------------
|
||||
|
||||
// Blizzard's names for the widget-method systems, keyed by the engine
|
||||
// registry each `FrameMethodEntry[]` is bound to.
|
||||
struct WidgetSystem {
|
||||
uintptr_t registry;
|
||||
const char *system;
|
||||
};
|
||||
const WidgetSystem kWidgetSystems[] = {
|
||||
{Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY, "GameTooltipAPI"},
|
||||
{Offsets::VAR_REGION_METHOD_REGISTRY, "ScriptRegionAPI"},
|
||||
{Offsets::VAR_FRAME_METHOD_REGISTRY, "SimpleFrameAPI"},
|
||||
{Offsets::VAR_TEXTURE_METHOD_REGISTRY, "SimpleTextureAPI"},
|
||||
{Offsets::VAR_FONTSTRING_METHOD_REGISTRY, "SimpleFontStringAPI"},
|
||||
{Offsets::VAR_FONT_METHOD_REGISTRY, "SimpleFontAPI"},
|
||||
{Offsets::VAR_EDITBOX_METHOD_REGISTRY, "SimpleEditBoxAPI"},
|
||||
{Offsets::VAR_MODEL_METHOD_REGISTRY, "SimpleModelAPI"},
|
||||
};
|
||||
|
||||
// Hashes of the frame classes this DLL adds (NameWidgetSystem).
|
||||
std::vector<WidgetSystem> g_ownWidgetSystems;
|
||||
|
||||
const char *WidgetSystemFor(const void *registry) {
|
||||
for (const WidgetSystem &w : kWidgetSystems)
|
||||
if (reinterpret_cast<const void *>(w.registry) == registry)
|
||||
return w.system;
|
||||
for (const WidgetSystem &w : g_ownWidgetSystems)
|
||||
if (reinterpret_cast<const void *>(w.registry) == registry)
|
||||
return w.system;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// "C_Spell" -> "Spell"; anything else is its own system name.
|
||||
const char *SystemNameForTable(const char *table) {
|
||||
if (table[0] == 'C' && table[1] == '_' && table[2] != '\0')
|
||||
return table + 2;
|
||||
return table;
|
||||
}
|
||||
|
||||
// The System an entry contributes a function to, or nullptr when it has
|
||||
// none (an undocumented global, a frame method on an unmapped registry).
|
||||
const char *SystemOf(const Entry &e) {
|
||||
switch (e.kind) {
|
||||
case KIND_TABLE:
|
||||
return SystemNameForTable(e.table);
|
||||
case KIND_GLOBAL:
|
||||
return e.doc != nullptr ? e.doc->system : nullptr;
|
||||
case KIND_FRAME_METHOD:
|
||||
return WidgetSystemFor(e.registry);
|
||||
case KIND_ENUM:
|
||||
return e.enumSystem;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
struct SystemInfo {
|
||||
const char *name;
|
||||
const char *ns; // nullptr for global and widget systems
|
||||
const char *type; // "System" or "ScriptObject"
|
||||
unsigned env; // Env bits; 0 when nothing recorded (structures only)
|
||||
};
|
||||
|
||||
// Merge one entry's contribution into the system list.
|
||||
void AddSystem(std::vector<SystemInfo> &systems, const char *name, const char *ns,
|
||||
const char *type, unsigned env) {
|
||||
if (name == nullptr)
|
||||
return;
|
||||
for (SystemInfo &s : systems) {
|
||||
if (SameTextNoCase(s.name, name)) {
|
||||
if (s.ns == nullptr)
|
||||
s.ns = ns;
|
||||
if (type != nullptr)
|
||||
s.type = type;
|
||||
s.env |= env;
|
||||
return;
|
||||
}
|
||||
}
|
||||
systems.push_back(SystemInfo{name, ns, type != nullptr ? type : "System", env});
|
||||
}
|
||||
|
||||
std::vector<SystemInfo> CollectSystems() {
|
||||
std::vector<SystemInfo> systems;
|
||||
for (const Entry &e : g_entries) {
|
||||
switch (e.kind) {
|
||||
case KIND_TABLE:
|
||||
AddSystem(systems, SystemNameForTable(e.table), e.table, "System", e.env);
|
||||
break;
|
||||
case KIND_GLOBAL:
|
||||
AddSystem(systems, SystemOf(e), nullptr, "System", e.env);
|
||||
break;
|
||||
case KIND_FRAME_METHOD:
|
||||
AddSystem(systems, SystemOf(e), nullptr, "ScriptObject", e.env);
|
||||
break;
|
||||
case KIND_ENUM:
|
||||
AddSystem(systems, e.enumSystem, nullptr, nullptr, 0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const Structure *s = g_structureHead; s != nullptr; s = s->next)
|
||||
AddSystem(systems, s->system, nullptr, nullptr, 0);
|
||||
const int events = Event::Custom::ReservedCount();
|
||||
for (int i = 0; i < events; ++i) {
|
||||
const EventDoc *doc = Event::Custom::ReservedDoc(i);
|
||||
if (doc != nullptr)
|
||||
AddSystem(systems, doc->system, nullptr, nullptr, 0);
|
||||
}
|
||||
return systems;
|
||||
}
|
||||
|
||||
const SystemInfo *FindSystem(const std::vector<SystemInfo> &systems, const char *query) {
|
||||
for (const SystemInfo &s : systems)
|
||||
if (SameTextNoCase(s.name, query) || SameTextNoCase(s.ns, query))
|
||||
return &s;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *EnvironmentName(unsigned env) {
|
||||
if (env == (ENV_GAME | ENV_GLUE) || env == 0)
|
||||
return "All";
|
||||
return env == ENV_GLUE ? "Glue" : "Game";
|
||||
}
|
||||
|
||||
// ---- Export builders (each leaves exactly what its comment says) -------------
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// Sets `Documentation = { line1 [, line2 [, line3]] }` on the table at
|
||||
// stack[-1]; no-op when every line is null.
|
||||
void SetDocumentation(void *L, const char *l1, const char *l2 = nullptr,
|
||||
const char *l3 = nullptr) {
|
||||
if (l1 == nullptr && l2 == nullptr && l3 == nullptr)
|
||||
return;
|
||||
Lua::PushString(L, "Documentation");
|
||||
Lua::NewTable(L);
|
||||
int n = 0;
|
||||
for (const char *line : {l1, l2, l3}) {
|
||||
if (line == nullptr)
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
Lua::PushString(L, line);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// A Lua literal from a descriptor's `defaultValue`, pushed as the value it
|
||||
// denotes: `true`/`false` -> boolean, a quoted string -> string, else number.
|
||||
void PushDefault(void *L, const char *literal) {
|
||||
if (std::strcmp(literal, "true") == 0) {
|
||||
Lua::PushBool(L, true);
|
||||
} else if (std::strcmp(literal, "false") == 0) {
|
||||
Lua::PushBool(L, false);
|
||||
} else if (literal[0] == '"') {
|
||||
const size_t len = std::strlen(literal);
|
||||
Lua::PushLString(L, literal + 1, static_cast<unsigned>(len >= 2 ? len - 2 : 0));
|
||||
} else {
|
||||
Lua::PushNumber(L, std::strtod(literal, nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
// Pushes one field record: { Name, Type, Nilable [, Default] [, Documentation] }.
|
||||
void PushField(void *L, const Field &f) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", f.name);
|
||||
Lua::SetFieldString(L, "Type", f.type);
|
||||
Lua::SetFieldBool(L, "Nilable", f.nilable);
|
||||
if (f.defaultValue != nullptr) {
|
||||
Lua::PushString(L, "Default");
|
||||
PushDefault(L, f.defaultValue);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
SetDocumentation(L, f.doc);
|
||||
}
|
||||
|
||||
// Sets `stack[-1][key] = { field records }`; no-op for an empty list.
|
||||
void SetFieldArray(void *L, const char *key, FieldList list) {
|
||||
if (list.count <= 0)
|
||||
return;
|
||||
Lua::PushString(L, key);
|
||||
Lua::NewTable(L);
|
||||
for (int i = 0; i < list.count; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
PushField(L, list.data[i]);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
constexpr const char *kExtensionNote = "ClassicAPI extension - not part of the Blizzard API.";
|
||||
constexpr const char *kGlueOnlyNote = "Available only on the glue screen.";
|
||||
constexpr const char *kGameOnlyNote = "Available only in-game.";
|
||||
// An undocumented function lists with no arguments, which is indistinguishable
|
||||
// from a function that genuinely takes none. Say which it is, so an empty
|
||||
// signature is never read as a real one.
|
||||
constexpr const char *kUndocumentedNote =
|
||||
"Not documented yet. Its arguments and return values are not listed here.";
|
||||
|
||||
// Pushes one function record. An undocumented entry still lists by name so
|
||||
// `/classicapi <System> list` shows the whole surface; its shape arrives with
|
||||
// the descriptor.
|
||||
void PushFunction(void *L, const Entry &e, unsigned systemEnv) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.name);
|
||||
Lua::SetFieldString(L, "Type", "Function");
|
||||
const char *envNote = nullptr;
|
||||
if (e.env != systemEnv && e.env != 0)
|
||||
envNote = (e.env == ENV_GLUE) ? kGlueOnlyNote : kGameOnlyNote;
|
||||
if (e.doc == nullptr) {
|
||||
SetDocumentation(L, kUndocumentedNote, envNote);
|
||||
return;
|
||||
}
|
||||
SetDocumentation(L, e.doc->summary, e.doc->extension ? kExtensionNote : nullptr, envNote);
|
||||
SetFieldArray(L, "Arguments", e.doc->args);
|
||||
SetFieldArray(L, "Returns", e.doc->rets);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Enumeration", NumValues, MinValue, MaxValue,
|
||||
// Fields = { { Name, Type = <enum>, EnumValue } } }.
|
||||
void PushEnumeration(void *L, const Entry &e) {
|
||||
int minValue = 0, maxValue = 0;
|
||||
for (int i = 0; i < e.enumCount; ++i) {
|
||||
const int v = e.enumEntries[i].value;
|
||||
if (i == 0 || v < minValue)
|
||||
minValue = v;
|
||||
if (i == 0 || v > maxValue)
|
||||
maxValue = v;
|
||||
}
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.name);
|
||||
Lua::SetFieldString(L, "Type", "Enumeration");
|
||||
Lua::SetFieldNumber(L, "NumValues", e.enumCount);
|
||||
Lua::SetFieldNumber(L, "MinValue", minValue);
|
||||
Lua::SetFieldNumber(L, "MaxValue", maxValue);
|
||||
Lua::PushString(L, "Fields");
|
||||
Lua::NewTable(L);
|
||||
for (int i = 0; i < e.enumCount; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.enumEntries[i].key);
|
||||
Lua::SetFieldString(L, "Type", e.name);
|
||||
Lua::SetFieldNumber(L, "EnumValue", e.enumEntries[i].value);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Structure" [, Documentation], Fields = { … } }.
|
||||
void PushStructure(void *L, const Structure &s) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", s.name);
|
||||
Lua::SetFieldString(L, "Type", "Structure");
|
||||
SetDocumentation(L, s.summary);
|
||||
SetFieldArray(L, "Fields", s.fields);
|
||||
}
|
||||
|
||||
// "LOSS_OF_CONTROL_ADDED" -> "LossOfControlAdded" (Blizzard's event Name).
|
||||
void PascalCase(const char *literal, char *out, size_t cap) {
|
||||
size_t o = 0;
|
||||
bool upper = true;
|
||||
for (const char *p = literal; *p != '\0' && o + 1 < cap; ++p) {
|
||||
if (*p == '_') {
|
||||
upper = true;
|
||||
continue;
|
||||
}
|
||||
const unsigned char c = static_cast<unsigned char>(*p);
|
||||
out[o++] = static_cast<char>(upper ? std::toupper(c) : std::tolower(c));
|
||||
upper = false;
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Event", LiteralName [, Documentation] [, Payload] }.
|
||||
void PushEvent(void *L, const char *literal, const EventDoc &doc) {
|
||||
char name[128];
|
||||
PascalCase(literal, name, sizeof name);
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", name);
|
||||
Lua::SetFieldString(L, "Type", "Event");
|
||||
Lua::SetFieldString(L, "LiteralName", literal);
|
||||
SetDocumentation(L, doc.summary);
|
||||
SetFieldArray(L, "Payload", doc.payload);
|
||||
}
|
||||
|
||||
// Pushes the whole System table for `sys`.
|
||||
void PushSystem(void *L, const SystemInfo &sys) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", sys.name);
|
||||
Lua::SetFieldString(L, "Type", sys.type);
|
||||
if (sys.ns != nullptr)
|
||||
Lua::SetFieldString(L, "Namespace", sys.ns);
|
||||
Lua::SetFieldString(L, "Environment", EnvironmentName(sys.env));
|
||||
|
||||
Lua::PushString(L, "Functions");
|
||||
Lua::NewTable(L);
|
||||
int n = 0;
|
||||
for (const Entry &e : g_entries) {
|
||||
if (e.kind != KIND_GLOBAL && e.kind != KIND_TABLE && e.kind != KIND_FRAME_METHOD)
|
||||
continue;
|
||||
if (!SameTextNoCase(SystemOf(e), sys.name) || IsDebugName(e.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushFunction(L, e, sys.env);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
|
||||
Lua::PushString(L, "Events");
|
||||
Lua::NewTable(L);
|
||||
n = 0;
|
||||
const int events = Event::Custom::ReservedCount();
|
||||
for (int i = 0; i < events; ++i) {
|
||||
const EventDoc *doc = Event::Custom::ReservedDoc(i);
|
||||
if (doc == nullptr || !SameTextNoCase(doc->system, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushEvent(L, Event::Custom::ReservedName(i), *doc);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
|
||||
Lua::PushString(L, "Tables");
|
||||
Lua::NewTable(L);
|
||||
n = 0;
|
||||
for (const Structure *s = g_structureHead; s != nullptr; s = s->next) {
|
||||
if (!SameTextNoCase(s->system, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushStructure(L, *s);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
for (const Entry &e : g_entries) {
|
||||
if (e.kind != KIND_ENUM || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushEnumeration(L, e);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// ---- Coverage ---------------------------------------------------------------
|
||||
|
||||
std::vector<std::string> Undocumented() {
|
||||
std::vector<std::string> out;
|
||||
const std::vector<SystemInfo> systems = CollectSystems();
|
||||
for (const Entry &e : g_entries) {
|
||||
if (IsDebugName(e.name))
|
||||
continue;
|
||||
switch (e.kind) {
|
||||
case KIND_GLOBAL:
|
||||
if (e.doc == nullptr) {
|
||||
out.push_back(e.name);
|
||||
} else if (e.doc->system == nullptr) {
|
||||
out.push_back(std::string(e.name) + " (doc has no system)");
|
||||
} else if (const SystemInfo *s = FindSystem(systems, e.doc->system);
|
||||
s != nullptr && s->ns != nullptr) {
|
||||
out.push_back(std::string(e.name) + " (system '" + e.doc->system +
|
||||
"' is namespaced)");
|
||||
}
|
||||
break;
|
||||
case KIND_TABLE:
|
||||
if (e.doc == nullptr)
|
||||
out.push_back(std::string(e.table) + "." + e.name);
|
||||
break;
|
||||
case KIND_FRAME_METHOD: {
|
||||
const char *sys = WidgetSystemFor(e.registry);
|
||||
if (sys == nullptr)
|
||||
out.push_back(std::string(e.name) + " (method registry not mapped)");
|
||||
else if (e.doc == nullptr)
|
||||
out.push_back(std::string(sys) + ":" + e.name);
|
||||
break;
|
||||
}
|
||||
case KIND_ORPHAN_METHOD_DOC: {
|
||||
const char *sys = WidgetSystemFor(e.registry);
|
||||
out.push_back(std::string(sys != nullptr ? sys : "?") + ":" + e.name +
|
||||
" (doc for unregistered method)");
|
||||
break;
|
||||
}
|
||||
case KIND_ENUM:
|
||||
if (e.enumSystem == nullptr)
|
||||
out.push_back(std::string(e.table) + "." + e.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const int events = Event::Custom::ReservedCount();
|
||||
for (int i = 0; i < events; ++i)
|
||||
if (Event::Custom::ReservedDoc(i) == nullptr)
|
||||
out.push_back(std::string("event ") + Event::Custom::ReservedName(i));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- Lua surface ------------------------------------------------------------
|
||||
|
||||
int __fastcall Script_GetSystems(void *L) {
|
||||
std::vector<SystemInfo> systems = CollectSystems();
|
||||
std::sort(systems.begin(), systems.end(), [](const SystemInfo &a, const SystemInfo &b) {
|
||||
return _stricmp(a.name, b.name) < 0;
|
||||
});
|
||||
Lua::NewTable(L);
|
||||
for (size_t i = 0; i < systems.size(); ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::PushString(L, systems[i].name);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSystem(void *L) {
|
||||
if (!Lua::IsString(L, 1)) {
|
||||
Lua::Error(L, "Usage: C_APIDocumentation.GetSystem(name)");
|
||||
return 0;
|
||||
}
|
||||
const char *query = Lua::ToString(L, 1);
|
||||
const std::vector<SystemInfo> systems = CollectSystems();
|
||||
const SystemInfo *sys = FindSystem(systems, query);
|
||||
if (sys == nullptr)
|
||||
return 0; // nil
|
||||
Lua::CheckStack(L, 32);
|
||||
PushSystem(L, *sys);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_UndocumentedAPI(void *L) {
|
||||
const std::vector<std::string> names = Undocumented();
|
||||
Lua::NewTable(L);
|
||||
for (size_t i = 0; i < names.size(); ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::PushString(L, names[i].c_str());
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::PushNumber(L, static_cast<double>(names.size()));
|
||||
return 2;
|
||||
}
|
||||
|
||||
const Field kGetSystemsRets[] = {
|
||||
Game::Doc::Req("systems", "table", "Array of system Names, sorted without regard to case."),
|
||||
};
|
||||
const Function kGetSystems{
|
||||
"Returns the name of every documented system.",
|
||||
{}, kGetSystemsRets, nullptr, true};
|
||||
|
||||
const Field kGetSystemArgs[] = {
|
||||
Game::Doc::Req("system", "string", "A system Name or Namespace; case does not matter."),
|
||||
};
|
||||
const Field kGetSystemRets[] = {
|
||||
Game::Doc::Opt("system", "table", nullptr,
|
||||
"The system in Blizzard's APIDocumentation table shape, or nil when unknown."),
|
||||
};
|
||||
const Function kGetSystem{
|
||||
"Returns one system's documentation table.",
|
||||
kGetSystemArgs, kGetSystemRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_APIDocumentation", "GetSystems", &Script_GetSystems,
|
||||
&kGetSystems);
|
||||
Game::Lua::RegisterTableFunction("C_APIDocumentation", "GetSystem", &Script_GetSystem,
|
||||
&kGetSystem);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_UndocumentedAPI", &Script_UndocumentedAPI);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- Recording (called from Game.cpp) ----------------------------------------
|
||||
|
||||
void BeginPass(Env env) {
|
||||
g_pass = (g_sealed & env) != 0 ? ENV_NONE : env;
|
||||
}
|
||||
|
||||
void EndPass() {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
g_sealed |= g_pass;
|
||||
g_pass = ENV_NONE;
|
||||
}
|
||||
|
||||
void RecordGlobal(const char *name, const Function *doc) {
|
||||
Record(KIND_GLOBAL, nullptr, name, nullptr, doc);
|
||||
}
|
||||
|
||||
void RecordTable(const char *table, const char *name, const Function *doc) {
|
||||
Record(KIND_TABLE, table, name, nullptr, doc);
|
||||
}
|
||||
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Method *docs, int docCount) {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const Function *doc = nullptr;
|
||||
for (int d = 0; d < docCount; ++d) {
|
||||
if (std::strcmp(docs[d].name, table[i].name) == 0) {
|
||||
doc = docs[d].doc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Record(KIND_FRAME_METHOD, nullptr, table[i].name, registry, doc);
|
||||
}
|
||||
for (int d = 0; d < docCount; ++d) {
|
||||
bool matched = false;
|
||||
for (int i = 0; i < count && !matched; ++i)
|
||||
matched = std::strcmp(docs[d].name, table[i].name) == 0;
|
||||
if (!matched)
|
||||
Record(KIND_ORPHAN_METHOD_DOC, nullptr, docs[d].name, registry, docs[d].doc);
|
||||
}
|
||||
}
|
||||
|
||||
void NameWidgetSystem(const void *registry, const char *system) {
|
||||
if (WidgetSystemFor(registry) != nullptr)
|
||||
return;
|
||||
g_ownWidgetSystems.push_back({reinterpret_cast<uintptr_t>(registry), system});
|
||||
}
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
if (g_pass == ENV_NONE || sub == nullptr)
|
||||
return;
|
||||
if (Entry *e = Find(KIND_ENUM, parent, sub, nullptr)) {
|
||||
e->env |= g_pass;
|
||||
if (e->enumSystem == nullptr)
|
||||
e->enumSystem = system;
|
||||
return;
|
||||
}
|
||||
Entry e{};
|
||||
e.kind = KIND_ENUM;
|
||||
e.env = g_pass;
|
||||
e.table = parent;
|
||||
e.name = sub;
|
||||
e.enumSystem = system;
|
||||
e.enumEntries = entries;
|
||||
e.enumCount = count;
|
||||
g_entries.push_back(e);
|
||||
}
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
// The Structure constructor lives here so the list head is private to this
|
||||
// TU; declared in Game.h beside the other descriptor types.
|
||||
Game::Doc::Structure::Structure(const char *n, const char *s, FieldList f, const char *sum)
|
||||
: name(n), system(s), fields(f), summary(sum), next(Api::Documentation::g_structureHead) {
|
||||
Api::Documentation::g_structureHead = this;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
// Recording side of the API documentation. The registrars in Game.cpp call
|
||||
// these; `Documentation.cpp` keeps the list, resolves systems, and exports
|
||||
// Blizzard-shaped `APIDocumentation` tables through `C_APIDocumentation`.
|
||||
// The descriptor model is the `Game::Doc` block in Game.h.
|
||||
namespace Api::Documentation {
|
||||
|
||||
enum Env : unsigned { ENV_NONE = 0, ENV_GAME = 1, ENV_GLUE = 2 };
|
||||
|
||||
// Bracket one `Run*Registrations()` pass. Recording happens only inside a
|
||||
// pass, and only the FIRST pass per environment records — every later pass
|
||||
// (each `/reload`, each logout→login) is a no-op. `EndPass` seals the
|
||||
// environment and logs one summary line to the debug log.
|
||||
void BeginPass(Env env);
|
||||
void EndPass();
|
||||
|
||||
void RecordGlobal(const char *name, const Game::Doc::Function *doc);
|
||||
void RecordTable(const char *table, const char *name, const Game::Doc::Function *doc);
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Game::Doc::Method *docs, int docCount);
|
||||
// Names the widget system of a method hash this DLL owns (a frame class it
|
||||
// adds), alongside the engine registries the exporter already knows.
|
||||
void NameWidgetSystem(const void *registry, const char *system);
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system);
|
||||
|
||||
} // namespace Api::Documentation
|
||||
@@ -0,0 +1,69 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// CanCancelAuction(index) — 4.3.4's Script_CanCancelAuction (0x00975180):
|
||||
// nothing without an auctioneer, nil for an owner-list index with no row or a
|
||||
// sold row, else 1. The gates are 1.12's own Script_CancelAuction (0x004D0260):
|
||||
// auctioneer GUID set, ftol(index) - 1 below the owner count (unsigned, so 0 and
|
||||
// negatives miss), non-null record. 1.12's owner records carry no sold flag, so
|
||||
// that 4.3.4 gate has nothing to read. The cancel-fee money check stays in
|
||||
// CancelAuction, as in 4.3.4.
|
||||
|
||||
namespace AuctionHouse::Cancel {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_CanCancelAuction(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: CanCancelAuction(index)");
|
||||
return 0;
|
||||
}
|
||||
if (Game::Read<uint32_t>(Offsets::VAR_AUCTION_AUCTIONEER_GUID_LO) == 0 &&
|
||||
Game::Read<uint32_t>(Offsets::VAR_AUCTION_AUCTIONEER_GUID_HI) == 0)
|
||||
return 0;
|
||||
|
||||
const uint32_t index = static_cast<uint32_t>(static_cast<int64_t>(Game::Lua::ToNumber(L, 1))) - 1;
|
||||
const auto *entries = reinterpret_cast<void *const *>(Offsets::VAR_AUCTION_OWNER_ENTRIES);
|
||||
if (index < Game::Read<uint32_t>(Offsets::VAR_AUCTION_OWNER_COUNT) && entries[index] != nullptr)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("index", "number", "Row in the \"owner\" auction list, from 1."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("canCancel", "number", nullptr,
|
||||
"1 when the row is one of the player's auctions, else nil. Nothing when the auction house is closed."),
|
||||
};
|
||||
const Game::Doc::Function kCanCancelAuction{
|
||||
"Returns whether CancelAuction can cancel a row of the owner auction list.", kArgs, kRets,
|
||||
"AuctionHouseGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("CanCancelAuction", &Script_CanCancelAuction, &kCanCancelAuction);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace AuctionHouse::Cancel
|
||||
+447
-40
@@ -19,23 +19,29 @@
|
||||
//
|
||||
// Filter parsing (`ParseFilters`) tokenizes the modern AuraFilters string:
|
||||
// tokens separated by `|` and/or whitespace, each optionally negated with a
|
||||
// leading `!`. Whole-token matching, so `RAID_PLAYER_DISPELLABLE` is not
|
||||
// mistaken for `PLAYER` and `!PLAYER` is a negation rather than a match.
|
||||
// Honored: `HELPFUL` / `HARMFUL` (descriptor slot ranges), `PLAYER` /
|
||||
// `!PLAYER` (caster == / != the local player, from the Aura::Source cache),
|
||||
// leading `!`, compared ignoring case as 4.3.4's parser does. Whole-token
|
||||
// matching, so `RAID_PLAYER_DISPELLABLE` is not mistaken for `PLAYER` and
|
||||
// `!PLAYER` is a negation rather than a match.
|
||||
// Honored: `HELPFUL` / `HARMFUL` (the aura's real polarity, read however this
|
||||
// server encodes it — see `Aura::Data::IsSlotHarmful`), `PLAYER` /
|
||||
// `!PLAYER` (caster is / isn't the local player or a unit they own, from the
|
||||
// Aura::Source cache), `RAID` (the engine's raid filter — see
|
||||
// `Aura::Data::IsRaidAura`), `CANCELABLE` / `NOT_CANCELABLE` (the descriptor's
|
||||
// cancelable bit),
|
||||
// `DISPELLABLE` / `!DISPELLABLE` (dispel type is / isn't one a
|
||||
// dispel/purge/steal can remove — Spell.dbc Dispel ∈ Magic/Curse/Disease/
|
||||
// Poison, matching the server's DISPEL_ALL_MASK), and `CROWD_CONTROL` /
|
||||
// `!CROWD_CONTROL` (is / isn't a hard control effect, via the shared
|
||||
// `Spell::CrowdControl` classifier that also backs C_LossOfControl). Every
|
||||
// other modern token (`RAID`, `CANCELABLE`, `INCLUDE_NAME_PLATE_ONLY`, `MAW`,
|
||||
// …) is accepted and ignored — they need a class-dispel matrix or systems
|
||||
// vanilla has no data for.
|
||||
// other modern token (`INCLUDE_NAME_PLATE_ONLY`, `MAW`, …) is accepted and
|
||||
// ignored — vanilla has no data for them.
|
||||
|
||||
#include "Data.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Instance.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "ui/ColorData.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
@@ -73,8 +79,10 @@ struct ParsedFilter {
|
||||
Data::CasterMode caster = Data::CasterMode::Any;
|
||||
Data::DispelMode dispel = Data::DispelMode::Any;
|
||||
Data::CcMode cc = Data::CcMode::Any;
|
||||
Data::CancelMode cancel = Data::CancelMode::Any;
|
||||
bool raid = false;
|
||||
|
||||
Data::Match ToMatch() const { return Data::Match{caster, dispel, cc}; }
|
||||
Data::Match ToMatch() const { return Data::Match{caster, dispel, cc, cancel, raid}; }
|
||||
};
|
||||
|
||||
// Reduces the parsed range tokens to the single `Data::Filter` the indexed /
|
||||
@@ -86,8 +94,8 @@ Data::Filter RangeFilter(const ParsedFilter &pf) {
|
||||
: Data::Filter::Helpful;
|
||||
}
|
||||
|
||||
// Tokenizes the filter string. Case-sensitive (modern documents the tokens as
|
||||
// upper-case constants). `HELPFUL`/`HARMFUL` are positive range selectors;
|
||||
// Tokenizes the filter string, ignoring case (4.3.4's Script_GetAuraInfo
|
||||
// compares tokens with SStrCmpI). `HELPFUL`/`HARMFUL` are positive range selectors;
|
||||
// negating them is meaningless for our slot-based ranges, so a leading `!` is
|
||||
// ignored on those. `PLAYER` honors negation (`!PLAYER` → NotPlayer). Any
|
||||
// unrecognized token is accepted and skipped.
|
||||
@@ -114,17 +122,25 @@ ParsedFilter ParseFilters(const char *filter) {
|
||||
}
|
||||
tok[n] = '\0';
|
||||
|
||||
if (strcmp(tok, "HELPFUL") == 0)
|
||||
if (_stricmp(tok, "HELPFUL") == 0)
|
||||
out.helpful = true;
|
||||
else if (strcmp(tok, "HARMFUL") == 0)
|
||||
else if (_stricmp(tok, "HARMFUL") == 0)
|
||||
out.harmful = true;
|
||||
else if (strcmp(tok, "PLAYER") == 0)
|
||||
else if (_stricmp(tok, "PLAYER") == 0)
|
||||
out.caster = negate ? Data::CasterMode::NotPlayer
|
||||
: Data::CasterMode::PlayerOnly;
|
||||
else if (strcmp(tok, "DISPELLABLE") == 0)
|
||||
else if (_stricmp(tok, "RAID") == 0)
|
||||
out.raid = !negate;
|
||||
else if (_stricmp(tok, "CANCELABLE") == 0)
|
||||
out.cancel = negate ? Data::CancelMode::NotCancelable
|
||||
: Data::CancelMode::CancelableOnly;
|
||||
else if (_stricmp(tok, "NOT_CANCELABLE") == 0)
|
||||
out.cancel = negate ? Data::CancelMode::CancelableOnly
|
||||
: Data::CancelMode::NotCancelable;
|
||||
else if (_stricmp(tok, "DISPELLABLE") == 0)
|
||||
out.dispel = negate ? Data::DispelMode::NotDispellable
|
||||
: Data::DispelMode::DispellableOnly;
|
||||
else if (strcmp(tok, "CROWD_CONTROL") == 0)
|
||||
else if (_stricmp(tok, "CROWD_CONTROL") == 0)
|
||||
out.cc = negate ? Data::CcMode::NotCrowdControl
|
||||
: Data::CcMode::CrowdControlOnly;
|
||||
// else: accepted and ignored (retail-only / unimplemented token).
|
||||
@@ -352,22 +368,177 @@ int __fastcall Script_GetPlayerAuraBySpellID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Iterates one slot range and pushes AuraData tables into `outer` at
|
||||
// sequential keys starting from `nextKey`. Updates `nextKey` so a
|
||||
// follow-up call can append to the same outer table.
|
||||
// C_UnitAuras.GetAuraSlots(unit [, filter [, maxSlots [, continuationToken]]])
|
||||
// -> continuationToken, slot1, slot2, ...
|
||||
//
|
||||
// Enumerates the opaque slot ids of every aura on `unit` matching `filter`, in
|
||||
// the order the by-index getters visit them, `maxSlots` at a time (nil / 0 =
|
||||
// all). The first return is the token to pass back for the next batch, or nil
|
||||
// when this batch reached the end — retail's batching contract, and what makes
|
||||
// AuraUtil.ForEachAura linear: one enumeration per batch plus a direct by-slot
|
||||
// fetch per aura, instead of a fresh from-slot-0 walk per index. The token is
|
||||
// the 1-based ordinal to resume at; opaque to callers. Encoding of the slot ids
|
||||
// is documented at `Data::OPAQUE_STRIDE`.
|
||||
int __fastcall Script_GetAuraSlots(void *L) {
|
||||
const char *unitToken = ArgUnit(L, 1);
|
||||
const char *filterStr = ArgOptString(L, 2);
|
||||
const int maxSlots = ArgInt(L, 3);
|
||||
const int token = ArgInt(L, 4);
|
||||
// ClassicAPI extension: a table as the 5th argument selects FILL mode —
|
||||
// the slot ids are written into it (t[1..n], stale tail cleared, t.n = n)
|
||||
// and the call returns `continuationToken, n` instead of the vararg list.
|
||||
// Lua 5.0 builds an `arg` table for every vararg CALL, so a Lua helper
|
||||
// that receives the retail multi-return allocates one table per batch;
|
||||
// filling the caller's table here is what keeps a per-frame scan
|
||||
// allocation-free (AuraUtil and pfUI's ScanAuraSlots use this form).
|
||||
const bool fill = Game::Lua::Type(L, 5) == Game::Lua::TYPE_TABLE;
|
||||
|
||||
int slots[Data::OPAQUE_SLOTS_MAX];
|
||||
int total = 0;
|
||||
if (unitToken != nullptr) {
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
const uint64_t guid = (unit == nullptr) ? GuidForOutOfRange(unitToken) : 0;
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
total = Data::CollectSlots(unit, guid, RangeFilter(pf), pf.ToMatch(), slots,
|
||||
Data::OPAQUE_SLOTS_MAX);
|
||||
}
|
||||
int start = (token > 0) ? token - 1 : 0;
|
||||
if (start > total)
|
||||
start = total;
|
||||
int n = total - start;
|
||||
if (maxSlots > 0 && n > maxSlots)
|
||||
n = maxSlots;
|
||||
const bool more = start + n < total;
|
||||
|
||||
if (fill) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slots[start + i]));
|
||||
Game::Lua::RawSet(L, 5);
|
||||
}
|
||||
// Clear what a previous, longer fill left past n. The array is dense
|
||||
// by construction, so stop at the first hole.
|
||||
for (int k = n + 1;; ++k) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(k));
|
||||
Game::Lua::RawGet(L, 5);
|
||||
const bool hole = Game::Lua::Type(L, -1) == Game::Lua::TYPE_NIL;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (hole)
|
||||
break;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(k));
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::RawSet(L, 5);
|
||||
}
|
||||
Game::Lua::PushString(L, "n");
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
Game::Lua::RawSet(L, 5);
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (more)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(start + n + 1));
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Vararg form. Everything below only pushes, so the args can go; a full
|
||||
// batch is up to OPAQUE_SLOTS_MAX + 1 values, past Lua's guaranteed C-call
|
||||
// headroom.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (Game::Lua::CheckStack(L, n + 1) == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (more)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(start + n + 1));
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
for (int i = 0; i < n; ++i)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slots[start + i]));
|
||||
return 1 + n;
|
||||
}
|
||||
|
||||
// Shared body of GetAuraDataBySlot (table) / UnitAuraBySlot (positional):
|
||||
// pushes the aura an opaque slot id from GetAuraSlots names, or a single nil
|
||||
// for an id that no longer names one.
|
||||
int PushAuraBySlot(void *L, Data::Emit emit) {
|
||||
const char *unitToken = ArgUnit(L, 1);
|
||||
if (unitToken == nullptr || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int slot = ArgInt(L, 2);
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
const uint64_t guid = (unit == nullptr) ? GuidForOutOfRange(unitToken) : 0;
|
||||
if (Data::PushBySlot(L, unit, guid, slot, emit))
|
||||
return (emit == Data::Emit::Positional) ? 15 : 1;
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_UnitAuras.GetAuraDataByAuraInstanceID(unit, auraInstanceID [, buf])
|
||||
// -> AuraData | nil
|
||||
// With `buf`, the fields go into that table (cleared of any this aura lacks)
|
||||
// and it is returned — the allocation-free form for UNIT_AURA_DELTA handlers
|
||||
// that look up each changed ID. Only auras of a unit in view have IDs.
|
||||
int __fastcall Script_GetAuraDataByAuraInstanceID(void *L) {
|
||||
const char *unitToken = ArgUnit(L, 1);
|
||||
if (unitToken == nullptr || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const auto id = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
const bool fill = Game::Lua::Type(L, 3) == Game::Lua::TYPE_TABLE;
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
const int slot = Aura::Instance::SlotForID(unit, id);
|
||||
if (slot < 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (fill) {
|
||||
Game::Lua::PushValue(L, 3);
|
||||
Data::Push(L, unit, slot, Data::Emit::Fill);
|
||||
} else {
|
||||
Data::Push(L, unit, slot);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kByInstanceIDArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("auraInstanceID", "number"),
|
||||
Game::Doc::Opt("buf", "table", nullptr,
|
||||
"A table to write the aura into and return instead of a new one."),
|
||||
};
|
||||
const Game::Doc::Field kByInstanceIDRets[] = {
|
||||
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when no aura on the unit has that ID."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataByAuraInstanceID{
|
||||
"The aura with the given instance ID on a unit.", kByInstanceIDArgs, kByInstanceIDRets};
|
||||
|
||||
// C_UnitAuras.GetAuraDataBySlot(unit, slot) -> AuraData | nil
|
||||
int __fastcall Script_GetAuraDataBySlot(void *L) {
|
||||
return PushAuraBySlot(L, Data::Emit::Table);
|
||||
}
|
||||
|
||||
// C_UnitAuras.UnitAuraBySlot(unit, slot) -> the 15 positional UnitAura values | nil
|
||||
// Zero-allocation sibling of GetAuraDataBySlot, namespaced under C_UnitAuras
|
||||
// like C_UnitAuras.UnitAura. What AuraUtil's non-packed iteration reads.
|
||||
int __fastcall Script_UnitAuraBySlot(void *L) {
|
||||
return PushAuraBySlot(L, Data::Emit::Positional);
|
||||
}
|
||||
|
||||
// Iterates the auras of one polarity (in `WalkOrder`, the same order the
|
||||
// by-index getters use) and pushes AuraData tables into `outer` at
|
||||
// sequential keys starting from `nextKey`. Updates `nextKey` so a follow-up
|
||||
// call can append to the same outer table.
|
||||
void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
Data::Filter filter, int &nextKey, Data::Match match) {
|
||||
const int start = (filter == Data::Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
const int end = (filter == Data::Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_TOTAL
|
||||
: Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!Data::IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (!Data::MatchesAura(match, Data::IsPlayerCast(unit, slot),
|
||||
Data::ReadSpellID(unit, slot)))
|
||||
int order[Offsets::UNIT_AURA_TOTAL];
|
||||
Data::WalkOrder(unit, filter, order);
|
||||
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
|
||||
const int slot = order[i];
|
||||
if (!Data::SlotMatchesFilter(unit, slot, filter, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
Data::Push(L, unit, slot);
|
||||
@@ -482,31 +653,267 @@ int __fastcall Script_GetAuraDispelTypeColor(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// CancelUnitBuff(unit, index [, filter]) — 4.3.4's Script_CancelUnitBuff.
|
||||
// Only the player's own auras: its other branch exits a vehicle seat, which
|
||||
// 1.12 has no counterpart for. The index counts in `FindNthSlot` order, the
|
||||
// same order 4.3.4's GetSortedAuraSlot walks, so it matches the
|
||||
// C_UnitAuras by-index getters for the same filter (HELPFUL by default).
|
||||
// The cancel gate is the engine's: the slot's cancelable bit, or the spell's
|
||||
// `SPELL_ATTR_EX_CHANNELED_1`, before the CMSG_CANCEL_AURA sender.
|
||||
int __fastcall Script_CancelUnitBuff(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: CancelUnitBuff(\"unit\", index[, \"filter\"])");
|
||||
return 0;
|
||||
}
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr ||
|
||||
Unit::Identity::GuidForToken(Game::Lua::ToString(L, 1)) !=
|
||||
Unit::Identity::PlayerGuid())
|
||||
return 0;
|
||||
const ParsedFilter pf = ParseFilters(ArgOptString(L, 3));
|
||||
const int slot =
|
||||
Data::FindNthSlot(player, ArgInt(L, 2), RangeFilter(pf), pf.ToMatch());
|
||||
if (slot < 0)
|
||||
return 0;
|
||||
const int spellID = static_cast<int>(Data::ReadSpellID(player, slot));
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
const bool cancelable =
|
||||
(Data::SlotNibble(player, slot) & Offsets::UNIT_AURA_FLAG_CANCELABLE) != 0 ||
|
||||
(Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX) &
|
||||
Offsets::SPELL_ATTR_EX_CHANNELED_1) != 0;
|
||||
if (!cancelable)
|
||||
return 0;
|
||||
using CancelAuraSend_t = void(__fastcall *)(int spellID);
|
||||
reinterpret_cast<CancelAuraSend_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CANCEL_AURA_SEND))(spellID);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kCancelUnitBuffArgs[] = {
|
||||
Game::Doc::Req("unit", "string", "Must be the player."),
|
||||
Game::Doc::Req("index", "number",
|
||||
"Which aura of the ones `filter` selects, from 1."),
|
||||
Game::Doc::Opt("filter", "string", "\"HELPFUL\"",
|
||||
"Aura filter tokens, the same as `C_UnitAuras.GetAuraDataByIndex`."),
|
||||
};
|
||||
const Game::Doc::Function kCancelUnitBuff{
|
||||
"Cancels one of your own auras by index.", kCancelUnitBuffArgs, {},
|
||||
"SpellGlobals"};
|
||||
|
||||
const Game::Doc::Field kAuraDataFields[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("icon", "string", "The icon's texture path."),
|
||||
Game::Doc::Req("applications", "number", "The stack count."),
|
||||
Game::Doc::Req("spellId", "number"),
|
||||
Game::Doc::Req("dispelName", "string",
|
||||
"Magic, Curse, Disease, or Poison; empty when the aura has no dispel type."),
|
||||
Game::Doc::Req("isHelpful", "bool"),
|
||||
Game::Doc::Req("isHarmful", "bool"),
|
||||
Game::Doc::Req("duration", "number", "In seconds; 0 when the duration is not known."),
|
||||
Game::Doc::Req("expirationTime", "number",
|
||||
"When the aura ends, in GetTime() seconds; 0 when not known."),
|
||||
Game::Doc::Opt("sourceUnit", "UnitToken", nullptr,
|
||||
"Nil when the cast was not seen or the caster has no unit token."),
|
||||
Game::Doc::Opt("sourceGUID", "WOWGUID", nullptr,
|
||||
"The caster's GUID, a ClassicAPI extension; nil when the cast was not seen."),
|
||||
Game::Doc::Opt("auraInstanceID", "number", nullptr,
|
||||
"Nil for an aura that is not in the unit's aura list."),
|
||||
Game::Doc::Req("charges", "number", "Always 0."),
|
||||
Game::Doc::Req("maxCharges", "number", "Always 0."),
|
||||
Game::Doc::Req("timeMod", "number", "Always 1."),
|
||||
Game::Doc::Req("isFromPlayerOrPlayerPet", "bool",
|
||||
"True when any player or a player's pet applied the aura."),
|
||||
Game::Doc::Req("canApplyAura", "bool",
|
||||
"True when the player or the pet knows the spell that applies the aura."),
|
||||
Game::Doc::Req("isRaid", "bool", "True when the aura passes the RAID filter."),
|
||||
Game::Doc::Req("isStealable", "bool",
|
||||
"True when the player can remove this buff from an enemy."),
|
||||
Game::Doc::Req("isBossAura", "bool", "Always false."),
|
||||
Game::Doc::Req("isNameplateOnly", "bool", "Always false."),
|
||||
Game::Doc::Req("nameplateShowAll", "bool", "Always false."),
|
||||
Game::Doc::Req("nameplateShowPersonal", "bool", "Always false."),
|
||||
Game::Doc::Req("shouldConsolidate", "bool", "Always false."),
|
||||
};
|
||||
const Game::Doc::Structure kAuraData{"AuraData", "UnitAuras", kAuraDataFields,
|
||||
"One aura on a unit."};
|
||||
|
||||
// The Classic Era `UnitAura` tuple, shared by the four positional getters.
|
||||
// `Data::PushPositional` is the source of the order.
|
||||
const Game::Doc::Field kPositionalRets[] = {
|
||||
Game::Doc::Opt("name", "string", nullptr,
|
||||
"Nil, as the only return, when there is no such aura."),
|
||||
Game::Doc::Opt("icon", "string"),
|
||||
Game::Doc::Opt("count", "number"),
|
||||
Game::Doc::Opt("dispelType", "string", nullptr,
|
||||
"Empty when the aura has no dispel type."),
|
||||
Game::Doc::Opt("duration", "number"),
|
||||
Game::Doc::Opt("expirationTime", "number"),
|
||||
Game::Doc::Opt("source", "UnitToken", nullptr, "Nil when the caster is not known."),
|
||||
Game::Doc::Opt("isStealable", "bool"),
|
||||
Game::Doc::Opt("nameplateShowPersonal", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("spellId", "number"),
|
||||
Game::Doc::Opt("canApplyAura", "bool"),
|
||||
Game::Doc::Opt("isBossDebuff", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("castByPlayer", "bool", nullptr,
|
||||
"True when a player applied the aura; false for a pet."),
|
||||
Game::Doc::Opt("nameplateShowAll", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("timeMod", "number", nullptr, "Always 1."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kAuraRets[] = {
|
||||
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when there is no such aura."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kByIndexArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"Tokens such as HELPFUL, HARMFUL, PLAYER, RAID; HELPFUL when no range is given."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataByIndex{
|
||||
"Returns the aura at an index among a unit's auras that match a filter.", kByIndexArgs,
|
||||
kAuraRets};
|
||||
const Game::Doc::Function kUnitAura{
|
||||
"Returns the aura at an index as positional values, without making a table.",
|
||||
kByIndexArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kLockedIndexArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Function kGetBuffDataByIndex{
|
||||
"Returns the buff at an index on a unit.", kLockedIndexArgs, kAuraRets};
|
||||
const Game::Doc::Function kGetDebuffDataByIndex{
|
||||
"Returns the debuff at an index on a unit.", kLockedIndexArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kLockedFilterArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"Further tokens such as PLAYER or RAID; the range is fixed."),
|
||||
};
|
||||
const Game::Doc::Function kUnitBuff{
|
||||
"Returns the buff at an index as positional values, without making a table.",
|
||||
kLockedFilterArgs, kPositionalRets};
|
||||
const Game::Doc::Function kUnitDebuff{
|
||||
"Returns the debuff at an index as positional values, without making a table.",
|
||||
kLockedFilterArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kSlotsArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Opt("filter", "AuraFilters"),
|
||||
Game::Doc::Opt("maxSlots", "number", nullptr, "Nil or 0 returns all of them."),
|
||||
Game::Doc::Opt("continuationToken", "number", nullptr,
|
||||
"The first return of the previous call, to get the next batch."),
|
||||
Game::Doc::Opt("buf", "table", nullptr,
|
||||
"A table to fill with the slot IDs instead; the call then returns the "
|
||||
"token and the count. A ClassicAPI extension."),
|
||||
};
|
||||
const Game::Doc::Field kSlotsRets[] = {
|
||||
Game::Doc::Opt("outContinuationToken", "number", nullptr,
|
||||
"Nil when this batch reaches the last aura."),
|
||||
Game::Doc::Vararg("number", "The slot IDs, valid only in the frame they are returned."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraSlots{
|
||||
"Returns the slot IDs of a unit's auras that match a filter, in batches.", kSlotsArgs,
|
||||
kSlotsRets};
|
||||
|
||||
const Game::Doc::Field kBySlotArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("slot", "number", "A slot ID from C_UnitAuras.GetAuraSlots."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataBySlot{
|
||||
"Returns the aura that a slot ID names.", kBySlotArgs, kAuraRets};
|
||||
const Game::Doc::Function kUnitAuraBySlot{
|
||||
"Returns the aura that a slot ID names as positional values, without making a table.",
|
||||
kBySlotArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kBySpellIDArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
|
||||
};
|
||||
const Game::Doc::Function kGetUnitAuraBySpellID{
|
||||
"Returns the first aura on a unit with a spell ID; the player's own aura comes first.",
|
||||
kBySpellIDArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kPlayerBySpellIDArgs[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetPlayerAuraBySpellID{
|
||||
"Returns the first aura on the player with a spell ID.", kPlayerBySpellIDArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kBySpellNameArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("spellName", "cstring", "The localized name; case does not matter."),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataBySpellName{
|
||||
"Returns the first aura on a unit with a spell name; the player's own aura comes first.",
|
||||
kBySpellNameArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kUnitAurasArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"With no HELPFUL or HARMFUL token, both are returned."),
|
||||
};
|
||||
const Game::Doc::Field kUnitAurasRets[] = {
|
||||
Game::Doc::Req("auras", "table", "An array of AuraData; empty when the unit has none."),
|
||||
};
|
||||
const Game::Doc::Function kGetUnitAuras{
|
||||
"Returns all of a unit's auras that match a filter, buffs first.", kUnitAurasArgs,
|
||||
kUnitAurasRets};
|
||||
|
||||
const Game::Doc::Field kDispelColorArgs[] = {
|
||||
Game::Doc::Opt("dispelName", "string", nullptr,
|
||||
"Magic, Curse, Disease, Poison, or Enrage; any other value gives the "
|
||||
"no-type color."),
|
||||
};
|
||||
const Game::Doc::Field kDispelColorRets[] = {
|
||||
Game::Doc::Req("color", "colorRGBA"),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDispelTypeColor{
|
||||
"Returns the border color for an aura's dispel type.", kDispelColorArgs,
|
||||
kDispelColorRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByIndex",
|
||||
&Script_GetAuraDataByIndex);
|
||||
&Script_GetAuraDataByIndex, &kGetAuraDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetBuffDataByIndex",
|
||||
&Script_GetBuffDataByIndex);
|
||||
&Script_GetBuffDataByIndex, &kGetBuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetDebuffDataByIndex",
|
||||
&Script_GetDebuffDataByIndex);
|
||||
&Script_GetDebuffDataByIndex, &kGetDebuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAura",
|
||||
&Script_UnitAura);
|
||||
&Script_UnitAura, &kUnitAura);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitBuff",
|
||||
&Script_UnitBuff);
|
||||
&Script_UnitBuff, &kUnitBuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
|
||||
&Script_UnitDebuff);
|
||||
&Script_UnitDebuff, &kUnitDebuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
|
||||
&Script_GetAuraSlots, &kGetAuraSlots);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
|
||||
&Script_GetAuraDataBySlot, &kGetAuraDataBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
|
||||
&Script_GetAuraDataByAuraInstanceID,
|
||||
&kGetAuraDataByAuraInstanceID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
|
||||
&Script_UnitAuraBySlot, &kUnitAuraBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
|
||||
&Script_GetUnitAuraBySpellID);
|
||||
&Script_GetUnitAuraBySpellID, &kGetUnitAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
|
||||
&Script_GetPlayerAuraBySpellID);
|
||||
&Script_GetPlayerAuraBySpellID, &kGetPlayerAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySpellName",
|
||||
&Script_GetAuraDataBySpellName);
|
||||
&Script_GetAuraDataBySpellName, &kGetAuraDataBySpellName);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuras",
|
||||
&Script_GetUnitAuras);
|
||||
&Script_GetUnitAuras, &kGetUnitAuras);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDispelTypeColor",
|
||||
&Script_GetAuraDispelTypeColor);
|
||||
&Script_GetAuraDispelTypeColor, &kGetAuraDispelTypeColor);
|
||||
Game::Lua::RegisterGlobalFunction("CancelUnitBuff", &Script_CancelUnitBuff,
|
||||
&kCancelUnitBuff);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -193,9 +193,23 @@ int __fastcall Script_RegisterComboDuration(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
Game::Doc::Req("baseSeconds", "number",
|
||||
"The duration at zero combo points; each point adds a fifth of the "
|
||||
"difference to maxSeconds."),
|
||||
Game::Doc::Req("maxSeconds", "number", "The duration at five combo points."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("success", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kRegisterComboDuration{
|
||||
"Sets the combo point durations of a finisher whose client data is missing or wrong.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterComboDuration",
|
||||
&Script_RegisterComboDuration);
|
||||
&Script_RegisterComboDuration, &kRegisterComboDuration);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
@@ -227,8 +241,10 @@ uint32_t TryComboScaledMs(const uint8_t *spellRecord, uint32_t spellId) {
|
||||
Offsets::VAR_SPELL_DURATION_COUNT,
|
||||
static_cast<uint32_t>(idx));
|
||||
if (row != nullptr) {
|
||||
baseMs = *reinterpret_cast<const int32_t *>(row + 0x4);
|
||||
maxMs = *reinterpret_cast<const int32_t *>(row + 0xC);
|
||||
baseMs = *reinterpret_cast<const int32_t *>(
|
||||
row + Offsets::OFF_SPELLDURATION_BASE_MS);
|
||||
maxMs = *reinterpret_cast<const int32_t *>(
|
||||
row + Offsets::OFF_SPELLDURATION_MAX_MS);
|
||||
} else if (idx != 0) {
|
||||
// Dangling duration index — the client data is broken for this
|
||||
// spell. Defer to a registered resolver (the known values are
|
||||
|
||||
+929
-329
File diff suppressed because it is too large
Load Diff
+179
-36
@@ -10,6 +10,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Shared aura-table primitives consumed by `C_UnitAuras.*` (and any
|
||||
@@ -18,19 +20,38 @@
|
||||
// `unit + OFF_CGUNIT_OBJECT_FIELDS`; the layout is documented in
|
||||
// `Offsets.h` under `OFF_UNIT_FIELD_AURA*`.
|
||||
//
|
||||
// Slot indexing convention used here matches the engine's:
|
||||
// Slots are absolute 0..47, and how polarity is read out of them DEPENDS ON THE
|
||||
// SERVER. `IsSlotHarmful` owns that decision; nothing else should look at either
|
||||
// signal directly.
|
||||
//
|
||||
// slot 0..31 → buffs (helpful)
|
||||
// slot 32..47 → debuffs (harmful)
|
||||
// Normally the slot range IS the polarity: positive auras are written to 0..31
|
||||
// and negative ones to 32..47 by a strict either/or, so nothing crosses over.
|
||||
// The flag nibble is no help — in a stock descriptor those bits record which
|
||||
// spell effect indices carry an aura, not whether the aura is good or bad.
|
||||
//
|
||||
// Callers that take a 1-based Lua index into "buffs" or "debuffs"
|
||||
// translate to the absolute 0..47 slot before calling in.
|
||||
// Turtle changes both halves. Once the 16 harmful slots are full it spills
|
||||
// further debuffs into 0..31 (setting UNIT_FLAG_AURAS_VISIBLE so the client
|
||||
// still renders them), which makes the range unreliable — and it repurposes the
|
||||
// nibble to carry the true polarity so the information survives the spill.
|
||||
// There, the nibble is authoritative. See `Offsets::UNIT_AURA_FLAG_HARMFUL`.
|
||||
//
|
||||
// Either way `Filter` selects on the aura's REAL polarity, so a spilled debuff
|
||||
// is reported as a debuff. The engine's own UnitBuff/UnitDebuff split purely by
|
||||
// slot range and would call it a buff; that is a 1.12 UI limitation we
|
||||
// deliberately do not mirror, since retail's contract is actual polarity. A
|
||||
// polarity walk visits its home range first and then the other range, so
|
||||
// indices are unchanged wherever no spill has happened — which is everywhere,
|
||||
// on a server that does not spill.
|
||||
//
|
||||
// Callers that take a 1-based Lua index into "buffs" or "debuffs" translate
|
||||
// to the absolute 0..47 slot before calling in.
|
||||
|
||||
namespace Aura::Data {
|
||||
|
||||
// Filter for slot iteration. The engine's `UnitBuff` / `UnitDebuff`
|
||||
// pick one direction; modern `C_UnitAuras.GetAuraDataByIndex` defaults
|
||||
// to helpful when no filter is specified.
|
||||
// Filter for slot iteration: the aura's polarity per its flag nibble (see the
|
||||
// file header). The engine's `UnitBuff` / `UnitDebuff` pick one direction;
|
||||
// modern `C_UnitAuras.GetAuraDataByIndex` defaults to helpful when no filter
|
||||
// is specified.
|
||||
enum class Filter { Helpful, Harmful };
|
||||
|
||||
// How a resolved aura is emitted onto the Lua stack by the single-aura push
|
||||
@@ -38,17 +59,27 @@ enum class Filter { Helpful, Harmful };
|
||||
// pushes the Classic-Era `UnitAura` 15-value tuple (net stack +15) with NO table
|
||||
// allocation, for the `C_UnitAuras.UnitAura/UnitBuff/UnitDebuff` accessors that
|
||||
// avoid the per-call GC churn. Selection + timing resolution are identical for
|
||||
// both — only the terminal leaf differs.
|
||||
enum class Emit { Table, Positional };
|
||||
// both — only the terminal leaf differs. `Fill` writes the `AuraData` fields
|
||||
// into the table already on top of the stack instead of a new one (net stack
|
||||
// 0), for getters that take a caller's table to reuse; fields the aura lacks
|
||||
// are cleared to nil.
|
||||
enum class Emit { Table, Positional, Fill };
|
||||
|
||||
// Caster restriction from the `PLAYER` / `!PLAYER` aura filter tokens.
|
||||
// `Any` = no restriction; `PlayerOnly` = only auras the local player cast
|
||||
// (`PLAYER`); `NotPlayer` = only auras NOT cast by the local player
|
||||
// (`!PLAYER`). Caster attribution comes from the `Aura::Source` cache; a
|
||||
// cache miss counts as "not the player" (so `PlayerOnly` excludes it and
|
||||
// `NotPlayer` includes it — matching `IsPlayerCast`'s miss semantics).
|
||||
// `Any` = no restriction; `PlayerOnly` = only auras the local player cast,
|
||||
// or a unit the player owns cast — pet, guardian, totem, charmed unit
|
||||
// (`PLAYER`); `NotPlayer` = the complement (`!PLAYER`). This is 4.3.4's
|
||||
// `CGSpellBook::IsAuraCaster`. Caster attribution comes from the
|
||||
// `Aura::Source` cache; a cache miss counts as "not the player" (so
|
||||
// `PlayerOnly` excludes it and `NotPlayer` includes it — matching
|
||||
// `IsPlayerCast`'s miss semantics).
|
||||
enum class CasterMode { Any, PlayerOnly, NotPlayer };
|
||||
|
||||
// Restriction from the `CANCELABLE` / `NOT_CANCELABLE` aura filter tokens:
|
||||
// the descriptor nibble's cancelable bit, the test `GetPlayerBuff`'s own
|
||||
// filter makes (and 4.3.4's GetSortedAuraSlot).
|
||||
enum class CancelMode { Any, CancelableOnly, NotCancelable };
|
||||
|
||||
// Restriction from the `DISPELLABLE` / `!DISPELLABLE` aura filter tokens.
|
||||
// `Any` = no restriction; `DispellableOnly` = only auras that can be
|
||||
// dispelled / purged / stolen by SOME mechanism (dispel type Magic, Curse,
|
||||
@@ -70,8 +101,16 @@ struct Match {
|
||||
CasterMode caster = CasterMode::Any;
|
||||
DispelMode dispel = DispelMode::Any;
|
||||
CcMode cc = CcMode::Any;
|
||||
CancelMode cancel = CancelMode::Any;
|
||||
// `RAID`: a helpful aura the player can cast on others, a harmful one the
|
||||
// player can dispel — the engine's raid filter (see `IsRaidAura`).
|
||||
bool raid = false;
|
||||
};
|
||||
|
||||
// The slot's descriptor flag nibble (0 for an empty slot or a null unit).
|
||||
// Bit `UNIT_AURA_FLAG_CANCELABLE` is the cancelable bit on every server.
|
||||
uint8_t SlotNibble(const uint8_t *unit, int slot);
|
||||
|
||||
// Reads the spell ID at the given absolute slot (0..47). Returns 0
|
||||
// if the slot is empty or the unit pointer is null.
|
||||
uint32_t ReadSpellID(const uint8_t *unit, int slot);
|
||||
@@ -83,11 +122,40 @@ uint32_t ReadSpellID(const uint8_t *unit, int slot);
|
||||
// to decide whether to surface an aura through Lua.
|
||||
bool IsSlotPopulated(const uint8_t *unit, int slot);
|
||||
|
||||
// Finds the absolute slot of the `oneBasedIndex`-th populated aura
|
||||
// matching `filter`. Returns -1 if no such aura. Walks
|
||||
// 0..UNIT_AURA_BUFF_COUNT-1 for Helpful, UNIT_AURA_BUFF_COUNT..
|
||||
// UNIT_AURA_TOTAL-1 for Harmful. When `playerOnly` is true, only auras
|
||||
// the local player cast (per `IsPlayerCast`) are counted/returned.
|
||||
// The aura's real polarity, read the way this server encodes it: the slot range
|
||||
// normally, the flag nibble on Turtle (see the file header). False for an empty
|
||||
// slot or a null unit. This is what `Filter` selects on, and the ONE place the
|
||||
// two encodings are chosen between.
|
||||
bool IsSlotHarmful(const uint8_t *unit, int slot);
|
||||
|
||||
// The engine's TOOLTIP visibility gate for a slot: occupied, visible nibble,
|
||||
// and the spell passes `FUN_GAMETOOLTIP_AURA_VISIBLE` — exactly what
|
||||
// `SetUnitBuff` / `SetUnitDebuff` count while turning an index into a slot.
|
||||
// Differs from `IsSlotPopulated` (the UnitBuff gate), so use THIS to compute
|
||||
// the index the engine's tooltip methods expect for a given slot.
|
||||
bool IsSlotTooltipVisible(const uint8_t *unit, int slot);
|
||||
|
||||
// Fills `out` with all UNIT_AURA_TOTAL absolute slots of `unit` in the order a
|
||||
// `filter` walk visits them, the order 4.3.4's sorted aura list gives
|
||||
// (`CGUnit_C::InsertSortedAura`):
|
||||
// 1. auras the player, or a unit the player owns, cast (`IsPlayerCast`);
|
||||
// 2. auras the player could apply (`canApplyAura`);
|
||||
// 3. the rest.
|
||||
// Within each group, the polarity's home range comes first (helpful 0..31,
|
||||
// harmful 32..47), then the other range, where spilled debuffs live. Every
|
||||
// enumeration that hands out indices or slot lists uses this so they agree on
|
||||
// order. The groups are cached per unit and rebuilt when its aura array changes
|
||||
// or a new frame starts, so a by-index loop pays for them once.
|
||||
void WalkOrder(const uint8_t *unit, Filter filter, int *out);
|
||||
|
||||
// `SlotMatches` plus the polarity test: populated, of `filter`'s polarity, and
|
||||
// passing `match`.
|
||||
bool SlotMatchesFilter(const uint8_t *unit, int slot, Filter filter,
|
||||
const Match &match);
|
||||
|
||||
// Finds the absolute slot of the `oneBasedIndex`-th populated aura of
|
||||
// `filter`'s polarity that passes `match`. Returns -1 if no such aura. Walks
|
||||
// all 48 slots in `WalkOrder`.
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
Match match = {});
|
||||
|
||||
@@ -99,30 +167,55 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// Finds the absolute slot of a populated aura whose locale-resolved
|
||||
// spell name exactly matches `spellName`. Case-sensitive. Otherwise
|
||||
// spell name matches `spellName`, ignoring case (Storm's SStrCmpI, as
|
||||
// 4.3.4's GetSortedAuraSlot compares names). Otherwise
|
||||
// same semantics as `FindSlotBySpellID`. Returns -1 if not found
|
||||
// or if `spellName` is null/empty.
|
||||
int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// True iff the aura at `slot` was cast by the local player, per the
|
||||
// `Aura::Source` cache. False on a cache miss (caster unknown) — so a
|
||||
// PLAYER-filtered query excludes auras whose cast we didn't observe.
|
||||
// True iff the aura at `slot` was cast by the local player or by a unit the
|
||||
// player owns (see `CasterMode`), per the `Aura::Source` cache. False on a
|
||||
// cache miss (caster unknown) — so a PLAYER-filtered query excludes auras
|
||||
// whose cast we didn't observe.
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot);
|
||||
|
||||
// Applies a `CasterMode` to a per-aura "was cast by the local player" answer.
|
||||
// `Any` → always true; `PlayerOnly` → the answer; `NotPlayer` → its negation.
|
||||
// The server's own timing for the local player's aura in absolute `slot`:
|
||||
// the engine's expiration (what `GetPlayerBuffTimeLeft` counts down) and the
|
||||
// applied duration from the same SMSG_UPDATE_AURA_DURATION, both exact —
|
||||
// diminishing returns and the caster's modifiers included. False when the
|
||||
// slot has no running timer (empty, until-cancelled, or elapsed).
|
||||
// `outDurationMs` is 0 when no duration packet was seen for the slot.
|
||||
bool PlayerAuraTiming(int slot, uint32_t *outExpirationMs, uint32_t *outDurationMs);
|
||||
|
||||
// Applies a `CasterMode` to a per-aura "was cast by the player or a unit
|
||||
// they own" answer. `Any` → always true; `PlayerOnly` → the answer;
|
||||
// `NotPlayer` → its negation.
|
||||
bool CasterMatches(CasterMode caster, bool isPlayerCast);
|
||||
|
||||
// The engine's raid filter — UnitBuff/UnitDebuff's third argument, and
|
||||
// 4.3.4's `RAID` token: a helpful aura passes when the player can cast its
|
||||
// spell on others (`FUN_SPELL_IS_CASTABLE_AURA`), a harmful one when its
|
||||
// dispel type is in the player's dispel list (`FUN_DISPEL_TYPE_KNOWN`).
|
||||
bool IsRaidAura(uint32_t spellID, bool harmful);
|
||||
|
||||
// True iff the spell's dispel type is one a dispel/purge/steal can remove —
|
||||
// Magic, Curse, Disease, or Poison (`Spell.dbc` Dispel ∈ 1..4). None,
|
||||
// Stealth, Invisibility, Enrage, etc. are not dispellable. Verified against
|
||||
// the server's DISPEL_ALL_MASK.
|
||||
bool IsDispellable(uint32_t spellID);
|
||||
|
||||
// Applies a full `Match` (caster + dispel predicates) to one aura, given the
|
||||
// per-aura "was cast by the local player" answer and its spell ID.
|
||||
bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID);
|
||||
// Applies a full `Match` to one aura, given the per-aura "was cast by the
|
||||
// local player" answer, its spell ID, its polarity, and whether it is
|
||||
// cancelable.
|
||||
bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID,
|
||||
bool harmful, bool cancelable);
|
||||
|
||||
// True iff descriptor `slot` on `unit` is populated (per `IsSlotPopulated`) AND
|
||||
// passes `match`. The caster answer (`IsPlayerCast` — a scan of the
|
||||
// `Aura::Source` cache, ~1000 entries in a raid) is computed ONLY when `match`
|
||||
// restricts on caster, so the common filter-less walk never pays for it.
|
||||
bool SlotMatches(const uint8_t *unit, int slot, const Match &match);
|
||||
|
||||
// Display stack count for the given slot (engine stores stacks-1,
|
||||
// we add 1). Returns 0 if the unit is null.
|
||||
@@ -154,16 +247,24 @@ const char *DispelName(uint32_t dispelTypeID);
|
||||
// sourceGUID (caster "0x..." GUID string; nil on miss — a ClassicAPI
|
||||
// extension, not in retail AuraData)
|
||||
//
|
||||
// And these from the spell and the player's spells:
|
||||
// isFromPlayerOrPlayerPet (caster GUID is a player or a pet),
|
||||
// canApplyAura (the player or pet knows the spell and it applies an aura),
|
||||
// isRaid (the aura passes the RAID filter, `IsRaidAura`),
|
||||
// isStealable (4.3.4's CanStealAura: a buff on an attackable unit whose
|
||||
// dispel type a spell the player or pet knows removes from
|
||||
// an enemy)
|
||||
//
|
||||
// And `auraInstanceID` from `Aura::Instance` (the table forms only).
|
||||
//
|
||||
// And these with vanilla-truthful defaults (matches modern's
|
||||
// "field present but inapplicable" semantics):
|
||||
// charges=0, maxCharges=0, auraInstanceID=nil, points=nil,
|
||||
// isStealable=false, isBossAura=false, isFromPlayerOrPlayerPet=false,
|
||||
// isNameplateOnly=false, nameplateShowAll=false,
|
||||
// nameplateShowPersonal=false, canApplyAura=false,
|
||||
// shouldConsolidate=false, isRaid=false
|
||||
// charges=0, maxCharges=0, points=nil,
|
||||
// isBossAura=false, isNameplateOnly=false, nameplateShowAll=false,
|
||||
// nameplateShowPersonal=false, shouldConsolidate=false
|
||||
//
|
||||
// Net stack effect: +1 for `Emit::Table` (the table), +15 for `Emit::Positional`
|
||||
// (the Classic-Era `UnitAura` tuple — see `Emit`).
|
||||
// (the Classic-Era `UnitAura` tuple), 0 for `Emit::Fill` — see `Emit`.
|
||||
void Push(void *L, const uint8_t *unit, int slot, Emit emit = Emit::Table);
|
||||
|
||||
// Fallback for the index path when the descriptor has dropped an aura's slot
|
||||
@@ -220,7 +321,7 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// As `PushGroupAuraBySpellID` but matched by locale-resolved spell name
|
||||
// (case-sensitive exact match), mirroring `FindSlotBySpellName`.
|
||||
// (ignoring case), mirroring `FindSlotBySpellName`.
|
||||
bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
@@ -230,4 +331,46 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
int outerIdx, int &nextKey);
|
||||
|
||||
// ── Opaque aura slots (GetAuraSlots / GetAuraDataBySlot) ────────────────────
|
||||
//
|
||||
// The by-index getters re-walk the descriptor from slot 0 on every call, so a
|
||||
// full scan through them is quadratic in the aura count. `CollectSlots`
|
||||
// enumerates once into opaque slot ids and `PushBySlot` fetches one aura by id
|
||||
// with no walk — the modern `C_UnitAuras.GetAuraSlots` / `GetAuraDataBySlot`
|
||||
// batching contract. An id is valid for the frame it was enumerated in.
|
||||
//
|
||||
// Encoding (kind = id / OPAQUE_STRIDE, k = id % OPAQUE_STRIDE):
|
||||
// kind 0 live descriptor slot k (0..UNIT_AURA_TOTAL-1)
|
||||
// kind 1 k-th eligible helpful cache-fallback entry (OPAQUE_CACHE_HELPFUL)
|
||||
// kind 2 k-th eligible harmful cache-fallback entry (OPAQUE_CACHE_HARMFUL)
|
||||
// kind 3 out-of-range group-array slot k (0..47) (OPAQUE_GROUP)
|
||||
// kind 4 k-th helpful group cache-fallback entry (OPAQUE_GROUP_CACHE_HELPFUL)
|
||||
// kind 5 k-th harmful group cache-fallback entry (OPAQUE_GROUP_CACHE_HARMFUL)
|
||||
// Fallback ordinals count the UNFILTERED eligible sequence (visible, not
|
||||
// already in the live array), so `PushBySlot` needs no filter; `CollectSlots`
|
||||
// applies the caller's `match` only when choosing which ids to emit.
|
||||
constexpr int OPAQUE_STRIDE = 0x100;
|
||||
constexpr int OPAQUE_CACHE_HELPFUL = 1 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_CACHE_HARMFUL = 2 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_GROUP = 3 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_GROUP_CACHE_HELPFUL = 4 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_GROUP_CACHE_HARMFUL = 5 * OPAQUE_STRIDE;
|
||||
|
||||
// Upper bound on ids one `CollectSlots` call can yield: the live array (or the
|
||||
// group array) plus its cache fallback, each at most UNIT_AURA_TOTAL entries.
|
||||
constexpr int OPAQUE_SLOTS_MAX = 2 * Offsets::UNIT_AURA_TOTAL;
|
||||
|
||||
// Writes the opaque ids of every aura on the unit matching `filter` + `match`
|
||||
// into `out` (at most `cap`), in the order the by-index getters visit them:
|
||||
// live slots (or, with no live CGUnit, the group array) first, then the cache
|
||||
// fallback. `unit` null selects the out-of-range group path for `guid`. Returns
|
||||
// the count written.
|
||||
int CollectSlots(const uint8_t *unit, uint64_t guid, Filter filter, Match match,
|
||||
int *out, int cap);
|
||||
|
||||
// Pushes the aura an opaque id names — table or positional per `emit` — and
|
||||
// returns true; pushes nothing and returns false for an id that names nothing
|
||||
// (stale, malformed, or the wrong path for the unit's current state).
|
||||
bool PushBySlot(void *L, const uint8_t *unit, uint64_t guid, int slot, Emit emit);
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
// UNIT_AURA_DELTA(unitTarget, updateInfo) — retail's UNIT_AURA payload under
|
||||
// its own name, so the engine's UNIT_AURA stays exactly as it is.
|
||||
//
|
||||
// Once per frame, from the world tick, each unit whose auras changed fires
|
||||
// once per unit token it has (`Unit::Identity::TokensForGUID`), the way the
|
||||
// engine fires UNIT_AURA. The change set is `Aura::Instance`'s.
|
||||
//
|
||||
// The payload is built to cost no garbage. It carries instance IDs only —
|
||||
// never AuraData; a handler reads the auras it cares about with
|
||||
// `C_UnitAuras.GetAuraDataByAuraInstanceID(unit, id, buf)` — and the
|
||||
// `updateInfo` table and its three ID lists are the same four tables on every
|
||||
// fire, refilled each time. So they are valid only during the handler; a
|
||||
// handler that keeps one must copy it.
|
||||
//
|
||||
// With nothing registered for the event, the tick reads one pointer and stops:
|
||||
// `Aura::Instance` records no change set, and nothing here runs.
|
||||
|
||||
#include "Game.h"
|
||||
#include "aura/Instance.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Aura::Delta {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Doc::Field kInfoFields[] = {
|
||||
Game::Doc::Req("isFullUpdate", "bool",
|
||||
"True when the unit's auras must be read again in full; the lists are then nil."),
|
||||
Game::Doc::Opt("addedAuraInstanceIDs", "table", nullptr,
|
||||
"Instance IDs of the auras added to the unit."),
|
||||
Game::Doc::Opt("updatedAuraInstanceIDs", "table", nullptr,
|
||||
"Instance IDs of the unit's auras that changed in place."),
|
||||
Game::Doc::Opt("removedAuraInstanceIDs", "table", nullptr,
|
||||
"Instance IDs of the auras removed from the unit."),
|
||||
};
|
||||
const Game::Doc::Structure kInfo{
|
||||
"UnitAuraDeltaInfo", "UnitAuras", kInfoFields,
|
||||
"What changed in a unit's auras. Valid only while the event handler runs."};
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("unitTarget", "UnitToken"),
|
||||
Game::Doc::Req("updateInfo", "UnitAuraDeltaInfo"),
|
||||
};
|
||||
const Game::Doc::Event kDoc{
|
||||
"UnitAuras",
|
||||
"Fires when a unit's auras change, with the instance IDs of the auras that "
|
||||
"were added, changed, or removed.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve _evt{"UNIT_AURA_DELTA", &kDoc};
|
||||
|
||||
// The reused payload tables, anchored in the registry of the Lua state that
|
||||
// made them. LUA_NOREF until first use in each state.
|
||||
constexpr int kNoRef = -2;
|
||||
int g_infoRef = kNoRef;
|
||||
int g_addedRef = kNoRef;
|
||||
int g_updatedRef = kNoRef;
|
||||
int g_removedRef = kNoRef;
|
||||
|
||||
// Pushes the table anchored at `ref`, making it on first use.
|
||||
void PushTable(void *L, int &ref) {
|
||||
if (ref == kNoRef) {
|
||||
Game::Lua::NewTable(L);
|
||||
ref = Game::Lua::Ref(L, Game::Lua::REGISTRY_INDEX);
|
||||
}
|
||||
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, ref);
|
||||
}
|
||||
|
||||
// Sets `key` on the table at the top to the list at `ref` refilled with `ids`,
|
||||
// or to nil when there are none.
|
||||
void SetList(void *L, const char *key, int &ref, const uint32_t *ids, int count) {
|
||||
Game::Lua::PushString(L, key);
|
||||
if (count == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
return;
|
||||
}
|
||||
PushTable(L, ref);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(ids[i]));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
// Clear what lies past the new end: a longer earlier list, or anything a
|
||||
// handler appended.
|
||||
for (int i = count + 1;; ++i) {
|
||||
Game::Lua::RawGetI(L, -1, i);
|
||||
const bool end = Game::Lua::Type(L, -1) == Game::Lua::TYPE_NIL;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (end)
|
||||
break;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i));
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
Game::Lua::SetN(L, -1, count);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
void Fire(const Aura::Instance::Delta &d) {
|
||||
char tokens[16][32];
|
||||
const int n = Unit::Identity::TokensForGUID(d.guid, tokens, 16);
|
||||
void *L = Game::Lua::State();
|
||||
if (n == 0 || L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
// Refilled for each token: a handler may have changed the tables.
|
||||
Game::Lua::PushString(L, tokens[i]);
|
||||
PushTable(L, g_infoRef);
|
||||
Game::Lua::SetFieldBool(L, "isFullUpdate", d.fullUpdate);
|
||||
SetList(L, "addedAuraInstanceIDs", g_addedRef, d.added, d.addedCount);
|
||||
SetList(L, "updatedAuraInstanceIDs", g_updatedRef, d.updated, d.updatedCount);
|
||||
SetList(L, "removedAuraInstanceIDs", g_removedRef, d.removed, d.removedCount);
|
||||
Event::Custom::FireValues(_evt.Slot(), L, top + 1, 2);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
const bool listening = Event::Custom::HasListeners(_evt.Slot());
|
||||
Aura::Instance::SetRecording(listening);
|
||||
if (listening)
|
||||
Aura::Instance::Drain(&Fire);
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
|
||||
|
||||
// Each registration pass is a new Lua state; the old state's refs are gone.
|
||||
void ResetRefs() {
|
||||
g_infoRef = g_addedRef = g_updatedRef = g_removedRef = kNoRef;
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&ResetRefs};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Aura::Delta
|
||||
@@ -0,0 +1,317 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
// Aura instance IDs. See `Instance.h` for the contract.
|
||||
|
||||
#include "Instance.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Data.h"
|
||||
#include "aura/Source.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Aura::Instance {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kSlots = Offsets::UNIT_AURA_TOTAL;
|
||||
constexpr int kFlagBytes = kSlots / 2; // UNIT_FIELD_AURAFLAGS: a nibble per slot
|
||||
|
||||
// What was last seen of one unit's aura fields, the IDs given to its visible
|
||||
// auras, and (while recording) its change set since the last Drain.
|
||||
struct Tracked {
|
||||
const uint8_t *object = nullptr;
|
||||
uint32_t spell[kSlots] = {};
|
||||
uint8_t flags[kFlagBytes] = {};
|
||||
uint8_t stacks[kSlots] = {};
|
||||
uint32_t id[kSlots] = {}; // 0 = no visible aura in the slot
|
||||
bool full = false;
|
||||
std::vector<uint32_t> added, updated, removed;
|
||||
};
|
||||
|
||||
std::unordered_map<uint64_t, Tracked> g_units;
|
||||
uint32_t g_nextID = 1;
|
||||
bool g_recording = false;
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
return Game::Read<const uint8_t *>(unit, Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
}
|
||||
|
||||
uint8_t Nibble(const uint8_t *flags, int slot) {
|
||||
return (flags[slot / 2] >> ((slot & 1) * 4)) & 0xF;
|
||||
}
|
||||
|
||||
bool Contains(const std::vector<uint32_t> &v, uint32_t id) {
|
||||
return std::find(v.begin(), v.end(), id) != v.end();
|
||||
}
|
||||
|
||||
bool Erase(std::vector<uint32_t> &v, uint32_t id) {
|
||||
const auto it = std::find(v.begin(), v.end(), id);
|
||||
if (it == v.end())
|
||||
return false;
|
||||
v.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearPending(Tracked &t) {
|
||||
t.full = false;
|
||||
t.added.clear();
|
||||
t.updated.clear();
|
||||
t.removed.clear();
|
||||
}
|
||||
|
||||
// The change-set writes, coalesced within one Drain interval: an aura added and
|
||||
// removed before the consumer heard of it is never reported, and an added or
|
||||
// removed aura is not also reported as updated. A pending full update already
|
||||
// covers everything.
|
||||
void NoteAdded(Tracked &t, uint32_t id) {
|
||||
if (g_recording && !t.full)
|
||||
t.added.push_back(id);
|
||||
}
|
||||
|
||||
void NoteChanged(Tracked &t, uint32_t id) {
|
||||
if (g_recording && !t.full && !Contains(t.added, id) && !Contains(t.updated, id))
|
||||
t.updated.push_back(id);
|
||||
}
|
||||
|
||||
void NoteRemoved(Tracked &t, uint32_t id) {
|
||||
if (!g_recording || t.full || Erase(t.added, id))
|
||||
return;
|
||||
Erase(t.updated, id);
|
||||
t.removed.push_back(id);
|
||||
}
|
||||
|
||||
// Records `unit`'s aura fields as they are now, with a fresh ID for every
|
||||
// visible aura. `full` marks the change set as a full update: the consumer has
|
||||
// never been told about these IDs.
|
||||
void Seed(Tracked &t, const uint8_t *unit, bool full) {
|
||||
const uint8_t *desc = Descriptor(unit);
|
||||
t.object = unit;
|
||||
ClearPending(t);
|
||||
t.full = full && g_recording;
|
||||
if (desc == nullptr) {
|
||||
std::memset(t.spell, 0, sizeof t.spell);
|
||||
std::memset(t.flags, 0, sizeof t.flags);
|
||||
std::memset(t.stacks, 0, sizeof t.stacks);
|
||||
std::memset(t.id, 0, sizeof t.id);
|
||||
return;
|
||||
}
|
||||
std::memcpy(t.spell, desc + Offsets::OFF_UNIT_FIELD_AURA, sizeof t.spell);
|
||||
std::memcpy(t.flags, desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS, sizeof t.flags);
|
||||
std::memcpy(t.stacks, desc + Offsets::OFF_UNIT_FIELD_AURAAPPLICATIONS, sizeof t.stacks);
|
||||
for (int slot = 0; slot < kSlots; ++slot)
|
||||
t.id[slot] = Data::IsSlotPopulated(unit, slot) ? g_nextID++ : 0;
|
||||
}
|
||||
|
||||
// Brings one slot's record in line with the live descriptor. A new spell, or a
|
||||
// visibility change, ends the old instance and starts a new one; the same
|
||||
// visible aura with new flags or stacks is the same instance, updated.
|
||||
void SyncSlot(Tracked &t, const uint8_t *unit, const uint8_t *desc, int slot) {
|
||||
const uint32_t spell = Game::Read<uint32_t>(desc, Offsets::OFF_UNIT_FIELD_AURA + slot * 4);
|
||||
const uint8_t *flags = desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS;
|
||||
const uint8_t nibble = Nibble(flags, slot);
|
||||
const uint8_t stacks = desc[Offsets::OFF_UNIT_FIELD_AURAAPPLICATIONS + slot];
|
||||
if (spell == t.spell[slot] && nibble == Nibble(t.flags, slot)) {
|
||||
if (stacks != t.stacks[slot]) {
|
||||
t.stacks[slot] = stacks;
|
||||
if (t.id[slot] != 0)
|
||||
NoteChanged(t, t.id[slot]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const bool visible = Data::IsSlotPopulated(unit, slot);
|
||||
if (visible && spell == t.spell[slot] && t.id[slot] != 0) {
|
||||
NoteChanged(t, t.id[slot]);
|
||||
} else {
|
||||
if (t.id[slot] != 0)
|
||||
NoteRemoved(t, t.id[slot]);
|
||||
t.id[slot] = visible ? g_nextID++ : 0;
|
||||
if (t.id[slot] != 0)
|
||||
NoteAdded(t, t.id[slot]);
|
||||
}
|
||||
t.spell[slot] = spell;
|
||||
// Only this slot's nibble: the byte's other half is the neighbour's, which
|
||||
// its own sync records.
|
||||
const uint8_t mask = static_cast<uint8_t>(0xF << ((slot & 1) * 4));
|
||||
t.flags[slot / 2] = static_cast<uint8_t>((t.flags[slot / 2] & ~mask) | (flags[slot / 2] & mask));
|
||||
t.stacks[slot] = stacks;
|
||||
}
|
||||
|
||||
void SyncAll(Tracked &t, const uint8_t *unit) {
|
||||
const uint8_t *desc = Descriptor(unit);
|
||||
if (desc == nullptr)
|
||||
return;
|
||||
// The common case is no change at all: three compares, no per-slot work.
|
||||
if (std::memcmp(t.spell, desc + Offsets::OFF_UNIT_FIELD_AURA, sizeof t.spell) == 0 &&
|
||||
std::memcmp(t.flags, desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS, sizeof t.flags) == 0 &&
|
||||
std::memcmp(t.stacks, desc + Offsets::OFF_UNIT_FIELD_AURAAPPLICATIONS,
|
||||
sizeof t.stacks) == 0)
|
||||
return;
|
||||
for (int slot = 0; slot < kSlots; ++slot)
|
||||
SyncSlot(t, unit, desc, slot);
|
||||
}
|
||||
|
||||
// The record for `unit`, created (seeded from the live fields) on first sight
|
||||
// and re-seeded when its GUID now names a different object — the old one left
|
||||
// view and a new one was built, so nothing recorded of it still holds. A
|
||||
// rebuilt unit is always a full update (the consumer holds its old IDs). For a
|
||||
// first sight, `full` says whether it is news to the consumer: true from the
|
||||
// change handlers (an aura changed on a unit the consumer has no IDs for),
|
||||
// false from the getters (the caller is reading the auras right now).
|
||||
Tracked *Track(const uint8_t *unit, bool full, bool *seeded) {
|
||||
*seeded = false;
|
||||
const uint64_t guid = Unit::Identity::GuidForObject(unit);
|
||||
if (guid == 0 || Descriptor(unit) == nullptr)
|
||||
return nullptr;
|
||||
auto [it, inserted] = g_units.try_emplace(guid);
|
||||
Tracked &t = it->second;
|
||||
if (inserted || t.object != unit) {
|
||||
Seed(t, unit, inserted ? full : true);
|
||||
*seeded = true;
|
||||
}
|
||||
return &t;
|
||||
}
|
||||
|
||||
// The engine's aura handlers name the slot that changed; syncing that slot is
|
||||
// what keeps a spell removed and applied again in one slot two instances (the
|
||||
// two arrive in separate update blocks, so each reaches its own sync).
|
||||
void OnAuraChange(const Aura::Source::Change &c) {
|
||||
if (c.unit == nullptr || c.slot < 0 || c.slot >= kSlots)
|
||||
return;
|
||||
const auto *unit = static_cast<const uint8_t *>(c.unit);
|
||||
bool seeded;
|
||||
Tracked *t = Track(unit, /*full*/ true, &seeded);
|
||||
if (t != nullptr && !seeded)
|
||||
SyncSlot(*t, unit, Descriptor(unit), c.slot);
|
||||
}
|
||||
|
||||
const Aura::Source::AutoObserve _obs{&OnAuraChange};
|
||||
|
||||
// Units that left view are forgotten; one coming back is seeded afresh with new
|
||||
// IDs. While recording, `Drain` does this every frame; otherwise this sweep
|
||||
// keeps the table to the units in view.
|
||||
constexpr uint32_t kSweepIntervalMs = 1000;
|
||||
uint32_t g_lastSweepMs = 0;
|
||||
|
||||
void OnWorldTick() {
|
||||
const uint32_t now = Time::Clock::NowMs();
|
||||
if (Time::Clock::Elapsed(g_lastSweepMs, now) < kSweepIntervalMs)
|
||||
return;
|
||||
g_lastSweepMs = now;
|
||||
for (auto it = g_units.begin(); it != g_units.end();) {
|
||||
if (Object::ByGuid(Offsets::TYPEMASK_UNIT, it->first) == nullptr)
|
||||
it = g_units.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
|
||||
|
||||
// A delta copied out of its record, so `Drain`'s callback can run Lua that
|
||||
// grows `g_units` without invalidating it.
|
||||
struct OwnedDelta {
|
||||
uint64_t guid;
|
||||
bool full;
|
||||
std::vector<uint32_t> added, updated, removed;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t IDForSlot(const uint8_t *unit, int slot) {
|
||||
if (unit == nullptr || slot < 0 || slot >= kSlots)
|
||||
return 0;
|
||||
bool seeded;
|
||||
Tracked *t = Track(unit, /*full*/ false, &seeded);
|
||||
if (t == nullptr)
|
||||
return 0;
|
||||
if (!seeded)
|
||||
SyncSlot(*t, unit, Descriptor(unit), slot);
|
||||
return t->id[slot];
|
||||
}
|
||||
|
||||
int SlotForID(const uint8_t *unit, uint32_t id) {
|
||||
if (unit == nullptr || id == 0)
|
||||
return -1;
|
||||
bool seeded;
|
||||
Tracked *t = Track(unit, /*full*/ false, &seeded);
|
||||
if (t == nullptr || seeded) // a fresh seed hands out new IDs only
|
||||
return -1;
|
||||
for (int slot = 0; slot < kSlots; ++slot) {
|
||||
if (t->id[slot] != id)
|
||||
continue;
|
||||
SyncSlot(*t, unit, Descriptor(unit), slot);
|
||||
return t->id[slot] == id ? slot : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void NoteUpdated(uint64_t guid, int slot) {
|
||||
if (slot < 0 || slot >= kSlots)
|
||||
return;
|
||||
const auto it = g_units.find(guid);
|
||||
if (it != g_units.end() && it->second.id[slot] != 0)
|
||||
NoteChanged(it->second, it->second.id[slot]);
|
||||
}
|
||||
|
||||
void SetRecording(bool on) {
|
||||
if (on == g_recording)
|
||||
return;
|
||||
g_recording = on;
|
||||
if (!on)
|
||||
for (auto &[guid, t] : g_units)
|
||||
ClearPending(t);
|
||||
}
|
||||
|
||||
void Drain(void (*fn)(const Delta &delta)) {
|
||||
std::vector<OwnedDelta> out;
|
||||
for (auto it = g_units.begin(); it != g_units.end();) {
|
||||
Tracked &t = it->second;
|
||||
const auto *unit = static_cast<const uint8_t *>(
|
||||
Object::ByGuid(Offsets::TYPEMASK_UNIT, it->first));
|
||||
if (unit == nullptr) {
|
||||
it = g_units.erase(it);
|
||||
continue;
|
||||
}
|
||||
if (unit != t.object)
|
||||
Seed(t, unit, /*full*/ true);
|
||||
else
|
||||
SyncAll(t, unit);
|
||||
if (t.full || !t.added.empty() || !t.updated.empty() || !t.removed.empty()) {
|
||||
out.push_back({it->first, t.full, std::move(t.added), std::move(t.updated),
|
||||
std::move(t.removed)});
|
||||
ClearPending(t);
|
||||
}
|
||||
++it;
|
||||
}
|
||||
for (const OwnedDelta &d : out) {
|
||||
const Delta view{d.guid,
|
||||
d.full,
|
||||
d.added.data(),
|
||||
static_cast<int>(d.added.size()),
|
||||
d.updated.data(),
|
||||
static_cast<int>(d.updated.size()),
|
||||
d.removed.data(),
|
||||
static_cast<int>(d.removed.size())};
|
||||
fn(view);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Aura::Instance
|
||||
@@ -0,0 +1,75 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Aura instance IDs — retail's `auraInstanceID`. 1.12 has no aura identity:
|
||||
// a unit's descriptor holds 48 spell IDs and nothing that tells one application
|
||||
// of a spell from the next. This module gives each aura a number when it lands
|
||||
// in a descriptor slot and keeps it until the aura leaves, so the same aura
|
||||
// reads the same ID from every getter, and an aura removed and applied again
|
||||
// gets a new one. IDs are never reused.
|
||||
//
|
||||
// Only the visible auras of a unit in view get an ID: the slots
|
||||
// `Aura::Data::IsSlotPopulated` accepts (the UnitBuff gate). An aura only the
|
||||
// `Aura::Source` cache or the out-of-range group array knows has none.
|
||||
//
|
||||
// Every change goes through one step: compare what was recorded for a slot
|
||||
// with the live descriptor. The engine's aura handlers (`Aura::Source`'s
|
||||
// Added / Removed / Stacks changes) run it for the slot they name, which is
|
||||
// what tells a removal and re-application of one spell apart; the getters run
|
||||
// it before answering; and `Drain` runs it over every tracked unit, which
|
||||
// catches what no handler reports (a bulk clear, an object rebuilt).
|
||||
//
|
||||
// While recording is on, each unit also collects the change set since the last
|
||||
// `Drain` — what `UNIT_AURA_DELTA` reports.
|
||||
|
||||
namespace Aura::Instance {
|
||||
|
||||
// The instance ID of the aura in absolute descriptor `slot` on `unit`, or 0
|
||||
// when the slot holds no visible aura.
|
||||
uint32_t IDForSlot(const uint8_t *unit, int slot);
|
||||
|
||||
// The absolute descriptor slot holding instance `id` on `unit`, or -1 when no
|
||||
// aura on the unit has that ID.
|
||||
int SlotForID(const uint8_t *unit, uint32_t id);
|
||||
|
||||
// Records the aura in `slot` on `guid` as updated in place — a timing change
|
||||
// with no descriptor change behind it (a refresh, a duration packet). No-op
|
||||
// for a slot with no ID or a unit nobody has asked about.
|
||||
void NoteUpdated(uint64_t guid, int slot);
|
||||
|
||||
// One unit's changes since the last `Drain`. With `fullUpdate` set the lists
|
||||
// are empty and the consumer re-reads every aura (the unit was first seen, or
|
||||
// its object was rebuilt).
|
||||
struct Delta {
|
||||
uint64_t guid;
|
||||
bool fullUpdate;
|
||||
const uint32_t *added;
|
||||
int addedCount;
|
||||
const uint32_t *updated;
|
||||
int updatedCount;
|
||||
const uint32_t *removed;
|
||||
int removedCount;
|
||||
};
|
||||
|
||||
// Turns change collection on or off. Off, no change set is kept, and turning
|
||||
// it off discards what was collected. IDs are assigned either way.
|
||||
void SetRecording(bool on);
|
||||
|
||||
// Reconciles every tracked unit with its descriptor, then hands each unit with
|
||||
// changes to `fn` and clears them. The deltas are copied out first, so `fn`
|
||||
// may run Lua that calls the getters; changes made meanwhile wait for the next
|
||||
// `Drain`.
|
||||
void Drain(void (*fn)(const Delta &delta));
|
||||
|
||||
} // namespace Aura::Instance
|
||||
+479
-78
@@ -18,16 +18,19 @@
|
||||
#include "ComboDuration.h"
|
||||
#include "Data.h"
|
||||
#include "Game.h"
|
||||
#include "Instance.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "player/StatSignal.h"
|
||||
#include "spell/CastEvents.h"
|
||||
#include "spell/Known.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "totem/Tracker.h"
|
||||
#include "unit/TokenEvents.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -60,11 +63,11 @@ using Time::Clock::NowMs;
|
||||
// Server-authoritative duration (ms). When the local player is the caster
|
||||
// we let the engine fold in the player's duration modifiers (skipMod = 0);
|
||||
// for any other caster we take the unmodified base (skipMod = 1) since we
|
||||
// only have the local player's mod tables. `unit = 0` selects the player
|
||||
// mod context per FUN_GET_SPELL_DURATION's signature.
|
||||
// only have the local player's mod tables. `pet = 0` selects the player
|
||||
// as the caster per FUN_GET_SPELL_DURATION's signature.
|
||||
uint32_t SpellDurationMs(const uint8_t *spellRecord, bool casterIsPlayer) {
|
||||
using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord,
|
||||
int unit, char skipMod);
|
||||
int pet, char skipMod);
|
||||
const int ms = reinterpret_cast<GetDuration_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GET_SPELL_DURATION))(
|
||||
spellRecord, 0, casterIsPlayer ? 0 : 1);
|
||||
@@ -324,7 +327,7 @@ Entry *Register(Entry *e) {
|
||||
Entry *Claim(uint32_t now) {
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used ||
|
||||
(e.expirationMs != 0 && now >= e.expirationMs &&
|
||||
(e.expirationMs != 0 && Time::Clock::Reached(now, e.expirationMs) &&
|
||||
!DescriptorListsAura(e.targetGuid, e.spellId)))
|
||||
return Register(&e);
|
||||
}
|
||||
@@ -359,6 +362,17 @@ Entry *Claim(uint32_t now) {
|
||||
// Defined in the duration-modifier section below; used by StoreFromCast's
|
||||
// refresh path too.
|
||||
void SignalAuraChanged(uint64_t guid);
|
||||
void SignalEntryChanged(const Entry &e);
|
||||
void SignalEntryRefreshed(const Entry &e);
|
||||
// Defined with the aura handlers below; used by the refresh flush.
|
||||
Change MakeChange(ChangeType type, void *unit, int slot, uint32_t spellId);
|
||||
void NotifyObservers(const Change &change);
|
||||
|
||||
// Defined with the duration-modifier machinery below. A trigger can CREATE the
|
||||
// aura it then edits, in which case the edit is armed at trigger time and
|
||||
// applied here, when the aura it was waiting for actually lands. See PendingMod.
|
||||
struct Entry;
|
||||
void ConsumePendingMods(Entry &e, uint32_t now);
|
||||
|
||||
// The SpellGo hook: authoritative caster + caster-modified (talented) timing.
|
||||
// Identity is `(target, spell, caster)`, so a second caster of the same spell
|
||||
@@ -387,6 +401,7 @@ void StoreFromCast(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
e = Claim(now);
|
||||
*e = {targetGuid, casterGuid, spellId, expirationMs, durationMs,
|
||||
now, SLOT_UNBOUND, KIND_UNKNOWN, true};
|
||||
ConsumePendingMods(*e, now);
|
||||
return;
|
||||
}
|
||||
e->stampMs = now; // refresh EvictAbsent grace on any touch
|
||||
@@ -396,9 +411,11 @@ void StoreFromCast(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
// An in-place recast refresh changes no descriptor field, so the engine
|
||||
// fires no UNIT_AURA — event-driven consumers (aura-bar addons counting
|
||||
// down a once-read duration) would never re-read the new timing. Signal it
|
||||
// ourselves (deferred to the world tick, coalesced). A NEW application
|
||||
// doesn't need this: its descriptor write fires the engine's own event.
|
||||
SignalAuraChanged(targetGuid);
|
||||
// ourselves (deferred to the world tick, coalesced), and report the
|
||||
// refresh. A NEW application doesn't need this: its descriptor write
|
||||
// fires the engine's own event.
|
||||
SignalEntryRefreshed(*e);
|
||||
ConsumePendingMods(*e, now);
|
||||
}
|
||||
|
||||
// The OnAuraAdded / OnAuraStacksChanged application hooks: a descriptor slot,
|
||||
@@ -429,6 +446,7 @@ void StoreFromApplication(uint64_t targetGuid, uint32_t spellId,
|
||||
e = Claim(now);
|
||||
*e = {targetGuid, casterGuid, spellId, expirationMs, durationMs, now,
|
||||
static_cast<int16_t>(slot), kind, true};
|
||||
ConsumePendingMods(*e, now);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -440,6 +458,11 @@ void StoreFromApplication(uint64_t targetGuid, uint32_t spellId,
|
||||
// back to unknown.
|
||||
if (kind != KIND_UNKNOWN)
|
||||
e->kind = kind;
|
||||
// An armed server edit applies whoever owns the timing — the server made it
|
||||
// AFTER the cast this entry came from, so it is the newer truth. It also
|
||||
// adopts the entry, which makes the ownership guard below keep the edited
|
||||
// value instead of the application's base-duration guess.
|
||||
ConsumePendingMods(*e, now);
|
||||
if (e->casterGuid != 0)
|
||||
return; // SpellGo owns this entry; keep its caster + talented timing
|
||||
if (casterGuid != 0)
|
||||
@@ -523,8 +546,10 @@ void Evict(uint64_t targetGuid, uint32_t spellId, int slot) {
|
||||
// Molten Blast -> Flame Shock: refresh (RefreshHolder → reset to max)
|
||||
// A trigger is matched either by exact spellID (from the server's script
|
||||
// binding, stable across ranks) or by SpellFamilyName + school; the affected
|
||||
// aura by SpellFamilyName + a family-flag overlap (+ optional icon) — rank-proof,
|
||||
// exactly how the server's scripts find it. Conflagrate's *full*-consume path
|
||||
// aura by SpellFamilyName plus a family-flag overlap and/or a SpellIconID —
|
||||
// rank-proof, exactly how the server's scripts find it, and the icon half is
|
||||
// what reaches a custom spell with no family flags at all (see
|
||||
// AffectedMatchesRaw). Conflagrate's *full*-consume path
|
||||
// removes Immolate, which clears
|
||||
// the descriptor slot → OnAuraRemoved already handles it; the reduce rule
|
||||
// covers the keep-ticking case. Probabilistic refreshes (Carnage's roll) are
|
||||
@@ -549,9 +574,23 @@ int g_modCount = 0;
|
||||
|
||||
// The affected-aura selector alone, shared with the Lua-facing by-family
|
||||
// refresh so the two cannot disagree about what they match.
|
||||
//
|
||||
// SpellFamilyName always has to match. Past that a rule discriminates by a
|
||||
// SpellFamilyFlags overlap, by SpellIconID, or by both; a zero mask or a zero
|
||||
// icon drops that half of the test. The icon-only form is what names an aura
|
||||
// whose SpellFamilyFlags are ZERO — every one of Turtle's custom spells is
|
||||
// (308 in the hunter family alone), so the mask cannot select them while the
|
||||
// icon still can, and an icon is shared by a spell's whole rank ladder, which
|
||||
// keeps it rank-proof exactly like a family flag. Registrars reject the
|
||||
// degenerate mask-0 + icon-0 rule, which would match a whole class family.
|
||||
bool AffectedMatchesRaw(const uint8_t *rec, uint32_t family, uint64_t mask,
|
||||
uint32_t icon) {
|
||||
if (!Spell::Lookup::IsFitToFamily(rec, family, mask))
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_FAMILY_NAME) != family)
|
||||
return false;
|
||||
if (mask != 0 && !Spell::Lookup::IsFitToFamily(rec, family, mask))
|
||||
return false;
|
||||
if (icon != 0 &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
@@ -607,33 +646,90 @@ void SignalAuraChanged(uint64_t guid) {
|
||||
g_pendingSignals[g_pendingSignalCount++] = guid;
|
||||
}
|
||||
|
||||
// An entry's timing changed in place: the unit's UNIT_AURA re-broadcast, and
|
||||
// the instance in the entry's slot reported as updated to UNIT_AURA_DELTA.
|
||||
void SignalEntryChanged(const Entry &e) {
|
||||
Aura::Instance::NoteUpdated(e.targetGuid, e.slot);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
}
|
||||
|
||||
// The refreshes of this frame (SignalRefreshed), resolved by the flush.
|
||||
struct PendingRefresh {
|
||||
uint64_t guid;
|
||||
uint32_t spellId;
|
||||
int slot;
|
||||
};
|
||||
constexpr int kPendingRefreshMax = 32;
|
||||
PendingRefresh g_pendingRefreshes[kPendingRefreshMax];
|
||||
int g_pendingRefreshCount = 0;
|
||||
|
||||
// A server refresh the entry mirrors — a recast, a mechanic's reset to a full
|
||||
// duration: the timing signal, and a Refreshed change for the observers. Not
|
||||
// for the tick-speed compression's periodic nudge (this client's estimate
|
||||
// moving, no server event) nor a reduction.
|
||||
void SignalEntryRefreshed(const Entry &e) {
|
||||
SignalEntryChanged(e);
|
||||
SignalRefreshed(e.targetGuid, e.spellId, e.slot);
|
||||
}
|
||||
|
||||
// The slot of `unit`'s descriptor holding `spellId`, or -1.
|
||||
int DescriptorSlotOf(const uint8_t *unit, uint32_t spellId) {
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
if (Aura::Data::ReadSpellID(unit, slot) == spellId)
|
||||
return slot;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Reports the frame's refreshes whose aura the descriptor still holds, in the
|
||||
// slot that was named when it still holds the spell, else wherever it is now.
|
||||
void FlushRefreshes() {
|
||||
constexpr int kUnitOrPlayerMask =
|
||||
(1 << Offsets::OBJECT_TYPE_UNIT) | (1 << Offsets::OBJECT_TYPE_PLAYER);
|
||||
const int count = g_pendingRefreshCount;
|
||||
g_pendingRefreshCount = 0; // an observer's own action may queue again
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const PendingRefresh &p = g_pendingRefreshes[i];
|
||||
void *unit = Object::ByGuid(kUnitOrPlayerMask, p.guid, nullptr, 0);
|
||||
if (unit == nullptr)
|
||||
continue;
|
||||
const auto *u = static_cast<const uint8_t *>(unit);
|
||||
int slot = p.slot;
|
||||
if (slot < 0 || Aura::Data::ReadSpellID(u, slot) != p.spellId)
|
||||
slot = DescriptorSlotOf(u, p.spellId);
|
||||
if (slot < 0)
|
||||
continue;
|
||||
NotifyObservers(MakeChange(ChangeType::Refreshed, unit, slot, p.spellId));
|
||||
}
|
||||
}
|
||||
|
||||
void FlushAuraSignals() {
|
||||
using Broadcast_t = void(__fastcall *)(uint64_t *guid, uint32_t eventCode);
|
||||
auto broadcast = reinterpret_cast<Broadcast_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_UNIT_EVENT_BROADCAST));
|
||||
FlushRefreshes();
|
||||
// The engine broadcast plus ClassicAPI's tokens (see Unit::TokenEvents).
|
||||
for (int i = 0; i < g_pendingSignalCount; ++i)
|
||||
broadcast(&g_pendingSignals[i], Offsets::UNIT_EVENT_UNIT_AURA);
|
||||
Unit::TokenEvents::Broadcast(&g_pendingSignals[i], Offsets::UNIT_EVENT_UNIT_AURA);
|
||||
g_pendingSignalCount = 0;
|
||||
}
|
||||
|
||||
void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
switch (m.op) {
|
||||
void ApplyModOp(Entry &e, int32_t op, int32_t valueMs, uint32_t now) {
|
||||
switch (op) {
|
||||
case MOD_REFRESH:
|
||||
if (e.durationMs > 0) {
|
||||
e.expirationMs = now + e.durationMs; // RefreshHolder → reset to max
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryRefreshed(e);
|
||||
}
|
||||
break;
|
||||
case MOD_SET:
|
||||
e.durationMs = static_cast<uint32_t>(m.valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(m.valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
e.durationMs = static_cast<uint32_t>(valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(valueMs);
|
||||
SignalEntryRefreshed(e);
|
||||
break;
|
||||
case MOD_REDUCE:
|
||||
if (e.expirationMs != 0) {
|
||||
if (e.expirationMs > now + static_cast<uint32_t>(m.valueMs)) {
|
||||
e.expirationMs -= static_cast<uint32_t>(m.valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
if (Time::Clock::Remaining(now, e.expirationMs) >
|
||||
static_cast<uint32_t>(valueMs)) {
|
||||
e.expirationMs -= static_cast<uint32_t>(valueMs);
|
||||
SignalEntryChanged(e);
|
||||
} else {
|
||||
e.used = false; // shaved to/past now → server removes it
|
||||
}
|
||||
@@ -645,6 +741,88 @@ void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
ApplyModOp(e, m.op, m.valueMs, now);
|
||||
}
|
||||
|
||||
// ---- Deferred duration edits (the trigger creates the aura it edits) ------
|
||||
//
|
||||
// A trigger does not always act on an aura that is ALREADY there. Bestial
|
||||
// Wrath's effect 0 is TRIGGER_SPELL(Scent of Blood), and the server's script
|
||||
// then stretches the very holder that trigger just created (tortoise-wow
|
||||
// spell_hunter_bestial_wrath). The trigger's own SMSG_SPELL_GO reaches us
|
||||
// first, so at trigger time either nothing matches yet, or something matches
|
||||
// and is overwritten moments later when the triggered cast stores its own base
|
||||
// duration. Either way the edit is lost.
|
||||
//
|
||||
// So a trigger also ARMS its targets, and the next matching store inside the
|
||||
// window re-applies the edit — from the cast path or the application path,
|
||||
// whichever the triggered aura arrives on.
|
||||
//
|
||||
// Only the IDEMPOTENT ops arm. Re-running refresh or set lands on the same
|
||||
// value, while reduce would shave a second time and remove would kill an aura
|
||||
// that was legitimately re-applied. That keeps Conflagrate (reduce) behaving
|
||||
// exactly as before.
|
||||
bool OpIsIdempotent(int32_t op) {
|
||||
return op == MOD_REFRESH || op == MOD_SET;
|
||||
}
|
||||
|
||||
struct PendingMod {
|
||||
uint64_t target;
|
||||
uint64_t caster;
|
||||
uint32_t affectedFamily;
|
||||
uint64_t affectedMask;
|
||||
uint32_t affectedIcon;
|
||||
int32_t op;
|
||||
int32_t valueMs;
|
||||
uint32_t untilMs;
|
||||
bool used;
|
||||
};
|
||||
// The triggered cast follows its trigger within one packet burst, so the window
|
||||
// only has to cover network jitter. Sized like the other arm tables.
|
||||
constexpr int kPendingModMax = 8;
|
||||
constexpr uint32_t kPendingModTtlMs = 2000;
|
||||
PendingMod g_pendingMods[kPendingModMax];
|
||||
|
||||
void ArmPendingMod(uint64_t target, uint64_t caster, const DurationMod &m,
|
||||
uint32_t now) {
|
||||
PendingMod *slot = nullptr;
|
||||
for (auto &p : g_pendingMods) {
|
||||
// Re-arming the same rule on the same target refreshes it in place.
|
||||
if (p.used && p.target == target && p.affectedFamily == m.affectedFamily &&
|
||||
p.affectedMask == m.affectedMask && p.affectedIcon == m.affectedIcon) {
|
||||
slot = &p;
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr && (!p.used || Time::Clock::Reached(now, p.untilMs)))
|
||||
slot = &p;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
slot = &g_pendingMods[0]; // all live — steal the first
|
||||
*slot = {target, caster, m.affectedFamily, m.affectedMask,
|
||||
m.affectedIcon, m.op, m.valueMs, now + kPendingModTtlMs, true};
|
||||
}
|
||||
|
||||
void ConsumePendingMods(Entry &e, uint32_t now) {
|
||||
for (auto &p : g_pendingMods) {
|
||||
if (!p.used || p.target != e.targetGuid ||
|
||||
Time::Clock::Reached(now, p.untilMs))
|
||||
continue;
|
||||
// Caster-scoped exactly like the immediate path, adoption included.
|
||||
if (e.casterGuid != 0 && e.casterGuid != p.caster)
|
||||
continue;
|
||||
if (!AffectedMatchesRaw(
|
||||
Spell::Lookup::RecordForID(static_cast<int>(e.spellId)),
|
||||
p.affectedFamily, p.affectedMask, p.affectedIcon))
|
||||
continue;
|
||||
if (e.casterGuid == 0)
|
||||
e.casterGuid = p.caster;
|
||||
ApplyModOp(e, p.op, p.valueMs, now);
|
||||
p.used = false; // one-shot, like the server's single-holder edit
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// On a trigger cast landing on its hit targets, mirror the server's duration
|
||||
// edit on the caster's own matching cached aura. Called from SpellGo_h before
|
||||
// the aura gate — triggers (Conflagrate, Molten Blast) apply no aura of their
|
||||
@@ -681,6 +859,11 @@ void ApplyDurationModifiers(uint32_t triggerSpellId, uint64_t caster,
|
||||
ApplyMod(e, m, now);
|
||||
break; // one matching aura per (rule, target), like the server
|
||||
}
|
||||
// The aura may not be here yet, or may be about to be overwritten by
|
||||
// the cast this trigger sets off. Arm the idempotent ops so the edit
|
||||
// lands when it arrives — see PendingMod.
|
||||
if (OpIsIdempotent(m.op))
|
||||
ArmPendingMod(targets[t], caster, m, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -689,8 +872,11 @@ bool RegisterDurationMod(uint32_t triggerSpellId, uint32_t triggerFamily,
|
||||
int32_t triggerSchool, uint32_t affectedFamily,
|
||||
uint64_t affectedMask, uint32_t affectedIcon, int op,
|
||||
int32_t valueMs) {
|
||||
// Trigger must be identified one way or the other.
|
||||
if ((triggerSpellId == 0 && triggerFamily == 0) || affectedMask == 0 ||
|
||||
// Trigger must be identified one way or the other, and the affected aura
|
||||
// needs at least one discriminator past its family — a flag overlap, an
|
||||
// icon, or both. Neither would match every aura of a class.
|
||||
if ((triggerSpellId == 0 && triggerFamily == 0) ||
|
||||
(affectedMask == 0 && affectedIcon == 0) ||
|
||||
op < MOD_REFRESH || op > MOD_REMOVE)
|
||||
return false;
|
||||
for (int i = 0; i < g_modCount; ++i) { // replace an identical rule
|
||||
@@ -760,10 +946,31 @@ int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDurationModArgs[] = {
|
||||
Game::Doc::Req("triggerFamily", "number", "The SpellFamilyName of the triggering cast."),
|
||||
Game::Doc::Req("triggerSchool", "number",
|
||||
"The school index the trigger must have, or below 0 for any school."),
|
||||
Game::Doc::Req("affectedFamily", "number", "The SpellFamilyName of the aura to change."),
|
||||
Game::Doc::Req("affectedFamilyFlags", "number",
|
||||
"A SpellFamilyFlags mask the aura must overlap, or 0 to match on the icon."),
|
||||
Game::Doc::Req("affectedIcon", "number",
|
||||
"The SpellIconID the aura must have, or 0 for any icon."),
|
||||
Game::Doc::Req("op", "string", "refresh, reduce, set, or remove."),
|
||||
Game::Doc::Opt("valueSeconds", "number", nullptr, "The amount for reduce and set."),
|
||||
};
|
||||
const Game::Doc::Field kDurationModRets[] = {
|
||||
Game::Doc::Req("success", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kRegisterAuraDurationModifierByTrigger{
|
||||
"Changes the shown duration of the caster's aura on a unit when a matching cast by "
|
||||
"that caster lands.",
|
||||
kDurationModArgs, kDurationModRets, nullptr, true};
|
||||
|
||||
void RegisterDurationModLua() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras",
|
||||
"RegisterAuraDurationModifierByTrigger",
|
||||
&Script_RegisterAuraDurationModifierByTrigger);
|
||||
&Script_RegisterAuraDurationModifierByTrigger,
|
||||
&kRegisterAuraDurationModifierByTrigger);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
|
||||
@@ -890,7 +1097,7 @@ void CompressTarget(const TickCompression &c, uint64_t targetGuid,
|
||||
// pollers pick it up on their own. First write nudges immediately.
|
||||
if (s->lastSignalMs == 0 || now - s->lastSignalMs >= 1000) {
|
||||
s->lastSignalMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryChanged(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1106,7 +1313,7 @@ int RefreshJudgementsImpl(uint64_t unitGuid, uint64_t attackerGuid, bool adopt)
|
||||
e.casterGuid = attackerGuid; // adoption claim
|
||||
e.expirationMs = now + e.durationMs; // RefreshHolder → own duration
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryRefreshed(e);
|
||||
++refreshed;
|
||||
}
|
||||
return refreshed;
|
||||
@@ -1143,23 +1350,6 @@ constexpr int kTrigAppMax = 32;
|
||||
TriggeredApplication g_trigApps[kTrigAppMax];
|
||||
int g_trigAppCount = 0;
|
||||
|
||||
// Talent gate: does the player currently know `spellID`? The engine's
|
||||
// spell-knowledge bitmap (same store IsPlayerSpell reads — covers talent
|
||||
// passives), checked live at trigger time so respecs are honored.
|
||||
bool PlayerKnowsSpell(uint32_t spellID) {
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
auto *bitmap = Game::Read<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELL_BITMAP));
|
||||
if (bitmap == nullptr)
|
||||
return false;
|
||||
const uint32_t spellCount = Game::Read<uint32_t>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_RECORD_COUNT));
|
||||
if (spellID > spellCount)
|
||||
return false;
|
||||
return (bitmap[spellID >> 5] & (1u << (spellID & 31))) != 0;
|
||||
}
|
||||
|
||||
// Armed rule instance awaiting its application on `victim`. Caster is
|
||||
// implicitly the local player (only player triggers arm).
|
||||
struct TrigAppArm {
|
||||
@@ -1216,7 +1406,7 @@ void RefreshTriggeredExisting(uint64_t victim, const TriggeredApplication &r,
|
||||
e.durationMs = newMs;
|
||||
e.expirationMs = now + newMs;
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(victim);
|
||||
SignalEntryRefreshed(e);
|
||||
break; // one aura instance per (target, spell, caster) — server touches one holder
|
||||
}
|
||||
}
|
||||
@@ -1235,7 +1425,7 @@ void ArmTriggeredApplications(uint64_t victim, uint32_t triggerSpellId,
|
||||
: !Spell::Lookup::IsFitToFamily(triggerRec, r.triggerFamily,
|
||||
r.triggerMask))
|
||||
continue;
|
||||
if (r.gateSpellId != 0 && !PlayerKnowsSpell(r.gateSpellId))
|
||||
if (r.gateSpellId != 0 && !Spell::Known::IsKnown(r.gateSpellId))
|
||||
continue;
|
||||
const uint32_t now = NowMs();
|
||||
RefreshTriggeredExisting(victim, r, now);
|
||||
@@ -1286,6 +1476,114 @@ int32_t ConsumeTriggeredApplication(uint64_t victim, const uint8_t *rec,
|
||||
// keeps the parse short and robust.
|
||||
constexpr int kMaxTargets = 16;
|
||||
|
||||
// ---- Triggered auras (SPELL_EFFECT_TRIGGER_SPELL) -------------------------
|
||||
//
|
||||
// An effect of type TRIGGER_SPELL makes the server cast EffectTriggerSpell[i]
|
||||
// as a side effect of the cast — Pounce 9005 → Pounce Bleed 9007 (Spell.dbc:
|
||||
// Effect = {6, 64, 80}, EffectTriggerSpell = {0, 9007, 0}). When the
|
||||
// triggered spell's own SMSG_SPELL_GO never reaches the client, its aura is
|
||||
// seated caster-less by OnAuraAdded. The trigger's SPELL_GO names the caster,
|
||||
// so attribution goes through it, read off the DBC link rather than a table.
|
||||
//
|
||||
// A hit by the TRIGGER doesn't prove the triggered aura landed: a target
|
||||
// immune to bleed takes Pounce's stun but not its bleed. So a fresh
|
||||
// application is ARMED, and only the application hook — the descriptor proof
|
||||
// that it landed — consumes the arm (same shape as the judgement arm above).
|
||||
// An instance this caster already holds on the target is refreshed in place
|
||||
// as well: the server's re-apply reuses the descriptor slot, so no
|
||||
// OnAuraAdded fires (the remove+add RefreshTriggeredExisting mirrors).
|
||||
//
|
||||
// Any caster: nothing here needs the caster's talents, and a remote caster
|
||||
// gets the base duration exactly as their directly cast auras do.
|
||||
|
||||
struct TrigAuraArm {
|
||||
uint64_t victim; // 0 = empty
|
||||
uint64_t caster;
|
||||
uint32_t spellId; // the triggered aura
|
||||
uint32_t untilMs;
|
||||
};
|
||||
constexpr int kTrigAuraArmMax = 8;
|
||||
constexpr uint32_t kTrigAuraArmTtlMs = 2000;
|
||||
TrigAuraArm g_trigAuraArms[kTrigAuraArmMax];
|
||||
|
||||
void ArmTriggeredAura(uint64_t victim, uint32_t spellId, uint64_t caster,
|
||||
uint32_t now) {
|
||||
TrigAuraArm *slot = nullptr;
|
||||
for (auto &a : g_trigAuraArms) {
|
||||
if (a.victim == victim && a.spellId == spellId && a.caster == caster) {
|
||||
slot = &a; // re-trigger inside the TTL — refresh in place
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr &&
|
||||
(a.victim == 0 || Time::Clock::Reached(now, a.untilMs)))
|
||||
slot = &a;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
slot = &g_trigAuraArms[0]; // all live — steal the first
|
||||
*slot = {victim, caster, spellId, now + kTrigAuraArmTtlMs};
|
||||
}
|
||||
|
||||
// The caster armed for `spellId` landing on `victim`, or 0. One-shot.
|
||||
uint64_t ConsumeTriggeredAura(uint64_t victim, uint32_t spellId, uint32_t now) {
|
||||
if (victim == 0)
|
||||
return 0;
|
||||
for (auto &a : g_trigAuraArms) {
|
||||
if (a.victim != victim || a.spellId != spellId ||
|
||||
Time::Clock::Reached(now, a.untilMs))
|
||||
continue;
|
||||
const uint64_t caster = a.caster;
|
||||
a = {};
|
||||
return caster;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Duration of an aura cast by `caster`. Combo-point finishers (Rupture,
|
||||
// Kidney Shot, …) have their real duration computed at cast time from the
|
||||
// combo points spent — the base-duration helper can't know it.
|
||||
// ComboDuration mirrors the server's computation from the CP snapshot its
|
||||
// send hook captured; 0 means "not combo-scaled", fall through to the base
|
||||
// path. 0 overall = unknown.
|
||||
uint32_t CastDurationMs(const uint8_t *rec, uint32_t spellId,
|
||||
bool casterIsPlayer) {
|
||||
const uint32_t comboMs =
|
||||
casterIsPlayer ? ComboDuration::TryComboScaledMs(rec, spellId) : 0;
|
||||
return comboMs != 0 ? comboMs : SpellDurationMs(rec, casterIsPlayer);
|
||||
}
|
||||
|
||||
void ApplyTriggeredAuras(uint64_t caster, uint32_t spellId, const uint8_t *rec,
|
||||
const uint64_t *targets, int numTargets) {
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
const auto *effects = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
const auto *triggered = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_TRIGGER_SPELL);
|
||||
const bool casterIsPlayer = (caster == Unit::Identity::PlayerGuid());
|
||||
const uint32_t now = NowMs();
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (effects[i] != Offsets::SPELL_EFFECT_TRIGGER_SPELL ||
|
||||
triggered[i] <= 0)
|
||||
continue;
|
||||
const auto auraId = static_cast<uint32_t>(triggered[i]);
|
||||
if (auraId == spellId)
|
||||
continue;
|
||||
const uint8_t *auraRec = Spell::Lookup::RecordForID(triggered[i]);
|
||||
if (!SpellAppliesAura(auraRec))
|
||||
continue;
|
||||
const uint32_t durationMs =
|
||||
CastDurationMs(auraRec, auraId, casterIsPlayer);
|
||||
const uint32_t expirationMs = durationMs > 0 ? now + durationMs : 0;
|
||||
for (int t = 0; t < numTargets; ++t) {
|
||||
ArmTriggeredAura(targets[t], auraId, caster, now);
|
||||
if (FindByCaster(targets[t], auraId, caster) != nullptr &&
|
||||
DescriptorListsAura(targets[t], auraId))
|
||||
StoreFromCast(targets[t], auraId, caster, expirationMs,
|
||||
durationMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The self-contained SPELL_GO processing (succeeded events, server-side
|
||||
// duration edits, aura-source caching). None of it reads engine descriptor
|
||||
// state, so it's order-independent of the engine's own SPELL_GO handler —
|
||||
@@ -1335,19 +1633,23 @@ void HandleSpellGo(uint64_t caster, uint32_t spellId, const uint64_t *targets,
|
||||
for (int i = 0; i < numTargets; ++i)
|
||||
ArmTriggeredApplications(targets[i], spellId, rec);
|
||||
|
||||
// The units this cast's auras land on: the hit list, or the caster itself
|
||||
// when the packet carries none (self-cast with a caster-implicit target).
|
||||
// Shared by the triggered auras and the cast's own aura below.
|
||||
const uint64_t *auraTargets = targets;
|
||||
int numAuraTargets = numTargets;
|
||||
if (numTargets == 0) {
|
||||
auraTargets = &caster;
|
||||
numAuraTargets = 1;
|
||||
}
|
||||
|
||||
ApplyTriggeredAuras(caster, spellId, rec, auraTargets, numAuraTargets);
|
||||
|
||||
if (!SpellAppliesAura(rec))
|
||||
return;
|
||||
|
||||
const bool casterIsPlayer = (caster == Unit::Identity::PlayerGuid());
|
||||
// Combo-point finishers (Rupture, Kidney Shot, …) have their real
|
||||
// duration computed at cast time from the combo points spent — the
|
||||
// base-duration helper can't know it. ComboDuration mirrors the
|
||||
// server's computation from the CP snapshot its send hook captured;
|
||||
// 0 means "not combo-scaled", fall through to the base path.
|
||||
uint32_t durationMs =
|
||||
casterIsPlayer ? ComboDuration::TryComboScaledMs(rec, spellId) : 0;
|
||||
if (durationMs == 0)
|
||||
durationMs = SpellDurationMs(rec, casterIsPlayer);
|
||||
const uint32_t durationMs = CastDurationMs(rec, spellId, casterIsPlayer);
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
|
||||
// Let the application hooks recover the player-talented duration for this
|
||||
@@ -1356,13 +1658,8 @@ void HandleSpellGo(uint64_t caster, uint32_t spellId, const uint64_t *targets,
|
||||
if (casterIsPlayer)
|
||||
RememberPlayerCast(spellId);
|
||||
|
||||
if (numTargets == 0) {
|
||||
// No explicit hit list (self-cast with caster-implicit target).
|
||||
StoreFromCast(caster, spellId, caster, expirationMs, durationMs);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < numTargets; ++i)
|
||||
StoreFromCast(targets[i], spellId, caster, expirationMs, durationMs);
|
||||
for (int i = 0; i < numAuraTargets; ++i)
|
||||
StoreFromCast(auraTargets[i], spellId, caster, expirationMs, durationMs);
|
||||
}
|
||||
|
||||
// SMSG_SPELL_GO parse (funnel subscriber). At the leaf handler the engine has
|
||||
@@ -1395,10 +1692,14 @@ const Net::PacketDispatch::AutoSubscribe _spellGoSub{&SpellGoSub};
|
||||
|
||||
// ---- Aura-application co-hooks (timing for proc / triggered auras) -------
|
||||
|
||||
// Classify by the absolute aura slot: 0..BUFF_COUNT-1 = buff (helpful),
|
||||
// BUFF_COUNT..TOTAL-1 = debuff (harmful).
|
||||
int8_t KindForSlot(int slot) {
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT ? KIND_HARMFUL : KIND_HELPFUL;
|
||||
// Defer to `Aura::Data::IsSlotHarmful` rather than testing the range here — how
|
||||
// polarity is encoded differs between servers, and that function is the one
|
||||
// place that decides. The descriptor write pass has already landed both the
|
||||
// slot and its flags by the time the application hooks fire.
|
||||
int8_t KindForSlot(const void *unit, int slot) {
|
||||
return Aura::Data::IsSlotHarmful(static_cast<const uint8_t *>(unit), slot)
|
||||
? KIND_HARMFUL
|
||||
: KIND_HELPFUL;
|
||||
}
|
||||
|
||||
// Stamp expiration for an aura that just landed/refreshed in `slot` on `unit`.
|
||||
@@ -1441,6 +1742,16 @@ void StampApplication(void *unit, uint32_t spellId, int slot) {
|
||||
durationMs = 1;
|
||||
}
|
||||
}
|
||||
// A triggered-aura arm (Pounce → Pounce Bleed): the server cast this aura
|
||||
// off a trigger whose SPELL_GO named the caster, and this application is
|
||||
// the proof it landed. Target-scoped, so it goes before the spell-scoped
|
||||
// recent-cast window below, for the same reason as the nettle arm.
|
||||
if (caster == 0) {
|
||||
caster = ConsumeTriggeredAura(unitGuid, spellId, NowMs());
|
||||
if (caster != 0)
|
||||
durationMs =
|
||||
SpellDurationMs(rec, caster == Unit::Identity::PlayerGuid());
|
||||
}
|
||||
if (caster == 0) {
|
||||
const bool byPlayer = WasRecentPlayerCast(spellId);
|
||||
durationMs = SpellDurationMs(rec, byPlayer);
|
||||
@@ -1454,7 +1765,7 @@ void StampApplication(void *unit, uint32_t spellId, int slot) {
|
||||
}
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
StoreFromApplication(unitGuid, spellId, caster, expirationMs, durationMs,
|
||||
slot, KindForSlot(slot));
|
||||
slot, KindForSlot(unit, slot));
|
||||
}
|
||||
|
||||
// Bump the player-stat-inputs signal when an aura change hits the LOCAL
|
||||
@@ -1466,6 +1777,30 @@ void NotifyIfPlayer(void *unit) {
|
||||
Player::StatSignal::Notify();
|
||||
}
|
||||
|
||||
const AutoObserve *g_observers = nullptr;
|
||||
|
||||
void NotifyObservers(const Change &change) {
|
||||
for (const AutoObserve *o = g_observers; o != nullptr; o = o->next)
|
||||
o->callback(change);
|
||||
}
|
||||
|
||||
// The change for the aura in `slot`, with the caster and polarity of the
|
||||
// instance this cache binds to it. A removed aura's slot already holds the
|
||||
// next aura's flags, so its polarity is the one recorded when it landed.
|
||||
Change MakeChange(ChangeType type, void *unit, int slot, uint32_t spellId) {
|
||||
Change c{type, unit, slot, spellId, 0, false, 0, 0};
|
||||
const Entry *e = FindInstance(Unit::Identity::GuidForObject(unit), spellId, slot);
|
||||
if (e != nullptr)
|
||||
c.caster = e->casterGuid;
|
||||
if (type != ChangeType::Removed)
|
||||
c.harmful = KindForSlot(unit, slot) == KIND_HARMFUL;
|
||||
else if (e != nullptr && e->kind != KIND_UNKNOWN)
|
||||
c.harmful = e->kind == KIND_HARMFUL;
|
||||
else
|
||||
c.harmful = slot >= Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
return c;
|
||||
}
|
||||
|
||||
// OnAuraAdded — a new aura occupies a slot (gives the spellId directly).
|
||||
using OnAuraAdded_t = void(__fastcall *)(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId);
|
||||
@@ -1476,6 +1811,7 @@ void __fastcall OnAuraAdded_h(void *unit, void *edx, uint32_t slot,
|
||||
g_origOnAuraAdded(unit, edx, slot, spellId);
|
||||
StampApplication(unit, spellId, static_cast<int>(slot));
|
||||
NotifyIfPlayer(unit);
|
||||
NotifyObservers(MakeChange(ChangeType::Added, unit, static_cast<int>(slot), spellId));
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraAdded{
|
||||
@@ -1485,17 +1821,24 @@ const Game::HookAutoRegister _hookAuraAdded{
|
||||
// OnAuraStacksChanged — an existing aura's stack count changed (e.g. Shadow
|
||||
// Weaving climbing). Only the slot is given, so read the spellID back from
|
||||
// the unit's aura array. Re-stamps expiration so stacking refreshes count.
|
||||
// The byte argument is the slot's OLD applications byte: the engine compares
|
||||
// it with the descriptor's new one and logs a stack line when the new is
|
||||
// higher (0x00612450).
|
||||
using OnAuraStacksChanged_t = void(__fastcall *)(void *unit, void *edx,
|
||||
int slot, uint8_t stackCount);
|
||||
int slot, uint8_t oldApplications);
|
||||
OnAuraStacksChanged_t g_origOnAuraStacksChanged = nullptr;
|
||||
|
||||
void __fastcall OnAuraStacksChanged_h(void *unit, void *edx, int slot,
|
||||
uint8_t stackCount) {
|
||||
g_origOnAuraStacksChanged(unit, edx, slot, stackCount);
|
||||
StampApplication(
|
||||
unit,
|
||||
Aura::Data::ReadSpellID(static_cast<const uint8_t *>(unit), slot), slot);
|
||||
uint8_t oldApplications) {
|
||||
g_origOnAuraStacksChanged(unit, edx, slot, oldApplications);
|
||||
const auto *u = static_cast<const uint8_t *>(unit);
|
||||
const uint32_t spellId = Aura::Data::ReadSpellID(u, slot);
|
||||
StampApplication(unit, spellId, slot);
|
||||
NotifyIfPlayer(unit);
|
||||
Change c = MakeChange(ChangeType::Stacks, unit, slot, spellId);
|
||||
c.oldStacks = static_cast<int>(oldApplications) + 1;
|
||||
c.newStacks = Aura::Data::ReadStacks(u, slot);
|
||||
NotifyObservers(c);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraStacks{
|
||||
@@ -1523,6 +1866,7 @@ OnAuraRemoved_t g_origOnAuraRemoved = nullptr;
|
||||
void __fastcall OnAuraRemoved_h(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId) {
|
||||
g_origOnAuraRemoved(unit, edx, slot, spellId);
|
||||
NotifyObservers(MakeChange(ChangeType::Removed, unit, static_cast<int>(slot), spellId));
|
||||
Evict(Unit::Identity::GuidForObject(unit), spellId, static_cast<int>(slot));
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
@@ -1550,6 +1894,42 @@ bool Get(uint64_t unitGuid, uint32_t spellId, int slot, uint64_t *outCaster,
|
||||
return true;
|
||||
}
|
||||
|
||||
void CastersForSlots(uint64_t unitGuid, const uint32_t *slotSpellIds,
|
||||
uint64_t *outCaster) {
|
||||
constexpr int kSlots = Offsets::UNIT_AURA_TOTAL;
|
||||
const Entry *seated[kSlots] = {};
|
||||
const Entry *sole[kSlots] = {};
|
||||
int soleCount[kSlots] = {};
|
||||
for (int i = 0; i < kSlots; ++i)
|
||||
outCaster[i] = 0;
|
||||
if (unitGuid == 0)
|
||||
return;
|
||||
// `FindInstance` for all slots in one pass: the entry seated in the slot
|
||||
// (FindBySlot), else the unit's only entry for the slot's spell (FindSole).
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
const Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != unitGuid)
|
||||
continue;
|
||||
if (e.slot >= 0 && e.slot < kSlots && slotSpellIds[e.slot] == e.spellId &&
|
||||
seated[e.slot] == nullptr)
|
||||
seated[e.slot] = &e;
|
||||
for (int s = 0; s < kSlots; ++s) {
|
||||
if (slotSpellIds[s] == e.spellId) {
|
||||
sole[s] = &e;
|
||||
++soleCount[s];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int s = 0; s < kSlots; ++s) {
|
||||
if (slotSpellIds[s] == 0)
|
||||
continue;
|
||||
if (seated[s] != nullptr)
|
||||
outCaster[s] = seated[s]->casterGuid;
|
||||
else if (soleCount[s] == 1)
|
||||
outCaster[s] = sole[s]->casterGuid;
|
||||
}
|
||||
}
|
||||
|
||||
bool AddDurationMod(uint32_t triggerSpellId, uint32_t affectedFamily,
|
||||
uint64_t affectedMask, uint32_t affectedIcon, int op,
|
||||
int32_t valueMs) {
|
||||
@@ -1641,7 +2021,9 @@ int RefreshJudgements(uint64_t unitGuid, uint64_t attackerGuid) {
|
||||
uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
uint64_t mask, uint32_t icon,
|
||||
uint64_t casterGuid) {
|
||||
if (unitGuid == 0 || mask == 0 || casterGuid == 0)
|
||||
// Same selector invariant the rule registrar enforces: family plus at
|
||||
// least one of flag-overlap / icon.
|
||||
if (unitGuid == 0 || (mask == 0 && icon == 0) || casterGuid == 0)
|
||||
return 0;
|
||||
const uint32_t now = NowMs();
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
@@ -1666,7 +2048,7 @@ uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
// duration, not a five-point one.
|
||||
e.expirationMs = now + e.durationMs;
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryRefreshed(e);
|
||||
return e.spellId;
|
||||
}
|
||||
return 0;
|
||||
@@ -1687,7 +2069,7 @@ void RestampPlayerChannel(uint32_t spellId, uint32_t remainingMs) {
|
||||
continue;
|
||||
e.expirationMs = now + remainingMs;
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryChanged(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1739,7 +2121,7 @@ int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut) {
|
||||
const Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != unitGuid || e.kind != want)
|
||||
continue;
|
||||
if (e.expirationMs != 0 && now >= e.expirationMs)
|
||||
if (e.expirationMs != 0 && Time::Clock::Reached(now, e.expirationMs))
|
||||
continue; // expired (infinite-duration entries pass)
|
||||
out[n++] = {e.spellId, e.casterGuid, e.expirationMs, e.durationMs,
|
||||
e.slot};
|
||||
@@ -1798,4 +2180,23 @@ void ObserveGroupAuras(uint64_t guid, const uint16_t *auraArray) {
|
||||
snap->touchMs = now;
|
||||
}
|
||||
|
||||
AutoObserve::AutoObserve(ChangeCallback cb) : callback(cb), next(g_observers) {
|
||||
g_observers = this;
|
||||
}
|
||||
|
||||
void SignalRefreshed(uint64_t unitGuid, uint32_t spellId, int slot) {
|
||||
if (unitGuid == 0 || spellId == 0)
|
||||
return;
|
||||
for (int i = 0; i < g_pendingRefreshCount; ++i) {
|
||||
PendingRefresh &p = g_pendingRefreshes[i];
|
||||
if (p.guid == unitGuid && p.spellId == spellId) {
|
||||
if (p.slot < 0)
|
||||
p.slot = slot;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (g_pendingRefreshCount < kPendingRefreshMax)
|
||||
g_pendingRefreshes[g_pendingRefreshCount++] = {unitGuid, spellId, slot};
|
||||
}
|
||||
|
||||
} // namespace Aura::Source
|
||||
|
||||
+54
-6
@@ -60,11 +60,21 @@ constexpr int SLOT_UNBOUND = -1;
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, int slot, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs);
|
||||
|
||||
// `Get`'s caster for every descriptor slot at once, in one pass over the
|
||||
// cache: `outCaster[slot]` is the caster `Get(unitGuid, slotSpellIds[slot],
|
||||
// slot, ...)` would report, or 0. `slotSpellIds` and `outCaster` hold
|
||||
// `Offsets::UNIT_AURA_TOTAL` entries. For walks that need every slot's caster
|
||||
// (the own-auras-first sort), where 48 separate `Get` scans would cost 48
|
||||
// passes.
|
||||
void CastersForSlots(uint64_t unitGuid, const uint32_t *slotSpellIds,
|
||||
uint64_t *outCaster);
|
||||
|
||||
// Refreshes `casterGuid`'s aura on `unitGuid` matching the same selector the
|
||||
// duration rules use — SpellFamilyName + family-flag overlap + optional icon
|
||||
// (0 = any) — to a full duration from now. Returns the spellID refreshed, or 0
|
||||
// if nothing matched. For a mechanic whose duration edit reaches the client
|
||||
// too late to be attributed to a cast packet, so no rule can express it.
|
||||
// duration rules use — SpellFamilyName plus a family-flag overlap and/or a
|
||||
// SpellIconID, at least one of the two — to a full duration from now. Returns
|
||||
// the spellID refreshed, or 0 if nothing matched. For a mechanic whose
|
||||
// duration edit reaches the client too late to be attributed to a cast packet,
|
||||
// so no rule can express it.
|
||||
uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
uint64_t mask, uint32_t icon,
|
||||
uint64_t casterGuid);
|
||||
@@ -108,8 +118,10 @@ enum DurationModOp {
|
||||
};
|
||||
|
||||
// Register a server duration-modifier rule from C++. The trigger is matched by
|
||||
// exact `triggerSpellId`; the affected aura by SpellFamilyName + a family-flag
|
||||
// overlap (+ optional `affectedIcon`, 0 = any). `valueMs` is the reduce/set
|
||||
// exact `triggerSpellId`; the affected aura by SpellFamilyName plus an
|
||||
// `affectedMask` family-flag overlap and/or an `affectedIcon`, at least one of
|
||||
// the two (0 drops that half of the test; a custom spell with no family flags
|
||||
// is named by its icon alone). `valueMs` is the reduce/set
|
||||
// amount in milliseconds (ignored by refresh/remove). Used by src/turtle
|
||||
// modules for the server's built-in mods (the family/school-matched variant is
|
||||
// still Lua-registerable via C_UnitAuras.RegisterAuraDurationModifierByTrigger).
|
||||
@@ -225,4 +237,40 @@ int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut);
|
||||
// any real SpellGo timing we already hold, so this never downgrades better data.
|
||||
void ObserveGroupAuras(uint64_t guid, const uint16_t *auraArray);
|
||||
|
||||
// An aura arriving on a unit, leaving it, or changing its stack count, as the
|
||||
// engine's own handlers see it (`OnAuraAdded`, `OnAuraRemoved`,
|
||||
// `OnAuraStacksChanged`), with the caster this cache binds to it — or its
|
||||
// duration set again while it stays (Refreshed, see `SignalRefreshed`).
|
||||
enum class ChangeType { Added, Removed, Stacks, Refreshed };
|
||||
|
||||
struct Change {
|
||||
ChangeType type;
|
||||
void *unit;
|
||||
int slot;
|
||||
uint32_t spellId;
|
||||
uint64_t caster; // 0 when unknown
|
||||
bool harmful;
|
||||
int oldStacks; // Stacks only: the count before the change
|
||||
int newStacks; // Stacks only: the count now
|
||||
};
|
||||
|
||||
using ChangeCallback = void (*)(const Change &change);
|
||||
|
||||
// Declare a file-scope `static const Aura::Source::AutoObserve _obs{&fn};`.
|
||||
// Added and Stacks reach the callback after the cache has stamped the aura,
|
||||
// Removed before the cache evicts it, so `caster` is the cache's in all three.
|
||||
struct AutoObserve {
|
||||
explicit AutoObserve(ChangeCallback cb);
|
||||
ChangeCallback callback;
|
||||
const AutoObserve *next;
|
||||
};
|
||||
|
||||
// A duration set again for an aura already on `unitGuid`: the server's
|
||||
// SMSG_UPDATE_AURA_DURATION for an occupied slot of the player, a recast this
|
||||
// cache refreshed, a server refresh mechanic it mirrors. Observers get it as a
|
||||
// Refreshed change at the world tick, once per aura per frame, and only if the
|
||||
// descriptor then still holds the spell — a slot that changed hands in the
|
||||
// same tick was an application, not a refresh. `slot` may be SLOT_UNBOUND.
|
||||
void SignalRefreshed(uint64_t unitGuid, uint32_t spellId, int slot);
|
||||
|
||||
} // namespace Aura::Source
|
||||
|
||||
+63
-51
@@ -16,22 +16,38 @@
|
||||
// (added 5.4.x) with exact same-semantic coalescing: addons listen
|
||||
// to DELAYED instead of BAG_UPDATE and rescan once per frame.
|
||||
//
|
||||
// See TODO #67 for the full investigation history.
|
||||
// Architecture: three MinHooks on the engine's three BAG_UPDATE
|
||||
// emitters, plus one tick subscription. Names are benilla-re's.
|
||||
//
|
||||
// Architecture: three MinHooks plus one tick subscription.
|
||||
//
|
||||
// 1. `FUN_004F91A0` — bag-slot diff loop. Hook just sets the
|
||||
// `g_pending` flag.
|
||||
// 2. `FUN_004F9370` — item→bag resolver (most common path). Hook
|
||||
// just sets the `g_pending` flag.
|
||||
// 3. `FUN_004F8DB0` — bag-descriptor change callback. Covers the
|
||||
// keyring `BAG_UPDATE(slot)` direct-fire branch that doesn't
|
||||
// route through `FUN_004F9370`.
|
||||
// 1. `RaiseBagEvents` (`0x004F91A0`) — compares the ten bag slots
|
||||
// (4 bags, 6 bank bags) with the cached container GUIDs and
|
||||
// fires BAG_CLOSED / BAG_UPDATE per changed slot. Called by the
|
||||
// bag-slot field watcher and once at login.
|
||||
// 2. `RaiseBagUpdate` (`0x004F9370`) — fires BAG_UPDATE for the
|
||||
// player or one of the ten cached containers; the most common
|
||||
// path. Its callers are the backpack and container slot
|
||||
// watchers, and the item-field watcher `0x004F9480`, which
|
||||
// passes an item's container — the PLAYER for equipped gear, so
|
||||
// a durability change on worn gear fires BAG_UPDATE(0) and
|
||||
// therefore DELAYED. That is the engine's behaviour, kept.
|
||||
// 3. `OnBackpackSlotChanged` (`0x004F8DB0`) — the field watcher on
|
||||
// backpack slots 23..38 and keyring slots 81..112. Keyring
|
||||
// changes fire BAG_UPDATE directly here; backpack changes go
|
||||
// through `RaiseBagUpdate`. Standard watcher ABI
|
||||
// `__fastcall(offset, unused, guidLo, guidHi, previous*, user)`,
|
||||
// ret 0x10 — the same shape as `OnContainerSlotChanged`.
|
||||
// 4. `Tick::WorldTick::AutoSubscribe` — shared per-frame
|
||||
// subscription (the engine's world-subsystem update at
|
||||
// `FUN_0066FD50`, single-hooked in `src/tick/WorldTick.cpp`).
|
||||
// Our callback drains `g_pending` and fires DELAYED if set.
|
||||
//
|
||||
// The hooks set `g_pending` after every call rather than only when
|
||||
// an event fired. The one call that fires nothing is
|
||||
// `RaiseBagUpdate` for a container outside the cache, which does not
|
||||
// occur in practice; mirroring that check here would be a parallel
|
||||
// copy of the engine's test. The bank's main slots 39..62 have no
|
||||
// engine watcher, so they never fire BAG_UPDATE or DELAYED.
|
||||
//
|
||||
// All hook targets are quiet (≤3 callers each, in code regions
|
||||
// other DLLs don't touch). The previous attempt at this feature
|
||||
// hooked `FUN_FIRE_EVENT` (100+ callers, in the chat path) and a
|
||||
@@ -68,7 +84,12 @@ namespace {
|
||||
|
||||
constexpr const char *kEventName = "BAG_UPDATE_DELAYED";
|
||||
|
||||
const Event::Custom::AutoReserve _reserve{kEventName};
|
||||
const Game::Doc::Event kUpdateDelayedDoc{
|
||||
"Container",
|
||||
"Fires once in each frame in which one or more BAG_UPDATE events fired, "
|
||||
"after all of them.",
|
||||
{}};
|
||||
const Event::Custom::AutoReserve _reserve{kEventName, &kUpdateDelayedDoc};
|
||||
|
||||
// Set by the bag-update hooks, drained by the world-tick hook.
|
||||
// Single bool, no atomics needed — the engine is single-threaded.
|
||||
@@ -83,40 +104,31 @@ AutoSubscribe::AutoSubscribe(Callback cb) : cb(cb), next(g_subs) {
|
||||
g_subs = this;
|
||||
}
|
||||
|
||||
// `FUN_004F91A0` — `__cdecl void()`. Bag-slot diff loop. Walks the
|
||||
// 10 bag slots, fires BAG_UPDATE for each that changed.
|
||||
using BagSlotDiff_t = void(__cdecl *)();
|
||||
BagSlotDiff_t BagSlotDiff_o = nullptr;
|
||||
void __cdecl BagSlotDiff_h() {
|
||||
BagSlotDiff_o();
|
||||
// `RaiseBagEvents` — `__cdecl void()`.
|
||||
using RaiseBagEvents_t = void(__cdecl *)();
|
||||
RaiseBagEvents_t RaiseBagEvents_o = nullptr;
|
||||
void __cdecl RaiseBagEvents_h() {
|
||||
RaiseBagEvents_o();
|
||||
g_pending = true;
|
||||
}
|
||||
|
||||
// `FUN_004F9370` — `__stdcall void(int guidLo, int guidHi)`. Most
|
||||
// common BAG_UPDATE path — fires from every per-item descriptor
|
||||
// field change.
|
||||
using BagItemToBag_t = void(__stdcall *)(int guidLo, int guidHi);
|
||||
BagItemToBag_t BagItemToBag_o = nullptr;
|
||||
void __stdcall BagItemToBag_h(int guidLo, int guidHi) {
|
||||
BagItemToBag_o(guidLo, guidHi);
|
||||
// `RaiseBagUpdate` — `__stdcall void(containerGuidLo, containerGuidHi)`.
|
||||
using RaiseBagUpdate_t = void(__stdcall *)(int guidLo, int guidHi);
|
||||
RaiseBagUpdate_t RaiseBagUpdate_o = nullptr;
|
||||
void __stdcall RaiseBagUpdate_h(int guidLo, int guidHi) {
|
||||
RaiseBagUpdate_o(guidLo, guidHi);
|
||||
g_pending = true;
|
||||
}
|
||||
|
||||
// `FUN_004F8DB0` — `__thiscall unsigned(this, a1, a2, a3, a4)` in the
|
||||
// Octo build. Wired with `__fastcall` + dummy EDX to match the
|
||||
// thiscall ABI: ECX takes `this`, EDX unused, stack takes the four
|
||||
// dword args, callee cleans 16 bytes (`RET 0x10`). Same trick
|
||||
// `Frame::RegisterEvent` uses in DllMain, just with four stack args
|
||||
// instead of one — the Octo function is wider than the vanilla
|
||||
// signature Ghidra showed when this hook was first written.
|
||||
using KeyringDescChange_t = unsigned(__fastcall *)(void *bagDesc, void *edx_unused,
|
||||
unsigned a1, unsigned a2,
|
||||
unsigned a3, unsigned a4);
|
||||
KeyringDescChange_t KeyringDescChange_o = nullptr;
|
||||
unsigned __fastcall KeyringDescChange_h(void *bagDesc, void *edx,
|
||||
unsigned a1, unsigned a2,
|
||||
unsigned a3, unsigned a4) {
|
||||
const unsigned result = KeyringDescChange_o(bagDesc, edx, a1, a2, a3, a4);
|
||||
// `OnBackpackSlotChanged` — field-watcher ABI, `__fastcall(offset,
|
||||
// unused, guidLo, guidHi, previous*, user)`, callee cleans 0x10.
|
||||
using BackpackSlotChanged_t = unsigned(__fastcall *)(unsigned offset, unsigned unused,
|
||||
unsigned guidLo, unsigned guidHi,
|
||||
const void *previous, void *user);
|
||||
BackpackSlotChanged_t BackpackSlotChanged_o = nullptr;
|
||||
unsigned __fastcall BackpackSlotChanged_h(unsigned offset, unsigned unused, unsigned guidLo,
|
||||
unsigned guidHi, const void *previous, void *user) {
|
||||
const unsigned result = BackpackSlotChanged_o(offset, unused, guidLo, guidHi, previous, user);
|
||||
g_pending = true;
|
||||
return result;
|
||||
}
|
||||
@@ -151,20 +163,20 @@ void OnWorldTick() {
|
||||
node->cb();
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookSlotDiff{
|
||||
Offsets::FUN_BAG_SLOT_DIFF,
|
||||
reinterpret_cast<void *>(&BagSlotDiff_h),
|
||||
reinterpret_cast<void **>(&BagSlotDiff_o)};
|
||||
static const Game::HookAutoRegister _hookRaiseBagEvents{
|
||||
Offsets::FUN_RAISE_BAG_EVENTS,
|
||||
reinterpret_cast<void *>(&RaiseBagEvents_h),
|
||||
reinterpret_cast<void **>(&RaiseBagEvents_o)};
|
||||
|
||||
static const Game::HookAutoRegister _hookItemToBag{
|
||||
Offsets::FUN_BAG_ITEM_TO_BAG,
|
||||
reinterpret_cast<void *>(&BagItemToBag_h),
|
||||
reinterpret_cast<void **>(&BagItemToBag_o)};
|
||||
static const Game::HookAutoRegister _hookRaiseBagUpdate{
|
||||
Offsets::FUN_RAISE_BAG_UPDATE,
|
||||
reinterpret_cast<void *>(&RaiseBagUpdate_h),
|
||||
reinterpret_cast<void **>(&RaiseBagUpdate_o)};
|
||||
|
||||
static const Game::HookAutoRegister _hookKeyringDesc{
|
||||
Offsets::FUN_BAG_KEYRING_DESC_CHANGE,
|
||||
reinterpret_cast<void *>(&KeyringDescChange_h),
|
||||
reinterpret_cast<void **>(&KeyringDescChange_o)};
|
||||
static const Game::HookAutoRegister _hookBackpackSlot{
|
||||
Offsets::FUN_ON_BACKPACK_SLOT_CHANGED,
|
||||
reinterpret_cast<void *>(&BackpackSlotChanged_h),
|
||||
reinterpret_cast<void **>(&BackpackSlotChanged_o)};
|
||||
|
||||
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
// CreateFromMixins(...) -- Mixin({}, ...); returns the new table.
|
||||
// Non-table extra arguments are skipped (the Lua form guards nil the same way).
|
||||
|
||||
#include "baselib/Mixin.h"
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::Mixin {
|
||||
|
||||
namespace {
|
||||
|
||||
// Shallow-copy every pair from the table at `srcIdx` onto the table at
|
||||
// `dstIdx` (both absolute, positive stack indices). Stack-balanced.
|
||||
void MixinInto(void *L, int dstIdx, int srcIdx) {
|
||||
void Into(void *L, int dstIdx, int srcIdx) {
|
||||
Game::Lua::PushNil(L);
|
||||
while (Game::Lua::Next(L, srcIdx) != 0) {
|
||||
// stack top: ..., key (-2), value (-1). Copy both so the originals
|
||||
@@ -44,6 +44,8 @@ void MixinInto(void *L, int dstIdx, int srcIdx) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_Mixin(void *L) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (top < 1 || Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
|
||||
@@ -52,7 +54,7 @@ int __fastcall Script_Mixin(void *L) {
|
||||
}
|
||||
for (int i = 2; i <= top; ++i)
|
||||
if (Game::Lua::Type(L, i) == Game::Lua::TYPE_TABLE)
|
||||
MixinInto(L, 1, i);
|
||||
Into(L, 1, i);
|
||||
Game::Lua::PushValue(L, 1); // return object
|
||||
return 1;
|
||||
}
|
||||
@@ -63,7 +65,7 @@ int __fastcall Script_CreateFromMixins(void *L) {
|
||||
const int objIdx = top + 1;
|
||||
for (int i = 1; i <= top; ++i)
|
||||
if (Game::Lua::Type(L, i) == Game::Lua::TYPE_TABLE)
|
||||
MixinInto(L, objIdx, i);
|
||||
Into(L, objIdx, i);
|
||||
Game::Lua::SetTop(L, objIdx); // the new table is the sole result
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace BaseLib::Mixin {
|
||||
|
||||
// dst[k] = v for every field of the table at `srcIdx` — Mixin()'s copy.
|
||||
// Both indices must be absolute.
|
||||
void Into(void *L, int dstIdx, int srcIdx);
|
||||
|
||||
} // namespace BaseLib::Mixin
|
||||
@@ -0,0 +1,132 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `securecall(func or name, ...)`, `securecallfunction(func, ...)` and
|
||||
// `secureexecuterange(tbl, func, ...)` — the later clients' protected calls,
|
||||
// less taint (1.12 has none). Retail, probed:
|
||||
// - securecall returns the function's results; a string names a global
|
||||
// function. An error goes to the error handler and the call returns
|
||||
// nothing.
|
||||
// - securecallfunction is securecall without the name lookup: a string is
|
||||
// a value that cannot be called, so it errors and returns nothing.
|
||||
// - secureexecuterange calls func(key, value, ...) for each entry of tbl in
|
||||
// `next` order; an error in one goes to the error handler and the walk
|
||||
// goes on. It returns nothing.
|
||||
// The error handler is the one every engine script run passes
|
||||
// (VAR_FRAMESCRIPT_ERROR_HANDLER_REF), as HookScript's wrapper uses.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace BaseLib::SecureCall {
|
||||
|
||||
namespace {
|
||||
|
||||
// Pushes the engine's script error handler; false (nothing pushed) before
|
||||
// the engine has set one.
|
||||
bool PushErrorHandler(void *L) {
|
||||
const int ref = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
|
||||
if (ref <= 0)
|
||||
return false;
|
||||
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, ref);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calls the function at stack index 1 with the values above it, under the
|
||||
// error handler. Returns the result count, 0 after an error.
|
||||
int CallProtected(void *L) {
|
||||
const int nargs = Game::Lua::GetTop(L) - 1;
|
||||
int errIdx = 0;
|
||||
if (PushErrorHandler(L)) {
|
||||
Game::Lua::Insert(L, 1); // [handler, func, args...]
|
||||
errIdx = 1;
|
||||
}
|
||||
if (Game::Lua::PCall(L, nargs, Game::Lua::MULTRET, errIdx) != 0)
|
||||
return 0;
|
||||
return Game::Lua::GetTop(L) - errIdx;
|
||||
}
|
||||
|
||||
int __fastcall Script_securecall(void *L) {
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_STRING) {
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
Game::Lua::Remove(L, 1);
|
||||
Game::Lua::Insert(L, 1);
|
||||
}
|
||||
return CallProtected(L);
|
||||
}
|
||||
|
||||
int __fastcall Script_securecallfunction(void *L) { return CallProtected(L); }
|
||||
|
||||
int __fastcall Script_secureexecuterange(void *L) {
|
||||
const int n = Game::Lua::GetTop(L); // tbl, func, extra args 3..n
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE)
|
||||
return 0;
|
||||
Game::Lua::PushNil(L);
|
||||
while (Game::Lua::Next(L, 1) != 0) { // [.., key, value]
|
||||
int errIdx = 0;
|
||||
if (PushErrorHandler(L))
|
||||
errIdx = n + 3;
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::PushValue(L, n + 1);
|
||||
Game::Lua::PushValue(L, n + 2);
|
||||
for (int i = 3; i <= n; ++i)
|
||||
Game::Lua::PushValue(L, i);
|
||||
Game::Lua::PCall(L, n, 0, errIdx); // key, value and the n - 2 extras
|
||||
Game::Lua::SetTop(L, n + 1); // the key, for the next step
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kCallArgs[] = {
|
||||
Game::Doc::Req("func", "function", "The function to call, or for securecall the name of a global function."),
|
||||
Game::Doc::Opt("...", "any", nullptr, "The arguments for the function."),
|
||||
};
|
||||
const Game::Doc::Field kCallRets[] = {
|
||||
Game::Doc::Opt("...", "any", nullptr, "The function's results, or nothing after an error."),
|
||||
};
|
||||
const Game::Doc::Field kRangeArgs[] = {
|
||||
Game::Doc::Req("tbl", "table", "The table whose entries are visited."),
|
||||
Game::Doc::Req("func", "function", "Called as func(key, value, ...) for each entry."),
|
||||
Game::Doc::Opt("...", "any", nullptr, "Extra arguments for each call."),
|
||||
};
|
||||
const Game::Doc::Function kSecureCall{
|
||||
"Calls a function and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kCallArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureCallFunction{
|
||||
"Calls a function and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kCallArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureExecuteRange{
|
||||
"Calls a function for each entry of a table. An error goes to the error handler and the other entries still run.",
|
||||
kRangeArgs, {}, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlobalFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlobalFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlueFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlueFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::SecureCall
|
||||
+188
-44
@@ -13,7 +13,8 @@
|
||||
|
||||
// String helpers that 1.12's Lua 5.0 is missing:
|
||||
//
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction (5.1).
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction (5.1),
|
||||
// also the global `strmatch`, as `string.find` is `strfind`.
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1).
|
||||
// - `string.gsub` TABLE replacement — 5.1 form (see the shim below).
|
||||
// - `strsplit(sep, str [, pieces])` — WoW global; split on any char.
|
||||
@@ -21,6 +22,15 @@
|
||||
// - `strtrim(str [, chars])` — WoW global; trim a char set.
|
||||
// - `strreplace(str, find, replace)` — plain-text replace (see below).
|
||||
// - `string.reverse(s)` / `strrev(s)` — 5.1 string function, missing in 5.0.
|
||||
// - `strlenutf8(s)` — WoW global; characters, not bytes.
|
||||
// - `strcmputf8i(a, b)` — WoW global; case-insensitive UTF-8 order.
|
||||
// - `strsplittable(sep, str [, pieces])`— WoW global; `strsplit` into a table.
|
||||
// - `strconcat(...)` / `string.concat` — WoW global; `..` over every argument.
|
||||
//
|
||||
// The two UTF-8 globals are 4.3.4's `Script_strlenutf8` and the newer
|
||||
// clients' `strcmputf8i`, each a thin push over a Storm helper 1.12 does not
|
||||
// have (`SStrLenUTF8` / `SStrCmpUTF8I`) — `Text::Utf8` mirrors those.
|
||||
// `strconcat` is 4.3.4's: `lua_concat` over the whole stack.
|
||||
//
|
||||
// The two pattern `string.*` ones reuse machinery 5.0 already ships:
|
||||
// - `match` is `find` returning captures / the whole match instead of the
|
||||
@@ -37,6 +47,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "text/Utf8.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
@@ -91,33 +102,17 @@ int __fastcall Script_string_match(void *L) {
|
||||
const auto Script_string_gmatch =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GFIND);
|
||||
|
||||
// `strsplit(sep, str [, pieces])` — the WoW global (also aliased as
|
||||
// `string.split`), split `str` on ANY character in `sep` and return the
|
||||
// pieces as multiple values. `pieces > 0` caps the result count, with the
|
||||
// unsplit remainder as the final piece; `0` / omitted = unlimited. Ported
|
||||
// from 3.3.5's `strsplit` (FUN_00816a60) with one deliberate change: we do
|
||||
// NOT `lua_settop(L, 0)` up front. Popping the args would leave the source
|
||||
// string unreferenced, so a GC step triggered by a `pushlstring` could free
|
||||
// the very buffer we're still scanning. Keeping the args on the stack keeps
|
||||
// the source GC-rooted; Lua returns the top N values (our pieces) regardless
|
||||
// of the args sitting below them. Each push is guarded by `lua_checkstack`
|
||||
// (this pushes an unbounded number of results), erroring like 3.3.5 on
|
||||
// genuine stack exhaustion.
|
||||
int __fastcall Script_strsplit(void *L) {
|
||||
const char *sep = Game::Lua::ToString(L, 1);
|
||||
const char *str = Game::Lua::ToString(L, 2);
|
||||
if (sep == nullptr || str == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strsplit(\"separators\", str [, pieces])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int pieces =
|
||||
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
|
||||
|
||||
// The scan behind `strsplit` and `strsplittable`: splits `str` on ANY
|
||||
// character in `sep` and hands each piece to `emit(start, length)` in order,
|
||||
// the unsplit remainder last. `pieces > 0` caps the count, with the
|
||||
// remainder (delimiters and all) as the final piece; `0` is unlimited.
|
||||
// Only scans for separators while unlimited or the cap hasn't been reached;
|
||||
// `pieces == 1` (or <= 0 but non-zero) yields the whole string as a single
|
||||
// piece — matches 3.3.5's `strsplit` (FUN_00816a60).
|
||||
template <typename Emit>
|
||||
void SplitPieces(const char *sep, const char *str, int pieces, Emit emit) {
|
||||
const char *segStart = str;
|
||||
int count = 0;
|
||||
// Only scan for separators while unlimited (pieces == 0) or the cap
|
||||
// hasn't been reached (pieces > 1). pieces == 1 (or <= 0 but non-zero)
|
||||
// yields the whole string as a single piece — matches 3.3.5.
|
||||
if (pieces == 0 || pieces > 1) {
|
||||
for (const char *p = str; *p != '\0'; ++p) {
|
||||
bool isSep = false;
|
||||
@@ -130,23 +125,108 @@ int __fastcall Script_strsplit(void *L) {
|
||||
if (!isSep)
|
||||
continue;
|
||||
++count;
|
||||
if (Game::Lua::CheckStack(L, count) == 0) {
|
||||
Game::Lua::Error(L, "strsplit(): Stack overflow");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushLString(L, segStart,
|
||||
static_cast<unsigned int>(p - segStart));
|
||||
emit(segStart, static_cast<unsigned int>(p - segStart));
|
||||
segStart = p + 1;
|
||||
if (count == pieces - 1)
|
||||
break; // cap hit; remainder becomes the final piece
|
||||
}
|
||||
}
|
||||
if (Game::Lua::CheckStack(L, count + 1) == 0) {
|
||||
Game::Lua::Error(L, "strsplit(): Stack overflow");
|
||||
emit(segStart, static_cast<unsigned int>(std::strlen(segStart)));
|
||||
}
|
||||
|
||||
// `strsplit(sep, str [, pieces])` — the WoW global (also aliased as
|
||||
// `string.split`): the pieces as multiple values. Ported from 3.3.5 with one
|
||||
// deliberate change: we do NOT `lua_settop(L, 0)` up front. Popping the args
|
||||
// would leave the source string unreferenced, so a GC step triggered by a
|
||||
// `pushlstring` could free the very buffer we're still scanning. Keeping the
|
||||
// args on the stack keeps the source GC-rooted; Lua returns the top N values
|
||||
// (our pieces) regardless of the args sitting below them. Each push is
|
||||
// guarded by `lua_checkstack` (this pushes an unbounded number of results),
|
||||
// erroring like 3.3.5 on genuine stack exhaustion.
|
||||
int __fastcall Script_strsplit(void *L) {
|
||||
const char *sep = Game::Lua::ToString(L, 1);
|
||||
const char *str = Game::Lua::ToString(L, 2);
|
||||
if (sep == nullptr || str == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strsplit(\"separators\", str [, pieces])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushString(L, segStart); // final piece (remainder to end)
|
||||
return count + 1;
|
||||
const int pieces =
|
||||
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
|
||||
|
||||
int count = 0;
|
||||
SplitPieces(sep, str, pieces, [L, &count](const char *start, unsigned int len) {
|
||||
++count;
|
||||
if (Game::Lua::CheckStack(L, count) == 0) {
|
||||
Game::Lua::Error(L, "strsplit(): Stack overflow");
|
||||
return; // unreachable
|
||||
}
|
||||
Game::Lua::PushLString(L, start, len);
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// `strsplittable(sep, str [, pieces])` — the same split as one array
|
||||
// table. Every piece goes to the next integer key of a fresh table, so the
|
||||
// Lua stack never grows and no piece count is too many — the point of the
|
||||
// table form. The args stay on the stack (GC-rooting the source, as above);
|
||||
// the table sits above them at index 4.
|
||||
int __fastcall Script_strsplittable(void *L) {
|
||||
const char *sep = Game::Lua::ToString(L, 1);
|
||||
const char *str = Game::Lua::ToString(L, 2);
|
||||
if (sep == nullptr || str == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strsplittable(\"separators\", str [, pieces])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int pieces =
|
||||
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
|
||||
|
||||
Game::Lua::SetTop(L, 3);
|
||||
Game::Lua::NewTable(L); // 4
|
||||
int count = 0;
|
||||
SplitPieces(sep, str, pieces, [L, &count](const char *start, unsigned int len) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++count));
|
||||
Game::Lua::PushLString(L, start, len);
|
||||
Game::Lua::RawSet(L, 4);
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strlenutf8(s)` — 4.3.4's `Script_strlenutf8`: `SStrLenUTF8` on the
|
||||
// string, the count of characters (code points) rather than bytes. Same
|
||||
// usage text and `lua_isstring` gate as the original.
|
||||
int __fastcall Script_strlenutf8(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: strlenutf8(string)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(Text::Utf8::Length(Game::Lua::ToString(L, 1))));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strcmputf8i(string1, string2)` — the newer clients' global (its usage
|
||||
// text is in their binaries) over `SStrCmpUTF8I`: a case-insensitive sort
|
||||
// order for UTF-8 text. The number is the first folded code-point
|
||||
// difference, as Storm returns it; FrameXML only ever tests the sign (`< 0`
|
||||
// in sort comparators, `== 0` for a name match).
|
||||
int __fastcall Script_strcmputf8i(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: strcmputf8i(string1, string2)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int result =
|
||||
Text::Utf8::CompareI(Game::Lua::ToString(L, 1), Game::Lua::ToString(L, 2));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(result));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strconcat(...)` / `string.concat(...)` — 4.3.4's `Script_strconcat`:
|
||||
// `lua_concat` over everything on the stack, i.e. the `..` operator applied
|
||||
// to every argument (numbers convert, anything else raises the concatenate
|
||||
// error), `""` for no arguments, and a single argument handed back as it is.
|
||||
int __fastcall Script_strconcat(void *L) {
|
||||
Game::Lua::Concat(L, Game::Lua::GetTop(L));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strjoin(delimiter, ...)` — the inverse of `strsplit`: concatenate the
|
||||
@@ -383,21 +463,85 @@ int __fastcall Script_string_gsub(void *L) {
|
||||
return engineGsub(L);
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kStrlenutf8Args[] = {
|
||||
Game::Doc::Req("string", "string", "UTF-8 text."),
|
||||
};
|
||||
const Game::Doc::Field kStrlenutf8Rets[] = {
|
||||
Game::Doc::Req("length", "number", "The number of characters; strlen counts bytes."),
|
||||
};
|
||||
const Game::Doc::Function kStrlenutf8{
|
||||
"The number of characters in a UTF-8 string, not the number of bytes.",
|
||||
kStrlenutf8Args, kStrlenutf8Rets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kStrcmputf8iArgs[] = {
|
||||
Game::Doc::Req("string1", "string"),
|
||||
Game::Doc::Req("string2", "string"),
|
||||
};
|
||||
const Game::Doc::Field kStrcmputf8iRets[] = {
|
||||
Game::Doc::Req("result", "number",
|
||||
"Negative when string1 sorts first, 0 when the two match ignoring "
|
||||
"case, positive when string2 sorts first."),
|
||||
};
|
||||
const Game::Doc::Function kStrcmputf8i{
|
||||
"Compares two UTF-8 strings ignoring case, for sorting and matching.",
|
||||
kStrcmputf8iArgs, kStrcmputf8iRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kStrsplittableArgs[] = {
|
||||
Game::Doc::Req("delimiter", "string", "Each character is a delimiter."),
|
||||
Game::Doc::Req("str", "string"),
|
||||
Game::Doc::Opt("pieces", "number", "0",
|
||||
"Maximum number of pieces; the last one holds the rest of the string. "
|
||||
"0 is unlimited."),
|
||||
};
|
||||
const Game::Doc::Field kStrsplittableRets[] = {
|
||||
Game::Doc::Req("chunks", "table", "Array of the pieces, in order."),
|
||||
};
|
||||
const Game::Doc::Function kStrsplittable{
|
||||
"Splits a string on delimiter characters and returns the pieces as an array.",
|
||||
kStrsplittableArgs, kStrsplittableRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kStrconcatArgs[] = {
|
||||
Game::Doc::Vararg("string", "Strings or numbers."),
|
||||
};
|
||||
const Game::Doc::Field kStrconcatRets[] = {
|
||||
Game::Doc::Req("result", "string"),
|
||||
};
|
||||
const Game::Doc::Function kStrconcat{
|
||||
"Joins every argument into one string with nothing between them.",
|
||||
kStrconcatArgs, kStrconcatRets, "LuaGlobals"};
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
Game::Lua::RegisterTableFunction("string", "gsub", &Script_string_gsub);
|
||||
Game::Lua::RegisterTableFunction("string", "reverse", &Script_string_reverse);
|
||||
Game::Lua::RegisterTableFunction("string", "split", &Script_strsplit);
|
||||
// The bare `gsub` global (1.12 exposes the string functions as globals
|
||||
// too) must match `string.gsub`, so addons calling either form get the
|
||||
// table-replacement upgrade.
|
||||
Game::Lua::RegisterGlobalFunction("gsub", &Script_string_gsub);
|
||||
Game::Lua::RegisterGlobalFunction("strsplit", &Script_strsplit);
|
||||
Game::Lua::RegisterGlobalFunction("strjoin", &Script_strjoin);
|
||||
Game::Lua::RegisterGlobalFunction("strtrim", &Script_strtrim);
|
||||
Game::Lua::RegisterTableFunction("string", "trim", &Script_strtrim);
|
||||
Game::Lua::RegisterTableFunction("string", "join", &Script_strjoin);
|
||||
Game::Lua::RegisterTableFunction("string", "concat", &Script_strconcat, &kStrconcat);
|
||||
// 4.3.4's compat.lua binds each short global and its string-table twin
|
||||
// to ONE function by assignment (`gsub = str.gsub`, `strmatch =
|
||||
// str.match`, `strrev = str.reverse`, `str.trim = strtrim`, `str.split =
|
||||
// strsplit`, `str.join = strjoin`), so `strtrim == string.trim`. A second
|
||||
// Register* call would mint a second closure over the same C function, so
|
||||
// the globals alias the table entries by value instead. The string-table
|
||||
// half is what lets `(" x "):trim()` work and libraries capture it at
|
||||
// file scope (AceConfigCmd-3.0: `strtrim = string.trim`); the bare `gsub`
|
||||
// gets the table-replacement upgrade by being the same function.
|
||||
Game::Lua::RegisterGlobalAlias("gsub", "string", "gsub");
|
||||
Game::Lua::RegisterGlobalAlias("strmatch", "string", "match");
|
||||
Game::Lua::RegisterGlobalAlias("strsplit", "string", "split");
|
||||
Game::Lua::RegisterGlobalAlias("strjoin", "string", "join");
|
||||
Game::Lua::RegisterGlobalAlias("strtrim", "string", "trim");
|
||||
Game::Lua::RegisterGlobalAlias("strrev", "string", "reverse");
|
||||
Game::Lua::RegisterGlobalAlias("strconcat", "string", "concat", &kStrconcat);
|
||||
Game::Lua::RegisterGlobalFunction("strreplace", &Script_strreplace);
|
||||
Game::Lua::RegisterGlobalFunction("strrev", &Script_string_reverse);
|
||||
Game::Lua::RegisterGlobalFunction("strlenutf8", &Script_strlenutf8, &kStrlenutf8);
|
||||
Game::Lua::RegisterGlobalFunction("strcmputf8i", &Script_strcmputf8i, &kStrcmputf8i);
|
||||
Game::Lua::RegisterGlobalFunction("strsplittable", &Script_strsplittable,
|
||||
&kStrsplittable);
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing these; RegisterTableFunction
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `success, ... = xpcall(f, msgh [, arg1, ...])` — call `f` under `msgh`,
|
||||
// forwarding the trailing arguments to it.
|
||||
//
|
||||
// 1.12's Lua takes only `xpcall(f, msgh)`. Its implementation is explicit
|
||||
// about it: `lua_settop(L, 2)` throws any further arguments away, then
|
||||
// `lua_pcall(L, 0, ...)` calls `f` with none. So the argument-forwarding form
|
||||
// doesn't merely fail — it silently calls `f` with nothing, and a function
|
||||
// that reads its parameters sees nil. (Note this is NOT a 5.0-vs-5.1
|
||||
// difference: both take `(f, msgh)`. Forwarding arrived later.)
|
||||
//
|
||||
// Callers work around it with a capturing closure —
|
||||
// `xpcall(function() return f(a, b) end, msgh)` — which is what
|
||||
// `ExecuteFrameScript` in the embedded addon does. That stays correct; this
|
||||
// just removes the need for it.
|
||||
//
|
||||
// Why a C function and not a Lua wrapper over the original: a Lua wrapper
|
||||
// would allocate a closure AND an `arg` table on every call (see the vararg
|
||||
// note in FunctionUtil.lua), putting an allocation on any error-guarded path,
|
||||
// including per-frame ones. `HookSecureFunc`'s `kUnhookableNames` also lists
|
||||
// `xpcall` precisely because shadowing a core primitive with a Lua closure
|
||||
// diverges from what C-side callers get. Replacing it with a real C function
|
||||
// has neither problem, and matches how `select` and `unpack` are backported
|
||||
// in this directory.
|
||||
//
|
||||
// A two-argument call is bit-for-bit the old behavior (zero forwarded args),
|
||||
// so this is purely additive for existing code.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::XPCall {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_xpcall(void *L) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (top < 2) {
|
||||
Game::Lua::Error(L, "bad argument #2 to 'xpcall' (value expected)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
|
||||
// `lua_pcall` wants the callee and its arguments contiguous on top, and
|
||||
// the handler at a fixed index. Incoming order is
|
||||
// `f(1), msgh(2), a1(3)..aN(top)`, so the handler sits between the two —
|
||||
// shuffle it below `f`:
|
||||
//
|
||||
// PushValue(2) → f, msgh, a1..aN, msgh
|
||||
// Insert(1) → msgh, f, msgh, a1..aN (top moved down to index 1)
|
||||
// Remove(3) → msgh, f, a1..aN
|
||||
//
|
||||
// 5.2 does the same reshuffle with a single `lua_rotate`, which this Lua
|
||||
// has no equivalent of.
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::Insert(L, 1);
|
||||
Game::Lua::Remove(L, 3);
|
||||
|
||||
const int status =
|
||||
Game::Lua::PCall(L, /*nargs*/ top - 2, Game::Lua::MULTRET, /*errfunc*/ 1);
|
||||
|
||||
// The stack now holds the handler plus either f's results or the handler's
|
||||
// return. Both cases want the status boolean in front of them, and the
|
||||
// handler itself dropped from the return set — which returning
|
||||
// `GetTop - 1` does, since results are taken from the top.
|
||||
Game::Lua::PushBool(L, status == 0);
|
||||
Game::Lua::Insert(L, 2);
|
||||
return Game::Lua::GetTop(L) - 1;
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("xpcall", &Script_xpcall);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("xpcall", &Script_xpcall);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::XPCall
|
||||
+320
-491
@@ -12,391 +12,271 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Backports the direct-action and override binding APIs introduced in 2.0.
|
||||
// Normal bindings are stored by vanilla's SetBinding implementation using the
|
||||
// later-client SPELL/ITEM/MACRO/CLICK command strings. The resolved-command
|
||||
// hook executes those four command families; every native binding command is
|
||||
// passed through unchanged.
|
||||
// The binding API 4.3.4 has and 1.12 lacks (decomp Ui/UIBindings.cpp,
|
||||
// UIBindings_Scripts.cpp):
|
||||
//
|
||||
// Override bindings live in a separate owner-scoped layer in front of the
|
||||
// native key lookup. They are deliberately never written to the saved binding
|
||||
// table, so clearing an owner immediately reveals the next override or the
|
||||
// unchanged native binding underneath it.
|
||||
// SetBindingSpell / Item / Macro / Click — "SPELL x" etc. into the
|
||||
// current set through the engine's own SetBinding.
|
||||
// SetOverrideBinding[Spell / Item / Macro / Click], ClearOverrideBindings
|
||||
// — owner-scoped bindings in front of the saved ones, never saved.
|
||||
// GetBindingAction(key [, checkOverride]) — sees overrides when asked.
|
||||
// GetBindingByKey(key) — what a press of the key runs.
|
||||
//
|
||||
// Overrides — 4.3.4's OVERRIDEKEYBINDING: per key (case-insensitive), two
|
||||
// lists (normal, priority) of {owner, command}; setting an owner's binding
|
||||
// unlinks its old one from both and links the new one at the head of its
|
||||
// priority's list, so the newest priority binding wins, then the newest
|
||||
// normal one. 4.3.4 consults them inside its key lookup, before the saved
|
||||
// binding, for every key it tries.
|
||||
//
|
||||
// Reduced keys — 4.3.4's GetReducedKeyBinding. 1.12's key dispatch
|
||||
// (FUN_BINDING_KEY_DISPATCH) tries only two keys: the pressed one, then the
|
||||
// pressed one minus its first modifier. 4.3.4 tries every subset of the
|
||||
// modifiers (ALT-SHIFT-Q: ALT-SHIFT-Q, SHIFT-Q, ALT-Q, Q). The co-hook walks
|
||||
// 4.3.4's list, tries an override, then the saved binding
|
||||
// (FUN_BINDINGS_GET_COMMAND_ACTION), for each key in turn — skipping one whose
|
||||
// command is unknown, as 4.3.4 does — and lets the engine run a saved binding
|
||||
// it finds.
|
||||
//
|
||||
// Around a direct action, the key-state override (Input::Modifier) hides the
|
||||
// matched key's own modifiers, as 4.3.4's ExecKey does; a Lua command sees
|
||||
// the live state, as in 1.12 (see KeyDispatch_h). Direct actions
|
||||
// (Bindings::Command) run only for a key: the command co-hook acts while the
|
||||
// manager's executing flag — set by the key dispatch, not by RunBinding — is
|
||||
// up, which is 4.3.4's ExecKey / ExecCommand split.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "macro/Execute.h"
|
||||
#include "bindings/Command.h"
|
||||
#include "input/Modifier.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <list>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Bindings::Api {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char kSpellPrefix[] = "SPELL ";
|
||||
constexpr const char kItemPrefix[] = "ITEM ";
|
||||
constexpr const char kMacroPrefix[] = "MACRO ";
|
||||
constexpr const char kClickPrefix[] = "CLICK ";
|
||||
constexpr const char kDefaultMouseButton[] = "LeftButton";
|
||||
using KeyDispatch_t = int(__fastcall *)(void *mgr, void *edx, const char *key, int isDown);
|
||||
using CommandExecute_t = int(__fastcall *)(void *mgr, void *edx, const char *cmd, int isDown);
|
||||
using GetCommandAction_t = const char *(__thiscall *)(void *mgr, const char *key);
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
|
||||
using ScriptSetBinding_t = int(__fastcall *)(void *L);
|
||||
using KeyDispatch_t = int(__thiscall *)(void *mgr, const char *key, int isDown);
|
||||
using CommandExecute_t = int(__thiscall *)(void *mgr, const char *cmd, int isDown);
|
||||
KeyDispatch_t g_keyDispatchOriginal = nullptr;
|
||||
CommandExecute_t g_commandExecuteOriginal = nullptr;
|
||||
|
||||
KeyDispatch_t KeyDispatch_o = nullptr;
|
||||
CommandExecute_t CommandExecute_o = nullptr;
|
||||
// --- override table (OVERRIDEKEYBINDING) ------------------------------------
|
||||
|
||||
enum class CommandType {
|
||||
None,
|
||||
Spell,
|
||||
Item,
|
||||
Macro,
|
||||
Click,
|
||||
};
|
||||
|
||||
struct ParsedCommand {
|
||||
CommandType type = CommandType::None;
|
||||
const char *argument = nullptr;
|
||||
};
|
||||
|
||||
struct OverrideEntry {
|
||||
const void *owner = nullptr;
|
||||
std::string key;
|
||||
struct Entry {
|
||||
const void *owner;
|
||||
std::string command;
|
||||
bool priority = false;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
std::vector<OverrideEntry> g_overrides;
|
||||
std::unordered_map<std::string, std::string> g_activeOverrides;
|
||||
uint64_t g_nextSequence = 0;
|
||||
struct OverrideKey {
|
||||
std::string key;
|
||||
std::list<Entry> lists[2]; // [0] normal, [1] priority; head = newest
|
||||
};
|
||||
|
||||
// Normalizes a binding key to the engine's own canonical form so a stored
|
||||
// override matches the string FUN_BINDING_KEY_DISPATCH builds at keypress. The
|
||||
// engine's key-string builder (FUN_004b6630) always emits modifiers in the
|
||||
// fixed order ALT-CTRL-SHIFT- ahead of the base key, regardless of the order
|
||||
// the caller passed them; vanilla SetBinding never reorders, so the override
|
||||
// layer does it here. Both the store and the lookup run through this, so they
|
||||
// stay consistent while also matching the engine's keypress form.
|
||||
std::string CanonicalKey(const char *key) {
|
||||
std::string result = key ? key : "";
|
||||
for (char &character : result) {
|
||||
if (character >= 'a' && character <= 'z')
|
||||
character = static_cast<char>(character - 'a' + 'A');
|
||||
}
|
||||
std::vector<OverrideKey> g_overrides;
|
||||
|
||||
// Strip the three modifier prefixes in whatever order/repetition they
|
||||
// appear, then re-emit them in the engine's fixed order. Prefix-stripping
|
||||
// (rather than splitting on '-') keeps the minus base key correct, e.g.
|
||||
// "SHIFT--" -> modifier SHIFT + base "-".
|
||||
bool alt = false, ctrl = false, shift = false;
|
||||
size_t base = 0;
|
||||
for (bool matched = true; matched;) {
|
||||
matched = false;
|
||||
if (result.compare(base, 4, "ALT-") == 0) {
|
||||
alt = true;
|
||||
base += 4;
|
||||
matched = true;
|
||||
} else if (result.compare(base, 5, "CTRL-") == 0) {
|
||||
ctrl = true;
|
||||
base += 5;
|
||||
matched = true;
|
||||
} else if (result.compare(base, 6, "SHIFT-") == 0) {
|
||||
shift = true;
|
||||
base += 6;
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alt && !ctrl && !shift)
|
||||
return result;
|
||||
|
||||
std::string canonical;
|
||||
if (alt)
|
||||
canonical += "ALT-";
|
||||
if (ctrl)
|
||||
canonical += "CTRL-";
|
||||
if (shift)
|
||||
canonical += "SHIFT-";
|
||||
canonical += result.substr(base);
|
||||
|
||||
return canonical;
|
||||
OverrideKey *FindKey(const char *key) {
|
||||
for (OverrideKey &entry : g_overrides)
|
||||
if (_stricmp(entry.key.c_str(), key) == 0)
|
||||
return &entry;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string MakeCommand(const char *prefix, const char *arg) {
|
||||
std::string cmd = prefix;
|
||||
cmd += arg;
|
||||
return cmd;
|
||||
// OVERRIDEKEYBINDING::SetOverrideBinding.
|
||||
void SetOverride(OverrideKey &binding, const void *owner, bool priority, const char *command) {
|
||||
for (auto &list : binding.lists)
|
||||
list.remove_if([owner](const Entry &entry) { return entry.owner == owner; });
|
||||
if (command != nullptr && *command != '\0')
|
||||
binding.lists[priority ? 1 : 0].push_front(Entry{owner, command});
|
||||
}
|
||||
|
||||
std::string MakeClickCommand(const char *btnName, const char *mouseBtn) {
|
||||
std::string cmd = MakeCommand(kClickPrefix, btnName);
|
||||
if (mouseBtn != nullptr) {
|
||||
cmd += ':';
|
||||
cmd += mouseBtn;
|
||||
// OVERRIDEKEYBINDING::GetActiveBinding.
|
||||
const char *ActiveOverride(const char *key) {
|
||||
const OverrideKey *binding = FindKey(key);
|
||||
if (binding == nullptr)
|
||||
return nullptr;
|
||||
for (int i = 1; i >= 0; --i)
|
||||
if (!binding->lists[i].empty())
|
||||
return binding->lists[i].front().command.c_str();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// CGUIBindings::SetOverrideBinding.
|
||||
void SetOverrideBinding(const void *owner, bool priority, const char *key, const char *command) {
|
||||
OverrideKey *binding = FindKey(key);
|
||||
if (binding == nullptr) {
|
||||
g_overrides.push_back(OverrideKey{key, {}});
|
||||
binding = &g_overrides.back();
|
||||
}
|
||||
|
||||
return cmd;
|
||||
SetOverride(*binding, owner, priority, command);
|
||||
}
|
||||
|
||||
ParsedCommand ParseCommand(const char *cmd) {
|
||||
if (cmd == nullptr)
|
||||
// --- key dispatch -------------------------------------------------------------
|
||||
|
||||
void *Manager() { return Game::Read<void *>(Offsets::VAR_BINDINGS_MANAGER); }
|
||||
|
||||
const char *SavedCommand(void *mgr, const char *key) {
|
||||
return reinterpret_cast<GetCommandAction_t>(Offsets::FUN_BINDINGS_GET_COMMAND_ACTION)(mgr,
|
||||
key);
|
||||
}
|
||||
|
||||
// Whether the command table has `command` — the walk the engine inlines in
|
||||
// FUN_BINDING_COMMAND_EXECUTE (see OFF_BINDINGS_COMMAND_TABLE).
|
||||
bool IsKnownCommand(void *mgr, const char *command) {
|
||||
using HashHT_t = uint32_t(__stdcall *)(const char *);
|
||||
const uintptr_t table = reinterpret_cast<uintptr_t>(mgr) + Offsets::OFF_BINDINGS_COMMAND_TABLE;
|
||||
const uint32_t mask = Game::Read<uint32_t>(table + Offsets::OFF_HASHTABLE_MASK);
|
||||
if (*command == '\0' || mask == 0xFFFFFFFF)
|
||||
return false;
|
||||
const uint32_t hash = reinterpret_cast<HashHT_t>(Offsets::FUN_SSTR_HASH_HT)(command);
|
||||
const uintptr_t bucket =
|
||||
Game::Read<uintptr_t>(table + Offsets::OFF_HASHTABLE_SLOTS) + (hash & mask) * 12;
|
||||
const uint32_t link = Game::Read<uint32_t>(bucket);
|
||||
for (uintptr_t node = Game::Read<uintptr_t>(bucket + 8); node != 0 && (node & 1) == 0;
|
||||
node = Game::Read<uintptr_t>(node + link + 4)) {
|
||||
if (Game::Read<uint32_t>(node) == hash &&
|
||||
Game::StrCmpI(Game::Read<const char *>(node + Offsets::OFF_HASHTABLE_NODE_NAME),
|
||||
command) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// CGUIBindings::GetKeyBinding: the key's active override, else its saved
|
||||
// binding — and none when that command is neither a direct action nor a known
|
||||
// command (a saved one is never checked when set). 4.3.4's retry under the
|
||||
// generic modifier names (LSHIFT- → SHIFT-) is left out: 1.12 builds chords
|
||||
// from ALT-, CTRL- and SHIFT- only, so a pressed key is already generic.
|
||||
struct KeyBinding {
|
||||
const char *command = nullptr;
|
||||
bool isOverride = false;
|
||||
};
|
||||
|
||||
KeyBinding FindKeyBinding(void *mgr, const char *key) {
|
||||
KeyBinding binding{ActiveOverride(key), true};
|
||||
if (binding.command == nullptr)
|
||||
binding = {SavedCommand(mgr, key), false};
|
||||
if (binding.command == nullptr ||
|
||||
!(Command::IsDirectAction(binding.command) || IsKnownCommand(mgr, binding.command)))
|
||||
return {};
|
||||
return binding;
|
||||
}
|
||||
|
||||
struct PrefixEntry {
|
||||
const char *prefix;
|
||||
size_t length;
|
||||
CommandType type;
|
||||
};
|
||||
|
||||
static constexpr PrefixEntry prefixes[] = {
|
||||
{kSpellPrefix, sizeof(kSpellPrefix) - 1, CommandType::Spell},
|
||||
{kItemPrefix, sizeof(kItemPrefix) - 1, CommandType::Item},
|
||||
{kMacroPrefix, sizeof(kMacroPrefix) - 1, CommandType::Macro},
|
||||
{kClickPrefix, sizeof(kClickPrefix) - 1, CommandType::Click},
|
||||
};
|
||||
|
||||
for (const PrefixEntry &entry : prefixes) {
|
||||
if (std::strncmp(cmd, entry.prefix, entry.length) == 0)
|
||||
return {entry.type, cmd + entry.length};
|
||||
// CGUIBindings::GetReducedKeyBinding: up to three leading modifiers split off
|
||||
// at '-', then the key with each subset of them in 4.3.4's fixed order (bit j
|
||||
// keeps modifier j) — ALT-SHIFT-Q tries ALT-SHIFT-Q, SHIFT-Q, ALT-Q, Q. The
|
||||
// first reduced key with a binding wins and is left in `reduced`.
|
||||
KeyBinding FindReducedKeyBinding(void *mgr, const char *key, std::string &reduced) {
|
||||
std::string modifiers[3];
|
||||
size_t count = 0;
|
||||
const char *rest = key;
|
||||
while (count < 3) {
|
||||
const char *separator = std::strchr(rest, '-');
|
||||
if (separator == nullptr || separator == rest)
|
||||
break;
|
||||
modifiers[count++].assign(rest, separator - rest + 1);
|
||||
rest = separator + 1;
|
||||
}
|
||||
|
||||
static constexpr uint8_t kCombinations[4][9] = {
|
||||
{1, 0},
|
||||
{2, 1, 0},
|
||||
{4, 3, 2, 1, 0},
|
||||
{8, 7, 6, 5, 3, 4, 2, 1, 0},
|
||||
};
|
||||
for (uint8_t i = 0; i < kCombinations[count][0]; ++i) {
|
||||
reduced.clear();
|
||||
for (size_t j = 0; j < count; ++j)
|
||||
if ((1u << j) & kCombinations[count][i + 1])
|
||||
reduced += modifiers[j];
|
||||
reduced += rest;
|
||||
const KeyBinding binding = FindKeyBinding(mgr, reduced.c_str());
|
||||
if (binding.command != nullptr)
|
||||
return binding;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool CallGlobalString(void *L, const char *func, const char *arg) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (!Game::Lua::PushGlobalFunction(L, func)) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, arg);
|
||||
const bool ok = Game::Lua::PCall(L, 1, 0, 0) == 0;
|
||||
Game::Lua::SetTop(L, top);
|
||||
|
||||
return ok;
|
||||
int ExecuteCommand(void *mgr, const char *command, int isDown) {
|
||||
if (Command::IsDirectAction(command))
|
||||
return Command::RunKey(command, isDown);
|
||||
return g_commandExecuteOriginal(mgr, nullptr, command, isDown);
|
||||
}
|
||||
|
||||
bool CallTableString(void *L, const char *table, const char *func, const char *arg) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushString(L, table);
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int tableIndex = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushString(L, func);
|
||||
Game::Lua::GetTable(L, tableIndex);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, arg);
|
||||
const bool ok = Game::Lua::PCall(L, 1, 0, 0) == 0;
|
||||
Game::Lua::SetTop(L, top);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
void InvokeButtonClick(void *L, const char *btnName, size_t btnNameLength, const char *mouseBtn) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushLString(L, btnName, static_cast<unsigned int>(btnNameLength));
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return;
|
||||
}
|
||||
|
||||
const int frameIndex = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushString(L, "Click");
|
||||
Game::Lua::GetTable(L, frameIndex);
|
||||
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::PushValue(L, frameIndex);
|
||||
Game::Lua::PushString(L, mouseBtn);
|
||||
Game::Lua::PCall(L, 2, 0, 0);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
void ExecuteClick(void *L, const char *arg) {
|
||||
const char *separator = std::strrchr(arg, ':');
|
||||
if (separator == arg || (separator != nullptr && separator[1] == '\0'))
|
||||
return;
|
||||
|
||||
const char *mouseBtn = kDefaultMouseButton;
|
||||
size_t btnNameLength = std::strlen(arg);
|
||||
if (separator != nullptr) {
|
||||
btnNameLength = static_cast<size_t>(separator - arg);
|
||||
mouseBtn = separator + 1;
|
||||
}
|
||||
|
||||
if (btnNameLength == 0)
|
||||
return;
|
||||
|
||||
InvokeButtonClick(L, arg, btnNameLength, mouseBtn);
|
||||
}
|
||||
|
||||
// Returns true for every recognized direct-action command, including malformed
|
||||
// or stale commands. Vanilla cannot execute these namespaces, so a bad command
|
||||
// is consumed as a no-op instead of falling through to an unrelated handler.
|
||||
bool ExecuteSpecialCommand(const char *cmd, int isDown) {
|
||||
const ParsedCommand parsed = ParseCommand(cmd);
|
||||
if (parsed.type == CommandType::None)
|
||||
return false;
|
||||
|
||||
if (!isDown || parsed.argument == nullptr || *parsed.argument == '\0')
|
||||
return true;
|
||||
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return true;
|
||||
|
||||
switch (parsed.type) {
|
||||
case CommandType::Spell:
|
||||
CallGlobalString(L, "CastSpellByName", parsed.argument);
|
||||
break;
|
||||
case CommandType::Item:
|
||||
CallTableString(L, "C_Item", "UseItemByName", parsed.argument);
|
||||
break;
|
||||
case CommandType::Macro:
|
||||
Macro::Execute::Saved(L, parsed.argument);
|
||||
break;
|
||||
case CommandType::Click:
|
||||
ExecuteClick(L, parsed.argument);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const OverrideEntry *FindEffectiveOverride(const std::string &key) {
|
||||
const OverrideEntry *best = nullptr;
|
||||
for (const OverrideEntry &entry : g_overrides) {
|
||||
if (entry.key != key)
|
||||
continue;
|
||||
|
||||
if (best == nullptr || entry.priority > best->priority ||
|
||||
(entry.priority == best->priority && entry.sequence > best->sequence))
|
||||
best = &entry;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
void SetOverride(const void *owner, bool priority, const std::string &key, const std::string &cmd) {
|
||||
auto existing =
|
||||
std::find_if(g_overrides.begin(), g_overrides.end(),
|
||||
[&](const OverrideEntry &entry) {
|
||||
return entry.owner == owner && entry.key == key;
|
||||
});
|
||||
|
||||
if (existing == g_overrides.end()) {
|
||||
g_overrides.push_back(
|
||||
{owner, key, cmd, priority, ++g_nextSequence});
|
||||
return;
|
||||
}
|
||||
|
||||
existing->command = cmd;
|
||||
existing->priority = priority;
|
||||
existing->sequence = ++g_nextSequence;
|
||||
}
|
||||
|
||||
void RemoveOverride(const void *owner, const std::string &key) {
|
||||
g_overrides.erase(std::remove_if(g_overrides.begin(), g_overrides.end(),
|
||||
[&](const OverrideEntry &entry) {
|
||||
return entry.owner == owner &&
|
||||
entry.key == key;
|
||||
}),
|
||||
g_overrides.end());
|
||||
}
|
||||
|
||||
int ExecuteCommand(void *mgr, const char *cmd, int isDown) {
|
||||
if (ExecuteSpecialCommand(cmd, isDown))
|
||||
return 1;
|
||||
|
||||
return CommandExecute_o(mgr, cmd, isDown);
|
||||
}
|
||||
|
||||
int __fastcall CommandExecute_h(void *mgr, void *, const char *cmd, int isDown) {
|
||||
return ExecuteCommand(mgr, cmd, isDown);
|
||||
}
|
||||
|
||||
// Runs an override-resolved command with the same execution context the engine
|
||||
// wraps around a native keypress. FUN_BINDING_KEY_DISPATCH saves the frame-
|
||||
// script exec-context, zeroes it for the nested command, marks the manager as
|
||||
// mid-command (`+0xD8`), then restores the context and floors the nesting
|
||||
// depth. Native-table bindings already run inside this (the engine sets it up
|
||||
// before calling FUN_BINDING_COMMAND_EXECUTE, which we co-hook); overrides
|
||||
// bypass that path, so we mirror it here. Without it, a native Bindings.xml
|
||||
// command dispatched through an override would run without the context its
|
||||
// handler expects. Special SPELL/ITEM/MACRO/CLICK commands run harmlessly
|
||||
// inside the same wrapper.
|
||||
int DispatchOverrideCommand(void *mgr, const char *cmd, int isDown) {
|
||||
volatile uint32_t *context =
|
||||
reinterpret_cast<volatile uint32_t *>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
volatile uint32_t *depth = reinterpret_cast<volatile uint32_t *>(
|
||||
Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH);
|
||||
volatile uint32_t *executing =
|
||||
Game::Ptr<volatile uint32_t>(mgr, Offsets::OFF_BINDING_MANAGER_EXECUTING);
|
||||
|
||||
const uint32_t savedContext = *context;
|
||||
*depth = *depth + 1;
|
||||
if (*depth != 0)
|
||||
*context = 0;
|
||||
|
||||
*executing = 1;
|
||||
const int result = ExecuteCommand(mgr, cmd, isDown);
|
||||
*executing = 0;
|
||||
|
||||
if (*depth != 0)
|
||||
*context = savedContext;
|
||||
if (static_cast<int32_t>(*depth) - 1 > 0)
|
||||
*depth = *depth - 1;
|
||||
else
|
||||
*depth = 0;
|
||||
|
||||
// An override's command, run in the frame the engine dispatch gives a saved
|
||||
// one: exec context saved and cleared, depth bumped, the manager's executing
|
||||
// flag up (FUN_BINDING_KEY_DISPATCH, see Offsets.h).
|
||||
int RunOverride(void *mgr, const char *command, int isDown) {
|
||||
const std::string copy = command; // the command may clear its own override
|
||||
auto &context = Game::Ref<uint32_t>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
auto &depth = Game::Ref<int32_t>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH);
|
||||
auto &executing = Game::Ref<uint32_t>(mgr, Offsets::OFF_BINDING_MANAGER_EXECUTING);
|
||||
const uint32_t saved = context;
|
||||
if (++depth != 0)
|
||||
context = 0;
|
||||
executing = 1;
|
||||
const int result = ExecuteCommand(mgr, copy.c_str(), isDown);
|
||||
executing = 0;
|
||||
if (depth != 0)
|
||||
context = saved;
|
||||
depth = depth - 1 > 0 ? depth - 1 : 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
int __fastcall KeyDispatch_h(void *mgr, void *, const char *key, int isDown) {
|
||||
const std::string canonicalKey = CanonicalKey(key);
|
||||
// CGUIBindings::ExecKey. A saved binding is run by the engine, handed the
|
||||
// reduced key: it uses the key only for its table lookup, and finds it as the
|
||||
// first of its own two. No binding runs nothing, as in 4.3.4.
|
||||
int __fastcall KeyDispatch_h(void *mgr, void *edx, const char *key, int isDown) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
return g_keyDispatchOriginal(mgr, edx, key, isDown);
|
||||
|
||||
// Keep a key-down paired with its override command even if that override is
|
||||
// cleared while the command executes. The newly revealed native binding
|
||||
// must not receive only the key-up half of the same physical press.
|
||||
if (!isDown) {
|
||||
auto active = g_activeOverrides.find(canonicalKey);
|
||||
if (active != g_activeOverrides.end()) {
|
||||
DispatchOverrideCommand(mgr, active->second.c_str(), 0);
|
||||
g_activeOverrides.erase(active);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
std::string reduced;
|
||||
const KeyBinding binding = FindReducedKeyBinding(mgr, key, reduced);
|
||||
if (binding.command == nullptr)
|
||||
return 0;
|
||||
// 4.3.4 hides the key's modifiers around every command. 1.12 Lua bindings
|
||||
// were written against the live state: an addon that binds SHIFT-E and
|
||||
// reads IsShiftKeyDown() inside its handler (Puppeteer) then sees nothing
|
||||
// held. Only the direct actions, which 1.12 never had, get the override.
|
||||
std::optional<Input::Modifier::KeyStateOverride> keyState;
|
||||
if (Command::IsDirectAction(binding.command))
|
||||
keyState.emplace(reduced.c_str());
|
||||
if (binding.isOverride)
|
||||
return RunOverride(mgr, binding.command, isDown);
|
||||
return g_keyDispatchOriginal(mgr, edx, reduced.c_str(), isDown);
|
||||
}
|
||||
|
||||
const OverrideEntry *entry = FindEffectiveOverride(canonicalKey);
|
||||
if (entry == nullptr)
|
||||
return KeyDispatch_o(mgr, key, isDown);
|
||||
// Direct actions only for a key (executing flag up); RunBinding reaches the
|
||||
// engine's command table, which has none of them — as in 4.3.4.
|
||||
int __fastcall CommandExecute_h(void *mgr, void *edx, const char *command, int isDown) {
|
||||
if (Game::Read<uint32_t>(mgr, Offsets::OFF_BINDING_MANAGER_EXECUTING) != 0 &&
|
||||
Command::IsDirectAction(command))
|
||||
return Command::RunKey(command, isDown);
|
||||
return g_commandExecuteOriginal(mgr, edx, command, isDown);
|
||||
}
|
||||
|
||||
if (isDown)
|
||||
g_activeOverrides[canonicalKey] = entry->command;
|
||||
// --- Lua ----------------------------------------------------------------------
|
||||
|
||||
return DispatchOverrideCommand(mgr, entry->command.c_str(), isDown);
|
||||
|
||||
const char *OptString(void *L, int idx) {
|
||||
return Game::Lua::IsString(L, idx) ? Game::Lua::ToString(L, idx) : nullptr;
|
||||
}
|
||||
|
||||
// SetBinding(key, command) through the engine's own Script_SetBinding: its
|
||||
// key normalization, current-set write, UPDATE_BINDINGS and 1/nil result.
|
||||
int SetBinding(void *L, const std::string &key, const std::string &command) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushString(L, key.c_str());
|
||||
Game::Lua::PushString(L, command.c_str());
|
||||
return reinterpret_cast<ScriptFn_t>(Offsets::FUN_SCRIPT_SET_BINDING)(L);
|
||||
}
|
||||
|
||||
int SetBindingCommand(void *L, const char *prefix, const char *usage) {
|
||||
@@ -404,216 +284,165 @@ int SetBindingCommand(void *L, const char *prefix, const char *usage) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::string key = Game::Lua::ToString(L, 1);
|
||||
const std::string command = MakeCommand(prefix, Game::Lua::ToString(L, 2));
|
||||
|
||||
// Preserve the stock SetBinding return value, binding-set selection,
|
||||
// key normalization, UPDATE_BINDINGS event, and SaveBindings behavior.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushString(L, key.c_str());
|
||||
Game::Lua::PushString(L, command.c_str());
|
||||
auto setBinding =
|
||||
reinterpret_cast<ScriptSetBinding_t>(Offsets::FUN_SCRIPT_SET_BINDING);
|
||||
|
||||
return setBinding(L);
|
||||
}
|
||||
|
||||
const char *OptionalMouseButton(void *L, int index, const char *usage) {
|
||||
if (Game::Lua::GetTop(L) < index || Game::Lua::Type(L, index) == Game::Lua::TYPE_NIL)
|
||||
return nullptr;
|
||||
|
||||
if (!Game::Lua::IsString(L, index)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Game::Lua::ToString(L, index);
|
||||
return SetBinding(L, Game::Lua::ToString(L, 1),
|
||||
std::string(prefix) + Game::Lua::ToString(L, 2));
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingSpell(void *L) {
|
||||
return SetBindingCommand(L, kSpellPrefix, "Usage: SetBindingSpell(key, spell)");
|
||||
return SetBindingCommand(L, "SPELL ", "Usage: SetBindingSpell(\"KEY\", \"spellname\")");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingItem(void *L) {
|
||||
return SetBindingCommand(L, kItemPrefix, "Usage: SetBindingItem(key, item)");
|
||||
return SetBindingCommand(L, "ITEM ", "Usage: SetBindingItem(\"KEY\", \"itemname\")");
|
||||
}
|
||||
int __fastcall Script_SetBindingMacro(void *L) {
|
||||
return SetBindingCommand(L, "MACRO ",
|
||||
"Usage: SetBindingMacro(\"KEY\", \"macroname\"|macroid)");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingMacro(void *L) {
|
||||
return SetBindingCommand(L, kMacroPrefix, "Usage: SetBindingMacro(key, macro)");
|
||||
std::string ClickCommand(const char *button, const char *mouseButton) {
|
||||
return std::string("CLICK ") + button + ":" +
|
||||
(mouseButton != nullptr ? mouseButton : "LeftButton");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingClick(void *L) {
|
||||
constexpr const char *usage = "Usage: SetBindingClick(key, buttonName [, mouseButton])";
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
Game::Lua::Error(
|
||||
L, "Usage: SetBindingClick(\"KEY\", \"buttonName\"[, \"mouseButton\"])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::string key = Game::Lua::ToString(L, 1);
|
||||
const char *buttonName = Game::Lua::ToString(L, 2);
|
||||
const char *mouseButton = OptionalMouseButton(L, 3, usage);
|
||||
const std::string command = MakeClickCommand(buttonName, mouseButton);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushString(L, key.c_str());
|
||||
Game::Lua::PushString(L, command.c_str());
|
||||
auto setBinding =
|
||||
reinterpret_cast<ScriptSetBinding_t>(Offsets::FUN_SCRIPT_SET_BINDING);
|
||||
|
||||
return setBinding(L);
|
||||
return SetBinding(L, Game::Lua::ToString(L, 1),
|
||||
ClickCommand(Game::Lua::ToString(L, 2), OptString(L, 3)));
|
||||
}
|
||||
|
||||
bool ReadOverrideHeader(void *L, const char *usage, const void **owner,
|
||||
bool *priority, std::string *key) {
|
||||
*owner = Game::Lua::ResolveObject(L, 1);
|
||||
if (*owner == nullptr || Game::Lua::Type(L, 2) != Game::Lua::TYPE_BOOLEAN ||
|
||||
!Game::Lua::IsString(L, 3)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return false;
|
||||
}
|
||||
// The owner argument: a frame, as 4.3.4 requires.
|
||||
const void *Owner(void *L) { return Game::Lua::ResolveFrame(L, 1, /*raiseError*/ false); }
|
||||
|
||||
*priority = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
*key = CanonicalKey(Game::Lua::ToString(L, 3));
|
||||
return true;
|
||||
}
|
||||
|
||||
int SetOverrideBindingCommand(void *L, const char *prefix, const char *usage) {
|
||||
const void *owner = nullptr;
|
||||
bool priority = false;
|
||||
|
||||
std::string key;
|
||||
if (!ReadOverrideHeader(L, usage, &owner, &priority, &key))
|
||||
return 0;
|
||||
|
||||
if (!Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
int __fastcall Script_SetOverrideBinding(void *L) {
|
||||
const void *owner = Owner(L);
|
||||
if (owner == nullptr || !Game::Lua::IsString(L, 3)) {
|
||||
Game::Lua::Error(L, "Usage: SetOverrideBinding(owner, isPriority, \"KEY\"[, \"COMMAND\"])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
SetOverride(owner, priority, key,
|
||||
MakeCommand(prefix, Game::Lua::ToString(L, 4)));
|
||||
|
||||
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), OptString(L, 4));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBinding(void *L) {
|
||||
constexpr const char *usage = "Usage: SetOverrideBinding(owner, isPriority, key [, command])";
|
||||
const void *owner = nullptr;
|
||||
bool priority = false;
|
||||
|
||||
std::string key;
|
||||
if (!ReadOverrideHeader(L, usage, &owner, &priority, &key))
|
||||
return 0;
|
||||
|
||||
if (Game::Lua::GetTop(L) < 4 ||
|
||||
Game::Lua::Type(L, 4) == Game::Lua::TYPE_NIL) {
|
||||
RemoveOverride(owner, key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!Game::Lua::IsString(L, 4)) {
|
||||
int SetOverrideCommand(void *L, const char *prefix, const char *usage) {
|
||||
const void *owner = Owner(L);
|
||||
if (owner == nullptr || !Game::Lua::IsString(L, 3) || !Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
SetOverride(owner, priority, key, Game::Lua::ToString(L, 4));
|
||||
const std::string command = std::string(prefix) + Game::Lua::ToString(L, 4);
|
||||
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), command.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingSpell(void *L) {
|
||||
return SetOverrideBindingCommand(
|
||||
L, kSpellPrefix,
|
||||
"Usage: SetOverrideBindingSpell(owner, isPriority, key, spell)");
|
||||
return SetOverrideCommand(
|
||||
L, "SPELL ", "Usage: SetOverrideBindingSpell(owner, isPriority, \"KEY\", \"spellname\")");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingItem(void *L) {
|
||||
return SetOverrideBindingCommand(
|
||||
L, kItemPrefix,
|
||||
"Usage: SetOverrideBindingItem(owner, isPriority, key, item)");
|
||||
return SetOverrideCommand(
|
||||
L, "ITEM ", "Usage: SetOverrideBindingItem(owner, isPriority, \"KEY\", \"itemname\")");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingMacro(void *L) {
|
||||
return SetOverrideBindingCommand(
|
||||
L, kMacroPrefix,
|
||||
"Usage: SetOverrideBindingMacro(owner, isPriority, key, macro)");
|
||||
return SetOverrideCommand(
|
||||
L, "MACRO ",
|
||||
"Usage: SetOverrideBindingMacro(owner, isPriority, \"KEY\", \"macroname\"|macroid)");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingClick(void *L) {
|
||||
constexpr const char *usage =
|
||||
"Usage: SetOverrideBindingClick(owner, "
|
||||
"isPriority, key, buttonName [, mouseButton])";
|
||||
|
||||
const void *owner = nullptr;
|
||||
bool priority = false;
|
||||
|
||||
std::string key;
|
||||
if (!ReadOverrideHeader(L, usage, &owner, &priority, &key))
|
||||
return 0;
|
||||
|
||||
if (!Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
const void *owner = Owner(L);
|
||||
if (owner == nullptr || !Game::Lua::IsString(L, 3) || !Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, "Usage: SetOverrideBindingClick(owner, isPriority, \"KEY\", "
|
||||
"\"buttonName\"[, \"mouseButton\"])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *buttonName = Game::Lua::ToString(L, 4);
|
||||
const char *mouseButton = OptionalMouseButton(L, 5, usage);
|
||||
SetOverride(owner, priority, key,
|
||||
MakeClickCommand(buttonName, mouseButton));
|
||||
|
||||
const std::string command = ClickCommand(Game::Lua::ToString(L, 4), OptString(L, 5));
|
||||
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), command.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_ClearOverrideBindings(void *L) {
|
||||
const void *owner = Game::Lua::ResolveObject(L, 1);
|
||||
const void *owner = Owner(L);
|
||||
if (owner == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: ClearOverrideBindings(owner)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_overrides.erase(std::remove_if(g_overrides.begin(), g_overrides.end(),
|
||||
[&](const OverrideEntry &entry) {
|
||||
return entry.owner == owner;
|
||||
}),
|
||||
g_overrides.end());
|
||||
for (OverrideKey &binding : g_overrides)
|
||||
SetOverride(binding, owner, false, nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
// A fresh FrameScript state makes every owner pointer from the previous
|
||||
// state stale. Permanent bindings remain in vanilla's binding table.
|
||||
g_overrides.clear();
|
||||
g_activeOverrides.clear();
|
||||
g_nextSequence = 0;
|
||||
// GetBindingAction(key [, checkOverride]): 4.3.4's GetCommandAction checks the
|
||||
// override first when asked; the saved binding is the engine's own answer.
|
||||
int __fastcall Script_GetBindingAction(void *L) {
|
||||
if (Game::Lua::IsString(L, 1) && Game::Lua::ToFlag(L, 2)) {
|
||||
if (const char *command = ActiveOverride(Game::Lua::ToString(L, 1))) {
|
||||
Game::Lua::PushString(L, command);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return reinterpret_cast<ScriptFn_t>(Offsets::FUN_SCRIPT_GET_BINDING_ACTION)(L);
|
||||
}
|
||||
|
||||
// GetBindingByKey(key): the command a press of `key` runs — the reduced-key
|
||||
// lookup ExecKey makes, overrides included.
|
||||
int __fastcall Script_GetBindingByKey(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetBindingByKey(\"action\"[, mode])");
|
||||
return 0;
|
||||
}
|
||||
std::string reduced;
|
||||
const KeyBinding binding = FindReducedKeyBinding(Manager(), Game::Lua::ToString(L, 1), reduced);
|
||||
if (binding.command == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, binding.command);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kGetBindingByKeyArgs[] = {
|
||||
Game::Doc::Req("key", "string", "A key with its modifiers, such as \"ALT-SHIFT-Q\"."),
|
||||
};
|
||||
const Game::Doc::Field kGetBindingByKeyRets[] = {
|
||||
Game::Doc::Opt("command", "string", nullptr,
|
||||
"The command a press of the key runs, else nil. When the key itself has no "
|
||||
"binding, the key with fewer modifiers is tried."),
|
||||
};
|
||||
const Game::Doc::Function kGetBindingByKey{"Returns the command that a key press runs.",
|
||||
kGetBindingByKeyArgs, kGetBindingByKeyRets,
|
||||
"BindingsGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetBindingByKey", &Script_GetBindingByKey,
|
||||
&kGetBindingByKey);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingSpell", &Script_SetBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingItem", &Script_SetBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingMacro", &Script_SetBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingClick", &Script_SetBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBinding",
|
||||
&Script_SetOverrideBinding);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingSpell",
|
||||
&Script_SetOverrideBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingItem",
|
||||
&Script_SetOverrideBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingMacro",
|
||||
&Script_SetOverrideBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingClick",
|
||||
&Script_SetOverrideBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("ClearOverrideBindings",
|
||||
&Script_ClearOverrideBindings);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBinding", &Script_SetOverrideBinding);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingSpell", &Script_SetOverrideBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingItem", &Script_SetOverrideBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingMacro", &Script_SetOverrideBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingClick", &Script_SetOverrideBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("ClearOverrideBindings", &Script_ClearOverrideBindings);
|
||||
Game::Lua::RegisterGlobalFunction("GetBindingAction", &Script_GetBindingAction);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
const Game::HookAutoRegister _hookKeyDispatch{
|
||||
Offsets::FUN_BINDING_KEY_DISPATCH, reinterpret_cast<void *>(&KeyDispatch_h),
|
||||
reinterpret_cast<void **>(&KeyDispatch_o)};
|
||||
// Owners are frames of the Lua state being torn down; their overrides go with it.
|
||||
void PrepareForReload() { g_overrides.clear(); }
|
||||
const Game::ReloadAutoRegister _reload{&PrepareForReload};
|
||||
|
||||
const Game::HookAutoRegister _hookKeyDispatch{Offsets::FUN_BINDING_KEY_DISPATCH,
|
||||
reinterpret_cast<void *>(&KeyDispatch_h),
|
||||
reinterpret_cast<void **>(&g_keyDispatchOriginal)};
|
||||
|
||||
const Game::HookAutoRegister _hookCommandExecute{
|
||||
Offsets::FUN_BINDING_COMMAND_EXECUTE,
|
||||
reinterpret_cast<void *>(&CommandExecute_h),
|
||||
reinterpret_cast<void **>(&CommandExecute_o)};
|
||||
Offsets::FUN_BINDING_COMMAND_EXECUTE, reinterpret_cast<void *>(&CommandExecute_h),
|
||||
reinterpret_cast<void **>(&g_commandExecuteOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -12,7 +12,35 @@
|
||||
<Binding name="TARGETFOCUS">
|
||||
TargetUnit("focus");
|
||||
</Binding>
|
||||
<Binding name="INTERACTMOUSEOVER">
|
||||
if ( not C_PlayerInteractionManager.InteractUnit("mouseover", false, false) ) then
|
||||
C_PlayerInteractionManager.InteractUnit("anyinteract");
|
||||
end
|
||||
</Binding>
|
||||
<Binding name="INTERACTTARGET">
|
||||
C_PlayerInteractionManager.InteractUnit("anyinteract");
|
||||
</Binding>
|
||||
<Binding name="AOELOOT">
|
||||
C_Loot.LootAllCorpses()
|
||||
</Binding>
|
||||
<!-- 4.3.4's modified-click actions and defaults (FrameXML\Bindings.xml).
|
||||
Read by src/bindings/ModifiedClick.cpp. -->
|
||||
<ModifiedClick action="SELFCAST" default="ALT"/>
|
||||
<ModifiedClick action="FOCUSCAST" default="NONE"/>
|
||||
<ModifiedClick action="AUTOLOOTTOGGLE" default="SHIFT"/>
|
||||
<ModifiedClick action="MAILAUTOLOOTTOGGLE" default="SHIFT"/>
|
||||
<ModifiedClick action="STICKYCAMERA" default="CTRL"/>
|
||||
<ModifiedClick action="CHATLINK" default="SHIFT-BUTTON1"/>
|
||||
<ModifiedClick action="DRESSUP" default="CTRL-BUTTON1"/>
|
||||
<ModifiedClick action="SOCKETITEM" default="SHIFT-BUTTON2"/>
|
||||
<ModifiedClick action="SPLITSTACK" default="SHIFT"/>
|
||||
<ModifiedClick action="PICKUPACTION" default="SHIFT"/>
|
||||
<ModifiedClick action="PICKUPITEM" default="SHIFT"/>
|
||||
<ModifiedClick action="COMPAREITEMS" default="SHIFT"/>
|
||||
<ModifiedClick action="OPENALLBAGS" default="SHIFT"/>
|
||||
<ModifiedClick action="QUESTWATCHTOGGLE" default="SHIFT"/>
|
||||
<ModifiedClick action="TOKENWATCHTOGGLE" default="SHIFT"/>
|
||||
<ModifiedClick action="SHOWITEMFLYOUT" default="ALT"/>
|
||||
<ModifiedClick action="SHOWMULTICASTFLYOUT" default="ALT"/>
|
||||
<ModifiedClick action="TRADESEARCHADD" default="SHIFT"/>
|
||||
</Bindings>
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The SPELL / ITEM / MACRO / CLICK binding commands — 4.3.4's
|
||||
// CGUIBindings::ExecKey branches (decomp UIBindings.cpp), on 1.12's own
|
||||
// primitives:
|
||||
//
|
||||
// SPELL x on release: the spellbook slot named x (FUN_RESOLVE_SPELL_NAME_TO_SLOT)
|
||||
// cast through FUN_SPELL_CAST_DISPATCH at you when SELFCAST is held,
|
||||
// at your focus when FOCUSCAST is, else with no explicit target.
|
||||
// ITEM x on release: the item named x you carry (equipment, then bags)
|
||||
// used through FUN_ITEM_USE with the same target.
|
||||
// MACRO x on release: macro index x when x is all digits, else the macro
|
||||
// named x, run through FUN_MACRO_RUN_BODY with the button
|
||||
// "LeftButton", as 4.3.4 runs it.
|
||||
// CLICK f:b press and release: the frame named f gets a mouse down and a
|
||||
// mouse up for button b.
|
||||
//
|
||||
// The release timing is 4.3.4's (and vanilla action buttons'): the command
|
||||
// runs when the key comes back up.
|
||||
//
|
||||
// CLICK: 4.3.4's frames take a `btn` argument on OnLayerMouseDown / Up that
|
||||
// marks a virtual click — no hit test, and a button clicks for ANY press
|
||||
// (down) or release (up) bit it is registered for. 1.12's take only the mouse
|
||||
// event and hit-test it, so the virtual branch is built from the button's own
|
||||
// pieces, in 4.3.4's order: the frame's OnMouseDown / OnMouseUp script with
|
||||
// the button name, then the Button state machine (click / double-click
|
||||
// vmethods, SetButtonState). Only frames that are Buttons get the second part,
|
||||
// as only CSimpleButton overrides the handlers 4.3.4 runs.
|
||||
|
||||
#include "bindings/Command.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "bindings/ModifiedClick.h"
|
||||
#include "frame/ClickFrame.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Use.h"
|
||||
#include "macro/Execute.h"
|
||||
#include "spell/Placement.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Bindings::Command {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char kSpell[] = "SPELL ";
|
||||
constexpr const char kItem[] = "ITEM ";
|
||||
constexpr const char kMacro[] = "MACRO ";
|
||||
constexpr const char kClick[] = "CLICK ";
|
||||
|
||||
bool HasPrefix(const char *command, const char *prefix) {
|
||||
return _strnicmp(command, prefix, std::strlen(prefix)) == 0;
|
||||
}
|
||||
|
||||
// The target 4.3.4 picks for SPELL / ITEM: you for SELFCAST, your focus for
|
||||
// FOCUSCAST, else none (the engine then uses your target).
|
||||
uint64_t ModifiedTarget() {
|
||||
if (ModifiedClick::IsModifiedClick("SELFCAST"))
|
||||
return Unit::Identity::PlayerGuid();
|
||||
if (ModifiedClick::IsModifiedClick("FOCUSCAST"))
|
||||
return Unit::Focus::Get();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CastSpell(const char *name) {
|
||||
Spell::Placement::DispatchSpellCastByName(name, ModifiedTarget());
|
||||
}
|
||||
|
||||
void UseItem(const char *name) {
|
||||
const Item::Arg::Resolved byName{0, 0, name};
|
||||
Item::Location::ByGUIDResult found{};
|
||||
if (!Item::Location::FindByArgNoLua(byName, &found) || found.item == nullptr)
|
||||
return;
|
||||
Item::Use::UseItem(found.item, ModifiedTarget());
|
||||
}
|
||||
|
||||
// StringToMouseButton: the click mask for a button name, 0 for any other
|
||||
// (the click vmethod then names it "UNKNOWN").
|
||||
uint32_t MaskForButton(const char *button) {
|
||||
static const struct {
|
||||
uintptr_t name;
|
||||
uint32_t mask;
|
||||
} kButtons[] = {
|
||||
{Offsets::VAR_BUTTON_NAME_LEFT, 0x01}, {Offsets::VAR_BUTTON_NAME_RIGHT, 0x04},
|
||||
{Offsets::VAR_BUTTON_NAME_MIDDLE, 0x02}, {Offsets::VAR_BUTTON_NAME_4, 0x08},
|
||||
{Offsets::VAR_BUTTON_NAME_5, 0x10},
|
||||
};
|
||||
for (const auto &entry : kButtons)
|
||||
if (_stricmp(reinterpret_cast<const char *>(entry.name), button) == 0)
|
||||
return entry.mask;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The frame's OnMouseDown / OnMouseUp, fired as the engine fires them.
|
||||
void FireMouseScript(void *frame, uintptr_t slotOffset, const char *button) {
|
||||
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
|
||||
uint32_t *slot = Game::Ptr<uint32_t>(frame, slotOffset);
|
||||
if (slot[0] == 0)
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
const int top = L != nullptr ? Game::Lua::GetTop(L) : 0;
|
||||
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
|
||||
frame, slot, reinterpret_cast<const char *>(Offsets::VAR_SCRIPT_FMT_S), button);
|
||||
if (L != nullptr)
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
using ButtonMethod_t = void(__thiscall *)(void *button, uint32_t arg, int flag);
|
||||
|
||||
void CallButton(void *button, uintptr_t slot, uint32_t arg) {
|
||||
auto *vtable = Game::Read<uintptr_t *>(button, 0);
|
||||
reinterpret_cast<ButtonMethod_t>(vtable[slot / sizeof(uintptr_t)])(button, arg, 0);
|
||||
}
|
||||
|
||||
int &ButtonState(void *button) { return Game::Ref<int>(button, Offsets::OFF_BUTTON_STATE); }
|
||||
bool StateLocked(void *button) {
|
||||
return Game::Read<int>(button, Offsets::OFF_BUTTON_STATE_LOCKED) != 0;
|
||||
}
|
||||
|
||||
// The button's click under the engine's "in click" flag.
|
||||
template <typename Fn> void InClick(Fn fn) {
|
||||
auto &flag = Game::Ref<int>(Offsets::VAR_BUTTON_IN_CLICK);
|
||||
flag = 1;
|
||||
fn();
|
||||
flag = 0;
|
||||
}
|
||||
|
||||
// CSimpleButton::OnLayerMouseDown with a `btn`.
|
||||
void VirtualMouseDown(void *frame, const char *button) {
|
||||
FireMouseScript(frame, Offsets::OFF_FRAME_ONMOUSEDOWN_SLOT, button);
|
||||
if (!Game::Lua::IsA(frame, Offsets::VAR_BUTTON_LUA_TYPE_ID) ||
|
||||
ButtonState(frame) == Offsets::BUTTON_STATE_DISABLED)
|
||||
return;
|
||||
if (Game::Read<uint32_t>(frame, Offsets::OFF_BUTTON_CLICK_MASK) & 0xFF)
|
||||
InClick([&] { CallButton(frame, Offsets::VTBL_BUTTON_CLICK, MaskForButton(button)); });
|
||||
if (!StateLocked(frame) && ButtonState(frame) != Offsets::BUTTON_STATE_DISABLED)
|
||||
CallButton(frame, Offsets::VTBL_BUTTON_SET_STATE, Offsets::BUTTON_STATE_PUSHED);
|
||||
}
|
||||
|
||||
// CSimpleButton::OnLayerMouseUp with a `btn`.
|
||||
void VirtualMouseUp(void *frame, const char *button) {
|
||||
FireMouseScript(frame, Offsets::OFF_FRAME_ONMOUSEUP_SLOT, button);
|
||||
if (!Game::Lua::IsA(frame, Offsets::VAR_BUTTON_LUA_TYPE_ID) ||
|
||||
ButtonState(frame) != Offsets::BUTTON_STATE_PUSHED)
|
||||
return;
|
||||
if (Game::Read<uint32_t>(frame, Offsets::OFF_BUTTON_CLICK_MASK) & 0xFF00) {
|
||||
InClick([&] {
|
||||
const uint32_t now = Time::Clock::NowMs();
|
||||
auto &last = Game::Ref<uint32_t>(frame, Offsets::OFF_BUTTON_LAST_CLICK_MS);
|
||||
const uint32_t mask = MaskForButton(button);
|
||||
if (Game::Read<uint32_t>(frame, Offsets::OFF_BUTTON_ONDOUBLECLICK_HANDLER) != 0 &&
|
||||
last != 0 && Time::Clock::Elapsed(last, now) <= Offsets::BUTTON_DOUBLE_CLICK_MS) {
|
||||
CallButton(frame, Offsets::VTBL_BUTTON_DOUBLE_CLICK, mask);
|
||||
last = 0;
|
||||
} else {
|
||||
CallButton(frame, Offsets::VTBL_BUTTON_CLICK, mask);
|
||||
last = now;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!StateLocked(frame) && ButtonState(frame) != Offsets::BUTTON_STATE_DISABLED)
|
||||
CallButton(frame, Offsets::VTBL_BUTTON_SET_STATE, Offsets::BUTTON_STATE_NORMAL);
|
||||
}
|
||||
|
||||
int Click(const char *target, int isDown) {
|
||||
const char *separator = std::strchr(target, ':');
|
||||
if (separator == nullptr)
|
||||
return 0;
|
||||
void *frame = Frame::ClickFrame::Find(Game::Lua::State(), target,
|
||||
static_cast<size_t>(separator - target));
|
||||
if (frame == nullptr || frame == Game::Read<void *>(Offsets::VAR_GAME_STATE_PTR))
|
||||
return 0; // no such frame, or the world frame
|
||||
const char *button = separator + 1;
|
||||
if (isDown)
|
||||
VirtualMouseDown(frame, button);
|
||||
else
|
||||
VirtualMouseUp(frame, button);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsDirectAction(const char *command) {
|
||||
return command != nullptr && (HasPrefix(command, kSpell) || HasPrefix(command, kItem) ||
|
||||
HasPrefix(command, kMacro) || HasPrefix(command, kClick));
|
||||
}
|
||||
|
||||
int RunKey(const char *command, int isDown) {
|
||||
if (HasPrefix(command, kSpell)) {
|
||||
if (!isDown)
|
||||
CastSpell(command + sizeof kSpell - 1);
|
||||
return 1;
|
||||
}
|
||||
if (HasPrefix(command, kItem)) {
|
||||
if (!isDown)
|
||||
UseItem(command + sizeof kItem - 1);
|
||||
return 1;
|
||||
}
|
||||
if (HasPrefix(command, kMacro)) {
|
||||
if (!isDown)
|
||||
Macro::Execute::Saved(command + sizeof kMacro - 1, "LeftButton");
|
||||
return 1;
|
||||
}
|
||||
return Click(command + sizeof kClick - 1, isDown);
|
||||
}
|
||||
|
||||
} // namespace Bindings::Command
|
||||
@@ -0,0 +1,29 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Bindings::Command {
|
||||
|
||||
// True for the direct-action command families a key binding can hold:
|
||||
// "SPELL x", "ITEM x", "MACRO x", "CLICK frame:button" (case-insensitive
|
||||
// prefix, as 4.3.4 tests them).
|
||||
bool IsDirectAction(const char *command);
|
||||
|
||||
// Runs a direct-action command for a key press or release, as 4.3.4's
|
||||
// CGUIBindings::ExecKey does. Returns the engine's handled flag: 1 for SPELL,
|
||||
// ITEM and MACRO, 1 for CLICK when the frame exists, else 0. Only call it for
|
||||
// IsDirectAction commands.
|
||||
int RunKey(const char *command, int isDown);
|
||||
|
||||
} // namespace Bindings::Command
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user