mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd33288c9 | ||
|
|
bf7e476c10 | ||
|
|
8f3ed96d51 | ||
|
|
e1ffaab6b4 | ||
|
|
c13b1c8eb7 | ||
|
|
bedc5d6740 | ||
|
|
bdef428ff7 | ||
|
|
25b1a1a95c | ||
|
|
5e23332880 | ||
|
|
eb36b3e09e | ||
|
|
16452519a6 | ||
|
|
33c5f3041d | ||
|
|
e8a03d584b | ||
|
|
adba73a6d8 | ||
|
|
d09137cae0 | ||
|
|
5721fb65a7 | ||
|
|
3afd403eae | ||
|
|
e1030213d3 | ||
|
|
c1cf437bf8 | ||
|
|
07a91feff0 | ||
|
|
88ea4356a0 | ||
|
|
50909ce440 | ||
|
|
d48d003828 | ||
|
|
d51f5f72a9 | ||
|
|
04efd4caf1 | ||
|
|
329d6b75ba | ||
|
|
6a677ce447 | ||
|
|
f6f88ab4ee | ||
|
|
17c928028e | ||
|
|
a47f38417f | ||
|
|
1282e3f6db | ||
|
|
4e4fad9196 | ||
|
|
67063876da | ||
|
|
b34368ef14 | ||
|
|
4983c9fd83 | ||
|
|
9e5114141e | ||
|
|
6be25d78e0 | ||
|
|
06e68d04a8 | ||
|
|
8bfe0b008c | ||
|
|
e9c2e7416a | ||
|
|
6641e5ac13 | ||
|
|
db1f00e0a3 | ||
|
|
82062309a9 | ||
|
|
2a218a0a76 | ||
|
|
e8a9003bf0 | ||
|
|
aa0279a600 | ||
|
|
a754d1ba22 | ||
|
|
d55877649f | ||
|
|
f55779f87e | ||
|
|
91e88d768a | ||
|
|
d358bd9515 | ||
|
|
4ba82e59e4 | ||
|
|
b05c15ffa7 | ||
|
|
4daf6b9364 | ||
|
|
4c6c9deae6 | ||
|
|
983cc3bdff | ||
|
|
18ac033381 | ||
|
|
81434f7014 | ||
|
|
22605dad64 | ||
|
|
7378e0b128 | ||
|
|
94b0545631 | ||
|
|
243ab37836 | ||
|
|
a12fff6704 | ||
|
|
d747c699f5 | ||
|
|
88d76c04ad | ||
|
|
47d4b9c660 | ||
|
|
8e9e30567c | ||
|
|
8be78ad946 | ||
|
|
0dea869ab1 | ||
|
|
b4dffb75c0 | ||
|
|
9445fd6333 | ||
|
|
c8effdd6ec | ||
|
|
b3ac1c07f7 | ||
|
|
cfbc4f4e9c | ||
|
|
bb9579c398 | ||
|
|
5642644833 | ||
|
|
58e7e1a2c6 | ||
|
|
5339c97b1e | ||
|
|
7406ac5db1 | ||
|
|
1c263152e3 | ||
|
|
ab211962e5 | ||
|
|
33a23c563c | ||
|
|
17304929b5 | ||
|
|
d88f02fc90 | ||
|
|
1c79742cc3 | ||
|
|
d696a16ac4 | ||
|
|
2c1962b207 | ||
|
|
23cdbe196d | ||
|
|
8b6760cd77 | ||
|
|
7c2317aca1 | ||
|
|
b764b14df1 | ||
|
|
fc1bd30bef | ||
|
|
fe29ec050c | ||
|
|
7fafb89d1d | ||
|
|
bcefe3b87f | ||
|
|
9e03f69e9b | ||
|
|
694a7415ab | ||
|
|
23b102c60c | ||
|
|
e165aa6cd4 | ||
|
|
8ed32e6c8e | ||
|
|
26da489773 | ||
|
|
9c73430ddd | ||
|
|
e712af11a5 | ||
|
|
45291cb16b | ||
|
|
9bfbb2e6b0 | ||
|
|
d6bf05c12c | ||
|
|
07a4cbde43 | ||
|
|
017dc5b6f1 | ||
|
|
ddcb6ba9f0 | ||
|
|
17cf323a52 | ||
|
|
a0b7d03c28 | ||
|
|
15cae8a474 | ||
|
|
49865e9b9e | ||
|
|
98d4504b6f | ||
|
|
597530d558 | ||
|
|
2165a8e986 | ||
|
|
cdb515e0c3 | ||
|
|
d55613e88c | ||
|
|
3c0bf3abed | ||
|
|
3179d1741e | ||
|
|
63c003db26 | ||
|
|
d01e51d862 | ||
|
|
92d868424c | ||
|
|
eb4c42b1e0 | ||
|
|
2968f51661 | ||
|
|
dbd3f7f6a5 | ||
|
|
2f5cec35ea | ||
|
|
706b8597b6 | ||
|
|
644aa21459 | ||
|
|
a77d8f3fac | ||
|
|
766d128a5d | ||
|
|
6f543ec762 | ||
|
|
d9102bae3b | ||
|
|
ca6189cc1e | ||
|
|
7e86aa9dc5 | ||
|
|
2ca5624aa0 | ||
|
|
19397782f7 | ||
|
|
284b951038 | ||
|
|
1df48772f4 | ||
|
|
939b52ae69 | ||
|
|
2403ff7233 | ||
|
|
b8d55e8dea | ||
|
|
5550cb02b7 | ||
|
|
492c70d156 | ||
|
|
c68ec17e0c | ||
|
|
b17aeda203 | ||
|
|
23d2fe4bbb | ||
|
|
f6a8b850e6 | ||
|
|
aac41faab9 | ||
|
|
f119f448ed | ||
|
|
44943bfab1 | ||
|
|
73d2c2f320 | ||
|
|
762968d24d | ||
|
|
d2672d818d | ||
|
|
17f44f93f8 | ||
|
|
73d90107b0 | ||
|
|
ed389591ac | ||
|
|
1867262cd9 | ||
|
|
9b21f983b3 | ||
|
|
3b729f08cb | ||
|
|
cdb3faa982 | ||
|
|
bb79311556 | ||
|
|
e881e18720 | ||
|
|
ba4520493a | ||
|
|
dc80ade21b | ||
|
|
6addd6d5b1 | ||
|
|
2576eb424e | ||
|
|
d80d87e28b | ||
|
|
a0ebfe99b4 | ||
|
|
2bc624c271 | ||
|
|
39608efe75 | ||
|
|
af1d6d25a4 | ||
|
|
5efe47cc34 | ||
|
|
aacbc0d92c | ||
|
|
241a5befd2 | ||
|
|
edab1d1e2a | ||
|
|
b55424bab6 | ||
|
|
11191e8d4c | ||
|
|
4263bae064 | ||
|
|
6f928575fa | ||
|
|
1aeafd41a7 | ||
|
|
3003b1129e | ||
|
|
bf9250141a | ||
|
|
3df21247a2 | ||
|
|
3b33a926ed | ||
|
|
dfddf1698d | ||
|
|
6308429b26 | ||
|
|
37e8b3242c | ||
|
|
bdc574d6ce | ||
|
|
85e3f3c051 | ||
|
|
ee2b4d22ac | ||
|
|
59a603f9b2 | ||
|
|
0332e49ac2 | ||
|
|
b1b564a533 | ||
|
|
444875ae38 | ||
|
|
a7ffe1c704 | ||
|
|
9f75737882 | ||
|
|
10d5790975 | ||
|
|
b571483c5f | ||
|
|
0a79016b0f | ||
|
|
3c4f24192a | ||
|
|
93b4dc5db8 | ||
|
|
d786c607b8 | ||
|
|
598c66894e | ||
|
|
75b4ea3863 | ||
|
|
5f66e381d2 | ||
|
|
4dd892297d | ||
|
|
51cb7a5674 | ||
|
|
cc0bf769c4 | ||
|
|
ad13440743 | ||
|
|
999a5846c4 | ||
|
|
9b5d12c8a1 | ||
|
|
d1bab7a3a4 | ||
|
|
e657bac266 | ||
|
|
0248661bb7 | ||
|
|
56fef8ec08 | ||
|
|
09ac68e674 | ||
|
|
19ad6e6514 | ||
|
|
41bb687608 | ||
|
|
d9cc3f9d7e | ||
|
|
6b19c8bbb5 | ||
|
|
09e57c3d97 | ||
|
|
2f3c67be1a | ||
|
|
f94af6cb73 | ||
|
|
1d370ada65 | ||
|
|
35c78ea6be | ||
|
|
ad83eb9575 | ||
|
|
8995e82627 | ||
|
|
94b521881b | ||
|
|
634a440def | ||
|
|
4dce58a457 | ||
|
|
99e9621590 | ||
|
|
e8fc583f19 | ||
|
|
03f63d391a | ||
|
|
6df403444c | ||
|
|
cb7953b503 | ||
|
|
774df8d0d4 | ||
|
|
36e1aa87e4 | ||
|
|
40532aca8e | ||
|
|
8592254b2d | ||
|
|
289f82a7e7 |
@@ -1,9 +1,9 @@
|
||||
name: Release
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
branches: ['**']
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -16,12 +16,8 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# Stamp the addon's TOC version from the tag (drops the leading `v`)
|
||||
# BEFORE CMake configure, so the embed step picks up the stamped TOC.
|
||||
# The DLL bundles AddOns/!!!ClassicAPI/ at build time — see the
|
||||
# `embed_addon` custom command in CMakeLists.txt and the file-read
|
||||
# hook in src/addons/Embedded.cpp.
|
||||
- name: Stamp embedded TOC version
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = '${{ github.ref_name }}' -replace '^v',''
|
||||
@@ -29,13 +25,26 @@ jobs:
|
||||
(Get-Content $toc) -replace '^## Version:.*', "## Version: $version" |
|
||||
Set-Content $toc
|
||||
|
||||
- name: Configure
|
||||
- name: Configure (tag)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: cmake -B build -A Win32 "-DCLASSICAPI_TAG=${{ github.ref_name }}"
|
||||
|
||||
- name: Configure (branch)
|
||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||
run: cmake -B build -A Win32
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --config Release
|
||||
|
||||
- name: Upload artifact
|
||||
if: "!startsWith(github.ref, 'refs/tags/v')"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ClassicAPI-${{ github.sha }}
|
||||
path: build/Release/ClassicAPI.dll
|
||||
|
||||
- name: Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: build/Release/ClassicAPI.dll
|
||||
@@ -1,26 +1,33 @@
|
||||
## Interface: 11200
|
||||
## Title: !!!ClassicAPI
|
||||
## Notes: Library to emulate aspects of the modern WoW API on 1.12.1 / Lua 5.0.
|
||||
## Version: 1.3
|
||||
## Version: DEV
|
||||
## Author: Brues
|
||||
## X-Website: https://github.com/brues-code/ClassicAPI
|
||||
|
||||
locales\locales.xml
|
||||
|
||||
GlobalStrings.lua
|
||||
Compat.lua
|
||||
Util\Print.lua
|
||||
Util\Constants.lua
|
||||
Util\String.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\Color.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
|
||||
@@ -28,4 +35,6 @@ Util\EquipmentManager.lua
|
||||
Util\PlayerUtil.lua
|
||||
Util\SlashCommandsRegistry.lua
|
||||
Util\LinkUtil.lua
|
||||
Util\TradeSkillLink.lua
|
||||
Util\IconDataProvider.lua
|
||||
Util\GameTooltip.lua
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
@@ -8,5 +8,34 @@ if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
unknownColor.colorStr = unknownColor:GenerateHexColor()
|
||||
return unknownColor
|
||||
end})
|
||||
|
||||
-- Compatibility with OLDER forks of pfUI. Our DLL adds the modern
|
||||
-- HookScript widget method; older pfUI forks branch on
|
||||
-- `if button.HookScript` and take a modern code path they were never
|
||||
-- written for on their pfActionBar buttons, which breaks them. While
|
||||
-- the actionbar module builds those buttons, temporarily wrap
|
||||
-- CreateFrame to shadow HookScript with a falsy field so those forks
|
||||
-- fall back to their vanilla SetScript path. The maintained "brues"
|
||||
-- fork handles HookScript itself, so it's detected via the TOC
|
||||
-- X-Website tag and left alone. The wrapper is removed as soon as the
|
||||
-- actionbar module finishes loading so it affects nothing else.
|
||||
local website = GetAddOnMetadata("pfUI", "X-Website")
|
||||
if not website or not strfind(website, 'brues') then
|
||||
local _createFrame = CreateFrame
|
||||
local HookedCreateFrame = function(frameType, name, parent, template)
|
||||
local frame = _createFrame(frameType, name, parent, template)
|
||||
if frameType == "Button" and strfind(name or "", "pfActionBar") then
|
||||
frame.HookScript = false
|
||||
end
|
||||
return frame
|
||||
end
|
||||
hooksecurefunc(pfUI, 'LoadModule', function(frame, moduleName)
|
||||
if moduleName == "updatenotify" then
|
||||
CreateFrame = HookedCreateFrame
|
||||
elseif moduleName == "actionbar" then
|
||||
CreateFrame = _createFrame
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
-- On-demand, throttled cache warmer (/warmcache). A brute-force scan of the
|
||||
-- whole ID space floods the client's network buffers and panics in
|
||||
-- WDataStoreSmallBuffer ("not enough memory") — the engine can't hold that
|
||||
-- many pending queries. So this warms a *bounded* range, started/stopped
|
||||
-- manually, at a low fixed rate. Tune WARM_RATE / WARM_INTERVAL if needed,
|
||||
-- but keep it modest. Note: IDs the server doesn't know (gaps in the range)
|
||||
-- get no response, so their pending entries linger — warm ranges you expect
|
||||
-- to be mostly real, not arbitrary millions.
|
||||
--
|
||||
-- /warmcache item 1 40000 warm item IDs 1..40000
|
||||
-- /warmcache <type> <a> <b> type = item | quest | creature | object | all
|
||||
-- /warmcache stop abort an in-progress warm
|
||||
local WARM_RATE = 20 -- IDs queued per tick
|
||||
local WARM_INTERVAL = 0.2 -- seconds between ticks (-> ~20 IDs/sec)
|
||||
|
||||
local WARM_FN = {
|
||||
item = function(id) C_Item.RequestLoadItemDataByID(id) end,
|
||||
quest = function(id) C_QuestLog.RequestLoadQuestByID(id) end,
|
||||
creature = function(id) C_CreatureInfo.RequestLoadCreatureByID(id) end,
|
||||
object = function(id) C_GameObjectInfo.RequestLoadGameObjectByID(id) end,
|
||||
all = function(id)
|
||||
C_QuestLog.RequestLoadQuestByID(id)
|
||||
C_Item.RequestLoadItemDataByID(id)
|
||||
C_CreatureInfo.RequestLoadCreatureByID(id)
|
||||
C_GameObjectInfo.RequestLoadGameObjectByID(id)
|
||||
end,
|
||||
}
|
||||
|
||||
local warm = { running = false, cancel = false }
|
||||
|
||||
local function warm_msg(s)
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccff[WarmCache]|r " .. s)
|
||||
end
|
||||
|
||||
local function warm_step(fn, id, stop, lastMsg)
|
||||
if warm.cancel then
|
||||
warm.running = false
|
||||
warm_msg("stopped at " .. (id - 1))
|
||||
return
|
||||
end
|
||||
local n = 0
|
||||
while n < WARM_RATE and id <= stop do
|
||||
fn(id)
|
||||
n = n + 1
|
||||
id = id + 1
|
||||
end
|
||||
if id > stop then
|
||||
warm.running = false
|
||||
warm_msg("done (through " .. stop .. ")")
|
||||
else
|
||||
if (id - 1) - lastMsg >= 1000 then
|
||||
warm_msg((id - 1) .. " / " .. stop .. " ...")
|
||||
lastMsg = id - 1
|
||||
end
|
||||
C_Timer.After(WARM_INTERVAL, function() warm_step(fn, id, stop, lastMsg) end)
|
||||
end
|
||||
end
|
||||
|
||||
SLASH_WARMCACHE1 = "/warmcache"
|
||||
SlashCmdList["WARMCACHE"] = function(msg)
|
||||
local args = {}
|
||||
for tok in string.gmatch(msg or "", "%S+") do
|
||||
table.insert(args, string.lower(tok))
|
||||
end
|
||||
local cmd = args[1]
|
||||
|
||||
if cmd == "stop" then
|
||||
if warm.running then warm.cancel = true else warm_msg("not running") end
|
||||
return
|
||||
end
|
||||
if warm.running then
|
||||
warm_msg("already running - /warmcache stop first")
|
||||
return
|
||||
end
|
||||
|
||||
local fn = WARM_FN[cmd]
|
||||
local a, b = tonumber(args[2]), tonumber(args[3])
|
||||
if not fn or not a or not b or a < 1 or b < a then
|
||||
warm_msg("usage: /warmcache item|quest|creature|object|all <startID> <stopID>")
|
||||
warm_msg(" /warmcache stop")
|
||||
return
|
||||
end
|
||||
|
||||
warm.running, warm.cancel = true, false
|
||||
warm_msg(string.format("warming %s %d..%d (~%d/sec, /warmcache stop to abort)",
|
||||
cmd, a, b, math.floor(WARM_RATE / WARM_INTERVAL)))
|
||||
warm_step(fn, a, b, a - 1)
|
||||
end
|
||||
@@ -10,11 +10,14 @@ function ColorMixin:OnLoad(r, g, b, a)
|
||||
self:SetRGBA(r, g, b, a)
|
||||
end
|
||||
|
||||
function ColorMixin:IsEqualTo(otherColor)
|
||||
function ColorMixin:IsRGBEqualTo(otherColor)
|
||||
return self.r == otherColor.r
|
||||
and self.g == otherColor.g
|
||||
and self.b == otherColor.b
|
||||
and self.a == otherColor.a
|
||||
end
|
||||
|
||||
function ColorMixin:IsEqualTo(otherColor)
|
||||
return self:IsRGBEqualTo(otherColor) and self.a == otherColor.a
|
||||
end
|
||||
|
||||
function ColorMixin:GetRGB()
|
||||
@@ -52,6 +55,39 @@ function ColorMixin:GenerateHexColorMarkup()
|
||||
return "|c"..self:GenerateHexColor()
|
||||
end
|
||||
|
||||
function ColorMixin:GenerateHexColorNoAlpha()
|
||||
return string.format("%.2x%.2x%.2x", self:GetRGBAsBytes())
|
||||
end
|
||||
|
||||
function ColorMixin:GetHSL()
|
||||
local r, g, b, a = self.r, self.g, self.b, self.a
|
||||
local max, min = math.max(r, g, b), math.min(r, g, b)
|
||||
local h, s, l
|
||||
l = (max + min) / 2
|
||||
if max == min then
|
||||
h, s = 0, 0 -- achromatic
|
||||
else
|
||||
local d = max - min
|
||||
if l > 0.5 then
|
||||
s = d / (2 - max - min)
|
||||
else
|
||||
s = d / (max + min)
|
||||
end
|
||||
if max == r then
|
||||
h = (g - b) / d
|
||||
if g < b then
|
||||
h = h + 6
|
||||
end
|
||||
elseif max == g then
|
||||
h = (b - r) / d + 2
|
||||
elseif max == b then
|
||||
h = (r - g) / d + 4
|
||||
end
|
||||
h = h / 6
|
||||
end
|
||||
return h, s, l, a or 1
|
||||
end
|
||||
|
||||
function ColorMixin:WrapTextInColorCode(text)
|
||||
return WrapTextInColorCode(text, self:GenerateHexColor())
|
||||
end
|
||||
@@ -60,6 +96,10 @@ function WrapTextInColorCode(text, colorHexString)
|
||||
return string.format("|c%s%s|r", colorHexString, text)
|
||||
end
|
||||
|
||||
function WrapTextInColor(text, color)
|
||||
return WrapTextInColorCode(text, color:GenerateHexColor())
|
||||
end
|
||||
|
||||
function ColorMixin:WrapTextInColorTableCode(text)
|
||||
return WrapTextInColorCode(text, self:GenerateHexColor())
|
||||
end
|
||||
|
||||
@@ -20,6 +20,24 @@ function CreateColorFromHexString(hexColor)
|
||||
end
|
||||
end
|
||||
|
||||
function CreateColorFromRGBAHexString(hexColor)
|
||||
if string.len(hexColor) == 8 then
|
||||
local r, g, b, a = ExtractColorValueFromHex(hexColor, 1), ExtractColorValueFromHex(hexColor, 3), ExtractColorValueFromHex(hexColor, 5), ExtractColorValueFromHex(hexColor, 7);
|
||||
return CreateColor(r, g, b, a);
|
||||
else
|
||||
error("CreateColorFromRGBAHexString input must be hexadecimal digits in this format: RRGGBBAA.", 2);
|
||||
end
|
||||
end
|
||||
|
||||
function CreateColorFromRGBHexString(hexColor)
|
||||
if string.len(hexColor) == 6 then
|
||||
local r, g, b = ExtractColorValueFromHex(hexColor, 1), ExtractColorValueFromHex(hexColor, 3), ExtractColorValueFromHex(hexColor, 5);
|
||||
return CreateColor(r, g, b, 1);
|
||||
else
|
||||
error("CreateColorFromRGBHexString input must be hexadecimal digits in this format: RRGGBB.", 2);
|
||||
end
|
||||
end
|
||||
|
||||
function CreateColorFromBytes(r, g, b, a)
|
||||
return CreateColor(r / 255, g / 255, b / 255, a / 255);
|
||||
end
|
||||
@@ -31,6 +49,10 @@ function AreColorsEqual(left, right)
|
||||
return left == right;
|
||||
end
|
||||
|
||||
function IsRGBAEqualToColor(r, g, b, a, color)
|
||||
return (color.r == r) and (color.g == g) and (color.b == b) and (color.a == a);
|
||||
end
|
||||
|
||||
function GetClassColor(classFilename)
|
||||
local color = RAID_CLASS_COLORS[classFilename];
|
||||
if color then
|
||||
@@ -46,7 +68,23 @@ end
|
||||
function GetClassColoredTextForUnit(unit, text)
|
||||
local _, classFilename = UnitClass(unit);
|
||||
local color = GetClassColorObj(classFilename);
|
||||
if (color) then
|
||||
if (color) then
|
||||
return color:WrapTextInColorCode(text);
|
||||
end
|
||||
end
|
||||
|
||||
function GetFactionColor(factionGroupTag)
|
||||
-- Both PLAYER_FACTION_GROUP (tag -> index) and PLAYER_FACTION_COLORS
|
||||
-- (index -> color) live in our Constants.lua. Guarded defensively in
|
||||
-- case a client variant leaves one unset.
|
||||
local group = PLAYER_FACTION_GROUP[factionGroupTag];
|
||||
return PLAYER_FACTION_COLORS and PLAYER_FACTION_COLORS[group];
|
||||
end
|
||||
|
||||
function RGBToColorCode(r, g, b)
|
||||
return string.format("|cff%.2x%.2x%.2x", r * 255, g * 255, b * 255);
|
||||
end
|
||||
|
||||
function RGBTableToColorCode(rgbTable)
|
||||
return RGBToColorCode(rgbTable.r, rgbTable.g, rgbTable.b);
|
||||
end
|
||||
|
||||
@@ -27,13 +27,12 @@ INVSLOT_BACK = 15;
|
||||
INVSLOT_MAINHAND = 16;
|
||||
INVSLOT_OFFHAND = 17;
|
||||
INVSLOT_RANGED = 18;
|
||||
INVSLOT_TABARD = 19;
|
||||
INVSLOT_LAST_EQUIPPED = INVSLOT_TABARD;
|
||||
INVSLOT_TABARD = 19; INVSLOT_LAST_EQUIPPED = INVSLOT_TABARD;
|
||||
|
||||
INVSLOTS_EQUIPABLE_IN_COMBAT = {
|
||||
[INVSLOT_MAINHAND] = true,
|
||||
[INVSLOT_OFFHAND] = true,
|
||||
[INVSLOT_RANGED] = true,
|
||||
[INVSLOT_MAINHAND] = true,
|
||||
[INVSLOT_OFFHAND] = true,
|
||||
[INVSLOT_RANGED] = true,
|
||||
}
|
||||
|
||||
-- Item IDs
|
||||
@@ -106,20 +105,24 @@ BAG_ITEM_QUALITY_COLORS =
|
||||
};
|
||||
|
||||
-- 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,
|
||||
};
|
||||
|
||||
WOW_PROJECT_MAINLINE = 1;
|
||||
WOW_PROJECT_CLASSIC = 2;
|
||||
WOW_PROJECT_BURNING_CRUSADE_CLASSIC = 5;
|
||||
WOW_PROJECT_WRATH_CLASSIC = 11;
|
||||
WOW_PROJECT_CATACLYSM_CLASSIC = 14;
|
||||
WOW_PROJECT_MISTS_CLASSIC = 19;
|
||||
WOW_PROJECT_ID = WOW_PROJECT_CLASSIC;
|
||||
|
||||
--
|
||||
-- Glyph
|
||||
--
|
||||
|
||||
@@ -83,12 +83,11 @@ function EventUtil.TriggerOnVariablesLoaded()
|
||||
end
|
||||
|
||||
function EventUtil.ContinueOnAddOnLoaded(addOnName, callback)
|
||||
-- 1.12's IsAddOnLoaded returns a single bool. 3.3.5 split that into
|
||||
-- (isLoadedOrLoading, isLoaded); treat truthy as "ready to use".
|
||||
if IsAddOnLoaded(addOnName) then
|
||||
callback()
|
||||
return
|
||||
end
|
||||
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
|
||||
if isLoaded then
|
||||
callback();
|
||||
return;
|
||||
end
|
||||
|
||||
EventUtil.RegisterOnceFrameEventAndCallback("ADDON_LOADED", callback, addOnName)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
-- Backport of FrameXML/FormattingUtil.lua (feasible subset) to vanilla
|
||||
-- 1.12 / Lua 5.0.
|
||||
--
|
||||
-- 5.0 notes: no `#`/`%`/string-method syntax — uses string.*, math.mod.
|
||||
-- The currency/cost helpers (C_CurrencyInfo etc.) are omitted. GetMoneyString
|
||||
-- uses text symbols, not coin texture markup (markup doesn't render in 1.12).
|
||||
--
|
||||
-- The FrameXML format-string globals these functions consume
|
||||
-- (LARGE_NUMBER_SEPERATOR, PERCENTAGE_STRING, GOLD_AMOUNT_SYMBOL, …) are
|
||||
-- defined in the locale layer (locales/enUS.lua), which loads first.
|
||||
|
||||
COPPER_PER_SILVER = 100;
|
||||
SILVER_PER_GOLD = 100;
|
||||
COPPER_PER_GOLD = COPPER_PER_SILVER * SILVER_PER_GOLD;
|
||||
|
||||
function FormatLargeNumber(amount)
|
||||
amount = tostring(amount);
|
||||
local newDisplay = "";
|
||||
local strlen = string.len(amount);
|
||||
-- Insert a separator before each group of three trailing digits.
|
||||
for i = 4, strlen, 3 do
|
||||
newDisplay = LARGE_NUMBER_SEPERATOR ..
|
||||
string.sub(amount, -(i - 1), -(i - 3)) .. newDisplay;
|
||||
end
|
||||
-- Prepend everything before the first separator.
|
||||
local lead = (math.mod(strlen, 3) == 0) and 3 or math.mod(strlen, 3);
|
||||
newDisplay = string.sub(amount, 1, lead) .. newDisplay;
|
||||
return newDisplay;
|
||||
end
|
||||
|
||||
function FormatPercentage(percentage, roundToNearestInteger)
|
||||
if roundToNearestInteger then
|
||||
percentage = Round(percentage * 100);
|
||||
else
|
||||
percentage = percentage * 100;
|
||||
end
|
||||
return string.format(PERCENTAGE_STRING, percentage);
|
||||
end
|
||||
|
||||
function FormatFraction(numerator, denominator)
|
||||
return string.format(GENERIC_FRACTION_STRING, numerator, denominator);
|
||||
end
|
||||
|
||||
function SplitTextIntoLines(text, delimiter)
|
||||
local lines = {};
|
||||
local startIndex = 1;
|
||||
local foundIndex = string.find(text, delimiter);
|
||||
while foundIndex do
|
||||
table.insert(lines, string.sub(text, startIndex, foundIndex - 1));
|
||||
startIndex = foundIndex + 2;
|
||||
foundIndex = string.find(text, delimiter, startIndex);
|
||||
end
|
||||
if startIndex <= string.len(text) then
|
||||
table.insert(lines, string.sub(text, startIndex));
|
||||
end
|
||||
return lines;
|
||||
end
|
||||
|
||||
function SplitTextIntoHeaderAndNonHeader(text)
|
||||
local foundIndex = string.find(text, "|n");
|
||||
local len = string.len(text);
|
||||
if not foundIndex then
|
||||
return text; -- no newline: whole thing is a header
|
||||
elseif len == 2 then
|
||||
return nil; -- the string was just a newline
|
||||
elseif foundIndex == 1 then
|
||||
return string.sub(text, 3); -- leading newline: rest is a header
|
||||
elseif foundIndex == len - 1 then
|
||||
return string.sub(text, 1, foundIndex - 1); -- trailing newline
|
||||
else
|
||||
return string.sub(text, 1, foundIndex - 1), string.sub(text, foundIndex + 2);
|
||||
end
|
||||
end
|
||||
|
||||
-- (money, separateThousands [, checkGoldThreshold, showZeroAsGold])
|
||||
-- Text-symbol form ("12g 34s 56c"); the coin-texture form from the
|
||||
-- reference is dropped since texture markup doesn't render in 1.12. The
|
||||
-- checkGoldThreshold arg is accepted for signature compatibility but not
|
||||
-- applied (it relied on modern Constants tables).
|
||||
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold)
|
||||
local gold = math.floor(money / COPPER_PER_GOLD);
|
||||
local silver = math.floor(math.mod(money, COPPER_PER_GOLD) / COPPER_PER_SILVER);
|
||||
local copper = math.mod(money, COPPER_PER_SILVER);
|
||||
|
||||
local goldString = (separateThousands and FormatLargeNumber(gold) or tostring(gold)) ..
|
||||
GOLD_AMOUNT_SYMBOL;
|
||||
local silverString = silver .. SILVER_AMOUNT_SYMBOL;
|
||||
local copperString = copper .. COPPER_AMOUNT_SYMBOL;
|
||||
|
||||
if showZeroAsGold and money == 0 then
|
||||
return goldString;
|
||||
end
|
||||
|
||||
local moneyString = "";
|
||||
local separator = "";
|
||||
if gold > 0 then
|
||||
moneyString = goldString;
|
||||
separator = " ";
|
||||
end
|
||||
if silver > 0 then
|
||||
moneyString = moneyString .. separator .. silverString;
|
||||
separator = " ";
|
||||
end
|
||||
if copper > 0 or moneyString == "" then
|
||||
moneyString = moneyString .. separator .. copperString;
|
||||
end
|
||||
return moneyString;
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
-- Backport of FrameXML/FrameUtil.lua (core subset) to vanilla 1.12.
|
||||
--
|
||||
-- Only the dependency-light, commonly-called helpers are ported. Omitted:
|
||||
-- - the fade/flash functions (UIFrameFade*/UIFrameFlash*) are already
|
||||
-- native in 1.12.
|
||||
-- - RegisterFrameForUnitEvents (no RegisterUnitEvent in 1.12), and the
|
||||
-- scale/layout/secure/EventRegistry helpers (rely on APIs absent here).
|
||||
|
||||
FrameUtil = FrameUtil or {};
|
||||
|
||||
function FrameUtil.RegisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.UnregisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:UnregisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.GetRootParent(frame)
|
||||
local parent = frame:GetParent();
|
||||
while parent do
|
||||
local nextParent = parent:GetParent();
|
||||
if not nextParent then
|
||||
break;
|
||||
end
|
||||
parent = nextParent;
|
||||
end
|
||||
return parent;
|
||||
end
|
||||
|
||||
-- Reparents while preserving the frame's render layering.
|
||||
function FrameUtil.SetParentMaintainRenderLayering(frame, parent)
|
||||
local origStrata = frame:GetFrameStrata();
|
||||
local origFrameLevel = frame:GetFrameLevel();
|
||||
frame:SetParent(parent);
|
||||
frame:SetFrameStrata(origStrata);
|
||||
frame:SetFrameLevel(origFrameLevel);
|
||||
end
|
||||
|
||||
function DoesAncestryInclude(ancestry, frame)
|
||||
if ancestry then
|
||||
local currentFrame = frame;
|
||||
while currentFrame do
|
||||
if currentFrame == ancestry then
|
||||
return true;
|
||||
end
|
||||
currentFrame = currentFrame:GetParent();
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function DoesAncestryIncludeAny(ancestry, frames)
|
||||
for _, frame in ipairs(frames) do
|
||||
if DoesAncestryInclude(ancestry, frame) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
@@ -0,0 +1,92 @@
|
||||
function GameTooltip_Hide()
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
|
||||
GameTooltip.shoppingTooltips = GameTooltip.shoppingTooltips or { ShoppingTooltip1, ShoppingTooltip2 }
|
||||
|
||||
function GameTooltip_ShowCompareItem(self, shift)
|
||||
if ( not self ) then
|
||||
self = GameTooltip;
|
||||
end
|
||||
local item, link = self:GetItem();
|
||||
link = link or self.compareLink
|
||||
if ( not link ) then
|
||||
return;
|
||||
end
|
||||
|
||||
local shoppingTooltip1, shoppingTooltip2 = unpack(self.shoppingTooltips or { ShoppingTooltip1, ShoppingTooltip2 });
|
||||
|
||||
local SEPARATION = 6;
|
||||
local backdrop = shoppingTooltip1.GetBackdrop and shoppingTooltip1:GetBackdrop();
|
||||
local GAP = SEPARATION + ((type(backdrop) == "table" and backdrop.edgeSize) or 0);
|
||||
|
||||
local item1 = nil;
|
||||
local item2 = nil;
|
||||
local side = "left";
|
||||
if ( shoppingTooltip1:SetHyperlinkCompareItem(link, 1, shift, self) ) then
|
||||
item1 = true;
|
||||
end
|
||||
if ( shoppingTooltip2:SetHyperlinkCompareItem(link, 2, shift, self) ) then
|
||||
item2 = true;
|
||||
end
|
||||
|
||||
-- find correct side
|
||||
local rightDist = 0;
|
||||
local leftPos = self:GetLeft();
|
||||
local rightPos = self:GetRight();
|
||||
if ( not rightPos ) then
|
||||
rightPos = 0;
|
||||
end
|
||||
if ( not leftPos ) then
|
||||
leftPos = 0;
|
||||
end
|
||||
|
||||
rightDist = GetScreenWidth() - rightPos;
|
||||
|
||||
if (leftPos and (rightDist < leftPos)) then
|
||||
side = "left";
|
||||
else
|
||||
side = "right";
|
||||
end
|
||||
|
||||
-- see if we should slide the tooltip
|
||||
if ( self:GetAnchorType() and self:GetAnchorType() ~= "ANCHOR_PRESERVE" ) then
|
||||
local totalWidth = 0;
|
||||
if ( item1 ) then
|
||||
totalWidth = totalWidth + shoppingTooltip1:GetWidth();
|
||||
end
|
||||
if ( item2 ) then
|
||||
totalWidth = totalWidth + shoppingTooltip2:GetWidth();
|
||||
end
|
||||
|
||||
if ( (side == "left") and (totalWidth > leftPos) ) then
|
||||
self:SetAnchorType(self:GetAnchorType(), (totalWidth - leftPos), 0);
|
||||
elseif ( (side == "right") and (rightPos + totalWidth) > GetScreenWidth() ) then
|
||||
self:SetAnchorType(self:GetAnchorType(), -((rightPos + totalWidth) - GetScreenWidth()), 0);
|
||||
end
|
||||
end
|
||||
|
||||
if ( item1 ) then
|
||||
shoppingTooltip1:SetOwner(self, "ANCHOR_NONE");
|
||||
shoppingTooltip1:ClearAllPoints();
|
||||
if ( side and side == "left" ) then
|
||||
shoppingTooltip1:SetPoint("TOPRIGHT", self, "TOPLEFT", -GAP, -10);
|
||||
else
|
||||
shoppingTooltip1:SetPoint("TOPLEFT", self, "TOPRIGHT", GAP, -10);
|
||||
end
|
||||
shoppingTooltip1:SetHyperlinkCompareItem(link, 1, shift, self);
|
||||
shoppingTooltip1:Show();
|
||||
|
||||
if ( item2 ) then
|
||||
shoppingTooltip2:SetOwner(shoppingTooltip1, "ANCHOR_NONE");
|
||||
shoppingTooltip2:ClearAllPoints();
|
||||
if ( side and side == "left" ) then
|
||||
shoppingTooltip2:SetPoint("TOPRIGHT", shoppingTooltip1, "TOPLEFT", -GAP, 0);
|
||||
else
|
||||
shoppingTooltip2:SetPoint("TOPLEFT", shoppingTooltip1, "TOPRIGHT", GAP, 0);
|
||||
end
|
||||
shoppingTooltip2:SetHyperlinkCompareItem(link, 2, shift, self);
|
||||
shoppingTooltip2:Show();
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -15,8 +15,6 @@
|
||||
-- - 1.12 has no concept of an item GUID and no public item-lock API,
|
||||
-- so those return nil / no-op.
|
||||
|
||||
C_Item.GetItemInfo = GetItemInfo
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Item = {}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
-- Backport of FrameXML/PixelUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- PixelUtil snaps frame sizes/points to whole physical pixels so
|
||||
-- borders and textures stay crisp at fractional UI scales. The table
|
||||
-- ports verbatim — its only math deps (Round, Lerp,
|
||||
-- ClampedPercentageBetween) all live in MathUtil, which loads first.
|
||||
--
|
||||
-- The one piece vanilla lacks is the native `GetPhysicalScreenSize()`
|
||||
-- (added in 7.0). The ClassicAPI DLL provides it (see
|
||||
-- src/screen/Info.cpp) by reading the engine's `gxResolution` CVar, so
|
||||
-- it's a real global registered at engine boot — no Lua polyfill
|
||||
-- needed here.
|
||||
|
||||
PixelUtil = {};
|
||||
|
||||
function PixelUtil.GetPixelToUIUnitFactor()
|
||||
local physicalWidth, physicalHeight = GetPhysicalScreenSize();
|
||||
return 768.0 / physicalHeight;
|
||||
end
|
||||
|
||||
function PixelUtil.GetNearestPixelSize(uiUnitSize, layoutScale, minPixels)
|
||||
if uiUnitSize == 0 and (not minPixels or minPixels == 0) then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local uiUnitFactor = PixelUtil.GetPixelToUIUnitFactor();
|
||||
local numPixels = Round((uiUnitSize * layoutScale) / uiUnitFactor);
|
||||
if minPixels then
|
||||
if uiUnitSize < 0.0 then
|
||||
if numPixels > -minPixels then
|
||||
numPixels = -minPixels;
|
||||
end
|
||||
else
|
||||
if numPixels < minPixels then
|
||||
numPixels = minPixels;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return numPixels * uiUnitFactor / layoutScale;
|
||||
end
|
||||
|
||||
function PixelUtil.ConvertPixelsToUI(desiredPixels, layoutScale)
|
||||
return PixelUtil.GetNearestPixelSize(desiredPixels, layoutScale);
|
||||
end
|
||||
|
||||
function PixelUtil.ConvertPixelsToUIForRegion(desiredPixels, region)
|
||||
return PixelUtil.GetNearestPixelSize(desiredPixels, region:GetEffectiveScale());
|
||||
end
|
||||
|
||||
function PixelUtil.SetWidth(region, width, minPixels)
|
||||
region:SetWidth(PixelUtil.GetNearestPixelSize(width, region:GetEffectiveScale(), minPixels));
|
||||
end
|
||||
|
||||
function PixelUtil.SetHeight(region, height, minPixels)
|
||||
region:SetHeight(PixelUtil.GetNearestPixelSize(height, region:GetEffectiveScale(), minPixels));
|
||||
end
|
||||
|
||||
function PixelUtil.SetSize(region, width, height, minWidthPixels, minHeightPixels)
|
||||
PixelUtil.SetWidth(region, width, minWidthPixels);
|
||||
PixelUtil.SetHeight(region, height, minHeightPixels);
|
||||
end
|
||||
|
||||
function PixelUtil.SetPoint(region, point, relativeTo, relativePoint, offsetX, offsetY, minOffsetXPixels, minOffsetYPixels)
|
||||
region:SetPoint(point, relativeTo, relativePoint,
|
||||
PixelUtil.GetNearestPixelSize(offsetX, region:GetEffectiveScale(), minOffsetXPixels),
|
||||
PixelUtil.GetNearestPixelSize(offsetY, region:GetEffectiveScale(), minOffsetYPixels)
|
||||
);
|
||||
end
|
||||
|
||||
function PixelUtil.SetStatusBarValue(statusBar, value)
|
||||
local width = statusBar:GetWidth();
|
||||
if width and width > 0.0 then
|
||||
local min, max = statusBar:GetMinMaxValues();
|
||||
local percent = ClampedPercentageBetween(value, min, max);
|
||||
if percent == 0.0 or percent == 1.0 then
|
||||
statusBar:SetValue(value);
|
||||
else
|
||||
local numPixels = PixelUtil.GetNearestPixelSize(statusBar:GetWidth() * percent, statusBar:GetEffectiveScale());
|
||||
local roundedValue = Lerp(min, max, numPixels / width);
|
||||
statusBar:SetValue(roundedValue);
|
||||
end
|
||||
else
|
||||
statusBar:SetValue(value);
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,228 @@
|
||||
-- Backport of the modern `PlayerLocation` / `PlayerLocationMixin` parameter
|
||||
-- object (Blizzard_ObjectAPI). Like `ItemLocation`, it's a lightweight
|
||||
-- descriptor that wraps one of several ways to identify a player — a GUID, a
|
||||
-- unit token, a chat line, a /who index, a battlefield-scoreboard index, etc.
|
||||
-- — so retail/Classic addon code can construct and inspect it unchanged.
|
||||
--
|
||||
-- It's pure table bookkeeping: every Create/Set/Get/Is is just fields, no
|
||||
-- engine access. The GUID / unit / chatLine / whoIndex / battlefieldScore
|
||||
-- kinds all map to things vanilla 1.12 genuinely has. The community, voice,
|
||||
-- and Battle.net kinds have no vanilla producer, but they're kept as harmless
|
||||
-- data holders so the mixin matches retail exactly for any code that builds
|
||||
-- or reads them. Only the two methods that reach into systems 1.12 lacks —
|
||||
-- `IsValid` (Club) and `IsBattleNetGUID` (account info) — are guarded so they
|
||||
-- degrade gracefully instead of erroring on the missing `C_Club` /
|
||||
-- `C_AccountInfo` namespaces.
|
||||
|
||||
local CreateFromMixins = CreateFromMixins
|
||||
|
||||
PlayerLocation = PlayerLocation or {}
|
||||
PlayerLocationMixin = PlayerLocationMixin or {}
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromGUID(guid)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetGUID(guid)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromUnit(unit)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetUnit(unit)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromChatLineID(lineID)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetChatLineID(lineID)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromBattlefieldScoreIndex(battlefieldScoreIndex)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetBattlefieldScoreIndex(battlefieldScoreIndex)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromWhoIndex(whoIndex)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetWhoIndex(whoIndex)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromVoiceID(memberID, channelID)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetVoiceID(memberID, channelID)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromBattleNetID(battleNetID)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetBattleNetID(battleNetID)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromCommunityChatData(clubID, streamID, epoch, position)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetCommunityData(clubID, streamID, epoch, position)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[static]] function PlayerLocation:CreateFromCommunityInvitation(clubID, guid)
|
||||
local playerLocation = CreateFromMixins(PlayerLocationMixin)
|
||||
playerLocation:SetCommunityInvitation(clubID, guid)
|
||||
return playerLocation
|
||||
end
|
||||
|
||||
--[[public api]]
|
||||
function PlayerLocationMixin:SetGUID(guid)
|
||||
self:ClearAndSetField("guid", guid)
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsValid()
|
||||
if self:IsCommunityData() then
|
||||
-- Vanilla has no Community/Club system; if the namespace is
|
||||
-- absent a community location can never be resolved.
|
||||
if C_Club and C_Club.CanResolvePlayerLocationFromClubMessageData then
|
||||
return C_Club.CanResolvePlayerLocationFromClubMessageData(self.communityClubID, self.communityStreamID, self.communityEpoch, self.communityPosition)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsGUID()
|
||||
return self.guid ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsBattleNetGUID()
|
||||
-- No Battle.net accounts on 1.12; guarded so it returns false instead
|
||||
-- of erroring on a missing C_AccountInfo.
|
||||
if self.guid and C_AccountInfo and C_AccountInfo.IsGUIDBattleNetAccountType then
|
||||
return C_AccountInfo.IsGUIDBattleNetAccountType(self.guid)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetGUID()
|
||||
return self.guid or self.communityClubInviterGUID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetUnit(unit)
|
||||
self:ClearAndSetField("unit", unit)
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsUnit()
|
||||
return self.unit ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetUnit()
|
||||
return self.unit
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetChatLineID(lineID)
|
||||
self:ClearAndSetField("chatLineID", lineID)
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsChatLineID()
|
||||
return self.chatLineID ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetChatLineID()
|
||||
return self.chatLineID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetBattlefieldScoreIndex(index)
|
||||
self:ClearAndSetField("battlefieldScoreIndex", index)
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsBattlefieldScoreIndex()
|
||||
return self.battlefieldScoreIndex ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetBattlefieldScoreIndex()
|
||||
return self.battlefieldScoreIndex
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetWhoIndex(whoIndex)
|
||||
self:ClearAndSetField("whoIndex", whoIndex)
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsWhoIndex()
|
||||
return self.whoIndex ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetWhoIndex()
|
||||
return self.whoIndex
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetVoiceID(memberID, channelID)
|
||||
self:Clear()
|
||||
self.voiceMemberID = memberID
|
||||
self.voiceChannelID = channelID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsVoiceID()
|
||||
return self.voiceMemberID ~= nil and self.voiceChannelID ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetVoiceID()
|
||||
return self.voiceMemberID, self.voiceChannelID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetBattleNetID(battleNetID)
|
||||
self:Clear()
|
||||
self.battleNetID = battleNetID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsBattleNetID()
|
||||
return self.battleNetID ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:GetBattleNetID()
|
||||
return self.battleNetID
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetCommunityData(clubID, streamID, epoch, position)
|
||||
self:Clear()
|
||||
self.communityClubID = clubID
|
||||
self.communityStreamID = streamID
|
||||
self.communityEpoch = epoch
|
||||
self.communityPosition = position
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsCommunityData()
|
||||
return self.communityClubID ~= nil and self.communityStreamID ~= nil and self.communityEpoch ~= nil and self.communityPosition ~= nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:SetCommunityInvitation(clubID, guid)
|
||||
self:Clear()
|
||||
self.communityClubID = clubID
|
||||
self.communityClubInviterGUID = guid
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:IsCommunityInvitation()
|
||||
return self.communityClubID ~= nil and self.communityClubInviterGUID ~= nil
|
||||
end
|
||||
|
||||
--[[private api]]
|
||||
function PlayerLocationMixin:Clear()
|
||||
self.guid = nil
|
||||
self.unit = nil
|
||||
self.chatLineID = nil
|
||||
self.battlefieldScoreIndex = nil
|
||||
self.whoIndex = nil
|
||||
self.voiceMemberID = nil
|
||||
self.voiceChannelID = nil
|
||||
self.communityClubID = nil
|
||||
self.communityStreamID = nil
|
||||
self.communityEpoch = nil
|
||||
self.communityPosition = nil
|
||||
self.communityClubInviterGUID = nil
|
||||
self.battleNetID = nil
|
||||
end
|
||||
|
||||
function PlayerLocationMixin:ClearAndSetField(fieldName, field)
|
||||
self:Clear()
|
||||
self[fieldName] = field
|
||||
end
|
||||
@@ -1,5 +1,4 @@
|
||||
local className = UnitClass('player')
|
||||
local classFile, classID = UnitClassBase('player')
|
||||
local className, classFile, classID = UnitClass('player'), UnitClassBase('player')
|
||||
|
||||
PlayerUtil = PlayerUtil or {}
|
||||
|
||||
@@ -33,7 +32,7 @@ function PlayerUtil.GetCurrentSpeed()
|
||||
end
|
||||
|
||||
function GetPlayerGuid()
|
||||
return UnitGUID("player");
|
||||
return UnitGUID("player")
|
||||
end
|
||||
|
||||
function IsPlayerGuid(guid)
|
||||
@@ -43,4 +42,4 @@ end
|
||||
function GetNameAndServerNameFromGUID(unitGUID)
|
||||
local _, _, _, _, _, name = GetPlayerInfoByGUID(unitGUID);
|
||||
return name, GetRealmName()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,4 +4,22 @@ function RegisterNewSlashCommand(callback, command, commandAlias)
|
||||
_G["SLASH_"..name.."1"] = "/"..command;
|
||||
_G["SLASH_"..name.."2"] = "/"..commandAlias;
|
||||
SlashCmdList[name] = callback;
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["FOCUS"] = function(msg)
|
||||
if msg == "" then
|
||||
FocusUnit();
|
||||
else
|
||||
FocusUnit(msg);
|
||||
end
|
||||
end
|
||||
|
||||
SlashCmdList["CLEARFOCUS"] = function(msg)
|
||||
ClearFocus()
|
||||
end
|
||||
|
||||
SlashCmdList["EQUIP_SET"] = function(msg)
|
||||
if msg ~= "" then
|
||||
C_EquipmentSet.UseEquipmentSet(C_EquipmentSet.GetEquipmentSetID(msg))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
function string.match(s, pattern, init)
|
||||
if s == nil then return nil end
|
||||
local r1, r2, c1, c2, c3, c4, c5, c6, c7, c8, c9 =
|
||||
string.find(s, pattern, init)
|
||||
if r1 == nil then return nil end
|
||||
if c1 ~= nil then
|
||||
return c1, c2, c3, c4, c5, c6, c7, c8, c9
|
||||
end
|
||||
return string.sub(s, r1, r2)
|
||||
end
|
||||
|
||||
string.gmatch = string.gfind
|
||||
@@ -65,6 +65,18 @@ function CreateTableReverseEnumerator(tbl, minIndex, maxIndex)
|
||||
return Enumerator, tbl, maxIndex
|
||||
end
|
||||
|
||||
function tDeleteItem(tbl, item)
|
||||
local size = tGetN(tbl);
|
||||
local index = size;
|
||||
while index > 0 do
|
||||
if item == tbl[index] then
|
||||
tRemove(tbl, index);
|
||||
end
|
||||
index = index - 1;
|
||||
end
|
||||
return size - tGetN(tbl);
|
||||
end
|
||||
|
||||
function tIndexOf(tbl, item)
|
||||
for i, v in ipairs(tbl) do
|
||||
if item == v then
|
||||
@@ -73,6 +85,15 @@ function tIndexOf(tbl, item)
|
||||
end
|
||||
end
|
||||
|
||||
function tContains(tbl, item)
|
||||
for k, v in pairs(tbl) do
|
||||
if item == v then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function TableUtil.ContainsAllKeys(lhsTable, rhsTable)
|
||||
for key, _ in pairs(lhsTable) do
|
||||
if rhsTable[key] == nil then
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
SECONDS_PER_MIN = 60;
|
||||
SECONDS_PER_HOUR = 60 * SECONDS_PER_MIN;
|
||||
SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;
|
||||
SECONDS_PER_MONTH = 30 * SECONDS_PER_DAY;
|
||||
SECONDS_PER_YEAR = 12 * SECONDS_PER_MONTH;
|
||||
|
||||
function SecondsToMinutes(seconds)
|
||||
return seconds / SECONDS_PER_MIN;
|
||||
end
|
||||
|
||||
function MinutesToSeconds(minutes)
|
||||
return minutes * SECONDS_PER_MIN;
|
||||
end
|
||||
|
||||
function ConvertSecondsToUnits(timestamp)
|
||||
timestamp = math.max(timestamp, 0);
|
||||
local days = math.floor(timestamp / SECONDS_PER_DAY);
|
||||
timestamp = timestamp - (days * SECONDS_PER_DAY);
|
||||
local hours = math.floor(timestamp / SECONDS_PER_HOUR);
|
||||
timestamp = timestamp - (hours * SECONDS_PER_HOUR);
|
||||
local minutes = math.floor(timestamp / SECONDS_PER_MIN);
|
||||
timestamp = timestamp - (minutes * SECONDS_PER_MIN);
|
||||
local seconds = math.floor(timestamp);
|
||||
local milliseconds = timestamp - seconds;
|
||||
return {
|
||||
days = days,
|
||||
hours = hours,
|
||||
minutes = minutes,
|
||||
seconds = seconds,
|
||||
milliseconds = milliseconds,
|
||||
};
|
||||
end
|
||||
|
||||
function SecondsToClock(seconds, displayZeroHours)
|
||||
local units = ConvertSecondsToUnits(seconds);
|
||||
if units.hours > 0 or displayZeroHours then
|
||||
return string.format(HOURS_MINUTES_SECONDS, units.hours, units.minutes, units.seconds);
|
||||
else
|
||||
return string.format(MINUTES_SECONDS, units.minutes, units.seconds);
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,855 @@
|
||||
-- Trade-skill *list* links (`|Htrade:...|h`) — backport of the 2.x+ shareable
|
||||
-- profession link. The link is built entirely in the DLL
|
||||
-- (`GetTradeSkillListLink` / `GetCraftListLink`, see src/tradeskill/Link.cpp);
|
||||
-- this file only intercepts clicks on received links and renders the linker's
|
||||
-- recipe list in a scroll frame (the UI half).
|
||||
--
|
||||
-- link: |cffffd000|Htrade:<skillLineID>:<curRank>:<maxRank>:<linkerName>:<base64 bits>|h[Name]|h|r
|
||||
--
|
||||
-- "Which recipes are known" is a bitfield over the skill line's canonical
|
||||
-- recipe list (deterministic per DBC). The viewer lists the recipes the linker
|
||||
-- knows (with an "X of Y" summary of the whole skill line), regardless of what
|
||||
-- the reader can craft themselves.
|
||||
|
||||
-- Frame geometry mirrors the real TradeSkillFrame. Turtle ships an HD
|
||||
-- 768x512 two-column frame (TW-TradeSkill art); stock 1.12.1 only has the
|
||||
-- vanilla 384x512 single-column art, so on that client we fall back to the
|
||||
-- native Blizzard_TradeSkillUI geometry (list on top, detail below) rather
|
||||
-- than stretch the wide art. LO holds every layout value that differs.
|
||||
local TURTLE = (TURTLE_WOW_VERSION ~= nil);
|
||||
local FRAME_W = TURTLE and 768 or 384;
|
||||
local FRAME_H = 512;
|
||||
local NUM_ROWS = TURTLE and 19 or 8;
|
||||
local ROW_HEIGHT = 16;
|
||||
|
||||
local LO = TURTLE and {
|
||||
hit = { 2, 82, 2, 73 },
|
||||
quads = {
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopLeft", 512, 256, "TOPLEFT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopRight", 256, 256, "TOPRIGHT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotLeft2", 512, 256, "BOTTOMLEFT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotRight2", 256, 256, "BOTTOMRIGHT" },
|
||||
},
|
||||
close = { -77, -8 },
|
||||
bar = { 596, 15, 76, -44 },
|
||||
barBdr = { 604, 22 },
|
||||
summary = { "TOPRIGHT", -110, -70 },
|
||||
drop = { 40, -60 },
|
||||
scroll = { 296, 330, 37, -98 },
|
||||
-- detail scroll frame: { x, y, width, height }. Tall enough that the
|
||||
-- content fits (scrollbar auto-hides) — Turtle's right column is roomy.
|
||||
detail = { 371, -110, 340, 290 },
|
||||
exit = { "CENTER", "BOTTOMRIGHT", -128, 92 },
|
||||
} or {
|
||||
hit = { 0, 34, 0, 75 },
|
||||
quads = {
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft", 256, 256, "TOPLEFT" },
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight", 128, 256, "TOPRIGHT" },
|
||||
{ "Interface\\TradeSkillFrame\\UI-TradeSkill-BotLeft", 256, 256, "BOTTOMLEFT" },
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight", 128, 256, "BOTTOMRIGHT" },
|
||||
},
|
||||
close = { -29, -8 },
|
||||
bar = { 268, 15, 73, -37 },
|
||||
barBdr = { 278, 22 },
|
||||
summary = { "TOPRIGHT", -40, -58 },
|
||||
drop = { 12, -62 },
|
||||
scroll = { 296, 130, 21, -96 },
|
||||
-- detail scroll frame { x, y, width, height } — native TradeSkillFrame's
|
||||
-- TradeSkillDetailScrollFrame values. Small enough that many-reagent
|
||||
-- recipes scroll (clipping partial rows), like native.
|
||||
detail = { 20, -234, 297, 176 },
|
||||
exit = { "CENTER", "TOPLEFT", 305, -422 },
|
||||
sep = { 15, -221, 331 },
|
||||
};
|
||||
|
||||
-- Reagents render in a 2-column grid inside the detail scroll child (see
|
||||
-- the detail pane below). The detail is a real ScrollFrame like the
|
||||
-- native TradeSkillFrame, so the whole pane pixel-scrolls and clips
|
||||
-- partial reagent rows — `REAGENT_ROW_HEIGHT` is just the vertical stride
|
||||
-- of one reagent row.
|
||||
local REAGENT_ROW_HEIGHT = 43;
|
||||
|
||||
-- Scroll-frame templates. Stock mimics the native TradeSkillFrame, whose
|
||||
-- list/detail scrolls use the ClassTrainer templates (they add the
|
||||
-- decorative UI-ClassTrainer-ScrollBar track). Turtle's HD frame art
|
||||
-- doesn't want that groove, so it keeps the plain templates. Both detail
|
||||
-- templates are real ScrollFrames (ClassTrainerDetail... inherits
|
||||
-- UIPanelScrollFrameTemplate), so the pixel-scroll logic is identical.
|
||||
local LIST_SCROLL_TEMPLATE = TURTLE and "FauxScrollFrameTemplate"
|
||||
or "ClassTrainerListScrollFrameTemplate";
|
||||
local DETAIL_SCROLL_TEMPLATE = TURTLE and "UIPanelScrollFrameTemplate"
|
||||
or "ClassTrainerDetailScrollFrameTemplate";
|
||||
|
||||
local PROFESSION_ICONS = {
|
||||
[171] = "Trade_Alchemy",
|
||||
[164] = "Trade_BlackSmithing",
|
||||
[185] = "INV_Misc_Food_15", -- Cooking
|
||||
[333] = "Trade_Engraving", -- Enchanting
|
||||
[202] = "Trade_Engineering",
|
||||
[129] = "Spell_Holy_SealOfSacrifice", -- First Aid
|
||||
[165] = "INV_Misc_ArmorKit_17", -- Leatherworking
|
||||
[197] = "Trade_Tailoring",
|
||||
[186] = "Trade_Mining",
|
||||
[182] = "Trade_Herbalism",
|
||||
[393] = "INV_Misc_Pelt_Wolf_01", -- Skinning
|
||||
[356] = "Trade_Fishing",
|
||||
};
|
||||
|
||||
if TURTLE then
|
||||
PROFESSION_ICONS[142] = "Trade_Survival"
|
||||
PROFESSION_ICONS[755] = "INV_Misc_Gem_01" -- Jewelcrafting
|
||||
end
|
||||
|
||||
local function ProfessionIcon(skillLineID)
|
||||
local icon = PROFESSION_ICONS[skillLineID];
|
||||
return icon and ("Interface\\Icons\\" .. icon)
|
||||
or "Interface\\Icons\\INV_Misc_QuestionMark";
|
||||
end
|
||||
|
||||
local function DifficultyBand(skill, hi, lo)
|
||||
if not hi or hi <= 0 then
|
||||
return 3;
|
||||
end
|
||||
if not lo or lo == 0 then
|
||||
lo = (hi ~= 25) and (hi - 25) or 0;
|
||||
end
|
||||
local yellow = (hi + lo) / 2;
|
||||
if skill < lo then
|
||||
return 0; -- orange (optimal)
|
||||
elseif skill < yellow then
|
||||
return 1; -- yellow (medium)
|
||||
elseif skill < hi then
|
||||
return 2; -- green (easy)
|
||||
end
|
||||
return 3; -- grey (trivial)
|
||||
end
|
||||
|
||||
local BAND_COLOR = {
|
||||
[0] = DIFFICULT_DIFFICULTY_COLOR, -- orange
|
||||
[1] = FAIR_DIFFICULTY_COLOR, -- yellow
|
||||
[2] = EASY_DIFFICULTY_COLOR, -- green
|
||||
[3] = TRIVIAL_DIFFICULTY_COLOR, -- grey
|
||||
};
|
||||
|
||||
local function CraftableCount(entry)
|
||||
local reagents = entry.reagents;
|
||||
if not reagents or table.getn(reagents) == 0 then
|
||||
return nil;
|
||||
end
|
||||
local best;
|
||||
for i = 1, table.getn(reagents) do
|
||||
local r = reagents[i];
|
||||
local need = r.count or 1;
|
||||
local n = math.floor((C_Item.GetItemCount(r.itemID) or 0) / need);
|
||||
if not best or n < best then
|
||||
best = n;
|
||||
end
|
||||
end
|
||||
return best;
|
||||
end
|
||||
|
||||
local function ParseTradeLink(link)
|
||||
-- Current format, with the linker's name: trade:id:cur:max:Name:bits
|
||||
local _, _, id, cur, max, who, bits =
|
||||
string.find(link, "^trade:(%d+):(%d+):(%d+):([^:]+):([%w%+/]*)$");
|
||||
if not id then
|
||||
-- Legacy format (no name): trade:id:cur:max:bits
|
||||
who = nil;
|
||||
_, _, id, cur, max, bits =
|
||||
string.find(link, "^trade:(%d+):(%d+):(%d+):([%w%+/]*)$");
|
||||
end
|
||||
if not id then
|
||||
return nil;
|
||||
end
|
||||
return tonumber(id), tonumber(cur), tonumber(max), who, bits or "";
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Display frame (built lazily on first use)
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local frame;
|
||||
local function ResolveSubclasses(list, onLoaded)
|
||||
local present, subs, anyItem = {}, {}, false;
|
||||
for i = 1, table.getn(list) do
|
||||
local r = list[i];
|
||||
if r.createdItem and r.createdItem > 0 then
|
||||
anyItem = true;
|
||||
if not r.subclass then
|
||||
-- GetItemInfoInstant's 3rd return is the localized subclass
|
||||
-- name (or nil when uncached).
|
||||
local _, _, subType = C_Item.GetItemInfoInstant(r.createdItem);
|
||||
if subType and subType ~= "" then
|
||||
r.subclass = subType;
|
||||
elseif onLoaded then
|
||||
-- Not cached — re-derive when the item loads.
|
||||
Item:CreateFromItemID(r.createdItem):ContinueOnItemLoad(onLoaded);
|
||||
end
|
||||
end
|
||||
if r.subclass and not present[r.subclass] then
|
||||
present[r.subclass] = true;
|
||||
table.insert(subs, r.subclass);
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(subs);
|
||||
return subs, anyItem;
|
||||
end
|
||||
|
||||
local SUBCLASS_ALL = {};
|
||||
|
||||
local function SubClassButton_OnClick()
|
||||
local v = this.value;
|
||||
if v == SUBCLASS_ALL then
|
||||
v = nil;
|
||||
end
|
||||
frame.subclassFilter = v;
|
||||
UIDropDownMenu_SetText(v or ALL_SUBCLASSES, frame.dropdown);
|
||||
frame:ApplyFilter();
|
||||
end
|
||||
|
||||
local function SubClassDropDown_Initialize()
|
||||
local info = {};
|
||||
info.text = ALL_SUBCLASSES;
|
||||
info.value = SUBCLASS_ALL;
|
||||
info.checked = (frame.subclassFilter == nil);
|
||||
info.func = SubClassButton_OnClick;
|
||||
UIDropDownMenu_AddButton(info);
|
||||
local subs = frame.subclasses or {};
|
||||
for i = 1, table.getn(subs) do
|
||||
local b = {};
|
||||
b.text = subs[i];
|
||||
b.value = subs[i];
|
||||
b.checked = (frame.subclassFilter == subs[i]);
|
||||
b.func = SubClassButton_OnClick;
|
||||
UIDropDownMenu_AddButton(b);
|
||||
end
|
||||
end
|
||||
|
||||
local function RefreshSubclasses()
|
||||
if not (frame and frame:IsShown() and frame.recipes) then
|
||||
return;
|
||||
end
|
||||
local subs, anyItem = ResolveSubclasses(frame.recipes, RefreshSubclasses);
|
||||
frame.subclasses = subs;
|
||||
if anyItem then
|
||||
UIDropDownMenu_Initialize(frame.dropdown, SubClassDropDown_Initialize);
|
||||
UIDropDownMenu_SetWidth(120, frame.dropdown);
|
||||
UIDropDownMenu_SetText(frame.subclassFilter or ALL_SUBCLASSES, frame.dropdown);
|
||||
frame.dropdown:Show();
|
||||
else
|
||||
frame.dropdown:Hide();
|
||||
end
|
||||
if frame.subclassFilter then
|
||||
frame:ApplyFilter();
|
||||
end
|
||||
end
|
||||
|
||||
local function CreateFrame_TradeSkillLink()
|
||||
local f = CreateFrame("Frame", "ClassicAPITradeSkillLinkFrame", UIParent);
|
||||
f:SetSize(FRAME_W, FRAME_H);
|
||||
f:SetPoint("CENTER", 0, 0);
|
||||
f:SetMovable(true);
|
||||
f:EnableMouse(true);
|
||||
f:SetHitRectInsets(LO.hit[1], LO.hit[2], LO.hit[3], LO.hit[4]);
|
||||
f:RegisterForDrag("LeftButton");
|
||||
f:SetScript("OnDragStart", function() this:StartMoving(); end);
|
||||
f:SetScript("OnDragStop", function() this:StopMovingOrSizing(); end);
|
||||
f:SetFrameStrata("DIALOG");
|
||||
f:Hide();
|
||||
tinsert(UISpecialFrames, f:GetName())
|
||||
|
||||
local portrait = f:CreateTexture(nil, "BACKGROUND");
|
||||
portrait:SetSize(60, 60);
|
||||
portrait:SetPoint("TOPLEFT", 7, -6);
|
||||
f.portrait = portrait;
|
||||
|
||||
for i = 1, table.getn(LO.quads) do
|
||||
local q = LO.quads[i];
|
||||
local tx = f:CreateTexture(nil, "BORDER");
|
||||
tx:SetTexture(q[1]);
|
||||
tx:SetSize(q[2], q[3]);
|
||||
tx:SetPoint(q[4]);
|
||||
end
|
||||
|
||||
-- Horizontal divider between the recipe list and the detail pane.
|
||||
-- Stock only (Turtle's HD frame art already has its own).
|
||||
if LO.sep then
|
||||
-- Decoded the BLP: it's a thin divider rod whose graphic lives in the
|
||||
-- top quarter of the 256x64 texture (a 2nd rod fills the next quarter,
|
||||
-- bottom half empty). So sample the top quarter, stretched to width —
|
||||
-- the same region the native TradeSkillFrame uses for this divider.
|
||||
local bar = f:CreateTexture(nil, "ARTWORK");
|
||||
bar:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-HorizontalBar");
|
||||
bar:SetTexCoord(0, 1, 0, 0.25);
|
||||
bar:SetSize(LO.sep[3], 16);
|
||||
bar:SetPoint("TOPLEFT", LO.sep[1], LO.sep[2]);
|
||||
end
|
||||
|
||||
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal");
|
||||
title:SetPoint("TOP", 0, -18);
|
||||
f.title = title;
|
||||
|
||||
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton");
|
||||
close:SetPoint("TOPRIGHT", LO.close[1], LO.close[2]);
|
||||
|
||||
local exit = CreateFrame("Button", nil, f, "UIPanelButtonTemplate");
|
||||
exit:SetSize(80, 22);
|
||||
exit:SetPoint(LO.exit[1], f, LO.exit[2], LO.exit[3], LO.exit[4]);
|
||||
exit:SetText(EXIT or "Exit");
|
||||
exit:SetScript("OnClick", function() f:Hide(); end);
|
||||
|
||||
local bar = CreateFrame("StatusBar", nil, f);
|
||||
bar:SetSize(LO.bar[1], LO.bar[2]);
|
||||
bar:SetPoint("TOPLEFT", LO.bar[3], LO.bar[4]);
|
||||
bar:SetStatusBarTexture("Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar");
|
||||
bar:SetStatusBarColor(0.0, 0.0, 1.0, 0.5);
|
||||
bar:SetMinMaxValues(0, 1);
|
||||
local barBg = bar:CreateTexture(nil, "BACKGROUND");
|
||||
barBg:SetAllPoints(bar);
|
||||
barBg:SetTexture(1, 1, 1, 0.2);
|
||||
barBg:SetVertexColor(0.0, 0.0, 0.75, 0.5);
|
||||
-- Border: tooltip edge around the bar (TradeSkillRankFrameBorder).
|
||||
local border = CreateFrame("Frame", nil, bar);
|
||||
border:SetSize(LO.barBdr[1], LO.barBdr[2]);
|
||||
border:SetPoint("LEFT", -5, 1);
|
||||
border:SetBackdrop({
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16,
|
||||
});
|
||||
border:SetBackdropBorderColor(0.4, 0.4, 0.4);
|
||||
local barName = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall");
|
||||
barName:SetPoint("LEFT", 6, 1);
|
||||
local barRank = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
|
||||
barRank:SetJustifyH("LEFT");
|
||||
barRank:SetWidth(128);
|
||||
barRank:SetPoint("LEFT", barName, "RIGHT", 13, 0);
|
||||
f.bar = bar;
|
||||
f.barName = barName;
|
||||
f.barRank = barRank;
|
||||
|
||||
local summary = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
|
||||
summary:SetPoint(LO.summary[1], LO.summary[2], LO.summary[3]);
|
||||
f.summary = summary;
|
||||
|
||||
local dropdown = CreateFrame("Frame", "ClassicAPITradeSkillLinkSubClass", f,
|
||||
"UIDropDownMenuTemplate");
|
||||
dropdown:SetPoint("TOPLEFT", LO.drop[1], LO.drop[2]);
|
||||
f.dropdown = dropdown;
|
||||
f.subclassFilter = nil; -- nil = all subclasses
|
||||
|
||||
local scroll = CreateFrame("ScrollFrame", "ClassicAPITradeSkillLinkScroll",
|
||||
f, LIST_SCROLL_TEMPLATE);
|
||||
scroll:SetSize(LO.scroll[1], LO.scroll[2]);
|
||||
scroll:SetPoint("TOPLEFT", LO.scroll[3], LO.scroll[4]);
|
||||
scroll:SetScript("OnVerticalScroll", function()
|
||||
FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, function() f:Refresh(); end)
|
||||
end)
|
||||
f.scroll = scroll;
|
||||
|
||||
f.rows = {};
|
||||
for i = 1, NUM_ROWS do
|
||||
local rowName = "ClassicAPITradeSkillLinkRow" .. i;
|
||||
local row = CreateFrame("Button", rowName, f,
|
||||
"ClassTrainerSkillButtonTemplate");
|
||||
row:SetWidth(290); -- height (16) comes from the template
|
||||
if i == 1 then
|
||||
row:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0);
|
||||
else
|
||||
row:SetPoint("TOPLEFT", f.rows[i - 1], "BOTTOMLEFT", 0, 0);
|
||||
end
|
||||
|
||||
row:SetNormalTexture("");
|
||||
row:SetDisabledTexture("");
|
||||
row:SetHighlightTexture("");
|
||||
|
||||
local txt = getglobal(rowName .. "Text");
|
||||
if txt then
|
||||
txt:ClearAllPoints();
|
||||
txt:SetPoint("LEFT", row, "LEFT", 4, 0);
|
||||
end
|
||||
|
||||
local sel = row:CreateTexture(nil, "BACKGROUND");
|
||||
sel:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2");
|
||||
sel:SetAllPoints(row);
|
||||
sel:Hide();
|
||||
row.select = sel;
|
||||
|
||||
-- Override the template's trainer scripts with ours.
|
||||
row:SetScript("OnClick", function()
|
||||
if this.entry then
|
||||
f:SelectRecipe(this.entry);
|
||||
end
|
||||
end);
|
||||
|
||||
f.rows[i] = row;
|
||||
end
|
||||
|
||||
-- Detail pane: a real ScrollFrame (like the native TradeSkillFrame's
|
||||
-- TradeSkillDetailScrollFrame) whose scroll child holds the product
|
||||
-- header + reagent grid, so the whole pane pixel-scrolls — clipping
|
||||
-- partial reagent rows — with the standard UIPanelScrollBar. The bar
|
||||
-- auto-hides when the content fits (scrollBarHideable).
|
||||
local detailScroll = CreateFrame("ScrollFrame",
|
||||
"ClassicAPITradeSkillLinkDetailScroll", f, DETAIL_SCROLL_TEMPLATE);
|
||||
detailScroll:SetSize(LO.detail[3], LO.detail[4]);
|
||||
detailScroll:SetPoint("TOPLEFT", LO.detail[1], LO.detail[2]);
|
||||
detailScroll.scrollBarHideable = 1; -- template sets this too; belt-and-suspenders
|
||||
f.detailScroll = detailScroll;
|
||||
-- Off until a recipe overflows the pane (toggled in RenderDetail).
|
||||
-- The template shows its slider and enables the wheel by default; on
|
||||
-- the roomy Turtle pane that leaves a dead scrollbar that still eats
|
||||
-- wheel events and over-scrolls a hair. Start both disabled.
|
||||
detailScroll:EnableMouseWheel(false);
|
||||
local detailSB = getglobal("ClassicAPITradeSkillLinkDetailScrollScrollBar");
|
||||
if detailSB then
|
||||
detailSB:Hide();
|
||||
end
|
||||
-- The template re-shows its slider when the frame transitions to shown
|
||||
-- (on first open, before the auto-selected recipe's RenderDetail runs),
|
||||
-- leaving a dead scrollbar until the first click. Re-assert the hidden
|
||||
-- state on show unless the current recipe genuinely overflows.
|
||||
detailScroll:SetScript("OnShow", function()
|
||||
f:UpdateDetailScrollbar();
|
||||
RunNextFrame(function()
|
||||
if f:IsShown() then
|
||||
f:UpdateDetailScrollbar();
|
||||
end
|
||||
end);
|
||||
end);
|
||||
|
||||
local detailChild = CreateFrame("Frame",
|
||||
"ClassicAPITradeSkillLinkDetailChild", detailScroll);
|
||||
detailChild:SetSize(LO.detail[3], LO.detail[4]);
|
||||
detailScroll:SetScrollChild(detailChild);
|
||||
f.detailChild = detailChild;
|
||||
|
||||
local prodIcon = CreateFrame("Button", nil, detailChild);
|
||||
prodIcon:SetSize(37, 37); -- native TradeSkillSkillIcon (37x37 @ 8,-3)
|
||||
prodIcon:SetPoint("TOPLEFT", 8, -3);
|
||||
prodIcon:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
local hdrLeft = detailChild:CreateTexture(nil, "BACKGROUND");
|
||||
hdrLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderLeft");
|
||||
hdrLeft:SetSize(256, 64); -- Blizzard's native fill width (+64 cap = 320 total)
|
||||
hdrLeft:SetPoint("TOPLEFT", prodIcon, "TOPLEFT", -8, 6);
|
||||
local hdrRight = detailChild:CreateTexture(nil, "BACKGROUND");
|
||||
hdrRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderRight");
|
||||
hdrRight:SetSize(64, 64);
|
||||
hdrRight:SetPoint("TOPLEFT", hdrLeft, "TOPRIGHT", 0, 0);
|
||||
local prodCount = prodIcon:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
|
||||
prodCount:SetPoint("BOTTOMRIGHT", -5, 2);
|
||||
prodIcon.count = prodCount;
|
||||
f.prodIcon = prodIcon;
|
||||
|
||||
local prodName = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormal");
|
||||
prodName:SetPoint("LEFT", prodIcon, "RIGHT", 5, 0);
|
||||
prodName:SetWidth(LO.detail[3] - 60);
|
||||
prodName:SetJustifyH("LEFT");
|
||||
prodName:SetJustifyV("MIDDLE");
|
||||
f.prodName = prodName;
|
||||
|
||||
local prodDesc = detailChild:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall");
|
||||
prodDesc:SetPoint("TOPLEFT", prodIcon, "BOTTOMLEFT", 0, -6);
|
||||
prodDesc:SetWidth(LO.detail[3] - 20);
|
||||
prodDesc:SetJustifyH("LEFT");
|
||||
prodDesc:SetJustifyV("TOP");
|
||||
f.prodDesc = prodDesc;
|
||||
|
||||
local reagentLabel = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall");
|
||||
reagentLabel:SetPoint("TOPLEFT", prodDesc, "BOTTOMLEFT", 0, -8);
|
||||
reagentLabel:SetText(SPELL_REAGENTS or "Reagents:");
|
||||
f.reagentLabel = reagentLabel;
|
||||
|
||||
prodIcon:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
|
||||
if this.itemLink then
|
||||
GameTooltip:SetHyperlink(this.itemLink);
|
||||
elseif this.spellID then
|
||||
GameTooltip:SetSpellByID(this.spellID);
|
||||
end
|
||||
GameTooltip:Show();
|
||||
end);
|
||||
prodIcon:SetScript("OnLeave", GameTooltip_Hide);
|
||||
|
||||
-- Reagent buttons live in the scroll child (2 per row), built on
|
||||
-- demand so any reagent count is handled and they scroll/clip with
|
||||
-- the child.
|
||||
f.reagents = {};
|
||||
local function AcquireReagent(i)
|
||||
if f.reagents[i] then
|
||||
return f.reagents[i];
|
||||
end
|
||||
local rowN = math.floor((i - 1) / 2);
|
||||
local col = (i - 1) - rowN * 2;
|
||||
local btn = CreateFrame("Button", nil, detailChild);
|
||||
btn:SetSize(39, 39);
|
||||
btn:SetPoint("TOPLEFT", reagentLabel, "BOTTOMLEFT",
|
||||
col * 134 - 5, -3 - rowN * REAGENT_ROW_HEIGHT);
|
||||
-- Extend the hit area rightward over the name plate so the tooltip
|
||||
-- fires anywhere on the element, not just the icon. Stop short of the
|
||||
-- next column (pitch 134 − icon 39) so hovers don't bleed across.
|
||||
btn:SetHitRectInsets(0, -(134 - 39), 0, 0);
|
||||
btn:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
local plate = btn:CreateTexture(nil, "BACKGROUND");
|
||||
plate:SetTexture("Interface\\QuestFrame\\UI-QuestItemNameFrame");
|
||||
plate:SetSize(114, 64);
|
||||
plate:SetPoint("LEFT", btn, "RIGHT", -10, 0);
|
||||
local cnt = btn:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
|
||||
cnt:SetPoint("BOTTOMRIGHT", -4, 1);
|
||||
btn.count = cnt;
|
||||
local nm = btn:CreateFontString(nil, "ARTWORK", "GameFontHighlight");
|
||||
nm:SetPoint("LEFT", plate, "LEFT", 15, 0);
|
||||
nm:SetSize(80, 36);
|
||||
nm:SetJustifyH("LEFT");
|
||||
nm:SetJustifyV("MIDDLE");
|
||||
btn.nameText = nm;
|
||||
btn:SetScript("OnEnter", function()
|
||||
if this.itemLink then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
|
||||
GameTooltip:SetHyperlink(this.itemLink);
|
||||
GameTooltip:Show();
|
||||
end
|
||||
end);
|
||||
btn:SetScript("OnLeave", GameTooltip_Hide);
|
||||
f.reagents[i] = btn;
|
||||
return btn;
|
||||
end
|
||||
|
||||
-- Show the detail slider + enable the wheel only when the current
|
||||
-- recipe overflows the pane (tracked by `detailNeedScroll`).
|
||||
function f:UpdateDetailScrollbar()
|
||||
local need = self.detailNeedScroll and true or false;
|
||||
self.detailScroll:EnableMouseWheel(need);
|
||||
local base = "ClassicAPITradeSkillLinkDetailScroll";
|
||||
local sb = getglobal(base .. "ScrollBar");
|
||||
if sb then sb:SetShown(need) end
|
||||
local top = getglobal(base .. "Top");
|
||||
if top then top:SetShown(need) end
|
||||
local bottom = getglobal(base .. "Bottom");
|
||||
if bottom then bottom:SetShown(need) end
|
||||
end
|
||||
|
||||
function f:RenderDetail(entry)
|
||||
local function itemInfo(id)
|
||||
local item = Item:CreateFromItemID(id);
|
||||
if not item:IsItemDataCached() then
|
||||
item:ContinueOnItemLoad(function()
|
||||
if self:IsShown() and self.selectedSpellID == entry.spellID then
|
||||
self:RenderDetail(entry);
|
||||
end
|
||||
end);
|
||||
end
|
||||
return item:GetItemName(), item:GetItemIcon(), item:GetItemLink();
|
||||
end
|
||||
|
||||
local prodTex, prodText, prodLink;
|
||||
local created = entry.createdItem or 0;
|
||||
if created > 0 then
|
||||
local name, tex, link = itemInfo(created);
|
||||
prodText = name or entry.name;
|
||||
prodTex = tex;
|
||||
prodLink = link;
|
||||
else
|
||||
prodText = entry.name;
|
||||
prodTex = C_Spell.GetSpellTexture(entry.spellID);
|
||||
end
|
||||
local descText = (created > 0) and ""
|
||||
or (C_Spell.GetSpellDescription(entry.spellID) or "");
|
||||
self.prodDesc:SetText(descText);
|
||||
self.prodIcon:SetNormalTexture(prodTex or
|
||||
"Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
self.prodName:SetText(prodText or "");
|
||||
self.prodName:SetTextColor(NORMAL_FONT_COLOR:GetRGB());
|
||||
self.prodIcon.itemLink = prodLink; -- item tooltip when created > 0
|
||||
self.prodIcon.spellID = entry.spellID; -- else spell tooltip
|
||||
local made = entry.numMade or 0;
|
||||
self.prodIcon.count:SetText(made > 1 and tostring(made) or "");
|
||||
self.prodIcon:Show();
|
||||
|
||||
-- Fill every reagent slot; the scroll child clips/scrolls the
|
||||
-- overflow. Buttons past the count are hidden.
|
||||
local reagents = entry.reagents or {};
|
||||
local total = table.getn(reagents);
|
||||
for i = 1, total do
|
||||
local btn = AcquireReagent(i);
|
||||
local r = reagents[i];
|
||||
local name, tex, link = itemInfo(r.itemID);
|
||||
btn:SetNormalTexture(tex or "Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
btn.itemLink = link;
|
||||
btn.nameText:SetText(name or ("item:" .. r.itemID));
|
||||
local need = r.count or 1;
|
||||
local have = C_Item.GetItemCount(r.itemID) or 0;
|
||||
if have >= need then
|
||||
btn:GetNormalTexture():SetVertexColor(1, 1, 1);
|
||||
btn.nameText:SetTextColor(HIGHLIGHT_FONT_COLOR:GetRGB());
|
||||
else
|
||||
btn:GetNormalTexture():SetVertexColor(0.5, 0.5, 0.5);
|
||||
btn.nameText:SetTextColor(GRAY_FONT_COLOR:GetRGB());
|
||||
end
|
||||
local haveText = (have >= 100) and "*" or tostring(have);
|
||||
btn.count:SetText(haveText .. " /" .. need);
|
||||
btn:Show();
|
||||
end
|
||||
for i = total + 1, table.getn(self.reagents) do
|
||||
self.reagents[i].itemLink = nil;
|
||||
self.reagents[i]:Hide();
|
||||
end
|
||||
|
||||
-- Grow the scroll child to the content height so the pane scrolls
|
||||
-- once the header + reagent rows exceed the visible area. Reagent
|
||||
-- row 0 sits ~67px below the child top (icon + label); add the
|
||||
-- wrapped description height for spell recipes.
|
||||
local descH = (descText ~= "") and self.prodDesc:GetHeight() or 0;
|
||||
local rows = math.ceil(total / 2);
|
||||
local contentH = 67 + descH + rows * REAGENT_ROW_HEIGHT + 8;
|
||||
self.detailChild:SetHeight(math.max(contentH, LO.detail[4]));
|
||||
-- Reset to the top and manage the slider explicitly: a range that
|
||||
-- stays 0 (content fits, e.g. Turtle's roomy pane) never fires the
|
||||
-- range-changed event that `scrollBarHideable` relies on, so the
|
||||
-- slider would otherwise sit visible-but-useless. Show it only when
|
||||
-- the content actually overflows.
|
||||
self.detailNeedScroll = contentH > LO.detail[4];
|
||||
local sb = getglobal("ClassicAPITradeSkillLinkDetailScrollScrollBar");
|
||||
if sb then
|
||||
sb:SetValue(0);
|
||||
end
|
||||
self.detailScroll:SetVerticalScroll(0);
|
||||
self:UpdateDetailScrollbar();
|
||||
RunNextFrame(function()
|
||||
if self:IsShown() then
|
||||
self:UpdateDetailScrollbar();
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function f:SelectRecipe(entry)
|
||||
self.selectedSpellID = entry.spellID;
|
||||
self.selectedEntry = entry;
|
||||
self:RenderDetail(entry); -- also resets the detail scroll to top
|
||||
self:Refresh();
|
||||
end
|
||||
|
||||
function f:ClearDetail()
|
||||
self.selectedSpellID = nil;
|
||||
self.selectedEntry = nil;
|
||||
self.prodIcon:Hide();
|
||||
self.prodName:SetText("");
|
||||
self.prodDesc:SetText("");
|
||||
for i = 1, table.getn(self.reagents) do
|
||||
self.reagents[i].itemLink = nil;
|
||||
self.reagents[i]:Hide();
|
||||
end
|
||||
self.detailChild:SetHeight(LO.detail[4]);
|
||||
self.detailNeedScroll = false;
|
||||
self:UpdateDetailScrollbar();
|
||||
self.detailScroll:SetVerticalScroll(0);
|
||||
end
|
||||
|
||||
function f:Refresh()
|
||||
local recipes = self.displayed or {};
|
||||
local total = table.getn(recipes);
|
||||
FauxScrollFrame_Update(self.scroll, total, NUM_ROWS, ROW_HEIGHT);
|
||||
local offset = FauxScrollFrame_GetOffset(self.scroll);
|
||||
for i = 1, NUM_ROWS do
|
||||
local row = self.rows[i];
|
||||
local entry = recipes[offset + i];
|
||||
if entry then
|
||||
-- Append " [N]" when the reader can craft any (Blizzard's list).
|
||||
local craftable = CraftableCount(entry);
|
||||
local label = entry.name or "";
|
||||
if craftable and craftable > 0 then
|
||||
label = label .. " [" .. craftable .. "]";
|
||||
end
|
||||
row:SetText(label);
|
||||
local r, g, b = BAND_COLOR[entry.band or 3]:GetRGB();
|
||||
if self.selectedSpellID and entry.spellID == self.selectedSpellID then
|
||||
row.select:SetVertexColor(r, g, b);
|
||||
row.select:Show();
|
||||
row:SetTextColor(1, 1, 1);
|
||||
else
|
||||
row.select:Hide();
|
||||
row:SetTextColor(r, g, b);
|
||||
end
|
||||
row.spellID = entry.spellID;
|
||||
row.entry = entry;
|
||||
row:Show();
|
||||
else
|
||||
row.spellID = nil;
|
||||
row.entry = nil;
|
||||
row.select:Hide();
|
||||
row:Hide();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function f:ApplyFilter()
|
||||
local src = self.recipes or {};
|
||||
if not self.subclassFilter then
|
||||
self.displayed = src;
|
||||
else
|
||||
local out = {};
|
||||
for i = 1, table.getn(src) do
|
||||
if src[i].subclass == self.subclassFilter then
|
||||
table.insert(out, src[i]);
|
||||
end
|
||||
end
|
||||
self.displayed = out;
|
||||
end
|
||||
local sb = getglobal("ClassicAPITradeSkillLinkScrollScrollBar");
|
||||
if sb then
|
||||
sb:SetValue(0);
|
||||
end
|
||||
self:Refresh();
|
||||
end
|
||||
|
||||
f:RegisterEvent("BAG_UPDATE_DELAYED");
|
||||
f:SetScript("OnEvent", function()
|
||||
if not f:IsShown() then
|
||||
return;
|
||||
end
|
||||
f:Refresh();
|
||||
if f.selectedEntry then
|
||||
f:RenderDetail(f.selectedEntry);
|
||||
end
|
||||
end);
|
||||
|
||||
return f;
|
||||
end
|
||||
|
||||
local function ShowTradeSkillLink(link, text)
|
||||
local id, cur, max, who, bits = ParseTradeLink(link);
|
||||
if not id then
|
||||
return false;
|
||||
end
|
||||
local recipes = C_TradeSkillUI.GetTradeSkillListRecipes(id, bits);
|
||||
if not recipes then
|
||||
return false;
|
||||
end
|
||||
|
||||
local prof = text and string.match(text, "|h%[(.+)%]|h") or "Trade Skills";
|
||||
local total = table.getn(recipes);
|
||||
local knownList = {};
|
||||
for i = 1, total do
|
||||
local r = recipes[i];
|
||||
if r.isKnown then
|
||||
r.band = DifficultyBand(cur or 0, r.trivialLevel, r.greenLevel);
|
||||
r.name = GetSpellInfo(r.spellID) or ("spell:" .. r.spellID);
|
||||
table.insert(knownList, r);
|
||||
end
|
||||
end
|
||||
table.sort(knownList, function(a, b)
|
||||
if a.band ~= b.band then
|
||||
return a.band < b.band;
|
||||
end
|
||||
return a.name < b.name;
|
||||
end);
|
||||
local known = table.getn(knownList);
|
||||
|
||||
if not frame then
|
||||
frame = CreateFrame_TradeSkillLink();
|
||||
end
|
||||
frame.title:SetText(who and who ~= "" and (who .. " - " .. prof) or prof);
|
||||
SetPortraitToTexture(frame.portrait, ProfessionIcon(id));
|
||||
frame.bar:SetMinMaxValues(0, (max and max > 0) and max or 1);
|
||||
frame.bar:SetValue(cur or 0);
|
||||
frame.barName:SetText(prof);
|
||||
frame.barRank:SetText(string.format("%d/%d", cur or 0, max or 0));
|
||||
frame.summary:SetText(string.format("%d of %d recipes known", known, total));
|
||||
frame.recipes = knownList;
|
||||
frame.subclassFilter = nil;
|
||||
|
||||
frame:ClearDetail();
|
||||
frame:Show();
|
||||
RefreshSubclasses();
|
||||
frame:ApplyFilter();
|
||||
local first = frame.displayed and frame.displayed[1];
|
||||
if first then
|
||||
frame:SelectRecipe(first);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Click interception: handle `trade:` before the engine's SetHyperlink (which
|
||||
-- in 1.12 only knows item:/enchant: and would reject the link).
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local originalSetItemRef = SetItemRef;
|
||||
function SetItemRef(link, text, button, chatFrame)
|
||||
if type(link) == "string" and string.sub(link, 1, 6) == "trade:" then
|
||||
if IsShiftKeyDown() and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Insert(text);
|
||||
return;
|
||||
end
|
||||
if ShowTradeSkillLink(link, text) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
return originalSetItemRef(link, text, button, chatFrame);
|
||||
end
|
||||
|
||||
local function CreateProfessionLinkButton(parentName, getLink)
|
||||
local parent = _G[parentName];
|
||||
local btnName = parentName .. "LinkButton";
|
||||
if _G[btnName] or not parent then
|
||||
return;
|
||||
end
|
||||
local title = _G[parentName .. "TitleText"];
|
||||
local btn = CreateFrame("Button", btnName, parent);
|
||||
btn:SetSize(16, 16);
|
||||
|
||||
local function reposition()
|
||||
if not title then
|
||||
return;
|
||||
end
|
||||
btn:ClearAllPoints();
|
||||
if title:GetJustifyH() == "LEFT" then
|
||||
btn:SetPoint("LEFT", title, "LEFT", title:GetStringWidth() + 4, 0);
|
||||
else
|
||||
btn:SetPoint("LEFT", title, "CENTER", title:GetStringWidth() / 2 + 4, 0);
|
||||
end
|
||||
end
|
||||
reposition()
|
||||
btn:SetScript("OnShow", function() RunNextFrame(reposition) end)
|
||||
hooksecurefunc(title, "SetText", function() RunNextFrame(reposition) end)
|
||||
|
||||
local icon = "Interface\\GossipFrame\\PetitionGossipIcon";
|
||||
btn:SetNormalTexture(icon);
|
||||
btn:SetHighlightTexture(icon, "ADD");
|
||||
|
||||
btn:SetScript("OnClick", function()
|
||||
local link = getLink();
|
||||
if not link then
|
||||
return;
|
||||
end
|
||||
if not ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Show();
|
||||
end
|
||||
ChatFrameEditBox:Insert(link);
|
||||
end);
|
||||
btn:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_TOPLEFT");
|
||||
GameTooltip:SetText(LINK_TRADESKILL_TOOLTIP, nil, nil, nil, nil, 1);
|
||||
GameTooltip:Show();
|
||||
end);
|
||||
btn:SetScript("OnLeave", GameTooltip_Hide);
|
||||
end
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_TradeSkillUI", function()
|
||||
CreateProfessionLinkButton("TradeSkillFrame", C_TradeSkillUI.GetTradeSkillListLink);
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_CraftUI", function()
|
||||
CreateProfessionLinkButton("CraftFrame", C_TradeSkillUI.GetCraftListLink)
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow", function()
|
||||
CreateProfessionLinkButton("ATSWFrame", function()
|
||||
return atsw_oldmode and C_TradeSkillUI.GetCraftListLink() or C_TradeSkillUI.GetTradeSkillListLink()
|
||||
end)
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow2", function()
|
||||
CreateProfessionLinkButton("ATSWFrame", function()
|
||||
return ATSW_TradeSkill() and C_TradeSkillUI.GetTradeSkillListLink() or C_TradeSkillUI.GetCraftListLink()
|
||||
end)
|
||||
end)
|
||||
@@ -0,0 +1,53 @@
|
||||
if GetLocale() ~= "deDE" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Fokus setzen";
|
||||
BINDING_NAME_TARGETFOCUS = "Fokus anvisieren";
|
||||
FOCUSTARGET = "Ziel als Fokus festlegen";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4IGegenstand:Gegenstände;";
|
||||
ITEMS_EQUIPPED = "%d |4Gegenstand:Gegenstände; ausgerüstet";
|
||||
ITEMS_IN_INVENTORY = "%d |4Gegenstand:Gegenstände; im Inventar";
|
||||
ITEM_SLOTS_IGNORED = "%d |4Platz:Plätze; ignoriert";
|
||||
ITEM_MISSING = "%s fehlt";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4Gegenstand fehlt:Gegenstände fehlen;";
|
||||
TARGETFOCUS = "Fokussierte Einheit anvisieren";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "K";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "G";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "Wenn Ihr diesen Gegenstand ersetzt, werden sich folgende Werte ändern:";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Gesundheit";
|
||||
ITEM_MOD_AGILITY_SHORT = "Beweglichkeit";
|
||||
ITEM_MOD_STRENGTH_SHORT = "Stärke";
|
||||
ITEM_MOD_INTELLECT_SHORT = "Intelligenz";
|
||||
ITEM_MOD_SPIRIT_SHORT = "Willenskraft";
|
||||
ITEM_MOD_STAMINA_SHORT = "Ausdauer";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Angriffskraft";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Distanzangriffskraft";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Kritischer Trefferwert (Nah)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Kritischer Trefferwert (Distanz)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Trefferwert (Nah)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Trefferwert (Distanz)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Trefferwert (Zauber)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Kritischer Trefferwert (Zauber)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Bonusschaden";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Bonusheilung";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Manaregeneration";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Verteidigung";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Ausweichen";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parieren";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Blocken";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Blocken";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Schaden pro Sekunde";
|
||||
LARGE_NUMBER_SEPERATOR = ".";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "S";
|
||||
SLASH_CLEARFOCUS1 = "/fokusaufheben";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/Ausrüstungsset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/fokus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Hier klicken, um einen Link zu Eurem Beruf zu erstellen.";
|
||||
@@ -0,0 +1,51 @@
|
||||
BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "If you replace this item, the following stat changes will occur:";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Health";
|
||||
ITEM_MOD_AGILITY_SHORT = "Agility";
|
||||
ITEM_MOD_STRENGTH_SHORT = "Strength";
|
||||
ITEM_MOD_INTELLECT_SHORT = "Intellect";
|
||||
ITEM_MOD_SPIRIT_SHORT = "Spirit";
|
||||
ITEM_MOD_STAMINA_SHORT = "Stamina";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Attack Power";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Ranged Attack Power";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Critical Strike (Melee)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Critical Strike (Ranged)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Hit (Melee)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Hit (Ranged)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Hit (Spell)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Critical Strike (Spell)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Spell Damage";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Spell Healing";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Mana Regeneration";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Defense";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Dodge";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parry";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Block";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Block Value";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Damage Per Second";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
|
||||
@@ -0,0 +1,53 @@
|
||||
if GetLocale() ~= "esES" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Establecer objetivo";
|
||||
BINDING_NAME_TARGETFOCUS = "Seleccionar enfocado";
|
||||
FOCUSTARGET = "Establecer objetivo";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objeto:objetos;";
|
||||
ITEMS_EQUIPPED = "Equipado: %d |4objeto:objetos;";
|
||||
ITEMS_IN_INVENTORY = "En inventario: %d |4objeto:objetos;";
|
||||
ITEM_SLOTS_IGNORED = "Ignorado: %d |4casilla:casillas;";
|
||||
ITEM_MISSING = "Falta: %s";
|
||||
ITEMS_NOT_IN_INVENTORY = "Faltan: %d |4objeto:objetos;";
|
||||
TARGETFOCUS = "Fijar unidad objetivo";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "o";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "Si reemplazas este objeto, se producirán los siguientes cambios en las estadísticas:";
|
||||
ITEM_MOD_MANA_SHORT = "Maná";
|
||||
ITEM_MOD_HEALTH_SHORT = "Salud";
|
||||
ITEM_MOD_AGILITY_SHORT = "agilidad";
|
||||
ITEM_MOD_STRENGTH_SHORT = "fuerza";
|
||||
ITEM_MOD_INTELLECT_SHORT = "intelecto";
|
||||
ITEM_MOD_SPIRIT_SHORT = "espíritu";
|
||||
ITEM_MOD_STAMINA_SHORT = "aguante";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Poder de ataque";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Poder de ataque a distancia";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Golpe crítico (cuerpo a cuerpo)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Golpe crítico (a distancia)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Golpe (cuerpo a cuerpo)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Golpe (a distancia)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Golpe (hechizo)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Golpe crítico (hechizo)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Bon. daño";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Bon. sanac.";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Regeneración de maná";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Defensa";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Esquiva";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parada";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Bloqueo";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Valor de bloqueo";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "daño por segundo";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "p";
|
||||
SLASH_CLEARFOCUS1 = "/quitarenfoque";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/conjequip";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/enfoque";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Haz clic aquí para crear un vínculo a tu profesión.";
|
||||
@@ -0,0 +1,53 @@
|
||||
if GetLocale() ~= "frFR" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Définir cible";
|
||||
BINDING_NAME_TARGETFOCUS = "Reprendre cible";
|
||||
FOCUSTARGET = "Définir la cible de focalisation";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objet:objets;";
|
||||
ITEMS_EQUIPPED = "%d |4objet:objets; équipés";
|
||||
ITEMS_IN_INVENTORY = "%d |4objet:objets; dans l'inventaire";
|
||||
ITEM_SLOTS_IGNORED = "%d |4emplacement ignoré:emplacements ignorés;";
|
||||
ITEM_MISSING = "Objet manquant : %s";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4objet:objets; manquants";
|
||||
TARGETFOCUS = "Cibler l'unité focalisée";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "o";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "Si vous remplacez cet objet, les caractéristiques suivantes seront modifiées :";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Vie";
|
||||
ITEM_MOD_AGILITY_SHORT = "Agilité";
|
||||
ITEM_MOD_STRENGTH_SHORT = "Force";
|
||||
ITEM_MOD_INTELLECT_SHORT = "Intelligence";
|
||||
ITEM_MOD_SPIRIT_SHORT = "Esprit";
|
||||
ITEM_MOD_STAMINA_SHORT = "Endurance";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Puissance d'attaque";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Puissance d'attaque à distance";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Score de critique (mêlée)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Score de critique (distance)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Toucher (mêlée)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Toucher (distance)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Toucher (sorts)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Score de critique (sorts)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Bonus aux dégâts";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Bonus aux soins";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Régénération du mana";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Défense";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Esquive";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parade";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Blocage";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Valeur de blocage";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Dégâts par seconde";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "a";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/annulerfocal";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/équipens";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focal";
|
||||
LINK_TRADESKILL_TOOLTIP = "Cliquez ici pour créer un lien vers votre profession.";
|
||||
@@ -0,0 +1,9 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/..\FrameXML\UI.xsd">
|
||||
<Script file="enUS.lua"/>
|
||||
<Script file="deDE.lua"/>
|
||||
<Script file="esES.lua"/>
|
||||
<Script file="frFR.lua"/>
|
||||
<Script file="ruRU.lua"/>
|
||||
<Script file="zhCN.lua"/>
|
||||
<Script file="zhTW.lua"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,53 @@
|
||||
if GetLocale() ~= "ruRU" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "Запомненная цель";
|
||||
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
|
||||
FOCUSTARGET = "Запомнить цель";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
|
||||
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
|
||||
ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов; в сумках";
|
||||
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячейки пропущены;";
|
||||
ITEM_MISSING = "Предмет для ячейки %s отсутствует";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствуют;";
|
||||
TARGETFOCUS = "Восстановить цель";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "м";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "з";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "Если вы замените этот предмет, произойдут следующие изменения:";
|
||||
ITEM_MOD_MANA_SHORT = "к мане";
|
||||
ITEM_MOD_HEALTH_SHORT = "к здоровью";
|
||||
ITEM_MOD_AGILITY_SHORT = "к ловкости";
|
||||
ITEM_MOD_STRENGTH_SHORT = "к силе";
|
||||
ITEM_MOD_INTELLECT_SHORT = "к интеллекту";
|
||||
ITEM_MOD_SPIRIT_SHORT = "к духу";
|
||||
ITEM_MOD_STAMINA_SHORT = "к выносливости";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "к силе атаки";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "к силе атаки дальнего боя";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "к показателю критического удара в ближнем бою";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "к показателю критического удара в дальнем бою";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "к меткости в ближнем бою";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "к меткости в дальнем бою";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "к меткости заклинаний";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "к критическому эффекту заклинаний";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "к дополнительному урону";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "к дополнительному исцелению";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "к восполнению маны";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Защита";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Уклонение";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Парирование";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Блок";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "к показателю блокирования";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "к урону в секунду";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "с";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/сбросфокуса";
|
||||
SLASH_EQUIP_SET1 = "/комплект";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/фокус";
|
||||
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
|
||||
@@ -0,0 +1,53 @@
|
||||
if GetLocale() ~= "zhCN" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "焦点目标";
|
||||
BINDING_NAME_TARGETFOCUS = "目标焦点";
|
||||
FOCUSTARGET = "设定焦点目标";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "已装备%d件物品";
|
||||
ITEMS_IN_INVENTORY = "背包中有%d件物品";
|
||||
ITEM_SLOTS_IGNORED = "忽略%d个栏位";
|
||||
ITEM_MISSING = "%s缺失";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
TARGETFOCUS = "选中焦点目标";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "铜";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "金";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "如果你替换该物品,将会产生以下的属性变更:";
|
||||
ITEM_MOD_MANA_SHORT = "法力值";
|
||||
ITEM_MOD_HEALTH_SHORT = "生命值";
|
||||
ITEM_MOD_AGILITY_SHORT = "敏捷";
|
||||
ITEM_MOD_STRENGTH_SHORT = "力量";
|
||||
ITEM_MOD_INTELLECT_SHORT = "智力";
|
||||
ITEM_MOD_SPIRIT_SHORT = "精神";
|
||||
ITEM_MOD_STAMINA_SHORT = "耐力";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "攻击强度";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "远程攻击强度";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "爆击(近战)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "爆击(远程)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "命中(近战)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "命中(远程)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "命中(法术)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "爆击(法术)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "伤害加成";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "治疗加成";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "法力回复";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "防御";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "躲闪";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "招架";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "格挡";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "格挡值";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒伤害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "银";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "点击这里以创建一个链接到你的专业的快捷方式。";
|
||||
@@ -0,0 +1,53 @@
|
||||
if GetLocale() ~= "zhTW" then return end
|
||||
|
||||
BINDING_NAME_FOCUSTARGET = "鎖定目標";
|
||||
BINDING_NAME_TARGETFOCUS = "選定鎖定目標";
|
||||
FOCUSTARGET = "設定鎖定目標";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
ITEMS_EQUIPPED = "裝備%d件物品";
|
||||
ITEMS_IN_INVENTORY = "%d件物品在物品欄中";
|
||||
ITEM_SLOTS_IGNORED = "忽略%d個欄位";
|
||||
ITEM_MISSING = "缺少%s物品";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
TARGETFOCUS = "選定鎖定單位";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "銅";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "金";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ITEM_DELTA_DESCRIPTION = "若你更換此裝備,下列屬性將會改變:";
|
||||
ITEM_MOD_MANA_SHORT = "法力";
|
||||
ITEM_MOD_HEALTH_SHORT = "生命力";
|
||||
ITEM_MOD_AGILITY_SHORT = "敏捷";
|
||||
ITEM_MOD_STRENGTH_SHORT = "力量";
|
||||
ITEM_MOD_INTELLECT_SHORT = "智力";
|
||||
ITEM_MOD_SPIRIT_SHORT = "精神";
|
||||
ITEM_MOD_STAMINA_SHORT = "耐力";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "攻擊強度";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "遠程攻擊強度";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "致命一擊(近戰)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "致命一擊(遠程)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "命中(近戰)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "命中(遠程)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "命中(法術)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "致命一擊(法術)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "傷害加成";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "治療加成";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "法力恢復";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "防禦";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "閃躲";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "招架";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "格擋";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "格擋值";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒傷害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
SILVER_AMOUNT_SYMBOL = "銀";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/清除專注目標";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/裝備套裝";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/專注目標";
|
||||
LINK_TRADESKILL_TOOLTIP = "點擊這裡以製造一個你專業技能的連結。";
|
||||
@@ -0,0 +1,217 @@
|
||||
-- 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")
|
||||
@@ -0,0 +1,7 @@
|
||||
## Interface: 11200
|
||||
## Title: AoE Loot Test
|
||||
## Notes: Test bed for ClassicAPI's C_Loot.ScanNearbyLoot / LootUnitItem. /aoeloot
|
||||
## Dependencies: !!!ClassicAPI
|
||||
## Version: DEV
|
||||
|
||||
AoELootTest.lua
|
||||
+46
-7
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(ClassicAPI LANGUAGES CXX)
|
||||
project(ClassicAPI LANGUAGES CXX RC)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -47,9 +47,29 @@ add_custom_command(
|
||||
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_CLASSICAPI_HEADER}
|
||||
${EMBEDDED_BINDINGS_HEADER})
|
||||
|
||||
# Dev-build sentinel — represents "newer than every plausible
|
||||
# release". Encoded as v9999.99.99 = 9999 * 10000 + 99 * 100 + 99 =
|
||||
@@ -57,17 +77,20 @@ add_library(${PROJECT_NAME} SHARED
|
||||
# `CLASSIC_API_VERSION >= X` always pass during local development;
|
||||
# CI release builds overwrite it via `-DCLASSICAPI_TAG=vX.Y.Z`.
|
||||
set(CLASSICAPI_VERSION_VALUE 99999999)
|
||||
set(CLASSICAPI_VER_MAJOR 9999)
|
||||
set(CLASSICAPI_VER_MINOR 99)
|
||||
set(CLASSICAPI_VER_PATCH 99)
|
||||
|
||||
set(CLASSICAPI_TAG
|
||||
""
|
||||
CACHE STRING "Release tag like v1.2.3")
|
||||
|
||||
if(CLASSICAPI_TAG MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
|
||||
set(_MAJOR "${CMAKE_MATCH_1}")
|
||||
set(_MINOR "${CMAKE_MATCH_2}")
|
||||
set(_PATCH "${CMAKE_MATCH_3}")
|
||||
set(CLASSICAPI_VER_MAJOR "${CMAKE_MATCH_1}")
|
||||
set(CLASSICAPI_VER_MINOR "${CMAKE_MATCH_2}")
|
||||
set(CLASSICAPI_VER_PATCH "${CMAKE_MATCH_3}")
|
||||
math(EXPR CLASSICAPI_VERSION_VALUE
|
||||
"${_MAJOR} * 10000 + ${_MINOR} * 100 + ${_PATCH}")
|
||||
"${CLASSICAPI_VER_MAJOR} * 10000 + ${CLASSICAPI_VER_MINOR} * 100 + ${CLASSICAPI_VER_PATCH}")
|
||||
elseif(CLASSICAPI_TAG)
|
||||
message(
|
||||
FATAL_ERROR
|
||||
@@ -79,7 +102,23 @@ target_compile_definitions(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE CLASSICAPI_VERSION_VALUE=${CLASSICAPI_VERSION_VALUE})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE minhook advapi32)
|
||||
# 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.
|
||||
set(CLASSICAPI_VER_STRING
|
||||
"${CLASSICAPI_VER_MAJOR}.${CLASSICAPI_VER_MINOR}.${CLASSICAPI_VER_PATCH}.0")
|
||||
set(CLASSICAPI_VERSION_RC "${CMAKE_BINARY_DIR}/generated/version.rc")
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.rc.in"
|
||||
"${CLASSICAPI_VERSION_RC}"
|
||||
@ONLY)
|
||||
target_sources(${PROJECT_NAME} PRIVATE "${CLASSICAPI_VERSION_RC}")
|
||||
|
||||
# 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.
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE minhook advapi32 ole32)
|
||||
|
||||
target_include_directories(
|
||||
${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program 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.
|
||||
|
||||
This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -18,41 +18,57 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Container](docs/API.md#container) | `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [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` |
|
||||
| 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` (sets persist to `WTF\Account\...\ClassicAPI_EquipmentSets.txt`) |
|
||||
| [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` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID` |
|
||||
| [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:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `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:SetUnitAura`, `OnTooltipSetItem` (script) |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [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.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `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.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [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.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
|
||||
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `math.fmod`, `select`, `string.gmatch`, `string.match`, `table.wipe` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [Table](docs/API.md#table) | `table.wipe` |
|
||||
| [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` |
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
| [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` |
|
||||
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes` |
|
||||
| [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` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` (backports `GlobalColor.dbc`; companion addon wraps each row with `ColorMixin` and assigns `_G[baseTag]` + `_G[baseTag.."_CODE"]`) |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterComboDuration` |
|
||||
| [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` |
|
||||
|
||||
### GlueXML calls
|
||||
|
||||
@@ -71,6 +87,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
| [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) |
|
||||
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
|
||||
| State | `IsLoggedIn` |
|
||||
|
||||
### Macros
|
||||
|
||||
@@ -84,10 +101,23 @@ functions, just behavior the stock 1.12 engine didn't have. See the
|
||||
| `CastSpellByName("<spellID>")` | Same — numeric strings resolve through the engine's name resolver |
|
||||
| `CastSpellNoToggle("<name>")` in a macro | Engine's macro parser now recognizes it as a primary-spell line, so the macro slot in an addon like pfUI highlights when its spell is auto-repeating or its self-aura is active |
|
||||
|
||||
### Console commands
|
||||
|
||||
Registered on the engine's developer console (the `~` console available
|
||||
when launching with `-console`), not as Lua functions. See the
|
||||
[Console section in the Lua reference](docs/API.md#console) for details.
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `ExportInterfaceFiles code` | Extracts Blizzard's UI source (`.lua`/`.xml`/`.toc`/`.xsd`) from the MPQs to `BlizzardInterfaceCode\` |
|
||||
| `ExportInterfaceFiles art` | Extracts Blizzard's UI art (`.blp`/`.tga`) from the MPQs to `BlizzardInterfaceArt\` |
|
||||
| `ExportDBCFiles` | Extracts the client's `.dbc` tables from the MPQs to `DBFilesClient\` |
|
||||
|
||||
### Events
|
||||
|
||||
| Event | Payload |
|
||||
|-------|---------|
|
||||
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
|
||||
| `BAG_UPDATE_DELAYED` | *(none)* |
|
||||
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
|
||||
| `EQUIPMENT_SWAP_PENDING` | `setID` |
|
||||
@@ -97,10 +127,16 @@ functions, just behavior the stock 1.12 engine didn't have. See the
|
||||
| `GLOBAL_MOUSE_UP` | `button` |
|
||||
| `HEARTHSTONE_BOUND` | *(none)* |
|
||||
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
|
||||
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex, playerIndex` |
|
||||
| `LOOT_HISTORY_ROLL_COMPLETE` | `itemIndex` |
|
||||
| `LOOT_SCAN_COMPLETED` | *(none)* |
|
||||
| `LOSS_OF_CONTROL_ADDED` | `eventIndex` |
|
||||
| `LOSS_OF_CONTROL_UPDATE` | `unitToken` (always `"player"`) |
|
||||
| `MODIFIER_STATE_CHANGED` | `keyName, down` |
|
||||
| `NAME_PLATE_CREATED` | `nameplateFrame` |
|
||||
| `NAME_PLATE_UNIT_ADDED` | `unitToken` ("nameplateN") |
|
||||
| `NAME_PLATE_UNIT_REMOVED` | `unitToken` ("nameplateN") |
|
||||
| `PLAYER_EQUIPMENT_CHANGED` | `equipmentSlot, hasCurrent` |
|
||||
| `PLAYER_FOCUS_CHANGED` | *(none)* |
|
||||
| `PLAYER_STARTED_LOOKING` | *(none)* |
|
||||
| `PLAYER_STOPPED_LOOKING` | *(none)* |
|
||||
@@ -110,8 +146,14 @@ functions, just behavior the stock 1.12 engine didn't have. See the
|
||||
| `PLAYER_STOPPED_TURNING` | *(none)* |
|
||||
| `QUEST_ACCEPTED` | `questLogIndex, questID` |
|
||||
| `QUEST_DATA_LOAD_RESULT` | `questID, success` |
|
||||
| `QUEST_REMOVED` | `questID` |
|
||||
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
|
||||
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
|
||||
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_STARTED` | `numConsumers, utteranceID, durationMS, destination` |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FINISHED` | `numConsumers, utteranceID, destination` |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FAILED` | `status, utteranceID, destination` |
|
||||
| `VOICE_CHAT_TTS_VOICES_UPDATE` | *(none)* |
|
||||
|
||||
### Globals
|
||||
|
||||
@@ -123,6 +165,7 @@ functions, just behavior the stock 1.12 engine didn't have. See the
|
||||
| Unit stat | `LE_UNIT_STAT_STRENGTH` … `LE_UNIT_STAT_SPIRIT` |
|
||||
| Addon security | `Enum.AddOnSecurityStatus.{Secure,Insecure,Banned,NotAvailable}` |
|
||||
| Power type | `Enum.PowerType.{HealthCost,None,Mana,Rage,Focus,Energy,Happiness}` |
|
||||
| Inventory type | `Enum.InventoryType.Index*Type` (0–34, e.g. `IndexHeadType`=1 … `IndexRelicType`=28) |
|
||||
|
||||
### Unit tokens
|
||||
|
||||
@@ -131,16 +174,42 @@ functions, just behavior the stock 1.12 engine didn't have. See the
|
||||
| `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). |
|
||||
|
||||
### Bindings
|
||||
|
||||
Injected into the engine's **Targeting Functions** group at FrameXML
|
||||
Bindings.xml load time, so they appear in the keybind UI alongside
|
||||
native targeting bindings instead of orphaned at the bottom.
|
||||
|
||||
| Binding | Action |
|
||||
|---------|--------|
|
||||
| `FOCUSTARGET` | `FocusUnit("target")` — pin current target as focus |
|
||||
| `TARGETFOCUS` | `TargetUnit("focus")` — switch target to the focus |
|
||||
|
||||
See [Focus / Bindings](docs/API.md#bindings-focustarget--targetfocus) for the implementation note.
|
||||
|
||||
### Behaviors
|
||||
|
||||
Transparent engine tweaks — no API to call, they just fix a vanilla limitation:
|
||||
|
||||
| Tweak | Effect |
|
||||
|-------|--------|
|
||||
| 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. |
|
||||
|
||||
## Installation
|
||||
|
||||
Use [VanillaFixes](https://github.com/hannesmann/vanillafixes) to load the
|
||||
DLL.
|
||||
Download the prebuilt `ClassicAPI.dll` from the
|
||||
[latest release](https://github.com/brues-code/ClassicAPI/releases/latest)
|
||||
(or [build it yourself](#building)). It's loaded with
|
||||
[VanillaFixes](https://github.com/hannesmann/vanillafixes):
|
||||
|
||||
1. Install VanillaFixes if it isn't already.
|
||||
2. Copy `ClassicAPI.dll` into your game directory.
|
||||
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.
|
||||
|
||||
## Bundled addon: !!!ClassicAPI
|
||||
|
||||
The Lua-side companion library lives in
|
||||
@@ -227,4 +296,4 @@ at configure time; the value exposed to Lua will be `X*10000 + Y*100 + Z`.
|
||||
|
||||
## License
|
||||
|
||||
LGPL v3 or later. See the headers in `src/` for the full notice.
|
||||
GPL v3 or later. See the headers in `src/` for the full notice.
|
||||
|
||||
@@ -782,24 +782,28 @@ The probe helpers (`_classicapi_ProbeSpellRecord`,
|
||||
`_classicapi_FindSpellField`) were temporarily exposed to derive the
|
||||
offset and removed in the same commit that shipped `GetSpellSchool`.
|
||||
|
||||
## 38. `GetSpellRadius(spellID)` — easy
|
||||
## ~~38. `GetSpellRadius(spellID)`~~ — DONE
|
||||
|
||||
Returns the AOE radius in yards, or `nil` for non-AOE spells.
|
||||
`Spell.dbc` has a `SpellRadiusIndex` field that points into
|
||||
`SpellRadius.dbc` (instance at `0x00C0D7A8` per `docs/DBCs.md`,
|
||||
already wired up via `VAR_SPELL_RANGE_RECORDS`-style helpers in
|
||||
`src/spell/Info.cpp`).
|
||||
Shipped in [src/spell/Radius.cpp](src/spell/Radius.cpp) as
|
||||
`C_Spell.GetSpellRadius(spellID)` plus the vanilla-positional
|
||||
`GetSpellRadius(slot, bookType)`. Returns `(baseRadius, modifiedRadius)`,
|
||||
or `nil` for non-AOE spells. `modifiedRadius` applies the local player's
|
||||
talent/item radius mods (e.g. Arctic Reach) via the shared `Spell::Mod`
|
||||
helper — verified in-game: Frost Nova (10230) → `10, 10` without Arctic
|
||||
Reach, `10, 12` with rank 2 (+20%).
|
||||
|
||||
Stride and field layout for `SpellRadius.dbc`: needs derivation
|
||||
(should be `{id, radius_min, radius, radius_max}` per the public
|
||||
schema, but verify). Sub-DBC lookup pattern matches what
|
||||
`LookupSubRecord` already does in `Info.cpp` for icon / cast time /
|
||||
range.
|
||||
The radius is **per-effect**, not a single spell-level field: `Spell.dbc`
|
||||
has `EffectRadiusIndex[3]` at record `+0x160` (verified by decompiling
|
||||
`FUN_006e6350`, which reads `spellRec[+0x160]`/`[+0x164]` for effects
|
||||
0/1). Each non-zero index resolves into `SpellRadius.dbc` (records
|
||||
`0x00C0D7B0`, count `0x00C0D7B4`), record shape
|
||||
`{ id@+0, radius@+4 (f32), radiusPerLevel@+8 (f32), ... }`. We return the
|
||||
largest base radius across the three effects. Base radius only — the
|
||||
engine adds `radiusPerLevel * casterLevel`, but that needs a unit context
|
||||
and modern `GetSpellRadius` reports the base value.
|
||||
|
||||
Useful for AOE damage trackers, ground-effect addons, and any aura
|
||||
library that wants to render an indicator radius. Modern WoW exposes
|
||||
this via `GetSpellRadius` (deprecated) or returns from
|
||||
`C_Spell.GetSpellInfo`.
|
||||
See `OFF_SPELL_RECORD_EFFECT_RADIUS_INDEX`, `VAR_SPELL_RADIUS_RECORDS`,
|
||||
`OFF_SPELL_RADIUS_VALUE` in [src/Offsets.h](src/Offsets.h).
|
||||
|
||||
## ~~39. `GetFactionParentID(factionID)`~~ — DONE
|
||||
|
||||
@@ -2309,45 +2313,55 @@ that want this behavior on Octo-style multi-DLL clients can
|
||||
fall back to debouncing `BAG_UPDATE` themselves via an
|
||||
`OnUpdate` timer.
|
||||
|
||||
## 68. `UnitChannelInfo(unit)` / `UnitCastingInfo(unit)` — medium
|
||||
## ~~68. `UnitChannelInfo(unit)` / `UnitCastingInfo(unit)`~~ — DONE
|
||||
|
||||
Modern signatures return `(name, _, texture, startMs, endMs, _, _,
|
||||
spellID)` for the unit's currently channeled / cast spell. 1.12
|
||||
exposes nothing equivalent — the closest is `SPELLCAST_CHANNEL_*`
|
||||
events, which only fire for the local player and don't carry
|
||||
spellID payloads.
|
||||
Shipped in [src/spell/Cast.cpp](src/spell/Cast.cpp): `UnitCastingInfo`,
|
||||
`CastingInfo`, `UnitChannelInfo`, `ChannelInfo`. Verified in-game —
|
||||
Blizzard channel `(…, 907894314, 907902314, …, 10187)` (8s), Conjure Food
|
||||
cast `(…, 907928268, 907931268, …, 28612)` (3s), `GetTime()*1000` falls
|
||||
between start/end.
|
||||
|
||||
The data is sitting in the broadcast descriptor:
|
||||
**The start/end-time hunt resolved with a firm negative**, confirmed via
|
||||
the cross-binary technique. 3.3.5's `Script_UnitCastingInfo`
|
||||
(`0x00611DF0`) / `Script_UnitChannelInfo` (`0x00612090`) read cast times
|
||||
from **per-unit CGUnit fields** (cast `+0xa6c/+0xa78/+0xa7c`, channel
|
||||
`+0xa80/+0xa84/+0xa88`) — TBC/WotLK additions that don't exist in the
|
||||
1.12 CGUnit (the tell: 1.12's *player* cast spellID is a global,
|
||||
`VAR_CURRENT_CAST_SPELL`, not a unit field). 1.12 stores no cast times
|
||||
anywhere readable; the cast bar is Lua-driven off `SPELLCAST_START`, and
|
||||
remote units' casts are transient spell-visual objects
|
||||
(`FUN_0060d7c0`), not a queryable record. The cast-timer globals at
|
||||
`0x00cee8d4..e4` are a shared visual-system slot (written for *any*
|
||||
channeling unit via `FUN_00612a30 → FUN_0060d7c0`), so they're not
|
||||
player-safe.
|
||||
|
||||
- `UNIT_FIELD_CHANNEL_SPELL` at descriptor `+0x228` (already in
|
||||
`Offsets.h` as `OFF_UNIT_FIELD_CHANNEL_SPELL`). Verified via the
|
||||
warlock/clicker/fishing diffs in the `IsAssistingRitual` session
|
||||
— non-zero = currently channeling that spell.
|
||||
- `UNIT_FIELD_CHANNEL_OBJECT` at descriptor `+0x38/+0x3C`
|
||||
(`OFF_UNIT_FIELD_CHANNEL_OBJECT`). 64-bit GUID of the channel
|
||||
target — bobber for fishing, lock for lockpicking, etc. Zero for
|
||||
channels that don't bind to a GameObject (warlock spells, Ritual
|
||||
of Summoning).
|
||||
So timing is **self-tracked for the local player** on `WorldTick`:
|
||||
- cast: `VAR_CURRENT_CAST_SPELL` 0→nonzero → `startMs = FUN_OS_TICKCOUNT_MS`,
|
||||
`endMs = start + SpellCastTimes base ms`. Instant casts (cast time 0)
|
||||
and channels are filtered out of the cast path.
|
||||
- channel: player's broadcast `UNIT_FIELD_CHANNEL_SPELL` (`+0x228`),
|
||||
`endMs = start + SpellDuration base ms`.
|
||||
|
||||
That gives us spellID immediately, and `name` + `texture` via the
|
||||
existing `GetSpellInfo(spellID)` chain. Worth shipping a partial
|
||||
function that omits the `startMs`/`endMs` returns and let callers
|
||||
do their own duration estimation off `SPELLCAST_CHANNEL_START` —
|
||||
or hunt for the per-unit cast-time fields the engine uses for the
|
||||
local castbar.
|
||||
`FUN_OS_TICKCOUNT_MS` shares `GetTime()`'s epoch (×0.001), so the ms
|
||||
values feed addon progress math directly. Other units: channel
|
||||
spellID/name/texture from the broadcast `+0x228` (no times); regular
|
||||
casts return nil (no vanilla data).
|
||||
|
||||
What's missing for the full modern signature: the start/end-time
|
||||
fields. The local player has `Script_SpellStopCasting`-adjacent
|
||||
state at fixed VAs (`m_castingSpellId`, `m_castStartMs`,
|
||||
`m_castEndMs`) — but for *other* units we'd need to either find
|
||||
broadcast UpdateFields holding the same, or compute end-time on
|
||||
the client from `Spell.dbc`'s cast time when we see a fresh
|
||||
UNIT_FIELD_CHANNEL_SPELL value. The 5.4.8/3.3.5 binaries are the
|
||||
right next stop for this.
|
||||
`isTradeskill` is real — `SPELL_ATTR_TRADESPELL` (Attributes bit 0x20,
|
||||
the version-stable bit 3.3.5's Script_UnitCastingInfo reads). Verified:
|
||||
Rough Blasting Powder (Engineering) → true, Conjure Food → false.
|
||||
|
||||
Companion field to investigate while we're in here: `desc 0x1320`
|
||||
captures the *prior* channeled spellID after the channel ends —
|
||||
see entry #71 for what that's about.
|
||||
Cast time is the **effective** time, not base: we call the engine's
|
||||
cast-time helper `FUN_006e3340` (base + level scaling + cast-time SpellMod
|
||||
op 10 + the caster's haste/cast-speed multiplier at descriptor +0x22c) —
|
||||
the same helper the engine's own cast-start uses, so our end time matches
|
||||
the cast bar. Verified: talented Frostbolt (Improved Frostbolt -0.5s)
|
||||
reads 2.5s, not the 3.0s base. (Calling the helper beats replicating op 10
|
||||
via `Spell::Mod`, which would miss the cast-speed multiplier.)
|
||||
|
||||
**Follow-up:** `delayTimeMs` pushback tracking (currently 0).
|
||||
|
||||
Companion field (`desc 0x1320`, prior channeled spell) — see entry #71.
|
||||
|
||||
## 69. `IsGathering()` — easy (local player only)
|
||||
|
||||
@@ -2711,7 +2725,26 @@ matching modern's 3-tuple. `CastSpellNoToggle("<name>")` macros are
|
||||
also recognized because `Spell::CastNoToggle` already hooks the same
|
||||
parser to register that pattern.
|
||||
|
||||
## 88. `PLAYER_EQUIPMENT_CHANGED(equipmentSlot, hasCurrent)` event
|
||||
## ~~88. `PLAYER_EQUIPMENT_CHANGED(equipmentSlot, hasCurrent)` event~~ — DONE
|
||||
|
||||
**Resolution (2026-07-04, src/player/Equipment.cpp):** the 2026-05-22
|
||||
investigation kept trying to hook a FIRER of UNIT_INVENTORY_CHANGED.
|
||||
The winning move was the opposite: the engine has a generic
|
||||
descriptor-field observer system (the "__AUCMirrorHandler__" nodes —
|
||||
registrar `FUN_00467E70`, dispatcher `FUN_00465570` with per-node
|
||||
mirror + memcmp change detection). `FUN_004F8CC0` registers observers
|
||||
for the bag/backpack/bank/keyring GUID ranges but never for equipment
|
||||
`0x4A8..0x538` — so we co-hook it and REGISTER OUR OWN observer per
|
||||
equipment field, mirroring the engine's registration exactly (bank 4,
|
||||
size 8, player GUID). Callback ABI verified from the dispatcher call
|
||||
site at `0x004655DC` + `FUN_004F8DB0`'s `RET 0x10`:
|
||||
`__fastcall(fieldOffset /*ecx*/, size /*edx*/, guidLo, guidHi,
|
||||
oldValue*, userArg)`. hasCurrent read from the invMgr GUID array (the
|
||||
same "current truth" the engine's bag callback reads). No polling, no
|
||||
hot hook, engine-managed lifetime (setup re-runs each enter-world).
|
||||
Offsets documented at `FUN_DESC_OBSERVER_REGISTER` in Offsets.h.
|
||||
|
||||
Original notes below kept for the investigation trail.
|
||||
|
||||
WotLK-era event that fires every time a paperdoll slot changes
|
||||
(equip, unequip, swap). Heavily used by gear-tracking,
|
||||
@@ -2814,7 +2847,33 @@ Same diff feeds `EQUIPMENT_SETS_CHANGED` /
|
||||
module — currently those fire only on user-side mutations, not
|
||||
on inventory changes that affect a set's "equipped count".
|
||||
|
||||
## 89. `UPDATE_INVENTORY_DURABILITY` event
|
||||
## ~~89. `UPDATE_INVENTORY_DURABILITY` event~~ — DONE
|
||||
|
||||
**Resolution (2026-07-04, src/player/Equipment.cpp):** co-hook the
|
||||
engine's inventory-alerts recompute `FUN_004C7EE0` — the routine that
|
||||
walks equipment slots, reads each item's durability/broken state from
|
||||
the descriptor, and unconditionally fires vanilla's
|
||||
`UPDATE_INVENTORY_ALERTS` (event 501). The engine calls it whenever an
|
||||
owned item's fields change (the item post-update handler `FUN_005D9A90`
|
||||
routes every item values-update through it, alongside
|
||||
UNIT_INVENTORY_CHANGED and the bag-update chain) plus equip and
|
||||
enter-world paths — i.e., it IS the engine's own "durability may have
|
||||
changed" signal. Post-original we diff a per-slot {guid, durability}
|
||||
snapshot (seeded silently on the first recompute per enter-world) and
|
||||
fire once per recompute with a real change. No polling, no hot hook.
|
||||
|
||||
**Dead end tried first:** bank-1 (ITEM field) descriptor observers per
|
||||
equipped item, via the TODO #88 observer system. They register without
|
||||
error but are NEVER invoked — item values-updates demonstrably arrive
|
||||
(probed the values pre-pass `FUN_00465330`) and player-bank observers
|
||||
fire in the same session; registration arithmetic fully verified (bank
|
||||
base `FUN_00465690(1)=0x18`, bank walk item 0→1→2, anchors match
|
||||
dispatch). Prime suspect: the registrar's internal wrapper lookup
|
||||
(`FUN_00464890`, the observer registry — NOT the object manager)
|
||||
silently no-ops for item objects. See the DEAD END note at
|
||||
`OFF_DESC_ITEM_DURABILITY` in Offsets.h.
|
||||
|
||||
Original notes below kept for the investigation trail.
|
||||
|
||||
Fires when any equipped item's durability changes — picks up
|
||||
combat damage, repairs, item destruction. Used by Repair-O-Matic
|
||||
@@ -3287,3 +3346,472 @@ the matching read. Both should be straightforward once the field
|
||||
offset is identified — 3.3.5's `Script_*` equivalents are a good
|
||||
starting point for what the parameter shape and bounds-clamp
|
||||
behavior should be.
|
||||
|
||||
## ~~97. Lift GameTooltip's 30-line cap~~ — DONE
|
||||
|
||||
Shipped as [src/tooltip/LinePool.cpp](src/tooltip/LinePool.cpp): pure C++,
|
||||
no addon Lua. Co-hooks the XML-OnLoad region scan (FUN_00529650, vtable
|
||||
slot 9) via `Game::HookAutoRegister`; after the engine sets up the
|
||||
template's 30 lines it creates `TextLeft/Right 31..60` in C++ (alloc →
|
||||
ctor → name → font-copied-from-prev → anchor, offsets scaled pixel →
|
||||
internal like Script_SetPoint) and appends them to the tooltip's three
|
||||
line arrays, bumping `numLinesAllocated`. Runs once per GameTooltip-family
|
||||
frame at creation + per `/reload`, so it auto-covers GameTooltip,
|
||||
ShoppingTooltip1/2, ItemRefTooltip, AtlasLootTooltip, WorldMapTooltip, …
|
||||
Verified in-game: AtlasLoot tooltip rendered a 31st line correctly. Cap is
|
||||
`kMaxLines = 60`. The design notes below record the RE trail; Approach A
|
||||
(hook AddLine) / Approach B (addon Lua) were both explored and dropped in
|
||||
favor of the scan-hook.
|
||||
|
||||
Vanilla 1.12's `GameTooltip:AddLine` silently drops any line past the
|
||||
pool of `TextLeftN`/`TextRightN` FontStrings the XML template
|
||||
pre-declares (30). Modern (3.3.5+) engines grow the pool on demand.
|
||||
Addons that append comparison summaries, item-info footers, or aura
|
||||
details to a full item tooltip get their tail chopped. Practical
|
||||
example — pfUI's eqcompare block (base-stat deltas inline + extended
|
||||
stats appended at the bottom) hits the cap on stat-heavy items where
|
||||
the vendor tooltip already has ~22 lines and the compare block adds
|
||||
another 10; extended-stat rows past line 30 disappear silently.
|
||||
|
||||
Also affects `ShoppingTooltip1`/`ShoppingTooltip2` (same
|
||||
`GameTooltipTemplate`).
|
||||
|
||||
### Root cause (verified in Ghidra on the 1.12 Octo binary)
|
||||
|
||||
`AddLine` internal impl at `FUN_00530270` — the shared body called
|
||||
from both `Script_GameTooltipAddLine` (`FUN_00531630`, method ptr
|
||||
stored in the GameTooltip method table alongside the `"AddLine"`
|
||||
string at `0x00854560`) and `Script_GameTooltipAddDoubleLine`:
|
||||
|
||||
```c
|
||||
void FUN_00530270(int this, char *leftText, char *rightText,
|
||||
colorLeft, colorRight, wrapFlag) {
|
||||
if ((*(int *)(this + 0x31c) != *(int *)(this + 0x320)) && anyHasText) {
|
||||
// set leftText on textLeft[numLines_used],
|
||||
// set rightText on textRight[numLines_used],
|
||||
// wrapFlag[numLines_used] = wrapFlag,
|
||||
// ++numLines_used
|
||||
}
|
||||
// else: silently drop the line
|
||||
}
|
||||
```
|
||||
|
||||
`0x320 == 800` decimal, matches decompile. Pool exhausted ⇒ nothing
|
||||
happens.
|
||||
|
||||
### CGameTooltip struct offsets (1.12)
|
||||
|
||||
Derived from `FUN_00530270` and the pool-init at `FUN_00529650`:
|
||||
|
||||
| Offset | Field |
|
||||
|--------|----------------------------------------------------|
|
||||
| `+0x31c` | `numLines` — current write index (0-based) |
|
||||
| `+0x320` | `numLinesAllocated` — pool size (30 default) |
|
||||
| `+0x328..+0x32c` | `textLeft[]` — array of `CSimpleFontString *`, count `+0x320` |
|
||||
| `+0x334..+0x338` | `textRight[]` — same, symmetric right column |
|
||||
| `+0x340..+0x344` | `wrapFlag[]` — per-line wrap bool |
|
||||
|
||||
Actual layout of the array slots: `+0x328` is the first ptr in an
|
||||
external heap block whose base is `param_1[0xcb]` = `+0x32c`. The
|
||||
init function reallocates all three arrays whenever `numLinesAllocated`
|
||||
would change — see `FUN_00536c80` (the reallocator) and `FUN_004368c0`
|
||||
(a matching alloc for the wrap-flag array of a different element size).
|
||||
|
||||
### The pool-init function
|
||||
|
||||
`FUN_00529650` — walks `%sTextLeftN` / `%sTextRightN` FontString names
|
||||
from the FrameXML-declared pool, counts them, reallocates the three
|
||||
arrays, stores the pointers. Called via vtable slot at `0x00808f84`
|
||||
(only xref) — likely `SetOwner` or an equivalent virtual reset method
|
||||
runs it. It's the natural growth path — if we can force it to see a
|
||||
larger pool of Lua-visible FontStrings and trigger it, the arrays
|
||||
resize themselves.
|
||||
|
||||
### The 3.3.5 reference (already REd)
|
||||
|
||||
3.3.5's `GameTooltip::AddLine` at `0x0061fec0` has the grow-on-demand
|
||||
branch we want to backport. Shape:
|
||||
|
||||
```c
|
||||
if (currentLine == numLinesAllocated - 1) {
|
||||
format(buf, 256, "%sTextLeft%d", tooltipName, currentLine + 1);
|
||||
auto *fs = SimpleFontString::create(this, /*layer*/2, /*subLayer*/1);
|
||||
fs->setName(buf);
|
||||
fs->setFontObject(prevLeft->fontObject);
|
||||
fs->anchor(TOPLEFT, prevLeft, BOTTOMLEFT, 0.0, calculatedYOffset);
|
||||
// ... mirror for TextRight ...
|
||||
// ... append fs to textLeft[]/textRight[] at [numLinesAllocated],
|
||||
// then ++numLinesAllocated.
|
||||
}
|
||||
// fall through to the existing "set text on line[currentLine]" path
|
||||
```
|
||||
|
||||
3.3.5 helpers (offsets 3.3.5-specific — 1.12 equivalents need to be
|
||||
re-derived):
|
||||
- `FUN_00485240` — SimpleFontString constructor `(parent, layer, sublayer)`
|
||||
- `FUN_0048b6c0` — set-name helper (registers under global `_G[name]`)
|
||||
- `FUN_0048a260` — anchor helper `(this, anchorPoint, relativeTo, relativePoint, xoff, yoff)`
|
||||
|
||||
Struct offsets in 3.3.5 were `+0x2a4` (numLines) / `+0x2a8` (numAlloc) /
|
||||
`+0x2b4` (textLeft) / `+0x2c0` (textRight) / `+0x2cc` (wrapFlag). All
|
||||
shifted vs. 1.12 (see above), unsurprising given TBC/WotLK bloat.
|
||||
|
||||
### Implementation plan
|
||||
|
||||
**Approach A — pre-hook AddLine with a grow-if-needed prelude
|
||||
(preferred)**:
|
||||
|
||||
1. Locate the 1.12 equivalents of the three 3.3.5 helpers. Trace them
|
||||
from existing `CSimpleFontString`-creating sites in the engine —
|
||||
the CreateFontString API dispatch is a good starting point (it's
|
||||
Lua-visible and calls the same constructor).
|
||||
2. Locate the array reallocators used by `FUN_00529650`
|
||||
(`FUN_00536c80` for the two pointer arrays, `FUN_004368c0` for the
|
||||
wrap-flag byte array).
|
||||
3. MinHook `FUN_00530270`. Pre-hook body:
|
||||
- If `numLines != numLinesAllocated`, call original — done.
|
||||
- Otherwise: realloc the three arrays to `numLinesAllocated + 1`,
|
||||
create two new `CSimpleFontString` (left + right), name them
|
||||
`"%sTextLeft%d"` / `"%sTextRight%d"` with `tooltip->GetName()` and
|
||||
the new index, anchor each below the prior-slot FontString on the
|
||||
same side, use the prior FontString's `fontObject` (`+0xd8` in the
|
||||
3.3.5 layout — confirm on 1.12) as the default, write the pointers
|
||||
into `textLeft[numLinesAllocated]` / `textRight[numLinesAllocated]`,
|
||||
zero-init `wrapFlag[numLinesAllocated]`, and bump
|
||||
`numLinesAllocated`.
|
||||
- Then tail-call the original — it now succeeds normally.
|
||||
|
||||
**Approach B — pre-declare a wider pool via Lua** (fallback if the
|
||||
constructor helpers turn out to be too tangled):
|
||||
|
||||
1. In `!!!ClassicAPI` addon, at load time, call
|
||||
`GameTooltip:CreateFontString("GameTooltipTextLeft" .. i, "ARTWORK",
|
||||
"GameTooltipText")` for `i = 31..60`, plus `TextRightN`, plus set
|
||||
anchors matching the pattern in the template.
|
||||
2. Force the engine's pool-init to re-scan. If we can't reach the
|
||||
vtable trigger cleanly, expose a small C entrypoint that walks the
|
||||
pool the same way `FUN_00529650` does — takes `tooltip` as arg,
|
||||
reallocates arrays, refills.
|
||||
|
||||
Approach B is less code but ties the cap to a fixed number (60 lines
|
||||
is enough for pfUI's use case but arbitrary). Approach A grows on
|
||||
demand like retail and has no ceiling.
|
||||
|
||||
### Testing
|
||||
|
||||
- Hover a random-suffix weapon with several equip-spell bonuses (Krol
|
||||
Blade, Ironfoe, any ZG/AQ trinket) — tooltip should render >30 lines
|
||||
without truncation.
|
||||
- Hover an item that produces a 40+ line tooltip when pfUI's eqcompare
|
||||
block is enabled (Priest Judgement Set chest against another
|
||||
Judgement chest: item tooltip ~22 lines + compare block ~10 lines).
|
||||
- Verify no crash / no visual glitches on:
|
||||
- ShoppingTooltip1/2 (same class, same `GameTooltipTemplate`)
|
||||
- Rapid re-hovers (create/destroy pressure on the added FontStrings)
|
||||
- Item tooltips that stay under 30 lines (pre-existing behavior unchanged)
|
||||
|
||||
### Risks
|
||||
|
||||
- If the array reallocation logic in `FUN_00536c80` doesn't null-out
|
||||
the tail on grow, `textLeft[N]`/`textRight[N]` might contain
|
||||
garbage for a frame — anchor before write, or memset to zero.
|
||||
- FontString lifetime: 3.3.5 leaks the extras for the tooltip
|
||||
lifetime (they stay hidden when unused). GameTooltip is a
|
||||
singleton, so leak is bounded. Fine.
|
||||
- Interaction with `pfUI`'s existing tooltip skinning (frame borders,
|
||||
status bar reposition, etc.): the layer we're modifying is beneath
|
||||
the skinning; shouldn't touch anything visible unless anchor math
|
||||
is wrong.
|
||||
|
||||
### Related
|
||||
|
||||
If we ever consider dropping the whole "GameTooltipTemplate declares
|
||||
30 FontStrings in XML" scheme and instead having the engine start
|
||||
with 0 and grow entirely on demand — that would be even cleaner, but
|
||||
requires patching every consumer of the initial pool (there aren't
|
||||
many, but they need audit). Filed for future if the incremental grow
|
||||
turns out to be fragile.
|
||||
|
||||
### Investigation update (resolved unknowns)
|
||||
|
||||
Traced the pool machinery end-to-end. The open questions above are now
|
||||
answered, and they point away from both the AddLine hook (Approach A)
|
||||
and the "trigger the engine's scan" idea — a **hybrid** is cleaner than
|
||||
either.
|
||||
|
||||
**The scan is virtual slot 9, run only at XML-template instantiation.**
|
||||
`FUN_00529650` is slot 9 (`vtable+0x24`) of the CGameTooltip vtable
|
||||
(base `0x00808f60`; the null at `0x00808f5c` is the boundary, and slot 2
|
||||
`0x005368d0` is the `tooltip:method()` dispatcher — confirms the class).
|
||||
The two vtable-base xrefs are the constructors `FUN_00529240` /
|
||||
`FUN_00529480`. The scan's prologue calls
|
||||
`FUN_0076a2f0(node) → FUN_0076a060(node)`, which reads the XML
|
||||
`"inherits"` attribute and child `"Frames"` — i.e. the scan is the
|
||||
frame's **XML OnLoad / region-setup** virtual. It runs once when
|
||||
`GameTooltip` is built from `GameTooltipTemplate` (and again per
|
||||
`/reload`), **not** on `SetOwner`/show. So we can't cheaply re-trigger
|
||||
it — the `node` arg is a live XML-parse handle we don't have, and
|
||||
calling it with a bogus node runs the inherits/Frames path on garbage.
|
||||
|
||||
**The scan finds lines by `_G[name]`, so Lua-created FontStrings count.**
|
||||
`numLinesAllocated` (`+0x320`) is literally "how many contiguous
|
||||
`%sTextLeftN` **and** `%sTextRightN` exist by name": pass 0 counts (stop
|
||||
at the first missing index), pass 1 reallocs the three arrays to that
|
||||
count and fills the pointers. The name lookup `FUN_0076c760` does
|
||||
`lua_pushstring(name); gettable(_G); if type==TABLE: FrameScript_GetObject`
|
||||
— a plain `_G[name]` resolve, so a FontString made with
|
||||
`GameTooltip:CreateFontString("GameTooltipTextLeft31", …)` **is**
|
||||
visible to it. (Names must be contiguous and exist in *both* columns.)
|
||||
|
||||
**The array reallocators are simple and callable from C.**
|
||||
`FUN_00536c80` (pointer arrays) and `FUN_004368c0` (wrap-flag array) are
|
||||
the same routine with different SMem tags:
|
||||
`__thiscall(descriptor, newCount)` where the descriptor is
|
||||
`{ count@+0x0, cap@+0x4, data@+0x8 }` — reallocs `data` to `newCount*4`
|
||||
and copies the old contents. In the tooltip the three descriptors are:
|
||||
|
||||
| Descriptor `this` | count / cap / data | array |
|
||||
|-------------------|--------------------|-------|
|
||||
| `tooltip + 0x324` | `+0x324 / +0x328 / +0x32c` | `textLeft[]` (`CSimpleFontString*`) |
|
||||
| `tooltip + 0x330` | `+0x330 / +0x334 / +0x338` | `textRight[]` |
|
||||
| `tooltip + 0x33c` | `+0x33c / +0x340 / +0x344` | `wrapFlag[]` (int, 4-byte) |
|
||||
|
||||
`numLinesAllocated` (`+0x320`) is a separate field the scan sets equal
|
||||
to the count; AddLine (`FUN_00530270`) gates on
|
||||
`numLines(+0x31c) != numLinesAllocated(+0x320)`.
|
||||
|
||||
**Recommended approach — Lua creates FontStrings, C grows the arrays
|
||||
(no hook, no XML re-run).** Replicate the scan's pass 1 directly:
|
||||
|
||||
1. **Lua (`!!!ClassicAPI`), at load / `PLAYER_LOGIN`:** for each frame on
|
||||
`GameTooltipTemplate` (`GameTooltip`, `ShoppingTooltip1/2`,
|
||||
`ItemRefTooltip`, …), create `<name>TextLeft31..N` and
|
||||
`<name>TextRight31..N` via `frame:CreateFontString(nm, "ARTWORK",
|
||||
"GameTooltipText")`, each anchored `TOPLEFT`/`TOPRIGHT` to the prior
|
||||
line's `BOTTOMLEFT`/`BOTTOMRIGHT` (mirror the template's chain).
|
||||
This avoids re-deriving the 1.12 FontString ctor / setName / anchor
|
||||
helpers that Approach A needed — Lua already wraps them.
|
||||
2. **C entrypoint** `GrowTooltipPool(tooltipObj, newCount)` (expose as a
|
||||
Lua global, called once per frame right after step 1):
|
||||
- `FUN_00536c80(tooltip+0x324, newCount)`; set `+0x328 = newCount`.
|
||||
- `FUN_00536c80(tooltip+0x330, newCount)`; set `+0x334 = newCount`.
|
||||
- `FUN_004368c0(tooltip+0x33c, newCount)`; set `+0x340 = newCount`.
|
||||
- for `i` in `[oldAlloc, newCount)`: resolve `<name>TextLeftN`/
|
||||
`RightN` via the `_G[name]` dance (same as `FUN_0076c760`), and if
|
||||
either is missing stop at `i`; else write the two pointers into the
|
||||
new `textLeft`/`textRight` data slots and zero `wrapFlag[i]`.
|
||||
- set `numLinesAllocated (+0x320) = i`.
|
||||
Resolve `tooltipObj` from the Lua frame the same way our other
|
||||
tooltip methods do (`FrameScript_GetObject`).
|
||||
|
||||
Runs once per frame per session (re-runs on `/reload`, which recreates
|
||||
the frames → scan resets to 30 → our addon re-creates + re-grows). No
|
||||
per-line hook (dodges the MinHook-collision risk on a moderately hot
|
||||
function), no XML-node fabrication.
|
||||
|
||||
Remaining risk to validate in-game: whether AddLine's downstream layout
|
||||
/ `Show` sizing walks `numLines` FontStrings by their anchors (expected)
|
||||
vs. any hidden fixed-30 assumption elsewhere; and that the Lua-created
|
||||
lines inherit height/spacing identical to the template's 1..30 so the
|
||||
tooltip grows seamlessly. Both are observable on the first >30-line
|
||||
hover.
|
||||
|
||||
## 98. `UnitThreatSituation` / `UnitDetailedThreatSituation` / `GetThreatStatusColor` — NOT FEASIBLE in a client DLL
|
||||
|
||||
Modern (WotLK+) exposes three threat APIs:
|
||||
[`UnitThreatSituation`](https://warcraft.wiki.gg/wiki/API_UnitThreatSituation)
|
||||
(unit → 0..3 aggro status),
|
||||
[`UnitDetailedThreatSituation`](https://warcraft.wiki.gg/wiki/API_UnitDetailedThreatSituation)
|
||||
(isTanking, status, threatPct, rawThreatPct, threatValue), and
|
||||
[`GetThreatStatusColor`](https://warcraft.wiki.gg/wiki/API_GetThreatStatusColor)
|
||||
(status → RGB). None of them can be backed by ClassicAPI, because
|
||||
**the underlying data does not exist on the 1.12 client** — not in a
|
||||
form a DLL can read, and not by any amount of RE.
|
||||
|
||||
### Threat is 100% server-authoritative in vanilla; no threat opcode exists
|
||||
|
||||
Vanilla 1.12's threat lives entirely inside the server's
|
||||
`ThreatManager` (per-creature `ThreatList` of `HostileReference`s —
|
||||
see Turtle's
|
||||
[`src/game/Threat/ThreatManager.cpp`](C:\Git\turtle-wow\Dumps\Source%20Code\16%20-%20Development_server\patch_1172\src\game\Threat\ThreatManager.cpp)).
|
||||
The 3.3.5 client can show a threat meter because WotLK added
|
||||
`SMSG_THREAT_UPDATE` / `SMSG_HIGHEST_THREAT_UPDATE` (and the client
|
||||
keeps a per-unit threat table those packets feed). **1.12 has no such
|
||||
opcode** — the protocol never transmits per-unit threat values, so
|
||||
there is nothing in client memory to read. This is the same class of
|
||||
limitation as `C_QuestLog.IsQuestFlaggedCompleted` (TODO #1): the
|
||||
client simply isn't told. Unlike cast timing (TODO #91, recoverable
|
||||
from `SMSG_SPELL_START`) or aura casters (recoverable from
|
||||
`SMSG_SPELL_GO`), there is **no incidental packet** that leaks threat
|
||||
either — the server computes tanking/aggro decisions internally and
|
||||
broadcasts none of the numbers.
|
||||
|
||||
### How Turtle actually does it — a *server-side* addon-channel protocol
|
||||
|
||||
TWThreat (`C:\WoW\Octo\Interface\Addons\TWThreat`) is not reading
|
||||
client memory; it round-trips through a **custom server patch**:
|
||||
|
||||
1. Client → server: the addon `SendAddonMessage("TWT_UDTSv4"[.._TM],
|
||||
"limit=NN", "PARTY"/"RAID")` — a normal `CMSG_MESSAGECHAT` addon
|
||||
message (`TWThreat.lua:1207` `TWT.UnitDetailedThreatSituation`).
|
||||
2. Server intercepts the prefix in
|
||||
[`ChatHandler.cpp:1051`](turtle-wow\Dumps\Source%20Code\16%20-%20Development_server\patch_1172\src\game\Handlers\ChatHandler.cpp)
|
||||
(`strstr(msg, "TWT_UDTSv4")`), validates
|
||||
`GetSelectedCreature()->CanHaveThreatList()`, and calls
|
||||
`ThreatManager::UnitDetailedThreatSituation(creature, player, limit,
|
||||
tankMode)`.
|
||||
3. That serializer (`ThreatManager.cpp:441`) walks the real
|
||||
`getThreatList()` and builds a `TWTv4=name:pct:threat;…` string,
|
||||
then `SendAddonMessage`s it back.
|
||||
4. Client parses it in `CHAT_MSG_ADDON` → `TWT.handleThreatPacket`
|
||||
(`TWThreat.lua:790`).
|
||||
|
||||
Every threat number the meter shows originated on the server and was
|
||||
put on the wire by a server-side code change ClassicAPI has no
|
||||
counterpart to. A client DLL cannot manufacture data the server never
|
||||
sends.
|
||||
|
||||
### Even where a server *does* expose it, the transport is async
|
||||
|
||||
The Turtle path is a request/response over the addon chat channel —
|
||||
fundamentally asynchronous (poll on `OnUpdate`, handle the reply on a
|
||||
later frame). It cannot back a **synchronous** `UnitThreatSituation()`
|
||||
that returns a value in the same call, which is exactly why TWThreat is
|
||||
structured around events and a cached `TWT.threats` table rather than a
|
||||
blocking query. A ClassicAPI shim would at best have to fake a
|
||||
synchronous return from a stale cache, and the cache only exists if a
|
||||
Turtle-style server is feeding it — i.e. the shim would add nothing the
|
||||
addon isn't already doing in Lua.
|
||||
|
||||
### `GetThreatStatusColor` is the only client-pure piece — and it's trivial Lua
|
||||
|
||||
`GetThreatStatusColor(status)` is just a fixed 0..3 → RGB map (grey /
|
||||
yellow / orange / red); no engine data involved. But it's meaningless
|
||||
without a real `status` to feed it, and it's a two-line Lua table
|
||||
lookup — no reason to spend a DLL global on it. If we ever ship a
|
||||
threat polyfill it belongs beside the (Lua, server-dependent) status
|
||||
source, not in the DLL.
|
||||
|
||||
**Verdict:** skip all three. The data is server-only with no vanilla
|
||||
opcode, the only working implementation (Turtle) is a server patch plus
|
||||
an async addon-channel protocol that lives correctly in Lua, and the
|
||||
one client-pure function is a trivial color table. Nothing here is a
|
||||
client-DLL problem.
|
||||
|
||||
### If we synthesized it anyway
|
||||
|
||||
"Can't read it" and "can't estimate it" are separate claims. We can't
|
||||
read threat (above); *estimating* it is possible, but it splits into two
|
||||
tiers, and neither one is a DLL feature.
|
||||
|
||||
**Tier 1 — client-only proxy from the mob's target (small, honest,
|
||||
nearly useless).** The one threat-correlated fact vanilla *does* sync to
|
||||
the client is the creature's current target (`UNIT_FIELD_TARGET` in the
|
||||
descriptor — the same field `"<mob>target"` / `targettarget` reads). A
|
||||
mob attacks its highest-threat valid target, so `mobTarget == unit` ≈
|
||||
"unit is tanking". That yields:
|
||||
|
||||
- `UnitThreatSituation(unit, mob)` → `3` when the mob targets `unit`,
|
||||
else `nil`. It **cannot** produce `1` ("higher than tank, about to
|
||||
pull") or distinguish `2` from `3` — those are numeric comparisons and
|
||||
we have no numbers.
|
||||
- `UnitDetailedThreatSituation` → `isTanking` only; `status` degenerate,
|
||||
`threatpct` / `rawthreatpct` / `threatvalue` all unavailable.
|
||||
- Laggy: it changes only when the mob switches target (a descriptor
|
||||
update), so it *trails* real threat and gives zero predictive warning
|
||||
— the entire reason a threat meter exists.
|
||||
|
||||
This needs no DLL: `UnitIsUnit("<mob>target", unit)` already does it in
|
||||
pure Lua. Shipping it under the real API names would be actively
|
||||
misleading (callers expect the 0–3 gradient plus percentages), so it's
|
||||
not worth it even as a stub.
|
||||
|
||||
**Tier 2 — a real threat model (accurate-ish, huge, cooperative, ~95%
|
||||
Lua).** This is what KTM / KLHThreatMeter / vanilla-Omen actually do, and
|
||||
the only synthesis that fills the percentage/prediction dimensions the
|
||||
API is *for*:
|
||||
|
||||
1. **Model your own threat.** Parse your outgoing combat events and
|
||||
convert them to threat via a per-spell coefficient database (damage ≈
|
||||
1 threat per point, healing ≈ 0.5/HP applied to *every* mob in combat,
|
||||
flat-threat abilities like Sunder Armor / Demo Shout, per-spell
|
||||
bonus/penalty coefficients), then apply your active multipliers —
|
||||
stance/form (Defensive 1.3×, Berserker 0.8×, Bear/Cat), Righteous Fury
|
||||
(+Holy threat), Blessing of Salvation (0.7×), threat-reduction
|
||||
trinkets — and handle the sets/wipes: taunt (sets you to current top),
|
||||
Feign Death, Vanish, Fade, Cower.
|
||||
2. **Broadcast** your per-mob estimate over `SendAddonMessage` and
|
||||
**aggregate** everyone else's broadcasts into a per-mob table.
|
||||
3. Derive `isTanking` / `status` / the percentages by comparing each
|
||||
member to the current tank against the **110% melee / 130% ranged**
|
||||
pull threshold — the exact constants both Turtle's server serializer
|
||||
(`ThreatManager.cpp:494`, `threatValue*100 / (tankThreat * (isMelee ?
|
||||
1.1 : 1.3))`) and TWThreat's client fallback (`TWThreat.lua:932`)
|
||||
use. Worth stealing verbatim so a synthesized `threatpct` lines up
|
||||
with what real-threat clients show.
|
||||
|
||||
Why it isn't ours to build:
|
||||
|
||||
- The coefficient DB is **game-knowledge, not in the binary** — you
|
||||
cannot RE it out of the client; it's the classic hand-maintained
|
||||
Threat-1.0/KTM data. The parsing, broadcast, and aggregation are all
|
||||
Lua. The talent/aura/stance *inputs* are already exposed by existing
|
||||
ClassicAPI functions (`GetTalentInfo`, the aura APIs, `IsPlayerSpell`,
|
||||
the spell-mod reads). **None of it is a client-DLL problem.**
|
||||
- **Cooperative-only.** You can precisely model only members running the
|
||||
same addon and broadcasting; a non-participating tank is invisible (the
|
||||
original KTM caveat). The intuitive objection — "I can see everyone's
|
||||
damage in the combat log, why can't I model them from one client?" —
|
||||
fails for three compounding reasons, none liftable by the DLL:
|
||||
1. **You don't actually see everyone's events.** Vanilla's combat log
|
||||
is visibility/range-gated — the server only broadcasts combat events
|
||||
for units near you, not the whole raid. A stacked 5-man logs most of
|
||||
it (so a single-observer model degrades *gracefully* in dungeons); a
|
||||
spread 40-man leaves you blind to the ranged camp, the back-line
|
||||
healers, anyone outside your bubble. The DLL can't recover these —
|
||||
the server never sends the packets, so there's nothing to hook.
|
||||
2. **Threat ≠ damage, and the missing factor is hidden.** Threat is
|
||||
`damage × stance × talents × buffs`, not 1:1. The talent factor
|
||||
(Defiance, Subtlety, Feral Instinct, Shadow Affinity, Frost
|
||||
Channeling — the passive threat-mod talents) is **unreadable for
|
||||
other players in vanilla**; there's no inspect path to it. Stance /
|
||||
Blessing of Salvation / Fade are at least observable auras when the
|
||||
unit is in range, but talents are pure hidden state, so an observer
|
||||
can't derive a stranger's coefficient from their damage.
|
||||
3. **Much threat has no attributable log line.** In-combat healing
|
||||
generates threat on *every* mob the healer is engaged with (not the
|
||||
mob the heal touched); buffs / HoTs / PW:Fortitude, flat-threat
|
||||
abilities, and pet threat either don't appear in the log or can't be
|
||||
pinned to the right mob from a single line.
|
||||
|
||||
Broadcasting sidesteps all three because each client computes threat
|
||||
from *private, authoritative* knowledge — its own exact casts, its own
|
||||
talents, its own active stance/buffs — then shares the finished number.
|
||||
The Tier-1 mob-target read is worth keeping here *only* as a cross-check
|
||||
— it reveals who actually holds aggro regardless of who's broadcasting.
|
||||
- **It's an estimate that drifts.** Addon threat models desync from the
|
||||
server's true numbers (rounding, unmodeled effects, out-of-range or
|
||||
missed events). That drift is precisely why Blizzard later added
|
||||
`SMSG_THREAT_UPDATE` and why Turtle chose a server serializer over
|
||||
blessing an addon guess.
|
||||
|
||||
**The only DLL-shaped angle — and it's an *input*, not threat.** The one
|
||||
place a DLL beats Lua is the feed: vanilla's combat log is localized text
|
||||
keyed by unit *name* (ambiguous with duplicate names, breaks per-locale).
|
||||
Hooking the damage/heal packet handlers (`SMSG_SPELLNONMELEEDAMAGELOG`,
|
||||
`SMSG_ATTACKERSTATEUPDATE`, `SMSG_PERIODICAURALOG`) to emit precise,
|
||||
GUID-keyed, unlocalized damage/heal events would make any Lua threat
|
||||
model far more robust. But (a) that's a **separate feature** — a
|
||||
`COMBAT_LOG_EVENT`-style feed, useful well beyond threat — that should be
|
||||
filed on its own merits; (b) it still only *feeds* the Lua model — threat
|
||||
stays an estimate, computed cooperatively in Lua; and (c) on the actual
|
||||
target environment SuperWoW already exposes GUID-keyed combat info +
|
||||
`UNIT_CASTEVENT`, so even this niche is largely pre-filled.
|
||||
|
||||
**Bottom line on synthesis:** Tier 1 is a Lua one-liner too degenerate to
|
||||
wear the API's name; Tier 2 is a full KTM/Omen reimplementation that is
|
||||
almost entirely Lua data + logic, cooperative-only, and inherently an
|
||||
estimate. Anyone who wants vanilla threat should run an existing threat
|
||||
addon. ClassicAPI's only worthwhile contribution in this space is an
|
||||
unrelated precise-combat-log event feed — filed as its own thing, not as
|
||||
"threat".
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# 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,38 @@
|
||||
// Generated from cmake/version.rc.in by CMake configure_file().
|
||||
// Stamps the PE VERSIONINFO resource so Explorer -> Properties -> Details
|
||||
// and GetFileVersionInfo() report the same version as the CLASSIC_API_VERSION
|
||||
// Lua global. Both derive from the CLASSICAPI_TAG (vMAJOR.MINOR.PATCH) cache
|
||||
// var; an untagged dev build gets the 9999.99.99 "newer than everything"
|
||||
// sentinel.
|
||||
|
||||
#include <winver.h>
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION @CLASSICAPI_VER_MAJOR@,@CLASSICAPI_VER_MINOR@,@CLASSICAPI_VER_PATCH@,0
|
||||
PRODUCTVERSION @CLASSICAPI_VER_MAJOR@,@CLASSICAPI_VER_MINOR@,@CLASSICAPI_VER_PATCH@,0
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "ClassicAPI Team"
|
||||
VALUE "FileDescription", "ClassicAPI - modern WoW API backport for 1.12.1"
|
||||
VALUE "FileVersion", "@CLASSICAPI_VER_STRING@"
|
||||
VALUE "InternalName", "ClassicAPI"
|
||||
VALUE "LegalCopyright", "Licensed under GPLv3"
|
||||
VALUE "OriginalFilename", "ClassicAPI.dll"
|
||||
VALUE "ProductName", "ClassicAPI"
|
||||
VALUE "ProductVersion", "@CLASSICAPI_VER_STRING@"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
// 0x0409 = U.S. English, 1200 = Unicode codepage.
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
+5159
-882
File diff suppressed because it is too large
Load Diff
+9
-2
@@ -3,7 +3,9 @@
|
||||
Catalogue of the Lua C API functions linked into the client. Vanilla WoW is
|
||||
built against **Lua 5.0** (not 5.1), and the entire API plus the auxiliary
|
||||
library lives in a contiguous block of `.text` from roughly `0x006F2000` to
|
||||
`0x006F5100`. ~120 functions; the public C API is the core of that.
|
||||
`0x006F5100`. ~120 functions; the public C API is the core of that. The
|
||||
thread / coroutine machinery (`lua_resume`, `lua_yield`, the inner
|
||||
`luaE_newthread`) lives slightly higher at `0x006F6000`–`0x006F6900`.
|
||||
|
||||
**Cross-verified against two binaries:**
|
||||
|
||||
@@ -75,6 +77,8 @@ context but not byte-by-byte verified.
|
||||
|
||||
| VA | Function | Conv. | Notes | Conf |
|
||||
|--------------|-------------------------|----------|--------------------------------------------------------------|------|
|
||||
| `0x006F2F80` | `lua_xmove` | fastcall | `(from, to, n)` — decr `from->top` by `n*0x10`, copy each 16-byte TValue, incr `to->top`. Used by `coroutine.*` to shuttle args/returns between threads | ★ |
|
||||
| `0x006F3030` | `lua_newthread` | fastcall | GC check then calls inner `luaE_newthread` at `0x006F6B10`; pushes the new state onto L with tag 8 (LUA_TTHREAD). Returns `lua_State *` in eax — Ghidra reports `void` (decompiler artifact) | ★ |
|
||||
| `0x006F3070` | `lua_gettop` | fastcall | `(top - base) >> 4` | ★ |
|
||||
| `0x006F3080` | `lua_settop` | fastcall | | ✓ |
|
||||
| `0x006F30D0` | `lua_remove` | fastcall | shift-down loop + decr_top; previously misidentified as `lua_pushvalue` | ★ |
|
||||
@@ -155,7 +159,10 @@ behaviour the comment describes (`push 0; mov edx, 1; mov ecx, L`) is
|
||||
| `0x006F4290` | *internal CFunction trampoline* | | called via `lua_cpcall` | ★ |
|
||||
| `0x006F4320` | `lua_load` | fastcall | reader / loader entry | ★ |
|
||||
| `0x006F4370` | `lua_dump` | fastcall | requires tag 6 + `Closure.isC == 0` | ★ |
|
||||
| `0x006F4940` | `lua_error` | cdecl | `(L, msg)` — args on stack; reaches `luaD_throw` via wrappers | ✓ |
|
||||
| `0x006F4940` | `lua_error` | **cdecl** varargs | `(L, fmt, ...)` — printf-style; engine prepends `luaL_where` then `lua_concat`. Single-string callers OK (cdecl tolerates extra-arg-less calls); user-controlled strings should use `Error(L, "%s", msg)` to avoid format-string interpretation | ✓ |
|
||||
| `0x006F6620` | `lua_resume` | fastcall | `(L_co, nargs) → int`. Zero internal callers (coroutine lib was stripped from registration tables); linker kept the symbol. Stores nargs in a local, calls `luaD_rawrunprotected(L, &resume_inner, &nargs)` at `0x006F5DAC`; inner static helper at `0x006F67B0` | ★ |
|
||||
| `0x006F6860` | `lua_yield` | fastcall | `(L, nresults) → int (-1)`. Sets `0x10` flag on CallInfo state field rather than directly writing `L->status` like later Lua versions. Errors with "attempt to yield across metamethod/C-call boundary" or "cannot yield a C function" when called from the wrong context | ★ |
|
||||
| `0x006F6B10` | *internal* `luaE_newthread` | fastcall | Inner allocator called by `lua_newthread` at `0x006F3030`. Allocates the new state, sets tag 8 via the push helper at `0x006F7B20`, inits via `0x006F6C40`, copies global-state fields from the parent. Returns `lua_State *` | ★ |
|
||||
|
||||
### Misc / GC / version
|
||||
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// 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"
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace Common {
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// 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"
|
||||
|
||||
+65
-9
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
@@ -48,7 +48,16 @@ namespace Lua {
|
||||
F(PCall, lua_pcall, LUA_PCALL) \
|
||||
F(Next, lua_next, LUA_NEXT) \
|
||||
F(Type, lua_type, LUA_TYPE) \
|
||||
F(Error, lua_error, LUA_ERROR)
|
||||
F(Error, lua_error, LUA_ERROR) \
|
||||
F(ToPointer, lua_topointer, LUA_TO_POINTER) \
|
||||
F(ToThread, lua_tothread, LUA_TO_THREAD) \
|
||||
F(IsCFunction, lua_iscfunction, LUA_IS_C_FUNCTION)\
|
||||
F(XMove, lua_xmove, LUA_X_MOVE) \
|
||||
F(NewThread, lua_newthread, LUA_NEW_THREAD) \
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
|
||||
F(SetN, luaL_setn, LUAL_SETN)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
@@ -186,20 +195,67 @@ 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. The engine ships it as both a
|
||||
// global and as `string.format`; the global is faster (one
|
||||
// gettable instead of two) and equally available across builds.
|
||||
// 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(fmt, n) → string
|
||||
Call(L, 2, 1); // format(expandedFmt, n) → string
|
||||
}
|
||||
} // namespace Lua
|
||||
|
||||
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);
|
||||
} // namespace
|
||||
|
||||
void RegisterCommand(const char *name, CommandHandler handler, int category,
|
||||
const char *description) {
|
||||
auto fn = reinterpret_cast<RegisterCommand_t>(Offsets::FUN_CONSOLE_COMMAND_REGISTER);
|
||||
fn(name, reinterpret_cast<void *>(handler), category, description);
|
||||
}
|
||||
|
||||
void Write(const char *line) {
|
||||
auto fn = reinterpret_cast<Write_t>(Offsets::FUN_CONSOLE_WRITE);
|
||||
fn(line, 0);
|
||||
}
|
||||
} // namespace Console
|
||||
|
||||
namespace {
|
||||
ModuleAutoRegister *g_moduleHead = nullptr;
|
||||
GlueModuleAutoRegister *g_glueModuleHead = nullptr;
|
||||
|
||||
+80
-4
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// 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
|
||||
@@ -49,6 +49,8 @@ constexpr int TYPE_NUMBER = 3;
|
||||
constexpr int TYPE_STRING = 4;
|
||||
constexpr int TYPE_TABLE = 5;
|
||||
constexpr int TYPE_FUNCTION = 6;
|
||||
constexpr int TYPE_USERDATA = 7;
|
||||
constexpr int TYPE_THREAD = 8;
|
||||
|
||||
using lua_isnumber_t = bool(__fastcall *)(void *L, int index);
|
||||
using lua_isstring_t = bool(__fastcall *)(void *L, int index);
|
||||
@@ -76,7 +78,28 @@ 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_error_t = void(__cdecl *)(void *L, const char *);
|
||||
// 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
|
||||
// is fine because cdecl tolerates extra-arg-less calls. New callers
|
||||
// forwarding user-supplied error strings should use `Error(L, "%s",
|
||||
// userMsg)` to avoid format-string interpretation of `%` in the
|
||||
// message body.
|
||||
using lua_error_t = void(__cdecl *)(void *L, const char *fmt, ...);
|
||||
using lua_topointer_t = const void *(__fastcall *)(void *L, int idx);
|
||||
using lua_tothread_t = void *(__fastcall *)(void *L, int idx);
|
||||
using lua_iscfunction_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_xmove_t = void(__fastcall *)(void *from, void *to, int n);
|
||||
// `lua_newthread` returns the new `lua_State *` in eax, but Ghidra
|
||||
// infers `void` because the inner call's value just flows through.
|
||||
// We declare the return type honestly here; callers that don't want
|
||||
// to rely on the calling convention preserving eax can read the
|
||||
// thread back from L's top via `ToPointer(L, -1)`.
|
||||
using lua_newthread_t = void *(__fastcall *)(void *L);
|
||||
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);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -110,6 +133,15 @@ extern const lua_pcall_t PCall;
|
||||
extern const lua_next_t Next;
|
||||
extern const lua_type_t Type;
|
||||
extern const lua_error_t Error;
|
||||
extern const lua_topointer_t ToPointer;
|
||||
extern const lua_tothread_t ToThread;
|
||||
extern const lua_iscfunction_t IsCFunction;
|
||||
extern const lua_xmove_t XMove;
|
||||
extern const lua_newthread_t NewThread;
|
||||
extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
extern const luaL_setn_t SetN;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
@@ -212,6 +244,50 @@ void PushLocalizedFormatInt(void *L, const char *globalName,
|
||||
const char *fallback, int n);
|
||||
} // namespace Lua
|
||||
|
||||
// Developer-console command system — the `~` console available when the
|
||||
// client is launched with `-console`. Vanilla maintains a
|
||||
// `TSExplicitList<CONSOLECOMMAND>`; these wrap the engine's registrar
|
||||
// and output paths. See the "Console commands & Interface file export"
|
||||
// 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);
|
||||
|
||||
// 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).
|
||||
enum Category {
|
||||
CATEGORY_DEBUG = 0,
|
||||
CATEGORY_GRAPHICS = 1,
|
||||
CATEGORY_CONSOLE = 2,
|
||||
CATEGORY_COMBAT = 3,
|
||||
CATEGORY_GAME = 4,
|
||||
CATEGORY_DEFAULT = 5,
|
||||
CATEGORY_NET = 6,
|
||||
CATEGORY_SOUND = 7,
|
||||
CATEGORY_GM = 8,
|
||||
};
|
||||
|
||||
// Registers a console command. `name` and `description` are stored BY
|
||||
// POINTER (the engine doesn't copy them), so pass string literals or
|
||||
// otherwise process-lifetime storage. Dedup-safe: re-registering the
|
||||
// same name is a harmless no-op, so it's fine to call from a hook that
|
||||
// fires more than once. Safe any time after engine boot — the command
|
||||
// table self-initializes on first use. `description` may be nullptr.
|
||||
void RegisterCommand(const char *name, CommandHandler handler, int category,
|
||||
const char *description);
|
||||
|
||||
// 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);
|
||||
|
||||
} // namespace Console
|
||||
|
||||
// Self-registration for API modules. Each module .cpp declares a file-scope
|
||||
// `static const Game::ModuleAutoRegister _r{&RegisterLuaFunctions};`, which
|
||||
// chains itself onto a global list at DLL-load time. `RunModuleRegistrations`
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
+2223
-40
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetActionInfo(slot)` — modern-WoW-style action descriptor lookup.
|
||||
|
||||
+60
-4
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// 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
|
||||
@@ -40,13 +40,14 @@
|
||||
// Same pattern WeirdUtils.dll uses (we reverse-engineered it from
|
||||
// `0x10015A74` while figuring this out). The addon load pass then
|
||||
// reads each `.lua` file through the same hooked read path —
|
||||
// `GlobalStrings.lua`, `Util\Print.lua`, etc. all surface from
|
||||
// `Compat.lua`, `Util\Print.lua`, etc. all surface from
|
||||
// embedded buffers.
|
||||
|
||||
#include "embedded_classicapi.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "bindings/Inject.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -248,6 +249,14 @@ void DecideSource() {
|
||||
int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
size_t *outSize, size_t extraBytes,
|
||||
int flag1, int flag2) {
|
||||
// FrameXML Bindings.xml gets an inline splice for our TARGETING
|
||||
// additions — see `bindings/Inject.cpp`. Returns true with the
|
||||
// patched buffer ready for the engine's binding parser to consume.
|
||||
if (Bindings::Inject::TryHandle(unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2, FileRead_o)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *suffix = StripAddonPrefix(path);
|
||||
if (suffix == nullptr) {
|
||||
// Path isn't under `Interface\AddOns\!!!ClassicAPI\` — straight
|
||||
@@ -301,11 +310,58 @@ using AddonInit_t = void(__fastcall *)(const char *accountName);
|
||||
AddonInit_t AddonInit_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.
|
||||
//
|
||||
// Entries are pointed to by `[VAR_ADDON_LIST_HEAD]` (`0x00BE1B6C`),
|
||||
// with the next-pointer field at `entry + 0x10` (computed as
|
||||
// `*(value-at-VAR_ADDON_LIST_CTRL) + entry + 4` = `0xC + entry + 4`
|
||||
// = `entry + 0x10`). Low-bit-1 means "sentinel" (end of list).
|
||||
// Name pointer lives at `entry + 0x14`.
|
||||
constexpr uintptr_t VAR_ADDON_LIST_HEAD = 0x00BE1B6C;
|
||||
constexpr int OFF_ADDON_ENTRY_NAME_PTR = 0x14;
|
||||
|
||||
void MoveEmbeddedToListHead() {
|
||||
uintptr_t entry = *reinterpret_cast<uintptr_t *>(VAR_ADDON_LIST_HEAD);
|
||||
const int linkOffset = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL));
|
||||
while ((entry & 1) == 0 && entry != 0) {
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + OFF_ADDON_ENTRY_NAME_PTR);
|
||||
if (name != nullptr && std::strcmp(name, kAddonName) == 0) {
|
||||
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;
|
||||
}
|
||||
entry = *reinterpret_cast<const uintptr_t *>(
|
||||
entry + linkOffset + 4);
|
||||
}
|
||||
}
|
||||
|
||||
void __fastcall AddonInit_h(const char *accountName) {
|
||||
AddonInit_o(accountName);
|
||||
auto TocParser = reinterpret_cast<TocParser_t>(Offsets::FUN_TOC_PARSER);
|
||||
TocParser(kAddonName);
|
||||
MoveEmbeddedToListHead();
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookFileRead{
|
||||
|
||||
+118
-7
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_AddOns.*` convenience wrappers. Modern WoW splits the legacy
|
||||
@@ -41,6 +41,11 @@ using CanLoadFn_t = uint8_t(__fastcall *)(const uint8_t *entry, char fullCheck,
|
||||
int *outStatus, int *outDepHandle,
|
||||
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`.
|
||||
using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry);
|
||||
|
||||
// Resolves the user's arg to a `const uint8_t *` that the engine's
|
||||
// per-field accessors can consume. They all read the addon name as
|
||||
@@ -115,13 +120,24 @@ int PushFieldOrNil(void *L, FieldByName_t accessor) {
|
||||
// `C_AddOns.GetAddOnName(indexOrName)` — the addon's directory name.
|
||||
// For numeric input, returns the engine's canonical casing. For
|
||||
// string input, returns the input verbatim once existence is
|
||||
// confirmed.
|
||||
// confirmed; nil for an unregistered name.
|
||||
//
|
||||
// String-input existence isn't free: `ResolveAddOnName` echoes the
|
||||
// string for the field accessors below (`GetAddOnTitle`, etc.) which
|
||||
// short-circuit cleanly on unknown names via the engine's own NULL-
|
||||
// return paths. `GetAddOnName` doesn't call any engine accessor, so
|
||||
// it'd otherwise echo any string. Probe the registry explicitly.
|
||||
int __fastcall Script_GetAddOnName(void *L) {
|
||||
const uint8_t *entry = ResolveAddOnName(L);
|
||||
if (entry == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_STRING &&
|
||||
!AddOnExistsByName(reinterpret_cast<const char *>(entry))) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushString(L, reinterpret_cast<const char *>(entry));
|
||||
return 1;
|
||||
}
|
||||
@@ -163,9 +179,13 @@ int __fastcall Script_IsAddOnLoadable(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
auto isLoD = reinterpret_cast<EntryByteFn_t>(
|
||||
Offsets::FUN_ADDON_IS_LOAD_ON_DEMAND);
|
||||
if (isLoD(entry) != 0) {
|
||||
// Already-loaded addons are trivially loadable — skip the
|
||||
// recursive dep walk and return (true, nil) directly. `IS_LOADED`
|
||||
// reads the byte at `entry+0x18` (the loader sets that to 1 on
|
||||
// successful load).
|
||||
auto isLoaded = reinterpret_cast<EntryByteFn_t>(
|
||||
Offsets::FUN_ADDON_IS_LOADED);
|
||||
if (isLoaded(entry) != 0) {
|
||||
Game::Lua::PushBoolean(L, 1);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
@@ -229,6 +249,36 @@ int __fastcall Script_GetAddOnSecurity(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_AddOns.IsAddOnLoaded(indexOrName)` → `(loadedOrLoading, loaded)`.
|
||||
//
|
||||
// Modern WoW distinguishes "load-in-progress" from "fully loaded" —
|
||||
// matters for LoD addons whose load is split across multiple
|
||||
// `LoadAddOn` callbacks. Vanilla 1.12's `FUN_0051F240` is fully
|
||||
// synchronous: the loaded byte at `entry+0x18` flips from 0 → 1
|
||||
// inside a single call, so the "loading" state is never observable
|
||||
// from Lua. We return the same boolean twice — `loadedOrLoading`
|
||||
// and `loaded` are identical here.
|
||||
//
|
||||
// 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.
|
||||
int __fastcall Script_IsAddOnLoaded(void *L) {
|
||||
const uint8_t *entry = ResolveAddOnName(L);
|
||||
if (entry == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
auto isLoaded = reinterpret_cast<EntryByteFn_t>(
|
||||
Offsets::FUN_ADDON_IS_LOADED);
|
||||
const bool loaded = isLoaded(entry) != 0;
|
||||
Game::Lua::PushBool(L, loaded);
|
||||
Game::Lua::PushBool(L, loaded);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `C_AddOns.DoesAddOnExist(indexOrName)` — true iff the engine's
|
||||
// addon registry has a matching entry. For numeric input we trust
|
||||
// `GetByIndex`'s OOR-NULL; for string input we walk the flat array,
|
||||
@@ -250,6 +300,63 @@ int __fastcall Script_DoesAddOnExist(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `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.
|
||||
//
|
||||
// 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.
|
||||
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);
|
||||
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);
|
||||
if (count == 0)
|
||||
return 0;
|
||||
auto names = *reinterpret_cast<const char *const *const *>(
|
||||
record + Offsets::OFF_ADDON_OPTIONALDEPS_ARRAY);
|
||||
if (names == nullptr)
|
||||
return 0;
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
Game::Lua::PushString(L, names[i]); // NULL → pushnil, per engine
|
||||
return static_cast<int>(count);
|
||||
}
|
||||
|
||||
// `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
|
||||
@@ -276,6 +383,10 @@ static void RegisterLuaFunctions() {
|
||||
&Script_GetAddOnSecurity);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "DoesAddOnExist",
|
||||
&Script_DoesAddOnExist);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "IsAddOnLoaded",
|
||||
&Script_IsAddOnLoaded);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnOptionalDependencies",
|
||||
&Script_GetAddOnOptionalDependencies);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AddOnSecurityStatus",
|
||||
kAddOnSecurityStatusEntries,
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `CLASSIC_API_VERSION` global — a single integer addons can read for
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid,
|
||||
// buyout)` — post `numStacks` auctions of `quantity` each from a single
|
||||
// source stack, in one call. A ClassicAPI extension modelled on the retail
|
||||
// multi-sell flow; vanilla's `StartAuction` posts exactly one item object.
|
||||
//
|
||||
// Why this is a tick-driven state machine (not a synchronous loop):
|
||||
// vanilla's `CMSG_AUCTION_SELL_ITEM` (opcode 0x256) is
|
||||
// `{ auctioneerGuid, itemGuid, bid, buyout, runTime }` — NO count field
|
||||
// (WotLK added the server-side count; 1.12 has none, verified from
|
||||
// `Script_StartAuction`). So the server posts the *entire* item object a
|
||||
// GUID points at. To post a partial `quantity` out of a larger source, the
|
||||
// client must first materialize that quantity as its own server-committed
|
||||
// item object — i.e. split it into a bag slot (`CMSG_SPLIT_ITEM`) — and both
|
||||
// the split and the resulting post are server-authoritative and async (the
|
||||
// new stack's GUID doesn't exist client-side until `SMSG_UPDATE_OBJECT`
|
||||
// lands). We drive the split→post→split→post sequence off `WorldTick`,
|
||||
// keying each transition on observed bag state.
|
||||
//
|
||||
// Free-slot cost: a partial post needs at most ONE reused general-bag slot
|
||||
// (split `quantity` off → post that stack → the post empties the slot →
|
||||
// reuse it for the next split). The final stack (once the source is whittled
|
||||
// to exactly `quantity`) posts the source object directly, no split. Posting
|
||||
// a whole existing stack (`quantity` == the source count) needs zero free
|
||||
// slots.
|
||||
//
|
||||
// Posting reuses the engine's `Script_StartAuction` verbatim (write the
|
||||
// target stack's GUID into the sell-slot globals, push bid/buyout/runTime,
|
||||
// call it) so all of its validation runs per stack — damaged-item reject,
|
||||
// buyout normalization, runTime table check, auctioneer/"AH open" check.
|
||||
//
|
||||
// Events (retail multi-sell names, fired via Event::Custom):
|
||||
// AUCTION_MULTISELL_START — job accepted, posting begins
|
||||
// AUCTION_MULTISELL_UPDATE(done, total) — after each stack lands
|
||||
// AUCTION_MULTISELL_FAILURE — aborted (source changed, no slot,
|
||||
// AH closed, or a step timed out)
|
||||
//
|
||||
// Limitations (v1): source is the single stack at `itemLocation` (not
|
||||
// aggregated across bags); temp splits use general-purpose bags only, so
|
||||
// family-restricted items (arrows/shards in specialty bags) aren't
|
||||
// supported; one job at a time.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Swap.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace AuctionHouse::PostItem {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kEvtStart = "AUCTION_MULTISELL_START";
|
||||
constexpr const char *kEvtUpdate = "AUCTION_MULTISELL_UPDATE";
|
||||
constexpr const char *kEvtFailure = "AUCTION_MULTISELL_FAILURE";
|
||||
const Event::Custom::AutoReserve _r1{kEvtStart};
|
||||
const Event::Custom::AutoReserve _r2{kEvtUpdate};
|
||||
const Event::Custom::AutoReserve _r3{kEvtFailure};
|
||||
|
||||
// Generous per-step abort bound. A split or a post is one server round-trip
|
||||
// (~tens of frames); 600 frames (~10s at 60fps) means "something's wrong"
|
||||
// (disconnect, server refusal we can't see) rather than normal latency.
|
||||
constexpr int kMaxWaitTicks = 600;
|
||||
|
||||
enum class Phase { Idle, SplitWait, PostWait };
|
||||
|
||||
struct Job {
|
||||
bool active;
|
||||
int srcBag, srcSlot; // source stack (shrinks in place as we split off it)
|
||||
int itemID; // guards against the source slot changing under us
|
||||
int quantity;
|
||||
int totalStacks;
|
||||
int postedStacks;
|
||||
int bid, buyout, runTime;
|
||||
Phase phase;
|
||||
int workBag, workSlot; // slot currently being split-into / posted-from
|
||||
uint64_t postGuid; // GUID we posted — detect it leaving bags
|
||||
int waitTicks;
|
||||
};
|
||||
Job g_job{};
|
||||
|
||||
// --- small CGItem / cache readers ---------------------------------------
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, int unused);
|
||||
const uint8_t *PeekItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int CGItemCount(const uint8_t *item) {
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const int *>(desc + Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
}
|
||||
|
||||
uint64_t CGItemGuid(const uint8_t *item) {
|
||||
auto *inst = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_INSTANCE_BLOCK);
|
||||
if (inst == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(inst + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
}
|
||||
|
||||
struct SlotInfo {
|
||||
const uint8_t *item; // null = empty slot
|
||||
int itemID;
|
||||
int count;
|
||||
uint64_t guid;
|
||||
};
|
||||
|
||||
SlotInfo ReadSlot(void *L, int bag, int slot) {
|
||||
SlotInfo r{nullptr, 0, 0, 0};
|
||||
const uint8_t *item = Item::Location::ResolveBag(L, bag, slot);
|
||||
if (item == nullptr)
|
||||
return r;
|
||||
r.item = item;
|
||||
r.itemID = Item::ID::FromCGItem(item);
|
||||
r.count = CGItemCount(item);
|
||||
r.guid = CGItemGuid(item);
|
||||
return r;
|
||||
}
|
||||
|
||||
// --- auction-house globals ----------------------------------------------
|
||||
|
||||
uint64_t AuctioneerGuid() {
|
||||
const uint32_t lo = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_AUCTION_AUCTIONEER_GUID_LO);
|
||||
const uint32_t hi = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_AUCTION_AUCTIONEER_GUID_HI);
|
||||
return (static_cast<uint64_t>(hi) << 32) | lo;
|
||||
}
|
||||
bool AHOpen() { return AuctioneerGuid() != 0; }
|
||||
|
||||
void SetSellGuid(uint64_t guid) {
|
||||
*reinterpret_cast<uint32_t *>(Offsets::VAR_AUCTION_SELL_GUID_LO) =
|
||||
static_cast<uint32_t>(guid);
|
||||
*reinterpret_cast<uint32_t *>(Offsets::VAR_AUCTION_SELL_GUID_HI) =
|
||||
static_cast<uint32_t>(guid >> 32);
|
||||
}
|
||||
|
||||
// Point the sell slot at `guid` and fire the engine's own StartAuction for
|
||||
// this bid/buyout/runTime. StartAuction reads its args from L[1..3]; we own
|
||||
// the stack at frame-tail, so set top to 0, push the three, call, reset.
|
||||
void PostStack(void *L, uint64_t guid) {
|
||||
SetSellGuid(guid);
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_job.bid));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_job.buyout));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_job.runTime));
|
||||
auto fn = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_START_AUCTION);
|
||||
fn(L);
|
||||
Game::Lua::SetTop(L, 0);
|
||||
}
|
||||
|
||||
// --- free-slot discovery (general-purpose bags only) --------------------
|
||||
|
||||
bool BagIsGeneral(int bag) {
|
||||
if (bag == 0)
|
||||
return true; // backpack is always general
|
||||
if (bag < 1 || bag > 4)
|
||||
return false;
|
||||
const uint8_t *bagItem =
|
||||
Item::Location::ResolveEquipmentSlot(Offsets::INVSLOT_BAG1 + bag - 1);
|
||||
if (bagItem == nullptr)
|
||||
return false; // no bag equipped there
|
||||
const int id = Item::ID::FromCGItem(bagItem);
|
||||
if (id == 0)
|
||||
return false;
|
||||
const uint8_t *rec = PeekItemRecord(static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
return *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_ITEMSTATS_BAG_FAMILY) == 0;
|
||||
}
|
||||
|
||||
bool FindFreeGeneralSlot(void *L, int *outBag, int *outSlot) {
|
||||
for (int bag = 0; bag <= 4; ++bag) {
|
||||
if (!BagIsGeneral(bag))
|
||||
continue;
|
||||
const int n = Item::Location::GetBagSlotCount(bag);
|
||||
for (int slot = 1; slot <= n; ++slot) {
|
||||
if (Item::Location::ResolveBag(L, bag, slot) == nullptr) {
|
||||
*outBag = bag;
|
||||
*outSlot = slot;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- state machine ------------------------------------------------------
|
||||
|
||||
void Finish(bool success) {
|
||||
g_job.active = false;
|
||||
if (!success)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtFailure), "");
|
||||
// success needs no event: the last PostWait already fired
|
||||
// AUCTION_MULTISELL_UPDATE(total, total).
|
||||
}
|
||||
|
||||
void Tick() {
|
||||
if (!g_job.active)
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int savedTop = Game::Lua::GetTop(L);
|
||||
|
||||
if (!AHOpen()) { // window closed / walked away mid-job
|
||||
Finish(false);
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (g_job.phase) {
|
||||
case Phase::Idle: {
|
||||
if (g_job.postedStacks >= g_job.totalStacks) {
|
||||
Finish(true);
|
||||
break;
|
||||
}
|
||||
const SlotInfo s = ReadSlot(L, g_job.srcBag, g_job.srcSlot);
|
||||
if (s.item == nullptr || s.itemID != g_job.itemID || s.count < g_job.quantity) {
|
||||
Finish(false); // source moved / consumed / insufficient
|
||||
break;
|
||||
}
|
||||
if (s.count == g_job.quantity) {
|
||||
// Final exact stack — post the source object directly, no split.
|
||||
g_job.workBag = g_job.srcBag;
|
||||
g_job.workSlot = g_job.srcSlot;
|
||||
g_job.postGuid = s.guid;
|
||||
PostStack(L, s.guid);
|
||||
g_job.phase = Phase::PostWait;
|
||||
g_job.waitTicks = 0;
|
||||
} else {
|
||||
int fb = 0, fs = 0;
|
||||
if (!FindFreeGeneralSlot(L, &fb, &fs)) {
|
||||
Finish(false); // no room to materialize the stack
|
||||
break;
|
||||
}
|
||||
if (!Item::Swap::MoveCount(L, g_job.srcBag, g_job.srcSlot, fb, fs,
|
||||
g_job.quantity)) {
|
||||
Finish(false);
|
||||
break;
|
||||
}
|
||||
g_job.workBag = fb;
|
||||
g_job.workSlot = fs;
|
||||
g_job.phase = Phase::SplitWait;
|
||||
g_job.waitTicks = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Phase::SplitWait: {
|
||||
const SlotInfo w = ReadSlot(L, g_job.workBag, g_job.workSlot);
|
||||
if (w.item != nullptr && w.itemID == g_job.itemID && w.count == g_job.quantity) {
|
||||
g_job.postGuid = w.guid;
|
||||
PostStack(L, w.guid);
|
||||
g_job.phase = Phase::PostWait;
|
||||
g_job.waitTicks = 0;
|
||||
} else if (++g_job.waitTicks > kMaxWaitTicks) {
|
||||
Finish(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Phase::PostWait: {
|
||||
const SlotInfo w = ReadSlot(L, g_job.workBag, g_job.workSlot);
|
||||
// Post confirmed once the server pulls the item out of the slot.
|
||||
if (w.item == nullptr || w.guid != g_job.postGuid) {
|
||||
++g_job.postedStacks;
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtUpdate), "%d%d",
|
||||
g_job.postedStacks, g_job.totalStacks);
|
||||
g_job.phase = Phase::Idle;
|
||||
g_job.waitTicks = 0;
|
||||
} else if (++g_job.waitTicks > kMaxWaitTicks) {
|
||||
Finish(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&Tick};
|
||||
|
||||
// --- Lua entry ----------------------------------------------------------
|
||||
|
||||
// Reads `loc.field` as an int. False if missing/non-numeric. Leaves the
|
||||
// stack as found. (Same shape as Item::Location's file-static helper.)
|
||||
bool ReadIntField(void *L, int locIdx, const char *field, int *out) {
|
||||
Game::Lua::PushString(L, field);
|
||||
Game::Lua::GetTable(L, locIdx);
|
||||
const bool ok = Game::Lua::IsNumber(L, -1);
|
||||
if (ok)
|
||||
*out = static_cast<int>(Game::Lua::ToNumber(L, -1));
|
||||
Game::Lua::SetTop(L, -2);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Map the modern `duration` arg to a valid engine runTime (minutes).
|
||||
// Accepts the 1/2/3 index form and the raw {120,480,1440} value form.
|
||||
// Returns 0 for anything else.
|
||||
int MapDuration(int d) {
|
||||
auto *table = reinterpret_cast<const uint32_t *>(Offsets::VAR_AUCTION_DURATION_TABLE);
|
||||
if (d >= 1 && d <= Offsets::AUCTION_DURATION_TABLE_COUNT)
|
||||
return static_cast<int>(table[d - 1]);
|
||||
for (int i = 0; i < Offsets::AUCTION_DURATION_TABLE_COUNT; ++i)
|
||||
if (static_cast<int>(table[i]) == d)
|
||||
return d;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Push (false, reason) and return 2 — the immediate-rejection shape.
|
||||
int Reject(void *L, const char *reason) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushString(L, reason);
|
||||
return 2;
|
||||
}
|
||||
|
||||
constexpr int kLuaTable = 5; // LUA_TTABLE (no named constant in Game::Lua)
|
||||
|
||||
int __fastcall Script_C_AuctionHouse_PostItem(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != kLuaTable) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid, buyout)");
|
||||
return 0;
|
||||
}
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3) ||
|
||||
!Game::Lua::IsNumber(L, 4) || !Game::Lua::IsNumber(L, 5)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid, buyout)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int bagID = 0, slotIndex = 0;
|
||||
if (!ReadIntField(L, 1, "bagID", &bagID) ||
|
||||
!ReadIntField(L, 1, "slotIndex", &slotIndex))
|
||||
return Reject(L, "itemLocation must be a { bagID, slotIndex } table");
|
||||
|
||||
const int duration = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
const int quantity = static_cast<int>(Game::Lua::ToNumber(L, 3));
|
||||
const int numStacks = static_cast<int>(Game::Lua::ToNumber(L, 4));
|
||||
const int bid = static_cast<int>(Game::Lua::ToNumber(L, 5));
|
||||
const int buyout = Game::Lua::IsNumber(L, 6) ? static_cast<int>(Game::Lua::ToNumber(L, 6)) : 0;
|
||||
|
||||
const int runTime = MapDuration(duration);
|
||||
if (runTime == 0)
|
||||
return Reject(L, "invalid duration (use 1/2/3 or 120/480/1440)");
|
||||
if (quantity < 1 || quantity > 255)
|
||||
return Reject(L, "quantity must be 1..255");
|
||||
if (numStacks < 1)
|
||||
return Reject(L, "numStacks must be >= 1");
|
||||
if (bid < 1)
|
||||
return Reject(L, "bid must be >= 1");
|
||||
|
||||
if (!AHOpen())
|
||||
return Reject(L, "auction house is not open");
|
||||
if (g_job.active)
|
||||
return Reject(L, "a multi-post is already in progress");
|
||||
|
||||
const SlotInfo s = ReadSlot(L, bagID, slotIndex);
|
||||
if (s.item == nullptr)
|
||||
return Reject(L, "source slot is empty");
|
||||
|
||||
const long need = static_cast<long>(quantity) * numStacks;
|
||||
if (static_cast<long>(s.count) < need)
|
||||
return Reject(L, "not enough items for numStacks x quantity");
|
||||
|
||||
// A partial post (source larger than one stack) needs one reusable
|
||||
// general-bag slot to materialize each split. A whole-stack post
|
||||
// (count == quantity, numStacks == 1) needs none.
|
||||
if (s.count > quantity) {
|
||||
int fb = 0, fs = 0;
|
||||
if (!FindFreeGeneralSlot(L, &fb, &fs))
|
||||
return Reject(L, "need at least one free general bag slot to split");
|
||||
}
|
||||
|
||||
g_job = Job{};
|
||||
g_job.active = true;
|
||||
g_job.srcBag = bagID;
|
||||
g_job.srcSlot = slotIndex;
|
||||
g_job.itemID = s.itemID;
|
||||
g_job.quantity = quantity;
|
||||
g_job.totalStacks = numStacks;
|
||||
g_job.postedStacks = 0;
|
||||
g_job.bid = bid;
|
||||
g_job.buyout = buyout;
|
||||
g_job.runTime = runTime;
|
||||
g_job.phase = Phase::Idle;
|
||||
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kEvtStart), "");
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_AuctionHouse", "PostItem",
|
||||
&Script_C_AuctionHouse_PostItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace AuctionHouse::PostItem
|
||||
+42
-18
@@ -1,12 +1,12 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
// `C_UnitAuras.*` — modern aura-data namespace. Returns AuraData
|
||||
// tables built by `Aura::Data::Push` (see `Data.h`). The fields the
|
||||
@@ -60,6 +60,13 @@ Data::Filter ParseFilter(const char *filter) {
|
||||
return Data::Filter::Helpful;
|
||||
}
|
||||
|
||||
// The `PLAYER` filter token restricts results to auras the local player
|
||||
// cast. No other modern token contains the substring "PLAYER", so a plain
|
||||
// substring test is safe.
|
||||
bool HasPlayerFilter(const char *filter) {
|
||||
return filter != nullptr && strstr(filter, "PLAYER") != nullptr;
|
||||
}
|
||||
|
||||
const char *ArgUnit(void *L, int idx) {
|
||||
if (!Game::Lua::IsString(L, idx))
|
||||
return nullptr;
|
||||
@@ -82,14 +89,19 @@ const char *ArgOptString(void *L, int idx) {
|
||||
// or nil if no such aura. Used by `GetAuraDataByIndex` and the
|
||||
// filter-locked aliases.
|
||||
int PushAuraByIndex(void *L, const char *unitToken, int index,
|
||||
Data::Filter filter) {
|
||||
Data::Filter filter, bool playerOnly = false) {
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
const int slot = Data::FindNthSlot(unit, index, filter);
|
||||
if (slot < 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
const int slot = Data::FindNthSlot(unit, index, filter, playerOnly);
|
||||
if (slot >= 0) {
|
||||
Data::Push(L, unit, slot);
|
||||
return 1;
|
||||
}
|
||||
Data::Push(L, unit, slot);
|
||||
// Descriptor exhausted — an aura the engine dropped from the slot array
|
||||
// (rogue stealth, party range fluctuation) may still be live in the
|
||||
// Aura::Source cache. Surface it after the descriptor entries.
|
||||
if (Data::PushNthCacheFallback(L, unit, index, filter, playerOnly))
|
||||
return 1;
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -101,7 +113,8 @@ int __fastcall Script_GetAuraDataByIndex(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
return PushAuraByIndex(L, unit, index, ParseFilter(filterStr));
|
||||
return PushAuraByIndex(L, unit, index, ParseFilter(filterStr),
|
||||
HasPlayerFilter(filterStr));
|
||||
}
|
||||
|
||||
int __fastcall Script_GetBuffDataByIndex(void *L) {
|
||||
@@ -139,7 +152,8 @@ int __fastcall Script_GetUnitAuraBySpellID(void *L) {
|
||||
f = ParseFilter(filterStr);
|
||||
fp = &f;
|
||||
}
|
||||
const int slot = Data::FindSlotBySpellID(unit, static_cast<uint32_t>(spellID), fp);
|
||||
const int slot = Data::FindSlotBySpellID(unit, static_cast<uint32_t>(spellID),
|
||||
fp, HasPlayerFilter(filterStr));
|
||||
if (slot < 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
@@ -163,7 +177,8 @@ int __fastcall Script_GetAuraDataBySpellName(void *L) {
|
||||
f = ParseFilter(filterStr);
|
||||
fp = &f;
|
||||
}
|
||||
const int slot = Data::FindSlotBySpellName(unit, spellName, fp);
|
||||
const int slot = Data::FindSlotBySpellName(unit, spellName, fp,
|
||||
HasPlayerFilter(filterStr));
|
||||
if (slot < 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
@@ -192,7 +207,7 @@ int __fastcall Script_GetPlayerAuraBySpellID(void *L) {
|
||||
// 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::Filter filter, int &nextKey, bool playerOnly) {
|
||||
const int start = (filter == Data::Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
@@ -202,10 +217,14 @@ void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!Data::IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !Data::IsPlayerCast(unit, slot))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
Data::Push(L, unit, slot);
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
}
|
||||
// Append auras the descriptor dropped but Aura::Source still has live.
|
||||
Data::AppendCacheFallbacks(L, unit, filter, playerOnly, outerIdx, nextKey);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetUnitAuras(void *L) {
|
||||
@@ -218,14 +237,19 @@ int __fastcall Script_GetUnitAuras(void *L) {
|
||||
if (unit == nullptr)
|
||||
return 1;
|
||||
|
||||
// Range tokens are independent of PLAYER: an explicit HELPFUL/HARMFUL
|
||||
// selects that range, neither selects both. So `"PLAYER"` alone returns
|
||||
// both ranges restricted to player-cast auras (matching retail).
|
||||
const bool playerOnly = HasPlayerFilter(filterStr);
|
||||
const bool hasHelpful = filterStr != nullptr && strstr(filterStr, "HELPFUL") != nullptr;
|
||||
const bool hasHarmful = filterStr != nullptr && strstr(filterStr, "HARMFUL") != nullptr;
|
||||
const bool both = !hasHelpful && !hasHarmful;
|
||||
|
||||
int nextKey = 1;
|
||||
if (filterStr == nullptr) {
|
||||
// No filter — return both ranges.
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Helpful, nextKey);
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Harmful, nextKey);
|
||||
} else {
|
||||
AppendRangeToArray(L, unit, 1, ParseFilter(filterStr), nextKey);
|
||||
}
|
||||
if (hasHelpful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Helpful, nextKey, playerOnly);
|
||||
if (hasHarmful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Harmful, nextKey, playerOnly);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
// 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/>.
|
||||
|
||||
// See ComboDuration.h for the contract and the protocol findings that
|
||||
// motivate the design (the server never transmits target-debuff
|
||||
// durations in 1.12).
|
||||
|
||||
#include "ComboDuration.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "spell/Mod.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Aura::ComboDuration {
|
||||
|
||||
namespace {
|
||||
|
||||
using Net::CDataStore;
|
||||
|
||||
uint32_t NowMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
}
|
||||
|
||||
// ---- Combo-point snapshot (NetClient send co-hook) ------------------------
|
||||
|
||||
// The player's live combo points, read the same way Script_GetComboPoints
|
||||
// (0x0051A190) does: the CP byte on the CGPlayer +0xE68 sub-struct. The player
|
||||
// comes from the shared Unit::Identity::PlayerObject() (the object-manager GUID
|
||||
// resolve, our fastest player-object path — returns null pre-world rather than
|
||||
// throwing), so this stays safe from any context the send hook can fire in.
|
||||
uint8_t CurrentComboPoints() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
const uint8_t *info = *reinterpret_cast<const uint8_t *const *>(
|
||||
player + Offsets::OFF_CGPLAYER_INFO);
|
||||
if (info == nullptr)
|
||||
return 0;
|
||||
return *(info + Offsets::OFF_CGPLAYER_COMBO_POINTS);
|
||||
}
|
||||
|
||||
// One cast in flight at a time is the practical case (a second finisher
|
||||
// can't be cast until the server consumes the first one's combo points),
|
||||
// so a single slot with a spellID match and a short TTL suffices.
|
||||
struct Capture {
|
||||
uint32_t spellId;
|
||||
uint8_t comboPoints;
|
||||
uint32_t capturedMs;
|
||||
};
|
||||
Capture g_capture{0, 0, 0};
|
||||
|
||||
// Worst-case send -> SMSG_SPELL_GO roundtrip. Past this the snapshot is
|
||||
// stale (the cast failed / was rejected) and must not scale a later cast.
|
||||
constexpr uint32_t kCaptureTtlMs = 2000;
|
||||
|
||||
int ConsumeCapturedCP(uint32_t spellId) {
|
||||
const Capture c = g_capture;
|
||||
g_capture = {0, 0, 0};
|
||||
if (c.spellId != spellId || c.comboPoints == 0)
|
||||
return 0;
|
||||
if (NowMs() - c.capturedMs >= kCaptureTtlMs)
|
||||
return 0;
|
||||
return c.comboPoints;
|
||||
}
|
||||
|
||||
// Co-hook on the NetClient send — the one funnel every outgoing CMSG
|
||||
// passes through, including casts released from nampower's spell queue
|
||||
// (which bypass any co-hook on Spell_C_CastSpell itself via trampoline
|
||||
// re-entry, but still build and send CMSG_CAST_SPELL through the engine).
|
||||
// Snapshot the combo points at the moment the cast request leaves: the
|
||||
// server consumes them before SMSG_SPELL_GO arrives, so reading at
|
||||
// SpellGo time is too late.
|
||||
using NetSend_t = void(__fastcall *)(void *conn, void *edx,
|
||||
CDataStore *packet);
|
||||
NetSend_t g_origNetSend = nullptr;
|
||||
|
||||
void __fastcall NetSend_h(void *conn, void *edx, CDataStore *packet) {
|
||||
if (packet != nullptr) {
|
||||
const uint32_t saved = packet->m_read;
|
||||
const uint32_t opcode = Net::Read<uint32_t>(packet);
|
||||
if (opcode == Offsets::OP_CMSG_CAST_SPELL) {
|
||||
const uint32_t spellId = Net::Read<uint32_t>(packet);
|
||||
if (spellId != 0)
|
||||
g_capture = {spellId, CurrentComboPoints(), NowMs()};
|
||||
}
|
||||
packet->m_read = saved;
|
||||
}
|
||||
g_origNetSend(conn, edx, packet);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _sendHook{
|
||||
Offsets::FUN_NET_SEND, reinterpret_cast<void *>(&NetSend_h),
|
||||
reinterpret_cast<void **>(&g_origNetSend)};
|
||||
|
||||
// ---- Duration data (SpellDuration.dbc + Lua overrides) --------------------
|
||||
|
||||
// Lua-registered overrides for spells whose client DBC row is missing or
|
||||
// wrong. Turtle's reworked Rip is the motivating case: its DurationIndex
|
||||
// dangles (records[87] == NULL in Turtle's SpellDuration.dbc) because the
|
||||
// rework lives entirely server-side, so the values must come from config.
|
||||
struct Override {
|
||||
uint32_t spellId;
|
||||
int32_t baseMs;
|
||||
int32_t maxMs;
|
||||
};
|
||||
constexpr int kMaxOverrides = 64;
|
||||
Override g_overrides[kMaxOverrides];
|
||||
int g_overrideCount = 0;
|
||||
|
||||
const Override *FindOverride(uint32_t spellId) {
|
||||
for (int i = 0; i < g_overrideCount; ++i) {
|
||||
if (g_overrides[i].spellId == spellId)
|
||||
return &g_overrides[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Known values for spells whose duration row DANGLES — a non-zero
|
||||
// DurationIndex pointing at a SpellDuration row the client doesn't have.
|
||||
// Consulted ONLY when that exact condition holds, so the gate is the bug
|
||||
// itself, not a server fingerprint: on a stock client every one of these
|
||||
// resolves to a valid row and this table is never reached, and if the
|
||||
// broken client data is ever fixed the DBC row takes over automatically.
|
||||
//
|
||||
// Turtle's reworked Rip is the known case: all six ranks point at
|
||||
// SpellDuration row 87, which Turtle added on the SERVER only
|
||||
// ({8000, 0, 18000} — live server data shows 10/12/14/16/18s at 1..5 CP,
|
||||
// i.e. 8s + 2s per combo point); their client patch never shipped the
|
||||
// row (the client table jumps 86 -> 105). Verified the only dangling
|
||||
// duration index in the entire client Spell.dbc.
|
||||
constexpr Override kDanglingRowKnown[] = {
|
||||
{1079, 8000, 18000}, {9492, 8000, 18000}, {9493, 8000, 18000},
|
||||
{9752, 8000, 18000}, {9894, 8000, 18000}, {9896, 8000, 18000},
|
||||
};
|
||||
|
||||
const Override *FindDanglingRowKnown(uint32_t spellId) {
|
||||
for (const auto &k : kDanglingRowKnown) {
|
||||
if (k.spellId == spellId)
|
||||
return &k;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// `C_UnitAuras.RegisterComboDuration(spellID, baseSeconds, maxSeconds)`
|
||||
// -> true on success. Re-registering a spellID replaces its entry.
|
||||
// Values are in seconds to match the C_UnitAuras duration convention;
|
||||
// the formula applied is the server's own:
|
||||
// `base + (max - base) * comboPoints / 5`.
|
||||
int __fastcall Script_RegisterComboDuration(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2) ||
|
||||
!Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: C_UnitAuras.RegisterComboDuration(spellID, "
|
||||
"baseSeconds, maxSeconds)");
|
||||
return 0;
|
||||
}
|
||||
const auto spellId = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
const auto baseMs = static_cast<int32_t>(Game::Lua::ToNumber(L, 2) * 1000.0);
|
||||
const auto maxMs = static_cast<int32_t>(Game::Lua::ToNumber(L, 3) * 1000.0);
|
||||
if (spellId == 0 || baseMs < 0 || maxMs <= 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
for (int i = 0; i < g_overrideCount; ++i) {
|
||||
if (g_overrides[i].spellId == spellId) {
|
||||
g_overrides[i] = {spellId, baseMs, maxMs};
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (g_overrideCount >= kMaxOverrides) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
g_overrides[g_overrideCount++] = {spellId, baseMs, maxMs};
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterComboDuration",
|
||||
&Script_RegisterComboDuration);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t TryComboScaledMs(const uint8_t *spellRecord, uint32_t spellId) {
|
||||
if (spellRecord == nullptr || spellId == 0)
|
||||
return 0;
|
||||
const int cp = ConsumeCapturedCP(spellId);
|
||||
if (cp <= 0 || cp > 5)
|
||||
return 0;
|
||||
|
||||
int32_t baseMs = 0;
|
||||
int32_t maxMs = 0;
|
||||
if (const Override *o = FindOverride(spellId)) {
|
||||
baseMs = o->baseMs;
|
||||
maxMs = o->maxMs;
|
||||
} else {
|
||||
const int idx = *reinterpret_cast<const int *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_DURATION_INDEX);
|
||||
const uint8_t *row =
|
||||
DBC::Record(Offsets::VAR_SPELL_DURATION_RECORDS,
|
||||
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);
|
||||
} else if (idx != 0) {
|
||||
// Dangling duration index — the client data is broken for this
|
||||
// spell. Fall back to the built-in known values (Turtle Rip).
|
||||
const Override *k = FindDanglingRowKnown(spellId);
|
||||
if (k == nullptr)
|
||||
return 0;
|
||||
baseMs = k->baseMs;
|
||||
maxMs = k->maxMs;
|
||||
} else {
|
||||
return 0; // no duration row at all
|
||||
}
|
||||
}
|
||||
|
||||
// The server scales only when the row has a combo range: base != max
|
||||
// ((v)mangos CalculateSpellDuration). base < 0 is the "infinite"
|
||||
// sentinel; fixed-duration rows (base == max) take the regular path.
|
||||
if (baseMs < 0 || maxMs <= 0 || baseMs == maxMs)
|
||||
return 0;
|
||||
|
||||
int32_t durationMs = baseMs + (maxMs - baseMs) * cp / 5;
|
||||
if (durationMs <= 0)
|
||||
return 0;
|
||||
|
||||
// Player duration SpellMods apply on top of the combo-scaled value,
|
||||
// matching the server's order (scale, then ApplySpellMod).
|
||||
durationMs = static_cast<int32_t>(Spell::Mod::Apply(
|
||||
spellRecord, Offsets::SPELLMOD_OP_DURATION,
|
||||
static_cast<float>(durationMs)));
|
||||
return durationMs > 0 ? static_cast<uint32_t>(durationMs) : 0;
|
||||
}
|
||||
|
||||
} // namespace Aura::ComboDuration
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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>
|
||||
|
||||
// `Aura::ComboDuration` — combo-point-scaled durations for finisher
|
||||
// auras (Rupture, Kidney Shot, Slice and Dice, Rip, …).
|
||||
//
|
||||
// The 1.12 server computes a finisher's real duration at cast time as
|
||||
// `base + (max - base) * comboPoints / 5` from the spell's
|
||||
// SpellDuration.dbc row and never transmits the result for auras the
|
||||
// player puts on OTHER units (verified with a whole-protocol sniff:
|
||||
// SMSG_UPDATE_AURA_DURATION is self-scoped, and nothing else carries
|
||||
// it). So this module mirrors the computation client-side:
|
||||
//
|
||||
// - a co-hook on the NetClient send (`FUN_NET_SEND`) snapshots the
|
||||
// player's combo points the instant CMSG_CAST_SPELL leaves — the
|
||||
// one moment the count is still intact (the server clears it
|
||||
// before SMSG_SPELL_GO comes back);
|
||||
// - `TryComboScaledMs` consumes that snapshot when `Aura::Source`'s
|
||||
// SpellGo hook stores the aura, computing the scaled duration from
|
||||
// SpellDuration.dbc and applying the player's duration SpellMods on
|
||||
// top, matching the server's order of operations. Spells whose
|
||||
// duration index DANGLES (client data bug — Turtle's reworked Rip
|
||||
// is the known case) fall back to built-in values gated on that
|
||||
// exact condition; a Lua-registered override
|
||||
// (`C_UnitAuras.RegisterComboDuration`) beats both, as an escape
|
||||
// hatch for other servers whose values differ from the client DBC.
|
||||
namespace Aura::ComboDuration {
|
||||
|
||||
// Combo-scaled duration (ms) for a player-cast spell, or 0 when it
|
||||
// doesn't apply: no combo points captured for this cast, the spell's
|
||||
// duration row isn't combo-scaled (base == max) and has no override,
|
||||
// or the row is missing. A 0 return means "use the regular base
|
||||
// duration path".
|
||||
uint32_t TryComboScaledMs(const uint8_t *spellRecord, uint32_t spellId);
|
||||
|
||||
} // namespace Aura::ComboDuration
|
||||
+294
-51
@@ -1,12 +1,12 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
// Aura-table primitives. See `Data.h` for the contract; see `Api.cpp`
|
||||
// for the Lua-binding surface that consumes these.
|
||||
@@ -15,7 +15,10 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Source.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -38,6 +41,13 @@ const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
unit + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
}
|
||||
|
||||
// Keys into the `Aura::Source` caster/expiration cache. Thin alias for the
|
||||
// shared `Unit::Identity::GuidForObject` so the lookup here and the
|
||||
// OnAuraAdded hook in `Aura::Source` read the GUID the same way.
|
||||
uint64_t UnitGuid(const uint8_t *unit) {
|
||||
return Unit::Identity::GuidForObject(unit);
|
||||
}
|
||||
|
||||
const uint8_t *SpellRecord(uint32_t spellID) {
|
||||
return DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT, spellID);
|
||||
@@ -110,6 +120,13 @@ const uint8_t *FindPlayerBuffEntry(uint32_t spellID) {
|
||||
// what Lua's `GetTime()` returns (engineMs * 0.001), so converting
|
||||
// `expirationMs * 0.001` lands directly on the same timeline addons
|
||||
// compare against on the Lua side. No epoch reconciliation needed.
|
||||
//
|
||||
// **Read as `uint32_t`**: GetTickCount overflows `INT_MAX` at ~24.86
|
||||
// days of system uptime. A signed read flips negative past that
|
||||
// point, which broke a `<= 0` early-bail that was supposed to catch
|
||||
// only the "no timing" (0) sentinel. The engine's own arithmetic
|
||||
// works on the unsigned tick because both sides of `expiration -
|
||||
// currentMs` wrap together.
|
||||
double PlayerBuffExpirationSeconds(const uint8_t *entry) {
|
||||
if (entry == nullptr)
|
||||
return 0.0;
|
||||
@@ -117,12 +134,12 @@ double PlayerBuffExpirationSeconds(const uint8_t *entry) {
|
||||
entry + Offsets::OFF_PLAYER_BUFF_SLOT_CODE);
|
||||
if (slotCode < 0)
|
||||
return 0.0;
|
||||
auto *expirationTable = reinterpret_cast<const int *>(
|
||||
auto *expirationTable = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_BUFF_EXPIRATION_TABLE));
|
||||
const int expirationMs = expirationTable[slotCode];
|
||||
if (expirationMs <= 0)
|
||||
const uint32_t expirationMs = expirationTable[slotCode];
|
||||
if (expirationMs == 0)
|
||||
return 0.0;
|
||||
return static_cast<double>(static_cast<uint32_t>(expirationMs)) * 0.001;
|
||||
return static_cast<double>(expirationMs) * 0.001;
|
||||
}
|
||||
|
||||
// Computes the spell's base duration in seconds via Spell.dbc →
|
||||
@@ -212,7 +229,20 @@ bool IsSlotPopulated(const uint8_t *unit, int slot) {
|
||||
return IsVisible(SpellRecord(spellID));
|
||||
}
|
||||
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter) {
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
uint64_t casterGuid = 0;
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (!Aura::Source::Get(UnitGuid(unit), spellID, &casterGuid, &expMs, &durMs))
|
||||
return false;
|
||||
return casterGuid != 0 && casterGuid == Unit::Identity::PlayerGuid();
|
||||
}
|
||||
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly) {
|
||||
if (unit == nullptr || oneBasedIndex < 1)
|
||||
return -1;
|
||||
const int start = (filter == Filter::Harmful)
|
||||
@@ -225,6 +255,8 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter) {
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
continue;
|
||||
if (++matches == oneBasedIndex)
|
||||
return slot;
|
||||
}
|
||||
@@ -232,7 +264,7 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter) {
|
||||
}
|
||||
|
||||
int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter) {
|
||||
const Filter *filter, bool playerOnly) {
|
||||
if (unit == nullptr || spellID == 0)
|
||||
return -1;
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
@@ -244,14 +276,17 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (ReadSpellID(unit, slot) == spellID)
|
||||
return slot;
|
||||
if (ReadSpellID(unit, slot) != spellID)
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter) {
|
||||
const Filter *filter, bool playerOnly) {
|
||||
if (unit == nullptr || spellName == nullptr || *spellName == '\0')
|
||||
return -1;
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
@@ -264,8 +299,11 @@ int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
const char *name = LocalizedSpellName(SpellRecord(ReadSpellID(unit, slot)));
|
||||
if (name != nullptr && std::strcmp(name, spellName) == 0)
|
||||
return slot;
|
||||
if (name == nullptr || std::strcmp(name, spellName) != 0)
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -312,10 +350,13 @@ const char *DispelName(uint32_t dispelTypeID) {
|
||||
return (name == nullptr || *name == '\0') ? nullptr : name;
|
||||
}
|
||||
|
||||
void Push(void *L, const uint8_t *unit, int slot) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
// Writes the full modern AuraData table on top of the Lua stack from
|
||||
// already-resolved values. Shared by the descriptor path (`Push`) and the
|
||||
// cache-fallback path (`PushFromCache`). Net stack effect: +1 (the table).
|
||||
static void BuildTable(void *L, uint32_t spellID, int applications,
|
||||
bool isHelpful, double duration, double expirationTime,
|
||||
uint64_t casterGuid) {
|
||||
const uint8_t *spellRecord = SpellRecord(spellID);
|
||||
const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
|
||||
uint32_t dispelTypeID = 0;
|
||||
if (spellRecord != nullptr) {
|
||||
@@ -332,43 +373,32 @@ void Push(void *L, const uint8_t *unit, int slot) {
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
Game::Lua::SetFieldString(L, "icon", icon);
|
||||
Game::Lua::SetFieldNumber(L, "applications",
|
||||
static_cast<double>(ReadStacks(unit, slot)));
|
||||
static_cast<double>(applications));
|
||||
Game::Lua::SetFieldNumber(L, "spellId", static_cast<double>(spellID));
|
||||
Game::Lua::SetFieldString(L, "dispelName", dispel);
|
||||
Game::Lua::SetFieldBool(L, "isHelpful", isHelpful);
|
||||
Game::Lua::SetFieldBool(L, "isHarmful", !isHelpful);
|
||||
|
||||
// Timing fields. Vanilla doesn't broadcast cast time / duration
|
||||
// for ANY unit's auras except the local player's — for everyone
|
||||
// else (target, party, raid, mouseover) `expirationTime` stays 0
|
||||
// and addons have to track expiry themselves via aura-add events.
|
||||
// For the player we read the engine's `VAR_PLAYER_BUFF_TABLE`
|
||||
// (same data `GetPlayerBuffTimeLeft` returns), which gives us a
|
||||
// real expiration timestamp.
|
||||
//
|
||||
// `duration` comes from Spell.dbc → SpellDuration.dbc with
|
||||
// level scaling. That's the *base* value; talent / glyph
|
||||
// duration-extension modifiers (Improved PW:F, etc.) are
|
||||
// already baked into the expiration timestamp on the
|
||||
// caster's side, so `expirationTime - GetTime()` reflects the
|
||||
// true remaining time even when `duration` doesn't include
|
||||
// the talent boost. We populate `duration` for any unit (the
|
||||
// base value is identical regardless of who's affected); only
|
||||
// `expirationTime` is player-gated.
|
||||
double duration = 0.0;
|
||||
double expirationTime = 0.0;
|
||||
const uint8_t *player = LocalPlayer();
|
||||
if (spellID != 0) {
|
||||
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
|
||||
duration = SpellBaseDurationSeconds(spellID, unitLevel);
|
||||
}
|
||||
if (unit != nullptr && unit == player) {
|
||||
const uint8_t *entry = FindPlayerBuffEntry(spellID);
|
||||
if (entry != nullptr)
|
||||
expirationTime = PlayerBuffExpirationSeconds(entry);
|
||||
}
|
||||
Game::Lua::SetFieldNumber(L, "duration", duration);
|
||||
Game::Lua::SetFieldNumber(L, "expirationTime", expirationTime);
|
||||
|
||||
if (casterGuid != 0) {
|
||||
// `sourceUnit` is a token (nil when the caster maps to no current
|
||||
// token); `sourceGUID` is the raw "0x..." GUID and is set whenever we
|
||||
// have a caster — a ClassicAPI extension (not in retail AuraData) that
|
||||
// survives the caster leaving token range and doubles as a unit token
|
||||
// under SuperWoW.
|
||||
char tokenBuf[32];
|
||||
char guidBuf[Guid::STRING_SIZE];
|
||||
const char *sourceUnit = Unit::Identity::TokenFromGUID(
|
||||
casterGuid, tokenBuf, sizeof tokenBuf);
|
||||
const char *sourceGUID =
|
||||
Guid::FormatAsString(casterGuid, guidBuf, sizeof guidBuf);
|
||||
if (sourceUnit != nullptr)
|
||||
Game::Lua::SetFieldString(L, "sourceUnit", sourceUnit);
|
||||
if (sourceGUID != nullptr)
|
||||
Game::Lua::SetFieldString(L, "sourceGUID", sourceGUID);
|
||||
}
|
||||
|
||||
Game::Lua::SetFieldNumber(L, "charges", 0);
|
||||
Game::Lua::SetFieldNumber(L, "maxCharges", 0);
|
||||
Game::Lua::SetFieldNumber(L, "timeMod", 1);
|
||||
@@ -384,10 +414,223 @@ void Push(void *L, const uint8_t *unit, int slot) {
|
||||
Game::Lua::SetFieldBool(L, "shouldConsolidate", false);
|
||||
Game::Lua::SetFieldBool(L, "isRaid", false);
|
||||
|
||||
// `sourceUnit`, `auraInstanceID`, `points` deliberately omitted
|
||||
// — modern returns nil for those when they don't apply, and
|
||||
// Lua reading a missing key yields nil, so the table doesn't
|
||||
// need an explicit entry.
|
||||
// `auraInstanceID` and `points` are deliberately omitted — modern returns
|
||||
// nil for those when they don't apply, and Lua reading a missing key
|
||||
// yields nil, so the table doesn't need an explicit entry.
|
||||
}
|
||||
|
||||
void Push(void *L, const uint8_t *unit, int slot) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
|
||||
// Timing fields. Vanilla doesn't broadcast cast time / duration for ANY
|
||||
// unit's auras except the local player's — for everyone else (target,
|
||||
// party, raid, mouseover) `expirationTime` stays 0 unless the
|
||||
// `Aura::Source` cache observed the cast. For the player we read the
|
||||
// engine's `VAR_PLAYER_BUFF_TABLE` (same data `GetPlayerBuffTimeLeft`
|
||||
// returns), which gives a real expiration timestamp.
|
||||
//
|
||||
// `duration` comes from Spell.dbc → SpellDuration.dbc with level scaling
|
||||
// (the *base* value); talent / glyph duration-extension modifiers are
|
||||
// already baked into the expiration timestamp on the caster's side, so
|
||||
// `expirationTime - GetTime()` reflects true remaining time even when the
|
||||
// base `duration` doesn't include the talent boost.
|
||||
double duration = 0.0;
|
||||
double expirationTime = 0.0;
|
||||
uint64_t casterGuid = 0;
|
||||
const uint8_t *player = LocalPlayer();
|
||||
if (spellID != 0) {
|
||||
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
|
||||
duration = SpellBaseDurationSeconds(spellID, unitLevel);
|
||||
}
|
||||
if (unit != nullptr && unit == player) {
|
||||
const uint8_t *entry = FindPlayerBuffEntry(spellID);
|
||||
if (entry != nullptr)
|
||||
expirationTime = PlayerBuffExpirationSeconds(entry);
|
||||
}
|
||||
// Caster + non-player expiration from the SMSG_SPELL_GO cache, captured at
|
||||
// cast time (the descriptor has neither). The player-buff table above is
|
||||
// authoritative for the player's own expiration; the cache fills it for
|
||||
// everyone else. Prefer the applied (caster-modified) duration so it stays
|
||||
// consistent with `expirationTime` — otherwise a talented DoT (Improved
|
||||
// Shadow Word: Pain) would show remaining time exceeding the base.
|
||||
if (spellID != 0) {
|
||||
uint64_t c = 0;
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (Aura::Source::Get(UnitGuid(unit), spellID, &c, &expMs, &durMs)) {
|
||||
if (expirationTime == 0.0 && expMs != 0)
|
||||
expirationTime = static_cast<double>(expMs) * 0.001;
|
||||
if (durMs != 0)
|
||||
duration = static_cast<double>(durMs) * 0.001;
|
||||
casterGuid = c;
|
||||
}
|
||||
}
|
||||
|
||||
BuildTable(L, spellID, ReadStacks(unit, slot), isHelpful, duration,
|
||||
expirationTime, casterGuid);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Builds an AuraData table from an `Aura::Source` cache entry — the fallback
|
||||
// when the descriptor has dropped the aura's slot. Stack count isn't broadcast
|
||||
// in SMSG_SPELL_GO, so `applications` defaults to 1. `duration` uses the
|
||||
// applied (caster-modified) ms when known, else the Spell.dbc base.
|
||||
void PushFromCache(void *L, const uint8_t *unit,
|
||||
const Aura::Source::CachedAura &c, bool isHelpful) {
|
||||
double duration = 0.0;
|
||||
if (c.durationMs != 0) {
|
||||
duration = static_cast<double>(c.durationMs) * 0.001;
|
||||
} else {
|
||||
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
|
||||
duration = SpellBaseDurationSeconds(c.spellId, unitLevel);
|
||||
}
|
||||
const double expirationTime =
|
||||
(c.expirationMs != 0) ? static_cast<double>(c.expirationMs) * 0.001 : 0.0;
|
||||
BuildTable(L, c.spellId, 1, isHelpful, duration, expirationTime,
|
||||
c.casterGuid);
|
||||
}
|
||||
|
||||
// Reconciles the `Aura::Source` cache against `unit`'s descriptor: when the
|
||||
// descriptor holds at least one raw aura the unit is in view and fully synced,
|
||||
// so it's authoritative — any cached entry for the unit whose spell isn't in
|
||||
// the array was removed while we couldn't see it (e.g. cancelled out of range,
|
||||
// so no OnAuraRemoved reached us) and must be dropped before the fallback
|
||||
// resurfaces it as a phantom (an enumerated aura whose SetUnitAura tooltip is
|
||||
// empty). Reads the RAW slots (not IsSlotPopulated): a spell that's present
|
||||
// but flag-hidden is still legitimately on the unit. Skips when the array is
|
||||
// empty — that can't be told apart from an out-of-range descriptor drop, and
|
||||
// reconciling then would wipe still-valid out-of-range entries.
|
||||
void ReconcileCache(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
uint32_t present[Offsets::UNIT_AURA_TOTAL];
|
||||
int n = 0;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
const uint32_t id = ReadSpellID(unit, slot);
|
||||
if (id != 0)
|
||||
present[n++] = id;
|
||||
}
|
||||
if (n == 0)
|
||||
return;
|
||||
Aura::Source::EvictAbsent(UnitGuid(unit), present, n);
|
||||
}
|
||||
|
||||
// True if `unit`'s descriptor currently exposes any visible aura in either
|
||||
// range. The cache fallback is only trustworthy when the descriptor has been
|
||||
// wholesale-cleared: the two states it exists for — rogue stealth and party
|
||||
// range fluctuation — clear the *entire* aura array at once. If the unit shows
|
||||
// even one live aura, the descriptor is authoritative, so a cache entry the
|
||||
// descriptor lacks is a genuinely-gone aura we never saw removed (e.g. a
|
||||
// no-duration debuff — Ghost/Spirit — on a unit that died and resurrected out
|
||||
// of our visibility, so no OnAuraRemoved fired and its infinite entry never
|
||||
// expired). Surfacing it then produces a stuck phantom.
|
||||
bool DescriptorHasVisibleAura(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
if (IsSlotPopulated(unit, slot))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Counts populated descriptor slots matching `filter` (and `playerOnly`).
|
||||
int CountSlots(const uint8_t *unit, Filter filter, bool playerOnly) {
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
const int start = (filter == Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
const int end = (filter == Filter::Harmful) ? Offsets::UNIT_AURA_TOTAL
|
||||
: Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
int n = 0;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
continue;
|
||||
++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
constexpr int kFallbackMax = Offsets::UNIT_AURA_TOTAL;
|
||||
|
||||
// Whether cache entry `c` should be surfaced as a fallback for `unit` under
|
||||
// `filter`/`playerOnly`: the spell must be a user-visible aura, it must not
|
||||
// already be in a populated descriptor slot (no double-listing the same live
|
||||
// aura) and, when player-filtered, must have been cast by us. Superseded /
|
||||
// dispelled auras are kept out of the cache by `Aura::Source`'s removal
|
||||
// eviction, not filtered here.
|
||||
bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
Filter filter, bool playerOnly) {
|
||||
if (!IsVisible(SpellRecord(c.spellId)))
|
||||
return false; // hidden/internal aura (e.g. SPELL_ATTR_HIDDEN_CLIENTSIDE):
|
||||
// the descriptor path drops these via IsSlotPopulated, so
|
||||
// the cache path must apply the same engine predicate or
|
||||
// they leak through as tooltip-less phantom buffs.
|
||||
if (FindSlotBySpellID(unit, c.spellId, &filter, false) >= 0)
|
||||
return false; // still in the descriptor — returned via the slot path
|
||||
if (playerOnly && c.casterGuid != Unit::Identity::PlayerGuid())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, bool playerOnly) {
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return false; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
// The descriptor held `descCount` matches; the caller already found fewer
|
||||
// than `oneBasedIndex` there, so the cache supplies index
|
||||
// `oneBasedIndex - descCount` (1-based) of the entries it didn't.
|
||||
const int target = oneBasedIndex - CountSlots(unit, filter, playerOnly);
|
||||
if (target < 1)
|
||||
return false;
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
if (guid == 0)
|
||||
return false;
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(
|
||||
guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!FallbackEligible(unit, buf[i], filter, playerOnly))
|
||||
continue;
|
||||
if (++seen == target) {
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
bool playerOnly, int outerIdx, int &nextKey) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
if (guid == 0)
|
||||
return;
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(
|
||||
guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!FallbackEligible(unit, buf[i], filter, playerOnly))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
+43
-10
@@ -1,12 +1,12 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -47,22 +47,29 @@ bool IsSlotPopulated(const uint8_t *unit, int slot);
|
||||
// Finds the absolute slot of the `oneBasedIndex`-th populated aura
|
||||
// matching `filter`. Returns -1 if no such aura. Walks
|
||||
// 0..UNIT_AURA_BUFF_COUNT-1 for Helpful, UNIT_AURA_BUFF_COUNT..
|
||||
// UNIT_AURA_TOTAL-1 for Harmful.
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter);
|
||||
// UNIT_AURA_TOTAL-1 for Harmful. When `playerOnly` is true, only auras
|
||||
// the local player cast (per `IsPlayerCast`) are counted/returned.
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly = false);
|
||||
|
||||
// Finds the absolute slot of a populated aura with the given spell
|
||||
// ID, optionally restricted to one filter range. `filter` of
|
||||
// nullptr searches both ranges (helpful first, then harmful).
|
||||
// Returns -1 if not found.
|
||||
// `playerOnly` restricts to player-cast auras. Returns -1 if not found.
|
||||
int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter);
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
|
||||
// Finds the absolute slot of a populated aura whose locale-resolved
|
||||
// spell name exactly matches `spellName`. Case-sensitive. 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);
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
|
||||
// True iff the aura at `slot` was cast by the local player, per the
|
||||
// `Aura::Source` cache. False on a cache miss (caster unknown) — so a
|
||||
// PLAYER-filtered query excludes auras whose cast we didn't observe.
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot);
|
||||
|
||||
// Display stack count for the given slot (engine stores stacks-1,
|
||||
// we add 1). Returns 0 if the unit is null.
|
||||
@@ -85,12 +92,18 @@ const char *DispelName(uint32_t dispelTypeID);
|
||||
//
|
||||
// Populates these fields with real data:
|
||||
// name, icon, applications, spellId, dispelName,
|
||||
// isHelpful, isHarmful, timeMod
|
||||
// isHelpful, isHarmful, timeMod, duration
|
||||
//
|
||||
// And these from the `Aura::Source` SMSG_SPELL_GO cache when available
|
||||
// (else their inapplicable default):
|
||||
// expirationTime (player: engine buff table; others: cache; 0 on miss),
|
||||
// sourceUnit (caster token from the cache; nil on miss),
|
||||
// sourceGUID (caster "0x..." GUID string; nil on miss — a ClassicAPI
|
||||
// extension, not in retail AuraData)
|
||||
//
|
||||
// And these with vanilla-truthful defaults (matches modern's
|
||||
// "field present but inapplicable" semantics):
|
||||
// duration=0, expirationTime=0, charges=0, maxCharges=0,
|
||||
// sourceUnit=nil, auraInstanceID=nil, points=nil,
|
||||
// charges=0, maxCharges=0, auraInstanceID=nil, points=nil,
|
||||
// isStealable=false, isBossAura=false, isFromPlayerOrPlayerPet=false,
|
||||
// isNameplateOnly=false, nameplateShowAll=false,
|
||||
// nameplateShowPersonal=false, canApplyAura=false,
|
||||
@@ -99,4 +112,24 @@ const char *DispelName(uint32_t dispelTypeID);
|
||||
// Net stack effect: +1 (the table).
|
||||
void Push(void *L, const uint8_t *unit, int slot);
|
||||
|
||||
// Fallback for the index path when the descriptor has dropped an aura's slot
|
||||
// (rogue stealth, party range fluctuation) but it's still active server-side.
|
||||
// `oneBasedIndex` is the same index the caller passed to `FindNthSlot`; this
|
||||
// resolves it against the `Aura::Source` cache *after* the descriptor matches
|
||||
// (so cache entries append after descriptor ones, preserving existing index
|
||||
// order). On a hit it pushes the AuraData table and returns true; on a miss it
|
||||
// pushes nothing and returns false (caller pushes nil). Only entries whose
|
||||
// helpful/harmful classification matches `filter` and that aren't already in a
|
||||
// populated descriptor slot are considered.
|
||||
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, bool playerOnly = false);
|
||||
|
||||
// Appends every eligible `Aura::Source` cache fallback for `unit` matching
|
||||
// `filter` into the array table at `outerIdx`, continuing from `nextKey`
|
||||
// (updated in place). The bulk-enumeration analog of `PushNthCacheFallback`,
|
||||
// for `GetUnitAuras`. Skips entries already present in a populated descriptor
|
||||
// slot so it never double-lists.
|
||||
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
bool playerOnly, int outerIdx, int &nextKey);
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
// 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.
|
||||
|
||||
// See `Source.h` for the contract. This is the backend: a co-hook on
|
||||
// `FUN_SPELL_GO` that mirrors nampower's `SpellGoHook` parse to capture the
|
||||
// caster + duration of every aura-applying cast, and a small fixed-size
|
||||
// cache `Aura::Data::Push` reads from.
|
||||
|
||||
#include "Source.h"
|
||||
|
||||
#include "ComboDuration.h"
|
||||
#include "Data.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "player/StatSignal.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Aura::Source {
|
||||
|
||||
namespace {
|
||||
|
||||
using Net::CDataStore;
|
||||
|
||||
// A cast applies an aura iff any of its three effects names an aura. The
|
||||
// EffectApplyAuraName[3] int32 array sits at `+0x16C` (already used by the
|
||||
// shapeshift / mechanic-immunity code). Gating on this keeps pure-damage
|
||||
// and utility casts out of the cache.
|
||||
bool SpellAppliesAura(const uint8_t *spellRecord) {
|
||||
if (spellRecord == nullptr)
|
||||
return false;
|
||||
const auto *auraNames = reinterpret_cast<const int32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (auraNames[i] != 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t NowMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
}
|
||||
|
||||
// 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.
|
||||
uint32_t SpellDurationMs(const uint8_t *spellRecord, bool casterIsPlayer) {
|
||||
using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord,
|
||||
int unit, char skipMod);
|
||||
const int ms = reinterpret_cast<GetDuration_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GET_SPELL_DURATION))(
|
||||
spellRecord, 0, casterIsPlayer ? 0 : 1);
|
||||
return ms > 0 ? static_cast<uint32_t>(ms) : 0;
|
||||
}
|
||||
|
||||
// ---- Cache ---------------------------------------------------------------
|
||||
|
||||
// Helpful/harmful classification, recorded from the aura's descriptor slot
|
||||
// when it's applied (the only place the buff/debuff split is known). SpellGo
|
||||
// gives no slot, so casts we only saw there stay Unknown until/unless an
|
||||
// application hook fires for the same aura.
|
||||
enum Kind : int8_t { KIND_UNKNOWN = -1, KIND_HELPFUL = 0, KIND_HARMFUL = 1 };
|
||||
|
||||
struct Entry {
|
||||
uint64_t targetGuid;
|
||||
uint64_t casterGuid;
|
||||
uint32_t spellId;
|
||||
uint32_t expirationMs; // 0 = infinite / unknown duration
|
||||
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
|
||||
int8_t kind; // Kind; descriptor-slot-derived, KIND_UNKNOWN if only seen via SpellGo
|
||||
bool used;
|
||||
};
|
||||
|
||||
constexpr int kCacheSize = 256;
|
||||
Entry g_cache[kCacheSize];
|
||||
int g_writeCursor = 0;
|
||||
|
||||
// `fromCast` true: the SpellGo hook — authoritative caster + caster-modified
|
||||
// (talented) timing. False: the OnAuraAdded application hook — timing only,
|
||||
// no caster, and it must not clobber an entry SpellGo already owns (that
|
||||
// would replace talented timing with the unmodified base), so it skips
|
||||
// entries that already carry a caster.
|
||||
void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
uint32_t expirationMs, uint32_t durationMs, bool fromCast,
|
||||
int8_t kind) {
|
||||
if (targetGuid == 0 || spellId == 0)
|
||||
return;
|
||||
|
||||
// Update an existing entry for this exact aura instance.
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId) {
|
||||
// Learn the classification whenever a slot-derived kind arrives —
|
||||
// independent of caster/timing ownership, and never downgrade a
|
||||
// known kind back to unknown.
|
||||
if (kind != KIND_UNKNOWN)
|
||||
e.kind = kind;
|
||||
if (!fromCast && e.casterGuid != 0)
|
||||
return; // SpellGo owns this entry; keep its caster + timing
|
||||
if (casterGuid != 0)
|
||||
e.casterGuid = casterGuid;
|
||||
e.expirationMs = expirationMs;
|
||||
e.durationMs = durationMs;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Take a free slot, else an expired one, else evict round-robin.
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || (e.expirationMs != 0 && now >= e.expirationMs)) {
|
||||
e = {targetGuid, casterGuid, spellId, expirationMs, durationMs,
|
||||
kind, true};
|
||||
return;
|
||||
}
|
||||
}
|
||||
Entry &slot = g_cache[g_writeCursor];
|
||||
g_writeCursor = (g_writeCursor + 1) % kCacheSize;
|
||||
slot = {targetGuid, casterGuid, spellId, expirationMs, durationMs, kind,
|
||||
true};
|
||||
}
|
||||
|
||||
// Evict the entry for an aura the engine reports gone. Keyed by (target,
|
||||
// spell) like the rest of the cache. Without this, the GetAuraDataByIndex
|
||||
// fallback would keep surfacing a dropped aura until its computed expiry —
|
||||
// e.g. a Rank 1 buff replaced by Rank 2 (engine drops Rank 1 from the
|
||||
// descriptor) would show as a phantom second aura, or a dispelled buff would
|
||||
// linger.
|
||||
void Evict(uint64_t targetGuid, uint32_t spellId) {
|
||||
if (targetGuid == 0 || spellId == 0)
|
||||
return;
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId) {
|
||||
e.used = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wipe the whole cache. Used on a map transition (see OnWorldTick).
|
||||
void FlushAll() {
|
||||
for (auto &e : g_cache)
|
||||
e.used = false;
|
||||
}
|
||||
|
||||
// -1 = no map seen yet (never a valid Map.dbc id), so the first tick just
|
||||
// records the current map without flushing.
|
||||
int g_lastMapId = -1;
|
||||
|
||||
// Drop entries whose timed aura has elapsed so the table doesn't fill with
|
||||
// stale combat debuffs. Infinite-duration entries (expirationMs == 0) stay
|
||||
// until overwritten.
|
||||
//
|
||||
// Also flush the entire cache on a map-ID change. A battleground/instance/
|
||||
// continent transition (including the teleport out of a BG via /afk) bulk-
|
||||
// clears the player's aura descriptor WITHOUT firing per-aura OnAuraRemoved,
|
||||
// so entries for auras that are now gone would linger and the
|
||||
// GetAuraDataByIndex fallback would keep surfacing them — the "buffs/debuffs
|
||||
// stuck after leaving a battleground" report. Every other unit from the old
|
||||
// map has despawned too, and the local player's real auras re-sync into the
|
||||
// descriptor / PLAYER_BUFF tables on entering the new world, so a full flush
|
||||
// loses nothing legitimate. Crucially, the descriptor drops the fallback
|
||||
// must survive — rogue stealth and party-range fluctuation — do NOT change
|
||||
// the map ID, so they're unaffected.
|
||||
void OnWorldTick() {
|
||||
const int mapId = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CURRENT_MAP_ID));
|
||||
if (mapId != g_lastMapId) {
|
||||
if (g_lastMapId != -1)
|
||||
FlushAll();
|
||||
g_lastMapId = mapId;
|
||||
}
|
||||
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.expirationMs != 0 && now >= e.expirationMs)
|
||||
e.used = false;
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
// ---- SMSG_SPELL_GO co-hook ----------------------------------------------
|
||||
|
||||
// The hit-target list is all we need: each entry is a unit the cast landed
|
||||
// on, which is exactly where an aura gets applied. We deliberately stop
|
||||
// before the missed-target list / target mask — the post-mask "intended"
|
||||
// target is redundant with the hit list for aura purposes, and skipping it
|
||||
// keeps the parse short and robust.
|
||||
constexpr int kMaxTargets = 16;
|
||||
|
||||
using SpellGo_t = void(__fastcall *)(uint64_t *itemGUID, uint64_t *casterGUID,
|
||||
uint32_t spellId, CDataStore *packet);
|
||||
SpellGo_t g_origSpellGo = nullptr;
|
||||
|
||||
void __fastcall SpellGo_h(uint64_t *itemGUID, uint64_t *casterGUID,
|
||||
uint32_t spellId, CDataStore *packet) {
|
||||
uint64_t targets[kMaxTargets];
|
||||
int numTargets = 0;
|
||||
|
||||
if (packet != nullptr) {
|
||||
const uint32_t savedRead = packet->m_read;
|
||||
Net::Read<int16_t>(packet); // castFlags (unused)
|
||||
const uint8_t numHit = Net::Read<uint8_t>(packet);
|
||||
for (int i = 0; i < numHit; ++i) {
|
||||
const uint64_t guid = Net::Read<uint64_t>(packet);
|
||||
if (numTargets < kMaxTargets)
|
||||
targets[numTargets++] = guid;
|
||||
}
|
||||
packet->m_read = savedRead; // restore so the engine re-parses cleanly
|
||||
}
|
||||
|
||||
g_origSpellGo(itemGUID, casterGUID, spellId, packet);
|
||||
|
||||
const uint64_t caster = (casterGUID != nullptr) ? *casterGUID : 0;
|
||||
if (caster == 0 || spellId == 0)
|
||||
return;
|
||||
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
|
||||
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 expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
|
||||
if (numTargets == 0) {
|
||||
// No explicit hit list (self-cast with caster-implicit target).
|
||||
Store(caster, spellId, caster, expirationMs, durationMs, true,
|
||||
KIND_UNKNOWN);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < numTargets; ++i)
|
||||
Store(targets[i], spellId, caster, expirationMs, durationMs, true,
|
||||
KIND_UNKNOWN);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{Offsets::FUN_SPELL_GO,
|
||||
reinterpret_cast<void *>(&SpellGo_h),
|
||||
reinterpret_cast<void **>(&g_origSpellGo)};
|
||||
|
||||
// ---- Aura-application co-hooks (timing for proc / triggered auras) -------
|
||||
|
||||
// Stamp expiration for an aura that just landed/refreshed on `unit`. Used by
|
||||
// both the add and stack-change hooks. No caster is available from these
|
||||
// paths, so it stamps timing only with `fromCast=false` — Store skips any
|
||||
// entry SpellGo already owns, so a directly-cast aura keeps its talented
|
||||
// timing. Base (unmodified) duration is the best estimate without a caster.
|
||||
void StampApplication(void *unit, uint32_t spellId, int8_t kind) {
|
||||
if (spellId == 0)
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
|
||||
if (!SpellAppliesAura(rec))
|
||||
return;
|
||||
const uint64_t unitGuid = Unit::Identity::GuidForObject(unit);
|
||||
if (unitGuid == 0)
|
||||
return;
|
||||
const uint32_t durationMs = SpellDurationMs(rec, /*casterIsPlayer*/ false);
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
Store(unitGuid, spellId, /*casterGuid*/ 0, expirationMs, durationMs,
|
||||
/*fromCast*/ false, kind);
|
||||
}
|
||||
|
||||
// Classify by the absolute aura slot: 0..BUFF_COUNT-1 = buff (helpful),
|
||||
// BUFF_COUNT..TOTAL-1 = debuff (harmful).
|
||||
int8_t KindForSlot(int slot) {
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT ? KIND_HARMFUL : KIND_HELPFUL;
|
||||
}
|
||||
|
||||
// Bump the player-stat-inputs signal when an aura change hits the LOCAL
|
||||
// player — buffs/debuffs move GetSpellBonusHealing's flat and Spirit/Armor
|
||||
// terms, so its lazy cache must invalidate. Guarded on the player object so
|
||||
// other units' aura churn (combat) doesn't needlessly invalidate it.
|
||||
void NotifyIfPlayer(void *unit) {
|
||||
if (static_cast<const uint8_t *>(unit) == Unit::Identity::PlayerObject())
|
||||
Player::StatSignal::Notify();
|
||||
}
|
||||
|
||||
// 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);
|
||||
OnAuraAdded_t g_origOnAuraAdded = nullptr;
|
||||
|
||||
void __fastcall OnAuraAdded_h(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId) {
|
||||
g_origOnAuraAdded(unit, edx, slot, spellId);
|
||||
StampApplication(unit, spellId, KindForSlot(static_cast<int>(slot)));
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraAdded{
|
||||
Offsets::FUN_ON_AURA_ADDED, reinterpret_cast<void *>(&OnAuraAdded_h),
|
||||
reinterpret_cast<void **>(&g_origOnAuraAdded)};
|
||||
|
||||
// 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.
|
||||
using OnAuraStacksChanged_t = void(__fastcall *)(void *unit, void *edx,
|
||||
int slot, uint8_t stackCount);
|
||||
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),
|
||||
KindForSlot(slot));
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraStacks{
|
||||
Offsets::FUN_ON_AURA_STACKS_CHANGED,
|
||||
reinterpret_cast<void *>(&OnAuraStacksChanged_h),
|
||||
reinterpret_cast<void **>(&g_origOnAuraStacksChanged)};
|
||||
|
||||
// OnAuraRemoved — a descriptor aura slot went empty: the aura fell off, was
|
||||
// dispelled, was cancelled by its owner, or was replaced by a higher rank
|
||||
// (the diff dispatcher fires Removed(old) + Added(new)). Evict the cache
|
||||
// entry so the descriptor-drop fallback stops surfacing it. Same ABI as
|
||||
// OnAuraAdded (unit in ecx, slot + spellId on the stack); `slot` is unused
|
||||
// here — we evict by spellId.
|
||||
//
|
||||
// Always evict, even for a death-persistent aura (flask, world buff) on a
|
||||
// dead unit: the server keeps those through death WITHOUT changing any
|
||||
// field, so death itself fires no OnAuraRemoved. Any removal we do receive
|
||||
// for one is therefore genuine — e.g. the owner cancelled the flask while
|
||||
// dead — and must clear the cache, or the fallback keeps surfacing a phantom
|
||||
// whose SetUnitAura tooltip is empty.
|
||||
using OnAuraRemoved_t = void(__fastcall *)(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId);
|
||||
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);
|
||||
(void)slot;
|
||||
Evict(Unit::Identity::GuidForObject(unit), spellId);
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraRemoved{
|
||||
Offsets::FUN_ON_AURA_REMOVED, reinterpret_cast<void *>(&OnAuraRemoved_h),
|
||||
reinterpret_cast<void **>(&g_origOnAuraRemoved)};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs) {
|
||||
if (unitGuid == 0 || spellId == 0)
|
||||
return false;
|
||||
for (const auto &e : g_cache) {
|
||||
if (e.used && e.targetGuid == unitGuid && e.spellId == spellId) {
|
||||
*outCaster = e.casterGuid;
|
||||
*outExpirationMs = e.expirationMs;
|
||||
*outDurationMs = e.durationMs;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count) {
|
||||
if (unitGuid == 0 || presentSpellIds == nullptr || count <= 0)
|
||||
return;
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || e.targetGuid != unitGuid)
|
||||
continue;
|
||||
bool present = false;
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (presentSpellIds[i] == e.spellId) {
|
||||
present = true;
|
||||
break;
|
||||
}
|
||||
if (!present)
|
||||
e.used = false;
|
||||
}
|
||||
}
|
||||
|
||||
int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut) {
|
||||
if (unitGuid == 0 || out == nullptr || maxOut <= 0)
|
||||
return 0;
|
||||
const int8_t want = harmful ? KIND_HARMFUL : KIND_HELPFUL;
|
||||
const uint32_t now = NowMs();
|
||||
int n = 0;
|
||||
for (const auto &e : g_cache) {
|
||||
if (n >= maxOut)
|
||||
break;
|
||||
if (!e.used || e.targetGuid != unitGuid || e.kind != want)
|
||||
continue;
|
||||
if (e.expirationMs != 0 && now >= e.expirationMs)
|
||||
continue; // expired (infinite-duration entries pass)
|
||||
out[n++] = {e.spellId, e.casterGuid, e.expirationMs, e.durationMs};
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
} // namespace Aura::Source
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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 caster / duration cache, fed by a co-hook on `SMSG_SPELL_GO`
|
||||
// (`FUN_SPELL_GO`). The vanilla unit aura array (`UNIT_FIELD_AURA`) stores
|
||||
// only spell IDs — it has no record of *who* cast an aura, and no
|
||||
// cast/expiration timing for any unit but the local player. The one place
|
||||
// the client ever sees the caster + a server-authoritative duration is the
|
||||
// `SMSG_SPELL_GO` packet at cast time. We parse it (the same packet
|
||||
// nampower parses for its `AURA_CAST_ON_*` events) and remember, per
|
||||
// `(targetGuid, spellId)`, who cast it and when it should expire.
|
||||
//
|
||||
// `Aura::Data::Push` consults this to fill `sourceUnit` (caster, resolved
|
||||
// to a unit token) and `expirationTime` for non-player units. The cache is
|
||||
// inherently best-effort: it only knows about casts observed since login,
|
||||
// and an entry survives until it expires (combat-duration auras) or is
|
||||
// evicted under pressure. Misses leave the modern-truthful defaults
|
||||
// (`sourceUnit` nil, `expirationTime` 0) in place — same outcome as before
|
||||
// this module existed.
|
||||
|
||||
namespace Aura::Source {
|
||||
|
||||
// Looks up the cached caster + timing for the aura `spellId` currently on
|
||||
// the unit identified by `unitGuid`. Returns true and fills the out params
|
||||
// on a hit. `*outExpirationMs` is an absolute `GetTickCount`-epoch timestamp
|
||||
// (0 = unknown / infinite-duration aura); `*outDurationMs` is the applied
|
||||
// duration including the caster's modifiers (talents etc.; 0 = none) — use
|
||||
// it for the `duration` field so it stays consistent with `expirationTime`;
|
||||
// `*outCaster` is the caster's 64-bit GUID (never 0 on a hit). Returns false
|
||||
// on a miss or for zero inputs.
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs);
|
||||
|
||||
// One cached aura, as returned by `Enumerate`.
|
||||
struct CachedAura {
|
||||
uint32_t spellId;
|
||||
uint64_t casterGuid; // 0 = caster unknown (application hook, no SpellGo)
|
||||
uint32_t expirationMs; // 0 = infinite / unknown
|
||||
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
|
||||
};
|
||||
|
||||
// Evicts every cached entry for `unitGuid` whose spellId is NOT present in
|
||||
// `presentSpellIds[0..count)`. Used to reconcile the cache against a unit's
|
||||
// authoritative descriptor when it is back in view (a populated aura array
|
||||
// means the unit is fully synced): an entry the descriptor no longer lists
|
||||
// was removed while we couldn't observe it — e.g. a buff the owner cancelled
|
||||
// while out of our range, whose `OnAuraRemoved` we never received — so drop
|
||||
// it before the descriptor-drop fallback resurfaces it as a phantom. The
|
||||
// caller must only invoke this when the descriptor is populated (count > 0);
|
||||
// an empty array can't distinguish "out of range" from "genuinely buffless",
|
||||
// so reconciling then would wrongly wipe still-valid out-of-range entries.
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *presentSpellIds, int count);
|
||||
|
||||
// Fills `out` with up to `maxOut` cached, non-expired auras on `unitGuid`
|
||||
// whose helpful/harmful classification matches `harmful`. Returns the count
|
||||
// written. The classification is recorded from the aura's descriptor slot at
|
||||
// application time (`OnAuraAdded`/`OnAuraStacksChanged`); entries we only ever
|
||||
// saw via `SMSG_SPELL_GO` have no slot and so an unknown classification —
|
||||
// those are excluded (we can't tell which range they belong to).
|
||||
//
|
||||
// This is the fallback source for `Aura::Data` when the unit descriptor has
|
||||
// dropped an aura's slot (rogue stealth, party range fluctuation) even though
|
||||
// the aura is still active server-side: the cache survives descriptor clears.
|
||||
int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut);
|
||||
|
||||
} // namespace Aura::Source
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `BAG_UPDATE_DELAYED` event — fires once per frame in which any
|
||||
@@ -56,6 +56,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "bag/UpdateDelayed.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
@@ -71,8 +72,15 @@ const Event::Custom::AutoReserve _reserve{kEventName};
|
||||
// Single bool, no atomics needed — the engine is single-threaded.
|
||||
bool g_pending = false;
|
||||
|
||||
// C++ subscribers notified once per bag-change frame (see UpdateDelayed.h).
|
||||
AutoSubscribe *g_subs = nullptr;
|
||||
|
||||
} // namespace
|
||||
|
||||
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 *)();
|
||||
@@ -123,6 +131,9 @@ void OnWorldTick() {
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "");
|
||||
// Notify C++ subscribers of the bag-change frame (see UpdateDelayed.h).
|
||||
for (auto *node = g_subs; node != nullptr; node = node->next)
|
||||
node->cb();
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookSlotDiff{
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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
|
||||
|
||||
// Authoritative "a bag changed this frame" subscription. `Bag::UpdateDelayed`
|
||||
// co-hooks the three (and only three) engine `BAG_UPDATE` fire sites and
|
||||
// coalesces them into a single per-frame signal — the same signal that backs
|
||||
// the `BAG_UPDATE_DELAYED` event. Subscribe here to run C++ work exactly once
|
||||
// per frame in which any bag actually changed, instead of polling every frame.
|
||||
//
|
||||
// Callbacks fire at the tail of the world tick, after every BAG_UPDATE for the
|
||||
// frame has already fired (so bag contents are settled). No firing on frames
|
||||
// where nothing changed. Order across subscribers is unspecified.
|
||||
//
|
||||
// Module pattern, matching `Tick::WorldTick::AutoSubscribe`: declare a
|
||||
// file-scope `static const Bag::UpdateDelayed::AutoSubscribe _s{&Callback};`.
|
||||
|
||||
namespace Bag::UpdateDelayed {
|
||||
|
||||
using Callback = void (*)();
|
||||
|
||||
struct AutoSubscribe {
|
||||
explicit AutoSubscribe(Callback cb);
|
||||
Callback cb;
|
||||
AutoSubscribe *next;
|
||||
};
|
||||
|
||||
} // namespace Bag::UpdateDelayed
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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/>.
|
||||
|
||||
// Lua 5.1 math-library additions that 1.12's Lua 5.0 is missing:
|
||||
//
|
||||
// - `math.fmod(x, y)` — floating-point remainder of `x / y`.
|
||||
//
|
||||
// This is a pure rename: Lua 5.0 ships the exact same C function under the
|
||||
// name `math.mod`; 5.1 renamed the Lua-facing entry to `math.fmod`. We
|
||||
// register `math.fmod` as a direct alias of the engine's `math.mod` C
|
||||
// function (`FUN_LUA_MATH_FMOD`) — no wrapper. `math.mod` stays available too
|
||||
// (we don't touch it), so code written for either name works.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace BaseLib::MathLib {
|
||||
|
||||
namespace {
|
||||
|
||||
// `math.fmod` is 5.0's `math.mod` verbatim — register the engine function
|
||||
// pointer directly rather than wrapping it.
|
||||
const auto Script_math_fmod =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_MATH_FMOD);
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("math", "fmod", Script_math_fmod);
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing this; RegisterTableFunction
|
||||
// targets whichever state is active in each registration hook.
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterFns};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterFns};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::MathLib
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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/>.
|
||||
|
||||
// `select(index, ...)` — the Lua 5.1 base-library vararg helper, missing
|
||||
// from 1.12's Lua 5.0 (it predates the function). Two forms:
|
||||
// - `select('#', ...)` → the number of extra arguments.
|
||||
// - `select(n, ...)` → all arguments from position `n` onward.
|
||||
// `n` may be negative to index from the end (`-1` = the last arg).
|
||||
//
|
||||
// Ported from Lua 5.1's `luaB_select` (lbaselib.c). The numeric form needs
|
||||
// no pushes at all: the varargs are already sitting on the stack at
|
||||
// positions `2..top`, so returning `top - n` hands back exactly the slice
|
||||
// from `n+1` onward — the same trick the reference implementation uses.
|
||||
// The `'#'` form pushes the count and returns 1.
|
||||
//
|
||||
// Registered on BOTH Lua states (in-game + glue): it's a language-level
|
||||
// helper, state-agnostic, and both states are 5.0 and equally missing it.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::Select {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_select(void *L) {
|
||||
const int n = Game::Lua::GetTop(L);
|
||||
if (n < 1) {
|
||||
Game::Lua::Error(L, "bad argument #1 to 'select' (number expected, got no value)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
|
||||
// `select('#', ...)` — count form. Only inspect the string when the
|
||||
// arg *is* a string: calling lua_tostring on a number would convert
|
||||
// the stack value in place (Lua 5.0/5.1 semantics), corrupting arg 1.
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_STRING) {
|
||||
const char *s = Game::Lua::ToString(L, 1);
|
||||
if (s != nullptr && s[0] == '#') {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n - 1));
|
||||
return 1;
|
||||
}
|
||||
// A non-'#' string falls through: lua_isnumber accepts numeric
|
||||
// strings (e.g. "2"), matching the reference's luaL_checkint
|
||||
// coercion; a non-numeric string errors below.
|
||||
}
|
||||
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "bad argument #1 to 'select' (number expected)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
int i = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (i < 0)
|
||||
i = n + i; // negative indexes count back from the last argument
|
||||
else if (i > n)
|
||||
i = n;
|
||||
if (i < 1) {
|
||||
Game::Lua::Error(L, "bad argument #1 to 'select' (index out of range)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
// The varargs from position i+1..n are already on the stack; returning
|
||||
// their count hands exactly that slice back to the caller.
|
||||
return n - i;
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("select", &Script_select);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("select", &Script_select);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::Select
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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/>.
|
||||
|
||||
// Lua 5.1 string-library additions that 1.12's Lua 5.0 is missing:
|
||||
//
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction.
|
||||
// - `string.gmatch(s, pattern)` — match iterator.
|
||||
//
|
||||
// Both are the same underlying pattern engine as functions 5.0 already ships:
|
||||
// - `match` is `find` returning captures / the whole match instead of the
|
||||
// start/end indices — so we call the engine's `str_find` and transform.
|
||||
// - `gmatch` is exactly 5.0's `string.gfind` (renamed in 5.1); we register
|
||||
// it as a direct alias of the engine's `gfind` C function.
|
||||
//
|
||||
// NOTE: only the `string.foo(s, ...)` call form works — NOT the
|
||||
// `("x"):foo(...)` method sugar. WoW's Lua VM strips type-metatables for
|
||||
// every value except tables/userdata (both `lua_setmetatable` and the VM's
|
||||
// `luaT_gettmbyobj` hard-return "no metamethod" for strings), so strings have
|
||||
// no `__index`, and there's no client-side way to add one short of hooking a
|
||||
// hot VM-core function. This is the long-standing vanilla constraint.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace BaseLib::StringLib {
|
||||
|
||||
namespace {
|
||||
|
||||
// `string.match(s, pattern [, init])`. Calls the engine `str_find` and
|
||||
// reshapes its result: no match → nil; match with captures → the captures;
|
||||
// match with no captures → the whole matched substring.
|
||||
int __fastcall Script_string_match(void *L) {
|
||||
// Normalize to exactly (s, pattern, init): pad missing args with nil so
|
||||
// str_find defaults `init` to 1, and drop any stray 4th value so it can't
|
||||
// be misread as find's `plain` flag (match has no plain form).
|
||||
Game::Lua::SetTop(L, 3);
|
||||
constexpr int kBase = 3;
|
||||
|
||||
auto strFind = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_FIND);
|
||||
const int nres = strFind(L); // pushes start,end,caps.. above kBase, or a nil
|
||||
|
||||
if (nres <= 1) {
|
||||
// No match. str_find pushed the nil (return 1); guard the degenerate
|
||||
// nres==0 case by pushing our own nil so the shape holds.
|
||||
if (nres < 1)
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nres == 2) {
|
||||
// No captures → the whole matched substring s[start..end] (1-based,
|
||||
// inclusive). An empty match has end == start-1 → an empty string.
|
||||
const int start = static_cast<int>(Game::Lua::ToNumber(L, kBase + 1));
|
||||
const int end = static_cast<int>(Game::Lua::ToNumber(L, kBase + 2));
|
||||
// str_find already coerced arg 1 to a string, so this is safe.
|
||||
const char *s = Game::Lua::ToString(L, 1);
|
||||
Game::Lua::SetTop(L, kBase); // drop start, end
|
||||
const size_t len = (end >= start) ? static_cast<size_t>(end - start + 1) : 0;
|
||||
Game::Lua::PushLString(L, s + start - 1, static_cast<unsigned int>(len));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Captures present → drop start & end, hand back only the captures.
|
||||
Game::Lua::Remove(L, kBase + 1); // remove start; end → kBase+1, caps → kBase+2..
|
||||
Game::Lua::Remove(L, kBase + 1); // remove end; caps → kBase+1..
|
||||
return nres - 2;
|
||||
}
|
||||
|
||||
// `string.gmatch` is 5.0's `string.gfind` verbatim — register the engine
|
||||
// function pointer directly rather than wrapping it.
|
||||
const auto Script_string_gmatch =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GFIND);
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing these; RegisterTableFunction
|
||||
// targets whichever state is active in each registration hook.
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterFns};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterFns};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::StringLib
|
||||
@@ -0,0 +1,20 @@
|
||||
<!-- Spliced into the engine's Interface\FrameXML\Bindings.xml at
|
||||
load time by src/bindings/Inject.cpp. The `header` attribute on
|
||||
the root tag tells the splicer which engine section to extend —
|
||||
it picks the appropriate insertion point so the bindings appear
|
||||
at the tail of that group in the keybind UI. The engine's own
|
||||
XML parser ignores attributes on the root tag, so this is
|
||||
metadata for the splicer only. -->
|
||||
<Bindings header="TARGETING">
|
||||
<Binding name="FOCUSTARGET">
|
||||
FocusUnit("target");
|
||||
</Binding>
|
||||
<Binding name="TARGETFOCUS">
|
||||
TargetUnit("focus");
|
||||
</Binding>
|
||||
<Binding name="AOELOOT">
|
||||
if not C_Loot.IsScanInProgress() then
|
||||
C_Loot.ScanNearbyLoot()
|
||||
end
|
||||
</Binding>
|
||||
</Bindings>
|
||||
@@ -0,0 +1,297 @@
|
||||
// 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/>.
|
||||
|
||||
// Inject ClassicAPI-owned bindings into a chosen engine section.
|
||||
//
|
||||
// Why a file-read hook rather than an addon-side Bindings.xml: vanilla
|
||||
// stores every binding in a single linearly-indexed table, and the
|
||||
// keybind UI walks that table in index order with `HEADER_*` pseudo-
|
||||
// entries splitting groups. Addon Bindings.xml files get parsed
|
||||
// AFTER FrameXML, so even with `header="TARGETING"` an addon binding
|
||||
// receives a display index higher than every engine binding — it
|
||||
// renders orphaned at the very bottom of the keybind list rather than
|
||||
// under TARGETING.
|
||||
//
|
||||
// To land inside a specific section, the binding's display index has
|
||||
// to fall between that section's `HEADER_*` entry and the next one.
|
||||
// The cleanest way to arrange that is to splice the bindings into the
|
||||
// FrameXML file the engine itself parses, so they take their indices
|
||||
// from the same running counter every native binding does.
|
||||
//
|
||||
// What gets spliced is driven by `src/bindings/Bindings.xml`. The
|
||||
// root `<Bindings header="X">` tag names the engine section to extend
|
||||
// (e.g. `"TARGETING"`); the inner `<Binding>` children are spliced
|
||||
// verbatim. The splicer locates `header="X"` in the engine file
|
||||
// (which appears on the first binding of section X), then the next
|
||||
// `header="..."` after that (start of the following section), and
|
||||
// inserts our content just before that next-section marker — so our
|
||||
// entries land at the tail of section X. If X is the last section in
|
||||
// the file, the splice goes just before `</Bindings>`.
|
||||
//
|
||||
// The engine's binding parser is `FUN_004b6f70`. It calls
|
||||
// `FUN_FILE_READ` (`0x00648620`) to load the file, then walks `<Binding>`
|
||||
// elements assigning each the next display index from a running
|
||||
// counter. By the time our injected bindings are processed, the
|
||||
// engine has already emitted the section's `HEADER_*` and a sequence
|
||||
// of native entries; ours append to that sequence and the next
|
||||
// section's header follows.
|
||||
|
||||
#include "Inject.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include "embedded_bindings.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace Bindings::Inject {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kFrameXMLBindingsPath = "Interface\\FrameXML\\Bindings.xml";
|
||||
|
||||
// Parsed view of `src/bindings/Bindings.xml`:
|
||||
// - `header` is the value of `header="..."` on the root `<Bindings>`
|
||||
// tag. Tells the splicer which engine section to extend.
|
||||
// - `innerData / innerSize` point at the bytes between the
|
||||
// `<Bindings ...>` opening and `</Bindings>` closing tags.
|
||||
//
|
||||
// All-zero fields mean "embedded buffer is malformed" — defensive,
|
||||
// shouldn't happen since the source is build-time-validated. Splice
|
||||
// is skipped (engine sees its original Bindings.xml unchanged).
|
||||
struct EmbeddedConfig {
|
||||
char header[64];
|
||||
const char *innerData;
|
||||
size_t innerSize;
|
||||
};
|
||||
|
||||
// Advances `p` past any `<!-- ... -->` comment block starting at the
|
||||
// current position. Returns the new position (past the closing `-->`)
|
||||
// if a comment was skipped, or `p` unchanged if no comment was at
|
||||
// the current position. Caps at `end` if the comment is unterminated.
|
||||
const char *SkipCommentIfAny(const char *p, const char *end) {
|
||||
if (p + 4 > end) return p;
|
||||
if (std::memcmp(p, "<!--", 4) != 0) return p;
|
||||
p += 4;
|
||||
while (p + 3 <= end) {
|
||||
if (std::memcmp(p, "-->", 3) == 0)
|
||||
return p + 3;
|
||||
++p;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
// Linear scan for `marker` in `[start, end)`, skipping any `<!--...-->`
|
||||
// comment blocks (where unrelated occurrences of XML tags might live).
|
||||
const char *FindOutsideComment(const char *start, const char *end, const char *marker) {
|
||||
const size_t markerLen = std::strlen(marker);
|
||||
const char *p = start;
|
||||
while (p + markerLen <= end) {
|
||||
const char *afterComment = SkipCommentIfAny(p, end);
|
||||
if (afterComment != p) {
|
||||
p = afterComment;
|
||||
continue;
|
||||
}
|
||||
if (std::memcmp(p, marker, markerLen) == 0)
|
||||
return p;
|
||||
++p;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Parses `header="X"` out of the byte range `[tagStart, tagEnd)` (the
|
||||
// opening `<Bindings ...>` tag). Copies the value into `out` (size
|
||||
// `outSize`) and returns true on success.
|
||||
bool ExtractHeaderAttr(const char *tagStart, const char *tagEnd,
|
||||
char *out, size_t outSize) {
|
||||
static constexpr const char kAttr[] = "header=\"";
|
||||
constexpr size_t kAttrLen = sizeof(kAttr) - 1;
|
||||
for (const char *p = tagStart; p + kAttrLen < tagEnd; ++p) {
|
||||
if (std::memcmp(p, kAttr, kAttrLen) != 0) continue;
|
||||
const char *valStart = p + kAttrLen;
|
||||
const char *valEnd = valStart;
|
||||
while (valEnd < tagEnd && *valEnd != '"') ++valEnd;
|
||||
const size_t len = static_cast<size_t>(valEnd - valStart);
|
||||
if (len == 0 || len + 1 > outSize) return false;
|
||||
std::memcpy(out, valStart, len);
|
||||
out[len] = '\0';
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
EmbeddedConfig ResolveEmbeddedConfig() {
|
||||
EmbeddedConfig out = {};
|
||||
const char *content = reinterpret_cast<const char *>(kEmbeddedBindingsXml);
|
||||
const char *end = content + kEmbeddedBindingsXmlSize;
|
||||
|
||||
// Locate `<Bindings` opening tag (skipping any comment blocks
|
||||
// that mention the tag literally for documentation purposes).
|
||||
const char *open = FindOutsideComment(content, end, "<Bindings");
|
||||
if (open == nullptr) return out;
|
||||
|
||||
// Find the closing `>` of the opening tag.
|
||||
const char *tagEnd = open;
|
||||
while (tagEnd < end && *tagEnd != '>') ++tagEnd;
|
||||
if (tagEnd == end) return out;
|
||||
|
||||
// Pull out the `header="..."` attribute. Required — this tells
|
||||
// the splicer which engine section to extend.
|
||||
if (!ExtractHeaderAttr(open, tagEnd, out.header, sizeof(out.header)))
|
||||
return out;
|
||||
|
||||
const char *innerStart = tagEnd + 1;
|
||||
const char *close = FindOutsideComment(innerStart, end, "</Bindings>");
|
||||
if (close == nullptr || close < innerStart) {
|
||||
out.header[0] = '\0';
|
||||
return out;
|
||||
}
|
||||
|
||||
out.innerData = innerStart;
|
||||
out.innerSize = static_cast<size_t>(close - innerStart);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Computes the byte offset in the engine's Bindings.xml buffer where
|
||||
// our entries should be spliced — i.e., the tail of the section
|
||||
// identified by `headerName`. Strategy:
|
||||
//
|
||||
// 1. Find `header="<HEADER>"` in the engine's buffer (skipping
|
||||
// comments). This is the attribute the engine itself uses on
|
||||
// the FIRST binding of each section, so this finds where that
|
||||
// section *starts*.
|
||||
// 2. Find the NEXT `header="` after that. That marks the start of
|
||||
// the FOLLOWING section.
|
||||
// 3. Back up from that next-header position to the `<` of the
|
||||
// `<Binding` tag containing it — that's our splice point.
|
||||
// 4. If there's no next header (last section in the file), splice
|
||||
// just before `</Bindings>`.
|
||||
//
|
||||
// Returns `nullptr` if step 1 fails (header missing from engine file
|
||||
// — e.g., a heavily-modified FrameXML). Caller falls back to passing
|
||||
// the original buffer through unchanged.
|
||||
const char *FindSplicePosition(const char *content, size_t size,
|
||||
const char *headerName) {
|
||||
const char *end = content + size;
|
||||
|
||||
char headerNeedle[80];
|
||||
std::snprintf(headerNeedle, sizeof(headerNeedle),
|
||||
"header=\"%s\"", headerName);
|
||||
|
||||
const char *sectionStart = FindOutsideComment(content, end, headerNeedle);
|
||||
if (sectionStart == nullptr) return nullptr;
|
||||
|
||||
// Walk past the matched attribute, then look for the next
|
||||
// `header="` anywhere later in the file.
|
||||
const char *afterCurrent = sectionStart + std::strlen(headerNeedle);
|
||||
const char *nextHeader = FindOutsideComment(afterCurrent, end, "header=\"");
|
||||
|
||||
if (nextHeader != nullptr) {
|
||||
// Back up to the `<` of the `<Binding` tag that owns this
|
||||
// next-header attribute — that `<` is where the next section
|
||||
// visually starts, and we splice immediately before it.
|
||||
const char *p = nextHeader;
|
||||
while (p > content && *p != '<') --p;
|
||||
return p;
|
||||
}
|
||||
|
||||
// Last section in the file — splice just before `</Bindings>`.
|
||||
return FindOutsideComment(content, end, "</Bindings>");
|
||||
}
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
if (c >= 'A' && c <= 'Z') return static_cast<char>(c + 32);
|
||||
return c;
|
||||
}
|
||||
|
||||
bool PathEqualsCI(const char *a, const char *b) {
|
||||
while (*a != '\0' && *b != '\0') {
|
||||
if (NormalizeChar(*a) != NormalizeChar(*b)) return false;
|
||||
++a; ++b;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
using SMemAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using SMemFree_t = void(__stdcall *)(void *buf, const char *file, int line, int flags);
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn original) {
|
||||
if (path == nullptr || !PathEqualsCI(path, kFrameXMLBindingsPath))
|
||||
return false;
|
||||
|
||||
// Pull the real file through the original reader with the caller's
|
||||
// requested `extraBytes` honored — the original allocates a Storm
|
||||
// buffer of `fileSize + extraBytes` and zero-fills the tail.
|
||||
void *origBuf = nullptr;
|
||||
size_t origSize = 0;
|
||||
const int readOk = original(unused, path, &origBuf, &origSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (readOk == 0 || origBuf == nullptr)
|
||||
return false;
|
||||
|
||||
static const EmbeddedConfig config = ResolveEmbeddedConfig();
|
||||
if (config.innerData == nullptr || config.innerSize == 0 ||
|
||||
config.header[0] == '\0') {
|
||||
// Embedded XML is missing the `<Bindings header="...">` root
|
||||
// or the `</Bindings>` close tag. Pass the original through.
|
||||
if (outBuf != nullptr) *outBuf = origBuf;
|
||||
if (outSize != nullptr) *outSize = origSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *content = static_cast<const char *>(origBuf);
|
||||
const char *splice = FindSplicePosition(content, origSize, config.header);
|
||||
if (splice == nullptr) {
|
||||
// Requested section isn't present in this engine file (heavily
|
||||
// modified FrameXML or different build). Pass through unchanged
|
||||
// rather than risking corruption.
|
||||
if (outBuf != nullptr) *outBuf = origBuf;
|
||||
if (outSize != nullptr) *outSize = origSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
const size_t splicePos = static_cast<size_t>(splice - content);
|
||||
const size_t newSize = origSize + config.innerSize;
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
|
||||
void *newBuf = SMemAlloc(newSize + extraBytes, __FILE__, __LINE__, 0);
|
||||
if (newBuf == nullptr) {
|
||||
// Allocation failed — fall back to the original buffer so the
|
||||
// engine still loads its own bindings.
|
||||
if (outBuf != nullptr) *outBuf = origBuf;
|
||||
if (outSize != nullptr) *outSize = origSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto *dest = static_cast<char *>(newBuf);
|
||||
std::memcpy(dest, content, splicePos);
|
||||
std::memcpy(dest + splicePos, config.innerData, config.innerSize);
|
||||
std::memcpy(dest + splicePos + config.innerSize,
|
||||
content + splicePos, origSize - splicePos);
|
||||
if (extraBytes > 0)
|
||||
std::memset(dest + newSize, 0, extraBytes);
|
||||
|
||||
SMemFree(origBuf, __FILE__, __LINE__, 0);
|
||||
|
||||
if (outBuf != nullptr) *outBuf = newBuf;
|
||||
if (outSize != nullptr) *outSize = newSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Bindings::Inject
|
||||
@@ -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/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Bindings::Inject {
|
||||
|
||||
// `FUN_FILE_READ` shape — Storm-style: __stdcall, returns 1 on success
|
||||
// with the buffer allocated via SMemAlloc into `*outBuf` (size in
|
||||
// `*outSize`, plus `extraBytes` of trailing zero padding).
|
||||
using FileReadFn = int(__stdcall *)(int unused, const char *path,
|
||||
void **outBuf, size_t *outSize,
|
||||
size_t extraBytes,
|
||||
int flag1, int flag2);
|
||||
|
||||
// Path-matches `Interface\FrameXML\Bindings.xml`. On match: fetches
|
||||
// the real file via `original`, splices our ClassicAPI bindings into
|
||||
// the TARGETING block, and returns `true` with `*outBuf` / `*outSize`
|
||||
// set to a fresh Storm buffer the caller can SMemFree normally.
|
||||
// Returns `false` for any other path — caller should fall through to
|
||||
// its own logic.
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn original);
|
||||
|
||||
} // namespace Bindings::Inject
|
||||
Vendored
+224
@@ -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/>.
|
||||
|
||||
#include "cache/QueryLoad.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Cache::QueryLoad {
|
||||
|
||||
namespace {
|
||||
|
||||
// A cache class's `_GetRecord`. callback=nullptr → pure peek (returns the
|
||||
// loaded data block or nullptr, no query). Non-null callback → also fires
|
||||
// the network query on a miss and stores `(callback, userData)` on the
|
||||
// entry's pending list. dedup=1 so repeat fetches for the same id before
|
||||
// the response lands don't queue the callback twice.
|
||||
using GetCacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, char dedup);
|
||||
|
||||
// A cache class's response parser, which we hook (e.g.
|
||||
// FUN_CREATURE_QUERY_RESPONSE / FUN_GAMEOBJECT_QUERY_RESPONSE).
|
||||
using CacheResponse_t = void(__thiscall *)(void *cache, void *packetReader, int flag);
|
||||
|
||||
// Engine callback shape (__stdcall, ret 8) — see Item::Data. We only pass
|
||||
// it to make `_GetRecord` send the query (the trigger is callback != 0);
|
||||
// completion is observed via the response hook + tick sweep.
|
||||
void __stdcall NoOpCallback(void * /*userData*/, int /*success*/) {}
|
||||
|
||||
const uint8_t *CacheFetch(uintptr_t getRecord, void *cache, uint32_t id, void *callback) {
|
||||
auto fn = reinterpret_cast<GetCacheRecord_t>(getRecord);
|
||||
const uint64_t zeroGuid = 0;
|
||||
void *userData =
|
||||
callback ? reinterpret_cast<void *>(static_cast<uintptr_t>(id)) : nullptr;
|
||||
return fn(cache, id, &zeroGuid, callback, userData, 1);
|
||||
}
|
||||
|
||||
struct Pending {
|
||||
uint32_t id;
|
||||
int ticksWaiting;
|
||||
};
|
||||
|
||||
constexpr int kMaxCaches = 8;
|
||||
constexpr int kMaxPending = 256;
|
||||
constexpr int kMaxWaitTicks = 1200; // give up and fire failure after this many WorldTicks
|
||||
|
||||
struct Registered {
|
||||
void *cache;
|
||||
const char *eventName;
|
||||
uintptr_t getRecord; // this cache class's _GetRecord
|
||||
Pending pending[kMaxPending];
|
||||
int pendingCount;
|
||||
};
|
||||
|
||||
Registered g_caches[kMaxCaches];
|
||||
int g_cacheCount = 0;
|
||||
|
||||
Registered *Find(void *cache) {
|
||||
for (int i = 0; i < g_cacheCount; ++i) {
|
||||
if (g_caches[i].cache == cache)
|
||||
return &g_caches[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Fire(const Registered *r, uint32_t id, bool success) {
|
||||
Event::Custom::FireIdSuccess(Event::Custom::Lookup(r->eventName),
|
||||
static_cast<int>(id), success);
|
||||
}
|
||||
|
||||
int FindPending(const Registered *r, uint32_t id) {
|
||||
for (int i = 0; i < r->pendingCount; ++i) {
|
||||
if (r->pending[i].id == id)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void RemovePendingAt(Registered *r, int idx) {
|
||||
--r->pendingCount;
|
||||
if (idx != r->pendingCount)
|
||||
r->pending[idx] = r->pending[r->pendingCount];
|
||||
}
|
||||
|
||||
bool IsLoaded(const Registered *r, uint32_t id) {
|
||||
return CacheFetch(r->getRecord, r->cache, id, nullptr) != nullptr;
|
||||
}
|
||||
|
||||
// Fire + remove any pending id whose entry is now loaded.
|
||||
void Sweep(Registered *r) {
|
||||
for (int i = 0; i < r->pendingCount;) {
|
||||
if (IsLoaded(r, r->pending[i].id)) {
|
||||
const uint32_t id = r->pending[i].id;
|
||||
RemovePendingAt(r, i);
|
||||
Fire(r, id, true);
|
||||
continue;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
// Shared post-response dispatch: a parser for `cache` just ran, so sweep
|
||||
// that cache's pending set (no-op for caches we don't track).
|
||||
void DispatchSweep(void *cache) {
|
||||
Registered *r = Find(cache);
|
||||
if (r != nullptr)
|
||||
Sweep(r);
|
||||
}
|
||||
|
||||
// One response-handler hook per cache class. Each calls its own original
|
||||
// trampoline (the parser fills the entry + walks the engine's own
|
||||
// callback list), then sweeps via the shared dispatch. `__thiscall`
|
||||
// mimicked as `__fastcall(ecx, edx_unused, …)`.
|
||||
using Trampoline_t = void(__fastcall *)(void *cache, void *edx, void *packetReader, int flag);
|
||||
|
||||
CacheResponse_t g_origCreatureParser = nullptr;
|
||||
void __fastcall CreatureParser_h(void *cache, void * /*edx*/, void *packetReader, int flag) {
|
||||
reinterpret_cast<Trampoline_t>(g_origCreatureParser)(cache, nullptr, packetReader, flag);
|
||||
DispatchSweep(cache);
|
||||
}
|
||||
|
||||
CacheResponse_t g_origGameObjectParser = nullptr;
|
||||
void __fastcall GameObjectParser_h(void *cache, void * /*edx*/, void *packetReader, int flag) {
|
||||
reinterpret_cast<Trampoline_t>(g_origGameObjectParser)(cache, nullptr, packetReader, flag);
|
||||
DispatchSweep(cache);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookCreature{
|
||||
Offsets::FUN_CREATURE_QUERY_RESPONSE, reinterpret_cast<void *>(&CreatureParser_h),
|
||||
reinterpret_cast<void **>(&g_origCreatureParser)};
|
||||
const Game::HookAutoRegister _hookGameObject{
|
||||
Offsets::FUN_GAMEOBJECT_QUERY_RESPONSE, reinterpret_cast<void *>(&GameObjectParser_h),
|
||||
reinterpret_cast<void **>(&g_origGameObjectParser)};
|
||||
|
||||
// Timeout + safety net. The hooks cover the happy path; this fires
|
||||
// failure for ids that never resolve after `kMaxWaitTicks`, and
|
||||
// re-checks loaded as a backstop for fills observed off the hook path.
|
||||
void OnWorldTick() {
|
||||
for (int c = 0; c < g_cacheCount; ++c) {
|
||||
Registered *r = &g_caches[c];
|
||||
for (int i = 0; i < r->pendingCount;) {
|
||||
if (IsLoaded(r, r->pending[i].id)) {
|
||||
const uint32_t id = r->pending[i].id;
|
||||
RemovePendingAt(r, i);
|
||||
Fire(r, id, true);
|
||||
continue;
|
||||
}
|
||||
if (r->pending[i].ticksWaiting >= kMaxWaitTicks) {
|
||||
const uint32_t id = r->pending[i].id;
|
||||
RemovePendingAt(r, i);
|
||||
Fire(r, id, false);
|
||||
continue;
|
||||
}
|
||||
++r->pending[i].ticksWaiting;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr) {
|
||||
if (cacheInstance == nullptr || eventName == nullptr || getRecordAddr == 0)
|
||||
return;
|
||||
if (Find(cacheInstance) != nullptr) // idempotent (safe across /reload)
|
||||
return;
|
||||
if (g_cacheCount >= kMaxCaches)
|
||||
return;
|
||||
g_caches[g_cacheCount].cache = cacheInstance;
|
||||
g_caches[g_cacheCount].eventName = eventName;
|
||||
g_caches[g_cacheCount].getRecord = getRecordAddr;
|
||||
g_caches[g_cacheCount].pendingCount = 0;
|
||||
++g_cacheCount;
|
||||
}
|
||||
|
||||
bool IsCached(void *cacheInstance, uint32_t id) {
|
||||
const Registered *r = Find(cacheInstance);
|
||||
return r != nullptr && IsLoaded(r, id);
|
||||
}
|
||||
|
||||
bool RequestLoad(void *cacheInstance, uint32_t id) {
|
||||
Registered *r = Find(cacheInstance);
|
||||
if (r == nullptr || id == 0)
|
||||
return false;
|
||||
|
||||
// Already cached — fire success synchronously (modern RequestLoad
|
||||
// contract: the completion event fires even on a cache hit).
|
||||
if (IsLoaded(r, id)) {
|
||||
Fire(r, id, true);
|
||||
return true;
|
||||
}
|
||||
// Already in flight — one completion event per id.
|
||||
if (FindPending(r, id) >= 0)
|
||||
return true;
|
||||
if (r->pendingCount >= kMaxPending)
|
||||
return false;
|
||||
|
||||
// Send the query (non-null callback is the trigger), then track until
|
||||
// the response hook / tick fires the event.
|
||||
CacheFetch(r->getRecord, cacheInstance, id, reinterpret_cast<void *>(&NoOpCallback));
|
||||
r->pending[r->pendingCount].id = id;
|
||||
r->pending[r->pendingCount].ticksWaiting = 0;
|
||||
++r->pendingCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Cache::QueryLoad
|
||||
Vendored
+65
@@ -0,0 +1,65 @@
|
||||
// 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>
|
||||
|
||||
// Shared async-load machinery for the client query caches that aren't
|
||||
// item/quest — creature, gameobject, and (potentially) the other
|
||||
// SMSG_*_QUERY caches. Modeled on `Item::Data`'s pending/timeout/event
|
||||
// logic, generalized so several caches share one set of code.
|
||||
//
|
||||
// These caches are sibling classes: they share the record/entry hash-map
|
||||
// shape but differ in `_GetRecord` and response-parser per class (the
|
||||
// creature class reads its loaded flag at entry+0x50, the gameobject
|
||||
// class at +0x98, etc.). So a module registers BOTH its cache's
|
||||
// `_GetRecord` address and the dispatcher routes completion by cache
|
||||
// instance. The dispatcher hooks each class's response parser (MinHook
|
||||
// allows only one hook per target, but the parsers are distinct
|
||||
// functions, so hooking creature's and gameobject's separately is fine);
|
||||
// item/quest have their own parsers and aren't touched.
|
||||
//
|
||||
// static const char *kEvent = "CREATURE_DATA_LOAD_RESULT";
|
||||
// static const Event::Custom::AutoReserve _r{kEvent}; // reserve the event
|
||||
// ...
|
||||
// Cache::QueryLoad::Register(cacheInstance, kEvent,
|
||||
// Offsets::FUN_CREATURE_GET_RECORD); // at init
|
||||
// Cache::QueryLoad::RequestLoad(cacheInstance, id); // fire a load
|
||||
//
|
||||
// The completion event fires as `(id: number, success: bool)` — success
|
||||
// when the cache fills, failure on timeout.
|
||||
|
||||
namespace Cache::QueryLoad {
|
||||
|
||||
// Register a query cache for async-load tracking. `cacheInstance` is the
|
||||
// cache global (e.g. `Offsets::VAR_CREATURE_CACHE` cast to void*);
|
||||
// `eventName` is a Custom event the caller has reserved via
|
||||
// `Event::Custom::AutoReserve`, fired `(id, success)` on completion;
|
||||
// `getRecordAddr` is the cache class's `_GetRecord` (e.g.
|
||||
// `Offsets::FUN_CREATURE_GET_RECORD`). `cacheInstance`/`eventName` must
|
||||
// outlive the process (pass static values). Idempotent — calling again
|
||||
// for the same cache is a no-op (safe across /reload).
|
||||
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr);
|
||||
|
||||
// True if `id` is already in the cache (pure peek, no network query).
|
||||
bool IsCached(void *cacheInstance, uint32_t id);
|
||||
|
||||
// Issue an async load for `id`. If already cached, fires the completion
|
||||
// event with success synchronously. Otherwise sends the query and tracks
|
||||
// the id until the response lands (or it times out). Returns false only
|
||||
// if the cache isn't registered or its pending set is full; true
|
||||
// otherwise (already-cached, already-pending, or newly-tracked).
|
||||
bool RequestLoad(void *cacheInstance, uint32_t id);
|
||||
|
||||
} // namespace Cache::QueryLoad
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)`
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// 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
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// 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
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Class metadata accessors backed by `ChrClasses.dbc`.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.GetContainerItemCharges(containerIndex, slotIndex)` —
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.GetItemCooldown(itemID)` — modern signature only
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.GetContainerItemDurability(containerIndex, slotIndex)` —
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
@@ -130,11 +130,39 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `C_Container.CalculateTotalNumberOfFreeBagSlots()` — the total number of
|
||||
// free slots across the player's *general-purpose* bags (backpack + equipped
|
||||
// bags 0..4 whose BagFamily is 0). Specialty bags (quivers, soul bags, herb
|
||||
// bags, …) are excluded, mirroring FrameXML's implementation exactly:
|
||||
//
|
||||
// for bag = BACKPACK .. lastBag:
|
||||
// free, family = C_Container.GetContainerNumFreeSlots(bag)
|
||||
// if family == 0 then total = total + free end
|
||||
//
|
||||
// Takes no arguments. Caveat inherited from `GetContainerNumFreeSlots`:
|
||||
// vanilla-era server data leaves most bags' BagFamily at 0, so specialty
|
||||
// bags that retail *would* exclude (Soul Pouch, Enchanting Bag, …) get
|
||||
// counted here — the client just doesn't carry the family bit for them. The
|
||||
// result stays consistent with what `GetContainerNumFreeSlots` reports for
|
||||
// those same bags.
|
||||
int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
int total = 0;
|
||||
for (int bagID = 0; bagID <= 4; bagID++) {
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
if (info.slotCount > 0 && info.bagType == 0)
|
||||
total += CountFreeSlotsInBag(L, bagID, info.slotCount);
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(total));
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
|
||||
&Script_C_Container_GetContainerNumFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
|
||||
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_Container.HasContainerItem(bagIndex, slotIndex)` — returns whether the
|
||||
// given bag slot currently holds an item (a Classic Era addition). It's the
|
||||
// occupancy sibling of `GetContainerItemID`: resolves the slot to its CGItem
|
||||
// the same way (`Item::Location::ResolveBag` → engine `PackBagSlot` →
|
||||
// `GetItemBySlot`), and a populated slot yields a non-null pointer while an
|
||||
// empty slot yields null. Presence only — it does not depend on the item's
|
||||
// static data being cached. Returns `false` for empty slots and invalid
|
||||
// bag/slot indices.
|
||||
|
||||
#include "Game.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
namespace Container::HasItem {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Container_HasContainerItem(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: C_Container.HasContainerItem(bagIndex, slotIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
const bool hasItem = Item::Location::ResolveBag(L, bagID, slotIndex) != nullptr;
|
||||
Game::Lua::PushBool(L, hasItem);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "HasContainerItem",
|
||||
&Script_C_Container_HasContainerItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::HasItem
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.GetContainerItemID(bagIndex, slotIndex)` — modern
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_Container.GetContainerItemInfo(containerIndex, slotIndex)` — the modern
|
||||
// structured-table accessor (namespaced + table-returning since Patch
|
||||
// 10.0.2). Vanilla's global `GetContainerItemInfo` returns a flat 5-tuple
|
||||
// (`texture, itemCount, locked, quality, readable` — verified by decoding
|
||||
// `Script_GetContainerItemInfo` at `0x004F9670`); backported addons expect
|
||||
// the `ContainerItemInfo` table with the wider modern field set. We assemble
|
||||
// that table from the same sources the engine reads plus the extra fields it
|
||||
// never surfaced in 1.12.
|
||||
//
|
||||
// Field sources:
|
||||
// iconFileID — ItemStats displayInfoID → ItemDisplayInfo.dbc icon path.
|
||||
// (1.12 has no fileID system; we return the icon *path*
|
||||
// string, same convention as `GetItemIcon`.)
|
||||
// stackCount — CGItem descriptor `+OFF_DESCRIPTOR_STACK_COUNT` (the
|
||||
// engine's `itemCount` return).
|
||||
// isLocked — CGItem `+OFF_ITEM_CLIENT_LOCK & 1` (the `locked` return).
|
||||
// quality — ItemStats `+OFF_ITEMSTATS_QUALITY`; nil when uncached
|
||||
// (modern contract: `Enum.ItemQuality or nil`).
|
||||
// isReadable — static page text OR the per-instance readable-text id,
|
||||
// mirroring the engine's `readable` branch at `0x004F97BD`.
|
||||
// hasLoot — ItemStats LOOTABLE flag (best-effort static approximation:
|
||||
// vanilla can't track post-loot dynamic emptiness client-side).
|
||||
// hyperlink — engine link builder via `Item::Link::FromCGItem` (enchant /
|
||||
// random-suffix decorated, same as `GetContainerItemLink`).
|
||||
// isFiltered — always false (vanilla has no bag search-filter system).
|
||||
// hasNoValue — ItemStats sellPrice == 0.
|
||||
// itemID — instance block itemID (`Item::ID::FromCGItem`).
|
||||
// isBound — CGItem descriptor SOULBOUND flag (`C_Item.IsBound`'s bit).
|
||||
// itemName — instance-decorated display name, base ItemStats name on miss.
|
||||
//
|
||||
// Returns nil for an empty slot or an invalid bag/slot index (modern
|
||||
// contract). Cache-derived fields (icon / quality / hasLoot / hasNoValue /
|
||||
// name) are omitted/nil when the item's static data isn't cached yet — the
|
||||
// live per-instance fields (count / locked / bound / itemID / link) are always
|
||||
// present. Passive: does not warm the item cache.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/StatAccum.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Container::ItemInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
bool BuildIconPath(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
out[0] = '\0';
|
||||
const char *iconName = DBC::StringField(
|
||||
Offsets::VAR_ITEMDISPLAYINFO_RECORDS, Offsets::VAR_ITEMDISPLAYINFO_COUNT,
|
||||
displayInfoID, Offsets::OFF_ITEMDISPLAYINFO_ICON);
|
||||
if (iconName == nullptr || iconName[0] == '\0')
|
||||
return false;
|
||||
std::snprintf(out, outSize, "Interface\\Icons\\%s", iconName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mirrors the engine's `readable` decision (0x004F97BD..0x004F97CF): a slot
|
||||
// is readable when its static page text is set OR the per-instance
|
||||
// readable-text id is non-zero (letters / server-authored notes).
|
||||
bool ItemReadable(const uint8_t *item, const uint8_t *record) {
|
||||
if (record != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(record + Offsets::OFF_ITEMSTATS_PAGE_TEXT) != 0)
|
||||
return true;
|
||||
const uint8_t *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
return descriptor != nullptr &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_READABLE_TEXT_ID) != 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.GetContainerItemInfo(containerIndex, slotIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
const uint8_t *item = Item::Location::ResolveBag(L, bagID, slotIndex);
|
||||
if (item == nullptr) {
|
||||
Game::Lua::PushNil(L); // empty slot / bad index → nil (modern contract)
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Gather everything into locals BEFORE touching the Lua stack. The
|
||||
// engine link/name builders and cache/DBC reads below are pure C; keeping
|
||||
// them off the half-built table avoids any stack interleaving. ---
|
||||
const int itemID = Item::ID::FromCGItem(item);
|
||||
const uint8_t *record = (itemID > 0)
|
||||
? Item::StatAccum::FetchRecord(static_cast<uint32_t>(itemID))
|
||||
: nullptr;
|
||||
|
||||
// Live per-instance descriptor fields.
|
||||
const uint8_t *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
item + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
int stackCount = 0;
|
||||
bool isBound = false;
|
||||
if (descriptor != nullptr) {
|
||||
stackCount = *reinterpret_cast<const int *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
isBound = (*reinterpret_cast<const uint32_t *>(
|
||||
descriptor + Offsets::OFF_DESCRIPTOR_FLAGS) &
|
||||
Offsets::ITEM_FLAG_SOULBOUND) != 0;
|
||||
}
|
||||
const bool isLocked = (*reinterpret_cast<const uint32_t *>(
|
||||
item + Offsets::OFF_ITEM_CLIENT_LOCK) &
|
||||
Offsets::ITEM_CLIENT_LOCK_BIT) != 0;
|
||||
const bool isReadable = ItemReadable(item, record);
|
||||
|
||||
// Static template fields (cache-dependent).
|
||||
bool haveIcon = false;
|
||||
char iconPath[260];
|
||||
bool haveQuality = false;
|
||||
uint32_t quality = 0;
|
||||
bool hasLoot = false;
|
||||
bool hasNoValue = false;
|
||||
if (record != nullptr) {
|
||||
const uint32_t displayInfoID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
haveIcon = BuildIconPath(displayInfoID, iconPath, sizeof(iconPath));
|
||||
quality = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
haveQuality = true;
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_FLAGS);
|
||||
hasLoot = (flags & Offsets::ITEM_FLAG_LOOTABLE) != 0;
|
||||
hasNoValue = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SELL_PRICE) == 0;
|
||||
}
|
||||
|
||||
const char *hyperlink = Item::Link::FromCGItem(item);
|
||||
|
||||
char nameBuf[256];
|
||||
const char *itemName = nullptr;
|
||||
if (Item::Link::NameFromCGItem(item, nameBuf, sizeof(nameBuf))) {
|
||||
itemName = nameBuf;
|
||||
} else if (record != nullptr) {
|
||||
const char *baseName = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
if (baseName != nullptr && baseName[0] != '\0')
|
||||
itemName = baseName;
|
||||
}
|
||||
|
||||
// --- Build the ContainerItemInfo table (only stack-safe SetField* now). ---
|
||||
Game::Lua::NewTable(L);
|
||||
if (haveIcon)
|
||||
Game::Lua::SetFieldString(L, "iconFileID", iconPath);
|
||||
Game::Lua::SetFieldNumber(L, "stackCount", static_cast<double>(stackCount));
|
||||
Game::Lua::SetFieldBool(L, "isLocked", isLocked);
|
||||
if (haveQuality)
|
||||
Game::Lua::SetFieldNumber(L, "quality", static_cast<double>(quality));
|
||||
Game::Lua::SetFieldBool(L, "isReadable", isReadable);
|
||||
Game::Lua::SetFieldBool(L, "hasLoot", hasLoot);
|
||||
if (hyperlink != nullptr && hyperlink[0] != '\0')
|
||||
Game::Lua::SetFieldString(L, "hyperlink", hyperlink);
|
||||
Game::Lua::SetFieldBool(L, "isFiltered", false);
|
||||
Game::Lua::SetFieldBool(L, "hasNoValue", hasNoValue);
|
||||
Game::Lua::SetFieldNumber(L, "itemID", static_cast<double>(itemID));
|
||||
Game::Lua::SetFieldBool(L, "isBound", isBound);
|
||||
if (itemName != nullptr)
|
||||
Game::Lua::SetFieldString(L, "itemName", itemName);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemInfo",
|
||||
&Script_C_Container_GetContainerItemInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::ItemInfo
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.MoveItem(srcBag, srcSlot, dstBag, dstSlot, count)` —
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.IsContainerItemOpenable(bag, slot)` — positional-arg
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.GetContainerItemRepairCost(containerIndex, slotIndex)`
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Container.SwapItems(srcBag, srcSlot, dstBag, dstSlot)` — atomic
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
// 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/>.
|
||||
//
|
||||
// Restores Lua 5.0's stripped `coroutine.*` library to the in-game Lua
|
||||
// state. WoW 1.12.1 ships with the C-level coroutine machinery linked
|
||||
// in (`lua_resume`, `lua_yield`, `lua_newthread`, threads as a real
|
||||
// type tag 8) but the `coroutine` global table was never registered.
|
||||
// The five trampolines below mirror Lua 5.0's `lbaselib.c` shapes,
|
||||
// cross-checked against 5.4.8's wired-up library via Ghidra (see the
|
||||
// "Cross-binary reference" technique in CLAUDE.md).
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Coroutine {
|
||||
|
||||
// Lua 5.0 result codes returned by `lua_resume`.
|
||||
static constexpr int LUA_OK = 0;
|
||||
static constexpr int LUA_YIELD = 1;
|
||||
|
||||
// Field offsets in `lua_State`, confirmed from `lua_resume`'s error
|
||||
// cleanup path (`L->ci = L->base_ci; L->base = base_ci->base`):
|
||||
// *(L+0x14) = *(L+0x28)
|
||||
// *(L+0x0C) = **(L+0x28)
|
||||
static constexpr uintptr_t OFF_L_CI = 0x14;
|
||||
static constexpr uintptr_t OFF_L_BASE_CI = 0x28;
|
||||
|
||||
// Reads `L->ci` (the current CallInfo). When greater than `L->base_ci`
|
||||
// the coroutine's body function is mid-execution (paused at a yield);
|
||||
// when equal, no Lua function is on this thread's call stack.
|
||||
static void *LuaCI(void *L) {
|
||||
return *reinterpret_cast<void **>(reinterpret_cast<uintptr_t>(L) + OFF_L_CI);
|
||||
}
|
||||
static void *LuaBaseCI(void *L) {
|
||||
return *reinterpret_cast<void **>(reinterpret_cast<uintptr_t>(L) + OFF_L_BASE_CI);
|
||||
}
|
||||
|
||||
// Coroutine status codes returned by `CoStatus` below — fed into the
|
||||
// `kStatusNames` table for the `status` API. The dense 0..3 encoding
|
||||
// also lets us label "unsuspended" states for resume's error path.
|
||||
static constexpr int CO_RUNNING = 0;
|
||||
static constexpr int CO_SUSPENDED = 1;
|
||||
static constexpr int CO_NORMAL = 2;
|
||||
static constexpr int CO_DEAD = 3;
|
||||
|
||||
static const char *const kStatusNames[] = {
|
||||
"running",
|
||||
"suspended",
|
||||
"normal",
|
||||
"dead",
|
||||
};
|
||||
|
||||
// Inner status classifier. Equivalent to Lua 5.0's `auxstatus` from
|
||||
// lbaselib.c, but we can't rely on `L->status` here: this build's
|
||||
// `lua_yield` doesn't write status (it sets a CI state flag bit
|
||||
// `0x10` instead), so the canonical "ttype(status) == LUA_YIELD"
|
||||
// check fails. We use the `ci > base_ci` signal instead — true iff
|
||||
// a Lua function is currently paused on this thread's call stack.
|
||||
//
|
||||
// State map:
|
||||
// co->ci > co->base_ci → SUSPENDED (yielded; body paused)
|
||||
// co->ci == co->base_ci, top==0 → DEAD (returned, all consumed)
|
||||
// co->ci == co->base_ci, top>0 → SUSPENDED (initial: fn pushed,
|
||||
// never resumed)
|
||||
// After AuxResume's XMove cycle, both "returned" and "yielded" leave
|
||||
// co's stack empty; the ci comparison is what distinguishes them.
|
||||
// "Normal" (coroutine that resumed a deeper coroutine — appears as
|
||||
// the parent in the call chain) is rare and would require walking
|
||||
// the global thread list; we collapse it into "suspended".
|
||||
static int CoStatus(void *L, void *co) {
|
||||
if (co == L)
|
||||
return CO_RUNNING;
|
||||
if (LuaCI(co) > LuaBaseCI(co))
|
||||
return CO_SUSPENDED;
|
||||
return Game::Lua::GetTop(co) == 0 ? CO_DEAD : CO_SUSPENDED;
|
||||
}
|
||||
|
||||
// Common back-end for resume / wrap. Returns nres ≥ 0 on success
|
||||
// (with values already xmove'd onto L), or -1 on error (with the
|
||||
// error message left on L's stack).
|
||||
//
|
||||
// WoW's modified Lua doesn't follow the standard `lua_resume` status
|
||||
// contract: it returns `0` for normal completion and `1` for both
|
||||
// "yielded" AND "errored mid-execution". We disambiguate the two
|
||||
// `s==1` cases by checking the call-stack depth — a yielded
|
||||
// coroutine still has its body paused (`ci > base_ci`), while an
|
||||
// errored coroutine has been unwound back to `base_ci`. Errors hit
|
||||
// from the "dead/already-resumed" early-out (e.g. the inner
|
||||
// `cannot resume dead coroutine` path) also land in `s != 0` with
|
||||
// `ci == base_ci`, so the same branch covers them.
|
||||
static int AuxResume(void *L, void *co, int nargs) {
|
||||
const int st = CoStatus(L, co);
|
||||
if (st != CO_SUSPENDED) {
|
||||
Game::Lua::PushString(L, st == CO_DEAD
|
||||
? "cannot resume dead coroutine"
|
||||
: "cannot resume non-suspended coroutine");
|
||||
return -1;
|
||||
}
|
||||
Game::Lua::XMove(L, co, nargs);
|
||||
const int s = Game::Lua::Resume(co, nargs);
|
||||
const bool yielded = LuaCI(co) > LuaBaseCI(co);
|
||||
if (s == LUA_OK || yielded) {
|
||||
const int nres = Game::Lua::GetTop(co);
|
||||
Game::Lua::XMove(co, L, nres);
|
||||
return nres;
|
||||
}
|
||||
// Error path: engine left a single error message on co's stack.
|
||||
Game::Lua::XMove(co, L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Create(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_FUNCTION || Game::Lua::IsCFunction(L, 1)) {
|
||||
Game::Lua::ArgError(L, 1, "Lua function expected");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::NewThread(L); // [arg1, NL]
|
||||
void *NL = Game::Lua::ToThread(L, -1);
|
||||
if (NL == nullptr) {
|
||||
Game::Lua::Error(L, "coroutine.create: failed to allocate thread");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushValue(L, 1); // [arg1, NL, fn]
|
||||
Game::Lua::XMove(L, NL, 1); // [arg1, NL]; NL has fn
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Yield(void *L) {
|
||||
return Game::Lua::Yield(L, Game::Lua::GetTop(L));
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Resume(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_THREAD) {
|
||||
Game::Lua::ArgError(L, 1, "coroutine expected");
|
||||
return 0; // unreachable
|
||||
}
|
||||
void *co = Game::Lua::ToThread(L, 1);
|
||||
const int nargs = Game::Lua::GetTop(L) - 1;
|
||||
const int r = AuxResume(L, co, nargs);
|
||||
if (r < 0) {
|
||||
// Error message is on top of L's stack. Push `false` and
|
||||
// shuffle it below the message: [..., msg] -> [..., false, msg].
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::Insert(L, -2);
|
||||
return 2;
|
||||
}
|
||||
// Success: `r` values on top of L's stack. Push `true` and slide
|
||||
// it into position so returns are [..., true, v1, v2, ..., vr].
|
||||
Game::Lua::PushBool(L, true);
|
||||
Game::Lua::Insert(L, -(r + 1));
|
||||
return r + 1;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Status(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_THREAD) {
|
||||
Game::Lua::ArgError(L, 1, "coroutine expected");
|
||||
return 0; // unreachable
|
||||
}
|
||||
void *co = Game::Lua::ToThread(L, 1);
|
||||
Game::Lua::PushString(L, kStatusNames[CoStatus(L, co)]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `coroutine.wrap`'s returned closure. The captured coroutine lives
|
||||
// in upvalue 1; each invocation forwards args via resume and either
|
||||
// returns the yielded/returned values directly (no { ok, ... }
|
||||
// wrapping like resume does) or re-raises on error.
|
||||
static int __fastcall WrapAux(void *L) {
|
||||
void *co = Game::Lua::ToThread(L, Game::Lua::UpvalueIndex(1));
|
||||
if (co == nullptr) {
|
||||
Game::Lua::Error(L, "coroutine.wrap: missing thread upvalue");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int nargs = Game::Lua::GetTop(L);
|
||||
const int r = AuxResume(L, co, nargs);
|
||||
if (r < 0) {
|
||||
// The error message is at -1. `lua_error`'s 1.12 wrapper takes
|
||||
// a printf-format string + va args; we pass `%s` so any `%`
|
||||
// characters in the message are treated as literal.
|
||||
const char *msg = Game::Lua::ToString(L, -1);
|
||||
Game::Lua::Error(L, "%s", msg != nullptr ? msg : "coroutine error");
|
||||
return 0; // unreachable
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Wrap(void *L) {
|
||||
// Reuse cocreate to build the thread (validates arg, allocates,
|
||||
// moves fn into the new state). Leaves the thread on top of L.
|
||||
Script_Coroutine_Create(L);
|
||||
Game::Lua::PushCClosure(L, &WrapAux, 1); // captures thread as upvalue 1
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("coroutine", "create", &Script_Coroutine_Create);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "resume", &Script_Coroutine_Resume);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "yield", &Script_Coroutine_Yield);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "status", &Script_Coroutine_Status);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "wrap", &Script_Coroutine_Wrap);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Coroutine
|
||||
@@ -0,0 +1,383 @@
|
||||
// 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/>.
|
||||
|
||||
// `C_CreatureInfo.GetCreatureInfoByID(creatureID)` -> table | nil
|
||||
//
|
||||
// Reads the client-side creature cache (creaturecache.wdb, fed by
|
||||
// SMSG_CREATURE_QUERY_RESPONSE) for a creature/NPC by ID and returns:
|
||||
//
|
||||
// {
|
||||
// creatureID = <id>,
|
||||
// name = "Misthoof Stag", -- primary name
|
||||
// subName = "", -- title/subtitle ("" if none)
|
||||
// type = 1, -- CreatureType (1=Beast, 7=Humanoid, …)
|
||||
// family = 0, -- CreatureFamily
|
||||
// rank = 0, -- 0=normal,1=elite,2=rareelite,3=worldboss,4=rare
|
||||
// displayID = 10957, -- model display ID
|
||||
// }
|
||||
//
|
||||
// `GetCreatureInfoByID` is a synchronous *peek*: returns the data only
|
||||
// if the creature is already cached (seen this session, or loaded from
|
||||
// creaturecache.wdb at login); nil otherwise. `RequestLoadCreatureByID`
|
||||
// fetches an uncached creature — it issues the query and fires
|
||||
// `CREATURE_DATA_LOAD_RESULT(creatureID, success)` when the response
|
||||
// lands, via the shared `Cache::QueryLoad` dispatcher.
|
||||
//
|
||||
// Field offsets verified against the binary (rank read by the engine's
|
||||
// classification helper as `[block+0x20]`) and real creaturecache.wdb
|
||||
// rows. See Offsets.h `OFF_CREATURE_*`.
|
||||
//
|
||||
// This file also hosts the two static-DBC lookups on the same table:
|
||||
//
|
||||
// C_CreatureInfo.GetRaceInfo(raceID) -> { raceName, clientFileString, raceID }
|
||||
// C_CreatureInfo.GetClassInfo(classID) -> { className, classFile, classID }
|
||||
//
|
||||
// retail backports over `ChrRaces.dbc` / `ChrClasses.dbc`. Unlike the
|
||||
// creature cache these DBCs are always loaded, localized, and synchronous —
|
||||
// so a by-id lookup works for any race/class id at any time, with no WDB
|
||||
// cold-cache problem. The file-string columns (`clientFileString` /
|
||||
// `classFile`) are the same non-localized tokens `UnitRace`/`UnitClass`
|
||||
// return as their 2nd value ("NightElf", "WARRIOR"), so they round-trip
|
||||
// with the unit APIs. See Offsets.h `OFF_CHRRACES_*` / `OFF_CHRCLASSES_*`.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cache/QueryLoad.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Creature::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
// Completion event for RequestLoadCreatureByID. Reserved at static-init
|
||||
// so the engine allocates it in its event table; fired (creatureID,
|
||||
// success) by the Cache::QueryLoad dispatcher.
|
||||
constexpr const char *kCreatureDataLoadResult = "CREATURE_DATA_LOAD_RESULT";
|
||||
const Event::Custom::AutoReserve _reserveLoadResult{kCreatureDataLoadResult};
|
||||
|
||||
// Generic cache `_GetRecord`. With callback=nullptr it only peeks the
|
||||
// hash table (no network query) and returns the data block (entry+0x18)
|
||||
// if the creature is loaded, else nullptr.
|
||||
using GetCacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, char dedup);
|
||||
|
||||
const uint8_t *PeekCreature(uint32_t creatureID) {
|
||||
auto fn = reinterpret_cast<GetCacheRecord_t>(Offsets::FUN_CREATURE_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, creatureID, &zeroGuid, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCreatureInfoByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int creatureID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (creatureID <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *rec = PeekCreature(static_cast<uint32_t>(creatureID));
|
||||
if (rec == nullptr)
|
||||
return 0; // not cached -> nil
|
||||
|
||||
const char *name = *reinterpret_cast<const char *const *>(rec + Offsets::OFF_CREATURE_NAME);
|
||||
const char *subName =
|
||||
*reinterpret_cast<const char *const *>(rec + Offsets::OFF_CREATURE_SUBNAME);
|
||||
const uint32_t type = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_TYPE);
|
||||
const uint32_t family = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_FAMILY);
|
||||
const uint32_t rank = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_RANK);
|
||||
const uint32_t displayID =
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_DISPLAYID);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "creatureID", creatureID);
|
||||
Game::Lua::SetFieldString(L, "name", name); // NULL coerced to ""
|
||||
Game::Lua::SetFieldString(L, "subName", subName);
|
||||
Game::Lua::SetFieldNumber(L, "type", type);
|
||||
Game::Lua::SetFieldNumber(L, "family", family);
|
||||
Game::Lua::SetFieldNumber(L, "rank", rank);
|
||||
Game::Lua::SetFieldNumber(L, "displayID", displayID);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.RequestLoadCreatureByID(creatureID)` -> bool. Async
|
||||
// fetch for an uncached creature: issues SMSG_CREATURE_QUERY and fires
|
||||
// `CREATURE_DATA_LOAD_RESULT(creatureID, success)` when the response
|
||||
// lands (or on timeout). Fires success synchronously if already cached.
|
||||
// Returns true if the request was accepted (false for bad input or a
|
||||
// full pending set) — mirrors C_Item.RequestLoadItemData(ByID).
|
||||
int __fastcall Script_RequestLoadCreatureByID(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int creatureID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (creatureID <= 0) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE);
|
||||
const bool accepted =
|
||||
Cache::QueryLoad::RequestLoad(cache, static_cast<uint32_t>(creatureID));
|
||||
Game::Lua::PushBool(L, accepted);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetRaceInfo(raceID)` — RaceInfo table, or nil for a
|
||||
// non-numeric / non-positive id or one with no ChrRaces row.
|
||||
int __fastcall Script_GetRaceInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int raceID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *file =
|
||||
(raceID > 0) ? DBC::StringField(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT,
|
||||
static_cast<uint32_t>(raceID),
|
||||
Offsets::OFF_CHRRACES_FILENAME)
|
||||
: nullptr;
|
||||
if (file == nullptr)
|
||||
return 0; // unknown id -> nil
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT,
|
||||
static_cast<uint32_t>(raceID),
|
||||
Offsets::OFF_CHRRACES_NAMES);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "raceName", name); // NULL coerced to ""
|
||||
Game::Lua::SetFieldString(L, "clientFileString", file);
|
||||
Game::Lua::SetFieldNumber(L, "raceID", raceID);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetClassInfo(classID)` — ClassInfo table, or nil for a
|
||||
// non-numeric / non-positive id or one with no ChrClasses row.
|
||||
int __fastcall Script_GetClassInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int classID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *file =
|
||||
(classID > 0) ? DBC::StringField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT,
|
||||
static_cast<uint32_t>(classID),
|
||||
Offsets::OFF_CHRCLASSES_FILENAME)
|
||||
: nullptr;
|
||||
if (file == nullptr)
|
||||
return 0; // unknown id -> nil
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CHRCLASSES_RECORDS,
|
||||
Offsets::VAR_CHRCLASSES_COUNT,
|
||||
static_cast<uint32_t>(classID),
|
||||
Offsets::OFF_CHRCLASSES_NAMES);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "className", name); // NULL coerced to ""
|
||||
Game::Lua::SetFieldString(L, "classFile", file);
|
||||
Game::Lua::SetFieldNumber(L, "classID", classID);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetCreatureFamilyInfo(creatureFamilyID)` — CreatureFamilyInfo
|
||||
// table for a `CreatureFamily.dbc` row (1 = Wolf, 27 = Wind Serpent, …), or
|
||||
// nil for a non-numeric / non-positive / unused id. `iconFile` is a texture
|
||||
// path string here (retail exposes a numeric fileID; vanilla's DBC stores the
|
||||
// path) — empty "" for families with no icon (e.g. warlock pets).
|
||||
int __fastcall Script_GetCreatureFamilyInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int familyID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *name =
|
||||
(familyID > 0) ? DBC::LocalizedField(Offsets::VAR_CREATUREFAMILY_RECORDS,
|
||||
Offsets::VAR_CREATUREFAMILY_COUNT,
|
||||
static_cast<uint32_t>(familyID),
|
||||
Offsets::OFF_CREATUREFAMILY_NAMES)
|
||||
: nullptr;
|
||||
if (name == nullptr)
|
||||
return 0; // unused / OOR id -> nil
|
||||
const char *icon = DBC::StringField(Offsets::VAR_CREATUREFAMILY_RECORDS,
|
||||
Offsets::VAR_CREATUREFAMILY_COUNT,
|
||||
static_cast<uint32_t>(familyID),
|
||||
Offsets::OFF_CREATUREFAMILY_ICON);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "id", familyID);
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
Game::Lua::SetFieldString(L, "iconFile", icon); // NULL/empty coerced to ""
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetCreatureFamilyIDs()` — array of every populated
|
||||
// `CreatureFamily.dbc` id (sparse: skips the unused rows). Round-trips with
|
||||
// GetCreatureFamilyInfo for each element.
|
||||
int __fastcall Script_GetCreatureFamilyIDs(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_CREATUREFAMILY_COUNT);
|
||||
int n = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CREATUREFAMILY_RECORDS,
|
||||
Offsets::VAR_CREATUREFAMILY_COUNT,
|
||||
static_cast<uint32_t>(id),
|
||||
Offsets::OFF_CREATUREFAMILY_NAMES);
|
||||
if (name == nullptr)
|
||||
continue; // unused row
|
||||
++n;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(id));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetFactionInfo(raceID)` — faction-group info for a race:
|
||||
// { name = localized ("Alliance"/"Horde"/…), groupTag = english }. Mirrors
|
||||
// the player branch of `Script_UnitFactionGroup` (`0x00516630`): resolve the
|
||||
// race's FactionTemplate group mask, then scan `FactionGroup.dbc` for the row
|
||||
// whose bit the mask sets and whose localized name is non-empty (which is what
|
||||
// skips the always-set "Player" / the nameless "Monster" rows and lands on
|
||||
// Alliance / Horde). nil for a non-numeric / non-positive / unknown race or a
|
||||
// race with no named group. See Offsets.h for the full chain.
|
||||
int __fastcall Script_GetFactionInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int raceID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (raceID <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *crRec = DBC::Record(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT,
|
||||
static_cast<uint32_t>(raceID));
|
||||
if (crRec == nullptr)
|
||||
return 0; // unknown race
|
||||
const int ftId = *reinterpret_cast<const int *>(
|
||||
crRec + Offsets::OFF_CHRRACES_FACTION_TEMPLATE);
|
||||
if (ftId <= 0)
|
||||
return 0;
|
||||
|
||||
const uint8_t *ftRec = DBC::Record(Offsets::VAR_FACTIONTEMPLATE_RECORDS,
|
||||
Offsets::VAR_FACTIONTEMPLATE_COUNT,
|
||||
static_cast<uint32_t>(ftId));
|
||||
if (ftRec == nullptr)
|
||||
return 0;
|
||||
const uint32_t mask = *reinterpret_cast<const uint32_t *>(
|
||||
ftRec + Offsets::OFF_FACTIONTEMPLATE_GROUP_MASK);
|
||||
|
||||
// FactionGroup is stored contiguously (records-base + i*stride), not as a
|
||||
// pointer array — iterate by hand rather than via DBC::Record.
|
||||
auto *fgBase = *reinterpret_cast<const uint8_t *const *>(
|
||||
Offsets::VAR_FACTIONGROUP_RECORDS);
|
||||
const int fgCount =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_FACTIONGROUP_COUNT);
|
||||
const int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
if (fgBase == nullptr)
|
||||
return 0;
|
||||
|
||||
for (int i = 0; i < fgCount; ++i) {
|
||||
const uint8_t *row = fgBase + i * Offsets::FACTIONGROUP_STRIDE;
|
||||
const int bit = *reinterpret_cast<const int *>(
|
||||
row + Offsets::OFF_FACTIONGROUP_BIT);
|
||||
if ((mask & (1u << (bit & 0x1F))) == 0)
|
||||
continue;
|
||||
const char *loc = *reinterpret_cast<const char *const *>(
|
||||
row + Offsets::OFF_FACTIONGROUP_NAMES + locale * 4);
|
||||
if (loc == nullptr || *loc == '\0')
|
||||
continue; // "Player" / "Monster" have empty localized names
|
||||
const char *eng = *reinterpret_cast<const char *const *>(
|
||||
row + Offsets::OFF_FACTIONGROUP_ENGLISH);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "name", loc);
|
||||
Game::Lua::SetFieldString(L, "groupTag", eng);
|
||||
return 1;
|
||||
}
|
||||
return 0; // no named group -> nil
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetCreatureTypeInfo(creatureTypeID)` — CreatureTypeInfo
|
||||
// table for a `CreatureType.dbc` row (1 = Beast … 11 = Totem), or nil for a
|
||||
// non-numeric / non-positive / unused id.
|
||||
int __fastcall Script_GetCreatureTypeInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0; // nil
|
||||
const int typeID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const char *name =
|
||||
(typeID > 0) ? DBC::LocalizedField(Offsets::VAR_CREATURETYPE_RECORDS,
|
||||
Offsets::VAR_CREATURETYPE_COUNT,
|
||||
static_cast<uint32_t>(typeID),
|
||||
Offsets::OFF_CREATURETYPE_NAMES)
|
||||
: nullptr;
|
||||
if (name == nullptr)
|
||||
return 0; // unused / OOR id -> nil
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "id", typeID);
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetCreatureTypeIDs()` — array of every populated
|
||||
// `CreatureType.dbc` id (contiguous 1..N in vanilla). Round-trips with
|
||||
// GetCreatureTypeInfo for each element.
|
||||
int __fastcall Script_GetCreatureTypeIDs(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_CREATURETYPE_COUNT);
|
||||
int n = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const char *name = DBC::LocalizedField(Offsets::VAR_CREATURETYPE_RECORDS,
|
||||
Offsets::VAR_CREATURETYPE_COUNT,
|
||||
static_cast<uint32_t>(id),
|
||||
Offsets::OFF_CREATURETYPE_NAMES);
|
||||
if (name == nullptr)
|
||||
continue;
|
||||
++n;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(id));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureInfoByID",
|
||||
&Script_GetCreatureInfoByID);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "RequestLoadCreatureByID",
|
||||
&Script_RequestLoadCreatureByID);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetRaceInfo",
|
||||
&Script_GetRaceInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetClassInfo",
|
||||
&Script_GetClassInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureFamilyInfo",
|
||||
&Script_GetCreatureFamilyInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureFamilyIDs",
|
||||
&Script_GetCreatureFamilyIDs);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetFactionInfo",
|
||||
&Script_GetFactionInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureTypeInfo",
|
||||
&Script_GetCreatureTypeInfo);
|
||||
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureTypeIDs",
|
||||
&Script_GetCreatureTypeIDs);
|
||||
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
|
||||
kCreatureDataLoadResult, Offsets::FUN_CREATURE_GET_RECORD);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Creature::Info
|
||||
+10
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetCursorInfo()` — backport of the modern WoW global. Surfaces
|
||||
@@ -45,6 +45,8 @@
|
||||
// `CursorHasItem` / engine `PickupX` calls when they need to detect
|
||||
// that.
|
||||
|
||||
#include "cursor/Info.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/ID.h"
|
||||
@@ -273,4 +275,9 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool HasItem() {
|
||||
return ReadVar(Offsets::VAR_CURSOR_ITEM_GUID_LO) != 0 ||
|
||||
ReadVar(Offsets::VAR_CURSOR_ITEM_GUID_HI) != 0;
|
||||
}
|
||||
|
||||
} // namespace Cursor::Info
|
||||
|
||||
@@ -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 Cursor::Info {
|
||||
|
||||
// True if a GUID-bearing item is currently on the cursor —
|
||||
// `VAR_CURSOR_ITEM_GUID != 0`, the same globals `GetCursorInfo`'s bag-item
|
||||
// path reads and the exact signal the engine's item-pickup path guards on.
|
||||
// Used to gate cursor pickups (don't clobber a held item).
|
||||
bool HasItem();
|
||||
|
||||
} // namespace Cursor::Info
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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 "cvar/Factory.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace CVar::Factory {
|
||||
|
||||
namespace {
|
||||
|
||||
// `FUN_REGISTER_CVAR` — verified ABI (see Offsets.h and the calibration
|
||||
// against the engine's own call site at 0x005E2490 / the Lua
|
||||
// Script_RegisterCVar path). MSVC `__fastcall` places the first two args in
|
||||
// ECX/EDX and pushes the rest right-to-left, which reproduces the engine's
|
||||
// exact push order: flags, defaultValue, callback, categoryId, hiddenBool,
|
||||
// userData (with flags as the first stack arg / last push).
|
||||
using RegisterCVar_t = void *(__fastcall *)(const char *name, int edxZero,
|
||||
uint32_t flags,
|
||||
const char *defaultValue,
|
||||
ChangeCallback callback,
|
||||
int categoryId, int hiddenBool,
|
||||
void *userData);
|
||||
|
||||
using FindCVar_t = void *(__fastcall *)(const char *name);
|
||||
|
||||
// `FUN_SET_CVAR_VALUE` — `__thiscall(cvar /*ecx*/, value, a3, a4, a5, a6)`,
|
||||
// i.e. ECX=cvar with `value` AND a3..a6 all passed on the stack (5 stack
|
||||
// args; the function cleans 20 bytes / `RET 0x14`). NOT __fastcall — that
|
||||
// would route `value` through EDX, push only 4 args, and leave the callee
|
||||
// popping 4 bytes too many → ESP corruption → silent crash. Mirror the
|
||||
// Script_SetCVar invocation (0x00488CA8): a3..a6 = 1, 0, 0, 1.
|
||||
using SetCVarValue_t = char(__thiscall *)(void *cvar, const char *value,
|
||||
int a3, int a4, int a5, int a6);
|
||||
|
||||
const char *ReadValue(void *cvar) {
|
||||
if (cvar == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const char *const *>(
|
||||
static_cast<const uint8_t *>(cvar) + Offsets::OFF_CVAR_VALUE_STR);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Handle Register(const char *name, const char *defaultValue, uint32_t flags,
|
||||
ChangeCallback callback, void *userData) {
|
||||
if (name == nullptr)
|
||||
return nullptr;
|
||||
auto reg = reinterpret_cast<RegisterCVar_t>(Offsets::FUN_REGISTER_CVAR);
|
||||
return reg(name, /*edxZero*/ 0, flags, defaultValue, callback,
|
||||
Offsets::CVAR_SCRIPT_CATEGORY, /*hiddenBool*/ 0, userData);
|
||||
}
|
||||
|
||||
Handle Find(const char *name) {
|
||||
if (name == nullptr)
|
||||
return nullptr;
|
||||
auto find = reinterpret_cast<FindCVar_t>(Offsets::FUN_FIND_CVAR);
|
||||
return find(name);
|
||||
}
|
||||
|
||||
const char *GetString(Handle cvar) {
|
||||
return ReadValue(cvar);
|
||||
}
|
||||
|
||||
int GetInt(Handle cvar, int fallback) {
|
||||
const char *value = ReadValue(cvar);
|
||||
if (value == nullptr || value[0] == '\0')
|
||||
return fallback;
|
||||
return std::atoi(value);
|
||||
}
|
||||
|
||||
void SetString(Handle cvar, const char *value) {
|
||||
if (cvar == nullptr || value == nullptr)
|
||||
return;
|
||||
auto set = reinterpret_cast<SetCVarValue_t>(Offsets::FUN_SET_CVAR_VALUE);
|
||||
set(cvar, value, 1, 0, 0, 1);
|
||||
}
|
||||
|
||||
void SetInt(Handle cvar, int value) {
|
||||
char buf[16];
|
||||
// Hand-format to avoid pulling in a locale-aware path; value range for
|
||||
// our cvars is small, 16 bytes is ample for a 32-bit int + sign + NUL.
|
||||
std::snprintf(buf, sizeof buf, "%d", value);
|
||||
SetString(cvar, buf);
|
||||
}
|
||||
|
||||
} // namespace CVar::Factory
|
||||
@@ -0,0 +1,80 @@
|
||||
// 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>
|
||||
|
||||
// CVar::Factory — create and drive engine CVars from C++.
|
||||
//
|
||||
// Vanilla 1.12 only exposes CVar *reading* and *registration without a
|
||||
// callback* through Lua (`RegisterCVar(name, default)`); there's no way for
|
||||
// an addon to register a cvar with a change-callback or to react to value
|
||||
// changes. This wraps the engine's internal registrar (`FUN_REGISTER_CVAR`)
|
||||
// and value-setter (`FUN_SET_CVAR_VALUE`) so other modules can publish
|
||||
// persisted, validated settings (clamped via the change callback and saved
|
||||
// to Config.wtf).
|
||||
//
|
||||
// CVars registered here persist to `WTF\Config.wtf` (the engine forces the
|
||||
// archive flag bit on at registration) and are visible to the in-game Lua
|
||||
// `GetCVar`/`SetCVar` globals, since cvar storage is process-global.
|
||||
|
||||
namespace CVar::Factory {
|
||||
|
||||
// Opaque handle to an engine CVar. Stable for the cvar's lifetime (vanilla
|
||||
// never reallocates a registered cvar).
|
||||
using Handle = void *;
|
||||
|
||||
// Change callback. Fires whenever the cvar's value changes (and once at
|
||||
// registration). Mirrors the engine ABI verified at FUN_SET_CVAR_VALUE:
|
||||
// - `cvar` — this cvar's Handle
|
||||
// - `prevValue` — the value before the change (may be null/empty)
|
||||
// - `newValue` — the value being applied
|
||||
// - `userData` — whatever was passed to Register
|
||||
// Return a nonzero value to ACCEPT the change, 0 to REJECT it (the engine
|
||||
// keeps the previous value). To clamp rather than reject, set the clamped
|
||||
// value with SetString from inside the callback and return 0 for the
|
||||
// out-of-range input. Must tolerate being invoked with partial context.
|
||||
using ChangeCallback = int(__fastcall *)(Handle cvar, const char *prevValue,
|
||||
const char *newValue, void *userData);
|
||||
|
||||
// Register a cvar (or fetch the existing one — the engine dedups by name,
|
||||
// and re-registering OR-merges `flags` and updates the callback). `name`
|
||||
// and `defaultValue` must outlive the process (the engine stores `name` by
|
||||
// pointer; pass string literals). `defaultValue` may be null for "". The
|
||||
// returned Handle is never null on success; null only if the engine
|
||||
// registrar itself failed.
|
||||
Handle Register(const char *name, const char *defaultValue,
|
||||
uint32_t flags = 0, ChangeCallback callback = nullptr,
|
||||
void *userData = nullptr);
|
||||
|
||||
// Look up an already-registered cvar by name, or null if none.
|
||||
Handle Find(const char *name);
|
||||
|
||||
// Read the cvar's current value string (the raw engine pointer at
|
||||
// `+OFF_CVAR_VALUE_STR`). Returns null if `cvar` is null. The pointer is
|
||||
// valid for the cvar's lifetime.
|
||||
const char *GetString(Handle cvar);
|
||||
|
||||
// Read the value parsed as a base-10 integer, or `fallback` if `cvar` is
|
||||
// null or the value isn't numeric.
|
||||
int GetInt(Handle cvar, int fallback = 0);
|
||||
|
||||
// Set the cvar's value (fires its change callback). No-op if `cvar` is
|
||||
// null. `value` is copied by the engine into the cvar's inline buffer.
|
||||
void SetString(Handle cvar, const char *value);
|
||||
|
||||
// Convenience: set from an integer.
|
||||
void SetInt(Handle cvar, int value);
|
||||
|
||||
} // namespace CVar::Factory
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Glue-side CVar bindings. Vanilla 1.12 registers `GetCVar`,
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Lookup.h"
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// 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
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// 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
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Temporary probe helpers used to derive byte offsets in the player /
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_EncodingUtil.EncodeBase64` / `DecodeBase64`, with `Base64Variant`
|
||||
|
||||
+32
-29
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
@@ -30,6 +30,16 @@ constexpr int kMaxDepth = 64;
|
||||
|
||||
// ---------------- encode ----------------
|
||||
|
||||
// tinycbor's grow-and-retry contract: on buffer overflow the encoder
|
||||
// switches to counting mode (end = NULL, accumulates bytes_needed) and
|
||||
// encoding must CONTINUE so the final tally is complete — a nested
|
||||
// container's tally only reaches the parent via close_container. So
|
||||
// CborErrorOutOfMemory is not a failure here; the caller reads
|
||||
// cbor_encoder_get_extra_bytes_needed() afterwards, resizes, retries.
|
||||
bool EncOk(CborError err) {
|
||||
return err == CborNoError || err == CborErrorOutOfMemory;
|
||||
}
|
||||
|
||||
bool EncodeValue(void *L, int idx, CborEncoder *enc, int depth);
|
||||
|
||||
bool IsLuaArray(void *L, int idx, int &outLen) {
|
||||
@@ -73,8 +83,8 @@ bool EncodeTable(void *L, int idx, CborEncoder *enc, int depth) {
|
||||
int arrayLen = 0;
|
||||
if (IsLuaArray(L, idx, arrayLen)) {
|
||||
CborEncoder arr;
|
||||
if (cbor_encoder_create_array(enc, &arr,
|
||||
static_cast<size_t>(arrayLen)) != CborNoError)
|
||||
if (!EncOk(cbor_encoder_create_array(enc, &arr,
|
||||
static_cast<size_t>(arrayLen))))
|
||||
return false;
|
||||
for (int i = 1; i <= arrayLen; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i));
|
||||
@@ -85,12 +95,12 @@ bool EncodeTable(void *L, int idx, CborEncoder *enc, int depth) {
|
||||
}
|
||||
Game::Lua::SetTop(L, -2);
|
||||
}
|
||||
return cbor_encoder_close_container(enc, &arr) == CborNoError;
|
||||
return EncOk(cbor_encoder_close_container(enc, &arr));
|
||||
}
|
||||
|
||||
const int pairs = CountTablePairs(L, idx);
|
||||
CborEncoder map;
|
||||
if (cbor_encoder_create_map(enc, &map, static_cast<size_t>(pairs)) != CborNoError)
|
||||
if (!EncOk(cbor_encoder_create_map(enc, &map, static_cast<size_t>(pairs))))
|
||||
return false;
|
||||
Game::Lua::PushNil(L);
|
||||
while (Game::Lua::Next(L, idx) != 0) {
|
||||
@@ -101,16 +111,16 @@ bool EncodeTable(void *L, int idx, CborEncoder *enc, int depth) {
|
||||
}
|
||||
Game::Lua::SetTop(L, -2);
|
||||
}
|
||||
return cbor_encoder_close_container(enc, &map) == CborNoError;
|
||||
return EncOk(cbor_encoder_close_container(enc, &map));
|
||||
}
|
||||
|
||||
bool EncodeValue(void *L, int idx, CborEncoder *enc, int depth) {
|
||||
switch (Game::Lua::Type(L, idx)) {
|
||||
case Game::Lua::TYPE_NIL:
|
||||
return cbor_encode_null(enc) == CborNoError;
|
||||
return EncOk(cbor_encode_null(enc));
|
||||
case Game::Lua::TYPE_BOOLEAN:
|
||||
return cbor_encode_boolean(enc,
|
||||
Game::Lua::ToBoolean(L, idx) != 0) == CborNoError;
|
||||
return EncOk(cbor_encode_boolean(enc,
|
||||
Game::Lua::ToBoolean(L, idx) != 0));
|
||||
case Game::Lua::TYPE_NUMBER: {
|
||||
const double n = Game::Lua::ToNumber(L, idx);
|
||||
// Whole numbers in [-2^53, 2^53] go out as integers so
|
||||
@@ -118,17 +128,17 @@ bool EncodeValue(void *L, int idx, CborEncoder *enc, int depth) {
|
||||
if (std::isfinite(n) && n == std::floor(n) &&
|
||||
n >= -9007199254740992.0 && n <= 9007199254740992.0) {
|
||||
if (n >= 0.0)
|
||||
return cbor_encode_uint(enc,
|
||||
static_cast<uint64_t>(n)) == CborNoError;
|
||||
return cbor_encode_negative_int(enc,
|
||||
static_cast<uint64_t>(-n)) == CborNoError;
|
||||
return EncOk(cbor_encode_uint(enc,
|
||||
static_cast<uint64_t>(n)));
|
||||
return EncOk(cbor_encode_negative_int(enc,
|
||||
static_cast<uint64_t>(-n)));
|
||||
}
|
||||
return cbor_encode_double(enc, n) == CborNoError;
|
||||
return EncOk(cbor_encode_double(enc, n));
|
||||
}
|
||||
case Game::Lua::TYPE_STRING: {
|
||||
const char *s = Game::Lua::ToString(L, idx);
|
||||
const unsigned int n = Game::Lua::StrLen(L, idx);
|
||||
return cbor_encode_text_string(enc, s, n) == CborNoError;
|
||||
return EncOk(cbor_encode_text_string(enc, s, n));
|
||||
}
|
||||
case Game::Lua::TYPE_TABLE:
|
||||
return EncodeTable(L, idx, enc, depth);
|
||||
@@ -143,22 +153,15 @@ int __fastcall Script_SerializeCBOR(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// tinycbor encodes into a fixed-size buffer; if it overflows, it
|
||||
// tracks the extra bytes needed so we can resize and retry.
|
||||
// tinycbor encodes into a fixed-size buffer; on overflow it keeps
|
||||
// encoding in counting mode (EncOk treats OutOfMemory as success) so
|
||||
// the extra-bytes tally is complete, then we resize and retry once.
|
||||
std::vector<uint8_t> buf(256);
|
||||
for (;;) {
|
||||
CborEncoder enc;
|
||||
cbor_encoder_init(&enc, buf.data(), buf.size(), 0);
|
||||
if (!EncodeValue(L, 1, &enc, 0)) {
|
||||
// If it was a buffer-size issue we'll catch it via the
|
||||
// extra-bytes-needed check below; otherwise this is a
|
||||
// genuine "value not representable" failure.
|
||||
const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
|
||||
if (extra == 0)
|
||||
return 0;
|
||||
buf.resize(buf.size() + extra);
|
||||
continue;
|
||||
}
|
||||
if (!EncodeValue(L, 1, &enc, 0))
|
||||
return 0; // genuine "value not representable" failure
|
||||
const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc);
|
||||
if (extra > 0) {
|
||||
buf.resize(buf.size() + extra);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
|
||||
// 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 Lesser General Public License for more details.
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_EncodingUtil.CompressString` / `DecompressString`, full
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user