mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
Compare commits
311
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 |
@@ -71,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
|
||||
|
||||
+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,63 +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\UiMapPoint.lua
|
||||
Util\UIParent.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
Util\AuraUtil.lua
|
||||
Util\SecureUnitMenu.lua
|
||||
Util\TextureUtil.lua
|
||||
Util\MacroOptions.lua
|
||||
Util\SecureStateDriver.lua
|
||||
Util\MacroIconSelection.lua
|
||||
|
||||
APIDocumentation\BaseAPIMixin.lua
|
||||
APIDocumentation\FieldsAPIMixin.lua
|
||||
APIDocumentation\FunctionsAPIMixin.lua
|
||||
APIDocumentation\SystemsAPIMixin.lua
|
||||
APIDocumentation\TablesAPIMixin.lua
|
||||
APIDocumentation\EventsAPIMixin.lua
|
||||
APIDocumentation\APIDocumentation.lua
|
||||
APIDocumentation\APIDocumentation_Bootstrap.lua
|
||||
@@ -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,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,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;
|
||||
+39
-61
@@ -45,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)
|
||||
@@ -73,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}
|
||||
@@ -129,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")
|
||||
@@ -140,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.
|
||||
@@ -152,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
|
||||
@@ -178,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"
|
||||
)
|
||||
|
||||
@@ -15,32 +15,29 @@ 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 = ...`). |
|
||||
| [Upvalue limit](docs/API.md#upvalue-limit) | A function can use up to 60 upvalues, the outer locals it refers to, matching Lua 5.1. Lua 5.0 stops at 32 and rejects the whole file, so a large handler that reads many file-level locals can be valid 5.1 and still fail to load here. Nothing changes for a function within 32. |
|
||||
| [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. 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. — 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 |
|
||||
|---------|--------|
|
||||
| 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. |
|
||||
| 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. |
|
||||
| [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 what the macro does. The icon, tooltip, cooldown, range, usable state and auto-repeat glow all follow the spell or item that the directive resolves to, conditions included. They update as the conditions change. The result goes into the engine field that action-bar addons already read, so they need no code of their own. An addon with its own macro parser can publish its result through `C_Macro.SetMacroDisplay`. It then does not need to replace the action globals. |
|
||||
| [`#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
|
||||
|
||||
Everything ClassicAPI adds, in full. Each section is collapsed — click a
|
||||
@@ -50,72 +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`, `C_AddOns.LoadAddOn` |
|
||||
| [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` |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [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` |
|
||||
| [Console](docs/API.md#console) | `CalculateStringEditDistance`, `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.SetCVarBitfield` |
|
||||
| [Cursor](docs/API.md#cursor) | `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.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `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.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`, `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:SetAtlas`, `texture:GetAtlas`, `texture:ResetTexCoord`, `texture:SetSpriteSheetCell`, `texture:SetDesaturation`, `texture:GetDesaturation`, `texture:SetMask`, `frame:CreateMaskTexture`, `texture:AddMaskTexture`, `texture:RemoveMaskTexture`, `texture:GetNumMaskTextures`, `texture:GetMaskTexture`, `fontstring:SetRotation`, `fontstring:GetRotation`, `editBox:SetCursorPosition`, `editBox:GetCursorPosition`, `editBox:GetUTF8CursorPosition`, `editBox:ClearHighlightText`, `editBox:HasFocus`, `editBox:HasText`, `editBox:SetHighlightColor`, `editBox:GetHighlightColor`, `editBox:ClearHistory`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:RegisterUnitEvent`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs`, `PreClick` (script), `PostClick` (script), `button:RegisterForClicks("AnyUp"/"AnyDown")`, `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.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.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`, `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`, `string.gmatch`, `string.gsub` (table replacement), `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `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`, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `StopMacro` |
|
||||
| [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.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.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` |
|
||||
| [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` |
|
||||
| [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`, `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`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [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`, `GetShapeshiftFormID`, `GetSheathState`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsOutdoors`, `IsStealthed`, `IsSwimming` |
|
||||
| [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`, `GetPhysicalScreenSize` |
|
||||
| [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` |
|
||||
| [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`, `GetTimeCached` |
|
||||
| [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`, `IsUnitToken`, `UnitClassBase`, `UnitCreatedBySpell`, `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`, `UnitTokenFromName` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `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` |
|
||||
| [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` |
|
||||
|
||||
@@ -135,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` |
|
||||
@@ -182,6 +202,46 @@ for what each command does, and
|
||||
|
||||
</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>
|
||||
|
||||
<details>
|
||||
<summary><b>Console commands</b> — developer console (<code>-console</code>)</summary>
|
||||
|
||||
@@ -195,6 +255,9 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `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>
|
||||
|
||||
@@ -205,7 +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` |
|
||||
@@ -213,6 +280,8 @@ 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` |
|
||||
@@ -220,12 +289,17 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `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)* |
|
||||
@@ -237,9 +311,12 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `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` |
|
||||
@@ -253,6 +330,8 @@ 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)* |
|
||||
@@ -261,6 +340,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FAILED` | `status, utteranceID, destination` |
|
||||
| `VOICE_CHAT_TTS_VOICES_UPDATE` | *(none)* |
|
||||
| `WEAPON_SLOT_CHANGED` | *(none)* |
|
||||
| `WEAR_EQUIPMENT_SET` | `setID` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -282,17 +362,20 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| 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>
|
||||
|
||||
@@ -335,24 +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` and more). It also holds the conditional slash
|
||||
`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 —
|
||||
@@ -369,44 +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
|
||||
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 Blizzard helpers):
|
||||
|
||||
| Global | Purpose |
|
||||
|--------|---------|
|
||||
| `print(...)` | Backport of Blizzard's `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
|
||||
|
||||
@@ -1538,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
|
||||
|
||||
@@ -1740,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:
|
||||
@@ -2018,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
|
||||
|
||||
@@ -3833,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`.
|
||||
|
||||
@@ -3883,7 +3811,7 @@ of descriptors feeds the in-game browser, a page generator, and a drift check.
|
||||
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()`.
|
||||
- `AddOns/!!!ClassicAPI/APIDocumentation/` — the 8-file port of
|
||||
- `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.
|
||||
@@ -3978,3 +3906,232 @@ 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)
|
||||
+6873
-1458
File diff suppressed because it is too large
Load Diff
+180
-113
@@ -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
|
||||
|
||||
|
||||
+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,6 +32,54 @@ 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;
|
||||
@@ -40,4 +89,17 @@ void BoundedCopy(char *dst, const char *src, size_t n) {
|
||||
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
|
||||
|
||||
@@ -12,15 +12,37 @@
|
||||
// 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
|
||||
|
||||
+11
-11
@@ -15,6 +15,7 @@
|
||||
#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"
|
||||
@@ -81,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);
|
||||
}
|
||||
|
||||
@@ -208,13 +207,14 @@ 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 module this
|
||||
// file names directly. The engine reads its config file during boot,
|
||||
// several calls before VanillaFixes invokes `Load`, so a `-config`
|
||||
// switch cannot be applied from the normal install path — by then the
|
||||
// file has been read. This is a single 4-byte store, not a hook, so it
|
||||
// carries none of the loader-lock cost described above.
|
||||
Config::FileSwitch::Apply();
|
||||
// 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).
|
||||
//
|
||||
|
||||
+85
-40
@@ -24,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
|
||||
@@ -45,8 +71,10 @@ 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) \
|
||||
@@ -56,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) \
|
||||
@@ -66,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);
|
||||
@@ -100,6 +132,37 @@ void *ResolveObject(void *L, int idx) {
|
||||
return result;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void PushObject(void *L, void *object) {
|
||||
RawGetI(L, REGISTRY_INDEX, ObjectLuaRef(object));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -121,11 +184,8 @@ void *ResolveChecked(void *L, int idx, const uintptr_t *typeIdVars, int count,
|
||||
Error(L, EngineString(Offsets::STR_FS_THIS_NON_OBJECT));
|
||||
return nullptr;
|
||||
}
|
||||
auto **vtable = *reinterpret_cast<void ***>(obj);
|
||||
auto IsA = reinterpret_cast<FrameScriptIsA_t>(
|
||||
vtable[Offsets::OFF_VMT_FRAMESCRIPT_ISA / sizeof(void *)]);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (IsA(obj, FrameScriptTypeId(typeIdVars[i])))
|
||||
if (IsA(obj, typeIdVars[i]))
|
||||
return obj;
|
||||
}
|
||||
if (raiseError)
|
||||
@@ -144,6 +204,13 @@ int FrameScriptTypeId(uintptr_t typeIdVar) {
|
||||
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);
|
||||
}
|
||||
@@ -371,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);
|
||||
@@ -415,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
|
||||
|
||||
@@ -461,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,
|
||||
@@ -470,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 {
|
||||
|
||||
+123
-10
@@ -58,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"`,
|
||||
@@ -71,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 *)();
|
||||
|
||||
@@ -79,6 +104,19 @@ 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
|
||||
@@ -229,8 +267,14 @@ 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);
|
||||
@@ -242,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
|
||||
@@ -264,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;
|
||||
@@ -280,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;
|
||||
@@ -287,8 +348,10 @@ 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;
|
||||
@@ -298,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;
|
||||
@@ -308,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.
|
||||
@@ -324,6 +391,21 @@ 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
|
||||
@@ -357,6 +439,9 @@ void *ResolveTypedObjectAny(void *L, int idx, const uintptr_t *typeIdVars,
|
||||
// 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);
|
||||
@@ -456,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
|
||||
@@ -483,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);
|
||||
|
||||
@@ -510,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,
|
||||
@@ -541,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
|
||||
|
||||
|
||||
+86
-102
@@ -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>
|
||||
|
||||
@@ -50,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;
|
||||
}
|
||||
|
||||
@@ -190,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
|
||||
+4423
-696
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
|
||||
+36
-41
@@ -12,42 +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>
|
||||
|
||||
@@ -70,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;
|
||||
|
||||
@@ -92,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));
|
||||
@@ -111,12 +100,7 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
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) {
|
||||
@@ -126,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,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
|
||||
+5
-7
@@ -20,13 +20,11 @@ namespace Action::Slot {
|
||||
int MacroSlotForID(uint32_t macroID) {
|
||||
if (macroID == 0)
|
||||
return 0;
|
||||
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] == macroID)
|
||||
return i + 1;
|
||||
}
|
||||
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) {
|
||||
|
||||
+4
-4
@@ -18,8 +18,9 @@
|
||||
// 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 ambiguous `0x40000000` tag; the macro-slot map says which of those ids
|
||||
// are macros (the rest are bag items). Pure C — no Lua stack.
|
||||
// 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 {
|
||||
|
||||
@@ -29,8 +30,7 @@ namespace Action::Slot {
|
||||
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 holds a bag
|
||||
// item rather than a macro.
|
||||
// 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
|
||||
|
||||
+50
-43
@@ -27,6 +27,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/LoadState.h"
|
||||
#include "addons/Registry.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -35,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);
|
||||
@@ -43,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
|
||||
@@ -53,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
|
||||
@@ -264,11 +264,10 @@ int __fastcall Script_GetAddOnSecurity(void *L) {
|
||||
// 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 — and that
|
||||
// same pointer is the name `LoadState` matches on.
|
||||
// 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) {
|
||||
@@ -311,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);
|
||||
@@ -360,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
|
||||
@@ -376,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() {
|
||||
@@ -401,6 +405,9 @@ static void RegisterLuaFunctions() {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
+13
-10
@@ -46,7 +46,6 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -98,7 +97,6 @@ struct Entry {
|
||||
std::vector<Entry> g_entries;
|
||||
Env g_pass = ENV_NONE;
|
||||
unsigned g_sealed = 0;
|
||||
int g_recordedThisPass = 0;
|
||||
|
||||
constexpr const char *kDebugPrefix = "_classicapi_";
|
||||
|
||||
@@ -131,7 +129,6 @@ void Record(Kind kind, const char *table, const char *name, const void *registry
|
||||
const Function *doc) {
|
||||
if (g_pass == ENV_NONE || name == nullptr)
|
||||
return;
|
||||
++g_recordedThisPass;
|
||||
if (Entry *e = Find(kind, table, name, registry)) {
|
||||
e->env |= g_pass;
|
||||
if (e->doc == nullptr)
|
||||
@@ -162,14 +159,21 @@ const WidgetSystem kWidgetSystems[] = {
|
||||
{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;
|
||||
}
|
||||
|
||||
@@ -612,19 +616,13 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
void BeginPass(Env env) {
|
||||
g_pass = (g_sealed & env) != 0 ? ENV_NONE : env;
|
||||
g_recordedThisPass = 0;
|
||||
}
|
||||
|
||||
void EndPass() {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
g_sealed |= g_pass;
|
||||
const Env done = g_pass;
|
||||
g_pass = ENV_NONE;
|
||||
const std::vector<std::string> missing = Undocumented();
|
||||
Debug::Log::Printf("[doc] %s pass: %d registrations recorded, %d undocumented",
|
||||
done == ENV_GLUE ? "glue" : "game", g_recordedThisPass,
|
||||
static_cast<int>(missing.size()));
|
||||
}
|
||||
|
||||
void RecordGlobal(const char *name, const Function *doc) {
|
||||
@@ -658,11 +656,16 @@ void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
++g_recordedThisPass;
|
||||
if (Entry *e = Find(KIND_ENUM, parent, sub, nullptr)) {
|
||||
e->env |= g_pass;
|
||||
if (e->enumSystem == nullptr)
|
||||
|
||||
@@ -34,6 +34,9 @@ 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);
|
||||
|
||||
|
||||
@@ -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
|
||||
+318
-32
@@ -19,25 +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.
|
||||
// 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 their pet, from the
|
||||
// Aura::Source cache),
|
||||
// `!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"
|
||||
|
||||
@@ -75,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 /
|
||||
@@ -88,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.
|
||||
@@ -116,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).
|
||||
@@ -462,6 +476,46 @@ int PushAuraBySlot(void *L, Data::Emit emit) {
|
||||
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);
|
||||
@@ -474,14 +528,16 @@ int __fastcall Script_UnitAuraBySlot(void *L) {
|
||||
return PushAuraBySlot(L, Data::Emit::Positional);
|
||||
}
|
||||
|
||||
// Iterates the auras of one polarity (in `SlotInFilterOrder`, the same order
|
||||
// the by-index getters use) and pushes AuraData tables into `outer` at
|
||||
// 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) {
|
||||
int order[Offsets::UNIT_AURA_TOTAL];
|
||||
Data::WalkOrder(unit, filter, order);
|
||||
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
|
||||
const int slot = Data::SlotInFilterOrder(filter, i);
|
||||
const int slot = order[i];
|
||||
if (!Data::SlotMatchesFilter(unit, slot, filter, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
@@ -597,37 +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);
|
||||
&Script_GetAuraSlots, &kGetAuraSlots);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
|
||||
&Script_GetAuraDataBySlot);
|
||||
&Script_GetAuraDataBySlot, &kGetAuraDataBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
|
||||
&Script_GetAuraDataByAuraInstanceID,
|
||||
&kGetAuraDataByAuraInstanceID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
|
||||
&Script_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
|
||||
|
||||
+572
-211
File diff suppressed because it is too large
Load Diff
+81
-33
@@ -59,19 +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 OR the
|
||||
// player's pet cast (`PLAYER`); `NotPlayer` = the complement (`!PLAYER`).
|
||||
// The pet is part of the set by definition — modern documents the token as
|
||||
// "cast by the player, or by the player's pet or vehicle". 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,
|
||||
@@ -93,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);
|
||||
@@ -119,11 +135,18 @@ bool IsSlotHarmful(const uint8_t *unit, int slot);
|
||||
// the index the engine's tooltip methods expect for a given slot.
|
||||
bool IsSlotTooltipVisible(const uint8_t *unit, int slot);
|
||||
|
||||
// The i-th slot (0..UNIT_AURA_TOTAL-1) in the order a `filter` walk visits the
|
||||
// descriptor: the polarity's home range 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.
|
||||
int SlotInFilterOrder(Filter filter, int i);
|
||||
// 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`.
|
||||
@@ -132,7 +155,7 @@ bool SlotMatchesFilter(const uint8_t *unit, int slot, Filter filter,
|
||||
|
||||
// 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 `SlotInFilterOrder`.
|
||||
// all 48 slots in `WalkOrder`.
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
Match match = {});
|
||||
|
||||
@@ -144,32 +167,49 @@ 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 or by the
|
||||
// player's pet, 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 player or their pet"
|
||||
// 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
|
||||
@@ -207,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
|
||||
@@ -273,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 = {});
|
||||
|
||||
|
||||
@@ -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
|
||||
+337
-62
@@ -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,11 @@ 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
|
||||
@@ -403,9 +411,10 @@ 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);
|
||||
}
|
||||
|
||||
@@ -637,12 +646,68 @@ 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;
|
||||
}
|
||||
|
||||
@@ -651,19 +716,20 @@ void ApplyModOp(Entry &e, int32_t op, int32_t valueMs, uint32_t now) {
|
||||
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>(valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryRefreshed(e);
|
||||
break;
|
||||
case MOD_REDUCE:
|
||||
if (e.expirationMs != 0) {
|
||||
if (e.expirationMs > now + static_cast<uint32_t>(valueMs)) {
|
||||
if (Time::Clock::Remaining(now, e.expirationMs) >
|
||||
static_cast<uint32_t>(valueMs)) {
|
||||
e.expirationMs -= static_cast<uint32_t>(valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryChanged(e);
|
||||
} else {
|
||||
e.used = false; // shaved to/past now → server removes it
|
||||
}
|
||||
@@ -880,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};
|
||||
@@ -1010,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1226,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;
|
||||
@@ -1263,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 {
|
||||
@@ -1336,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
|
||||
}
|
||||
}
|
||||
@@ -1355,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);
|
||||
@@ -1406,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 —
|
||||
@@ -1455,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
|
||||
@@ -1476,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
|
||||
@@ -1565,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);
|
||||
@@ -1590,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);
|
||||
@@ -1600,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{
|
||||
@@ -1609,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{
|
||||
@@ -1647,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);
|
||||
}
|
||||
@@ -1674,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) {
|
||||
@@ -1792,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;
|
||||
@@ -1813,7 +2069,7 @@ void RestampPlayerChannel(uint32_t spellId, uint32_t remainingMs) {
|
||||
continue;
|
||||
e.expirationMs = now + remainingMs;
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryChanged(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1865,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};
|
||||
@@ -1924,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
|
||||
|
||||
@@ -60,6 +60,15 @@ 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 plus a family-flag overlap and/or a
|
||||
// SpellIconID, at least one of the two — to a full duration from now. Returns
|
||||
@@ -228,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
|
||||
|
||||
+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
|
||||
@@ -49,7 +49,8 @@
|
||||
#include "Inject.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include "embedded_bindings.h"
|
||||
#include "embed/Resource.h"
|
||||
#include "embedded_resources.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -132,8 +133,10 @@ bool ExtractHeaderAttr(const char *tagStart, const char *tagEnd,
|
||||
|
||||
EmbeddedConfig ResolveEmbeddedConfig() {
|
||||
EmbeddedConfig out = {};
|
||||
const char *content = reinterpret_cast<const char *>(kEmbeddedBindingsXml);
|
||||
const char *end = content + kEmbeddedBindingsXmlSize;
|
||||
const Embed::Bytes xml = Embed::Resource(Embed::Ids::kBindingsXml);
|
||||
if (xml.data == nullptr) return out;
|
||||
const char *content = reinterpret_cast<const char *>(xml.data);
|
||||
const char *end = content + xml.size;
|
||||
|
||||
// Locate `<Bindings` opening tag (skipping any comment blocks
|
||||
// that mention the tag literally for documentation purposes).
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
// 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/>.
|
||||
|
||||
// Modified clicks — 4.3.4's MODIFIEDCLICK system (decomp Ui/MODIFIEDCLICK.cpp,
|
||||
// UIBindings.cpp, UIBindings_Scripts.cpp), which 1.12's binding manager lacks.
|
||||
//
|
||||
// An action ("SELFCAST", "CHATLINK", …) is bound to a modifier set and/or a
|
||||
// mouse button ("ALT", "SHIFT-BUTTON1", "NONE"), once per binding set (0
|
||||
// default, 1 account, 2 character, 3 current), exactly like key bindings. 1.12
|
||||
// has 4.3.4's binding sets, file format and load / save / copy steps, so each
|
||||
// gains the modified-click half 4.3.4 has, through a co-hook on the step:
|
||||
//
|
||||
// - Load (Bindings.xml): `<ModifiedClick action="X" default="Y"/>` defines
|
||||
// an action and its default. We register the actions from the same file
|
||||
// first. The engine's loop tests each root tag inline and logs "Unknown
|
||||
// node type" for anything but <Binding>; 4.3.4's loop has a ModifiedClick
|
||||
// branch there, so the loader runs with a status that drops exactly that
|
||||
// warning for a ModifiedClick node and forwards everything else.
|
||||
// - LoadBindings (a binding file into a set): the set starts from the
|
||||
// defaults, then `modifiedclick BINDING ACTION` lines apply.
|
||||
// - SaveBindings: `modifiedclick` lines for every action not left at its
|
||||
// default.
|
||||
// - CopyBindings: the set's bindings are copied with the key bindings.
|
||||
//
|
||||
// The actions live as long as the binding manager: the engine builds it on
|
||||
// every world load and /reload, so the table is cleared with the Lua state.
|
||||
// Key state comes from Input::Modifier (the left/right bits 4.3.4's
|
||||
// CGGameUI::IsKeyDown reads) and the clicked button from Frame::ClickEvents
|
||||
// (4.3.4's CSimpleTop::m_mouseButtonName).
|
||||
|
||||
#include "bindings/ModifiedClick.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "baselib/Ascii.h"
|
||||
#include "frame/ClickEvents.h"
|
||||
#include "input/Modifier.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Bindings::ModifiedClick {
|
||||
|
||||
namespace {
|
||||
|
||||
// 4.3.4's modifier bits, = Input::Modifier's left/right key bits (and what
|
||||
// Input::Modifier::ParseModifiers returns).
|
||||
constexpr uint32_t kLShift = 0x01, kRShift = 0x02, kLCtrl = 0x04, kRCtrl = 0x08,
|
||||
kLAlt = 0x10, kRAlt = 0x20;
|
||||
constexpr int kKeyCount = 6;
|
||||
|
||||
// MODIFIEDCLICK_DATA. Flag 1 = the set holds the default (never saved).
|
||||
constexpr uint32_t kFlagDefault = 0x1;
|
||||
struct Data {
|
||||
uint32_t flags = 0;
|
||||
uint32_t modifiers = 0;
|
||||
const char *button = nullptr;
|
||||
};
|
||||
|
||||
struct Click {
|
||||
std::string action;
|
||||
Data data[Offsets::BINDING_SET_COUNT];
|
||||
};
|
||||
|
||||
std::vector<Click> g_clicks;
|
||||
|
||||
Click *Find(const char *action) {
|
||||
if (action == nullptr)
|
||||
return nullptr;
|
||||
for (Click &click : g_clicks)
|
||||
if (_stricmp(click.action.c_str(), action) == 0)
|
||||
return &click;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool KeyDown(int key) { return (Input::Modifier::CurrentMask() & (1u << key)) != 0; }
|
||||
|
||||
bool PrefixIs(const char *s, const char *prefix) {
|
||||
return _strnicmp(s, prefix, std::strlen(prefix)) == 0;
|
||||
}
|
||||
|
||||
// CGUIBindings::AddMetaPrefix, appended to `out`.
|
||||
void AddMetaPrefix(uint32_t modifiers, std::string &out) {
|
||||
const auto side = [&](uint32_t left, uint32_t right, const char *both, const char *l,
|
||||
const char *r) {
|
||||
if ((modifiers & (left | right)) == (left | right))
|
||||
out += both;
|
||||
else if (modifiers & left)
|
||||
out += l;
|
||||
else if (modifiers & right)
|
||||
out += r;
|
||||
};
|
||||
side(kLAlt, kRAlt, "ALT-", "LALT-", "RALT-");
|
||||
side(kLCtrl, kRCtrl, "CTRL-", "LCTRL-", "RCTRL-");
|
||||
side(kLShift, kRShift, "SHIFT-", "LSHIFT-", "RSHIFT-");
|
||||
}
|
||||
|
||||
// BUTTONn <-> mouse-button name (IndexToMouseButton / MouseButtonToIndex).
|
||||
// 1..5 are the engine's own name literals, the strings GetMouseButtonClicked
|
||||
// and the click scope hold.
|
||||
const char *const kExtraButtons[] = {"Button6", "Button7", "Button8", "Button9"};
|
||||
|
||||
const char *ButtonForIndex(int index) {
|
||||
switch (index) {
|
||||
case 1: return reinterpret_cast<const char *>(Offsets::VAR_BUTTON_NAME_LEFT);
|
||||
case 2: return reinterpret_cast<const char *>(Offsets::VAR_BUTTON_NAME_RIGHT);
|
||||
case 3: return reinterpret_cast<const char *>(Offsets::VAR_BUTTON_NAME_MIDDLE);
|
||||
case 4: return reinterpret_cast<const char *>(Offsets::VAR_BUTTON_NAME_4);
|
||||
case 5: return reinterpret_cast<const char *>(Offsets::VAR_BUTTON_NAME_5);
|
||||
case 6: case 7: case 8: case 9: return kExtraButtons[index - 6];
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int IndexForButton(const char *button) {
|
||||
for (int index = 1; index <= 9; ++index)
|
||||
if (_stricmp(ButtonForIndex(index), button) == 0)
|
||||
return index;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// MODIFIEDCLICK::SetBinding.
|
||||
bool SetBinding(Click &click, int set, const char *binding) {
|
||||
const char *button = nullptr;
|
||||
uint32_t modifiers = 0;
|
||||
if (binding != nullptr && *binding != '\0' && _stricmp(binding, "NONE") != 0) {
|
||||
modifiers = Input::Modifier::ParseModifiers(binding);
|
||||
if (PrefixIs(binding, "BUTTON")) {
|
||||
binding += 6;
|
||||
button = ButtonForIndex(std::atoi(binding));
|
||||
++binding;
|
||||
}
|
||||
if (*binding != '\0')
|
||||
return false;
|
||||
}
|
||||
Data &data = click.data[set];
|
||||
if (set != Offsets::BINDING_SET_DEFAULT)
|
||||
data.flags &= ~kFlagDefault;
|
||||
else
|
||||
data.flags |= kFlagDefault;
|
||||
data.button = button;
|
||||
data.modifiers = modifiers;
|
||||
return true;
|
||||
}
|
||||
|
||||
// MODIFIEDCLICK::GetBinding.
|
||||
std::string GetBinding(const Click &click, int set) {
|
||||
const Data &data = click.data[set];
|
||||
if (data.modifiers == 0 && data.button == nullptr)
|
||||
return "NONE";
|
||||
std::string out;
|
||||
AddMetaPrefix(data.modifiers, out);
|
||||
if (data.button != nullptr)
|
||||
out += "BUTTON" + std::to_string(IndexForButton(data.button));
|
||||
else
|
||||
out.pop_back(); // the prefix's trailing '-'
|
||||
return out;
|
||||
}
|
||||
|
||||
// MODIFIEDCLICK::IsActive.
|
||||
bool IsActive(const Click &click, int set) {
|
||||
const Data &data = click.data[set];
|
||||
if (data.modifiers != 0) {
|
||||
int key = 0;
|
||||
while (!((1u << key) & data.modifiers) || !KeyDown(key)) {
|
||||
if (++key >= kKeyCount)
|
||||
return false;
|
||||
}
|
||||
if (data.button == nullptr)
|
||||
return true;
|
||||
} else if (data.button == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const char *current = Frame::ClickEvents::CurrentButton();
|
||||
return current == nullptr || std::strcmp(current, data.button) == 0;
|
||||
}
|
||||
|
||||
// One side of a modifier test: left only, right only, or either.
|
||||
bool SideHeld(uint32_t modifiers, uint32_t left, uint32_t right, int leftKey, int rightKey) {
|
||||
const uint32_t bits = modifiers & (left | right);
|
||||
if (bits == left)
|
||||
return KeyDown(leftKey);
|
||||
if (bits == right || (bits == (left | right) && !KeyDown(leftKey)))
|
||||
return KeyDown(rightKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Bindings.xml: <ModifiedClick action default> ---------------------------
|
||||
|
||||
using LoadFile_t = int(__fastcall *)(void *mgr, void *edx, const char *path, void *md5,
|
||||
void *status);
|
||||
LoadFile_t g_loadFileOriginal = nullptr;
|
||||
|
||||
using TreeLoad_t = void *(__fastcall *)(const char *data, uint32_t size);
|
||||
using TreeFree_t = void(__fastcall *)(void *tree);
|
||||
|
||||
// CGUIBindings::LoadModifiedClick for every root node of the file.
|
||||
void LoadModifiedClicks(const char *path, void *status) {
|
||||
void *data = nullptr;
|
||||
size_t size = 0;
|
||||
if (!reinterpret_cast<AddOns::EngineIO::FileReadFn>(Offsets::FUN_FILE_READ)(0, path, &data,
|
||||
&size, 0, 1, 0))
|
||||
return;
|
||||
void *tree = reinterpret_cast<TreeLoad_t>(Offsets::FUN_XML_TREE_LOAD)(
|
||||
static_cast<const char *>(data), static_cast<uint32_t>(size));
|
||||
reinterpret_cast<AddOns::EngineIO::SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE)(data, __FILE__,
|
||||
__LINE__, 0);
|
||||
if (tree == nullptr)
|
||||
return; // the engine loader reports the parse error
|
||||
const void *root = Game::Read<const void *>(tree, 0);
|
||||
for (const void *node = root ? Xml::FirstChild(root) : nullptr; node != nullptr;
|
||||
node = Xml::NextSibling(node)) {
|
||||
if (!Xml::TagIs(node, "ModifiedClick"))
|
||||
continue;
|
||||
const char *action = Xml::Attr(node, "action");
|
||||
if (action == nullptr || *action == '\0') {
|
||||
Xml::Warn(status, "Found modified click with no action in %s", path);
|
||||
continue;
|
||||
}
|
||||
if (Find(action) != nullptr) {
|
||||
Xml::Warn(status, "Modified click %s is defined more than once in %s", action, path);
|
||||
continue;
|
||||
}
|
||||
g_clicks.push_back(Click{action, {}});
|
||||
const char *binding = Xml::Attr(node, "default");
|
||||
if (binding != nullptr && *binding != '\0')
|
||||
SetBinding(g_clicks.back(), Offsets::BINDING_SET_DEFAULT, binding);
|
||||
}
|
||||
reinterpret_cast<TreeFree_t>(Offsets::FUN_XML_TREE_FREE)(tree);
|
||||
}
|
||||
|
||||
// The status the engine loader is given: a CStatus whose only used slot,
|
||||
// Add (vtable +0xC), forwards to the real status. Load passes at most two
|
||||
// string arguments after the format, and Add is __cdecl, so taking exactly
|
||||
// two is safe whatever the format consumes.
|
||||
struct LoadStatus {
|
||||
const void *const *vtable;
|
||||
void *real;
|
||||
};
|
||||
|
||||
void __cdecl LoadStatusAdd(LoadStatus *self, int level, const char *fmt, const char *a,
|
||||
const char *b) {
|
||||
if (fmt == reinterpret_cast<const char *>(Offsets::STR_BINDINGS_UNKNOWN_NODE) &&
|
||||
a != nullptr && Ascii::EqualCI(a, "ModifiedClick"))
|
||||
return; // a node this loader now knows (LoadModifiedClicks)
|
||||
using Add_t = void(__cdecl *)(void *status, int level, const char *fmt, ...);
|
||||
const void *vtable = Game::Read<const void *>(self->real, 0);
|
||||
Game::Read<Add_t>(vtable, Offsets::VTBL_XML_STATUS_ADD)(self->real, level, fmt, a, b);
|
||||
}
|
||||
|
||||
static_assert(Offsets::VTBL_XML_STATUS_ADD == 3 * sizeof(void *));
|
||||
const void *const kLoadStatusVtable[4] = {nullptr, nullptr, nullptr,
|
||||
reinterpret_cast<const void *>(&LoadStatusAdd)};
|
||||
|
||||
int __fastcall LoadFile_h(void *mgr, void *edx, const char *path, void *md5, void *status) {
|
||||
if (status == nullptr)
|
||||
status = reinterpret_cast<void *>(Offsets::VAR_BINDINGS_LOAD_STATUS);
|
||||
if (path != nullptr)
|
||||
LoadModifiedClicks(path, status);
|
||||
LoadStatus filtered{kLoadStatusVtable, status};
|
||||
return g_loadFileOriginal(mgr, edx, path, md5, &filtered);
|
||||
}
|
||||
|
||||
// --- binding files: modifiedclick lines -----------------------------------
|
||||
|
||||
using LoadSet_t = void(__fastcall *)(int set, const char *text);
|
||||
LoadSet_t g_loadSetOriginal = nullptr;
|
||||
|
||||
using Tokenize_t = void(__stdcall *)(const char **cursor, char *out, unsigned outSize,
|
||||
const char *delims, int *outQuoted);
|
||||
|
||||
// The modified-click half of 4.3.4's LoadBindings: start from the defaults,
|
||||
// then apply `modifiedclick BINDING ACTION` lines.
|
||||
void __fastcall LoadSet_h(int set, const char *text) {
|
||||
g_loadSetOriginal(set, text);
|
||||
if (set < 0 || set >= Offsets::BINDING_SET_COUNT)
|
||||
return;
|
||||
if (set != Offsets::BINDING_SET_DEFAULT)
|
||||
for (Click &click : g_clicks)
|
||||
click.data[set] = click.data[Offsets::BINDING_SET_DEFAULT];
|
||||
|
||||
auto tokenize = reinterpret_cast<Tokenize_t>(Offsets::FUN_STORM_STR_TOKENIZE);
|
||||
const char *lineDelims = reinterpret_cast<const char *>(Offsets::VAR_MACRO_LINE_DELIMS);
|
||||
const char *spaceDelims = reinterpret_cast<const char *>(Offsets::VAR_STR_SPACE_DELIM);
|
||||
char line[1024];
|
||||
char key[32];
|
||||
const char *cursor = text;
|
||||
while (cursor != nullptr && *cursor != '\0') {
|
||||
tokenize(&cursor, line, sizeof line, lineDelims, nullptr);
|
||||
const char *p = line;
|
||||
while (*p == ' ' || *p == '\t')
|
||||
++p;
|
||||
if (!PrefixIs(p, "modifiedclick "))
|
||||
continue;
|
||||
const char *action = p + 14;
|
||||
tokenize(&action, key, sizeof key, spaceDelims, nullptr);
|
||||
if (action != nullptr)
|
||||
if (Click *click = Find(action))
|
||||
SetBinding(*click, set, key);
|
||||
}
|
||||
}
|
||||
|
||||
using SaveSet_t = void(__fastcall *)(void *mgr, void *edx, int set, void *file);
|
||||
SaveSet_t g_saveSetOriginal = nullptr;
|
||||
|
||||
using WriteFile_t = int(__fastcall *)(void *file, const void *buffer, uint32_t size,
|
||||
uint32_t *written);
|
||||
|
||||
void __fastcall SaveSet_h(void *mgr, void *edx, int set, void *file) {
|
||||
g_saveSetOriginal(mgr, edx, set, file);
|
||||
if (set < 0 || set >= Offsets::BINDING_SET_COUNT)
|
||||
return;
|
||||
auto write = reinterpret_cast<WriteFile_t>(Offsets::FUN_OS_WRITE_FILE);
|
||||
for (const Click &click : g_clicks) {
|
||||
if (click.data[set].flags & kFlagDefault)
|
||||
continue;
|
||||
const std::string line =
|
||||
"modifiedclick " + GetBinding(click, set) + " " + click.action + "\r\n";
|
||||
uint32_t written = 0;
|
||||
write(file, line.data(), static_cast<uint32_t>(line.size()), &written);
|
||||
}
|
||||
}
|
||||
|
||||
using CopySet_t = void(__fastcall *)(void *mgr, void *edx, int source, int dest);
|
||||
CopySet_t g_copySetOriginal = nullptr;
|
||||
|
||||
void __fastcall CopySet_h(void *mgr, void *edx, int source, int dest) {
|
||||
g_copySetOriginal(mgr, edx, source, dest);
|
||||
if (source < 0 || source >= Offsets::BINDING_SET_COUNT || dest < 0 ||
|
||||
dest >= Offsets::BINDING_SET_COUNT)
|
||||
return;
|
||||
for (Click &click : g_clicks)
|
||||
click.data[dest] = click.data[source];
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _loadFileHook{Offsets::FUN_BINDINGS_LOAD_FILE,
|
||||
reinterpret_cast<void *>(&LoadFile_h),
|
||||
reinterpret_cast<void **>(&g_loadFileOriginal)};
|
||||
const Game::HookAutoRegister _loadSetHook{Offsets::FUN_BINDINGS_LOAD_SET,
|
||||
reinterpret_cast<void *>(&LoadSet_h),
|
||||
reinterpret_cast<void **>(&g_loadSetOriginal)};
|
||||
const Game::HookAutoRegister _saveSetHook{Offsets::FUN_BINDINGS_SAVE_SET,
|
||||
reinterpret_cast<void *>(&SaveSet_h),
|
||||
reinterpret_cast<void **>(&g_saveSetOriginal)};
|
||||
const Game::HookAutoRegister _copySetHook{Offsets::FUN_BINDINGS_COPY_SET,
|
||||
reinterpret_cast<void *>(&CopySet_h),
|
||||
reinterpret_cast<void **>(&g_copySetOriginal)};
|
||||
|
||||
// The binding manager is rebuilt with every Lua state; so are its actions.
|
||||
void PrepareForReload() { g_clicks.clear(); }
|
||||
const Game::ReloadAutoRegister _reload{&PrepareForReload};
|
||||
|
||||
// --- Lua --------------------------------------------------------------------
|
||||
|
||||
int __fastcall Script_GetNumModifiedClickActions(void *L) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_clicks.size()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetModifiedClickAction(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetModifiedClickAction(index)");
|
||||
return 0;
|
||||
}
|
||||
const int index = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
Game::Lua::PushString(L, index >= 0 && index < static_cast<int>(g_clicks.size())
|
||||
? g_clicks[index].action.c_str()
|
||||
: nullptr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetModifiedClick(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: SetModifiedClick(\"action\", \"binding\")");
|
||||
return 0;
|
||||
}
|
||||
const char *action = Game::Lua::ToString(L, 1);
|
||||
const char *binding = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
Click *click = Find(action);
|
||||
if (click == nullptr || !SetBinding(*click, Offsets::BINDING_SET_CURRENT, binding)) {
|
||||
char message[256];
|
||||
std::snprintf(message, sizeof message,
|
||||
"SetModifiedClick(): Unknown action (%s) or binding (%s)", action,
|
||||
binding != nullptr ? binding : "(null)");
|
||||
Game::Lua::Error(L, "%s", message);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetModifiedClick(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetModifiedClick(\"action\")");
|
||||
return 0;
|
||||
}
|
||||
const Click *click = Find(Game::Lua::ToString(L, 1));
|
||||
if (click == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushString(L, GetBinding(*click, Offsets::BINDING_SET_CURRENT).c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsModifiedClick(void *L) {
|
||||
const char *action = Game::Lua::IsString(L, 1) ? Game::Lua::ToString(L, 1) : nullptr;
|
||||
if (IsModifiedClick(action))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kIndexArgs[] = {Game::Doc::Req("index", "luaIndex")};
|
||||
const Game::Doc::Field kActionArgs[] = {Game::Doc::Req("action", "string")};
|
||||
const Game::Doc::Field kSetArgs[] = {Game::Doc::Req("action", "string"),
|
||||
Game::Doc::Opt("binding", "string")};
|
||||
const Game::Doc::Field kIsArgs[] = {Game::Doc::Opt("action", "string")};
|
||||
const Game::Doc::Field kCountRets[] = {Game::Doc::Req("count", "number")};
|
||||
const Game::Doc::Field kActionRets[] = {Game::Doc::Opt("action", "string")};
|
||||
const Game::Doc::Field kBindingRets[] = {Game::Doc::Opt("binding", "string")};
|
||||
const Game::Doc::Field kIsRets[] = {Game::Doc::Opt("isModified", "number")};
|
||||
|
||||
const Game::Doc::Function kGetNum{"Returns the number of modified-click actions.", {},
|
||||
kCountRets, "BindingsGlobals"};
|
||||
const Game::Doc::Function kGetAction{"Returns the name of the modified-click action at an index.",
|
||||
kIndexArgs, kActionRets, "BindingsGlobals"};
|
||||
const Game::Doc::Function kSet{"Sets the modifier keys and mouse button for an action.",
|
||||
kSetArgs, {}, "BindingsGlobals"};
|
||||
const Game::Doc::Function kGet{"Returns the modifier keys and mouse button set for an action.",
|
||||
kActionArgs, kBindingRets, "BindingsGlobals"};
|
||||
const Game::Doc::Function kIs{"Returns 1 when the keys and mouse button for an action are held.",
|
||||
kIsArgs, kIsRets, "BindingsGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetNumModifiedClickActions",
|
||||
&Script_GetNumModifiedClickActions, &kGetNum);
|
||||
Game::Lua::RegisterGlobalFunction("GetModifiedClickAction", &Script_GetModifiedClickAction,
|
||||
&kGetAction);
|
||||
Game::Lua::RegisterGlobalFunction("SetModifiedClick", &Script_SetModifiedClick, &kSet);
|
||||
Game::Lua::RegisterGlobalFunction("GetModifiedClick", &Script_GetModifiedClick, &kGet);
|
||||
Game::Lua::RegisterGlobalFunction("IsModifiedClick", &Script_IsModifiedClick, &kIs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsModifiedClick(const char *action) {
|
||||
if (action != nullptr) {
|
||||
const char *rest = action;
|
||||
const uint32_t modifiers = Input::Modifier::ParseModifiers(rest);
|
||||
if (modifiers == 0) {
|
||||
const Click *click = Find(action);
|
||||
return click != nullptr && IsActive(*click, Offsets::BINDING_SET_CURRENT);
|
||||
}
|
||||
return SideHeld(modifiers, kLShift, kRShift, 0, 1) &&
|
||||
SideHeld(modifiers, kLCtrl, kRCtrl, 2, 3) &&
|
||||
SideHeld(modifiers, kLAlt, kRAlt, 4, 5);
|
||||
}
|
||||
if ((Input::Modifier::CurrentMask() & 0x3F) != 0)
|
||||
return true;
|
||||
for (const Click &click : g_clicks)
|
||||
if (IsActive(click, Offsets::BINDING_SET_CURRENT))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Bindings::ModifiedClick
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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::ModifiedClick {
|
||||
|
||||
// 4.3.4's CGUIBindings::IsModifiedClick. `action` null: any modifier key is
|
||||
// held or any modified-click action is active. A modifier string ("SHIFT",
|
||||
// "LCTRL-ALT"): those keys are held. Otherwise: the named action's binding in
|
||||
// the current set is active.
|
||||
bool IsModifiedClick(const char *action);
|
||||
|
||||
} // namespace Bindings::ModifiedClick
|
||||
Vendored
+32
@@ -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/>.
|
||||
|
||||
#include "cache/Query.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketWriter.h"
|
||||
|
||||
namespace Cache::Query {
|
||||
|
||||
// The send of FUN_DBCACHE_ITEMSTATS_GET_RECORD (0x0055BB18..0x0055BB89),
|
||||
// step for step. Every DBCache<T> clone builds the same packet.
|
||||
void Send(void *cache, uint32_t key) {
|
||||
Net::OutPacket packet(Game::Read<uint32_t>(cache, Offsets::OFF_DBCACHE_QUERY_OPCODE));
|
||||
packet.PutU32(key);
|
||||
if (Game::Read<uint8_t>(cache, Offsets::OFF_DBCACHE_OWNER_FLAG) != 0)
|
||||
packet.PutGuid(0);
|
||||
packet.Send();
|
||||
}
|
||||
|
||||
} // namespace Cache::Query
|
||||
Vendored
+36
@@ -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>
|
||||
|
||||
// Re-sending a DBCache query.
|
||||
//
|
||||
// A DBCache `_GetRecord` sends its query only when it creates the pending
|
||||
// node; a lookup that finds the node pending just adds its callback. The
|
||||
// server drops a STATUS_LOGGEDIN query (every *_QUERY opcode) while the
|
||||
// player is not in the world - before login completes and behind every
|
||||
// loading screen - so a query sent then is never answered, and the node
|
||||
// stays pending for the rest of the session. Sending the query again is the
|
||||
// only way out; the answer then fills the node and runs every callback on it
|
||||
// through the engine's own receive.
|
||||
|
||||
namespace Cache::Query {
|
||||
|
||||
// Sends `cache`'s query for `key` exactly as its `_GetRecord` does for a new
|
||||
// node: the cache's opcode, the key, and a zero GUID when the cache's owner
|
||||
// flag is set.
|
||||
void Send(void *cache, uint32_t key);
|
||||
|
||||
} // namespace Cache::Query
|
||||
@@ -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/>.
|
||||
|
||||
// `pitchLimit <degrees>` — 4.3.4's console command (CGCamera::CCommand_PitchLimit)
|
||||
// that sets how far the camera may pitch up and down.
|
||||
//
|
||||
// 4.3.4 writes `degrees` (and its negation) into the two globals every camera
|
||||
// pitch clamp reads. 1.12 reads the very same pair, so this writes them and the
|
||||
// engine's own clamps, smooth-pitch target and cameraPitch range check all
|
||||
// follow. The only difference is that 1.12 keeps them in .rdata, hence the
|
||||
// unprotected write.
|
||||
//
|
||||
// Like 4.3.4 there is no validation: a bare `pitchLimit` parses as 0 and locks
|
||||
// the camera level, and values past 90 degrees are taken as given.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Camera::PitchLimit {
|
||||
|
||||
namespace {
|
||||
|
||||
using ToFloat_t = float(__stdcall *)(const char *);
|
||||
|
||||
constexpr float kDegreesToRadians = 0.017453292f; // 4.3.4's own constant
|
||||
|
||||
int __fastcall Console_PitchLimit(const char * /*command*/, const char *args) {
|
||||
const float limit =
|
||||
reinterpret_cast<ToFloat_t>(Offsets::FUN_SSTR_TO_FLOAT)(args) * kDegreesToRadians;
|
||||
const float max = limit;
|
||||
const float min = -limit;
|
||||
Common::PatchBytes(reinterpret_cast<void *>(Offsets::VAR_CAMERA_MAX_PITCH), &max,
|
||||
sizeof(max));
|
||||
Common::PatchBytes(reinterpret_cast<void *>(Offsets::VAR_CAMERA_MIN_PITCH), &min,
|
||||
sizeof(min));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Process-global, so the first glue boot covers the session; the registrar
|
||||
// dedups the repeat on each logout.
|
||||
void Register() {
|
||||
Game::Console::RegisterCommand("pitchLimit", &Console_PitchLimit,
|
||||
Game::Console::CATEGORY_GAME, nullptr);
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Camera::PitchLimit
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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/>.
|
||||
|
||||
// `IsChatAFK()` / `IsChatDND()` — 4.3.4's Script_IsChatAFK / Script_IsChatDND
|
||||
// push CGChat::m_afk / m_dnd, the chat module's own record of the player's
|
||||
// state. 1.12 keeps the AFK one (VAR_CHAT_AFK) with the same writers as 4.3.4.
|
||||
// It keeps no DND one: its /dnd branch tests the player's PLAYER_FLAGS DND bit
|
||||
// directly, so IsChatDND reads that bit, the value 1.12's own chat code uses.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::AwayStatus {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_IsChatAFK(void *L) {
|
||||
Game::Lua::PushBool(L, Game::Read<uint32_t>(Offsets::VAR_CHAT_AFK) != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsChatDND(void *L) {
|
||||
bool dnd = false;
|
||||
if (const uint8_t *player = Unit::Identity::PlayerObject()) {
|
||||
if (const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO))
|
||||
dnd = (Game::Read<uint32_t>(info, Offsets::OFF_PLAYER_INFO_FLAGS) & Offsets::PLAYER_FLAG_DND) != 0;
|
||||
}
|
||||
Game::Lua::PushBool(L, dnd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kAfkRets[] = {
|
||||
Game::Doc::Req("isAFK", "boolean", nullptr),
|
||||
};
|
||||
const Game::Doc::Function kIsChatAFK{
|
||||
"Returns whether the player is marked AFK. Changes as soon as /afk is typed or AFK is cleared.",
|
||||
{}, kAfkRets, "ChatGlobals"};
|
||||
|
||||
const Game::Doc::Field kDndRets[] = {
|
||||
Game::Doc::Req("isDND", "boolean", nullptr),
|
||||
};
|
||||
const Game::Doc::Function kIsChatDND{
|
||||
"Returns whether the player is marked DND.", {}, kDndRets, "ChatGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsChatAFK", &Script_IsChatAFK, &kIsChatAFK);
|
||||
Game::Lua::RegisterGlobalFunction("IsChatDND", &Script_IsChatDND, &kIsChatDND);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Chat::AwayStatus
|
||||
@@ -0,0 +1,153 @@
|
||||
// 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 `%` codes in sent chat, brought to 4.3.4's set.
|
||||
//
|
||||
// SendChatMessage runs its text through FUN_CHAT_PARSE_TEXT (4.3.4's
|
||||
// ChatParserParseText) before the drunk filter. 1.12's knows `%t` / `%n`
|
||||
// only: the target's name — the unit object's name, else its name-cache
|
||||
// name, else TARGET_TOKEN_NOT_FOUND. 4.3.4's (QuestTextParser.cpp) has two
|
||||
// cases, both resolved the same way and both prefixed `{rtN}` when the unit
|
||||
// carries a raid marker and a name was found:
|
||||
//
|
||||
// - `%t` / `%T` / `%n` / `%N` — the target (1.12 lacks only the prefix);
|
||||
// - `%f` / `%F` — the focus, or FOCUS_TOKEN_NOT_FOUND (1.12 lacks the code).
|
||||
//
|
||||
// The co-hook rewrites a copy of the text and hands it to the original: `%f`
|
||||
// becomes the focus text, and a `%t` / `%n` pair gets the target's `{rtN}` in
|
||||
// front of it, so the engine still produces the target's name. The copy walks
|
||||
// `%` codes the way the original does — a code pair stays together, any other
|
||||
// `%` consumes only itself — so text after a `%` is never read differently.
|
||||
// The `{rtN}` prefix renders through Chat::RaidMarkers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/RaidTarget.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Chat::ChatTokens {
|
||||
|
||||
namespace {
|
||||
|
||||
using Parse_t = bool(__fastcall *)(const char *text, char *out, int size);
|
||||
Parse_t g_parseOriginal = nullptr;
|
||||
|
||||
const char *GlobalText(const char *name) {
|
||||
using GetText_t = const char *(__fastcall *)(const char *name, int edx, int gender);
|
||||
return reinterpret_cast<GetText_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRAMESCRIPT_GET_TEXT))(name, -1, 0);
|
||||
}
|
||||
|
||||
// A unit's name the way the `%t` case finds the target's: the unit object's
|
||||
// name, else the name cache. Null when neither has one.
|
||||
const char *UnitName(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
if (void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0)) {
|
||||
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
|
||||
const char *name =
|
||||
reinterpret_cast<GetName_t>(static_cast<uintptr_t>(Offsets::FUN_OBJECT_GET_NAME))(
|
||||
unit, nullptr);
|
||||
if (name != nullptr && *name != '\0')
|
||||
return name;
|
||||
}
|
||||
return Unit::Identity::NameForGuid(guid);
|
||||
}
|
||||
|
||||
// `{rtN}` for a unit that carries raid marker N, else nothing.
|
||||
void AppendMarker(std::string &out, uint64_t guid) {
|
||||
for (int mark = 1; mark <= Unit::RaidTarget::kMarkCount; ++mark) {
|
||||
if (Unit::RaidTarget::GetGUIDByMark(mark) == guid) {
|
||||
char prefix[8];
|
||||
std::snprintf(prefix, sizeof prefix, "{rt%d}", mark);
|
||||
out += prefix;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsMarked(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return false;
|
||||
for (int mark = 1; mark <= Unit::RaidTarget::kMarkCount; ++mark)
|
||||
if (Unit::RaidTarget::GetGUIDByMark(mark) == guid)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsFocusCode(char c) { return c == 'f' || c == 'F'; }
|
||||
bool IsTargetCode(char c) { return c == 't' || c == 'T' || c == 'n' || c == 'N'; }
|
||||
|
||||
uint64_t TargetGuid() {
|
||||
return Game::Read<uint64_t>(Offsets::VAR_CURRENT_SELECTION_GUID_LO);
|
||||
}
|
||||
|
||||
bool __fastcall Parse_h(const char *text, char *out, int size) {
|
||||
if (text == nullptr)
|
||||
return g_parseOriginal(text, out, size);
|
||||
|
||||
// Leave the original's input untouched unless a code needs us: any `%f`,
|
||||
// or a `%t` / `%n` while the target is marked and named.
|
||||
const uint64_t target = TargetGuid();
|
||||
const bool targetPrefix = IsMarked(target) && UnitName(target) != nullptr;
|
||||
bool any = false;
|
||||
for (const char *p = std::strchr(text, '%'); p != nullptr; p = std::strchr(p + 1, '%'))
|
||||
if (IsFocusCode(p[1]) || (targetPrefix && IsTargetCode(p[1]))) {
|
||||
any = true;
|
||||
break;
|
||||
}
|
||||
if (!any)
|
||||
return g_parseOriginal(text, out, size);
|
||||
|
||||
std::string s;
|
||||
for (const char *p = text; *p != '\0';) {
|
||||
if (*p != '%') {
|
||||
s += *p++;
|
||||
continue;
|
||||
}
|
||||
const char c = p[1];
|
||||
if (IsFocusCode(c)) {
|
||||
const uint64_t focus = Unit::Focus::Get();
|
||||
if (const char *name = UnitName(focus)) {
|
||||
AppendMarker(s, focus);
|
||||
s += name;
|
||||
} else {
|
||||
s += GlobalText("FOCUS_TOKEN_NOT_FOUND");
|
||||
}
|
||||
p += 2;
|
||||
} else if (IsTargetCode(c)) {
|
||||
if (targetPrefix)
|
||||
AppendMarker(s, target);
|
||||
s.append(p, 2); // the original's own code — keep the pair together
|
||||
p += 2;
|
||||
} else {
|
||||
s += *p++; // the original consumes only the '%' here
|
||||
}
|
||||
}
|
||||
return g_parseOriginal(s.c_str(), out, size);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _parseHook{Offsets::FUN_CHAT_PARSE_TEXT,
|
||||
reinterpret_cast<void *>(&Parse_h),
|
||||
reinterpret_cast<void **>(&g_parseOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Chat::ChatTokens
|
||||
@@ -1,75 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetCurrentChatGUID()` — call from inside an addon's OnEvent for
|
||||
// any CHAT_MSG_* to get the sender's GUID. Vanilla 1.12 omits the
|
||||
// GUID from chat event args (added as arg12 of CHAT_MSG_* only in
|
||||
// 3.0+), so addons currently strcmp the localized sender name string
|
||||
// against an internal cache to identify the player. With this we
|
||||
// just read the GUID directly.
|
||||
//
|
||||
// The GUID is published for the duration of a chat dispatch by a
|
||||
// `DispatchScope` (see the header), which Chat::Dispatch — the single
|
||||
// owner of the FUN_0049A870 chat-dispatch hook — constructs around the
|
||||
// original call. The engine fires the CHAT_MSG_* event synchronously
|
||||
// inside that call, so `GetCurrentChatGUID` reads the right GUID from
|
||||
// the addon's OnEvent. The dispatcher is also called for synthetic chat
|
||||
// (system notifications, arena team events, etc.) with GUID = 0; for
|
||||
// those `GetCurrentChatGUID()` returns nil — matching the modern API's
|
||||
// behavior of a nil GUID for non-player-sourced chat.
|
||||
|
||||
#include "CurrentGUID.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::CurrentGUID {
|
||||
|
||||
namespace {
|
||||
|
||||
uint32_t g_currentGuidLo = 0;
|
||||
uint32_t g_currentGuidHi = 0;
|
||||
|
||||
int __fastcall Script_GetCurrentChatGUID(void *L) {
|
||||
if (g_currentGuidLo == 0 && g_currentGuidHi == 0)
|
||||
return 0;
|
||||
const uint64_t guid =
|
||||
(static_cast<uint64_t>(g_currentGuidHi) << 32) | g_currentGuidLo;
|
||||
char buf[Guid::STRING_SIZE];
|
||||
Game::Lua::PushString(L, Guid::FormatAsString(guid, buf, sizeof buf));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCurrentChatGUID",
|
||||
&Script_GetCurrentChatGUID);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DispatchScope::DispatchScope(uint32_t guidLo, uint32_t guidHi)
|
||||
: prevLo_(g_currentGuidLo), prevHi_(g_currentGuidHi) {
|
||||
g_currentGuidLo = guidLo;
|
||||
g_currentGuidHi = guidHi;
|
||||
}
|
||||
|
||||
DispatchScope::~DispatchScope() {
|
||||
g_currentGuidLo = prevLo_;
|
||||
g_currentGuidHi = prevHi_;
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Chat::CurrentGUID
|
||||
@@ -1,40 +0,0 @@
|
||||
// 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>
|
||||
|
||||
namespace Chat::CurrentGUID {
|
||||
|
||||
// RAII scope that publishes the sender GUID of the chat message currently being
|
||||
// dispatched, for `GetCurrentChatGUID()` to read. Chat::Dispatch constructs one
|
||||
// around the original chat-dispatch call: the engine fires the CHAT_MSG_* event
|
||||
// synchronously inside that call, so an addon's OnEvent sees the GUID of the
|
||||
// message that triggered it. Restores the prior value on destruction rather than
|
||||
// clearing to 0, so nested dispatch (a chat event firing during another's
|
||||
// OnEvent — e.g. an addon calling SendChatMessage from its handler) leaves the
|
||||
// outer context's GUID intact when the inner returns.
|
||||
class DispatchScope {
|
||||
public:
|
||||
DispatchScope(uint32_t guidLo, uint32_t guidHi);
|
||||
~DispatchScope();
|
||||
DispatchScope(const DispatchScope &) = delete;
|
||||
DispatchScope &operator=(const DispatchScope &) = delete;
|
||||
|
||||
private:
|
||||
uint32_t prevLo_;
|
||||
uint32_t prevHi_;
|
||||
};
|
||||
|
||||
} // namespace Chat::CurrentGUID
|
||||
+108
-36
@@ -23,56 +23,128 @@
|
||||
// - Chat::IconFilter::Sanitize — strip player-injected `|T` icon spoofs from
|
||||
// the message before the engine builds the chat line AND the speech bubble
|
||||
// (both come from the same buffer, so one sanitize covers both).
|
||||
// - Chat::RaidMarkers::Substitute — expand `{rt1}`/`{skull}`/… raid-target
|
||||
// tokens into inline marker icons. Runs AFTER Sanitize so a player gets the
|
||||
// fixed marker icons but still can't inject an arbitrary `|T` path.
|
||||
// - Chat::CurrentGUID::DispatchScope — publish the sender GUID for the
|
||||
// synchronous CHAT_MSG_* event so `GetCurrentChatGUID()` can read it.
|
||||
// - Chat::RaidMarkers::EventScope — mark the message whose CHAT_MSG_* text
|
||||
// gets `{rt1}`/`{skull}`/… expanded into marker icons (at the event fire;
|
||||
// the bubble expands its own). AFTER Sanitize, so a player gets the fixed
|
||||
// marker icons but still can't inject an arbitrary `|T` path.
|
||||
// - Chat::EventArgs::SenderScope — name the sender for the CHAT_MSG_* event
|
||||
// the original fires synchronously: its arg12 and `GetCurrentChatGUID()`.
|
||||
//
|
||||
// Signature — `__fastcall`, `RET 0x28`:
|
||||
// ECX = arg1 — the MESSAGE text (verified from the call site in FUN_0049D560:
|
||||
// `MOV ECX,[EBP-0x10]` = the raw/processed message. An older
|
||||
// note mislabeled this "sender name".) Copied into one buffer
|
||||
// fed to both the chat frame and the bubble spawn FUN_00608AC0.
|
||||
// EDX = arg2 — chat type byte
|
||||
// [esp+04] = arg3 [esp+08] = arg4 [esp+0c] = arg5
|
||||
// [esp+10] = arg6 [esp+14] = arg7
|
||||
// [esp+18] = arg8 — target GUID lo (whisper/channel target)
|
||||
// [esp+1c] = arg9 — target GUID hi
|
||||
// [esp+20] = arg10
|
||||
// [esp+24] = arg11 — sender GUID lo ← published
|
||||
// [esp+28] = arg12 — sender GUID hi ← published
|
||||
// Signature — `__fastcall`, `RET 0x28` (stack args at entry):
|
||||
// ECX = the MESSAGE text (`MOV ECX,[EBP-0x10]` in FUN_0049D560; copied into
|
||||
// one buffer that feeds both the chat frame and the bubble spawn
|
||||
// FUN_00608AC0)
|
||||
// EDX = chat type
|
||||
// [esp+04] senderName [esp+08] languageID [esp+0c] channel
|
||||
// [esp+10] targetName [esp+14] flags ("AFK"/"DND"/"GM")
|
||||
// [esp+18] / [esp+1c] speaker GUID lo/hi — the unit the bubble goes on
|
||||
// [esp+20] rank
|
||||
// [esp+24] / [esp+28] GUID lo/hi read right before the message text
|
||||
//
|
||||
// The `RET 0x28` cleanup (40 bytes = 10 stack args) MUST match the declared
|
||||
// signature exactly; an earlier 10-arg / `ret 0x20` version left 8 bytes
|
||||
// uncleaned per call and crashed the engine on a later indirect call.
|
||||
//
|
||||
// WHICH GUID IS THE SENDER. Neither pair is the sender for every caller, so
|
||||
// Sender() picks per caller and chat type (packet layouts in Offsets.h at
|
||||
// FUN_CHAT_DISPATCH):
|
||||
// - Monster say/yell/emote/whisper and raid boss emote: the last pair is the
|
||||
// monster's TARGET — often the local player — and the speaker pair, where
|
||||
// the packet has one, is a creature. 4.3.4 reports no GUID for a creature
|
||||
// sender, so these report none.
|
||||
// - Raid boss whisper (wire type 0x59): FUN_0049D560 reads name + TARGET and
|
||||
// dispatches it as a plain whisper (type 6), so the chat type no longer
|
||||
// shows it. The SMSG_MESSAGECHAT subscriber below keeps the wire type for
|
||||
// the dispatch the handler is about to make.
|
||||
// - The two pending-chat callers pass zeros for both pairs: the name-query
|
||||
// callback that sends a sender's queued messages once the name arrives
|
||||
// (every message from a sender the name cache did not know yet), and the
|
||||
// enter-world drain. The entry keeps the GUID it was waiting on, and
|
||||
// arg 5 / arg 7 point into that entry.
|
||||
// - Everything else: the last pair.
|
||||
|
||||
#include "CurrentGUID.h"
|
||||
#include "EventArgs.h"
|
||||
#include "IconFilter.h"
|
||||
#include "RaidMarkers.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <intrin.h>
|
||||
|
||||
namespace Chat::Dispatch {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int CHAT_MSG_MONSTER_SAY = 0x0B;
|
||||
constexpr int CHAT_MSG_MONSTER_YELL = 0x0C;
|
||||
constexpr int CHAT_MSG_MONSTER_EMOTE = 0x0D;
|
||||
constexpr int CHAT_MSG_MONSTER_WHISPER = 0x1A;
|
||||
constexpr int CHAT_MSG_RAID_BOSS_WHISPER = 0x59;
|
||||
constexpr int CHAT_MSG_RAID_BOSS_EMOTE = 0x5A;
|
||||
|
||||
using ChatDispatch_t = void(__fastcall *)(
|
||||
const char *message, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi);
|
||||
const char *senderName, int languageID, const char *channel, const char *targetName,
|
||||
const char *flags, int speakerGuidLo, int speakerGuidHi, int rank,
|
||||
int guidLo, int guidHi);
|
||||
|
||||
ChatDispatch_t g_original = nullptr;
|
||||
|
||||
// Wire chat type of the SMSG_MESSAGECHAT being handled. Set before the engine's
|
||||
// handler runs, which is the only caller that returns to
|
||||
// CHAT_DISPATCH_RET_PACKET, so it is always that packet's type there.
|
||||
int g_packetChatType = -1;
|
||||
|
||||
void OnPacket(uint32_t opcode, Net::CDataStore *packet) {
|
||||
if (opcode == Offsets::SMSG_MESSAGECHAT && packet != nullptr)
|
||||
g_packetChatType = Net::Read<uint8_t>(packet);
|
||||
}
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
bool IsMonsterChat(int chatType) {
|
||||
switch (chatType) {
|
||||
case CHAT_MSG_MONSTER_SAY:
|
||||
case CHAT_MSG_MONSTER_YELL:
|
||||
case CHAT_MSG_MONSTER_EMOTE:
|
||||
case CHAT_MSG_MONSTER_WHISPER:
|
||||
case CHAT_MSG_RAID_BOSS_EMOTE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t Sender(const void *caller, int chatType, const char *channel, const char *flags,
|
||||
int guidLo, int guidHi) {
|
||||
if (IsMonsterChat(chatType))
|
||||
return 0;
|
||||
const auto at = reinterpret_cast<uintptr_t>(caller);
|
||||
if (at == Offsets::CHAT_DISPATCH_RET_PENDING_NAME ||
|
||||
at == Offsets::CHAT_DISPATCH_RET_PENDING_DRAIN) {
|
||||
const auto *entry =
|
||||
reinterpret_cast<const uint8_t *>(channel) - Offsets::OFF_PENDING_CHAT_CHANNEL;
|
||||
// Both pointers must land in one entry, or this is not the layout the
|
||||
// drain is known to pass.
|
||||
if (reinterpret_cast<const uint8_t *>(flags) != entry + Offsets::OFF_PENDING_CHAT_FLAGS)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(entry + Offsets::OFF_PENDING_CHAT_GUID);
|
||||
}
|
||||
if (at == Offsets::CHAT_DISPATCH_RET_PACKET && g_packetChatType == CHAT_MSG_RAID_BOSS_WHISPER)
|
||||
return 0;
|
||||
return (static_cast<uint64_t>(static_cast<uint32_t>(guidHi)) << 32) |
|
||||
static_cast<uint32_t>(guidLo);
|
||||
}
|
||||
|
||||
void __fastcall ChatDispatch_h(
|
||||
const char *message, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi)
|
||||
const char *senderName, int languageID, const char *channel, const char *targetName,
|
||||
const char *flags, int speakerGuidLo, int speakerGuidHi, int rank,
|
||||
int guidLo, int guidHi)
|
||||
{
|
||||
// Strip player-injected `|T` icon escapes (covers chat line + speech bubble;
|
||||
// both build from this message). The 0x800 buffer matches the engine's own
|
||||
@@ -80,19 +152,19 @@ void __fastcall ChatDispatch_h(
|
||||
char iconBuf[0x800];
|
||||
const char *msg = Chat::IconFilter::Sanitize(message, iconBuf, sizeof iconBuf);
|
||||
|
||||
// Expand raid-target tokens ({rt1}/{skull}/…) into inline marker icons —
|
||||
// after the sanitize, so only these fixed, safe escapes are added.
|
||||
char markerBuf[0x800];
|
||||
msg = Chat::RaidMarkers::Substitute(msg, markerBuf, sizeof markerBuf);
|
||||
// Raid-target tokens ({rt1}/{skull}/…) become marker icons in the CHAT_MSG_*
|
||||
// text the original fires (and, separately, in the bubble after its strip);
|
||||
// the buffer keeps the tags. After the sanitize, so only these fixed, safe
|
||||
// escapes are added.
|
||||
Chat::RaidMarkers::EventScope markers;
|
||||
|
||||
// Publish the sender GUID for the CHAT_MSG_* event the original fires
|
||||
// synchronously; restored when this scope ends (after the original returns).
|
||||
Chat::CurrentGUID::DispatchScope guid(static_cast<uint32_t>(senderGuidLo),
|
||||
static_cast<uint32_t>(senderGuidHi));
|
||||
// The sender for the CHAT_MSG_* event the original fires synchronously;
|
||||
// restored when this scope ends (after the original returns).
|
||||
Chat::EventArgs::SenderScope sender(
|
||||
Sender(_ReturnAddress(), chatType, channel, flags, guidLo, guidHi), flags, true);
|
||||
|
||||
g_original(msg, chatType, arg3, arg4, arg5, arg6, arg7,
|
||||
targetGuidLo, targetGuidHi, arg10,
|
||||
senderGuidLo, senderGuidHi);
|
||||
g_original(msg, chatType, senderName, languageID, channel, targetName, flags,
|
||||
speakerGuidLo, speakerGuidHi, rank, guidLo, guidHi);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
// 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/>.
|
||||
|
||||
// CHAT_MSG_* arg11 (lineID) and arg12 (sender GUID), as 4.3.4 fires them,
|
||||
// `GetCurrentChatGUID()`, which reads the same sender from inside the handler,
|
||||
// and `GetCurrentChatEvent()`, which reads the whole payload back.
|
||||
//
|
||||
// 4.3.4's SendChatEvent (ChatFrame.cpp, 0x008E82E0) fires every chat event
|
||||
// with "%s%s%s%s%s%s%d%d%s%d%d%s%u%b": the ten arguments 1.12 already passes,
|
||||
// then lineID, the sender GUID string, and two Battle.net fields. 1.12 fires
|
||||
// the first ten from one place, FUN_CHAT_EVENT_FIRE, so its FUN_FIRE_EVENT
|
||||
// call is redirected here and the two are appended:
|
||||
//
|
||||
// arg11 lineID — ++counter for each message, before its event fires
|
||||
// (4.3.4: `uint32_t lineID = ++s_chatLineID`). Text emotes
|
||||
// get 0, as 4.3.4's AddTextEmoteMessage passes.
|
||||
// arg12 guid — the sender's GUID string, only for a PLAYER GUID and not
|
||||
// for a GM-tagged message; "" otherwise, never nil (4.3.4
|
||||
// leaves its guidString buffer empty).
|
||||
//
|
||||
// 4.3.4's arg13 (Battle.net presence ID) and arg14 (mobile flag) have no
|
||||
// 1.12 source and are left off.
|
||||
//
|
||||
// `GetCurrentChatGUID()` returns that same sender, as nil where arg12 is "",
|
||||
// so code written for the function and code reading arg12 always agree.
|
||||
//
|
||||
// `GetCurrentChatEvent()` (a ClassicAPI extension) returns the event name and
|
||||
// the arguments exactly as fired. 1.12's ChatFrame_OnEvent reads the `event`
|
||||
// and `arg1..` globals, which any handler or hook running before it may
|
||||
// overwrite; the payload read here is the one this fire passed. It lives only
|
||||
// for the fire, and a chat event fired from another's handler restores the
|
||||
// outer payload when it returns.
|
||||
//
|
||||
// The sender comes from a SenderScope. Chat::Dispatch opens one around the
|
||||
// chat-message dispatch with the GUID it picks for that chat type; the text
|
||||
// emote builder is co-hooked below because it fires without passing its GUID
|
||||
// on. A message fired outside any scope (FUN_0049B1B0, client-made notices)
|
||||
// has no sender and still takes a lineID, as every 4.3.4 AddChatMessage does.
|
||||
//
|
||||
// The call site is redirected rather than FUN_FIRE_EVENT hooked: every event
|
||||
// goes through FUN_FIRE_EVENT, chat goes through this one call. It also works
|
||||
// under Turtle's patch, whose .tdata detour returns to the PUSH just before
|
||||
// the redirected CALL.
|
||||
|
||||
#include "EventArgs.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "RaidMarkers.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Chat::EventArgs {
|
||||
|
||||
namespace {
|
||||
|
||||
uint64_t g_sender = 0; // 0 unless a player sender, already filtered
|
||||
bool g_takesLineID = true;
|
||||
uint32_t g_lineID = 0;
|
||||
|
||||
using Fire_t = void(__cdecl *)(int eventId, const char *fmt, ...);
|
||||
using TextEmote_t = void(__fastcall *)(const uint64_t *senderGuid, int emote, const char *target,
|
||||
int emoteNum);
|
||||
|
||||
TextEmote_t g_textEmoteOriginal = nullptr;
|
||||
|
||||
// The chat event being fired, as pushed. `argc` is 12 for the stock format and
|
||||
// 10 when another loader's format was fired unextended.
|
||||
struct Payload {
|
||||
int eventId;
|
||||
const char *text, *sender, *language, *channelString, *target, *flags;
|
||||
int zoneChannelID, channelNumber;
|
||||
const char *channelName;
|
||||
int channelUnk;
|
||||
uint32_t lineID;
|
||||
const char *guid;
|
||||
int argc;
|
||||
};
|
||||
|
||||
const Payload *g_payload = nullptr;
|
||||
|
||||
// Points g_payload at a fire for its duration, restoring the outer one after.
|
||||
class PayloadScope {
|
||||
public:
|
||||
explicit PayloadScope(const Payload &p) : prev_(g_payload) { g_payload = &p; }
|
||||
~PayloadScope() { g_payload = prev_; }
|
||||
PayloadScope(const PayloadScope &) = delete;
|
||||
PayloadScope &operator=(const PayloadScope &) = delete;
|
||||
|
||||
private:
|
||||
const Payload *prev_;
|
||||
};
|
||||
|
||||
// 1.12's chat tags are "AFK", "DND" and "GM"; 4.3.4's IsStaffChatFlag also
|
||||
// matches "DEV", which this client never sends.
|
||||
bool IsStaffFlag(const char *flags) { return flags != nullptr && std::strcmp(flags, "GM") == 0; }
|
||||
|
||||
// Same order as the engine's own ten; see Offsets.h at FUN_CHAT_EVENT_FIRE.
|
||||
// cdecl with exactly the pushed argument count, so the caller's ADD ESP,0x30
|
||||
// stays balanced.
|
||||
void __cdecl FireSite(int eventId, const char *fmt, const char *text, const char *sender,
|
||||
const char *language, const char *channelString, const char *target,
|
||||
const char *flags, int zoneChannelID, int channelNumber,
|
||||
const char *channelName, int channelUnk) {
|
||||
const auto fire = reinterpret_cast<Fire_t>(static_cast<uintptr_t>(Offsets::FUN_FIRE_EVENT));
|
||||
// `{rt1}` → marker icon in the event text only; the engine's buffer keeps
|
||||
// the tag for the bubble. 0x800 matches the engine's message-buffer cap.
|
||||
char markerBuf[0x800];
|
||||
text = Chat::RaidMarkers::ForChatEvent(text, markerBuf, sizeof markerBuf);
|
||||
// Another loader passing its own format: the arguments may not be ours to
|
||||
// extend, so fire exactly what was asked.
|
||||
Payload payload{eventId, text, sender, language, channelString, target, flags,
|
||||
zoneChannelID, channelNumber, channelName, channelUnk, 0, "", 10};
|
||||
if (fmt != reinterpret_cast<const char *>(Offsets::VAR_STR_CHAT_EVENT_FORMAT)) {
|
||||
PayloadScope scope(payload);
|
||||
fire(eventId, fmt, text, sender, language, channelString, target, flags, zoneChannelID,
|
||||
channelNumber, channelName, channelUnk);
|
||||
return;
|
||||
}
|
||||
const uint32_t lineID = g_takesLineID ? ++g_lineID : 0;
|
||||
char guid[Guid::STRING_SIZE] = "";
|
||||
if (g_sender != 0)
|
||||
Guid::FormatAsString(g_sender, guid, sizeof guid);
|
||||
payload.lineID = lineID;
|
||||
payload.guid = guid;
|
||||
payload.argc = 12;
|
||||
PayloadScope scope(payload);
|
||||
fire(eventId, "%s%s%s%s%s%s%d%d%s%d%d%s", text, sender, language, channelString, target, flags,
|
||||
zoneChannelID, channelNumber, channelName, channelUnk, static_cast<int>(lineID), guid);
|
||||
}
|
||||
|
||||
void __fastcall TextEmote_h(const uint64_t *senderGuid, int emote, const char *target,
|
||||
int emoteNum) {
|
||||
SenderScope scope(*senderGuid, nullptr, false); // the engine dereferences it unchecked too
|
||||
g_textEmoteOriginal(senderGuid, emote, target, emoteNum);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCurrentChatGUID(void *L) {
|
||||
if (g_sender == 0)
|
||||
return 0;
|
||||
char buf[Guid::STRING_SIZE];
|
||||
Game::Lua::PushString(L, Guid::FormatAsString(g_sender, buf, sizeof buf));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// event name, arg1..argN — the same values the handler received.
|
||||
int __fastcall Script_GetCurrentChatEvent(void *L) {
|
||||
using namespace Game::Lua;
|
||||
const Payload *p = g_payload;
|
||||
if (p == nullptr || CheckStack(L, p->argc + 1) == 0)
|
||||
return 0;
|
||||
const auto *entry = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR) +
|
||||
p->eventId * Offsets::EVENT_ENTRY_STRIDE;
|
||||
PushString(L, Game::Read<const char *>(entry, Offsets::OFF_EVENT_ENTRY_NAME));
|
||||
// A null string pushes nil, as it does through the engine's dispatcher.
|
||||
PushString(L, p->text);
|
||||
PushString(L, p->sender);
|
||||
PushString(L, p->language);
|
||||
PushString(L, p->channelString);
|
||||
PushString(L, p->target);
|
||||
PushString(L, p->flags);
|
||||
PushNumber(L, p->zoneChannelID);
|
||||
PushNumber(L, p->channelNumber);
|
||||
PushString(L, p->channelName);
|
||||
PushNumber(L, p->channelUnk);
|
||||
if (p->argc == 12) {
|
||||
PushNumber(L, p->lineID);
|
||||
PushString(L, p->guid);
|
||||
}
|
||||
return p->argc + 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kChatEventRets[] = {
|
||||
Game::Doc::Req("event", "string", "The CHAT_MSG_* event name."),
|
||||
Game::Doc::Opt("text", "string"),
|
||||
Game::Doc::Opt("playerName", "string"),
|
||||
Game::Doc::Opt("languageName", "string"),
|
||||
Game::Doc::Opt("channelName", "string"),
|
||||
Game::Doc::Opt("playerName2", "string"),
|
||||
Game::Doc::Opt("specialFlags", "string"),
|
||||
Game::Doc::Opt("zoneChannelID", "number"),
|
||||
Game::Doc::Opt("channelIndex", "number"),
|
||||
Game::Doc::Opt("channelBaseName", "string"),
|
||||
Game::Doc::Opt("unused", "number"),
|
||||
Game::Doc::Opt("lineID", "number"),
|
||||
Game::Doc::Opt("guid", "string"),
|
||||
};
|
||||
const Game::Doc::Function kGetCurrentChatEvent{
|
||||
"Returns the name and arguments of the CHAT_MSG_* event being handled now, or nothing "
|
||||
"outside one.",
|
||||
{}, kChatEventRets, "ChatGlobals", true};
|
||||
|
||||
// Once per process; a patched site no longer reaches FUN_FIRE_EVENT, so a
|
||||
// second pass would refuse anyway.
|
||||
bool g_applied = false;
|
||||
|
||||
void Apply() {
|
||||
if (g_applied)
|
||||
return;
|
||||
g_applied = true;
|
||||
auto *site = reinterpret_cast<uint8_t *>(Offsets::PATCH_CHAT_EVENT_FIRE_CALL);
|
||||
int32_t rel;
|
||||
std::memcpy(&rel, site + 1, sizeof rel);
|
||||
if (site[0] != 0xE8 ||
|
||||
reinterpret_cast<uintptr_t>(site) + 5 + rel != Offsets::FUN_FIRE_EVENT) {
|
||||
Debug::Log::Printf("Chat::EventArgs: chat event fire call is not the stock CALL; "
|
||||
"CHAT_MSG_* events keep 10 arguments");
|
||||
return;
|
||||
}
|
||||
rel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(&FireSite) -
|
||||
(reinterpret_cast<uintptr_t>(site) + 5));
|
||||
Common::PatchBytes(site + 1, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCurrentChatGUID", &Script_GetCurrentChatGUID);
|
||||
Game::Lua::RegisterGlobalFunction("GetCurrentChatEvent", &Script_GetCurrentChatEvent,
|
||||
&kGetCurrentChatEvent);
|
||||
Apply();
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Game::HookAutoRegister _textEmoteHook{Offsets::FUN_CHAT_TEXT_EMOTE,
|
||||
reinterpret_cast<void *>(&TextEmote_h),
|
||||
reinterpret_cast<void **>(&g_textEmoteOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
SenderScope::SenderScope(uint64_t senderGuid, const char *flags, bool takesLineID)
|
||||
: prevGuid_(g_sender), prevTakesLineID_(g_takesLineID) {
|
||||
g_sender = (Guid::IsPlayer(senderGuid) && !IsStaffFlag(flags)) ? senderGuid : 0;
|
||||
g_takesLineID = takesLineID;
|
||||
}
|
||||
|
||||
SenderScope::~SenderScope() {
|
||||
g_sender = prevGuid_;
|
||||
g_takesLineID = prevTakesLineID_;
|
||||
}
|
||||
|
||||
} // namespace Chat::EventArgs
|
||||
@@ -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
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::EventArgs {
|
||||
|
||||
// RAII scope naming the sender of the CHAT_MSG_* event fired inside it, for
|
||||
// the event's arg12 and for `GetCurrentChatGUID()`. The engine fires the event
|
||||
// synchronously inside the call the scope wraps. Restores the prior state on
|
||||
// destruction rather than resetting it, so a chat event fired from another's
|
||||
// handler leaves the outer message's sender intact when the inner one returns.
|
||||
//
|
||||
// `flags` is the message's chat tag ("AFK"/"DND"/"GM", or null). A sender
|
||||
// counts only when it is a player and the message is not GM-tagged, as in
|
||||
// 4.3.4. `takesLineID` is false only for text emotes, which 4.3.4 fires with
|
||||
// lineID 0.
|
||||
class SenderScope {
|
||||
public:
|
||||
SenderScope(uint64_t senderGuid, const char *flags, bool takesLineID);
|
||||
~SenderScope();
|
||||
SenderScope(const SenderScope &) = delete;
|
||||
SenderScope &operator=(const SenderScope &) = delete;
|
||||
|
||||
private:
|
||||
uint64_t prevGuid_;
|
||||
bool prevTakesLineID_;
|
||||
};
|
||||
|
||||
} // namespace Chat::EventArgs
|
||||
+45
-43
@@ -13,27 +13,19 @@
|
||||
|
||||
// Inline-texture (`|T`, `|A`) chat anti-spoof.
|
||||
//
|
||||
// Vanilla neutralizes player-*typed* escapes in chat (a typed `|cff…|r`,
|
||||
// `|Hitem…|h` or `|T…|t` shows as raw text) — only *trusted* text renders them
|
||||
// (addon `AddMessage`, shift-clicked links). Our Text::InlineTexture backport
|
||||
// breaks that parity for the two texture markers: it detects the neutralized
|
||||
// `||T` / `||A` form and draws the art anyway, so a player could `/say`
|
||||
// `|Tpath|t` and have a real icon appear in everyone's chat AND in the speech
|
||||
// bubble over their head.
|
||||
// A player-typed escape never renders: the edit box stores a typed `|` as `||`,
|
||||
// the escaped pipe, which every text path draws as a literal `|`. That alone
|
||||
// keeps a typed `|Tpath|t` literal. The send-side escape check
|
||||
// (SendChatMessage) also rejects a `|` followed by anything but `|` or `c` —
|
||||
// but it skips from a `|c` to the next `|r` unchecked, so a modified client
|
||||
// or an addon can still smuggle a real `|T` inside a colour run, and it would
|
||||
// render as art in everyone's chat and in the bubble over the sender.
|
||||
//
|
||||
// This restores vanilla's behavior for both markers by defanging them in the
|
||||
// chat path, while leaving `|c`/`|r`/`|H`/`|h` exactly as vanilla (already raw
|
||||
// for typed input). It CANNOT be done in the text emitter: addon `|T` and player
|
||||
// `|T` reach the emitter as byte-identical `||T` (vanilla doubles the
|
||||
// unrecognized escape for both), so the only place they're distinguishable is
|
||||
// the source — server-delivered chat flows through the chat dispatcher, addon
|
||||
// `print` does not. Chat::Dispatch owns that hook and calls this on the message.
|
||||
//
|
||||
// Defang = replace the `|` of each opener with a space, breaking the pipe→letter
|
||||
// adjacency the inline-texture detector keys on (`|T` and `||T` both contain the
|
||||
// `|T` substring, so this catches the doubled form too). The payload text stays
|
||||
// readable, the closer is inert without an opener, and every other escape is
|
||||
// untouched.
|
||||
// So server-delivered chat is checked once at the source — Chat::Dispatch
|
||||
// owns the dispatch hook and calls this on the message — and each real opener
|
||||
// gets the engine's own neutralization: its `|` is doubled, the bytes a typed
|
||||
// one has, so it shows as the literal text the sender typed. `||` pairs are
|
||||
// already literal and are left alone, as is every other escape.
|
||||
//
|
||||
// ANY new marker the emitter learns MUST be added here in the same change, or it
|
||||
// becomes a chat-spoof vector.
|
||||
@@ -44,17 +36,19 @@ namespace Chat::IconFilter {
|
||||
|
||||
namespace {
|
||||
|
||||
// True if `s` contains an inline-texture opener anywhere. Both markers count:
|
||||
// `|T` names a texture path and `|A` names an atlas, and either one would render
|
||||
// art from player-typed text if it survived to the fontstring.
|
||||
bool IsIconOpener(const char *s) {
|
||||
return s[0] == '|' && (s[1] == 'T' || s[1] == 'A');
|
||||
}
|
||||
// A real inline-texture opener at `s`: `|T` names a texture path and `|A` an
|
||||
// atlas. Callers step over `||` pairs, so `s` is always at a token start.
|
||||
bool IsIconOpener(const char *s) { return s[0] == '|' && (s[1] == 'T' || s[1] == 'A'); }
|
||||
|
||||
bool HasIconEscape(const char *s) {
|
||||
for (; *s != '\0'; ++s)
|
||||
for (; *s != '\0'; ++s) {
|
||||
if (s[0] == '|' && s[1] == '|') {
|
||||
++s; // escaped pipe
|
||||
continue;
|
||||
}
|
||||
if (IsIconOpener(s))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -66,27 +60,35 @@ const char *Sanitize(const char *msg, char *buf, size_t bufSize) {
|
||||
|
||||
// NEVER write `msg` in place: several of the chat handler's ~50 callers pass
|
||||
// read-only `.rdata` literals (system notifications), so an in-place edit
|
||||
// would fault. We copy only when a `|T` is actually present.
|
||||
// would fault. We copy only when an opener is actually present.
|
||||
//
|
||||
// Defanging must not leave a LONE `|` followed by a non-escape char — that's
|
||||
// an invalid escape, which the chat frame tolerates but the speech-bubble
|
||||
// text setter chokes on, producing an empty bubble (verified in-game). By the
|
||||
// time chat reaches us the escape is usually the doubled `||T` (vanilla's own
|
||||
// neutralization of typed escapes), where `||` is a VALID literal-pipe escape.
|
||||
// So: keep the pipe(s) and blank the `T` when the pipe completes a `||`; blank
|
||||
// BOTH chars for a lone `|T`. Either way no dangling `|` remains, and no `|T`
|
||||
// opener survives for the inline-texture detector to fire on.
|
||||
// Never leave a lone `|` before a non-escape character: the chat frame
|
||||
// tolerates it, but the speech-bubble builder ends the text there (it drops
|
||||
// a `|` with everything up to the next `[`). A doubled `||` is the valid
|
||||
// literal-pipe escape; when the buffer has no room for the extra byte, the
|
||||
// opener is blanked instead.
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; msg[i] != '\0' && j + 1 < bufSize;) {
|
||||
if (IsIconOpener(msg + i)) {
|
||||
const bool doubledPipe = (i > 0 && msg[i - 1] == '|');
|
||||
buf[j++] = doubledPipe ? '|' : ' '; // keep pipe only if it completes `||`
|
||||
if (j + 1 < bufSize)
|
||||
buf[j++] = ' '; // blank the `T` / `A`
|
||||
i += 2; // consumed the opener
|
||||
} else {
|
||||
if (msg[i] == '|' && msg[i + 1] == '|') {
|
||||
buf[j++] = msg[i++];
|
||||
if (j + 1 < bufSize)
|
||||
buf[j++] = msg[i++];
|
||||
continue;
|
||||
}
|
||||
if (IsIconOpener(msg + i)) {
|
||||
if (j + 3 < bufSize) {
|
||||
buf[j++] = '|';
|
||||
buf[j++] = '|';
|
||||
buf[j++] = msg[i + 1];
|
||||
} else {
|
||||
buf[j++] = ' ';
|
||||
if (j + 1 < bufSize)
|
||||
buf[j++] = ' ';
|
||||
}
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
buf[j++] = msg[i++];
|
||||
}
|
||||
buf[j] = '\0';
|
||||
return buf;
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
namespace Chat::IconFilter {
|
||||
|
||||
// Strips player-injected inline-texture (`|T…|t`) escapes from a chat message.
|
||||
// Returns `msg` unchanged when it contains no `|T` (the common case — no copy),
|
||||
// otherwise a sanitized copy written into `buf`. Called by Chat::Dispatch on the
|
||||
// message the engine's chat handler is about to display. See IconFilter.cpp for
|
||||
// the anti-spoof rationale and why sanitizing here (not in the text emitter) is
|
||||
// the only place player-typed and addon `|T` are distinguishable.
|
||||
// Neutralizes player-injected inline-texture (`|T…|t`, `|A…|a`) openers in a
|
||||
// chat message, as the edit box neutralizes typed ones: the `|` is doubled.
|
||||
// Returns `msg` unchanged when it contains no opener (the common case — no
|
||||
// copy), otherwise a sanitized copy written into `buf`. Called by
|
||||
// Chat::Dispatch on the message the engine's chat handler is about to display.
|
||||
// See IconFilter.cpp for how a real opener can reach chat at all.
|
||||
const char *Sanitize(const char *msg, char *buf, size_t bufSize);
|
||||
|
||||
} // namespace Chat::IconFilter
|
||||
|
||||
+23
-13
@@ -19,10 +19,15 @@
|
||||
// a `|T` escape that crops the shared raid-target sprite sheet
|
||||
// `Interface\TargetingFrame\UI-RaidTargetingIcons` to the matching 64×64 cell.
|
||||
//
|
||||
// Runs in Chat::Dispatch AFTER Chat::IconFilter::Sanitize. Order is deliberate:
|
||||
// sanitize first defangs any player-typed raw `|T` spoof, then we add ONLY the
|
||||
// fixed, known-safe marker escapes — so players get the markers without a path to
|
||||
// inject arbitrary textures.
|
||||
// Two consumers, as in 4.3.4, both AFTER Chat::Dispatch's
|
||||
// Chat::IconFilter::Sanitize (which defangs a smuggled `|T`, so only these
|
||||
// fixed, known-safe marker escapes are added):
|
||||
// - the CHAT_MSG_* event text (ForChatEvent, at Chat::EventArgs' fire site)
|
||||
// — 4.3.4's chat frame expands the tags for display;
|
||||
// - the speech bubble, after the bubble's own escape strip
|
||||
// (chatbubble/RaidIcons.cpp) — 4.3.4's CGChatBubbleFrame::ParseRaidIcons.
|
||||
// The message buffer itself keeps the `{rt1}` text: the bubble builder drops
|
||||
// every `|` code it does not know, so it must see the tags, not the markup.
|
||||
//
|
||||
// Icon→texcoord mapping verified against the client's own UnitPopup.lua
|
||||
// (RAID_TARGET_1..8): the sheet is 256×256 with 64-pixel cells, marker N at
|
||||
@@ -113,13 +118,6 @@ const char *Substitute(const char *msg, char *buf, size_t bufSize) {
|
||||
const int col = (marker - 1) % 4;
|
||||
const int row = (marker - 1) / 4;
|
||||
const int l = col * 64, r = l + 64, t = row * 64, b = t + 64;
|
||||
// Emit the pipe-DOUBLED form `||T…||t`. The inline emitter
|
||||
// renders both `|T` and `||T`, but the vanilla speech-bubble
|
||||
// text builder (FUN_004b1600) TRUNCATES at a lone `|T` while
|
||||
// PRESERVING `||` — so the doubled form is what survives into
|
||||
// the bubble's FontString (which pfUI's bubble reskin copies
|
||||
// and renders). Anti-spoof Sanitize runs before this, so these
|
||||
// are the only `||T` openers a player can produce.
|
||||
char markup[128];
|
||||
// `:0:0` = auto-size. The inline emitter resolves height/width
|
||||
// to the target line's font pixel height at draw time, so a
|
||||
@@ -128,8 +126,8 @@ const char *Substitute(const char *msg, char *buf, size_t bufSize) {
|
||||
// payload crops the 256x256 sheet to the cell (l,r,t,b).
|
||||
const int m = std::snprintf(
|
||||
markup, sizeof markup,
|
||||
"||TInterface\\TargetingFrame\\UI-RaidTargetingIcons:0:0:0:0:256:256:"
|
||||
"%d:%d:%d:%d||t",
|
||||
"|TInterface\\TargetingFrame\\UI-RaidTargetingIcons:0:0:0:0:256:256:"
|
||||
"%d:%d:%d:%d|t",
|
||||
l, r, t, b);
|
||||
// Reserve room for the literal tail after this token
|
||||
// (msg[k+1..]) so the rest of the message always fits.
|
||||
@@ -151,4 +149,16 @@ const char *Substitute(const char *msg, char *buf, size_t bufSize) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool g_inDispatch = false;
|
||||
} // namespace
|
||||
|
||||
EventScope::EventScope() : prev_(g_inDispatch) { g_inDispatch = true; }
|
||||
|
||||
EventScope::~EventScope() { g_inDispatch = prev_; }
|
||||
|
||||
const char *ForChatEvent(const char *text, char *buf, size_t bufSize) {
|
||||
return g_inDispatch ? Substitute(text, buf, bufSize) : text;
|
||||
}
|
||||
|
||||
} // namespace Chat::RaidMarkers
|
||||
|
||||
+22
-4
@@ -21,10 +21,28 @@ namespace Chat::RaidMarkers {
|
||||
// icon textures — the modern `{rt1}`..`{rt8}` / `{star}` / `{circle}` / `{skull}`
|
||||
// (etc.) chat substitutions, backported onto the inline-texture renderer. Returns
|
||||
// `msg` unchanged when it contains no `{` (the common case — no copy), otherwise a
|
||||
// rewritten copy in `buf`. Called by Chat::Dispatch AFTER the `|T` anti-spoof
|
||||
// sanitize, so a player can use the fixed, safe marker icons but still can't
|
||||
// inject an arbitrary texture path. See RaidMarkers.cpp for the token table and
|
||||
// the icon→texcoord mapping.
|
||||
// rewritten copy in `buf`. Only ever applied to text that went through the `|T`
|
||||
// anti-spoof sanitize, so a player can use the fixed, safe marker icons but
|
||||
// still can't inject an arbitrary texture path. See RaidMarkers.cpp for the
|
||||
// token table, the icon→texcoord mapping and the two consumers.
|
||||
const char *Substitute(const char *msg, char *buf, size_t bufSize);
|
||||
|
||||
// Marks a server chat message being dispatched (Chat::Dispatch opens it around
|
||||
// the engine's dispatch). Only its CHAT_MSG_* event gets the markers; client
|
||||
// notices and text emotes fire outside one. Restores the prior state.
|
||||
class EventScope {
|
||||
public:
|
||||
EventScope();
|
||||
~EventScope();
|
||||
EventScope(const EventScope &) = delete;
|
||||
EventScope &operator=(const EventScope &) = delete;
|
||||
|
||||
private:
|
||||
bool prev_;
|
||||
};
|
||||
|
||||
// The CHAT_MSG_* event text: Substitute inside an EventScope, else `text`.
|
||||
// Called from Chat::EventArgs' fire site.
|
||||
const char *ForChatEvent(const char *text, char *buf, size_t bufSize);
|
||||
|
||||
} // namespace Chat::RaidMarkers
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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/>.
|
||||
|
||||
// `SendSystemMessage(msg)` — a local CHAT_MSG_SYSTEM line.
|
||||
//
|
||||
// 4.3.4 calls CGChat::AddChatMessage(msg, CHAT_MSG_SYSTEM, NULL). 1.12's
|
||||
// equivalent is the chat line function FUN_CHAT_DISPATCH, called exactly as the
|
||||
// game-message display FUN_00496720 calls it for a chat-kind message (0x00496822):
|
||||
// chat type 10 in EDX and ten zero stack args. The engine copies the text into
|
||||
// its own 0x800 buffer with a bounded SStrCopy (0x0049A9F1), so any length is
|
||||
// safe. The call goes through Chat::Dispatch's hook like every engine system
|
||||
// line does.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::SystemMessage {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int CHAT_MSG_SYSTEM = 0x0A;
|
||||
|
||||
using ChatDispatch_t = void(__fastcall *)(
|
||||
const char *message, int chatType,
|
||||
const char *senderName, int languageID, const char *channel, const char *targetName,
|
||||
const char *flags, int speakerGuidLo, int speakerGuidHi, int rank,
|
||||
int guidLo, int guidHi);
|
||||
|
||||
int __fastcall Script_SendSystemMessage(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
Game::Lua::Error(L, "Usage: SendSystemMessage(\"message\")");
|
||||
auto dispatch = reinterpret_cast<ChatDispatch_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CHAT_DISPATCH));
|
||||
dispatch(Game::Lua::ToString(L, 1), CHAT_MSG_SYSTEM, nullptr, 0, nullptr, nullptr,
|
||||
nullptr, 0, 0, 0, 0, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("message", "string", "The text of the line."),
|
||||
};
|
||||
const Game::Doc::Function kSendSystemMessage{
|
||||
"Shows a line in chat as a system message, only on your own client. It fires "
|
||||
"CHAT_MSG_SYSTEM with the text as arg1.",
|
||||
kArgs, {}, "ChatGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("SendSystemMessage", &Script_SendSystemMessage,
|
||||
&kSendSystemMessage);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Chat::SystemMessage
|
||||
+7
-16
@@ -29,10 +29,9 @@
|
||||
// unchanged. This is the pre-7.2.5 "walk WorldFrame children" pattern the
|
||||
// wiki describes, but handed the exact set instead of the whole world.
|
||||
//
|
||||
// The engine keeps active bubbles on an intrusive Storm `TSList` whose
|
||||
// head is `VAR_CHAT_BUBBLE_LIST_HEAD` (`0x00B6F038`); each node's forward
|
||||
// link sits at `+0x318` and the list terminator carries the low-bit
|
||||
// sentinel. We mirror the engine's own walk in the per-frame update
|
||||
// The engine keeps active bubbles on an intrusive Storm `TSList`
|
||||
// (`VAR_CHAT_BUBBLE_LIST_HEAD` / `OFF_CHAT_BUBBLE_LINK_NEXT` in Offsets.h);
|
||||
// the list terminator carries the low-bit sentinel. We mirror the engine's own walk in the per-frame update
|
||||
// `FUN_004b1060` / teardown `FUN_004b0f80` verbatim. Bubbles whose owner
|
||||
// unit has despawned are pruned on the engine's next update tick, so a
|
||||
// just-orphaned bubble can linger one frame — harmless, and addons that
|
||||
@@ -42,32 +41,24 @@
|
||||
// frames, so every bubble is returned regardless.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "ui/FrameObject.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ChatBubble::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
// Storm TSList of live CGChatBubbleFrames. Head pointer, per-node
|
||||
// forward link, and the end-of-list low-bit sentinel — see the engine
|
||||
// walks at 0x004b1060 / 0x004b0f80.
|
||||
constexpr uintptr_t kListHead = 0x00B6F038;
|
||||
constexpr int kOffNextLink = 0x318;
|
||||
|
||||
} // namespace
|
||||
|
||||
static int __fastcall Script_GetAllChatBubbles(void *L) {
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
uintptr_t node = *reinterpret_cast<const uintptr_t *>(kListHead);
|
||||
uintptr_t node = *reinterpret_cast<const uintptr_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CHAT_BUBBLE_LIST_HEAD));
|
||||
int nextIndex = 1;
|
||||
while (node != 0 && (node & 1) == 0) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextIndex++));
|
||||
UI::FrameObject::Push(L, reinterpret_cast<void *>(node));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
node = *reinterpret_cast<const uintptr_t *>(node + kOffNextLink);
|
||||
node = *reinterpret_cast<const uintptr_t *>(node + Offsets::OFF_CHAT_BUBBLE_LINK_NEXT);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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/>.
|
||||
|
||||
// Raid-target icons in speech bubbles — 4.3.4's CGChatBubbleFrame::
|
||||
// ParseRaidIcons. 4.3.4's bubble Initialize strips the line's hyperlinks,
|
||||
// expands `{rt1}`/`{skull}`/… into icon markup, then sets the text. 1.12's
|
||||
// bubble builder (FUN_004B1600) strips into a stack buffer and sets it with
|
||||
// one CALL; that call is redirected here, so the tags are expanded after the
|
||||
// strip, exactly where 4.3.4 does it. The strip has to come first: it drops
|
||||
// every `|` code it does not know, an icon included.
|
||||
//
|
||||
// The message reached the builder through Chat::Dispatch, so the `|T`
|
||||
// anti-spoof sanitize already ran; only the fixed marker escapes are added.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "chat/RaidMarkers.h"
|
||||
#include "debug/Log.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace ChatBubble::RaidIcons {
|
||||
|
||||
namespace {
|
||||
|
||||
using SetText_t = void(__fastcall *)(void *fs, void *edx, const char *text, int expand);
|
||||
|
||||
// The redirected call: `__thiscall(fs, text, expand)`, RET 8 — the dummy-EDX
|
||||
// __fastcall shape matches its registers and stack.
|
||||
void __fastcall SetBubbleText(void *fs, void *edx, const char *text, int expand) {
|
||||
// The builder's own buffer is 0x800 bytes.
|
||||
char buf[0x800];
|
||||
const char *withIcons = Chat::RaidMarkers::Substitute(text, buf, sizeof buf);
|
||||
reinterpret_cast<SetText_t>(static_cast<uintptr_t>(Offsets::FUN_FONTSTRING_SET_TEXT))(
|
||||
fs, edx, withIcons, expand);
|
||||
}
|
||||
|
||||
// Once per process; a patched site no longer reaches FUN_FONTSTRING_SET_TEXT,
|
||||
// so a second pass would refuse anyway.
|
||||
bool g_applied = false;
|
||||
|
||||
void Apply() {
|
||||
if (g_applied)
|
||||
return;
|
||||
g_applied = true;
|
||||
auto *site = reinterpret_cast<uint8_t *>(Offsets::PATCH_CHAT_BUBBLE_SET_TEXT_CALL);
|
||||
int32_t rel;
|
||||
std::memcpy(&rel, site + 1, sizeof rel);
|
||||
if (site[0] != 0xE8 ||
|
||||
reinterpret_cast<uintptr_t>(site) + 5 + rel != Offsets::FUN_FONTSTRING_SET_TEXT) {
|
||||
Debug::Log::Printf("ChatBubble::RaidIcons: the bubble SetText call is not the stock "
|
||||
"CALL; bubbles keep raid tags as text");
|
||||
return;
|
||||
}
|
||||
rel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(&SetBubbleText) -
|
||||
(reinterpret_cast<uintptr_t>(site) + 5));
|
||||
Common::PatchBytes(site + 1, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Apply};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace ChatBubble::RaidIcons
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user