mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-15 15:40:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99de63755b | ||
|
|
7cd891e86c | ||
|
|
6e50577565 | ||
|
|
024c6d2021 | ||
|
|
4b47059788 | ||
|
|
1aa5a07823 | ||
|
|
00f32e0e2f | ||
|
|
5537432662 | ||
|
|
5962ccc83d | ||
|
|
f5acea6060 | ||
|
|
a86f1219c3 | ||
|
|
338c6777d6 | ||
|
|
aeb3bdf07b | ||
|
|
1caf9fde18 | ||
|
|
d496ff2aa7 | ||
|
|
8b96fccfe9 | ||
|
|
9d3bdfb6ad | ||
|
|
42d037f080 | ||
|
|
af4a379311 | ||
|
|
2b9d0e7127 | ||
|
|
a68fb0a6be | ||
|
|
721f32ac77 | ||
|
|
fcdc24471c | ||
|
|
58f5b4f78e | ||
|
|
6be4aac9fb | ||
|
|
a6270469e1 | ||
|
|
a8f73623c4 | ||
|
|
47b258ad95 | ||
|
|
ac6bd552d5 | ||
|
|
2a05baee8d | ||
|
|
80ef0e2988 | ||
|
|
5df5ce00cf | ||
|
|
1297b9522a | ||
|
|
b3188ced2a | ||
|
|
f55893fdf4 | ||
|
|
e0b2e56070 | ||
|
|
20dc9fc962 | ||
|
|
51d226f67d | ||
|
|
eb3c32094c | ||
|
|
26ca1b4e84 | ||
|
|
7331922070 | ||
|
|
27d8f90f03 | ||
|
|
35ef1228eb | ||
|
|
76cc907bd0 | ||
|
|
e9e431e014 | ||
|
|
fbcc1c9ae1 | ||
|
|
142faafb13 | ||
|
|
c243489916 | ||
|
|
917b15640e | ||
|
|
7ccbbaaf68 | ||
|
|
71805db62f | ||
|
|
85619155f8 | ||
|
|
46dc198505 | ||
|
|
7707127d5f | ||
|
|
fa7d71435f | ||
|
|
3467513df9 | ||
|
|
6000df8009 | ||
|
|
fde3beca9d | ||
|
|
452f3c14fc | ||
|
|
6bb12c3c12 | ||
|
|
7a08199ba5 | ||
|
|
a5bb3823fb | ||
|
|
8c941c757d | ||
|
|
2fff9564b2 | ||
|
|
bbefdb847d | ||
|
|
56d2dbbc27 | ||
|
|
0296e0e5a4 | ||
|
|
6812771a89 | ||
|
|
4d835aa674 | ||
|
|
dfbba225d2 | ||
|
|
930b0c6492 | ||
|
|
428c3308c6 | ||
|
|
a33146241e | ||
|
|
c7af8d396e | ||
|
|
a2a2ccb804 | ||
|
|
7a4ab29cb0 | ||
|
|
ef8ea710d4 | ||
|
|
a2d7d4e9e6 | ||
|
|
449841b4eb | ||
|
|
89c705b922 | ||
|
|
bf79ee1d93 | ||
|
|
4f99a1ed95 | ||
|
|
298eff528c | ||
|
|
b8b9da2adb | ||
|
|
40abcabe9d | ||
|
|
108a91a843 | ||
|
|
b4e5f4f2bb | ||
|
|
390eec4749 | ||
|
|
f86893c8ef | ||
|
|
d4ac5dc210 | ||
|
|
b0585f5628 | ||
|
|
6013d8b937 | ||
|
|
27277b11aa | ||
|
|
ccfdb8309c | ||
|
|
be7a8a0dd7 | ||
|
|
f9167bac06 | ||
|
|
7d82ae8e92 | ||
|
|
ed7e7628c7 | ||
|
|
a562573559 | ||
|
|
5aa382bbb1 | ||
|
|
d98d3a9fd5 | ||
|
|
3d91ae0f91 | ||
|
|
fedccf5b95 | ||
|
|
0ba73b8457 | ||
|
|
d9565fbe1f | ||
|
|
61db68dd7f | ||
|
|
f895d38f76 | ||
|
|
804ed61c76 | ||
|
|
aacace1645 | ||
|
|
4bf2c17e0f | ||
|
|
3191c4dcfc | ||
|
|
373dce8202 | ||
|
|
4c7f1c36d6 | ||
|
|
36c367a6d6 | ||
|
|
fe9e7dddcd | ||
|
|
9d8f1b1b5d | ||
|
|
9d1e14410c | ||
|
|
8b06caf708 | ||
|
|
4684088675 | ||
|
|
9a26207df2 | ||
|
|
2adfaff020 | ||
|
|
8a7ab490f4 | ||
|
|
e26f8958e9 | ||
|
|
f0a83f1f31 | ||
|
|
5070c892e2 | ||
|
|
8295ec9442 | ||
|
|
c5b1431073 | ||
|
|
b39c16578b | ||
|
|
7d7e392924 | ||
|
|
14fa610bc8 | ||
|
|
8ca415e9a0 | ||
|
|
becf2f4866 | ||
|
|
28658c2995 | ||
|
|
f5cce25d23 | ||
|
|
1306eaf1b8 | ||
|
|
807afc083f | ||
|
|
c26afcc050 | ||
|
|
a124629669 | ||
|
|
be03d45f0c | ||
|
|
66c878d03e | ||
|
|
3ad87eb2f3 | ||
|
|
a9d2ad73f3 | ||
|
|
db88fca050 | ||
|
|
26af9bd30f | ||
|
|
e7156caa47 | ||
|
|
02ad0ea6ab | ||
|
|
1dee8e5683 | ||
|
|
583f4bb011 | ||
|
|
23ebfbe7ab | ||
|
|
18927551d5 | ||
|
|
2c2f280c51 | ||
|
|
991baa2610 | ||
|
|
e68b690cf1 | ||
|
|
e5690b1c6a | ||
|
|
e91b1457fd | ||
|
|
09c5832ae8 | ||
|
|
4015fee5e2 | ||
|
|
744bfb31d1 | ||
|
|
8706d1d99b | ||
|
|
8637bf05e5 | ||
|
|
802039fd08 | ||
|
|
d14f0b44c1 | ||
|
|
008282d65a | ||
|
|
9a8c314d69 | ||
|
|
4b1bf16ba5 | ||
|
|
5528fb5fa4 | ||
|
|
7ab32df2aa | ||
|
|
8e1429bfac | ||
|
|
9322382596 | ||
|
|
c3cf9e6f3a | ||
|
|
f5d6046523 | ||
|
|
e1ebf6ecce | ||
|
|
698827e9c5 | ||
|
|
60530e32fc | ||
|
|
3cffb71b20 | ||
|
|
7887237a6e | ||
|
|
98af79137b | ||
|
|
622882725f | ||
|
|
0517bb3753 | ||
|
|
fb17488237 | ||
|
|
b1ee8cc183 | ||
|
|
c08ded6db4 | ||
|
|
640f78b2eb | ||
|
|
a51ec40e52 | ||
|
|
2ce5b007af | ||
|
|
be92d721f0 |
@@ -53,6 +53,9 @@ jobs:
|
||||
build:
|
||||
needs: pick-runner
|
||||
runs-on: ${{ needs.pick-runner.outputs.label }}
|
||||
outputs:
|
||||
tag: ${{ steps.ver.outputs.tag }}
|
||||
is_release: ${{ steps.ver.outputs.is_release }}
|
||||
# Windows PowerShell 5.1 is always present; the runner's default `pwsh`
|
||||
# (PowerShell Core) is not installed by default, and `shell: bash` on a
|
||||
# self-hosted box resolves to the WSL launcher in System32.
|
||||
@@ -97,12 +100,28 @@ jobs:
|
||||
- name: Build
|
||||
run: cmake --build build --config Release
|
||||
|
||||
# Dev builds carry their own symbols. With the PDB sitting next to the DLL
|
||||
# the client's crash handler resolves our frames to file and line — a
|
||||
# tester's log reads `Texture::Mask::PrimIndexed_h+12 (Mask.cpp,246)`
|
||||
# instead of a bare address — and the map resolves `0001:offset` addresses
|
||||
# by hand when there is no dump. Both match THIS build and nothing else,
|
||||
# which is why they travel with it rather than being rebuilt later from the
|
||||
# same commit. Unlike the release bundle these need no off-putting name:
|
||||
# the artifact is behind the Actions UI, so whoever downloads it is already
|
||||
# the audience for symbols.
|
||||
- name: Upload artifact
|
||||
if: steps.ver.outputs.is_release != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ClassicAPI-${{ github.sha }}
|
||||
path: build/Release/ClassicAPI.dll
|
||||
# Shared parent is build/Release, so all three land at the zip root.
|
||||
path: |
|
||||
build/Release/ClassicAPI.dll
|
||||
build/Release/ClassicAPI.pdb
|
||||
build/Release/ClassicAPI.map
|
||||
# A symbol-less artifact looks identical to a good one until someone
|
||||
# needs a stack out of it, so fail the build instead of warning.
|
||||
if-no-files-found: error
|
||||
|
||||
# Bundle both debug artifacts into one zip so a crash from a shipped DLL
|
||||
# can be triaged exactly. The PDB gives WinDbg/x64dbg/VS a line-level
|
||||
|
||||
@@ -43,10 +43,23 @@ Util\Rectangle.lua
|
||||
Util\Vector2D.lua
|
||||
Util\Vector3D.lua
|
||||
Util\Vector4D.lua
|
||||
Util\UiMapPoint.lua
|
||||
Util\UIParent.lua
|
||||
Util\ActionButton.lua
|
||||
Util\WorldMap.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
Util\AuraUtil.lua
|
||||
Util\SecureUnitMenu.lua
|
||||
Util\TextureUtil.lua
|
||||
Util\MacroOptions.lua
|
||||
Util\SecureStateDriver.lua
|
||||
Util\MacroIconSelection.lua
|
||||
|
||||
APIDocumentation\BaseAPIMixin.lua
|
||||
APIDocumentation\FieldsAPIMixin.lua
|
||||
APIDocumentation\FunctionsAPIMixin.lua
|
||||
APIDocumentation\SystemsAPIMixin.lua
|
||||
APIDocumentation\TablesAPIMixin.lua
|
||||
APIDocumentation\EventsAPIMixin.lua
|
||||
APIDocumentation\APIDocumentation.lua
|
||||
APIDocumentation\APIDocumentation_Bootstrap.lua
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
-- Backport of Blizzard_APIDocumentation/Blizzard_APIDocumentation.lua to
|
||||
-- vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the retail source:
|
||||
-- - Lua 5.0 has no length operator (#); sequence length comes from
|
||||
-- table.getn(t), matching the rest of this addon.
|
||||
-- - Lua 5.0 has no `...` inside a function body; WriteLineF takes its
|
||||
-- extra values from the implicit `arg` table.
|
||||
-- - ChatFrameUtil does not exist. HandleOpenDump uses this client's
|
||||
-- ChatFrame_OpenChat, which applies the text from ChatEdit_OnUpdate
|
||||
-- rather than immediately -- so the cursor is placed one frame later,
|
||||
-- after that text has landed.
|
||||
|
||||
local tGetN = table.getn;
|
||||
|
||||
APIDocumentationMixin = {};
|
||||
|
||||
-- "public"
|
||||
function APIDocumentationMixin:OnLoad()
|
||||
self.tables = {};
|
||||
self.functions = {};
|
||||
self.systems = {};
|
||||
self.fields = {};
|
||||
self.events = {};
|
||||
self.callbacks = {};
|
||||
|
||||
self.Commands = {
|
||||
Default = 1,
|
||||
CopyAPI = 2,
|
||||
OpenDump = 3,
|
||||
};
|
||||
|
||||
if DEFAULT_CHAT_FRAME then
|
||||
DEFAULT_CHAT_FRAME:SetMaxLines(2000);
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:HandleSlashCommand(command)
|
||||
local commands = { (" "):split(command) };
|
||||
|
||||
if commands[1] == "?" or commands[1] == "help" or commands[1] == "" then
|
||||
self:OutputUsage();
|
||||
elseif commands[1] == "stats" then
|
||||
self:OutputStats();
|
||||
elseif commands[1] == "system" then
|
||||
if commands[2] == "list" then
|
||||
self:OutputAllSystems();
|
||||
else
|
||||
self:OutputUsage();
|
||||
end
|
||||
elseif commands[1] == "s" or commands[1] == "search" then
|
||||
self:OutputAllAPIMatches(unpack(commands, 2));
|
||||
elseif commands[1] then
|
||||
self:TryHandlingSystemSearchCommand(unpack(commands));
|
||||
else
|
||||
self:OutputUsage();
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:HandleAPILink(link, command)
|
||||
local _, type, name, parentName = (":"):split(link);
|
||||
local apiInfo = self:FindAPIByName(type, name, parentName);
|
||||
if apiInfo then
|
||||
if command == self.Commands.CopyAPI then
|
||||
self:HandleCopyAPI(apiInfo);
|
||||
elseif command == self.Commands.OpenDump then
|
||||
self:HandleOpenDump(apiInfo);
|
||||
else
|
||||
self:HandleDefaultCommand(apiInfo);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:HandleDefaultCommand(apiInfo)
|
||||
self:WriteLine(" ");
|
||||
self:WriteAllLines(apiInfo:GetDetailedOutputLines());
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:HandleCopyAPI(apiInfo)
|
||||
local clipboardString = apiInfo:GetClipboardString();
|
||||
CopyToClipboard(clipboardString);
|
||||
self:WriteLineF("Copied to clipboard: %s", clipboardString);
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:HandleOpenDump(apiInfo)
|
||||
if apiInfo.Type == "Function" then
|
||||
local dumpString;
|
||||
local systemNamespace = apiInfo.System and apiInfo.System:GetNamespaceName() or nil;
|
||||
if systemNamespace and systemNamespace ~= "" then
|
||||
dumpString = ("/dump %s.%s()"):format(systemNamespace, apiInfo.Name);
|
||||
else
|
||||
dumpString = ("/dump %s()"):format(apiInfo.Name);
|
||||
end
|
||||
|
||||
local desiredCursorPosition = string.len(dumpString) - 1;
|
||||
ChatFrame_OpenChat(dumpString);
|
||||
RunNextFrame(function()
|
||||
if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:SetCursorPosition(desiredCursorPosition);
|
||||
end
|
||||
end);
|
||||
else
|
||||
self:WriteLine("Can only /dump functions");
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:FindAPIByName(apiType, name, parentName)
|
||||
local apiTable = self:GetAPITableByTypeName(apiType);
|
||||
if apiTable then
|
||||
for i, apiInfo in ipairs(apiTable) do
|
||||
if apiInfo:MatchesName(name, parentName) then
|
||||
return apiInfo;
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:GetAPITableByTypeName(apiType)
|
||||
if apiType == "function" then
|
||||
return self.functions;
|
||||
elseif apiType == "table" then
|
||||
return self.tables;
|
||||
elseif apiType == "system" then
|
||||
return self.systems;
|
||||
elseif apiType == "field" then
|
||||
return self.fields;
|
||||
elseif apiType == "event" then
|
||||
return self.events;
|
||||
elseif apiType == "callback" then
|
||||
return self.callbacks;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:OutputUsage()
|
||||
self:WriteLine("Usage:");
|
||||
|
||||
self:WriteLine("Search for API");
|
||||
self:WriteLine(self:GetIndentString() .. "/classicapi search <api name>");
|
||||
self:WriteLine(self:GetIndentString() .. "or");
|
||||
self:WriteLine(self:GetIndentString() .. "/classicapi s <api name>");
|
||||
self:WriteLine(self:GetIndentString() .. "Example: /classicapi search spell");
|
||||
self:WriteLine(" ");
|
||||
|
||||
self:WriteLine("List all systems");
|
||||
self:WriteLine(self:GetIndentString() .. "/classicapi system list");
|
||||
self:WriteLine(" ");
|
||||
|
||||
self:WriteLine("Search system for API");
|
||||
self:WriteLine(self:GetIndentString() .. "/classicapi <system name> search <api name>");
|
||||
self:WriteLine(self:GetIndentString() .. "or");
|
||||
self:WriteLine(self:GetIndentString() .. "/classicapi <system name> s <api name>");
|
||||
self:WriteLine(self:GetIndentString() .. "Example: /classicapi spell search cooldown");
|
||||
self:WriteLine(" ");
|
||||
|
||||
self:WriteLine("List all API in a system");
|
||||
self:WriteLine(self:GetIndentString() .. "/classicapi <system name> list");
|
||||
self:WriteLine(self:GetIndentString() .. "Example: /classicapi spell list");
|
||||
self:WriteLine(" ");
|
||||
self:WriteLine("All searches support Lua patterns.");
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:OutputStats()
|
||||
self:WriteLine("Stats:");
|
||||
self:WriteLineF("Total systems: %d", tGetN(self.systems));
|
||||
local totalFunctions = 0;
|
||||
local totalEvents = 0;
|
||||
local totalTables = 0;
|
||||
|
||||
for i, systemInfo in ipairs(self.systems) do
|
||||
totalFunctions = totalFunctions + systemInfo:GetNumFunctions();
|
||||
totalEvents = totalEvents + systemInfo:GetNumEvents();
|
||||
totalTables = totalTables + systemInfo:GetNumTables();
|
||||
end
|
||||
|
||||
self:WriteLineF("Total functions: %d", totalFunctions);
|
||||
self:WriteLineF("Total events: %d", totalEvents);
|
||||
self:WriteLineF("Total tables: %d", totalTables);
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:OutputAllSystems()
|
||||
self:WriteLineF("All systems (%d):", tGetN(self.systems));
|
||||
for i, systemInfo in ipairs(self.systems) do
|
||||
self:WriteLine(systemInfo:GetSingleOutputLine());
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:TryHandlingSystemSearchCommand(systemName, subCommand, apiToSearchFor)
|
||||
local system = self:FindSystemByName(systemName);
|
||||
if system then
|
||||
if subCommand == nil then
|
||||
self:WriteLine(system:GetSingleOutputLine());
|
||||
elseif subCommand == "s" or subCommand == "search" and apiToSearchFor then
|
||||
self:OutputAllSystemAPIMatches(system, apiToSearchFor);
|
||||
elseif subCommand == "list" then
|
||||
self:OutputAllSystemAPI(system);
|
||||
else
|
||||
self:OutputUsage();
|
||||
end
|
||||
else
|
||||
self:WriteLineF("No system found (%s)", tostring(systemName));
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:OutputAPIMatches(apiMatches, headerName)
|
||||
if apiMatches and tGetN(apiMatches) > 0 then
|
||||
self:WriteLineF("Found %d %s", tGetN(apiMatches), headerName);
|
||||
for i, api in ipairs(apiMatches) do
|
||||
self:WriteLine(self:GetIndentString() .. api:GetSingleOutputLine());
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:OutputAllAPIMatches(apiToSearchFor)
|
||||
if not apiToSearchFor or apiToSearchFor == "" then
|
||||
self:OutputUsage();
|
||||
return;
|
||||
end
|
||||
self:WriteLine(" ");
|
||||
|
||||
local apiMatches = self:FindAllAPIMatches(apiToSearchFor);
|
||||
if apiMatches then
|
||||
local total = tGetN(apiMatches.tables) + tGetN(apiMatches.functions) + tGetN(apiMatches.events) + tGetN(apiMatches.systems) + tGetN(apiMatches.callbacks);
|
||||
assert(total > 0);
|
||||
self:WriteLineF("Found %d API that matches %q", total, apiToSearchFor);
|
||||
|
||||
self:OutputAPIMatches(apiMatches.systems, "system(s)");
|
||||
self:OutputAPIMatches(apiMatches.functions, "function(s)");
|
||||
self:OutputAPIMatches(apiMatches.events, "events(s)");
|
||||
self:OutputAPIMatches(apiMatches.tables, "table(s)");
|
||||
self:OutputAPIMatches(apiMatches.callbacks, "callback(s)");
|
||||
else
|
||||
self:WriteLineF("No API found that matches %q", apiToSearchFor);
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:OutputAllSystemAPIMatches(system, apiToSearchFor)
|
||||
local apiMatches = system:FindAllAPIMatches(apiToSearchFor);
|
||||
if apiMatches then
|
||||
local total = tGetN(apiMatches.tables) + tGetN(apiMatches.functions) + tGetN(apiMatches.events);
|
||||
assert(total > 0);
|
||||
self:WriteLineF("Found %d API that matches %q", total, apiToSearchFor);
|
||||
|
||||
self:OutputAPIMatches(apiMatches.functions, "function(s)");
|
||||
self:OutputAPIMatches(apiMatches.events, "events(s)");
|
||||
self:OutputAPIMatches(apiMatches.tables, "table(s)");
|
||||
else
|
||||
self:WriteLineF("No API found that matches %q in %s", apiToSearchFor, system:GenerateAPILink());
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:OutputAllSystemAPI(system)
|
||||
local apiMatches = system:ListAllAPI();
|
||||
if apiMatches then
|
||||
self:WriteLineF("All API in %s", system:GenerateAPILink());
|
||||
|
||||
self:OutputAPIMatches(apiMatches.functions, "function(s)");
|
||||
self:OutputAPIMatches(apiMatches.events, "events(s)");
|
||||
self:OutputAPIMatches(apiMatches.tables, "table(s)");
|
||||
else
|
||||
self:WriteLineF("No API found in %s", system:GenerateAPILink());
|
||||
end
|
||||
end
|
||||
|
||||
--[[static]] function APIDocumentationMixin:AddAllMatches(apiContainer, matchesContainer, apiToSearchFor)
|
||||
for i, apiInfo in ipairs(apiContainer) do
|
||||
if apiInfo:MatchesSearchString(apiToSearchFor) then
|
||||
table.insert(matchesContainer, apiInfo);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:FindAllAPIMatches(apiToSearchFor)
|
||||
apiToSearchFor = apiToSearchFor:lower();
|
||||
|
||||
local matches = {
|
||||
tables = {},
|
||||
functions = {},
|
||||
events = {},
|
||||
systems = {},
|
||||
callbacks = {},
|
||||
};
|
||||
|
||||
self:AddAllMatches(self.tables, matches.tables, apiToSearchFor);
|
||||
self:AddAllMatches(self.functions, matches.functions, apiToSearchFor);
|
||||
self:AddAllMatches(self.systems, matches.systems, apiToSearchFor);
|
||||
self:AddAllMatches(self.events, matches.events, apiToSearchFor);
|
||||
self:AddAllMatches(self.callbacks, matches.callbacks, apiToSearchFor);
|
||||
|
||||
-- Only return something if we matched anything
|
||||
for name, subTable in pairs(matches) do
|
||||
if tGetN(subTable) > 0 then
|
||||
return matches;
|
||||
end
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:FindSystemByName(systemName)
|
||||
systemName = systemName:lower();
|
||||
for i, systemInfo in ipairs(self.systems) do
|
||||
if systemInfo:MatchesNameCaseInsenstive(systemName) then
|
||||
return systemInfo;
|
||||
end
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:AddDocumentationTable(documentationInfo)
|
||||
if documentationInfo.Name then
|
||||
self:AddSystem(documentationInfo);
|
||||
else
|
||||
for i, tableInfo in ipairs(documentationInfo.Tables) do
|
||||
self:AddTable(tableInfo);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:WriteLine(message)
|
||||
if DEFAULT_CHAT_FRAME then
|
||||
local info = ChatTypeInfo["SYSTEM"];
|
||||
DEFAULT_CHAT_FRAME:AddMessage(message, info.r, info.g, info.b, info.id);
|
||||
else
|
||||
print(message);
|
||||
end
|
||||
end
|
||||
|
||||
-- The `...` in the parameter list is what creates `arg`; a Lua 5.0 function
|
||||
-- declared without it has no `arg` at all.
|
||||
function APIDocumentationMixin:WriteLineF(format, ...)
|
||||
self:WriteLine(string.format(format, unpack(arg)));
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:WriteAllLines(lines)
|
||||
for i, line in ipairs(lines) do
|
||||
self:WriteLine(line);
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:GetIndentString(numIndent)
|
||||
return (" "):rep(numIndent or 1);
|
||||
end
|
||||
|
||||
-- "private"
|
||||
function APIDocumentationMixin:AddTable(documentationInfo)
|
||||
Mixin(documentationInfo, TablesAPIMixin);
|
||||
|
||||
table.insert(self.tables, documentationInfo);
|
||||
|
||||
if documentationInfo.Fields then
|
||||
for i, field in ipairs(documentationInfo.Fields) do
|
||||
field.Table = documentationInfo;
|
||||
self:AddField(field);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:AddFunction(documentationInfo)
|
||||
Mixin(documentationInfo, FunctionsAPIMixin);
|
||||
|
||||
table.insert(self.functions, documentationInfo);
|
||||
|
||||
if documentationInfo.Arguments then
|
||||
for i, field in ipairs(documentationInfo.Arguments) do
|
||||
field.Function = documentationInfo;
|
||||
self:AddField(field);
|
||||
end
|
||||
end
|
||||
|
||||
if documentationInfo.Returns then
|
||||
for i, field in ipairs(documentationInfo.Returns) do
|
||||
field.Function = documentationInfo;
|
||||
self:AddField(field);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:AddEvent(documentationInfo)
|
||||
Mixin(documentationInfo, EventsAPIMixin);
|
||||
|
||||
table.insert(self.events, documentationInfo);
|
||||
|
||||
if documentationInfo.Payload then
|
||||
for i, field in ipairs(documentationInfo.Payload) do
|
||||
field.Event = documentationInfo;
|
||||
self:AddField(field);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:AddField(documentationInfo)
|
||||
Mixin(documentationInfo, FieldsAPIMixin);
|
||||
|
||||
table.insert(self.fields, documentationInfo);
|
||||
end
|
||||
|
||||
function APIDocumentationMixin:AddSystem(documentationInfo)
|
||||
Mixin(documentationInfo, SystemsAPIMixin);
|
||||
|
||||
table.insert(self.systems, documentationInfo);
|
||||
|
||||
for i, functionInfo in ipairs(documentationInfo.Functions) do
|
||||
functionInfo.System = documentationInfo;
|
||||
self:AddFunction(functionInfo);
|
||||
end
|
||||
|
||||
for i, eventInfo in ipairs(documentationInfo.Events) do
|
||||
eventInfo.System = documentationInfo;
|
||||
self:AddEvent(eventInfo);
|
||||
end
|
||||
|
||||
for i, tableInfo in ipairs(documentationInfo.Tables) do
|
||||
tableInfo.System = documentationInfo;
|
||||
self:AddTable(tableInfo);
|
||||
end
|
||||
end
|
||||
|
||||
APIDocumentation = CreateFromMixins(APIDocumentationMixin);
|
||||
APIDocumentation:OnLoad();
|
||||
@@ -0,0 +1,66 @@
|
||||
-- Backport of Blizzard_APIDocumentation/Blizzard_APIDocumentation_Bootstrap.lua
|
||||
-- to vanilla 1.12 / Lua 5.0, plus the `/api` command and `api` link handler
|
||||
-- that retail keeps in Blizzard_ChatFrameBase and Blizzard_UIPanels_Game.
|
||||
--
|
||||
-- Implementation differences from the retail source:
|
||||
-- - Retail loads a generated data addon on demand
|
||||
-- (LoadAddOnWithErrorHandling). ClassicAPI keeps the documentation in the
|
||||
-- DLL instead, so APIDocumentation_LoadUI reads it through
|
||||
-- C_APIDocumentation and feeds each system to AddDocumentationTable. The
|
||||
-- load still happens on first use, so a session that never types /api
|
||||
-- builds no tables at all.
|
||||
-- - IsModifiedClick does not exist on this client. CHATLINK is Shift by
|
||||
-- default, so a shift-click stands in for it.
|
||||
-- - Lua 5.0 has no length operator (#); table.getn(t) gives the count.
|
||||
|
||||
local loaded = false;
|
||||
|
||||
-- Fetches every documented system from the DLL. Returns true once the
|
||||
-- documentation is in place, false when the DLL is absent (the addon can be
|
||||
-- installed on its own).
|
||||
function APIDocumentation_LoadUI()
|
||||
if loaded then
|
||||
return true;
|
||||
end
|
||||
-- Latch BEFORE the fetch: a system table the browser chokes on must not
|
||||
-- raise the same error on every later /api.
|
||||
loaded = true;
|
||||
|
||||
if not C_APIDocumentation then
|
||||
APIDocumentation:WriteLine("API documentation is not available (C_APIDocumentation is missing).");
|
||||
return false;
|
||||
end
|
||||
|
||||
-- ipairs, not a length call: it walks 1..nil whatever the table's internal
|
||||
-- shape, which is how every other consumer reads an array the DLL built.
|
||||
for i, name in ipairs(C_APIDocumentation.GetSystems()) do
|
||||
local system = C_APIDocumentation.GetSystem(name);
|
||||
if system then
|
||||
APIDocumentation:AddDocumentationTable(system);
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Retail calls this `/api`. Here the browser holds only what ClassicAPI adds,
|
||||
-- not the 1.12 API the client already ships, so it is named for what it
|
||||
-- documents. `/capi` is the short form.
|
||||
SLASH_CLASSICAPI1 = "/classicapi";
|
||||
SLASH_CLASSICAPI2 = "/capi";
|
||||
SlashCmdList["CLASSICAPI"] = function(msg)
|
||||
APIDocumentation_LoadUI();
|
||||
APIDocumentation:HandleSlashCommand(msg or "");
|
||||
end
|
||||
|
||||
LinkUtil.RegisterLinkHandler(LinkTypes.APIDocumentation, function(link, text, linkData, contextData)
|
||||
APIDocumentation_LoadUI();
|
||||
|
||||
local command = APIDocumentation.Commands.Default;
|
||||
if contextData and contextData.button == "RightButton" then
|
||||
command = APIDocumentation.Commands.CopyAPI;
|
||||
elseif IsShiftKeyDown() then
|
||||
command = APIDocumentation.Commands.OpenDump;
|
||||
end
|
||||
|
||||
APIDocumentation:HandleAPILink(link, command);
|
||||
end);
|
||||
@@ -0,0 +1,115 @@
|
||||
-- Backport of Blizzard_APIDocumentation/BaseAPIMixin.lua to vanilla 1.12 /
|
||||
-- Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the retail source: none. Every construct
|
||||
-- here (method calls on string literals, string.format's %s, table.insert,
|
||||
-- ipairs, CreateFromMixins) exists on this client.
|
||||
|
||||
BaseAPIMixin = {};
|
||||
|
||||
function BaseAPIMixin:GetType()
|
||||
return assert(false);
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GetPrettyType()
|
||||
return self:GetType();
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GetLinkHexColor()
|
||||
return "ffffff";
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GetName()
|
||||
return self.Name;
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GetFullName()
|
||||
return self:GetName();
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GetParentName()
|
||||
return "";
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GetLoweredParentName()
|
||||
if not self.loweredParentName then
|
||||
self.loweredParentName = self:GetParentName():lower();
|
||||
end
|
||||
return self.loweredParentName;
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GetLoweredName()
|
||||
if not self.loweredName then
|
||||
self.loweredName = self:GetName():lower();
|
||||
end
|
||||
return self.loweredName;
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GetClipboardString()
|
||||
return self:GetFullName();
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GenerateAPILink()
|
||||
return ("|cff%s|Hapi:%s:%s:%s|h%s|h|r"):format(self:GetLinkHexColor(), self:GetType(), self:GetName(), self:GetParentName(), self:GetFullName());
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GetSingleOutputLine()
|
||||
return ("%s %s"):format(self:GetPrettyType(), self:GenerateAPILink());
|
||||
end
|
||||
|
||||
function BaseAPIMixin:GetDetailedOutputLines()
|
||||
return { self:GetSingleOutputLine() };
|
||||
end
|
||||
|
||||
function BaseAPIMixin:MatchesSearchString(searchString)
|
||||
return false;
|
||||
end
|
||||
|
||||
function BaseAPIMixin:MatchesName(name, parentName)
|
||||
if name == self:GetName() then
|
||||
return not parentName or parentName == self:GetParentName();
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function BaseAPIMixin:MatchesNameCaseInsenstive(name, parentName)
|
||||
if name == self:GetLoweredName() then
|
||||
return not parentName or parentName == self:GetLoweredParentName();
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function BaseAPIMixin:MatchesAnyAPI(apiTable, searchString)
|
||||
if apiTable then
|
||||
for i, apiInfo in ipairs(apiTable) do
|
||||
if apiInfo:MatchesSearchString(searchString) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function BaseAPIMixin:MatchesAnyDocumentation(searchString)
|
||||
if self.Documentation then
|
||||
for i, documentation in ipairs(self.Documentation) do
|
||||
if documentation:lower():match(searchString) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BaseAPIMixin:AddDocumentationTags(lines)
|
||||
if self.Documentation then
|
||||
for i, documentation in ipairs(self.Documentation) do
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. documentation);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BaseAPIMixin:AddSystemTag(lines)
|
||||
if self.System then
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. ("Part of the %s system"):format(self.System:GenerateAPILink()));
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
-- Backport of Blizzard_APIDocumentation/EventsAPIMixin.lua to vanilla 1.12 /
|
||||
-- Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the retail source:
|
||||
-- - The clipboard snippet uses this client's event convention. Handlers
|
||||
-- here read the `event` and `arg1`..`argN` globals rather than a vararg
|
||||
-- list, so the copied body declares those instead of `local a = ...`.
|
||||
|
||||
EventsAPIMixin = CreateFromMixins(BaseAPIMixin);
|
||||
|
||||
function EventsAPIMixin:GetParentName() -- override
|
||||
if self.System then
|
||||
return self.System:GetName();
|
||||
end
|
||||
|
||||
return "";
|
||||
end
|
||||
|
||||
function EventsAPIMixin:GetType() -- override
|
||||
return "event";
|
||||
end
|
||||
|
||||
function EventsAPIMixin:GetLinkHexColor()
|
||||
return "77ff22";
|
||||
end
|
||||
|
||||
function EventsAPIMixin:GetClipboardString()
|
||||
local lines = {};
|
||||
table.insert(lines, ([[if event == "%s" then]]):format(self.LiteralName));
|
||||
if self.Payload then
|
||||
local names = {};
|
||||
for i, payloadInfo in ipairs(self.Payload) do
|
||||
table.insert(names, ("local %s = arg%d;"):format(payloadInfo:GetName(), i));
|
||||
end
|
||||
table.insert(lines, "\t" .. table.concat(names, " "));
|
||||
end
|
||||
table.insert(lines, "end");
|
||||
return table.concat(lines, "\r\n");
|
||||
end
|
||||
|
||||
function EventsAPIMixin:GetFullName(decorateOptionals, includeColorCodes) -- override
|
||||
if self.System then
|
||||
return ("Event.%s.%s -> %s"):format(self.System:GetName(), self:GetName(), self:GetPayloadString(decorateOptionals, includeColorCodes));
|
||||
end
|
||||
return ("Event.%s -> %s"):format(self:GetName(), self:GetPayloadString(decorateOptionals, includeColorCodes));
|
||||
end
|
||||
|
||||
function EventsAPIMixin:MatchesSearchString(searchString) -- override
|
||||
if self:GetLoweredName():match(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self.System and self.System:GetLoweredName():match(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self.LiteralName:lower():match(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self:MatchesAnyDocumentation(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self:MatchesAnyAPI(self.Payload, searchString) then
|
||||
return true
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function EventsAPIMixin:GetPayloadString(decorateOptionals, includeColorCodes) -- override
|
||||
if self.Payload then
|
||||
local values = {};
|
||||
for i, payloadInfo in ipairs(self.Payload) do
|
||||
if includeColorCodes ~= false then
|
||||
table.insert(values, ("%s|cff%s"):format(payloadInfo:GetPayloadString(decorateOptionals, includeColorCodes), self:GetLinkHexColor()));
|
||||
else
|
||||
table.insert(values, payloadInfo:GetPayloadString(decorateOptionals, includeColorCodes));
|
||||
end
|
||||
end
|
||||
return table.concat(values, ", ");
|
||||
end
|
||||
return "";
|
||||
end
|
||||
|
||||
function EventsAPIMixin:GetDetailedOutputLines() -- override
|
||||
local lines = {};
|
||||
table.insert(lines, self:GetSingleOutputLine());
|
||||
|
||||
self:AddSystemTag(lines);
|
||||
self:AddDocumentationTags(lines);
|
||||
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Literal Name: \"" .. self.LiteralName .. "\"");
|
||||
|
||||
if self.Payload then
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Payload");
|
||||
for i, payloadInfo in ipairs(self.Payload) do
|
||||
table.insert(lines, APIDocumentation:GetIndentString(2) .. ("%d. %s"):format(i, payloadInfo:GetSingleOutputLine()));
|
||||
end
|
||||
end
|
||||
|
||||
return lines;
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
-- Backport of Blizzard_APIDocumentation/FieldsAPIMixin.lua to vanilla 1.12 /
|
||||
-- Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the retail source: none.
|
||||
|
||||
FieldsAPIMixin = CreateFromMixins(BaseAPIMixin);
|
||||
|
||||
function FieldsAPIMixin:GetParentName()
|
||||
if self.Function then
|
||||
return self.Function:GetName();
|
||||
end
|
||||
|
||||
if self.Table then
|
||||
return self.Table:GetName();
|
||||
end
|
||||
return "";
|
||||
end
|
||||
|
||||
function FieldsAPIMixin:GetType()
|
||||
return "field";
|
||||
end
|
||||
|
||||
function FieldsAPIMixin:GetLinkHexColor()
|
||||
return "ffdd55";
|
||||
end
|
||||
|
||||
function FieldsAPIMixin:MatchesSearchString(searchString)
|
||||
if self:GetLoweredName():match(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self:MatchesAnyDocumentation(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function FieldsAPIMixin:GetLuaType()
|
||||
if self.InnerType then
|
||||
local complexType = APIDocumentation:FindAPIByName("table", self.InnerType);
|
||||
return ("%s of %s"):format(self.Type, complexType and complexType:GenerateAPILink() or self.InnerType)
|
||||
end
|
||||
|
||||
if self.EnumValue then
|
||||
return self.EnumValue;
|
||||
end
|
||||
|
||||
local complexType = APIDocumentation:FindAPIByName("table", self.Type);
|
||||
return complexType and complexType:GenerateAPILink() or self.Type;
|
||||
end
|
||||
|
||||
function FieldsAPIMixin:GetStrideIndex()
|
||||
return self.StrideIndex;
|
||||
end
|
||||
|
||||
function FieldsAPIMixin:IsOptional()
|
||||
return self.Default ~= nil or self.Nilable;
|
||||
end
|
||||
|
||||
function FieldsAPIMixin:GetSingleOutputLine()
|
||||
local optionalString = "";
|
||||
if self:IsOptional() then
|
||||
if self.Default ~= nil then
|
||||
optionalString = ("(default:%s) "):format(tostring(self.Default));
|
||||
else
|
||||
optionalString = "(optional) ";
|
||||
end
|
||||
end
|
||||
if self.Documentation then
|
||||
return ("%s%s %s - %s"):format(optionalString, self:GetLuaType(), self:GenerateAPILink(), table.concat(self.Documentation, " "));
|
||||
end
|
||||
return ("%s%s %s"):format(optionalString, self:GetLuaType(), self:GenerateAPILink());
|
||||
end
|
||||
|
||||
function FieldsAPIMixin:GetArgumentString(decorateOptionals, includeColorCodes)
|
||||
local prefix = decorateOptionals ~= false and self:IsOptional() and "optional " or "";
|
||||
if includeColorCodes ~= false then
|
||||
return ("|cff%s%s%s|r"):format(self:GetLinkHexColor(), prefix, self:GetName());
|
||||
end
|
||||
return ("%s%s"):format(prefix, self:GetName());
|
||||
end
|
||||
|
||||
function FieldsAPIMixin:GetReturnString(decorateOptionals, includeColorCodes)
|
||||
return self:GetArgumentString(decorateOptionals, includeColorCodes); -- Nothing special currently
|
||||
end
|
||||
|
||||
function FieldsAPIMixin:GetPayloadString(decorateOptionals, includeColorCodes)
|
||||
return self:GetReturnString(decorateOptionals, includeColorCodes); -- Nothing special currently
|
||||
end
|
||||
@@ -0,0 +1,121 @@
|
||||
-- Backport of Blizzard_APIDocumentation/FunctionsAPIMixin.lua to vanilla 1.12
|
||||
-- / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the retail source: none.
|
||||
--
|
||||
-- A widget-method system (Type "ScriptObject") carries no Namespace, so
|
||||
-- GetFullName prints its methods bare -- "SetSpellByID(spellID)" -- exactly as
|
||||
-- retail prints its own widget API.
|
||||
|
||||
FunctionsAPIMixin = CreateFromMixins(BaseAPIMixin);
|
||||
|
||||
function FunctionsAPIMixin:GetParentName()
|
||||
if self.System then
|
||||
return self.System:GetName();
|
||||
end
|
||||
|
||||
return "";
|
||||
end
|
||||
|
||||
function FunctionsAPIMixin:GetType()
|
||||
return "function";
|
||||
end
|
||||
|
||||
function FunctionsAPIMixin:GetLinkHexColor()
|
||||
return "55ddff";
|
||||
end
|
||||
|
||||
function FunctionsAPIMixin:GetFullName(decorateOptionals, includeColorCodes)
|
||||
if self.System and self.System:GetNamespaceName() ~= "" then
|
||||
return ("%s.%s(%s)"):format(self.System:GetNamespaceName(), self:GetName(), self:GetArgumentString(decorateOptionals, includeColorCodes));
|
||||
end
|
||||
return ("%s(%s)"):format(self:GetName(), self:GetArgumentString(decorateOptionals, includeColorCodes));
|
||||
end
|
||||
|
||||
function FunctionsAPIMixin:MatchesSearchString(searchString)
|
||||
if self:GetLoweredName():match(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self:MatchesAnyDocumentation(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self:MatchesAnyAPI(self.Arguments, searchString) then
|
||||
return true
|
||||
end
|
||||
|
||||
if self:MatchesAnyAPI(self.Returns, searchString) then
|
||||
return true
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function FunctionsAPIMixin:GetArgumentString(decorateOptionals, includeColorCodes)
|
||||
if self.Arguments then
|
||||
local values = {};
|
||||
for i, argumentInfo in ipairs(self.Arguments) do
|
||||
if includeColorCodes ~= false then
|
||||
table.insert(values, ("%s|cff%s"):format(argumentInfo:GetArgumentString(decorateOptionals, includeColorCodes), self:GetLinkHexColor()));
|
||||
else
|
||||
table.insert(values, argumentInfo:GetArgumentString(decorateOptionals, includeColorCodes));
|
||||
end
|
||||
end
|
||||
return table.concat(values, ", ");
|
||||
end
|
||||
return "";
|
||||
end
|
||||
|
||||
function FunctionsAPIMixin:GetReturnString(decorateOptionals, includeColorCodes)
|
||||
if self.Returns then
|
||||
local values = {};
|
||||
for i, returnInfo in ipairs(self.Returns) do
|
||||
if includeColorCodes ~= false then
|
||||
table.insert(values, ("%s|cff%s"):format(returnInfo:GetReturnString(decorateOptionals, includeColorCodes), self:GetLinkHexColor()));
|
||||
else
|
||||
table.insert(values, returnInfo:GetReturnString(decorateOptionals, includeColorCodes));
|
||||
end
|
||||
end
|
||||
return table.concat(values, ", ");
|
||||
end
|
||||
return "";
|
||||
end
|
||||
|
||||
function FunctionsAPIMixin:GetClipboardString()
|
||||
if self.Returns then
|
||||
return ("local %s = %s"):format(self:GetReturnString(false, false), self:GetFullName(false, false));
|
||||
end
|
||||
return self:GetFullName(false, false);
|
||||
end
|
||||
|
||||
function FunctionsAPIMixin:GetDetailedOutputLines()
|
||||
local lines = {};
|
||||
table.insert(lines, self:GetSingleOutputLine());
|
||||
|
||||
self:AddSystemTag(lines);
|
||||
self:AddDocumentationTags(lines);
|
||||
|
||||
if self.Arguments then
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Arguments");
|
||||
for i, argumentInfo in ipairs(self.Arguments) do
|
||||
if argumentInfo:GetStrideIndex() == 1 then
|
||||
table.insert(lines, APIDocumentation:GetIndentString(2) .. "(Variable arguments)");
|
||||
end
|
||||
|
||||
table.insert(lines, APIDocumentation:GetIndentString(2) .. ("%d. %s"):format(i, argumentInfo:GetSingleOutputLine()));
|
||||
end
|
||||
end
|
||||
|
||||
if self.Returns then
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Returns");
|
||||
for i, returnInfo in ipairs(self.Returns) do
|
||||
if returnInfo:GetStrideIndex() == 1 then
|
||||
table.insert(lines, APIDocumentation:GetIndentString(2) .. "(Variable returns)");
|
||||
end
|
||||
table.insert(lines, APIDocumentation:GetIndentString(2) .. ("%d. %s"):format(i, returnInfo:GetSingleOutputLine()));
|
||||
end
|
||||
end
|
||||
|
||||
return lines;
|
||||
end
|
||||
@@ -0,0 +1,159 @@
|
||||
-- Backport of Blizzard_APIDocumentation/SystemsAPIMixin.lua to vanilla 1.12 /
|
||||
-- Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the retail source:
|
||||
-- - Lua 5.0 has no length operator (#); sequence length comes from
|
||||
-- table.getn(t), matching the rest of this addon.
|
||||
-- - A search also matches a system's namespace. The namespace is the name
|
||||
-- people type, and retail's version checks only the short name, so
|
||||
-- "search C_Spell" found nothing while "search spell" found the system.
|
||||
|
||||
local tGetN = table.getn;
|
||||
|
||||
SystemsAPIMixin = CreateFromMixins(BaseAPIMixin);
|
||||
|
||||
function SystemsAPIMixin:GetType()
|
||||
return "system";
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:GetLinkHexColor()
|
||||
return "ff55dd";
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:GetNamespaceName()
|
||||
return self.Namespace or "";
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:GetLoweredNamespaceName()
|
||||
if not self.loweredNamespaceName then
|
||||
self.loweredNamespaceName = self:GetNamespaceName():lower();
|
||||
end
|
||||
return self.loweredNamespaceName;
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:MatchesSearchString(searchString)
|
||||
if self:GetLoweredName():match(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
local namespace = self:GetLoweredNamespaceName();
|
||||
if namespace ~= "" and namespace:match(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self:MatchesAnyDocumentation(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:GetSingleOutputLine()
|
||||
if self.Namespace then
|
||||
return ("%s %s (%s)"):format(self:GetPrettyType(), self:GenerateAPILink(), self.Namespace);
|
||||
end
|
||||
return BaseAPIMixin.GetSingleOutputLine(self);
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:GetDetailedOutputLines()
|
||||
local lines = {};
|
||||
table.insert(lines, self:GetSingleOutputLine());
|
||||
|
||||
self:AddDocumentationTags(lines);
|
||||
|
||||
if self.Functions and tGetN(self.Functions) > 0 then
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Functions");
|
||||
for i, functionInfo in ipairs(self.Functions) do
|
||||
table.insert(lines, APIDocumentation:GetIndentString(2) .. functionInfo:GetSingleOutputLine());
|
||||
end
|
||||
end
|
||||
|
||||
if self.Events and tGetN(self.Events) > 0 then
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Events");
|
||||
for i, eventInfo in ipairs(self.Events) do
|
||||
table.insert(lines, APIDocumentation:GetIndentString(2) .. eventInfo:GetSingleOutputLine());
|
||||
end
|
||||
end
|
||||
|
||||
if self.Tables and tGetN(self.Tables) > 0 then
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Tables");
|
||||
for i, tableInfo in ipairs(self.Tables) do
|
||||
table.insert(lines, APIDocumentation:GetIndentString(2) .. tableInfo:GetSingleOutputLine());
|
||||
end
|
||||
end
|
||||
|
||||
return lines;
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:MatchesName(name, parentName)
|
||||
if BaseAPIMixin.MatchesName(self, name, parentName) then
|
||||
return true;
|
||||
end
|
||||
|
||||
return name == self:GetNamespaceName();
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:MatchesNameCaseInsenstive(name, parentName)
|
||||
if BaseAPIMixin.MatchesNameCaseInsenstive(self, name, parentName) then
|
||||
return true;
|
||||
end
|
||||
|
||||
return name == self:GetLoweredNamespaceName();
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:FindAllAPIMatches(apiToSearchFor)
|
||||
apiToSearchFor = apiToSearchFor:lower();
|
||||
|
||||
local matches = {
|
||||
tables = {},
|
||||
functions = {},
|
||||
events = {},
|
||||
};
|
||||
|
||||
APIDocumentationMixin:AddAllMatches(self.Tables, matches.tables, apiToSearchFor);
|
||||
APIDocumentationMixin:AddAllMatches(self.Functions, matches.functions, apiToSearchFor);
|
||||
APIDocumentationMixin:AddAllMatches(self.Events, matches.events, apiToSearchFor);
|
||||
|
||||
-- Only return something if we matched anything
|
||||
for name, subTable in pairs(matches) do
|
||||
if tGetN(subTable) > 0 then
|
||||
return matches;
|
||||
end
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
local function AddAll(apiContainer, matchesContainer)
|
||||
if apiContainer then
|
||||
for i, apiInfo in ipairs(apiContainer) do
|
||||
table.insert(matchesContainer, apiInfo);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:ListAllAPI()
|
||||
local allAPI = {
|
||||
tables = {},
|
||||
functions = {},
|
||||
events = {},
|
||||
};
|
||||
|
||||
AddAll(self.Tables, allAPI.tables);
|
||||
AddAll(self.Functions, allAPI.functions);
|
||||
AddAll(self.Events, allAPI.events);
|
||||
|
||||
return allAPI;
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:GetNumTables()
|
||||
return self.Tables and tGetN(self.Tables) or 0;
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:GetNumFunctions()
|
||||
return self.Functions and tGetN(self.Functions) or 0;
|
||||
end
|
||||
|
||||
function SystemsAPIMixin:GetNumEvents()
|
||||
return self.Events and tGetN(self.Events) or 0;
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
-- Backport of Blizzard_APIDocumentation/TablesAPIMixin.lua to vanilla 1.12 /
|
||||
-- Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the retail source: none.
|
||||
|
||||
TablesAPIMixin = CreateFromMixins(BaseAPIMixin);
|
||||
|
||||
function TablesAPIMixin:GetParentName()
|
||||
if self.System then
|
||||
return self.System:GetName();
|
||||
end
|
||||
|
||||
return "";
|
||||
end
|
||||
|
||||
function TablesAPIMixin:GetType()
|
||||
return "table";
|
||||
end
|
||||
|
||||
function TablesAPIMixin:GetPrettyType()
|
||||
return self.Type:lower();
|
||||
end
|
||||
|
||||
function TablesAPIMixin:GetLinkHexColor()
|
||||
return "55ffdd";
|
||||
end
|
||||
|
||||
function TablesAPIMixin:GetFullName() -- override
|
||||
if self.Type == "Enumeration" then
|
||||
return ("Enum.%s"):format(self:GetName());
|
||||
end
|
||||
|
||||
return BaseAPIMixin.GetFullName(self);
|
||||
end
|
||||
|
||||
function TablesAPIMixin:MatchesSearchString(searchString)
|
||||
if self:GetLoweredName():match(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self:MatchesAnyDocumentation(searchString) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self:MatchesAnyAPI(self.Fields, searchString) then
|
||||
return true
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function TablesAPIMixin:GetDetailedOutputLines()
|
||||
local lines = {};
|
||||
table.insert(lines, self:GetSingleOutputLine());
|
||||
|
||||
self:AddSystemTag(lines);
|
||||
self:AddDocumentationTags(lines);
|
||||
|
||||
if self.Fields then
|
||||
if self.Type == "Enumeration" then
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Num Values: " .. self.NumValues);
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Min Value: " .. self.MinValue);
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Max Value: " .. self.MaxValue);
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Values");
|
||||
else
|
||||
table.insert(lines, APIDocumentation:GetIndentString() .. "Fields");
|
||||
end
|
||||
|
||||
for i, fieldInfo in ipairs(self.Fields) do
|
||||
table.insert(lines, APIDocumentation:GetIndentString(2) .. fieldInfo:GetSingleOutputLine());
|
||||
end
|
||||
end
|
||||
|
||||
return lines;
|
||||
end
|
||||
@@ -0,0 +1,98 @@
|
||||
-- ActionButtonUseKeyDown: action-bar key bindings act on the key press when
|
||||
-- the cvar is set, on the release otherwise -- 4.3.4's FrameXML handlers
|
||||
-- (ActionButton.lua, MultiActionBars.lua, PetActionBarFrame.lua). Each keeps
|
||||
-- 1.12's own body; 4.3.4 adds only the cvar test: the press always shows the
|
||||
-- button pushed and acts when the cvar is set, the release shows it normal
|
||||
-- and acts when it is not. The engine registers the cvar (default 0, so the
|
||||
-- release stays the default).
|
||||
|
||||
local GetCVarBool = ClassicAPI.C_CVar.GetCVarBool;
|
||||
|
||||
local function UseKeyDown()
|
||||
return GetCVarBool("ActionButtonUseKeyDown");
|
||||
end
|
||||
|
||||
-- 1.12's ActionButtonUp / MultiActionButtonUp body.
|
||||
local function UseActionButton(button, onSelf)
|
||||
if ( MacroFrame_SaveMacro ) then
|
||||
MacroFrame_SaveMacro();
|
||||
end
|
||||
UseAction(ActionButton_GetPagedID(button), 0, onSelf);
|
||||
if ( IsCurrentAction(ActionButton_GetPagedID(button)) ) then
|
||||
button:SetChecked(1);
|
||||
else
|
||||
button:SetChecked(0);
|
||||
end
|
||||
end
|
||||
|
||||
local function MainBarButton(id)
|
||||
if ( BonusActionBarFrame:IsShown() ) then
|
||||
return getglobal("BonusActionButton"..id), true;
|
||||
end
|
||||
return getglobal("ActionButton"..id), false;
|
||||
end
|
||||
|
||||
function ActionButtonDown(id)
|
||||
local button = MainBarButton(id);
|
||||
if ( button:GetButtonState() == "NORMAL" ) then
|
||||
button:SetButtonState("PUSHED");
|
||||
end
|
||||
if ( UseKeyDown() ) then
|
||||
UseActionButton(button);
|
||||
end
|
||||
end
|
||||
|
||||
function ActionButtonUp(id, onSelf)
|
||||
local button, bonus = MainBarButton(id);
|
||||
if ( button:GetButtonState() == "PUSHED" ) then
|
||||
button:SetButtonState("NORMAL");
|
||||
if ( not UseKeyDown() ) then
|
||||
-- 1.12's bonus-bar branch uses the action without onSelf.
|
||||
if ( bonus ) then
|
||||
UseActionButton(button);
|
||||
else
|
||||
UseActionButton(button, onSelf);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function MultiActionButtonDown(bar, id)
|
||||
local button = getglobal(bar.."Button"..id);
|
||||
if ( button:GetButtonState() == "NORMAL" ) then
|
||||
button:SetButtonState("PUSHED");
|
||||
end
|
||||
if ( UseKeyDown() ) then
|
||||
UseActionButton(button);
|
||||
end
|
||||
end
|
||||
|
||||
function MultiActionButtonUp(bar, id, onSelf)
|
||||
local button = getglobal(bar.."Button"..id);
|
||||
if ( button:GetButtonState() == "PUSHED" ) then
|
||||
button:SetButtonState("NORMAL");
|
||||
if ( not UseKeyDown() ) then
|
||||
UseActionButton(button, onSelf);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PetActionButtonDown(id)
|
||||
local button = getglobal("PetActionButton"..id);
|
||||
if ( button:GetButtonState() == "NORMAL" ) then
|
||||
button:SetButtonState("PUSHED");
|
||||
end
|
||||
if ( UseKeyDown() ) then
|
||||
CastPetAction(id);
|
||||
end
|
||||
end
|
||||
|
||||
function PetActionButtonUp(id)
|
||||
local button = getglobal("PetActionButton"..id);
|
||||
if ( button:GetButtonState() == "PUSHED" ) then
|
||||
button:SetButtonState("NORMAL");
|
||||
if ( not UseKeyDown() ) then
|
||||
CastPetAction(id);
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,10 @@
|
||||
if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
EventUtil.ContinueOnAddOnLoaded('pfUI', function()
|
||||
-- A pfUI that sets `handlesHookScript` was written against our
|
||||
-- additions and manages them itself, so none of the shims below apply
|
||||
-- to it.
|
||||
if pfUI.handlesHookScript then return end
|
||||
|
||||
-- pfUI\pfUI.lua redeclares the RAID_CLASS_COLORS object at least 14 times (insane!!)
|
||||
-- This stops it from replacing all our ColorMixin values
|
||||
pfUI.UpdateColors = function() end
|
||||
@@ -17,13 +22,12 @@ if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
-- CreateFrame to shadow HookScript with a falsy field so those forks
|
||||
-- fall back to their vanilla SetScript path. The wrapper is removed as
|
||||
-- soon as the actionbar module finishes loading so it affects nothing
|
||||
-- else. A pfUI is trusted to handle HookScript itself if EITHER signal
|
||||
-- is present: the maintained "brues" X-Website tag OR the
|
||||
-- `pfUI.handlesHookScript` capability flag. Without the website the
|
||||
-- flag is required; if neither is present we apply the shim.
|
||||
-- else. The maintained "brues" fork, identified by its X-Website tag,
|
||||
-- handles HookScript on its own buttons and is exempt from this part;
|
||||
-- every other fork gets the shim.
|
||||
local website = GetAddOnMetadata("pfUI", "X-Website")
|
||||
local hasBruesWebsite = website and strfind(website, 'brues')
|
||||
if not hasBruesWebsite and not pfUI.handlesHookScript then
|
||||
if not hasBruesWebsite then
|
||||
local _createFrame = CreateFrame
|
||||
local HookedCreateFrame = function(frameType, name, parent, template)
|
||||
local frame = _createFrame(frameType, name, parent, template)
|
||||
@@ -43,13 +47,10 @@ if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
end)
|
||||
end
|
||||
|
||||
if C_AddOns.DoesAddOnExist("SpecialTalentUI") then
|
||||
EventUtil.ContinueOnAddOnLoaded("SpecialTalentUI", CAPI_ApplyStandardColorGlobals)
|
||||
end
|
||||
|
||||
-- ShaguTweaks libpredict register's TBC events that ClassicAPI backports.
|
||||
if C_AddOns.DoesAddOnExist("ShaguTweaks") then
|
||||
EventUtil.ContinueOnAddOnLoaded("ShaguTweaks", function()
|
||||
if not ShaguTweaks or (ShaguTweaks.API and ShaguTweaks.API.classicapi_version) then return end
|
||||
local libp = ShaguTweaks.libpredict
|
||||
if libp then
|
||||
libp.sender:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
|
||||
@@ -79,6 +80,72 @@ end
|
||||
|
||||
CAPI_MouseoverClearedCompat(GameTooltip)
|
||||
|
||||
-- Restores vanilla calling for ONE frame's script handler. Our DLL hands script
|
||||
-- handlers modern positional arguments (`self`, then the event's own args) on
|
||||
-- top of the `this` / `arg1` globals vanilla uses. That is additive for a
|
||||
-- handler declared `function()`, but a handler declared with a parameter of its
|
||||
-- own -- one written to be BOTH called directly and used as a script -- now
|
||||
-- receives `self` in that parameter instead of the nil it was written for.
|
||||
--
|
||||
-- The wrapper below takes no parameters, so those extra arguments stop here,
|
||||
-- and the original runs with the empty argument list vanilla gave it. The
|
||||
-- globals are untouched by the nesting, so the handler still reads `this` and
|
||||
-- `event` normally.
|
||||
function CAPI_VanillaScriptArgsCompat(frame, script)
|
||||
if not frame then return end
|
||||
local original = frame:GetScript(script)
|
||||
if not original then return end
|
||||
frame:SetScript(script, function()
|
||||
original()
|
||||
end)
|
||||
end
|
||||
|
||||
-- LunaUnitFrames sets `SetupGroupHeader(groupType)` as its group headers'
|
||||
-- OnEvent while also calling it directly with a group name. As a handler it
|
||||
-- expects `groupType` to be nil so it falls back to `this.unitGroup`. Given
|
||||
-- `self` instead, it looks the header up by frame rather than by name, finds
|
||||
-- nothing, and indexes nil (units.lua:358, issue #41). Its raid counterpart
|
||||
-- takes the header frame itself, so `self` is already what it wants -- only the
|
||||
-- group headers need this.
|
||||
if C_AddOns.DoesAddOnExist("LunaUnitFrames") then
|
||||
EventUtil.ContinueOnAddOnLoaded("LunaUnitFrames", function()
|
||||
if not (LunaUF and LunaUF.Units) then return end
|
||||
|
||||
local function FixGroupHeader(unit)
|
||||
CAPI_VanillaScriptArgsCompat(getglobal("LUFHeader" .. (unit or "")),
|
||||
"OnEvent")
|
||||
end
|
||||
|
||||
-- Headers are built on demand, so cover the ones already up and every
|
||||
-- one created later.
|
||||
FixGroupHeader("party")
|
||||
FixGroupHeader("partytarget")
|
||||
FixGroupHeader("partypet")
|
||||
hooksecurefunc(LunaUF.Units, "LoadGroupHeader", function(self, unit)
|
||||
FixGroupHeader(unit)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Turtle's Options frame sets `GroupFrame_ToggleMovement(state)` as the
|
||||
-- "Toggle Movement" button's OnClick (FrameXML\OptionsFrame.lua), while the
|
||||
-- group dropdown and the addon itself call it directly with 0 or 1. As a
|
||||
-- handler it expects `state` to be nil so it toggles from the current mouse
|
||||
-- state. Given `self` instead, it does `1 - state` on the button and errors
|
||||
-- (Turtle_GroupUI.lua:409). The button is reused and its script re-set on every
|
||||
-- redraw, so wrap the global it reads rather than the frame: only a number
|
||||
-- passes through.
|
||||
if C_AddOns.DoesAddOnExist("Turtle_GroupUI") then
|
||||
EventUtil.ContinueOnAddOnLoaded("Turtle_GroupUI", function()
|
||||
local original = GroupFrame_ToggleMovement
|
||||
if not original then return end
|
||||
GroupFrame_ToggleMovement = function(state)
|
||||
if type(state) ~= "number" then state = nil end
|
||||
return original(state)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if C_AddOns.DoesAddOnExist("Puppeteer") then
|
||||
EventUtil.ContinueOnAddOnLoaded("Puppeteer", function()
|
||||
CAPI_MouseoverClearedCompat(PTEnemyUpdater)
|
||||
@@ -113,3 +180,97 @@ if C_AddOns.DoesAddOnExist("ModernSpellBook") and not HookScript then
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Compost-2.0 (rev 17406+, bundled by BigWigs among others) replaces most of
|
||||
-- its own body with stubs once it believes it is on Lua 5.1, on the reasoning
|
||||
-- that table recycling is pointless when the collector already does it:
|
||||
--
|
||||
-- local lua51 = loadstring("return function(...) return ... end") and true or false
|
||||
-- ...
|
||||
-- if lua51 then function lib:Erase() return {} end
|
||||
-- else function lib:Erase(t) --[[ clears t in place ]] end
|
||||
--
|
||||
-- Our 5.1 syntax backport makes that probe pass, so the stubs win. For
|
||||
-- GetTable and Reclaim that is harmless -- both are pure recycling hints. Erase
|
||||
-- is not: callers hand it a live table and expect its keys gone. The stub takes
|
||||
-- no parameter at all, so every `compost:Erase(t)` that clears in place (rather
|
||||
-- than assigning the returned table) silently stops clearing.
|
||||
--
|
||||
-- Puppeteer is the reported casualty. PTUnit:ClearAuras erases its
|
||||
-- TrackedDebuffTypes, AfflictedDebuffTypes and aura-ID sets in place, so a
|
||||
-- dispel type never leaves the set once applied: a cursed raid member keeps its
|
||||
-- debuff colour after the curse is cleaned, and HasDebuffID answers true for
|
||||
-- auras that are long gone. It takes a second addon shipping the newer Compost
|
||||
-- to show up, because AceLibrary arbitrates on the highest $Revision and
|
||||
-- ignores load order -- so a lone Puppeteer keeps its own working rev 11579.
|
||||
--
|
||||
-- Restore an in-place Erase matching the pre-stub body. Probed by behavior
|
||||
-- rather than by revision or addon name, so a Compost that already clears is
|
||||
-- left alone.
|
||||
--
|
||||
-- Re-checked on every ADDON_LOADED rather than once, because AceLibrary can
|
||||
-- upgrade a library at any point during load -- including from an addon loaded
|
||||
-- on demand mid-session -- and an upgrade installs the new copy's functions
|
||||
-- over ours. We cannot simply wait for a late event either, since this addon
|
||||
-- loads first and AceLibrary does not exist yet at that point.
|
||||
--
|
||||
-- Repeating is cheap because the verdict is cached against the function that
|
||||
-- earned it: a check settles to two identity comparisons once the live Erase
|
||||
-- has been either vetted or replaced, and only an Erase we have never seen
|
||||
-- costs a probe. The probe table is reused across checks (refilled each time,
|
||||
-- so leftover state cannot skew the answer) rather than built per check.
|
||||
local compostProbe = {}
|
||||
local vettedErase
|
||||
|
||||
local function EraseInPlace(self, t)
|
||||
if type(t) ~= "table" then return end
|
||||
setmetatable(t, nil)
|
||||
for k in pairs(t) do
|
||||
t[k] = nil
|
||||
end
|
||||
table.setn(t, 0)
|
||||
return t
|
||||
end
|
||||
|
||||
local function EnsureCompostErasesInPlace()
|
||||
if not (AceLibrary and AceLibrary.HasInstance
|
||||
and AceLibrary:HasInstance("Compost-2.0")) then
|
||||
return
|
||||
end
|
||||
|
||||
local compost = AceLibrary("Compost-2.0")
|
||||
if compost.Erase == EraseInPlace or compost.Erase == vettedErase then
|
||||
return
|
||||
end
|
||||
|
||||
compostProbe[1] = "array"
|
||||
compostProbe.key = "hash"
|
||||
compost:Erase(compostProbe)
|
||||
if next(compostProbe) == nil then
|
||||
vettedErase = compost.Erase -- clears in place; leave it alone
|
||||
return
|
||||
end
|
||||
|
||||
compost.Erase = EraseInPlace
|
||||
end
|
||||
|
||||
EventRegistry:RegisterFrameEventAndCallback("ADDON_LOADED",
|
||||
EnsureCompostErasesInPlace)
|
||||
EventRegistry:RegisterFrameEventAndCallback("PLAYER_LOGIN",
|
||||
EnsureCompostErasesInPlace)
|
||||
EnsureCompostErasesInPlace()
|
||||
|
||||
-- Some addons carry a copy of Blizzard's own color definitions -- a plain
|
||||
-- `NORMAL_FONT_COLOR = {r=1.0, g=0.82, b=0}` and its friends -- and assign
|
||||
-- them at file scope. That takes ColorMixin off those globals for every addon
|
||||
-- that loads later, which is what breaks pfUI's who list when it calls
|
||||
-- `NORMAL_FONT_COLOR:GetRGB()` (brues-code/pfUI#56).
|
||||
--
|
||||
-- Re-apply after each addon loads, so a clobbered color is repaired before the
|
||||
-- next addon reads it. This also covers an addon loaded on demand mid-session.
|
||||
-- The pass mixes the methods back into the table that is there, and costs one
|
||||
-- lookup per color when nothing is broken.
|
||||
EventRegistry:RegisterFrameEventAndCallback("ADDON_LOADED",
|
||||
CAPI_ApplyStandardColorGlobals)
|
||||
EventRegistry:RegisterFrameEventAndCallback("PLAYER_LOGIN",
|
||||
CAPI_ApplyStandardColorGlobals)
|
||||
|
||||
@@ -81,18 +81,89 @@ function ColorMixin:WrapTextInColorTableCode(text)
|
||||
return WrapTextInColorCode(text, self:GenerateHexColor())
|
||||
end
|
||||
|
||||
for _, v in pairs(ITEM_QUALITY_COLORS) do
|
||||
v.color = CreateColor(v.r, v.g, v.b, 1)
|
||||
-- Publishes the standard color globals (NORMAL_FONT_COLOR and friends) as
|
||||
-- ColorMixin objects, the way Blizzard_SharedXMLBase/Color.lua does.
|
||||
--
|
||||
-- Runs at load and again after every addon loads (see Util/AddOnCompat.lua).
|
||||
-- An addon that carries a copy of Blizzard's own definition -- a plain
|
||||
-- `NORMAL_FONT_COLOR = {r=1.0, g=0.82, b=0}` -- assigns it over ours at file
|
||||
-- scope, and every addon that loads after it then finds a color with no
|
||||
-- methods. A later pass mixes ColorMixin INTO the table that is there instead
|
||||
-- of replacing it, so the values that addon chose stay, and each reference it
|
||||
-- gave out gets the methods too.
|
||||
--
|
||||
-- Only a color that is absent or has lost its methods is touched. A tag that
|
||||
-- holds something other than a color table belongs to some other addon, so it
|
||||
-- is left alone. A `<TAG>_CODE` string that is already set is never rewritten:
|
||||
-- FrameXML writes those together with the values they go with.
|
||||
local standardColors
|
||||
|
||||
-- True for a table that carries r/g/b numbers.
|
||||
local function HasRGB(value)
|
||||
return type(value) == "table"
|
||||
and type(value.r) == "number"
|
||||
and type(value.g) == "number"
|
||||
and type(value.b) == "number"
|
||||
end
|
||||
|
||||
function CAPI_ApplyStandardColorGlobals()
|
||||
local envTbl = _G
|
||||
for _, dbColor in ipairs(C_UIColor.GetColors()) do
|
||||
if not envTbl[dbColor.baseTag] or not envTbl[dbColor.baseTag].GetRGB then
|
||||
local color = CreateColor(dbColor.color.r, dbColor.color.g, dbColor.color.b, dbColor.color.a)
|
||||
envTbl[dbColor.baseTag] = color
|
||||
envTbl[dbColor.baseTag.."_CODE"] = color:GenerateHexColorMarkup()
|
||||
-- True for a color table that still needs the mixin.
|
||||
local function IsPlainColorTable(value)
|
||||
return HasRGB(value) and not value.GetRGB
|
||||
end
|
||||
|
||||
-- ITEM_QUALITY_COLORS is the same story with one more level: FrameXML builds
|
||||
-- each entry as a plain {r, g, b, hex} table, and the `color` object on it is
|
||||
-- ours. An addon that carries its own copy of the table takes those objects
|
||||
-- with it, so repair the entries on the same pass. Keys run from -1, so this
|
||||
-- walks the table with pairs.
|
||||
local function ApplyItemQualityColors()
|
||||
if type(ITEM_QUALITY_COLORS) ~= "table" then return end
|
||||
|
||||
for _, quality in pairs(ITEM_QUALITY_COLORS) do
|
||||
if type(quality) == "table" then
|
||||
if IsPlainColorTable(quality.color) then
|
||||
quality.color = Mixin(quality.color, ColorMixin)
|
||||
quality.color.a = quality.color.a or 1
|
||||
elseif quality.color == nil and HasRGB(quality) then
|
||||
quality.color = CreateColor(quality.r, quality.g, quality.b, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CAPI_ApplyStandardColorGlobals()
|
||||
-- `C_UIColor.GetColors()` makes a color object for each of its 187 rows,
|
||||
-- so keep the values from the first call. Each later pass is then one
|
||||
-- lookup per tag.
|
||||
if not standardColors then
|
||||
standardColors = {}
|
||||
for _, dbColor in ipairs(C_UIColor.GetColors()) do
|
||||
table.insert(standardColors, {
|
||||
tag = dbColor.baseTag,
|
||||
r = dbColor.color.r,
|
||||
g = dbColor.color.g,
|
||||
b = dbColor.color.b,
|
||||
a = dbColor.color.a,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
for _, entry in ipairs(standardColors) do
|
||||
local current = _G[entry.tag]
|
||||
local color
|
||||
if current == nil then
|
||||
color = CreateColor(entry.r, entry.g, entry.b, entry.a)
|
||||
_G[entry.tag] = color
|
||||
elseif IsPlainColorTable(current) then
|
||||
color = Mixin(current, ColorMixin)
|
||||
color.a = color.a or 1
|
||||
end
|
||||
|
||||
if color and _G[entry.tag.."_CODE"] == nil then
|
||||
_G[entry.tag.."_CODE"] = color:GenerateHexColorMarkup()
|
||||
end
|
||||
end
|
||||
|
||||
ApplyItemQualityColors()
|
||||
end
|
||||
CAPI_ApplyStandardColorGlobals()
|
||||
|
||||
@@ -27,3 +27,18 @@ function EquipmentManager_GetLocationData(location)
|
||||
|
||||
return locationData;
|
||||
end
|
||||
|
||||
function EquipmentManager_EquipSet(setID)
|
||||
if ( C_EquipmentSet.EquipmentSetContainsLockedItems(setID) or C_Spell.UnitCastingInfo("player") ) then
|
||||
UIErrorsFrame:AddMessage(ERR_CLIENT_LOCKED_OUT, 1.0, 0.1, 0.1, 1.0);
|
||||
return;
|
||||
end
|
||||
C_EquipmentSet.UseEquipmentSet(setID);
|
||||
end
|
||||
|
||||
-- An equipment-set action button fires WEAR_EQUIPMENT_SET; equipping is the UI's job.
|
||||
local EquipmentManagerFrame = CreateFrame("Frame");
|
||||
EquipmentManagerFrame:SetScript("OnEvent", function()
|
||||
EquipmentManager_EquipSet(arg1);
|
||||
end);
|
||||
EquipmentManagerFrame:RegisterEvent("WEAR_EQUIPMENT_SET");
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
-- 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).
|
||||
-- - the scale/layout/secure/EventRegistry helpers (rely on APIs absent
|
||||
-- here).
|
||||
|
||||
FrameUtil = FrameUtil or {};
|
||||
|
||||
@@ -14,6 +14,12 @@ function FrameUtil.RegisterFrameForEvents(frame, events)
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.RegisterFrameForUnitEvents(frame, events, ...)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterUnitEvent(event, unpack(arg));
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.UnregisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:UnregisterEvent(event);
|
||||
|
||||
@@ -61,6 +61,18 @@ LinkTypes = {
|
||||
|
||||
LinkUtil = {};
|
||||
|
||||
-- `assertsafe` isn't a global in stock 1.12; defined as a local fallback so
|
||||
-- this file stays self-contained, the same way FunctionUtil.lua and Pools.lua
|
||||
-- do. Only fires when `condition` is falsy, matching Blizzard's semantics.
|
||||
local function assertsafe(condition, msg)
|
||||
if not condition then
|
||||
local handler = geterrorhandler and geterrorhandler();
|
||||
if handler then
|
||||
handler(msg or "assertion failed!");
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function LinkUtil.FormatLink(linkType, linkDisplayText, ...)
|
||||
local linkFormatTable = { string.format("|H%s", linkType) };
|
||||
for i = 1, arg.n do
|
||||
@@ -185,3 +197,25 @@ do
|
||||
s_linkHandlerFunctions[linkType] = handlerFunction;
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Click dispatch. Retail's SetItemRef opens with exactly this: build the
|
||||
-- context, offer the link to the registered handlers, and stop if one took it.
|
||||
-- 1.12's SetItemRef knows only item and player links and would reject anything
|
||||
-- else, so the dispatch wraps it here instead of living in the engine.
|
||||
--
|
||||
-- The chat renderer itself accepts any |H<type>:...|h[text]|h and makes it
|
||||
-- clickable, so a handler is all a new link type needs.
|
||||
--
|
||||
-- Capture-and-replace, not hooksecurefunc: a post-hook cannot stop the engine
|
||||
-- from rejecting the link first. 1.12 passes (link, text, button); `link` is
|
||||
-- the payload without the |H and |h wrappers, `text` the whole decorated link.
|
||||
------------------------------------------------------------------------------
|
||||
local originalSetItemRef = SetItemRef;
|
||||
function SetItemRef(link, text, button, chatFrame)
|
||||
local contextData = { button = button, frame = chatFrame };
|
||||
if LinkUtil.ProcessLink(link, text, contextData) == LinkProcessorResponse.Handled then
|
||||
return;
|
||||
end
|
||||
return originalSetItemRef(link, text, button, chatFrame);
|
||||
end
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
-- MacroIconSelection.lua -- the Macro UI's macro list shows the icon a
|
||||
-- `#showtooltip` resolved to, the way the action button does.
|
||||
--
|
||||
-- `GetMacroInfo` reports the icon the macro STORES, and it has to keep
|
||||
-- reporting that one: `MacroFrame_Update` seeds the icon selector from that
|
||||
-- return, so answering with a resolved icon there would pre-select an icon the
|
||||
-- player never picked, in the one place that edits the stored icon.
|
||||
--
|
||||
-- That leaves the list buttons showing the question mark for a macro whose
|
||||
-- action button shows a spell. `C_Macro.GetMacroIcon` returns the icon a macro
|
||||
-- currently shows, so paint the buttons from it once the stock update has
|
||||
-- drawn them. An empty slot and a macro with a chosen icon both answer with
|
||||
-- what stock already drew, so those are left alone.
|
||||
|
||||
local function ApplyIcon(texture, macroSlot)
|
||||
if not texture then
|
||||
return
|
||||
end
|
||||
local shown = C_Macro.GetMacroIcon(macroSlot)
|
||||
if shown then
|
||||
texture:SetTexture(shown)
|
||||
end
|
||||
end
|
||||
|
||||
local function PaintMacroList()
|
||||
if not (MacroFrame and MacroFrame.macroBase) then
|
||||
return
|
||||
end
|
||||
for i = 1, MAX_MACROS do
|
||||
ApplyIcon(_G["MacroButton" .. i .. "Icon"], MacroFrame.macroBase + i)
|
||||
end
|
||||
if MacroFrame.selectedMacro then
|
||||
ApplyIcon(MacroFrameSelectedMacroButtonIcon, MacroFrame.selectedMacro)
|
||||
end
|
||||
end
|
||||
|
||||
local function WrapMacroFrameUpdate()
|
||||
if not MacroFrame_Update then
|
||||
return
|
||||
end
|
||||
|
||||
local original = MacroFrame_Update
|
||||
-- No `...`: `MacroFrame_Update` takes no arguments, and Lua 5.0 would
|
||||
-- build an `arg` table for every vararg call.
|
||||
MacroFrame_Update = function()
|
||||
original()
|
||||
PaintMacroList()
|
||||
end
|
||||
end
|
||||
|
||||
-- Blizzard_MacroUI loads on demand.
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_MacroUI", WrapMacroFrameUpdate)
|
||||
@@ -24,8 +24,9 @@
|
||||
-- one ClassicAPI extension: [known:spellID] / [known:name].
|
||||
-- Where a keyword's underlying state does not exist on 1.12 (flying, flyable,
|
||||
-- vehicleui, unithasvehicleui) the condition is a constant false; where 1.12
|
||||
-- only has a partial answer (spec is always primary, button/actionbar default
|
||||
-- to the resting state) the closest honest mapping is used. An UNKNOWN keyword
|
||||
-- only has a partial answer (spec is always primary; actionbar defaults to the
|
||||
-- resting state, as does button when no click is running) the closest honest
|
||||
-- mapping is used. An UNKNOWN keyword
|
||||
-- is a hard false with a one-time warning -- never a silent pass -- so a typo
|
||||
-- can't wrongly show a frame. See docs/API.md for the full contract.
|
||||
--
|
||||
@@ -217,29 +218,46 @@ CONDITIONS.spec = function(target, args)
|
||||
return true;
|
||||
end
|
||||
|
||||
-- [mod:X/Y] passes when any value is a held modified click: a modifier string
|
||||
-- ("shift", "lctrl", "alt-shift") or a modified-click action ("SELFCAST"), as
|
||||
-- 4.3.4's EvaluateModifier asks IsModifiedClick. Bare [mod] is any of them.
|
||||
local IsModifiedClick = ClassicAPI.IsModifiedClick;
|
||||
|
||||
CONDITIONS.modifier = function(target, args)
|
||||
if args then
|
||||
for i = 1, args.n do
|
||||
local a = strlower(args[i]);
|
||||
if a == "shift" then
|
||||
if IsShiftKeyDown() then return true; end
|
||||
elseif a == "ctrl" then
|
||||
if IsControlKeyDown() then return true; end
|
||||
elseif a == "alt" then
|
||||
if IsAltKeyDown() then return true; end
|
||||
end
|
||||
if IsModifiedClick(args[i]) then return true; end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
return IsShiftKeyDown() or IsControlKeyDown() or IsAltKeyDown();
|
||||
return IsModifiedClick() ~= nil;
|
||||
end
|
||||
|
||||
-- Outside a click context the "current" button is the left button.
|
||||
-- [button:N] numbers the mouse buttons the way the rest of the modern API
|
||||
-- does -- 1 left, 2 RIGHT, 3 middle, 4, 5. That is deliberately not the order
|
||||
-- of the engine's internal click bitmask (where 2 is the middle button), which
|
||||
-- never reaches Lua. A name is matched as written, so a button clicked with an
|
||||
-- explicit name (`someButton:Click("LeftButton")`) answers [button:leftbutton]
|
||||
-- as well as [button:1].
|
||||
local BUTTON_NUMBER_NAMES = {
|
||||
["1"] = "leftbutton",
|
||||
["2"] = "rightbutton",
|
||||
["3"] = "middlebutton",
|
||||
["4"] = "button4",
|
||||
["5"] = "button5",
|
||||
};
|
||||
|
||||
CONDITIONS.button = function(target, args)
|
||||
if args then
|
||||
-- GetMouseButtonClicked() is the button of the click this macro is
|
||||
-- running inside. It reads nil when no click is running -- a state
|
||||
-- driver poll, or the re-evaluation behind a macro's #showtooltip
|
||||
-- display -- and the engine's own no-argument Button:Click() means the
|
||||
-- left button, so that is the resting answer.
|
||||
local current = strlower(GetMouseButtonClicked() or "LeftButton");
|
||||
for i = 1, args.n do
|
||||
local a = args[i];
|
||||
if a == "1" or strlower(a) == "leftbutton" then return true; end
|
||||
local a = strlower(args[i]);
|
||||
if (BUTTON_NUMBER_NAMES[a] or a) == current then return true; end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
@@ -346,6 +364,14 @@ CONDITIONS.worn = CONDITIONS.equipped;
|
||||
|
||||
local warned = {};
|
||||
|
||||
-- Keywords met during the current `ParseOptions` run that this parser does
|
||||
-- not own. A condition we cannot evaluate means the options were written for
|
||||
-- another macro parser's dialect, so the fact is recorded on the parsed
|
||||
-- clause set rather than reported here. The caller decides what to do with
|
||||
-- it: a command the player ran names the keyword, and the macro-display scan
|
||||
-- leaves the macro alone.
|
||||
local parseUnknowns;
|
||||
|
||||
-- piece -> { pred = fn, neg = bool, args = { n, [i] } or nil }
|
||||
local function ParseCondition(piece)
|
||||
local neg = false;
|
||||
@@ -368,10 +394,11 @@ local function ParseCondition(piece)
|
||||
|
||||
local pred = CONDITIONS[keyword];
|
||||
if not pred then
|
||||
if not warned[keyword] then
|
||||
warned[keyword] = true;
|
||||
print("SecureCmdOptionParse: unknown condition '" .. keyword .. "'");
|
||||
if not parseUnknowns then
|
||||
parseUnknowns = { n = 0 };
|
||||
end
|
||||
parseUnknowns.n = parseUnknowns.n + 1;
|
||||
parseUnknowns[parseUnknowns.n] = keyword;
|
||||
pred = AlwaysFalse;
|
||||
neg = false; -- an unknown keyword is a hard false
|
||||
end
|
||||
@@ -413,8 +440,10 @@ local function ParseClause(text)
|
||||
return { value = Trim(value), groups = groups };
|
||||
end
|
||||
|
||||
-- options -> { n, [i] = clause }. Splits on ';' at bracket depth 0.
|
||||
-- options -> { n, [i] = clause, unknown = { n, [i] = keyword } or nil }.
|
||||
-- Splits on ';' at bracket depth 0.
|
||||
local function ParseOptions(options)
|
||||
parseUnknowns = nil;
|
||||
local clauses = { n = 0 };
|
||||
local len = strlen(options);
|
||||
local pos = 1;
|
||||
@@ -436,6 +465,8 @@ local function ParseOptions(options)
|
||||
clauses[clauses.n] = ParseClause(strsub(options, pos, i - 1));
|
||||
pos = i + 1;
|
||||
end
|
||||
clauses.unknown = parseUnknowns;
|
||||
parseUnknowns = nil;
|
||||
return clauses;
|
||||
end
|
||||
|
||||
@@ -445,8 +476,80 @@ end
|
||||
|
||||
local cache = {};
|
||||
|
||||
-- `@unit` takes a character name as well as a token (`[target=Feral]`), and a
|
||||
-- name has to be searched for in the object manager. That search is the one
|
||||
-- expensive step here, so a pass remembers what it already found. The table
|
||||
-- is only built once a pass meets a name, and the next pass drops it: a token
|
||||
-- kept across passes could name a different unit by then.
|
||||
local passNames;
|
||||
|
||||
local function TokenForName(name)
|
||||
if passNames then
|
||||
local cached = passNames[name];
|
||||
if cached ~= nil then
|
||||
return cached or nil; -- false records "nobody there"
|
||||
end
|
||||
else
|
||||
passNames = {};
|
||||
end
|
||||
local token = UnitTokenFromName(name);
|
||||
passNames[name] = token or false;
|
||||
return token;
|
||||
end
|
||||
|
||||
-- A `@unit` piece as something the engine's unit functions accept, with no
|
||||
-- question asked about whether a unit is there. `IsUnitToken` tells a token
|
||||
-- from a name by asking the engine's own resolver, and it does not go looking
|
||||
-- for the unit -- which is what the conditions do next anyway.
|
||||
local function TokenForTarget(target)
|
||||
if IsUnitToken(target) then
|
||||
return target;
|
||||
end
|
||||
return TokenForName(target);
|
||||
end
|
||||
|
||||
-- Whether a `@unit` piece has a unit behind it right now. The pcall tells a
|
||||
-- token from a name in one step: the engine raises for a string that names no
|
||||
-- token, which is exactly the case to hand to the by-name search. That also
|
||||
-- keeps a typo failing the group instead of the caller.
|
||||
local function TargetExists(target)
|
||||
local ok, exists = pcall(UnitExists, target);
|
||||
if ok then
|
||||
return exists and true or false;
|
||||
end
|
||||
local token = TokenForName(target);
|
||||
return token ~= nil and UnitExists(token) and true or false;
|
||||
end
|
||||
|
||||
-- Targets that do not name a unit. `none` clears the target and `cursor`
|
||||
-- names a world position, so neither can be tested with UnitExists and both
|
||||
-- always pass the existence gate below.
|
||||
local NON_UNIT_TARGETS = { none = true, cursor = true };
|
||||
|
||||
local function GroupPasses(group)
|
||||
local target = group.target or "target";
|
||||
local target = group.target;
|
||||
local named = target and not NON_UNIT_TARGETS[strlower(target)];
|
||||
if group.n == 0 then
|
||||
-- A group that only names a unit (`[@mouseover]`) passes only while
|
||||
-- that unit exists, so `[@mouseover][] Spell` falls through to the
|
||||
-- next group when nothing is moused over. A bare `[]` and the
|
||||
-- non-unit targets always pass. Groups with conditions leave
|
||||
-- existence to them (`[@focus,noexists]` still works).
|
||||
if not named then
|
||||
return true;
|
||||
end
|
||||
return TargetExists(target);
|
||||
end
|
||||
if named then
|
||||
-- The conditions ask the engine about this unit, so a name has to
|
||||
-- become a token first. Existence is theirs to test, so it is not
|
||||
-- asked for here.
|
||||
target = TokenForTarget(target);
|
||||
if not target then
|
||||
return false;
|
||||
end
|
||||
end
|
||||
target = target or "target";
|
||||
for i = 1, group.n do
|
||||
local cond = group[i];
|
||||
local ok = cond.pred(target, cond.args);
|
||||
@@ -460,29 +563,56 @@ local function GroupPasses(group)
|
||||
return true;
|
||||
end
|
||||
|
||||
function SecureCmdOptionParse(options)
|
||||
-- Returns the matched value, the passing group's target, and the name of the
|
||||
-- first condition the options use that this parser does not own.
|
||||
--
|
||||
-- That third value marks the options as another macro parser's dialect. A
|
||||
-- caller that scans macro bodies it did not write passes `quiet` and reads it,
|
||||
-- so it can leave such a macro alone instead of naming a keyword the player
|
||||
-- has no reason to fix.
|
||||
function SecureCmdOptionParse(options, quiet)
|
||||
if type(options) ~= "string" then
|
||||
error("Usage: SecureCmdOptionParse(\"options\")");
|
||||
return nil; -- WoW's error() may not unwind
|
||||
end
|
||||
|
||||
-- Start of a pass: drop what the last one resolved by name. Nothing is
|
||||
-- allocated unless a name was actually met, so the all-token case (the
|
||||
-- common one, polled several times a second) costs nothing here.
|
||||
if passNames then
|
||||
passNames = nil;
|
||||
end
|
||||
|
||||
local clauses = cache[options];
|
||||
if not clauses then
|
||||
clauses = ParseOptions(options);
|
||||
cache[options] = clauses;
|
||||
end
|
||||
|
||||
-- Name each keyword once, for a caller that wants to hear about it.
|
||||
local unknown = clauses.unknown;
|
||||
if unknown and not quiet then
|
||||
for i = 1, unknown.n do
|
||||
local keyword = unknown[i];
|
||||
if not warned[keyword] then
|
||||
warned[keyword] = true;
|
||||
print("SecureCmdOptionParse: unknown condition '" .. keyword .. "'");
|
||||
end
|
||||
end
|
||||
end
|
||||
local foreign = unknown and unknown[1] or nil;
|
||||
|
||||
for i = 1, clauses.n do
|
||||
local clause = clauses[i];
|
||||
local groups = clause.groups;
|
||||
if groups.n == 0 then
|
||||
return clause.value;
|
||||
return clause.value, nil, foreign;
|
||||
end
|
||||
for j = 1, groups.n do
|
||||
if GroupPasses(groups[j]) then
|
||||
return clause.value, groups[j].target;
|
||||
return clause.value, groups[j].target, foreign;
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil;
|
||||
return nil, nil, foreign;
|
||||
end
|
||||
|
||||
@@ -1,222 +1,219 @@
|
||||
-- SecureStateDriver: automatic frame state / visibility from macro conditionals.
|
||||
--
|
||||
-- Backports FrameXML's SecureStateDriverManager. A registered driver re-runs
|
||||
-- SecureCmdOptionParse (Util/MacroOptions.lua) on a 0.2s poll (plus an instant
|
||||
-- rescan on the events below) and applies the result:
|
||||
-- * RegisterStateDriver(frame, "visibility", "[combat] hide; show") -> the
|
||||
-- value "show"/"hide" drives frame:Show()/Hide();
|
||||
-- * RegisterStateDriver / RegisterAttributeDriver for any other state ->
|
||||
-- frame:SetAttribute("state-"..state, value), so an OnAttributeChanged
|
||||
-- handler reacts to it;
|
||||
-- * RegisterUnitWatch(frame [, asState]) -> shows/hides (or sets
|
||||
-- "state-unitexists") as the frame's `unit` attribute comes and goes.
|
||||
-- SecureStateDriverManager
|
||||
-- Automatically sets states based on macro options for state driver frames
|
||||
-- Also handled showing/hiding frames based on unit existence (code originally by Tem)
|
||||
--
|
||||
-- Base: 3.3.5 FrameXML/SecureStateDriver.lua, with the anniversary refinements
|
||||
-- grafted in (the resolveDriver() consolidation, tonumber() coercion of driver
|
||||
-- values, the literal string 'nil' -> nil, and the "statehidden" attribute).
|
||||
-- 4.3.4 FrameXML/SecureStateDriver.lua, with three changes this client needs:
|
||||
-- * Each handler falls back to the vanilla globals (`this`, `arg1`, `arg2`)
|
||||
-- when called without arguments, so it keeps working while modern script
|
||||
-- arguments are switched off (SetModernScriptArgs(false)).
|
||||
-- * `string.match` instead of `strmatch`, which this client does not define.
|
||||
-- * Events this client does not define (UPDATE_STEALTH, ...) are skipped
|
||||
-- instead of registered, and UPDATE_SHAPESHIFT_FORMS (the 1.12 name) is
|
||||
-- registered alongside UPDATE_SHAPESHIFT_FORM.
|
||||
-- SecureCmdOptionParse is Util/MacroOptions.lua; SecureButton_GetUnit is
|
||||
-- SecureTemplates.lua (loaded earlier).
|
||||
|
||||
-- Register a frame attribute to be set automatically with changes in game state
|
||||
function RegisterAttributeDriver(frame, attribute, values)
|
||||
if ( attribute and values and attribute:sub(1, 1) ~= "_" ) then
|
||||
SecureStateDriverManager:SetAttribute("setframe", frame);
|
||||
SecureStateDriverManager:SetAttribute("setstate", attribute.." "..values);
|
||||
end
|
||||
end
|
||||
|
||||
-- Unregister a frame from the state driver manager.
|
||||
function UnregisterAttributeDriver(frame, attribute)
|
||||
if ( attribute ) then
|
||||
SecureStateDriverManager:SetAttribute("setframe", frame);
|
||||
SecureStateDriverManager:SetAttribute("setstate", attribute);
|
||||
else
|
||||
SecureStateDriverManager:SetAttribute("delframe", frame);
|
||||
end
|
||||
end
|
||||
|
||||
-- Bridge functions for compatibility
|
||||
function RegisterStateDriver(frame, state, values)
|
||||
return RegisterAttributeDriver(frame, "state-"..state, values);
|
||||
end
|
||||
|
||||
function UnregisterStateDriver(frame, state)
|
||||
return UnregisterAttributeDriver(frame, "state-"..state);
|
||||
end
|
||||
|
||||
-- Register a frame to be notified when a unit's existence changes, the
|
||||
-- unit is obtained from the frame's attributes. If asState is true then
|
||||
-- notification is via the 'state-unitexists' attribute with values
|
||||
-- true and false. Otherwise it's via :Show() and :Hide()
|
||||
function RegisterUnitWatch(frame, asState)
|
||||
if ( asState ) then
|
||||
SecureStateDriverManager:SetAttribute("addwatchstate", frame);
|
||||
else
|
||||
SecureStateDriverManager:SetAttribute("addwatch", frame);
|
||||
end
|
||||
end
|
||||
|
||||
-- Unregister a frame from the unit existence monitor.
|
||||
function UnregisterUnitWatch(frame)
|
||||
SecureStateDriverManager:SetAttribute("removewatch", frame);
|
||||
end
|
||||
|
||||
--
|
||||
-- Three deliberate deviations from upstream, all because 1.12 has no taint and
|
||||
-- our attribute system fires OnAttributeChanged SYNCHRONOUSLY:
|
||||
-- 1. No attribute-bootstrap manager. Upstream drives itself through
|
||||
-- SecureStateDriverManager:SetAttribute("setframe"/"setstate"/...) -- a
|
||||
-- taint-boundary trick. Our SetAttribute has a recursion guard (a nested
|
||||
-- SetAttribute stores the value but does NOT fire the handler, see
|
||||
-- src/frame/Attributes.cpp), so doing the registration-time resolve from
|
||||
-- inside that handler would silently swallow the target frame's own
|
||||
-- OnAttributeChanged. Instead the public Register* functions write plain
|
||||
-- local tables and resolve immediately at top level. Public signatures are
|
||||
-- unchanged from retail.
|
||||
-- 2. GetTime()-deadline throttle instead of the OnUpdate `elapsed` arg (1.12
|
||||
-- handlers take no parameters).
|
||||
-- 3. UpdateUnitWatch guards its "statehidden"/"state-unitexists" writes on a
|
||||
-- real change, so a watched frame's handler is not fired at 5 Hz.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- SecureButton_* (minimal -- no taint, no modifier/button suffix machinery)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- SecureButton_GetAttribute and SecureButton_GetUnit are defined in
|
||||
-- SecureTemplates.lua (loaded earlier via SecureTemplates.xml).
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Private state
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local secureAttributeDrivers = {}; -- [frame][attribute] = optionsString
|
||||
local unitExistsWatchers = {}; -- [frame] = doState (boolean)
|
||||
local unitExistsCache = setmetatable({}, {
|
||||
__index = function(t, k)
|
||||
local v = (UnitExists(k) and true) or false;
|
||||
t[k] = v;
|
||||
return v;
|
||||
end,
|
||||
});
|
||||
|
||||
-- Private implementation
|
||||
--
|
||||
local secureAttributeDrivers = {};
|
||||
local unitExistsWatchers = {};
|
||||
local unitExistsCache = setmetatable({},
|
||||
{ __index = function(t,k)
|
||||
local v = UnitExists(k) or false;
|
||||
t[k] = v;
|
||||
return v;
|
||||
end
|
||||
});
|
||||
local STATE_DRIVER_UPDATE_THROTTLE = 0.2;
|
||||
local nextUpdate = 0;
|
||||
local timer = 0;
|
||||
|
||||
local wipe = table.wipe;
|
||||
|
||||
-- Check to see if a frame is registered
|
||||
function UnitWatchRegistered(frame)
|
||||
return not (unitExistsWatchers[frame] == nil);
|
||||
end
|
||||
|
||||
local function SecureStateDriverManager_UpdateUnitWatch(frame, doState)
|
||||
local unit = SecureButton_GetUnit(frame);
|
||||
local exists = (unit and unitExistsCache[unit]);
|
||||
if ( doState ) then
|
||||
local attr = exists or false;
|
||||
if ( frame:GetAttribute("state-unitexists") ~= attr ) then
|
||||
frame:SetAttribute("state-unitexists", attr);
|
||||
end
|
||||
else
|
||||
if ( exists ) then
|
||||
frame:Show();
|
||||
frame:SetAttribute("statehidden", nil);
|
||||
else
|
||||
frame:Hide();
|
||||
frame:SetAttribute("statehidden", true);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local pairs = pairs;
|
||||
|
||||
function UnitWatchRegistered(frame)
|
||||
return unitExistsWatchers[frame] ~= nil;
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Resolvers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function UpdateUnitWatch(frame, doState)
|
||||
local unit = SecureButton_GetUnit(frame);
|
||||
local exists = (unit and unitExistsCache[unit]) or false;
|
||||
if doState then
|
||||
if frame:GetAttribute("state-unitexists") ~= exists then
|
||||
frame:SetAttribute("state-unitexists", exists);
|
||||
end
|
||||
else
|
||||
if exists then
|
||||
frame:Show();
|
||||
else
|
||||
frame:Hide();
|
||||
end
|
||||
local hidden = (not exists) or nil;
|
||||
if frame:GetAttribute("statehidden") ~= hidden then
|
||||
frame:SetAttribute("statehidden", hidden);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- consolidate duplicated code for footprint and maintainability
|
||||
local function resolveDriver(frame, attribute, values)
|
||||
local newValue = SecureCmdOptionParse(values);
|
||||
local newValue = SecureCmdOptionParse(values);
|
||||
|
||||
if attribute == "state-visibility" then
|
||||
if newValue == "show" then
|
||||
frame:Show();
|
||||
if frame:GetAttribute("statehidden") ~= nil then
|
||||
frame:SetAttribute("statehidden", nil);
|
||||
end
|
||||
elseif newValue == "hide" then
|
||||
frame:Hide();
|
||||
if frame:GetAttribute("statehidden") ~= true then
|
||||
frame:SetAttribute("statehidden", true);
|
||||
if ( attribute == "state-visibility" ) then
|
||||
if ( newValue == "show" ) then
|
||||
frame:Show();
|
||||
frame:SetAttribute("statehidden", nil);
|
||||
elseif ( newValue == "hide" ) then
|
||||
frame:Hide();
|
||||
frame:SetAttribute("statehidden", true);
|
||||
end
|
||||
elseif ( newValue ) then
|
||||
if ( newValue == 'nil' ) then
|
||||
newValue = nil;
|
||||
else
|
||||
newValue = tonumber(newValue) or newValue;
|
||||
end
|
||||
local oldValue = frame:GetAttribute(attribute);
|
||||
if ( newValue ~= oldValue ) then
|
||||
frame:SetAttribute(attribute, newValue);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function SecureStateDriverManager_OnUpdate(self,elapsed)
|
||||
elapsed = elapsed or arg1;
|
||||
timer = timer - elapsed;
|
||||
if ( timer <= 0 ) then
|
||||
timer = STATE_DRIVER_UPDATE_THROTTLE;
|
||||
|
||||
-- Handle state driver updates
|
||||
for frame,drivers in pairs(secureAttributeDrivers) do
|
||||
for attribute,values in pairs(drivers) do
|
||||
resolveDriver(frame, attribute, values);
|
||||
end
|
||||
end
|
||||
elseif newValue then
|
||||
if newValue == "nil" then
|
||||
newValue = nil;
|
||||
else
|
||||
newValue = tonumber(newValue) or newValue;
|
||||
|
||||
-- Handle unit existence changes
|
||||
wipe(unitExistsCache);
|
||||
for k in pairs(unitExistsCache) do
|
||||
unitExistsCache[k] = nil;
|
||||
end
|
||||
if frame:GetAttribute(attribute) ~= newValue then
|
||||
frame:SetAttribute(attribute, newValue);
|
||||
for frame,doState in pairs(unitExistsWatchers) do
|
||||
SecureStateDriverManager_UpdateUnitWatch(frame, doState);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Manager frame
|
||||
-- ---------------------------------------------------------------------------
|
||||
local function SecureStateDriverManager_OnEvent(self, event)
|
||||
timer = 0;
|
||||
end
|
||||
|
||||
SecureStateDriverManager = CreateFrame("Frame", "SecureStateDriverManager");
|
||||
SecureStateDriverManager:Hide(); -- hidden -> no OnUpdate until a driver exists
|
||||
|
||||
SecureStateDriverManager:SetScript("OnUpdate", function()
|
||||
local now = GetTime();
|
||||
if now < nextUpdate then
|
||||
local function SecureStateDriverManager_OnAttributeChanged(self, name, value)
|
||||
if ( not self ) then
|
||||
self, name, value = this, arg1, arg2;
|
||||
end
|
||||
if ( not value ) then
|
||||
return;
|
||||
end
|
||||
nextUpdate = now + STATE_DRIVER_UPDATE_THROTTLE;
|
||||
|
||||
for frame, drivers in pairs(secureAttributeDrivers) do
|
||||
for attribute, values in pairs(drivers) do
|
||||
if ( name == "setframe" ) then
|
||||
if ( not secureAttributeDrivers[value] ) then
|
||||
secureAttributeDrivers[value] = {};
|
||||
end
|
||||
SecureStateDriverManager:Show();
|
||||
elseif ( name == "delframe" ) then
|
||||
secureAttributeDrivers[value] = nil;
|
||||
elseif ( name == "setstate" ) then
|
||||
local frame = self:GetAttribute("setframe");
|
||||
local attribute, values = string.match(value, "^(%S+)%s*(.*)$");
|
||||
if ( values == "" ) then
|
||||
secureAttributeDrivers[frame][attribute] = nil;
|
||||
else
|
||||
secureAttributeDrivers[frame][attribute] = values;
|
||||
resolveDriver(frame, attribute, values);
|
||||
end
|
||||
elseif ( name == "addwatch" or name == "addwatchstate" ) then
|
||||
local doState = (name == "addwatchstate");
|
||||
unitExistsWatchers[value] = doState;
|
||||
SecureStateDriverManager:Show();
|
||||
SecureStateDriverManager_UpdateUnitWatch(value, doState);
|
||||
elseif ( name == "removewatch" ) then
|
||||
unitExistsWatchers[value] = nil;
|
||||
elseif ( name == "updatetime" ) then
|
||||
STATE_DRIVER_UPDATE_THROTTLE = value;
|
||||
end
|
||||
end
|
||||
|
||||
for k in pairs(unitExistsCache) do
|
||||
unitExistsCache[k] = nil;
|
||||
end
|
||||
for frame, doState in pairs(unitExistsWatchers) do
|
||||
UpdateUnitWatch(frame, doState);
|
||||
end
|
||||
end);
|
||||
SecureStateDriverManager = CreateFrame("Frame", "SecureStateDriverManager", nil, "SecureFrameTemplate");
|
||||
SecureStateDriverManager:Hide();
|
||||
SecureStateDriverManager:SetScript("OnUpdate", SecureStateDriverManager_OnUpdate);
|
||||
SecureStateDriverManager:SetScript("OnEvent", SecureStateDriverManager_OnEvent);
|
||||
SecureStateDriverManager:SetScript("OnAttributeChanged", SecureStateDriverManager_OnAttributeChanged);
|
||||
|
||||
SecureStateDriverManager:SetScript("OnEvent", function()
|
||||
nextUpdate = 0; -- force a rescan on the next frame
|
||||
end);
|
||||
|
||||
SecureStateDriverManager:SetScript("OnAttributeChanged", function()
|
||||
if arg1 == "updatetime" and tonumber(arg2) then
|
||||
STATE_DRIVER_UPDATE_THROTTLE = tonumber(arg2);
|
||||
end
|
||||
end);
|
||||
|
||||
-- Events that warrant an early rescan. Each is guarded so a name this client
|
||||
-- doesn't define (e.g. UPDATE_STEALTH) is simply skipped.
|
||||
local rescanEvents = {
|
||||
"MODIFIER_STATE_CHANGED", -- ClassicAPI-fired
|
||||
"PLAYER_FOCUS_CHANGED", -- ClassicAPI-fired
|
||||
"UPDATE_SHAPESHIFT_FORM", -- ClassicAPI-fired (singular)
|
||||
"UPDATE_SHAPESHIFT_FORMS", -- native 1.12 (plural)
|
||||
-- Events that trigger early rescans
|
||||
for _, event in ipairs({
|
||||
"MODIFIER_STATE_CHANGED",
|
||||
"ACTIONBAR_PAGE_CHANGED",
|
||||
"UPDATE_BONUS_ACTIONBAR",
|
||||
"PLAYER_ENTERING_WORLD",
|
||||
"UPDATE_SHAPESHIFT_FORM",
|
||||
"UPDATE_SHAPESHIFT_FORMS",
|
||||
"UPDATE_STEALTH",
|
||||
"PLAYER_TARGET_CHANGED",
|
||||
"PLAYER_FOCUS_CHANGED",
|
||||
"PLAYER_REGEN_DISABLED",
|
||||
"PLAYER_REGEN_ENABLED",
|
||||
"UNIT_PET",
|
||||
"RAID_ROSTER_UPDATE",
|
||||
"PARTY_MEMBERS_CHANGED",
|
||||
};
|
||||
for _, event in ipairs(rescanEvents) do
|
||||
if C_EventUtils.IsEventValid(event) then
|
||||
}) do
|
||||
if ( C_EventUtils.IsEventValid(event) ) then
|
||||
SecureStateDriverManager:RegisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Public API
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function RegisterAttributeDriver(frame, attribute, values)
|
||||
if attribute and values and string.sub(attribute, 1, 1) ~= "_" then
|
||||
local drivers = secureAttributeDrivers[frame];
|
||||
if not drivers then
|
||||
drivers = {};
|
||||
secureAttributeDrivers[frame] = drivers;
|
||||
end
|
||||
drivers[attribute] = values;
|
||||
SecureStateDriverManager:Show();
|
||||
resolveDriver(frame, attribute, values);
|
||||
end
|
||||
end
|
||||
|
||||
function UnregisterAttributeDriver(frame, attribute)
|
||||
if attribute then
|
||||
local drivers = secureAttributeDrivers[frame];
|
||||
if drivers then
|
||||
drivers[attribute] = nil;
|
||||
end
|
||||
else
|
||||
secureAttributeDrivers[frame] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
function RegisterStateDriver(frame, state, values)
|
||||
RegisterAttributeDriver(frame, "state-" .. state, values);
|
||||
end
|
||||
|
||||
function UnregisterStateDriver(frame, state)
|
||||
UnregisterAttributeDriver(frame, state and ("state-" .. state));
|
||||
end
|
||||
|
||||
function RegisterUnitWatch(frame, asState)
|
||||
local doState = (asState and true) or false;
|
||||
unitExistsWatchers[frame] = doState;
|
||||
SecureStateDriverManager:Show();
|
||||
UpdateUnitWatch(frame, doState);
|
||||
end
|
||||
|
||||
function UnregisterUnitWatch(frame)
|
||||
unitExistsWatchers[frame] = nil;
|
||||
end
|
||||
-- Deliberately ignoring mouseover and others' target changes because they change so much
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -92,3 +92,37 @@ function CreateSimpleTextureMarkup(file, width, height, xOffset, yOffset)
|
||||
, yOffset or 0
|
||||
);
|
||||
end
|
||||
|
||||
function CreateAtlasMarkup(atlasName, width, height, offsetX, offsetY)
|
||||
return string.format("|A:%s:%d:%d:%d:%d|a",
|
||||
atlasName
|
||||
, height or 0
|
||||
, width or 0
|
||||
, offsetX or 0
|
||||
, offsetY or 0
|
||||
);
|
||||
end
|
||||
|
||||
-- Raid target markers, published as atlas names.
|
||||
--
|
||||
-- All eight markers live in one 4x4 sheet, so naming a single marker means
|
||||
-- naming a cell inside that sheet — exactly what an atlas is for. Blizzard
|
||||
-- publishes no atlas names for these, so the names here follow the standalone
|
||||
-- marker files that other clients ship. An addon written against those files
|
||||
-- finds the art under the name it already knows.
|
||||
--
|
||||
-- Marker N sits at column (N-1) mod 4 and row floor((N-1) / 4), counting from
|
||||
-- the top left. That is the same cell math the chat marker tokens use.
|
||||
local RAID_TARGET_SHEET = "Interface\\TargetingFrame\\UI-RaidTargetingIcons";
|
||||
local RAID_TARGET_COLUMNS = 4;
|
||||
local RAID_TARGET_CELL = 0.25; -- a 64 pixel cell in a 256 pixel sheet
|
||||
|
||||
for marker = 1, 8 do
|
||||
local index = marker - 1;
|
||||
local left = math.mod(index, RAID_TARGET_COLUMNS) * RAID_TARGET_CELL;
|
||||
local top = math.floor(index / RAID_TARGET_COLUMNS) * RAID_TARGET_CELL;
|
||||
C_Texture.RegisterAtlas("UI-RaidTargetingIcon_"..marker, RAID_TARGET_SHEET,
|
||||
64, 64,
|
||||
left, left + RAID_TARGET_CELL,
|
||||
top, top + RAID_TARGET_CELL);
|
||||
end
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Backport of retail's Blizzard_ObjectAPI/UiMapPoint.lua. A UiMapPoint is the
|
||||
-- plain struct the C_Map waypoint calls take and return:
|
||||
-- { uiMapID = number, position = Vector2DMixin, z = number? }
|
||||
local CreateVector2D = CreateVector2D
|
||||
|
||||
UiMapPoint = {};
|
||||
|
||||
function UiMapPoint.CreateFromCoordinates(uiMapID, x, y, z)
|
||||
return { uiMapID = uiMapID, position = CreateVector2D(x, y), z = z };
|
||||
end
|
||||
|
||||
function UiMapPoint.CreateFromVector2D(uiMapID, position, z)
|
||||
return { uiMapID = uiMapID, position = CreateVector2D(position.x, position.y), z = z };
|
||||
end
|
||||
|
||||
function UiMapPoint.CreateFromVector3D(uiMapID, position)
|
||||
return { uiMapID = uiMapID, position = CreateVector2D(position.x, position.y), z = position.z };
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
-- The closed world map follows the player's zone, as the 3.x UI keeps it.
|
||||
--
|
||||
-- GetPlayerMapPosition("player") answers for whichever map the world map is
|
||||
-- set to, open or not, and returns 0,0 when the player isn't on that map.
|
||||
-- The 3.x FrameXML puts the map back on the player's zone whenever it is out
|
||||
-- of view: WorldMapFrame_OnHide ends with SetMapToCurrentZone(), and
|
||||
-- WatchFrame calls it on PLAYER_ENTERING_WORLD and, while the map is closed,
|
||||
-- on ZONE_CHANGED_NEW_AREA. Vanilla has no WatchFrame and doesn't reset the
|
||||
-- map when it closes, so after the player changes zones - or browses another
|
||||
-- zone and closes the map - the closed map stays on that other zone, and code
|
||||
-- written for 3.x that reads the player's position without setting the map
|
||||
-- first gets 0,0 (Zygor Guides Viewer's travel steps never complete).
|
||||
--
|
||||
-- WatchFrame skips the zone-change reset while "Show quest objectives on the
|
||||
-- map" is off; vanilla has no such option (the reset fed the tracker's POIs),
|
||||
-- so here it always runs.
|
||||
|
||||
local frame = CreateFrame("Frame")
|
||||
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
frame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
|
||||
frame:SetScript("OnEvent", function(self, ev)
|
||||
ev = ev or event
|
||||
if ev == "ZONE_CHANGED_NEW_AREA" and WorldMapFrame:IsShown() then
|
||||
return
|
||||
end
|
||||
SetMapToCurrentZone()
|
||||
end)
|
||||
|
||||
WorldMapFrame:HookScript("OnHide", function()
|
||||
SetMapToCurrentZone()
|
||||
end)
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Notizen für %s eintragen:";
|
||||
SET_NOTE = "Notiz eintragen";
|
||||
SILVER_AMOUNT_SYMBOL = "S";
|
||||
SLASH_CANCELAURA1 = "/auraabbruch";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/gestaltabbrechen";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/aktionsleistewechsel";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/fokusaufheben";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLEARTARGET1 = "/zielaufheben";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/klick";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_DISMOUNT1 = "/absitzen";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/anlegen";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/Ausrüstungsset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/platzanlegen";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/fokus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/baggressiv";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/bangriff";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/bautozauberaus";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/bautozauberan";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/bautozauberanaus";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/bdefensiv";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/bfolgen";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/bpassiv";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/bbleib";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/angriffstart";
|
||||
SLASH_STARTATTACK2 = "/startattack";
|
||||
SLASH_STOPATTACK1 = "/angriffstop";
|
||||
SLASH_STOPATTACK2 = "/stopattack";
|
||||
SLASH_STOPCASTING1 = "/stoppwirken";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/aktionsleisteumschalten";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/spezifischanvisieren";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/vorherigerfeind";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/vorherigesziel";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/feindanvisieren";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/feindlichenspielerwählen";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/freundanvisieren";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/freundlichenspielerwählen";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/gruppeanvisieren";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/schlachtzuganvisieren";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/benutzen";
|
||||
SLASH_USE2 = "/use";
|
||||
LINK_TRADESKILL_TOOLTIP = "Hier klicken, um einen Link zu Eurem Beruf zu erstellen.";
|
||||
|
||||
@@ -4,7 +4,7 @@ BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Are you sure you want to delete the equipment set %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Would you like to save the equipment set \"%s\"?";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:2:3:0:1|t";
|
||||
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:2:3:0:1|t";
|
||||
DEBUFF_SYMBOL_CURSE = "Cu";
|
||||
DEBUFF_SYMBOL_DISEASE = "Di";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
@@ -28,12 +28,13 @@ ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Some items from set %s cannot be swapped
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "One or more items from set %s were missing and could not be equipped.";
|
||||
CLEAR_FOCUS = "Clear Focus";
|
||||
SET_FOCUS = "Set Focus";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<no focus>";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
GEARSETS_POPUP_TEXT = "Enter Set Name (Max 16 Characters):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "All Icons";
|
||||
ICON_FILTER_ITEM = "Items";
|
||||
@@ -75,17 +76,78 @@ MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Equipment Manager";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "New Set";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
PLAYER_LIST_DELIMITER = ", ";
|
||||
SET_FRIENDNOTE_LABEL = "Set Notes for %s:";
|
||||
SET_NOTE = "Set Note";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:1:2:0:1|t";
|
||||
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:1:2:0:1|t";
|
||||
SLASH_CANCELAURA1 = "/cancelaura";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/cancelform";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/changeactionbar";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLEARTARGET1 = "/cleartarget";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_DISMOUNT1 = "/dismount";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/equip";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipslot";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/petaggressive";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/petattack";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/petautocasttoggle";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/petdefensive";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/petfollow";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/petpassive";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/petstay";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/startattack";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/stopattack";
|
||||
SLASH_STOPCASTING1 = "/stopcasting";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/swapactionbar";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/targetexact";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/targetlasttarget";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/targetraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Establecer nota para %s:";
|
||||
SET_NOTE = "Establecer nota";
|
||||
SILVER_AMOUNT_SYMBOL = "p";
|
||||
SLASH_CANCELAURA1 = "/cancelaraura";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/cancelforma";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/cambiarbarraccion";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/quitarenfoque";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLEARTARGET1 = "/quitarobjetivo";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/clic";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_DISMOUNT1 = "/desmontar";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/equipar";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/conjequip";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipranu";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/enfoque";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/mascagresiva";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/mascataque";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/nomascotaautohechizo";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/mascotautohechizo";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/alternarmascotautohechizo";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/mascdefensiva";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/mascsigue";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/mascpasiva";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/mascqueda";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/empezarataque";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/pararataque";
|
||||
SLASH_STOPCASTING1 = "/pararlanzamiento";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/intercambiarbarraccion";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/objetivoexacto";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/ultimoobjetivoenemigo";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/ultimobjetivo";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/objetivoenemigo";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/objetivojugadorenemigo";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/objetivoamistoso";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/objetivojugadoraliado";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/objetivogrupo";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/objetivobanda";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/usar";
|
||||
LINK_TRADESKILL_TOOLTIP = "Haz clic aquí para crear un vínculo a tu profesión.";
|
||||
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Écrire une note pour %s :";
|
||||
SET_NOTE = "Écrire une note";
|
||||
SILVER_AMOUNT_SYMBOL = "a";
|
||||
SLASH_CANCELAURA1 = "/annuleaura";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/annuleform";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/changebarreaction";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/annulerfocal";
|
||||
SLASH_CLEARTARGET1 = "/effacercible";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/cliquer";
|
||||
SLASH_DISMOUNT1 = "/descendre";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/équiper";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/équipens";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipempl";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focal";
|
||||
SLASH_PET_AGGRESSIVE1 = "/famagresse";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/famattaque";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/lancautofamoff";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/lancautofamon";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/lancautofamchange";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/famdéfend";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/famsuit";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/fampassif";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/famreste";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/lanceattaque";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/stopattaque";
|
||||
SLASH_STOPCASTING1 = "/stopsort";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/échangerbarreaction";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/cibleexacte";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/dernierennemi";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/dernièrecible";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/ciblerennemi";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/ciblerjoueurennemi";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/ciblerami";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/ciblerjoueurami";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/ciblergroupe";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/ciblerraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/utiliser";
|
||||
LINK_TRADESKILL_TOOLTIP = "Cliquez ici pour créer un lien vers votre profession.";
|
||||
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Заметка о |3-5(%s):";
|
||||
SET_NOTE = "Добавить заметку";
|
||||
SILVER_AMOUNT_SYMBOL = "с";
|
||||
SLASH_CANCELAURA1 = "/снятьауру";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/нормоблик";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/сменитьпанель";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/сбросфокуса";
|
||||
SLASH_CLEARTARGET1 = "/сбросцели";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/щелчок";
|
||||
SLASH_DISMOUNT1 = "/спешиться";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/экип";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/комплект";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/надеть";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/фокус";
|
||||
SLASH_PET_AGGRESSIVE1 = "/питомецагрессивный";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/питомецатака";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/питомецзаклинаниеоткл";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/питомецзаклинаниевкл";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/питомецавтозаклинание";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/питомецзащита";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/питомецзамной";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/питомецпассивный";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/питомецстоять";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/начатьатаку";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/стопатака";
|
||||
SLASH_STOPCASTING1 = "/стопзаклинание";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/сменитьпанелькоманд";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/цельточно";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/цельпослвраг";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/цельпослцель";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/цельвраг";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/цельдруг";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/цельгруппа";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/цельрейд";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/использовать";
|
||||
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
|
||||
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "为%s设置备注:";
|
||||
SET_NOTE = "设置备注";
|
||||
SILVER_AMOUNT_SYMBOL = "银";
|
||||
SLASH_CANCELAURA1 = "/cancelaura";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/cancelform";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/changeactionbar";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLEARTARGET1 = "/cleartarget";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_DISMOUNT1 = "/dismount";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/equip";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipslot";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/petaggressive";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/petattack";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/petautocasttoggle";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/petdefensive";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/petfollow";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/petpassive";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/petstay";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/startattack";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/stopattack";
|
||||
SLASH_STOPCASTING1 = "/stopcasting";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/swapactionbar";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/targetexact";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/targetlasttarget";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/targetraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
LINK_TRADESKILL_TOOLTIP = "点击这里以创建一个链接到你的专业的快捷方式。";
|
||||
|
||||
@@ -78,12 +78,72 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "設定%s的註記:";
|
||||
SET_NOTE = "設定註記";
|
||||
SILVER_AMOUNT_SYMBOL = "銀";
|
||||
SLASH_CANCELAURA1 = "/取消光環";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/取消形態";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/改變快捷列";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/清除專注目標";
|
||||
SLASH_CLEARTARGET1 = "/清除目標";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/點擊";
|
||||
SLASH_DISMOUNT1 = "/下馬";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/裝備";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/裝備套裝";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/裝備欄位";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/專注目標";
|
||||
SLASH_PET_AGGRESSIVE1 = "/寵物攻擊型";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/寵物攻擊";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/關閉寵物自動施法";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/開啟寵物自動施法";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/切換寵物自動施法";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/寵物防禦型";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/寵物跟隨";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/寵物被動型";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/寵物停留";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/開始攻擊";
|
||||
SLASH_STARTATTACK2 = "/startattack";
|
||||
SLASH_STOPATTACK1 = "/停止攻擊";
|
||||
SLASH_STOPATTACK2 = "/stopattack";
|
||||
SLASH_STOPCASTING1 = "/停止施法";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/切換快捷列";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/選定指定目標";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/選定前一敵人";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/選定前一目標";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/目標敵人";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/選定敵方玩家";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/目標友方";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/選定友方玩家";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/目標隊伍";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/目標團隊";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/使用";
|
||||
SLASH_USE2 = "/use";
|
||||
LINK_TRADESKILL_TOOLTIP = "點擊這裡以製造一個你專業技能的連結。";
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
-- explicit (this, event, arg1..arg9) parameters from XML inline scripts.
|
||||
-- * RegisterAllEvents is used when available; the curated default list is
|
||||
-- a fallback if the client doesn't expose it.
|
||||
-- * No GameTooltip:SetFrameStack -- /framestack walks the engine's
|
||||
-- frame list via EnumerateFrames(prev) and renders into our own
|
||||
-- FrameStackTooltip instead. Catches anonymous frames the modern
|
||||
-- engine method shows but a _G walk would miss.
|
||||
-- * /fstack accepts "true" (as on 3.3.5) or "hidden" to list hidden
|
||||
-- frames too.
|
||||
-- * No UIPanelDialogTemplate, no parentKey, no clampedToScreen, no
|
||||
-- OnSizeChanged in vanilla -- frames have fixed dimensions.
|
||||
-- * No debuglocals
|
||||
@@ -980,70 +978,6 @@ function DebugTooltip_OnLoad(self)
|
||||
end
|
||||
end
|
||||
|
||||
local _STRATA_RANK = {
|
||||
BACKGROUND = 1, LOW = 2, MEDIUM = 3, HIGH = 4,
|
||||
DIALOG = 5, FULLSCREEN = 6, FULLSCREEN_DIALOG = 7, TOOLTIP = 8,
|
||||
};
|
||||
|
||||
-- Walks every frame the engine knows about via EnumerateFrames (the
|
||||
-- engine-internal frame iterator). Beats a getfenv(0) walk on two
|
||||
-- counts: faster (the engine list is much shorter than _G), and
|
||||
-- includes anonymous frames that never appear in globals (parented
|
||||
-- templates, runtime `CreateFrame` with no `name` arg, etc.). The
|
||||
-- iteration order itself is engine-implementation detail; we re-
|
||||
-- sort by strata + level below either way.
|
||||
local function _CollectFramesUnderMouse(showHidden)
|
||||
local matches = {};
|
||||
local frame = EnumerateFrames(nil);
|
||||
while (frame) do
|
||||
local visOK, visible = pcall(frame.IsVisible, frame);
|
||||
local overOK, mouseOver = pcall(frame.IsMouseOver, frame);
|
||||
if (overOK and mouseOver and (showHidden or (visOK and visible))) then
|
||||
local nameOK, name = pcall(frame.GetName, frame);
|
||||
if (not nameOK or not name) then name = "<anonymous>"; end
|
||||
table.insert(matches, { name = name, frame = frame });
|
||||
end
|
||||
frame = EnumerateFrames(frame);
|
||||
end
|
||||
table.sort(matches, function(a, b)
|
||||
local sa = _STRATA_RANK[a.frame:GetFrameStrata()] or 0;
|
||||
local sb = _STRATA_RANK[b.frame:GetFrameStrata()] or 0;
|
||||
if (sa ~= sb) then return sa > sb; end
|
||||
local la = a.frame:GetFrameLevel() or 0;
|
||||
local lb = b.frame:GetFrameLevel() or 0;
|
||||
return la > lb;
|
||||
end);
|
||||
return matches;
|
||||
end
|
||||
|
||||
local function _UpdateFrameStackTooltip(tooltip, showHidden)
|
||||
tooltip:ClearLines();
|
||||
tooltip:AddLine(DEBUG_FRAMESTACK or "Frame Stack", 1, 1, 1);
|
||||
local matches = _CollectFramesUnderMouse(showHidden);
|
||||
local count = table.getn(matches);
|
||||
if (count == 0) then
|
||||
tooltip:AddLine("(no frames under cursor)", .7, .7, .7);
|
||||
else
|
||||
local shown = 0;
|
||||
for i = 1, count do
|
||||
local m = matches[i];
|
||||
local strata = m.frame:GetFrameStrata() or "?";
|
||||
local level = m.frame:GetFrameLevel() or 0;
|
||||
local label = string.format("%s |cff888888[%s:%d]|r",
|
||||
m.name, strata, level);
|
||||
tooltip:AddLine(label, 1, .82, 0);
|
||||
shown = shown + 1;
|
||||
if (shown >= 30) then
|
||||
tooltip:AddLine(string.format("|cffff8888...%d more|r",
|
||||
count - shown),
|
||||
1, 1, 1);
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
tooltip:Show();
|
||||
end
|
||||
|
||||
function FrameStackTooltip_Toggle(showHidden)
|
||||
local tooltip = FrameStackTooltip;
|
||||
if (tooltip:IsVisible()) then
|
||||
@@ -1053,7 +987,7 @@ function FrameStackTooltip_Toggle(showHidden)
|
||||
tooltip:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -32, 96);
|
||||
tooltip.default = 1;
|
||||
tooltip.showHidden = showHidden;
|
||||
_UpdateFrameStackTooltip(tooltip, showHidden);
|
||||
tooltip:SetFrameStack(showHidden);
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1064,7 +998,7 @@ function FrameStackTooltip_OnUpdate(self, elapsed)
|
||||
_frameStackAccum = _frameStackAccum + elapsed;
|
||||
if (_frameStackAccum >= FRAMESTACK_UPDATE_TIME) then
|
||||
_frameStackAccum = 0;
|
||||
_UpdateFrameStackTooltip(self, self.showHidden);
|
||||
self:SetFrameStack(self.showHidden);
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1095,7 +1029,7 @@ FrameStackTooltip_OnEnter = FrameStackTooltip_OnShow;
|
||||
|
||||
function FrameStackTooltip_HandleSlashCmd(msg)
|
||||
if (msg == nil) then msg = ""; end
|
||||
local showHidden = (string.find(msg, "hidden") ~= nil);
|
||||
local showHidden = (msg == "true" or string.find(msg, "hidden") ~= nil);
|
||||
FrameStackTooltip_Toggle(showHidden);
|
||||
end
|
||||
|
||||
|
||||
@@ -60,7 +60,11 @@
|
||||
</EdgeSize>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<!-- ARTWORK, not BACKGROUND: the Backdrop draws its own
|
||||
background into BACKGROUND after the textures declared here,
|
||||
which would bury this bar. The title text is in OVERLAY, so
|
||||
it still reads over the top. -->
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentTitleBG" file="Interface\QuestFrame\UI-QuestLogTitleHighlight">
|
||||
<Size x="280" y="20"/>
|
||||
<Anchors>
|
||||
@@ -278,7 +282,12 @@
|
||||
</EdgeSize>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<!-- ARTWORK, not BACKGROUND: the Backdrop above draws its
|
||||
background and border into BACKGROUND, and it is applied
|
||||
after the textures declared here, so a header in that layer
|
||||
ends up behind the window border. Every Blizzard frame that
|
||||
uses UI-DialogBox-Header puts it in ARTWORK for this reason. -->
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentTitleBG" file="Interface\DialogFrame\UI-DialogBox-Header">
|
||||
<Size x="256" y="64"/>
|
||||
<Anchors>
|
||||
@@ -322,8 +331,16 @@
|
||||
<OnDragStop>ScriptErrorsFrame_TitleButton_OnDragStop();</OnDragStop>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<!-- Width leaves room for the scroll bar inside the border.
|
||||
UIPanelScrollFrameTemplate hangs its bar off this frame's
|
||||
right edge (+6, 16 wide, button art a little wider), so the
|
||||
bar ends at about right + 23. The frame's interior margin is
|
||||
368, where the Close button ends, which puts the widest this
|
||||
can be at 328. Blizzard's own numbers do not carry over here:
|
||||
its ScriptErrorsFrame inherits UIPanelDialogTemplate, a thin
|
||||
GearManager border, not this 32px dialog backdrop. -->
|
||||
<ScrollFrame name="$parentScrollFrame" inherits="UIPanelScrollFrameTemplate">
|
||||
<Size x="343" y="170"/>
|
||||
<Size x="328" y="170"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="16" y="-32"/>
|
||||
@@ -331,7 +348,7 @@
|
||||
</Anchors>
|
||||
<ScrollChild>
|
||||
<EditBox name="$parentText" multiLine="true" letters="4000" autoFocus="false">
|
||||
<Size x="343" y="170"/>
|
||||
<Size x="328" y="170"/>
|
||||
<Scripts>
|
||||
<OnEscapePressed>this:ClearFocus();</OnEscapePressed>
|
||||
<OnEditFocusGained>this:HighlightText(0);</OnEditFocusGained>
|
||||
@@ -437,7 +454,9 @@
|
||||
</EdgeSize>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<!-- ARTWORK, not BACKGROUND: see the same note on
|
||||
EventTraceFrame above. -->
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentTitleBG" file="Interface\QuestFrame\UI-QuestLogTitleHighlight">
|
||||
<Size x="454" y="20"/>
|
||||
<Anchors>
|
||||
|
||||
+22
-4
@@ -2,6 +2,21 @@ cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(ClassicAPI LANGUAGES CXX RC)
|
||||
|
||||
# WoW.exe is x86, so the injected DLL must be x86 too — there is no valid
|
||||
# 64-bit configure of this project. Fail here rather than let one through:
|
||||
# a 64-bit build's first visible symptom is a static_assert deep in a
|
||||
# struct-layout check, which reads as an unrelated code fault. An IDE that
|
||||
# configures on its own (VS Code's CMake Tools picks an architecture from a
|
||||
# scanned kit) can flip an existing build directory this way, which is what
|
||||
# CMakePresets.json's `win32` preset exists to prevent.
|
||||
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
message(FATAL_ERROR
|
||||
"ClassicAPI builds 32-bit only. This configure uses "
|
||||
"${CMAKE_SIZEOF_VOID_P}-byte pointers. Delete the build directory, "
|
||||
"because CMake cannot switch platform in place. Then configure with "
|
||||
"`cmake --preset win32` or `cmake -B build -A Win32`.")
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
|
||||
@@ -125,6 +140,12 @@ configure_file(
|
||||
@ONLY)
|
||||
target_sources(${PROJECT_NAME} PRIVATE "${CLASSICAPI_VERSION_RC}")
|
||||
|
||||
# The .def exports `Load` (VanillaFixes' post-injection entry point)
|
||||
# undecorated so its GetProcAddress(module, "Load") resolves. See the
|
||||
# loader-lock rationale in src/DllMain.cpp. Listed as a source so CMake emits
|
||||
# a quoted /DEF: on every generator and relinks when the file changes.
|
||||
target_sources(${PROJECT_NAME} PRIVATE src/ClassicAPI.def)
|
||||
|
||||
# ole32: COM (CoInitializeEx / CoCreateInstance / CoTaskMemFree) for the
|
||||
# SAPI text-to-speech backend in src/voicechat/Tts.cpp. The SAPI GUIDs are
|
||||
# instantiated in that TU via <initguid.h>, so no sapi.lib is needed.
|
||||
@@ -163,8 +184,5 @@ set(CMAKE_SHARED_LINKER_FLAGS
|
||||
# identical-COMDAT fold); they are listed explicitly below so they stay ON
|
||||
# and the shipped DLL does not grow. (/OPT:ICF folds identical functions, so
|
||||
# a few map symbols share an address — harmless.)
|
||||
# /DEF exports `Load` (VanillaFixes' post-injection entry point) undecorated
|
||||
# so its GetProcAddress(module, "Load") resolves. See src/ClassicAPI.def and
|
||||
# the loader-lock rationale in src/DllMain.cpp.
|
||||
"/OPT:REF /OPT:ICF /MERGE:.rdata=.text /MERGE:.pdata=.text /SECTION:.text,EWR /NODEFAULTLIB:MSVCRT /MAP /DEBUG /DEF:${CMAKE_CURRENT_SOURCE_DIR}/src/ClassicAPI.def"
|
||||
"/OPT:REF /OPT:ICF /MERGE:.rdata=.text /MERGE:.pdata=.text /SECTION:.text,EWR /NODEFAULTLIB:MSVCRT /MAP /DEBUG"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": 3,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 21,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "win32",
|
||||
"displayName": "Win32 (x86)",
|
||||
"description": "The only supported configuration: WoW.exe is 32-bit, so the DLL must be 32-bit too. No generator is named, so CMake picks the newest installed Visual Studio, matching the documented command line.",
|
||||
"binaryDir": "${sourceDir}/build",
|
||||
"architecture": "Win32"
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "release",
|
||||
"displayName": "Release",
|
||||
"configurePreset": "win32",
|
||||
"configuration": "Release"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -22,19 +22,30 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| [Syntax](docs/API.md#lua-51-syntax) | Compiles the Lua 5.1 length (`#`), modulo (`%`), `...`-expression, `0x` hex-literal, and leveled long bracket (`[=[ ]=]`) syntax that vanilla's Lua 5.0 rejects, by rewriting addon source before it compiles. Each addon file also receives its `(name, table)` through `...` (`local name, tbl = ...`). |
|
||||
| [Upvalue limit](docs/API.md#upvalue-limit) | A function can use up to 60 upvalues, the outer locals it refers to, matching Lua 5.1. Lua 5.0 stops at 32 and rejects the whole file, so a large handler that reads many file-level locals can be valid 5.1 and still fail to load here. Nothing changes for a function within 32. |
|
||||
| [`for` loop variables](docs/API.md#for-loop-variables-per-pass) | Each pass of a `for` loop has its own loop variables, as in Lua 5.1, so a function made in the loop keeps that pass's values. On Lua 5.0 every such function sees the loop's last value, which is `nil` for a `for ... in` loop. |
|
||||
| [String methods](docs/API.md#string-methods-supper-sformat) | Every string value accepts method calls — `("asd"):upper()`, `("%d gold"):format(n)`, `msg:match("^!(%w+)")` — resolving through the `string` table, the way Lua 5.1 works. Works on literals and variables, in-world and on the login screen, and inside coroutines. |
|
||||
| [Script-handler arguments](docs/API.md#setmodernscriptargsenable--getmodernscriptargs) | Frame-script handlers receive their values as positional arguments — `OnMouseWheel(self, delta)`, `OnClick(self, button)`, `OnEvent(self, event, ...)`, etc. — so modern addon ports work unmodified. The `this` / `arg1` globals stay set. A handler that declares no parameters is unaffected. A handler that declared a parameter and expected nil now receives its real value. On by default. `SetModernScriptArgs(false)` restores exact vanilla dispatch. |
|
||||
| [XML script bodies](docs/API.md#xml-script-bodies-see-self-and-their-arguments) | An XML script body written for later clients, such as `<OnLoad>self:Init()</OnLoad>`, sees `self` and its script's arguments by name (`elapsed`, `button`, ...). Bodies that read `this` / `arg1` compile as before. |
|
||||
|
||||
### Modern client behaviors
|
||||
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| Tooltip line cap | Lifts `GameTooltip`'s hard 30-line limit to 60 for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). Stat-heavy tooltips and comparison blocks (e.g. pfUI's eqcompare) no longer have their extra lines silently dropped. Done in pure C++ by growing the engine's FontString pool at tooltip-creation time. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, and a button's `<NormalFont style="..."/>` sets its font. |
|
||||
| [Macro conditions](docs/API.md#more-commands-with-conditions) | `[conditions]` and `@unit` work on 38 slash commands, from `/cast` and `/use` to `/target`, `/follow`, `/cancelaura` and the `/pet` family. With `/cast [@mouseover,harm][] Fireball`, the spell goes to the mouseover unit. With no mouseover unit, it goes to your target. `@cursor` places a ground-target spell or item under the mouse and `@player` places it at your feet. A character name works where a unit token does (`[target=Feral]`). Addons read the same rules through `SecureCmdOptionParse`. |
|
||||
| [`#showtooltip`](docs/API.md#showtooltip-and-show) | A macro button shows what the macro does. The icon, tooltip, cooldown, range, usable state and auto-repeat glow all follow the spell or item that the directive resolves to, conditions included. They update as the conditions change. The result goes into the engine field that action-bar addons already read, so they need no code of their own. An addon with its own macro parser can publish its result through `C_Macro.SetMacroDisplay`. It then does not need to replace the action globals. |
|
||||
| [Event-driven nameplates](docs/API.md#nameplate) | The modern `C_NamePlate` API, driven by real events. `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` fire as plates appear and vanish, `nameplate1`..`nameplateN` tokens resolve with every `UnitX` function (and fire `UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "nameplateN"`), and `C_NamePlate.GetNamePlateForUnit` / `GetNamePlates` hand back the live frames. |
|
||||
| [Focus target](docs/API.md#focus) | A sticky focus unit. `FocusUnit("target")` pins it and `ClearFocus()` drops it, with `PLAYER_FOCUS_CHANGED` on every change. The `focus` / `focustarget` tokens resolve with every `UnitX` function and fire unit events (`UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "focus"`), and the predefined `FOCUSTARGET` / `TARGETFOCUS` keybinds are ready to set. |
|
||||
| [Retail-like `/reload`](docs/API.md#reload-picks-up-new-addons-and-new-files) | `/reload` picks up addon changes made while the game runs. A new folder under `Interface\AddOns\` registers and loads as a normal addon, new files added to an existing addon's TOC load, a newly installed addon's first SavedVariables save survives `/reload`, `##` metadata edits (`## SavedVariables:`, `## Dependencies:`, `## Title:`, …) take effect, and a deleted addon folder drops from the addon list. |
|
||||
| [Multi-flavor & conditional TOC](docs/API.md#conditional-and-multi-flavor-toc-loading) | Loads modern multi-flavor addons that ship one folder. Selects a version-specific TOC (`<Name>_ClassicAPI.toc` or `<Name>_Turtle.toc`) and the matching keybinding file (`Bindings_ClassicAPI.xml` / `Bindings_Turtle.xml`), accepts a comma-separated `## Interface:` version list (compatible when it includes the client version `11200`), and honors per-line `[AllowLoadGameType]` / `[AllowLoadTextLocale]` conditions and `[Family]` / `[Game]` / `[TextLocale]` path variables inside a TOC. |
|
||||
| [Launch switches](docs/API.md#launch) | Options you add to the shortcut that starts the game. `-config <name>` reads and writes `WTF\<name>` in place of `WTF\Config.wtf`, so one install can hold several settings profiles. `-gluescript` and `-gluescriptFile` run Lua at the login and character-select screens, every time those screens appear. `-gamescript` and `-gamescriptFile` run it one time, after you enter the world. Each script runs after the interface has loaded, so it can call interface functions and use frames. |
|
||||
| [Any texture size](docs/API.md#texture-size-and-shape) | Textures load at any size and any shape. Width and height do not need to be powers of two, and there is no fixed upper limit. The only limit is the maximum texture size of your graphics card. This applies to every texture path: `SetTexture`, sprite sheets, inline textures, tooltips, and masks. The client allocates memory for a large texture only when an addon loads one, so small textures cost nothing extra. |
|
||||
|
||||
Smaller behavior fixes, such as frame alpha and `SetParent` frame levels, are
|
||||
in **[docs/API.md](docs/API.md)** with the API that they change.
|
||||
|
||||
## Full API reference
|
||||
|
||||
@@ -48,62 +59,68 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded`, `C_AddOns.LoadAddOn` |
|
||||
| [APIDocumentation](docs/API.md#apidocumentation) | `/classicapi` in-game browser, `C_APIDocumentation.GetSystem`, `C_APIDocumentation.GetSystems` |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction`, `ActionButtonUseKeyDown` (cvar) |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [ClassColor](docs/API.md#classcolor) | `C_ClassColor.GetClassColor` |
|
||||
| [ColorUtil](docs/API.md#colorutil) | `C_ColorUtil.ConvertRGBToHSV`, `C_ColorUtil.ConvertHSVToRGB`, `C_ColorUtil.ConvertHSVToHSL`, `C_ColorUtil.ConvertHSLToHSV`, `C_ColorUtil.ConvertHSLToRGB`, `C_ColorUtil.GenerateTextColorCode`, `C_ColorUtil.WrapTextInColor`, `C_ColorUtil.WrapTextInColorCode` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown`, `StartAttack`, `StopAttack` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
|
||||
| [Console](docs/API.md#console) | `CalculateStringEditDistance`, `ConsoleEcho`, `ConsoleExec`, `ConsoleGetAllCommands`, `ConsoleGetColorFromType`, `ConsoleGetFontHeight`, `ConsoleIsActive`, `ConsolePrintAllMatchingCommands`, `SetConsoleKey` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVarBitfield`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarInfo`, `C_CVar.RemoveTempCVar`, `C_CVar.SetCVarBitfield`, `C_CVar.SetTempCVar`, `GetCVar`, `GetCVarDefault` |
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
|
||||
| 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` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByID`, `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.IsFactionActive`, `C_Reputation.IsFactionActiveByID`, `C_Reputation.IsFactionInactive`, `C_Reputation.SetFactionActive`, `C_Reputation.SetFactionActiveByID`, `C_Reputation.SetFactionInactive`, `C_Reputation.SetFactionInactiveByID`, `C_Reputation.SetSelectedFaction`, `C_Reputation.SetSelectedFactionByID`, `C_Reputation.SetWatchedFactionByID`, `C_Reputation.ToggleFactionAtWar`, `C_Reputation.ToggleFactionAtWarByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `fontstring:GetStringHeight`, `fontstring:GetUnboundedStringWidth`, `fontstring:GetWrappedWidth`, `fontstring:GetNumLines`, `fontstring:GetLineHeight`, `fontstring:IsTruncated`, `fontstring:SetMaxLines`, `fontstring:GetMaxLines`, `fontstring:SetFormattedText`, `texture:SetRotation`, `texture:GetRotation`, `texture:SetVertexOffset`, `texture:GetVertexOffset`, `texture:SetColorTexture`, `texture:SetMask`, `frame:CreateMaskTexture`, `texture:AddMaskTexture`, `texture:RemoveMaskTexture`, `texture:GetNumMaskTextures`, `texture:GetMaskTexture`, `fontstring:SetRotation`, `fontstring:GetRotation`, `editBox:SetCursorPosition`, `editBox:GetCursorPosition`, `editBox:GetUTF8CursorPosition`, `editBox:ClearHighlightText`, `editBox:HasFocus`, `editBox:HasText`, `editBox:SetHighlightColor`, `editBox:GetHighlightColor`, `editBox:ClearHistory`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs`, `PreClick` (script), `PostClick` (script), `GetClickFrame` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `CreateFrame("Cooldown")`, `cooldown:SetCooldown`, `cooldown:SetReverse`, `cooldown:GetReverse`, `cooldown:SetDrawEdge`, `cooldown:GetDrawEdge`, `frame:SetShown`, `frame:SetClampRectInsets`, `frame:GetClampRectInsets`, `smf:SetHyperlinksEnabled`, `smf:GetHyperlinksEnabled`, `editBox:SetCountInvisibleLetters`, `editBox:IsCountInvisibleLetters`, `fontstring:GetStringHeight`, `fontstring:GetUnboundedStringWidth`, `fontstring:GetWrappedWidth`, `fontstring:GetNumLines`, `fontstring:GetLineHeight`, `fontstring:IsTruncated`, `fontstring:SetMaxLines`, `fontstring:GetMaxLines`, `fontstring:SetFormattedText`, `texture:SetRotation`, `texture:GetRotation`, `texture:SetVertexOffset`, `texture:GetVertexOffset`, `texture:SetColorTexture`, `texture:SetAtlas`, `texture:GetAtlas`, `texture:ResetTexCoord`, `texture:SetSpriteSheetCell`, `texture:SetDesaturation`, `texture:GetDesaturation`, `texture:SetMask`, `frame:CreateMaskTexture`, `texture:AddMaskTexture`, `texture:RemoveMaskTexture`, `texture:GetNumMaskTextures`, `texture:GetMaskTexture`, `fontstring:SetRotation`, `fontstring:GetRotation`, `editBox:SetCursorPosition`, `editBox:GetCursorPosition`, `editBox:GetUTF8CursorPosition`, `editBox:ClearHighlightText`, `editBox:HasFocus`, `editBox:HasText`, `editBox:SetHighlightColor`, `editBox:GetHighlightColor`, `editBox:ClearHistory`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsProtected`, `button:SetNormalFontObject`, `button:GetNormalFontObject`, `frame:IsEventRegistered`, `frame:RegisterUnitEvent`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs`, `PreClick` (script), `PostClick` (script), `button:RegisterForClicks("AnyUp"/"AnyDown")`, `GetClickFrame` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject` (scripts) |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:AddTexture`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnit`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetFrameStack`, `GameTooltip:SetHyperlink` (full links, `spell:`, `quest:`), `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject`, `OnTooltipSetFrameStack` (scripts) |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemClassInfo`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemClassInfo`, `GetItemIcon`, `GetItemSubClassInfo`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo`, `IsActiveBattlefieldArena` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemClassInfo`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemTempEnchantInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemClassInfo`, `GetItemIcon`, `GetItemSubClassInfo`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `string.gmatch`, `string.gsub` (table replacement), `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `StopMacro` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |
|
||||
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [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` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetInfo`, `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestLogTitle`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Sound](docs/API.md#sound) | `C_Sound.GetRecentSoundFiles`, `C_Sound.GetSoundScaledVolume`, `C_Sound.IsPlaying`, `C_Sound.PlayItemSound`, `C_Sound.PlaySound`, `C_Sound.PlaySoundWithOptions`, `C_Sound.PlayVocalErrorSound`, `MuteSoundFile`, `PlaySound`, `UnmuteSoundFile` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.ContainsAnyDisenchantSpell`, `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetNumSpellBookSkillLines`, `C_SpellBook.GetPlayerSpellsByAura`, `C_SpellBook.GetSkillLineIndexByID`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemCastCount`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownDuration`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownInfo`, `C_SpellBook.GetSpellBookItemSkillLineIndex`, `C_SpellBook.GetSpellBookSkillLineInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsClassTalentSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `GetSheathState`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsOutdoors`, `IsStealthed`, `IsSwimming` |
|
||||
| [SwingTimer](docs/API.md#swingtimer) | `C_SwingTimer.EnableRangeCheck`, `C_SwingTimer.IsTargetWithinSwingRange` |
|
||||
| [System](docs/API.md#system) | `CopyToClipboard`, `GetPhysicalScreenSize` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetDirectionFinished`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes`, `C_TaxiMap.GetTaxiPaths`, `C_TaxiMap.GetTaxiPathWaypoints`, `C_TaxiMap.GetTaxiRoute` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTimeCached` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [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` |
|
||||
@@ -124,7 +141,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
|-------|-------|
|
||||
| [Account](docs/API.md#account) | `SaveAccount`, `DeleteAccount`, `GetSavedAccounts`, `LoginWithSavedAccount` (passwords encrypted in Windows Credential Manager, scoped per realmlist; plaintext never returned to Lua) |
|
||||
| [CharacterList](docs/API.md#characterlist) | `GetSavedCharacterOrder`, `SetSavedCharacterOrder` (persist to `WTF\Account\...\ClassicAPI.txt`) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| CVar | `GetCVar`, `SetCVar`, `RegisterCVar`, `GetCVarDefault`, `C_CVar.GetCVarBool`, `C_CVar.SetTempCVar`, `C_CVar.RemoveTempCVar` (storage is process-global — writes from glue are visible in-world and vice versa) |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsFirstLoadThisSession`, `C_Glue.IsOnGlueScreen` |
|
||||
| Script | `RunScript` (compile and run a Lua chunk in the glue state's globals — useful for slash-command-style helpers in GlueXML) |
|
||||
| State | `IsLoggedIn` |
|
||||
@@ -132,17 +149,42 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Macros</b> — engine-level parsing extensions</summary>
|
||||
<summary><b>Macros</b> — forms a macro body accepts</summary>
|
||||
|
||||
Engine-level extensions to macro parsing and dispatch — no new Lua
|
||||
functions. See the
|
||||
[Macros section in the Lua reference](docs/API.md#macros) for details.
|
||||
The DLL parses these forms, so an action-bar addon needs no code for them.
|
||||
See the [Macros section in the Lua reference](docs/API.md#macros) for
|
||||
details.
|
||||
|
||||
| Form | What it does |
|
||||
|------|--------------|
|
||||
| `/cast <spellID>` | `/cast 5019` casts Shoot if known; macro slot tags correctly for action-bar UI |
|
||||
| `CastSpellByName("<spellID>")` | Same — numeric strings resolve through the engine's name resolver |
|
||||
| `CastSpellNoToggle("<name>")` in a macro | Engine's macro parser now recognizes it as a primary-spell line, so the macro slot in an addon like pfUI highlights when its spell is auto-repeating or its self-aura is active |
|
||||
| `#showtooltip [conditions] Value` | First line of a macro. Sets what the button shows. Bare `#showtooltip` follows the first `/cast` or `/use` line instead. `#show` is the same form. |
|
||||
| `[conditions]` and `@unit` | Accepted by 42 commands. The slash-command list below names them. |
|
||||
| `!Name` | Starts an ability but never turns it off. Use it for auto-repeat shots and for the self-buffs (stance, aspect, seal, form, tracking). |
|
||||
| A line that starts with `#` | A comment. The line never reaches chat. |
|
||||
| `/cast <spellID>` | For a spell you know, `/cast 5019` casts it by ID. The macro slot also tags correctly for an action-bar addon. |
|
||||
| `CastSpellByName("<spellID>")` | A numeric string resolves through the engine name resolver, the same as `/cast`. |
|
||||
| `CastSpellNoToggle("<name>")` in a macro | The engine macro parser reads this as a primary-spell line. The macro slot in an addon such as pfUI then highlights while the spell is on auto-repeat or its self-aura is active. |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Slash commands</b> — <code>[conditions]</code> on 42 commands</summary>
|
||||
|
||||
The bundled addon registers these commands. Each one takes the same
|
||||
`[conditions]` and `@unit` syntax as `/cast`. Each client language has its
|
||||
own command names next to the English ones. See
|
||||
[More commands with `[conditions]`](docs/API.md#more-commands-with-conditions)
|
||||
for what each command does, and
|
||||
[`/castsequence`](docs/API.md#castsequence) and
|
||||
[`/castrandom`](docs/API.md#castrandom-and-userandom) for the list forms.
|
||||
|
||||
| Group | Commands |
|
||||
|-------|----------|
|
||||
| Casting | `/cast`, `/use`, `/castsequence`, `/castrandom`, `/userandom`, `/stopcasting`, `/stopmacro`, `/cancelaura`, `/cancelform`, `/dismount` |
|
||||
| Targeting | `/target`, `/targetexact`, `/cleartarget`, `/targetlasttarget`, `/targetlastenemy`, `/targetenemy`, `/targetfriend`, `/targetenemyplayer`, `/targetfriendplayer`, `/targetparty`, `/targetraid`, `/assist`, `/follow`, `/focus`, `/clearfocus`, `/startattack`, `/stopattack` |
|
||||
| Equipment | `/equip`, `/equipslot`, `/equipset` |
|
||||
| Action bars | `/changeactionbar`, `/swapactionbar`, `/click` |
|
||||
| Pet | `/petattack`, `/petfollow`, `/petstay`, `/petpassive`, `/petdefensive`, `/petaggressive`, `/petautocaston`, `/petautocastoff`, `/petautocasttoggle` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -158,6 +200,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `ExportInterfaceFiles code` | Extracts Blizzard's UI source (`.lua`/`.xml`/`.toc`/`.xsd`) from the MPQs to `BlizzardInterfaceCode\` |
|
||||
| `ExportInterfaceFiles art` | Extracts Blizzard's UI art (`.blp`/`.tga`) from the MPQs to `BlizzardInterfaceArt\` |
|
||||
| `ExportDBCFiles` | Extracts the client's `.dbc` tables from the MPQs to `DBFilesClient\` |
|
||||
| `ExportSoundFiles [subpath]` | Extracts the client's sound files from the MPQs to `BlizzardSound\`; the optional subpath narrows it (the full tree is ~1 GB) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -168,6 +211,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|-------|---------|
|
||||
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
|
||||
| `BAG_UPDATE_DELAYED` | *(none)* |
|
||||
| `CURSOR_CHANGED` | `isDefault, newCursorType, oldCursorType, oldCursorVirtualID` |
|
||||
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
|
||||
| `EQUIPMENT_SWAP_PENDING` | `setID` |
|
||||
| `EQUIPMENT_SWAP_FINISHED` | `success, setID` |
|
||||
@@ -176,6 +220,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `GLOBAL_MOUSE_UP` | `button` |
|
||||
| `HEARTHSTONE_BOUND` | *(none)* |
|
||||
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
|
||||
| `LEARNED_SPELL_IN_SKILL_LINE` | `spellID, skillLineIndex, isGuildPerkSpell` |
|
||||
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex, playerIndex` |
|
||||
| `LOOT_HISTORY_ROLL_COMPLETE` | `itemIndex` |
|
||||
| `LOOT_SCAN_COMPLETED` | *(none)* |
|
||||
@@ -193,11 +238,14 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `PLAYER_STOPPED_MOVING` | *(none)* |
|
||||
| `PLAYER_STARTED_TURNING` | *(none)* |
|
||||
| `PLAYER_STOPPED_TURNING` | *(none)* |
|
||||
| `PLAYER_SWING` | `swingDuration, swingType` |
|
||||
| `PLAYER_SWING_RANGE_UPDATE` | `swingType, isInRange, checksRange` |
|
||||
| `PLAYER_TOTEM_UPDATE` | `totemSlot` |
|
||||
| `QUEST_ACCEPTED` | `questLogIndex, questID` |
|
||||
| `QUEST_DATA_LOAD_RESULT` | `questID, success` |
|
||||
| `QUEST_REMOVED` | `questID` |
|
||||
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
|
||||
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
|
||||
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_START` | `unit, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_STOP` | `unit, castGUID, spellID, spellName, rank` |
|
||||
@@ -213,10 +261,13 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `UNIT_SPELLCAST_RETICLE_CLEAR` | `"player", "", spellID, spellName, rank` |
|
||||
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
|
||||
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
|
||||
| `USER_WAYPOINT_UPDATED` | *(none)* |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_STARTED` | `numConsumers, utteranceID, durationMS, destination` |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FINISHED` | `numConsumers, utteranceID, destination` |
|
||||
| `VOICE_CHAT_TTS_PLAYBACK_FAILED` | `status, utteranceID, destination` |
|
||||
| `VOICE_CHAT_TTS_VOICES_UPDATE` | *(none)* |
|
||||
| `WEAPON_SLOT_CHANGED` | *(none)* |
|
||||
| `WEAR_EQUIPMENT_SET` | `setID` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -234,8 +285,10 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Inventory type | `Enum.InventoryType.Index*Type` (0–34, e.g. `IndexHeadType`=1 … `IndexRelicType`=28) |
|
||||
| Item class | `Enum.ItemClass.{Consumable,Container,Weapon,Gem,Armor,Reagent,Projectile,Tradegoods,ItemEnhancement,Recipe,Quiver,Questitem,Key,Miscellaneous,…}` (0–19) |
|
||||
| Item quality | `Enum.ItemQuality.{Poor,Common,Uncommon,Rare,Epic,Legendary,Artifact}` (0–6) |
|
||||
| Cursor type | `Enum.UICursorType.{Default,Item,Money,Spell,PetAction,Merchant,Macro,Pet,…}` (0–20; this client yields 0–5, 7 and 9) |
|
||||
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
|
||||
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
|
||||
| Player swing type | `Enum.PlayerSwingType.{MainHand,OffHand,Ranged}` (0–2) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -301,7 +354,9 @@ functions but that consumer code still expects to find as globals —
|
||||
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
|
||||
`Item` / `ItemLocation`, `MathUtil`(`Lerp` / `Clamp` / `CreateCounter`),
|
||||
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), and `EventUtil`
|
||||
(`ContinueOnAddOnLoaded` etc.).
|
||||
(`ContinueOnAddOnLoaded` and more). It also holds the conditional slash
|
||||
commands and `SecureCmdOptionParse`, the parser that reads `[conditions]`
|
||||
and `@unit` for them.
|
||||
|
||||
**You don't have to install the addon manually** — the DLL embeds
|
||||
the contents of [`AddOns/!!!ClassicAPI/`](AddOns/!!!ClassicAPI/) and
|
||||
|
||||
@@ -951,88 +951,15 @@ See [src/unit/Tooltip.cpp](src/unit/Tooltip.cpp) and the
|
||||
`FUN_SCRIPT_GAMETOOLTIP_SET_UNIT_*` block in
|
||||
[src/Offsets.h](src/Offsets.h).
|
||||
|
||||
## 46. `GameTooltip:SetFrameStack([showHidden])` — deferred (Lua-side fix in place)
|
||||
## ~~46. `GameTooltip:SetFrameStack([showHidden])`~~ — DONE
|
||||
|
||||
Renders a tooltip listing the chain of frames at the mouse cursor,
|
||||
sorted by strata + level. The 3.0+ debug-tooling primitive used by
|
||||
Blizzard's `Blizzard_DebugTools` and every layout-debugging addon.
|
||||
|
||||
### Current state in this project
|
||||
|
||||
Our bundled `DebugTools/` backport ships its own `FrameStackTooltip`
|
||||
in [DebugTools/DebugTools.lua](DebugTools/DebugTools.lua), driven
|
||||
by `_CollectFramesUnderMouse`. As of the EnumerateFrames switch,
|
||||
the iterator walks the engine's own frame list — fast (typically
|
||||
~hundreds of entries) and includes anonymous frames the engine
|
||||
sees but a `_G` walk would miss.
|
||||
|
||||
What's still missing relative to the real engine `SetFrameStack`:
|
||||
|
||||
- **Anonymous-frame display name.** We currently show
|
||||
`<anonymous>` for frames where `frame:GetName()` returns nil.
|
||||
The engine's method shows the frame's pointer + the Lua-side
|
||||
registry-table address. To get there we'd need a Lua-callable
|
||||
shim that exposes a numeric handle per frame (vftable + Lua-ref
|
||||
lookup), which is non-trivial.
|
||||
- **Single-tooltip semantics.** Modern addons can call
|
||||
`GameTooltip:SetFrameStack(...)` directly on any tooltip;
|
||||
ours requires the `FrameStackTooltip` we ship in `DebugTools/`.
|
||||
Most consumers (`Blizzard_DebugTools` ports, layout addons via
|
||||
`/framestack`) don't care, but a strict polyfill would.
|
||||
|
||||
### C++ port from 3.3.5 — kept as a research note
|
||||
|
||||
What `Script_GameTooltip_SetFrameStack`
|
||||
actually does in 3.3.5 (Frostmourne client, registration entry at
|
||||
VA 0x00AD2CF0 → function VA 0x00626560 → inner builder
|
||||
`FUN_006230d0`):
|
||||
|
||||
- Pulls cursor (in frame space) via `FUN_0047BFF0` from a render-
|
||||
state global at `[DAT_00B499A8 + 0x1224 / +0x1228]`.
|
||||
- Walks an intrusive linked list at `[DAT_00B499A8 + 0xCD4]`. Each
|
||||
frame node reads:
|
||||
- `[+0x00]` — vftable; `vftable[+0x10]` = "is the frame visible
|
||||
enough to hit-test" predicate.
|
||||
- `[+0x0E0]` (`node[0x38]`) = strata; gates `(strata != 0 ||
|
||||
showHidden)`.
|
||||
- `[+0x0D0]..[+0x0DC]` (`node[0x34..0x37]`) = frame rect for
|
||||
hit-test.
|
||||
- `[+0x0D8]` (`node[0x36]`) flags, bit 2 → visible.
|
||||
- `[+0x288]` (`node[0xA2]`) = next pointer.
|
||||
- Per-frame name via `vftable[+0x04]` (a `GetDebugName()`-style
|
||||
virtual). If the frame is Lua-side (`vftable[+0x10]` returns
|
||||
type code 5), an extra `lua_rawgeti(L, REGISTRY, frame->luaRef)`
|
||||
pulls the Lua-table reference for the `"%s: %p (%s)"` format;
|
||||
otherwise it's `"%s: %p"`.
|
||||
- Builds into a scratch sort array at `DAT_00C5D370` (entry stride
|
||||
`0x410`), `qsort` against comparator `FUN_0061A5B0`.
|
||||
- Header line via `FUN_00819D40("DEBUG_FRAMESTACK", ...)` (Lua
|
||||
`GetGlobalString`) then per-entry `FUN_0061FEC0` (tooltip
|
||||
`AddLine` equivalent, takes color codes from a 4-byte-stride
|
||||
table at `0xAD2DC0` / `0xAD2DB8` / `0xAD2DB0` for
|
||||
visible/hidden/special states).
|
||||
|
||||
For a 1.12 port we'd need to derive:
|
||||
|
||||
- The frame-manager global (3.3.5's `DAT_00B499A8` — different VA
|
||||
in 1.12, different field offsets likely).
|
||||
- Frame struct offsets for rect, strata, flags, next, name vftable
|
||||
slot — the layout shifted between 1.12 and 3.3.5 (same way unit
|
||||
field offsets did, per the `OFF_UNIT_FIELD_HEALTH` discovery in
|
||||
CLAUDE.md).
|
||||
- AddTooltipLine engine address (1.12 has it — every
|
||||
`GameTooltip:SetX` method calls it internally).
|
||||
- Lua-side reference lookup helper (whatever 1.12 uses to map
|
||||
frame → Lua table; the `FrameScript_GetObject` chain at
|
||||
`0x6F3740` is part of this).
|
||||
|
||||
### Recommendation
|
||||
|
||||
The Lua-side iterator switch is done. The C++ port stays deferred
|
||||
unless we need the engine frame list for something else
|
||||
(`GetMouseFocus` polyfill, anonymous-frame inspector with extra
|
||||
fields beyond what Lua exposes, single-tooltip strict polyfill,
|
||||
etc.) that would amortize the RE cost.
|
||||
A mirror of 4.3.4's CGTooltip::SetFrameStack in
|
||||
[src/tooltip/FrameStack.cpp](src/tooltip/FrameStack.cpp), plus the
|
||||
`OnTooltipSetFrameStack` script in Tooltip::SetEvents. Every input exists in
|
||||
1.12: the CSimpleTop frame list, the NDC cursor + NDCToDDC, CLayoutFrame
|
||||
GetRect, the frame's visible / enabled-input / strata / level fields,
|
||||
FrameStrataToString and the two default tooltip colors (all in Offsets.h).
|
||||
The `/fstack` UI itself is addon Lua and can now call the method.
|
||||
|
||||
## ~~47. `IsPlayerSpell(spellID)`~~ — DONE
|
||||
|
||||
@@ -1120,51 +1047,50 @@ spell-API functions use. Returns `(1, nil)` / `(nil, 1)` /
|
||||
`(nil, nil)` per legacy convention. `C_Spell.IsSpellUsable` is the
|
||||
modern shape — same logic, returns proper booleans.
|
||||
|
||||
**Two engine-cache approaches investigated and discarded:**
|
||||
**Reworked for issue #51 (2026-09-12) — it now IS the engine's
|
||||
verdict.** `IsUsableAction` reads the per-slot cache
|
||||
(`0x00BC6B60` usable / `0x00BC67A0` noMana, reader `FUN_004E5230`),
|
||||
which the recompute `FUN_004E5050` fills per slot. For a player spell
|
||||
slot that recompute is one call: `FUN_SPELL_IS_USABLE(record,
|
||||
&noMana)` (`0x006E3D60`). For a pet spell slot it is `FUN_PET_ACTIONS_
|
||||
USABLE()` then the pet's power vs `FUN_GET_SPELL_COST(spellID, 0)`.
|
||||
`Spell::Usable` now runs exactly those two paths live (plus a
|
||||
knowledge gate on `VAR_PLAYER_SPELL_BITMAP`, since the helper is also
|
||||
fed item on-use spells and never checks knowledge). Verified check
|
||||
list of the helper is on `FUN_SPELL_IS_USABLE` in `Offsets.h`.
|
||||
|
||||
**The first version hand-rolled five checks instead** (known, alive,
|
||||
off cooldown, power, reagents) and diverged from `IsUsableAction`
|
||||
exactly where the helper does more or less — the issue #51 repro
|
||||
(Warrior, Whirlwind):
|
||||
|
||||
| State | `IsUsableAction` | old `IsUsableSpell` |
|
||||
|---|---|---|
|
||||
| Berserker, 0 rage, on CD | `nil, 1` | `nil, nil` (we tested cooldown first — the engine never does) |
|
||||
| Battle Stance, 25 rage | `nil, nil` | `1, nil` (we never tested stance) |
|
||||
|
||||
Cooldown is deliberately NOT part of usability: FrameXML greys
|
||||
(`IsUsableAction`) and swipes (`GetActionCooldown`) as two separate
|
||||
states, and the engine helper has no cooldown branch. Rule #1 case:
|
||||
`Item::Usable` had been calling the helper all along.
|
||||
|
||||
**Two engine-cache approaches investigated and discarded before the
|
||||
first version** (still true, kept for the record):
|
||||
|
||||
1. **Per-spell cache at `0x004F0F40` returning fields at +0x564
|
||||
/ +0x568** — initially looked promising (the action-bar
|
||||
dispatcher at `0x004E5BA0` reads these), but the writers
|
||||
(`0x004EFF17` cluster) showed they're parsed config integers,
|
||||
not real-time usability bools. False trail.
|
||||
not real-time usability bools. False trail. (Later identified as
|
||||
the macro entry's cached primary spell — see the `#showtooltip`
|
||||
notes.)
|
||||
2. **Action-bar per-slot caches at `0x00BC67A0` (noMana) and
|
||||
`0x00BC6B60` (usable)** — these ARE real-time, but only updated
|
||||
for the 120 action-bar slots. Spells off the bar stay
|
||||
uninitialized. Action-bar-only is the wrong abstraction since
|
||||
modern `IsUsableSpell(spellID)` is action-bar-agnostic.
|
||||
|
||||
Settled on a **manual five-check approach**:
|
||||
1. Player knows the spell (`VAR_PLAYER_SPELL_BITMAP` lookup —
|
||||
covers profession recipes, talents, racials, etc.).
|
||||
2. Player is alive (`HEALTH > 0`).
|
||||
3. Spell is not on cooldown (`FUN_SPELL_QUERY_COOLDOWN` —
|
||||
the engine helper `Script_GetSpellCooldown` calls internally
|
||||
after slot resolution, queried with `bookType=0` for player).
|
||||
4. Player has ≥ `ManaCost` of the spell's `PowerType` —
|
||||
only this failure flips `noMana=true`.
|
||||
5. Player has all required reagents in bags
|
||||
(`Spell.dbc` Reagent[8] / ReagentCount[8] at `+0x110` /
|
||||
`+0x130`, walked via `Item::Location::ResolveBag`).
|
||||
|
||||
**Not checked** (deliberate, different concerns): silence, GCD,
|
||||
stance/form, range, target type, line-of-sight, casting state.
|
||||
|
||||
**Verified on Turtle WoW:**
|
||||
- Mana check: Renew rank 3 (cost 105) reports usable at 144 mana
|
||||
and unusable at 39 mana, transitioning exactly at the cost
|
||||
boundary.
|
||||
- Reagent check: SHIPPED BUT UNVERIFIED — the Reagent[8] /
|
||||
ReagentCount[8] offsets at `+0x110` / `+0x130` are the
|
||||
CMaNGOS-documented vanilla layout, and we know spell records
|
||||
match CMaNGOS docs (PowerType=+0x7C and ManaCost=+0x80 both
|
||||
match), so high confidence — but no in-game verification yet.
|
||||
Should test with a reagent spell (e.g., Mage Teleport spell 3565
|
||||
needs Rune of Teleportation 17031). Failure mode if offsets are
|
||||
wrong: reagent check becomes a no-op (always passes).
|
||||
- Cooldown check: SHIPPED BUT UNVERIFIED in this implementation —
|
||||
the helper signature was traced via `Script_GetSpellCooldown` at
|
||||
`0x004B40A0` but not exercised in-game. Should test with a
|
||||
cooldown'd spell.
|
||||
modern `IsUsableSpell(spellID)` is action-bar-agnostic. The fix
|
||||
above sidesteps this by calling what FILLS the cache, not the
|
||||
cache.
|
||||
|
||||
**Important offset correction along the way.** Initial implementation
|
||||
read POWER1 at descriptor `+0x5C` based on the CMaNGOS-documented
|
||||
@@ -1741,7 +1667,14 @@ is a thin wrapper. If they're inlined, we'd either need to skip
|
||||
the clear ourselves (call after some no-op state setup) or
|
||||
manually call the line-emission primitives.
|
||||
|
||||
## ~~61. `GameTooltip:GetItem()` / `GetSpell()` / `GetUnit()`~~ — `GetItem` + `GetSpell` DONE; `GetUnit` skipped
|
||||
## ~~61. `GameTooltip:GetItem()` / `GetSpell()` / `GetUnit()`~~ — DONE
|
||||
|
||||
`GetUnit` shipped later in [src/unit/Tooltip.cpp](src/unit/Tooltip.cpp):
|
||||
`SetUnit` is re-registered in front of the engine's (no MinHook) to
|
||||
stage the token, and the token binds in the unit-builder co-hook that
|
||||
`Tooltip::SetEvents` already owned. The builder has three callers:
|
||||
SetUnit, the world-mouseover setter (→ `"mouseover"`), and an in-place
|
||||
refresh (keeps the token). The notes below are the original skip.
|
||||
|
||||
Modern query methods that return what the tooltip is currently
|
||||
showing:
|
||||
@@ -1847,10 +1780,17 @@ are supported.
|
||||
|
||||
### Unhookable globals
|
||||
|
||||
Modern WoW rejects `hooksecurefunc("<core lua/secure fn>", ...)` outright
|
||||
to keep callers from clobbering language primitives or breaking taint
|
||||
propagation. We mirror that with a name blacklist in `Script_HookSecureFunc`
|
||||
that fires `lua_error` with `"hooksecurefunc: function is unhookable"`
|
||||
Patch 11.0.0 (The War Within, 2024-07-23) made `hooksecurefunc` refuse a
|
||||
fixed set of names with a "Cannot hook function" error, to keep callers
|
||||
from clobbering language primitives or breaking taint propagation. No
|
||||
earlier client has the restriction (3.3.5, 4.3.4 and 5.4.8 carry no such
|
||||
string), so 11.0's published list is the reference — and it is NOT the
|
||||
engine's base-library table (`0x00811E28`, 36 entries, catalogued in
|
||||
`BlizzardScriptAPI.md` §3): 11.0 leaves `tostring`/`error`/`loadstring`
|
||||
hookable and includes secure-family names that are not base functions. We
|
||||
mirror the 11.0 list verbatim with a name blacklist in
|
||||
`Script_HookSecureFunc` that fires `lua_error` with
|
||||
`"hooksecurefunc: function is unhookable"`
|
||||
when the two-arg form targets one of: `getfenv`, `getmetatable`,
|
||||
`hooksecurefunc`, `ipairs`, `issecurevalue`, `issecurevariable`, `next`,
|
||||
`pairs`, `pcall`, `pcallwithenv`, `rawget`, `rawset`, `scrub`,
|
||||
@@ -3827,9 +3767,10 @@ private namespace table **only** when BOTH hold, else `nil`:
|
||||
boolean flag; approach (a) from the notes below — no parser hook).
|
||||
|
||||
The two paths stay separate, as required:
|
||||
- **Internal** `__addonns` (the `local name, ns = ...` preamble) stays
|
||||
**ungated** — an addon always gets its own namespace. Still in
|
||||
`luasyntax/Transpile.cpp`.
|
||||
- **Internal**: each addon file is called with `(name, ns)` as real
|
||||
arguments, as 4.3.4 does (`Script_RunAddonFile` in
|
||||
`luasyntax/Transpile.cpp`) — **ungated**, an addon always gets its own
|
||||
namespace. (Replaced the `__addonns` global preamble.)
|
||||
- **Public** `C_AddOns.GetAddOnLocalTable` carries the gate, in
|
||||
`addons/LocalTable.cpp`.
|
||||
|
||||
@@ -3854,3 +3795,121 @@ Not shipped by default: no addon opts in yet. The embedded `!!!ClassicAPI`
|
||||
addon could add `## AllowAddOnTableAccess: 1` to its `.toc` to expose its own
|
||||
namespace as a worked example, but that's a product/security decision left to
|
||||
the maintainer.
|
||||
|
||||
## 100. Self-documenting API + `/classicapi` browser — SHIPPED (first slice)
|
||||
|
||||
`docs/API.md` is 18.7k lines and its anchors are signature slugs, so a
|
||||
signature edit breaks the 281 internal + 88 README links into it. Blizzard
|
||||
solved the same problem by shipping the API as DATA
|
||||
(`Blizzard_APIDocumentationGenerated`: one table per system with typed
|
||||
`Functions` / `Events` / `Tables`) plus a browser addon
|
||||
(`Blizzard_APIDocumentation`, the `/api` command). warcraft.wiki.gg's
|
||||
Arguments/Returns blocks are those same tables rendered by a bot — so one set
|
||||
of descriptors feeds the in-game browser, a page generator, and a drift check.
|
||||
|
||||
**Shipped in this round**
|
||||
|
||||
- `namespace Game::Doc` in [src/Game.h](src/Game.h) — `Field` +
|
||||
`Req`/`Opt`/`Vararg`, `FieldList`, `Function`, `Method`, `Structure`,
|
||||
`Event`. Constant-initialized `const` objects; no heap, no dynamic init.
|
||||
- A trailing descriptor argument on all six `Game::Lua::Register*`, and on
|
||||
`Event::Custom::AutoReserve`. Zero changes inside any `Script_*`.
|
||||
- [src/api/Documentation.cpp](src/api/Documentation.cpp) — records every
|
||||
registration on the first pass of each Lua state, derives systems, and
|
||||
exports `C_APIDocumentation.GetSystems()` / `GetSystem(name)` lazily in
|
||||
Blizzard's exact table shape, plus `_classicapi_UndocumentedAPI()`.
|
||||
- `AddOns/!!!ClassicAPI/APIDocumentation/` — the 8-file port of
|
||||
`Blizzard_APIDocumentation`, driven by `/classicapi` (short `/capi`).
|
||||
Named for what it documents: the browser holds only ClassicAPI's own
|
||||
surface, so `/api` would have over-promised.
|
||||
- `Util/LinkUtil.lua` — the `SetItemRef` → `LinkUtil.ProcessLink` dispatch,
|
||||
wiring up a `RegisterLinkHandler` registry that had been dead code. Any
|
||||
future link type now registers a handler instead of stacking another
|
||||
`SetItemRef` override. Also fixed `assertsafe` being undefined there.
|
||||
- Documented: the whole `C_Spell` / `C_SpellBook` surface, the spell globals
|
||||
(`SpellGlobals`), the two SpellBook enums, the 13 `UNIT_SPELLCAST_*`
|
||||
events, `GameTooltipAPI` (frame-method example), and `C_Glue` (glue and
|
||||
mixed-environment example).
|
||||
|
||||
**The remaining sweep.** `_classicapi_UndocumentedAPI()` is the worklist —
|
||||
~700 registrations across ~46 namespaces. Do it a namespace at a time; the
|
||||
descriptor goes beside the registration and must be written against the
|
||||
`Script_*` BODY, not against `docs/API.md` (which is already stale in
|
||||
places — `IsHarmfulSpell` / `IsHelpfulSpell` describe an `AttributesEx` bit
|
||||
read that the code does not do; it walks the effects' implicit targets).
|
||||
`tools/New-ApiDocSkeletons.ps1` emits paste-ready skeletons from the 207
|
||||
single-line `Usage:` strings + the API.md headings, with `// TODO` on every
|
||||
guess — a starting point to verify, never to paste blind.
|
||||
|
||||
When the sweep finishes, delete the `= nullptr` / `= 0` defaults from the six
|
||||
registrar parameters: a registration without a descriptor then fails to
|
||||
compile, which is the whole point of putting the descriptor at the call site.
|
||||
|
||||
**Then, not before:** an `ExportAPIDocumentation` console command (glue-
|
||||
registered like `ExportInterfaceFiles`, writing one Blizzard-format `.lua`
|
||||
per system plus `Undocumented.txt`), a CI diff of the committed copies to
|
||||
catch descriptor drift, and a Markdown/wiki generator to replace the
|
||||
hand-maintained `docs/API.md` — the reason the descriptors exist at all.
|
||||
|
||||
## 101. `Frame:SetOnUpdateMode(mode)` + `Enum.OnUpdateMode` — medium
|
||||
|
||||
```
|
||||
Frame:SetOnUpdateMode(onUpdateMode) -- Enum.OnUpdateMode
|
||||
0 Disabled no OnUpdate, visible or not
|
||||
1 RunWhenVisible the vanilla behaviour (default)
|
||||
2 RunWhenVisibleOnce run once while visible; resets to Disabled BEFORE running
|
||||
3 RunOnce run once regardless of visibility; resets to Disabled BEFORE running
|
||||
4 RunAlways run regardless of visibility
|
||||
```
|
||||
|
||||
"Resets before running" is load-bearing: the handler re-arms by calling
|
||||
`SetOnUpdateMode` again from inside itself.
|
||||
|
||||
**Engine (verified 2026-09-15).** The per-frame dispatcher is `FUN_0076B2C0`,
|
||||
`__thiscall(frame, float elapsed)` at vtable `+0x38`, present in every frame
|
||||
vtable with subclass overrides chaining into it (same shape as the click
|
||||
vmethod `FUN_00779540`):
|
||||
|
||||
```c
|
||||
if (frame[0x128] != 0) // the OnUpdate slot
|
||||
FUN_007026F0(frame, frame + 0x128, "%f", elapsed); // fmt @0x00835160
|
||||
```
|
||||
|
||||
The walk is `FUN_00765650`: nine strata buckets of an intrusive list
|
||||
(`frame+0x310` = next, saved into the bucket before each call so a handler may
|
||||
unlink itself), calling `vtable+0x38` per node — with **no visibility check in
|
||||
the loop and none in the dispatcher**. Visibility is therefore *list
|
||||
membership*, not a test, and that list is the per-strata render list, so a
|
||||
hidden frame is simply absent. Contention: a literal scan of every DLL in
|
||||
`dll\` + `dll_local\` finds no reference to `FUN_0076B2C0` or `FUN_00765650`.
|
||||
|
||||
**Design.** Modes 0/1/2 need only a co-hook on `FUN_0076B2C0` — mode 0 returns
|
||||
without calling the original, mode 2 sets Disabled then calls it, mode 1 is the
|
||||
engine unchanged. Guard the detour on an empty mode map so non-users pay a
|
||||
compare.
|
||||
|
||||
Modes 3/4 cannot come from that hook: the frame is not in the walk's list while
|
||||
hidden, and forcing membership would put it in the RENDER list (it would try to
|
||||
draw). Drive them from `Tick::WorldTick` instead — an already-owned hook, so no
|
||||
second MinHook — calling the engine's own `FUN_0076B2C0(frame, elapsed)` so the
|
||||
handler sees exactly the fire it normally would, with a per-tick mark set by the
|
||||
hook so a *visible* `RunAlways` frame is not dispatched twice.
|
||||
|
||||
**Open question before writing it:** the `elapsed` for the self-driven modes.
|
||||
The walk receives it from its caller; cleanest is to capture it in the hook (one
|
||||
value per pass, shared by every frame) and reuse it, falling back to a
|
||||
`Time::Clock` delta when nothing visible ran that frame. Needs checking whether
|
||||
`Tick::WorldTick` (`FUN_0066FD50`, the world subsystem) runs before or after the
|
||||
UI pass, which decides whether the captured value is this frame's or last
|
||||
frame's.
|
||||
|
||||
**Cost:** one new MinHook (uncontended, guarded to ~zero when unused), one
|
||||
WorldTick subscriber, a frame-keyed mode map, `Enum.OnUpdateMode`, and
|
||||
`SetOnUpdateMode` / `GetOnUpdateMode`.
|
||||
|
||||
**Value, honestly.** `RunAlways` is the only genuinely new capability — a hidden
|
||||
frame currently cannot run OnUpdate at all, which is why addons keep a
|
||||
permanently-shown dummy frame for their tickers. `Disabled` duplicates
|
||||
`SetScript("OnUpdate", nil)` and `RunOnce` overlaps `C_Timer.After(0, …)`, so
|
||||
the rest is API parity. Worth it for the one capability plus a clean enum, not
|
||||
as a performance feature.
|
||||
|
||||
+5001
-818
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,8 @@ ImageBase `0x00400000`. All VAs in this doc use that base.
|
||||
|
||||
## How Blizzard wires Lua
|
||||
|
||||
There are two registries.
|
||||
There are two Blizzard registries, plus the Lua standard libraries, which are
|
||||
bound a third way (§3) and are NOT in the batch counts below.
|
||||
|
||||
### 1. Global functions — `FrameScript_RegisterFunction` at `0x00704120`
|
||||
|
||||
@@ -57,6 +58,62 @@ C3 ret
|
||||
23 tables found, **481 methods total**. The runtime dispatcher at `0x005368D0`
|
||||
resolves `frame:method(...)` by looking the name up in the matching context.
|
||||
|
||||
### 3. Lua standard libraries — `luaL_openlib` tables
|
||||
|
||||
The Lua 5.0 standard libraries, and Blizzard's replacements inside them, are
|
||||
bound by `luaL_openlib` walking one `luaL_reg { char *name; lua_CFunction
|
||||
func; }[]` table per library when the Lua state is created (`FUN_00703B80`,
|
||||
called immediately BEFORE `FUN_LOAD_SCRIPT_FUNCTIONS`). They never pass
|
||||
through `FrameScript_RegisterFunction`, so none of them appear in the 54
|
||||
batches above or in the loop scan behind `raw_globals.txt` — which is how
|
||||
`debugprofilestart` / `debugprofilestop` (stock since 1.7.0) went
|
||||
uncatalogued here until 2026-09. **Before declaring a global absent, check
|
||||
these tables or `/dump` the name in game.**
|
||||
|
||||
**Base library** — table `[0x00811E28]`, 36 entries, `.rdata`. Blizzard's
|
||||
own base library: the stock 5.0 set minus `dofile`/`loadfile`/`require`/
|
||||
`print` (`print` is a ClassicAPI backport), plus the `debug*` /
|
||||
`*errorhandler` / `date` / `time` additions.
|
||||
|
||||
```
|
||||
0x00702770 setglobal 0x00702DF0 type
|
||||
0x007027B0 getglobal 0x00702E30 tonumber
|
||||
0x007027E0 debugload 0x00702F40 tostring
|
||||
0x007027F0 debuginfo 0x007030A0 unpack
|
||||
0x00702800 debugprint 0x007030F0 rawequal
|
||||
0x00702810 debugdump 0x00703130 rawget
|
||||
0x00702820 debugbreak 0x00703160 rawset
|
||||
0x00702830 debugtimestamp 0x007031A0 assert
|
||||
0x00702840 debugprofilestart 0x00703200 gcinfo
|
||||
0x00702860 debugprofilestop 0x00703250 collectgarbage
|
||||
0x00702900 seterrorhandler 0x00703280 loadstring
|
||||
0x00702950 geterrorhandler 0x007032F0 pcall
|
||||
0x00702970 error 0x00703340 xpcall
|
||||
0x007029F0 getmetatable 0x007033A0 date
|
||||
0x00702A40 setmetatable 0x007035A0 time
|
||||
0x00702AC0 getfenv 0x00703760 debugstack
|
||||
0x00702BE0 setfenv
|
||||
0x00702C90 next
|
||||
0x00702CD0 ipairs
|
||||
0x00702DA0 pairs
|
||||
```
|
||||
|
||||
`debugprofilestart` stores `rdtsc` in `0x00CEEB58` (u64);
|
||||
`debugprofilestop` pushes `|rdtsc − start| / (ticksPerSecond / 1000)` as a
|
||||
double, where `ticksPerSecond` is `FUN_0042C020` = `(int64)(1000.0 /
|
||||
[0x008332C8])` — a TSC scale calibrated at boot (the image holds `1.0`). So:
|
||||
milliseconds with a sub-microsecond fraction, from the CPU counter, unrelated
|
||||
to the engine's millisecond tick and to `GetTime()`.
|
||||
|
||||
The `table`, `string` and `math` libraries have the same shape (the `table`
|
||||
library's `luaL_reg` is at `0x00822D70` — see `Offsets.h`
|
||||
`FUN_LUA_TABLE_INSERT`) and are not enumerated here. The short global
|
||||
aliases (`tinsert`, `getn`, `strlen`, `format`, `floor`, …) are not registered
|
||||
at all: an embedded Lua chunk at `.data 0x008722E8`, run by the same state
|
||||
init, assigns them from the library tables (`tinsert = tab.insert`). A module
|
||||
that registers over a library function must therefore re-bind its alias —
|
||||
`Game::Lua::RegisterGlobalAlias`.
|
||||
|
||||
## Scope
|
||||
|
||||
The 54 global-function batches split into two phases:
|
||||
|
||||
+104
-29
@@ -69,20 +69,19 @@ The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
earlier `0xFF` alpha force at emit was a misdiagnosis of a bubble that built
|
||||
while faded out; it froze icon-line text fully opaque while everything
|
||||
around it faded.
|
||||
- **Outlined fonts** — outline ink extends past the glyph advances; the
|
||||
engine itself grows line height by exactly 4.0 / 2.0 pen px for thick / thin
|
||||
outline (`face+0x180` bits 3 / 0, `.rdata` constants). `OutlineInkPen` reads
|
||||
the same flags and constants, and half the ink leads each icon, half trails
|
||||
— threaded through the shared advance helper so the emitter advance, the
|
||||
justify pre-shift, and the wrap-stepper probes all agree. Without it, coins
|
||||
clipped into outlined digits the moment the exact K removed the old
|
||||
miscalibration's accidental slack (`968335d`).
|
||||
- **Size, spacing and alignment are 4.3.4's rules** (decompiled source in
|
||||
`C:\Git\wowdecomp15595`; 3.3.5 has the same). See "Icon geometry" below.
|
||||
Every hand-tuned constant this module once carried (a 0.18 × font lead/trail
|
||||
pad, a 0.6 × font vertical centre, a −1px region nudge, a size multiplier)
|
||||
is gone.
|
||||
- **Measure** — three cold FontString-level co-hooks make measure match the
|
||||
drawn result: width (`FUN_00772890`), height (`FUN_007729B0`), and wrap
|
||||
(`FUN_005C7260`). `GetStringWidth`, `fontstring:GetStringHeight`, wrap
|
||||
breaks, and line height all count the icon.
|
||||
- **Tall icons** — an icon taller than the font grows its line height, so it
|
||||
does not overlap its neighbors, and it centers in the grown line. A hyperlink
|
||||
- **Tall icons** — a line holding an icon taller than the font moves itself
|
||||
and the lines after it down by the overflow, as 4.3.4 does, so it does not
|
||||
overlap its neighbors; the icon aligns by the line's vertical justification
|
||||
(below). A hyperlink
|
||||
that contains a tall icon is hoverable across the whole icon
|
||||
(`49934b0`, `b9d0705`).
|
||||
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
|
||||
@@ -258,15 +257,76 @@ per build — on the first wrapped line, detected by `text == [node+0x48]` (the
|
||||
node's text start) — and icons ACCUMULATE across lines. Multiple icons per line
|
||||
fall out of the segmentation loop naturally.
|
||||
|
||||
### Vertical centring — font-relative
|
||||
### Icon geometry — 4.3.4's rules, ported
|
||||
|
||||
`penXYZ[1]` sits near the text TOP, so the icon centre = `penY + fontHeight *
|
||||
g_centerFrac` (0.6, calibrated in-game across chat + pfUI's bubble). Font height
|
||||
comes from the engine's own `FUN_005c6fa0(flag, [node+0x1c])` (the emitter's
|
||||
glyph-sizing call), so centring holds across font sizes without a fixed pixel
|
||||
nudge. A TALL icon (height > font) centres in its GROWN line (see the tall-icon
|
||||
sections in Current design) — the string-height co-hook adds the overflow, so
|
||||
the extra room sits half above and half below the text.
|
||||
**Size** (`ParseEmbeddedTexture`, 0x00617BA0 in 4.3.4). A WRITTEN size is not
|
||||
pixels: it multiplies by `fontPx / (14 × ddcWidthScale)`, the reduction of the
|
||||
engine's `windowPx / (aspectComp × 1024) × fontHeight / NDCToDDCWidth(14 /
|
||||
(aspectComp × 1024))` (the call list of the real apwow.exe function confirms
|
||||
`CoordinateGetAspectCompensation` + `NDCToDDCWidth`). So sizes are relative to
|
||||
a 14-unit font and follow the text's size, UI scale and resolution. 1.12 holds
|
||||
ddcWidthScale in the same Coordinate static (`VAR_COORD_DDC_WIDTH_SCALE`,
|
||||
0x00832A44 — the four statics sit at 0x00832A40 with 4.3.4's exact initial
|
||||
values). On top of that, every size multiplies by the fontstring's layout
|
||||
scale (`fs+0x7C`) — 3.3.5's `CGxString::m_scale`, applied again although the
|
||||
font height already includes it. 4.3.4 dropped this factor; 3.3.5's was kept
|
||||
after a side-by-side look in game. Every wrap gets the same scale, as 4.3.4
|
||||
passes `m_layoutScale` to every `CalcWrapPoint`: the draw builder through the
|
||||
node's owner, and the three fs-level callers (the height getter, the
|
||||
display-text truncation inside the rebuild, the break array) through a
|
||||
fontstring scope their co-hooks set (`WrapLayoutScale`). Size cases: height ≤ 0 → font height; width absent → the height;
|
||||
width > 0 → width × scale, or a RATIO of the height when the height came from
|
||||
the font (`:0:2` is twice as wide); width ≤ 0 → the font height. Offsets are
|
||||
screen pixels and move only the drawn texture (+x right, +y UP), never the
|
||||
layout. `ResolveIconSize` is the one implementation.
|
||||
|
||||
**Spacing** (`InitializeTextLine`, case CODE_TEXTURESTART). An icon starts at
|
||||
the previous glyph's INK edge — bearing + cell width, clamped to ≥ 0 and
|
||||
capped at the glyph's own advance (`FUN_TEXT_GLYPH_BASE_ADVANCE`) — plus
|
||||
the face's outline ink (4 × scale thick / 2 × scale thin, `face+0x180` bits 3 /
|
||||
0; the same constants the 1.12 emitter grows its line by); with no glyph
|
||||
since the line start or the last icon it starts at the pen, no gap. The next
|
||||
glyph starts at `floor(iconRight + 0.5)` with no kerning against the icon.
|
||||
1.12's emitter doesn't report its last glyph's position (its pen is lazy), so
|
||||
`drawRun` replays the run's placements with the emitter's own helpers — the
|
||||
pair advance, the snap rounding (`FADD 0.5; __ftol`), `FUN_TEXT_GLYPH_DESC`
|
||||
(`CGxFont::NewCodeDesc`) and `FUN_TEXT_GLYPH_BEARING` — and floors the cell
|
||||
width in snap mode exactly as the emitter draws the quad. Measures count an
|
||||
icon as its width plus the outline gap when a glyph precedes it
|
||||
(`SumIconAdvances`); the engine measures width alone, but counting the gap
|
||||
keeps measure and render in step for tooltips and right-justified text. The
|
||||
advance cap is ClassicAPI's: every measure counts the preceding glyph by its
|
||||
advance, so an ink edge past the advance (a glyph cell wider than its step —
|
||||
seen on "[" in chat) pushed each following icon and the rest of the line past
|
||||
the measured width, and wrapped chat lines overran the frame edge.
|
||||
|
||||
**Vertical** (same case + `CreateGeometry`). By the node's vertical justify
|
||||
(`OFF_TEXT_NODE_JUSTIFY_V`, node+0x50: 0 top / 1 middle / 2 bottom — the
|
||||
branch of the origin finalize `FUN_005cdf70`): top → the icon's top on the line
|
||||
top; bottom → its bottom on the baseline; middle → centred on the font height
|
||||
below the line top. The baseline is `penXYZ[1]` and the line top is the draw
|
||||
builder's `linkState[1]` (baseline + font height + spacing), 4.3.4's
|
||||
m_baseLineHeight / m_lineHeight.
|
||||
|
||||
**Tall lines** (`CGxString::InitializeText` + `InitializeViewTranslation` +
|
||||
`GxuFontGetWrappedTextHeight`), mirrored step for step:
|
||||
- A line whose tallest icon reaches past the font height moves down by that
|
||||
overflow, and every later line with it: all of it before the line for
|
||||
bottom justify, all after for top, `floor(overflow / 2)` before and after
|
||||
for middle. The emitter co-hook moves the builder's pen y and link band
|
||||
(`penXYZ[1]` = its `local_48`, `linkState[1]/[3]` = its `local_74[0]/[2]`),
|
||||
which the builder lowers per line, so the move carries forward.
|
||||
- The node's summed overflow (`m_extraLineHeight`) joins the text height the
|
||||
origin finalize justifies by: the `FUN_TEXT_NODE_FINALIZE_ORIGIN` co-hook
|
||||
raises the node position y (`+0x24`) by half / all of it for middle /
|
||||
bottom justify for the call, so the engine's own math and rounding produce
|
||||
4.3.4's origin. It also runs on every reposition (`FUN_005ccb10`).
|
||||
- `GetStringHeight` adds the same per-line overflows, summed. The wrap
|
||||
stepper reports each line's overflow during the engine's own height pass,
|
||||
so the lines are the engine's breaks; the sum is kept per fontstring while
|
||||
the engine serves its cached height.
|
||||
A link's hit band grows toward where its icon reaches (below for top, above
|
||||
for bottom, half each way for middle); 4.3.4 keeps the band at the text.
|
||||
|
||||
### Measure / wrap — co-hook the tokenizer
|
||||
|
||||
@@ -417,11 +477,11 @@ formula.)
|
||||
|
||||
**Still icon-blind (accepted residuals):**
|
||||
|
||||
- Sub-pixel only for a string-final icon: the width hooks report its INK edge
|
||||
(advance minus the trail half-pad/ink — `TrailingIconTrimPen`), matching the
|
||||
engine's own last-glyph convention (the gxu width loop ends on ink, not
|
||||
advance — the source of the old −0.8px measurement, whose previous-glyph
|
||||
advance treatment is semantically CORRECT once an icon renders after it).
|
||||
- Sub-pixel only for an icon after a glyph: layout starts it at that glyph's
|
||||
INK edge while measures count the glyph's advance, so the two differ by the
|
||||
glyph's right bearing — the same discrepancy 4.3.4 has between its own
|
||||
layout and measure loops. A string-final icon measures exactly (the gxu
|
||||
width loop ends on the last glyph's ink, which is where the icon starts).
|
||||
What remains is the icon region's ±0.5px placement rounding and the base
|
||||
loop's ~0 contribution for the payload-0 icon token.
|
||||
- An fs whose `fs+0xFC` cache was filled while the feature was toggled
|
||||
@@ -544,9 +604,7 @@ backend note.)
|
||||
- **Still icon-blind** (accepted): the hyperlink hit-test past a tall icon
|
||||
OUTSIDE the link on the same line. The substring measure `FUN_00772AE0` was
|
||||
hooked once its real consumer surfaced (the tooltip auto-size measures
|
||||
wrapped SEGMENTS with it), and the trailing-icon width residual is trimmed
|
||||
to sub-pixel (`TrailingIconTrimPen` — a string-final icon reports its ink
|
||||
edge, not its advance, per the engine's own last-glyph convention).
|
||||
wrapped SEGMENTS with it).
|
||||
|
||||
## Goal & spec
|
||||
|
||||
@@ -556,9 +614,26 @@ Render the modern inline-texture escape (warcraft.wiki.gg/wiki/UI_escape_sequenc
|
||||
|Tpath:height[:width[:offsetX:offsetY[:texWidth:texHeight:left:right:top:bottom[:rV:gV:bV]]]]|t
|
||||
```
|
||||
|
||||
Atlas markup (`|A:atlas:…|a`) is Legion+ (needs an atlas DB 1.12 lacks) — out
|
||||
of scope. FileDataID paths are Legion+ — 1.12 resolves texture PATH strings
|
||||
only.
|
||||
Atlas markup is also supported:
|
||||
|
||||
```
|
||||
|A:atlasName:height:width[:offsetX:offsetY]|a
|
||||
```
|
||||
|
||||
It shares every scanner, measure, wrap and placement stage with `|T` — only the
|
||||
payload parser differs. `ParseAtlasIcon` resolves the name through
|
||||
`Texture::Atlas` and fills the SAME `IconDesc`, because an atlas is exactly the
|
||||
"path + normalized sub-rect" that struct already carries for a `|T` span with
|
||||
texcoords. A `height`/`width` of `0` resolves to the atlas's own pixel size at
|
||||
PARSE time (not the line font height `|T` falls back to), which is what keeps the
|
||||
rest of the pipeline marker-agnostic. Tiling atlases draw one quad, so
|
||||
`tilesHorizontally` / `tilesVertically` are not honored here.
|
||||
|
||||
Adding any further marker means touching `IconStartLen`, `FindIconClose`,
|
||||
`InlineSpanLen`, the ghost guard in `FlushLayout` — and, mandatory,
|
||||
`chat/IconFilter.cpp`, or the marker becomes a chat-spoof vector.
|
||||
|
||||
FileDataID paths are Legion+ — 1.12 resolves texture PATH strings only.
|
||||
|
||||
## Confirmed: 1.12 has ZERO inline-texture support
|
||||
|
||||
|
||||
@@ -1259,4 +1259,42 @@
|
||||
2: 0x00706040 CreateFont
|
||||
3: 0x007060B0 CreateFrame
|
||||
|
||||
=== luaL_openlib base library Table 0x00811E28 Count=36 (not a FrameScript_RegisterFunction batch; see BlizzardScriptAPI.md §3) ===
|
||||
0: 0x00702770 setglobal
|
||||
1: 0x007027B0 getglobal
|
||||
2: 0x007027E0 debugload
|
||||
3: 0x007027F0 debuginfo
|
||||
4: 0x00702800 debugprint
|
||||
5: 0x00702810 debugdump
|
||||
6: 0x00702820 debugbreak
|
||||
7: 0x00702830 debugtimestamp
|
||||
8: 0x00702840 debugprofilestart
|
||||
9: 0x00702860 debugprofilestop
|
||||
10: 0x00702900 seterrorhandler
|
||||
11: 0x00702950 geterrorhandler
|
||||
12: 0x00702970 error
|
||||
13: 0x007029F0 getmetatable
|
||||
14: 0x00702A40 setmetatable
|
||||
15: 0x00702AC0 getfenv
|
||||
16: 0x00702BE0 setfenv
|
||||
17: 0x00702C90 next
|
||||
18: 0x00702CD0 ipairs
|
||||
19: 0x00702DA0 pairs
|
||||
20: 0x00702DF0 type
|
||||
21: 0x00702E30 tonumber
|
||||
22: 0x00702F40 tostring
|
||||
23: 0x007030A0 unpack
|
||||
24: 0x007030F0 rawequal
|
||||
25: 0x00703130 rawget
|
||||
26: 0x00703160 rawset
|
||||
27: 0x007031A0 assert
|
||||
28: 0x00703200 gcinfo
|
||||
29: 0x00703250 collectgarbage
|
||||
30: 0x00703280 loadstring
|
||||
31: 0x007032F0 pcall
|
||||
32: 0x00703340 xpcall
|
||||
33: 0x007033A0 date
|
||||
34: 0x007035A0 time
|
||||
35: 0x00703760 debugstack
|
||||
|
||||
|
||||
|
||||
@@ -31,4 +31,13 @@ bool PatchBytes(void *dst, const void *src, size_t len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void BoundedCopy(char *dst, const char *src, size_t n) {
|
||||
if (dst == nullptr || n == 0)
|
||||
return;
|
||||
size_t i = 0;
|
||||
for (; src != nullptr && src[i] != '\0' && i + 1 < n; ++i)
|
||||
dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -11,8 +11,16 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Common {
|
||||
|
||||
bool PatchBytes(void *dst, const void *src, size_t len);
|
||||
|
||||
// Bounded, always-terminated string copy: writes at most `n - 1`
|
||||
// characters plus the terminator, and treats a null `src` as empty. `n`
|
||||
// is the size of `dst`. Stands in for `strncpy`, which MSVC deprecates
|
||||
// (C4996) and which does not terminate on truncation.
|
||||
void BoundedCopy(char *dst, const char *src, size_t n);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "Game.h"
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "config/FileSwitch.h"
|
||||
#include "event/Custom.h"
|
||||
#include "player/NameCache.h"
|
||||
#include "text/InlineTexture.h"
|
||||
@@ -207,6 +208,14 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
|
||||
if (reason == DLL_PROCESS_ATTACH) {
|
||||
DisableThreadLibraryCalls(hModule);
|
||||
|
||||
// The ONE thing that has to happen this early, and the only module this
|
||||
// file names directly. The engine reads its config file during boot,
|
||||
// several calls before VanillaFixes invokes `Load`, so a `-config`
|
||||
// switch cannot be applied from the normal install path — by then the
|
||||
// file has been read. This is a single 4-byte store, not a hook, so it
|
||||
// carries none of the loader-lock cost described above.
|
||||
Config::FileSwitch::Apply();
|
||||
|
||||
// Install nothing here (loader lock — see the block comment above).
|
||||
//
|
||||
// Under VanillaFixes, `Load` is the ONLY install trigger. VfPatcher.dll
|
||||
|
||||
+251
-18
@@ -14,6 +14,7 @@
|
||||
#include "Game.h"
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "api/Documentation.h"
|
||||
|
||||
namespace Game {
|
||||
|
||||
@@ -23,6 +24,22 @@ void *ResolveUnitToken(const char *token) {
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
float PixelToInternal(float px) {
|
||||
const float mul = Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (div == 0.0f)
|
||||
return 0.0f;
|
||||
return px * mul / (div * static_cast<float>(Offsets::UI_COORD_SCALE_UNIT));
|
||||
}
|
||||
|
||||
double InternalToPixel(float internal) {
|
||||
const float mul = Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (mul == 0.0f)
|
||||
return 0.0;
|
||||
return static_cast<double>(internal) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
|
||||
}
|
||||
|
||||
namespace Lua {
|
||||
// Each entry binds a typed function pointer in `Game::Lua::` to the
|
||||
// corresponding raw VA in `Offsets`. The X-macro keeps the column-aligned
|
||||
@@ -44,8 +61,10 @@ namespace Lua {
|
||||
F(NewTable, lua_newtable, LUA_NEW_TABLE) \
|
||||
F(GetTable, lua_gettable, LUA_GET_TABLE) \
|
||||
F(RawGet, lua_rawget, LUA_RAW_GET) \
|
||||
F(RawGetI, lua_rawgeti, LUA_RAWGETI) \
|
||||
F(SetTable, lua_settable, LUA_SET_TABLE) \
|
||||
F(RawSet, lua_rawset, LUA_RAW_SET) \
|
||||
F(GetMetatable,lua_getmetatable,LUA_GET_METATABLE)\
|
||||
F(SetMetatable,lua_setmetatable,LUA_SET_METATABLE)\
|
||||
F(Insert, lua_insert, LUA_INSERT) \
|
||||
F(Remove, lua_remove, LUA_REMOVE) \
|
||||
@@ -55,6 +74,7 @@ namespace Lua {
|
||||
F(PCall, lua_pcall, LUA_PCALL) \
|
||||
F(Next, lua_next, LUA_NEXT) \
|
||||
F(Type, lua_type, LUA_TYPE) \
|
||||
F(TypeName, lua_typename, LUA_TYPE_NAME) \
|
||||
F(Error, lua_error, LUA_ERROR) \
|
||||
F(ToPointer, lua_topointer, LUA_TO_POINTER) \
|
||||
F(ToThread, lua_tothread, LUA_TO_THREAD) \
|
||||
@@ -99,26 +119,104 @@ void *ResolveObject(void *L, int idx) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void RegisterGlobalFunction(const char *name, CFunction func) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
int ObjectLuaRef(void *object) {
|
||||
using ScriptRegister_t = void(__thiscall *)(void *object, void *nameOrNull);
|
||||
if (Game::Read<int>(object, Offsets::OFF_COBJECT_LUA_REFCOUNT) == 0)
|
||||
reinterpret_cast<ScriptRegister_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER)(object, nullptr);
|
||||
return Game::Read<int>(object, Offsets::OFF_COBJECT_LUA_REF);
|
||||
}
|
||||
|
||||
// Aliased to the same engine entry; the engine reads VAR_LUA_STATE
|
||||
// internally to decide which state to write to, and the glue hook
|
||||
// runs while that pointer is set to the glue state. Wrapped as a
|
||||
// named function (rather than `using RegisterGlueFunction = ...`) so
|
||||
// callers express intent at the call site.
|
||||
void RegisterGlueFunction(const char *name, CFunction func) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
void PushObject(void *L, void *object) {
|
||||
RawGetI(L, REGISTRY_INDEX, ObjectLuaRef(object));
|
||||
}
|
||||
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count) {
|
||||
auto fn = reinterpret_cast<RegisterFrameMethods_t>(Offsets::FUN_REGISTER_FRAME_METHODS);
|
||||
fn(table, count, context);
|
||||
const char *ObjectDisplayName(void *object) {
|
||||
using GetName_t = const char *(__thiscall *)(void *);
|
||||
const void *vtable = Game::Read<const void *>(object, 0);
|
||||
const char *name = Game::Read<GetName_t>(
|
||||
vtable, Offsets::VTBL_SCRIPTOBJECT_GET_NAME)(object);
|
||||
return name != nullptr ? name
|
||||
: reinterpret_cast<const char *>(Offsets::VAR_STR_UNNAMED);
|
||||
}
|
||||
|
||||
namespace {
|
||||
using FrameScriptIsA_t = char(__thiscall *)(void *self, int typeId);
|
||||
|
||||
const char *EngineString(uintptr_t address) {
|
||||
return reinterpret_cast<const char *>(address);
|
||||
}
|
||||
|
||||
// Shared body: the engine's prologue with N accepted type ids.
|
||||
void *ResolveChecked(void *L, int idx, const uintptr_t *typeIdVars, int count,
|
||||
bool raiseError) {
|
||||
if (Type(L, idx) != TYPE_TABLE) {
|
||||
if (raiseError)
|
||||
Error(L, EngineString(Offsets::STR_FS_THIS_NON_TABLE));
|
||||
return nullptr;
|
||||
}
|
||||
void *obj = ResolveObject(L, idx);
|
||||
if (obj == nullptr) {
|
||||
if (raiseError)
|
||||
Error(L, EngineString(Offsets::STR_FS_THIS_NON_OBJECT));
|
||||
return nullptr;
|
||||
}
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (IsA(obj, typeIdVars[i]))
|
||||
return obj;
|
||||
}
|
||||
if (raiseError)
|
||||
Error(L, EngineString(Offsets::STR_FS_WRONG_OBJECT_TYPE));
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int FrameScriptTypeId(uintptr_t typeIdVar) {
|
||||
auto &typeId = Game::Ref<int>(typeIdVar);
|
||||
if (typeId == 0) {
|
||||
auto &counter = Game::Ref<int>(Offsets::VAR_FRAMESCRIPT_TYPE_ID_COUNTER);
|
||||
counter += 1;
|
||||
typeId = counter;
|
||||
}
|
||||
return typeId;
|
||||
}
|
||||
|
||||
bool IsA(void *object, uintptr_t typeIdVar) {
|
||||
auto **vtable = *reinterpret_cast<void ***>(object);
|
||||
auto isA = reinterpret_cast<FrameScriptIsA_t>(
|
||||
vtable[Offsets::OFF_VMT_FRAMESCRIPT_ISA / sizeof(void *)]);
|
||||
return isA(object, FrameScriptTypeId(typeIdVar)) != 0;
|
||||
}
|
||||
|
||||
void *ResolveTypedObject(void *L, int idx, uintptr_t typeIdVar, bool raiseError) {
|
||||
return ResolveChecked(L, idx, &typeIdVar, 1, raiseError);
|
||||
}
|
||||
|
||||
void *ResolveTypedObjectAny(void *L, int idx, const uintptr_t *typeIdVars,
|
||||
int count, bool raiseError) {
|
||||
return ResolveChecked(L, idx, typeIdVars, count, raiseError);
|
||||
}
|
||||
|
||||
void *ResolveTooltip(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_GAMETOOLTIP_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveFrame(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_FRAME_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveRegion(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_REGION_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveTexture(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_TEXTURE_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveFontString(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_FONTSTRING_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveEditBox(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_EDITBOX_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
void *ResolveModel(void *L, int idx, bool raiseError) {
|
||||
return ResolveTypedObject(L, idx, Offsets::VAR_MODEL_LUA_TYPE_ID, raiseError);
|
||||
}
|
||||
|
||||
// Looks up `_G[name]`. If absent, creates a fresh table and binds it.
|
||||
@@ -136,11 +234,110 @@ void EnsureGlobalTable(void *L, const char *name) {
|
||||
Insert(L, -2); // [tbl, name, tbl]
|
||||
SetTable(L, GLOBALS_INDEX); // _G[name] = tbl; pops k+v. [tbl]
|
||||
}
|
||||
|
||||
// Same, one level down: ensures `parent[name]` is a table, with `parent`
|
||||
// on top of the stack. Leaves `[parent, sub]`. Raw access, so a namespace
|
||||
// carrying a metatable can't divert the read or the write.
|
||||
void EnsureSubTable(void *L, const char *name) {
|
||||
PushString(L, name); // [parent, name]
|
||||
RawGet(L, -2); // [parent, parent[name]]
|
||||
if (Type(L, -1) == TYPE_TABLE)
|
||||
return;
|
||||
SetTop(L, -2); // pop the non-table. [parent]
|
||||
NewTable(L); // [parent, sub]
|
||||
PushString(L, name); // [parent, sub, name]
|
||||
PushValue(L, -2); // [parent, sub, name, sub]
|
||||
RawSet(L, -4); // parent[name] = sub. [parent, sub]
|
||||
}
|
||||
|
||||
// The escape-hatch namespace. Registering a name is not the same as owning
|
||||
// it: the engine's registrars write `_G`, FrameXML and every addon load
|
||||
// after that, and whoever writes last wins with no error and no way for the
|
||||
// loser to notice. `GetServerTime` is the case on record — a client whose
|
||||
// GameTime.lua declares its own replaced ours on every login.
|
||||
//
|
||||
// So every name the registrars below bind is also bound under
|
||||
// `_G.ClassicAPI`, which nothing else writes. Two properties earn their
|
||||
// keep, and both depend on doing this HERE rather than from Lua:
|
||||
//
|
||||
// - It runs at registration, so the mirror captures OUR function. A
|
||||
// Lua-side `ClassicAPI.X = X` in the embedded addon would run after
|
||||
// FrameXML and faithfully copy the clobbered version.
|
||||
// - It mirrors by VALUE, reading the name back instead of making a
|
||||
// second closure over the same C function. So `ClassicAPI.X == X`
|
||||
// holds until something replaces the global, and the pair doubles as
|
||||
// a clobber check for addons and for us.
|
||||
//
|
||||
// Not covered: frame methods (`obj:Method()` lives in the per-frame-type
|
||||
// registry, not `_G`) and `RegisterIntegerEnum` values (a mirrored number
|
||||
// is a copy, not a reference).
|
||||
constexpr const char *kMirrorTable = "ClassicAPI";
|
||||
|
||||
void MirrorRegistration(const char *tableName, const char *name) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = GetTop(L);
|
||||
|
||||
// Read back whatever the registration actually bound.
|
||||
if (tableName != nullptr) {
|
||||
EnsureGlobalTable(L, tableName); // [src]
|
||||
PushString(L, name); // [src, name]
|
||||
RawGet(L, -2); // [src, value]
|
||||
} else {
|
||||
PushString(L, name); // [name]
|
||||
GetTable(L, GLOBALS_INDEX); // [value]
|
||||
}
|
||||
if (Type(L, -1) != TYPE_FUNCTION) {
|
||||
SetTop(L, top); // registration didn't land; nothing to mirror
|
||||
return;
|
||||
}
|
||||
const int valueIdx = GetTop(L); // absolute, so the pushes below can't skew it
|
||||
|
||||
EnsureGlobalTable(L, kMirrorTable); // [..., mirror]
|
||||
if (tableName != nullptr)
|
||||
EnsureSubTable(L, tableName); // [..., mirror, sub]
|
||||
PushString(L, name);
|
||||
PushValue(L, valueIdx);
|
||||
RawSet(L, -3); // target[name] = value
|
||||
SetTop(L, top);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void RegisterGlobalFunction(const char *name, CFunction func, const Doc::Function *doc) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
MirrorRegistration(nullptr, name);
|
||||
Api::Documentation::RecordGlobal(name, doc);
|
||||
}
|
||||
|
||||
// Aliased to the same engine entry; the engine reads VAR_LUA_STATE
|
||||
// internally to decide which state to write to, and the glue hook
|
||||
// runs while that pointer is set to the glue state. Wrapped as a
|
||||
// named function (rather than `using RegisterGlueFunction = ...`) so
|
||||
// callers express intent at the call site.
|
||||
void RegisterGlueFunction(const char *name, CFunction func, const Doc::Function *doc) {
|
||||
auto fn = reinterpret_cast<FrameScript_RegisterFunction_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_REGISTER_FUNCTION);
|
||||
fn(name, func);
|
||||
// `State()` is the glue state here for the same reason the engine
|
||||
// registrar writes there, so the mirror lands on the glue `_G`.
|
||||
MirrorRegistration(nullptr, name);
|
||||
Api::Documentation::RecordGlobal(name, doc);
|
||||
}
|
||||
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
|
||||
const Doc::Method *docs, int docCount) {
|
||||
auto fn = reinterpret_cast<RegisterFrameMethods_t>(Offsets::FUN_REGISTER_FRAME_METHODS);
|
||||
fn(table, count, context);
|
||||
Api::Documentation::RecordFrameMethods(context, table, count, docs, docCount);
|
||||
}
|
||||
|
||||
// Registers `func` at `_G[tableName][methodName]`. If the namespace
|
||||
// doesn't already exist, creates an empty table for it.
|
||||
void RegisterTableFunction(const char *tableName, const char *methodName, CFunction func) {
|
||||
void RegisterTableFunction(const char *tableName, const char *methodName, CFunction func,
|
||||
const Doc::Function *doc) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
@@ -149,10 +346,41 @@ void RegisterTableFunction(const char *tableName, const char *methodName, CFunct
|
||||
PushCClosure(L, func, 0); // [tbl, methodName, closure]
|
||||
SetTable(L, -3); // tbl[m]=c; pops k+v. [tbl]
|
||||
SetTop(L, -2); // pop tbl. []
|
||||
MirrorRegistration(tableName, methodName);
|
||||
Api::Documentation::RecordTable(tableName, methodName, doc);
|
||||
}
|
||||
|
||||
// `_G[alias] = _G[tableName][methodName]`, by VALUE. The engine binds its
|
||||
// short aliases this way (`tinsert = tab.insert` in the compat snippet it
|
||||
// runs at Lua init), so a module that registers over a library function has
|
||||
// to re-bind the alias to the same closure — that keeps `tinsert ==
|
||||
// table.insert` true, as the engine left it, instead of minting a second
|
||||
// closure. A plain raw write, exactly what the snippet's assignment did. No-op
|
||||
// if the source is not a function (nothing was registered to alias).
|
||||
void RegisterGlobalAlias(const char *alias, const char *tableName, const char *methodName,
|
||||
const Doc::Function *doc) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = GetTop(L);
|
||||
EnsureGlobalTable(L, tableName); // [tbl]
|
||||
PushString(L, methodName); // [tbl, m]
|
||||
RawGet(L, -2); // [tbl, value]
|
||||
if (Type(L, -1) != TYPE_FUNCTION) {
|
||||
SetTop(L, top);
|
||||
return;
|
||||
}
|
||||
PushString(L, alias); // [tbl, value, alias]
|
||||
PushValue(L, -2); // [tbl, value, alias, value]
|
||||
RawSet(L, GLOBALS_INDEX); // _G[alias] = value. [tbl, value]
|
||||
SetTop(L, top);
|
||||
MirrorRegistration(nullptr, alias);
|
||||
Api::Documentation::RecordGlobal(alias, doc);
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count) {
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
@@ -166,6 +394,7 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
}
|
||||
SetTable(L, -3); // parentTbl[sub] = subTbl
|
||||
SetTop(L, -2); // pop parentTbl
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
}
|
||||
|
||||
void SetFieldNumber(void *L, const char *key, double value) {
|
||||
@@ -303,8 +532,10 @@ ModuleAutoRegister::ModuleAutoRegister(Fn f) : fn(f), next(g_moduleHead) {
|
||||
}
|
||||
|
||||
void RunModuleRegistrations() {
|
||||
Api::Documentation::BeginPass(Api::Documentation::ENV_GAME);
|
||||
for (auto *node = g_moduleHead; node != nullptr; node = node->next)
|
||||
node->fn();
|
||||
Api::Documentation::EndPass();
|
||||
}
|
||||
|
||||
GlueModuleAutoRegister::GlueModuleAutoRegister(Fn f)
|
||||
@@ -313,8 +544,10 @@ GlueModuleAutoRegister::GlueModuleAutoRegister(Fn f)
|
||||
}
|
||||
|
||||
void RunGlueModuleRegistrations() {
|
||||
Api::Documentation::BeginPass(Api::Documentation::ENV_GLUE);
|
||||
for (auto *node = g_glueModuleHead; node != nullptr; node = node->next)
|
||||
node->fn();
|
||||
Api::Documentation::EndPass();
|
||||
}
|
||||
|
||||
ReloadAutoRegister::ReloadAutoRegister(Fn f) : fn(f), next(g_reloadHead) {
|
||||
|
||||
+223
-6
@@ -13,6 +13,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Game {
|
||||
@@ -70,6 +71,15 @@ inline T &Ref(uintptr_t address) {
|
||||
// across a dozen modules.
|
||||
void *ResolveUnitToken(const char *token);
|
||||
|
||||
// UI pixels <-> internal layout (anchor) units. Script_SetPoint converts its
|
||||
// pixel offsets with `px * [VAR_COORD_DDC_WIDTH_SCALE] / ([VAR_COORD_ASPECT_COMPENSATION]
|
||||
// * UI_COORD_SCALE_UNIT)`; Script_GetLeft and Script_GetStringWidth push the
|
||||
// inverse. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// (OFF_REGION_EFFECTIVE_SCALE) first. Both return 0 while the scale globals
|
||||
// are unset, so an early caller gets a zero offset rather than a wild one.
|
||||
float PixelToInternal(float px);
|
||||
double InternalToPixel(float internal);
|
||||
|
||||
using FrameScript_Initialize_t = bool(__fastcall *)();
|
||||
using LoadScriptFunctions_t = void(__fastcall *)();
|
||||
|
||||
@@ -78,6 +88,113 @@ using LoadScriptFunctions_t = void(__fastcall *)();
|
||||
// registering its 109 glue functions. See FUN_LOAD_GLUE_SCRIPT_FUNCTIONS.
|
||||
using LoadGlueScriptFunctions_t = void(__stdcall *)();
|
||||
|
||||
// --- API documentation descriptors -----------------------------------------
|
||||
//
|
||||
// Every Lua registration (see the `Register*` functions in `Game::Lua`) can
|
||||
// carry a descriptor: the signature the function presents to Lua — typed
|
||||
// arguments and returns, nilability, defaults — plus a one-sentence summary.
|
||||
// The descriptors are the single source of truth for the API's SHAPE.
|
||||
// `src/api/Documentation.cpp` records each registration on the first pass of
|
||||
// each Lua state and exports the result on demand as Blizzard
|
||||
// `APIDocumentation` tables (`C_APIDocumentation.GetSystems` / `GetSystem`),
|
||||
// which the embedded addon's `/api` browser renders. Prose beyond one
|
||||
// sentence stays in the docs; a descriptor is shape, not manual.
|
||||
//
|
||||
// Rules:
|
||||
// - One descriptor per REGISTERED NAME, not per C function. The same
|
||||
// `Script_*` bound twice with different return conventions
|
||||
// (`IsUsableSpell` → 1/nil pairs, `C_Spell.IsSpellUsable` → booleans)
|
||||
// gets two descriptors; a pair with the same shape may share one.
|
||||
// - `type` uses Blizzard's spellings — "number", "string", "bool",
|
||||
// "table", "luaIndex", "UnitToken", "SpellIdentifier", … — or the Name
|
||||
// of a `Structure` / enumeration declared elsewhere, which the browser
|
||||
// renders as a link.
|
||||
// - `Function::system` names the System a GLOBAL belongs to; convention
|
||||
// `<Area>Globals` ("SpellGlobals", "ItemGlobals"). Table functions
|
||||
// derive theirs from the table ("C_Spell" → System "Spell", Namespace
|
||||
// "C_Spell"); frame methods from the registry. A global's system must
|
||||
// not reuse a namespaced System's Name — the browser would prefix it.
|
||||
// - Descriptors are `const` objects with constant initializers (string
|
||||
// literals + file-scope arrays): no heap, no dynamic init.
|
||||
// - Summaries are user-facing text: one sentence, plain words, present
|
||||
// tense, no patch versions.
|
||||
namespace Doc {
|
||||
|
||||
struct Field {
|
||||
const char *name;
|
||||
const char *type;
|
||||
bool nilable;
|
||||
const char *defaultValue; // Lua literal ("false", "0", "\"player\""), or nullptr
|
||||
const char *doc; // one sentence, or nullptr
|
||||
};
|
||||
|
||||
// Required argument / always-present return.
|
||||
constexpr Field Req(const char *name, const char *type, const char *doc = nullptr) {
|
||||
return {name, type, false, nullptr, doc};
|
||||
}
|
||||
// Optional: nilable when no default is given, otherwise non-nil with that
|
||||
// default (Blizzard marks defaulted arguments `Nilable = false` + `Default`).
|
||||
constexpr Field Opt(const char *name, const char *type, const char *dflt = nullptr,
|
||||
const char *doc = nullptr) {
|
||||
return {name, type, dflt == nullptr, dflt, doc};
|
||||
}
|
||||
// Variable arguments / returns.
|
||||
constexpr Field Vararg(const char *type, const char *doc = nullptr) {
|
||||
return {"...", type, true, nullptr, doc};
|
||||
}
|
||||
|
||||
// A view over a file-scope `const Field[]`; `{}` means none.
|
||||
struct FieldList {
|
||||
const Field *data = nullptr;
|
||||
int count = 0;
|
||||
constexpr FieldList() = default;
|
||||
template <size_t N>
|
||||
constexpr FieldList(const Field (&a)[N]) : data(a), count(static_cast<int>(N)) {}
|
||||
};
|
||||
|
||||
// A registered function. Positional init: `{summary, args, rets}` for a
|
||||
// table function, `{summary, args, rets, "XGlobals"}` for a global, and
|
||||
// `{summary, args, rets, nullptr, true}` for a namespaced ClassicAPI
|
||||
// extension.
|
||||
struct Function {
|
||||
const char *summary;
|
||||
FieldList args;
|
||||
FieldList rets;
|
||||
const char *system; // globals / glue / aliases only; see the rules above
|
||||
bool extension; // a ClassicAPI-original API, not a Blizzard one
|
||||
};
|
||||
|
||||
// Pairs a frame method with its descriptor BY NAME (the engine's method
|
||||
// tables are name-keyed; `SetShown` exists on four registries with four
|
||||
// function pointers). Order is free; unmatched names are reported.
|
||||
struct Method {
|
||||
const char *name;
|
||||
const Function *doc;
|
||||
};
|
||||
|
||||
// A table shape a function returns or accepts → `Tables{Type="Structure"}`
|
||||
// in `system`. Self-registering: nothing binds a structure to Lua, so it
|
||||
// chains onto a static list at construction. Declare it next to the
|
||||
// function that returns it.
|
||||
struct Structure {
|
||||
Structure(const char *name, const char *system, FieldList fields,
|
||||
const char *summary = nullptr);
|
||||
const char *name;
|
||||
const char *system;
|
||||
FieldList fields;
|
||||
const char *summary;
|
||||
const Structure *next;
|
||||
};
|
||||
|
||||
// A custom event's payload, attached to its `Event::Custom::AutoReserve`.
|
||||
struct Event {
|
||||
const char *system;
|
||||
const char *summary;
|
||||
FieldList payload;
|
||||
};
|
||||
|
||||
} // namespace Doc
|
||||
|
||||
namespace Lua {
|
||||
using CFunction = int(__fastcall *)(void *L);
|
||||
|
||||
@@ -121,8 +238,14 @@ using lua_pushcclosure_t = void(__fastcall *)(void *L, CFunction fn, int upvals)
|
||||
using lua_newtable_t = void(__fastcall *)(void *L);
|
||||
using lua_gettable_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_rawget_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pushes t[n] for the table at idx, raw (no metamethods). With idx =
|
||||
// REGISTRY_INDEX it pushes a value stored under an integer ref.
|
||||
using lua_rawgeti_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
using lua_settable_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_rawset_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pushes the metatable of the value at idx and returns 1, or pushes
|
||||
// nothing and returns 0 when it has none.
|
||||
using lua_getmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
// Pops the table at the top and installs it as the metatable of the value
|
||||
// at idx; returns 1.
|
||||
using lua_setmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
@@ -134,6 +257,7 @@ using lua_call_t = void(__fastcall *)(void *L, int nargs, int nresults);
|
||||
using lua_pcall_t = int(__fastcall *)(void *L, int nargs, int nresults, int errfunc);
|
||||
using lua_next_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_type_t = int(__fastcall *)(void *L, int index);
|
||||
using lua_typename_t = const char *(__fastcall *)(void *L, int type);
|
||||
// The 1.12 `lua_error` wrapper at `0x6F4940` is actually variadic —
|
||||
// it does `lua_pushvfstring(L, fmt, args)` then prepends `luaL_where`
|
||||
// and throws. Existing callers pass a single literal (no `%`), which
|
||||
@@ -179,8 +303,10 @@ extern const lua_pushcclosure_t PushCClosure;
|
||||
extern const lua_newtable_t NewTable;
|
||||
extern const lua_gettable_t GetTable;
|
||||
extern const lua_rawget_t RawGet;
|
||||
extern const lua_rawgeti_t RawGetI;
|
||||
extern const lua_settable_t SetTable;
|
||||
extern const lua_rawset_t RawSet;
|
||||
extern const lua_getmetatable_t GetMetatable;
|
||||
extern const lua_setmetatable_t SetMetatable;
|
||||
extern const lua_insert_t Insert;
|
||||
extern const lua_remove_t Remove;
|
||||
@@ -190,6 +316,7 @@ extern const lua_call_t Call;
|
||||
extern const lua_pcall_t PCall;
|
||||
extern const lua_next_t Next;
|
||||
extern const lua_type_t Type;
|
||||
extern const lua_typename_t TypeName;
|
||||
extern const lua_error_t Error;
|
||||
extern const lua_topointer_t ToPointer;
|
||||
extern const lua_tothread_t ToThread;
|
||||
@@ -216,9 +343,84 @@ void *State();
|
||||
// the `self` slot of any `frame:method(...)` invocation.
|
||||
void *ResolveObject(void *L, int idx);
|
||||
|
||||
// The inverse: the object's Lua-registry ref, created on first use exactly as
|
||||
// the engine does it (GetMouseFocus and the script runners register an
|
||||
// object with FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER when its refcount is 0).
|
||||
int ObjectLuaRef(void *object);
|
||||
// Pushes the object's Lua table (registry[ObjectLuaRef(object)]).
|
||||
void PushObject(void *L, void *object);
|
||||
|
||||
// How the engine names an object in its frame-script usage errors:
|
||||
// `GetName() ?: "<unnamed>"` (VTBL_SCRIPTOBJECT_GET_NAME).
|
||||
const char *ObjectDisplayName(void *object);
|
||||
|
||||
// Same resolve, then the type gate every engine frame method applies to its
|
||||
// `self` before touching it: `lua_type == table`, resolve to the object, then
|
||||
// the `IsA(typeId)` vmethod at vtable+0x10. `ResolveObject` alone is not
|
||||
// enough for a method that casts the result — it hands back a
|
||||
// `CFrameScriptObject *` for ANY frame, so `GameTooltip.SetSpellByID(button)`
|
||||
// would reach the tooltip builder with a Button. Frame methods are ordinary
|
||||
// first-class Lua values (the per-type dispatcher is an `__index` handler
|
||||
// that returns the method closure), so that call is reachable from plain Lua
|
||||
// and the gate is not optional.
|
||||
//
|
||||
// `typeIdVar` is the class's `VAR_*_LUA_TYPE_ID`, which must match the
|
||||
// registry the method was registered on. The id is assigned lazily by
|
||||
// whichever method of the class runs first, so these mirror that assignment
|
||||
// and work even before any stock method of the type has been called.
|
||||
//
|
||||
// Raises the engine's own three errors and returns null. Pass
|
||||
// `raiseError = false` for an optional object ARGUMENT, where the caller
|
||||
// wants a silent null rather than a "this" error naming the wrong slot.
|
||||
// Returns null either way, so callers always check.
|
||||
void *ResolveTypedObject(void *L, int idx, uintptr_t typeIdVar,
|
||||
bool raiseError = true);
|
||||
|
||||
// For a method registered on SEVERAL registries, where any of those types is
|
||||
// a legitimate `self`. Accepts the object if `IsA` passes for any of the
|
||||
// listed ids — which is exactly what being in all of those registries means,
|
||||
// and avoids having to pin down the shared base class.
|
||||
void *ResolveTypedObjectAny(void *L, int idx, const uintptr_t *typeIdVars,
|
||||
int count, bool raiseError = true);
|
||||
|
||||
// The class's live type id, assigning it if the engine has not yet. Needed
|
||||
// directly only when calling a type-specific vtable slot.
|
||||
int FrameScriptTypeId(uintptr_t typeIdVar);
|
||||
|
||||
// The object's `IsA(typeId)` vmethod for a class's `VAR_*_LUA_TYPE_ID`.
|
||||
bool IsA(void *object, uintptr_t typeIdVar);
|
||||
|
||||
// Per-type sugar over `ResolveTypedObject`, so a call site cannot pair a
|
||||
// method with the wrong class's id. `idx` defaults to the `self` slot.
|
||||
void *ResolveTooltip(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveFrame(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveRegion(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveTexture(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveFontString(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveEditBox(void *L, int idx = 1, bool raiseError = true);
|
||||
void *ResolveModel(void *L, int idx = 1, bool raiseError = true);
|
||||
|
||||
// Every name these registrars bind is ALSO bound under `_G.ClassicAPI` —
|
||||
// `ClassicAPI.GetSpellInfo`, `ClassicAPI.C_Item.IsBound` — as an escape
|
||||
// hatch for names something else replaces later, and by value so
|
||||
// `ClassicAPI.X == X` until that happens. Automatic; a new module needs no
|
||||
// extra call. See `MirrorRegistration` in Game.cpp for why it lives here
|
||||
// and what it does not cover.
|
||||
//
|
||||
// Every registrar also RECORDS the registration for the API documentation
|
||||
// (`src/api/Documentation.cpp`): kind, table, name, registry, and which Lua
|
||||
// state's pass it ran in (in-game / glue / both). Only the FIRST pass of
|
||||
// each state records — later passes (every `/reload`) are no-ops — and a
|
||||
// registration made outside a `Run*Registrations` pass is not recorded at
|
||||
// all. The trailing `doc` argument is the descriptor (see `Game::Doc`
|
||||
// above); pass `nullptr` for a not-yet-documented registration and
|
||||
// `_classicapi_UndocumentedAPI()` lists it. The defaults exist only until
|
||||
// the sweep finishes; then they go and a missing descriptor stops compiling.
|
||||
|
||||
// Registers a single global Lua function (e.g. `GetSpellInfo`). The function
|
||||
// must use the WoW Lua C function ABI: `int __fastcall(void *L)`.
|
||||
void RegisterGlobalFunction(const char *name, CFunction func);
|
||||
void RegisterGlobalFunction(const char *name, CFunction func,
|
||||
const Doc::Function *doc = nullptr);
|
||||
|
||||
// Glue-state equivalent of `RegisterGlobalFunction`. Identical wire
|
||||
// (calls `FrameScript_RegisterFunction`); the engine routes the
|
||||
@@ -228,7 +430,8 @@ void RegisterGlobalFunction(const char *name, CFunction func);
|
||||
// its own 109 glue functions, and `VAR_LUA_STATE` still points at the
|
||||
// glue state. Calling outside that window would silently target the
|
||||
// wrong state.
|
||||
void RegisterGlueFunction(const char *name, CFunction func);
|
||||
void RegisterGlueFunction(const char *name, CFunction func,
|
||||
const Doc::Function *doc = nullptr);
|
||||
|
||||
// Frame-method registration entry: { name, func } pairs walked by the engine's
|
||||
// per-frame-type method-table iterator. Layout matches what the engine expects
|
||||
@@ -240,15 +443,26 @@ struct FrameMethodEntry {
|
||||
|
||||
// Registers a batch of methods on a per-frame-type registry (e.g.
|
||||
// GameTooltipMethodRegistry for `tooltip:Foo()` calls). `context` is the
|
||||
// registry address — see Offsets::VAR_*_METHOD_REGISTRY.
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count);
|
||||
// registry address — see Offsets::VAR_*_METHOD_REGISTRY. `docs` pairs
|
||||
// descriptors to the entries by NAME (any order, any subset).
|
||||
void RegisterFrameMethods(void *context, const FrameMethodEntry *table, int count,
|
||||
const Doc::Method *docs = nullptr, int docCount = 0);
|
||||
|
||||
// Registers `func` at `_G[tableName][methodName]`, creating the namespace
|
||||
// table if it doesn't already exist. This is how modern WoW C_*-style APIs
|
||||
// are bound — the engine has no built-in support for table-bound Lua
|
||||
// functions, so we manipulate the globals table directly via the Lua C API.
|
||||
void RegisterTableFunction(const char *tableName, const char *methodName,
|
||||
CFunction func);
|
||||
CFunction func, const Doc::Function *doc = nullptr);
|
||||
|
||||
// Binds `_G[alias]` to the SAME closure as `_G[tableName][methodName]`, by
|
||||
// value — the way the engine's own Lua-init snippet binds its short aliases
|
||||
// (`tinsert = table.insert`, `strlen = string.len`, …). Use it after
|
||||
// registering over a library function whose alias the snippet has already
|
||||
// captured, so the alias follows the replacement and `alias == table.method`
|
||||
// stays true. Mirrored under `_G.ClassicAPI` like every other registration.
|
||||
void RegisterGlobalAlias(const char *alias, const char *tableName, const char *methodName,
|
||||
const Doc::Function *doc = nullptr);
|
||||
|
||||
// Key/value pair for `RegisterIntegerEnum`. `key` becomes a field name
|
||||
// (PascalCase, matching Blizzard's `Enum.*` naming) and `value` is the
|
||||
@@ -261,8 +475,11 @@ struct EnumIntegerEntry {
|
||||
// Registers `_G[parent][sub] = { entries }` as an integer-valued enum
|
||||
// table, creating `_G[parent]` if needed. Used for Blizzard-style
|
||||
// `Enum.AddOnSecurityStatus = { Secure=0, Insecure=1, ... }` shapes.
|
||||
// `docSystem` is the documentation System the enumeration belongs to
|
||||
// ("SpellBook", "Item", …); the entries themselves are the documentation.
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count);
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem = nullptr);
|
||||
|
||||
// Set `t[key] = value` on the table currently at stack[-1] (the most
|
||||
// common shape used when populating a struct-style table mid-build).
|
||||
|
||||
+99
-110
@@ -11,7 +11,10 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "HookSecureFunc.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
@@ -19,14 +22,19 @@ namespace HookSecureFunc {
|
||||
|
||||
namespace {
|
||||
|
||||
// Names that must not be hooked when targeting `_G`. Hooking any of these
|
||||
// would either break taint propagation (the "secure" functions) or replace
|
||||
// core Lua language primitives whose behavior the engine depends on
|
||||
// internally — e.g. `pairs` is called from C in the iterator path; if Lua
|
||||
// `pairs` is replaced with a wrapper closure, the engine still gets the
|
||||
// original via its own table-method dispatch, but Lua-side iteration goes
|
||||
// through our wrapper, and the two diverging is a footgun. Modern WoW
|
||||
// rejects the hook outright; we mirror that. Sorted alphabetically.
|
||||
// Names that must not be hooked when targeting `_G`. This is the client's
|
||||
// own list, VERBATIM: patch 11.0.0 (The War Within, 2024-07-23) made
|
||||
// `hooksecurefunc` refuse exactly these 23 names with a "Cannot hook
|
||||
// function" error — the taint-relevant primitives (the "secure" family plus
|
||||
// the Lua primitives secure code paths run through). It is deliberately NOT
|
||||
// "every base-library function": 11.0 leaves `tostring`, `error`,
|
||||
// `loadstring`, `assert`, `collectgarbage`, … hookable, and includes names
|
||||
// that are not base-library functions at all. So do not derive this from
|
||||
// the engine's `luaopen_base` table (`0x00811E28`, 36 entries — see
|
||||
// BlizzardScriptAPI.md §3); that is a different set with a different
|
||||
// purpose. Names with no 1.12 counterpart (`wipe` is `table.wipe` here; the
|
||||
// secure family does not exist) stay for parity of the error an addon
|
||||
// sees. Sorted alphabetically.
|
||||
const char *const kUnhookableNames[] = {
|
||||
"getfenv", "getmetatable", "hooksecurefunc", "ipairs", "issecurevalue",
|
||||
"issecurevariable", "next", "pairs", "pcall", "pcallwithenv", "rawget",
|
||||
@@ -45,138 +53,113 @@ bool IsUnhookable(const char *name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Wrapper closure body — called when the hooked function is invoked.
|
||||
// Stack contains the wrapper's args (`[arg1..argN]`); the closure's
|
||||
// upvalues hold `(orig, callback)`.
|
||||
// upvalues hold `(orig, callback)`. 4.3.4's FrameScript_HookWrapper, less
|
||||
// its taint bookkeeping (1.12 has no taint):
|
||||
//
|
||||
// Modern `hooksecurefunc` semantics: original runs first, callback
|
||||
// runs after with the same args (returns ignored), original's
|
||||
// results propagate to the caller. No cap on return count. A buggy
|
||||
// callback must NOT prevent the original's results from reaching
|
||||
// the caller — that's why step (3) uses pcall.
|
||||
//
|
||||
// Stack-shuffling mirrors 3.3.5's `FUN_00817050` so the args sit at
|
||||
// the top for both calls without copying them twice; results sink
|
||||
// to the bottom and end up as the wrapper's natural return frame.
|
||||
int __fastcall HookWrapper(void *L) {
|
||||
// 1. orig(...) with lua_call — an error in the original propagates to
|
||||
// the caller, as it would unhooked.
|
||||
// 2. Results sink below the args, so they end up as the wrapper's
|
||||
// return frame without being copied again.
|
||||
// 3. callback(...) through lua_pcall with the engine's frame-script
|
||||
// message handler as errfunc: a broken hook is REPORTED the way any
|
||||
// script error is (the error frame / geterrorhandler), and the
|
||||
// original's results still reach the caller.
|
||||
int __fastcall Wrapper(void *L) {
|
||||
const int nargs = Game::Lua::GetTop(L);
|
||||
|
||||
// (1) orig(...) — pops orig + nargs args (copies), pushes results.
|
||||
// Stack after: `[arg1..argN, res1..resM]`.
|
||||
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(1));
|
||||
// (1) Stack after: `[arg1..argN, res1..resM]`.
|
||||
for (int i = 1; i <= nargs; ++i)
|
||||
Game::Lua::PushValue(L, i);
|
||||
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(1));
|
||||
Game::Lua::Insert(L, nargs + 1);
|
||||
Game::Lua::Call(L, nargs, Game::Lua::MULTRET);
|
||||
const int nresults = Game::Lua::GetTop(L) - nargs;
|
||||
|
||||
// (2) Shuffle results below the saved args. Each `Insert(L, 1)`
|
||||
// moves the current top down to position 1; running it
|
||||
// nresults times rotates the results block beneath the args.
|
||||
// (2) Each `Insert(L, 1)` moves the top value to the bottom; nresults
|
||||
// of them rotate the results block beneath the args.
|
||||
// Stack after: `[res1..resM, arg1..argN]`.
|
||||
for (int i = 0; i < nresults; ++i)
|
||||
Game::Lua::Insert(L, 1);
|
||||
|
||||
// (3) callback(...) via pcall — errors are caught and dropped so
|
||||
// the original's return values always reach the caller. This
|
||||
// is the load-bearing safety property of hooksecurefunc on
|
||||
// 3.x; on 1.12 it just means a buggy hook can't break the
|
||||
// hooked path. errfunc=0 → no traceback (3.3.5 uses
|
||||
// `registry["_TRACEBACK"]`, not a 1.12 convention; we'd be
|
||||
// discarding the formatted message anyway).
|
||||
// (3) Stack: `[res1..resM, errfunc, callback, arg1..argN]`. The
|
||||
// handler ref is the one every engine script pcall passes; 0 (not
|
||||
// yet set) falls back to no handler rather than a bad index.
|
||||
Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(2));
|
||||
Game::Lua::Insert(L, nresults + 1);
|
||||
if (Game::Lua::PCall(L, nargs, 0, 0) != 0) {
|
||||
// pcall failure leaves the error message on top; drop it.
|
||||
Game::Lua::Remove(L, -1);
|
||||
int errIdx = 0;
|
||||
const int errRef = Game::Read<int>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
|
||||
if (errRef > 0) {
|
||||
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, errRef);
|
||||
Game::Lua::Insert(L, nresults + 1);
|
||||
errIdx = nresults + 1;
|
||||
}
|
||||
Game::Lua::PCall(L, nargs, 0, errIdx);
|
||||
|
||||
// Stack: `[res1..resM]` — return the original's results verbatim.
|
||||
// Drop the handler and any error value — `[res1..resM]` remains.
|
||||
Game::Lua::SetTop(L, nresults);
|
||||
return nresults;
|
||||
}
|
||||
|
||||
// `hooksecurefunc(name, callback)` — hooks a global by name, target
|
||||
// table is `_G`.
|
||||
// `hooksecurefunc(table, name, callback)` — hooks `table[name]`.
|
||||
//
|
||||
// In both cases the original function runs first and its results
|
||||
// propagate; `callback` runs after with the same args (returns
|
||||
// discarded). The "secure" label refers to taint propagation in
|
||||
// 3.x+; on 1.12 it's just a post-hook with return preservation.
|
||||
void PushWrapper(void *L) {
|
||||
Game::Lua::PushCClosure(L, &Wrapper, 2);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// `hooksecurefunc([table,] "function", hookfunc)` — replaces
|
||||
// `table[name]` (default `_G`) with a wrapper that runs the original, then
|
||||
// `hookfunc` with the same args, and returns the original's results.
|
||||
// 4.3.4's Script_hooksecurefunc, less taint:
|
||||
// * a non-table first argument means `_G` is inserted at index 1, so
|
||||
// both forms share one index layout;
|
||||
// * the name only has to pass lua_isstring, so a number works (and is
|
||||
// looked up as its string, the same as lua_tostring makes it);
|
||||
// * the original is read with lua_gettable (inherited methods resolve
|
||||
// through __index) but the wrapper is written with lua_rawset, so a
|
||||
// target's __newindex never runs;
|
||||
// * the error texts are 4.3.4's.
|
||||
// The one addition is the 11.0 unhookable-name check for the `_G` form.
|
||||
int __fastcall Script_HookSecureFunc(void *L) {
|
||||
int targetIdx, nameIdx, callbackIdx;
|
||||
const int t1 = Game::Lua::Type(L, 1);
|
||||
const bool implicitGlobals = Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE;
|
||||
if (implicitGlobals) {
|
||||
Game::Lua::PushValue(L, Game::Lua::GLOBALS_INDEX);
|
||||
Game::Lua::Insert(L, 1);
|
||||
}
|
||||
|
||||
if (t1 == Game::Lua::TYPE_TABLE) {
|
||||
// Three-arg form: (table, name, callback)
|
||||
targetIdx = 1;
|
||||
nameIdx = 2;
|
||||
callbackIdx = 3;
|
||||
} else if (t1 == Game::Lua::TYPE_STRING) {
|
||||
// Two-arg form: (name, callback) — target = _G via pseudo-
|
||||
// index. Don't push _G with `lua_pushvalue(GLOBALS_INDEX)`:
|
||||
// it crashes (the pushvalue → setobj2s chain dereferences a
|
||||
// bad TValue pointer for that pseudo-index). `lua_gettable` /
|
||||
// `lua_settable` accept GLOBALS_INDEX as the table arg
|
||||
// directly, so we use it that way without pushing.
|
||||
targetIdx = Game::Lua::GLOBALS_INDEX;
|
||||
nameIdx = 1;
|
||||
callbackIdx = 2;
|
||||
} else {
|
||||
Game::Lua::Error(L,
|
||||
"hooksecurefunc(table, name, callback) or hooksecurefunc(name, callback)");
|
||||
if (!Game::Lua::IsString(L, 2) ||
|
||||
Game::Lua::Type(L, 3) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Error(L, "Usage: hooksecurefunc([table,] \"function\", hookfunc)");
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::SetTop(L, 3); // [t, name, hookfunc]
|
||||
// Converts a number name to its string in place; the string stays on
|
||||
// the stack, so the pointer outlives every use below.
|
||||
const char *name = Game::Lua::ToString(L, 2);
|
||||
|
||||
// Block the hook when targeting `_G` implicitly and the name is in the
|
||||
// unhookable set. Not applied to the three-arg form (table targets):
|
||||
// hooking `MyLib.pairs` is fine even if Lua's global `pairs` is not,
|
||||
// and a caller who passes `_G` explicitly is opting in deliberately.
|
||||
if (implicitGlobals && IsUnhookable(name)) {
|
||||
Game::Lua::Error(L, "hooksecurefunc: function is unhookable");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Game::Lua::Type(L, nameIdx) != Game::Lua::TYPE_STRING) {
|
||||
Game::Lua::Error(L, "hooksecurefunc: name must be a string");
|
||||
return 0;
|
||||
}
|
||||
if (Game::Lua::Type(L, callbackIdx) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Error(L, "hooksecurefunc: callback must be a function");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Block the hook when targeting `_G` and the name is in the unhookable
|
||||
// set. We don't apply this to the three-arg form (table targets):
|
||||
// hooking `MyLib.pairs` is fine even if Lua's global `pairs` is not —
|
||||
// the blacklist is specifically about `_G[name]`. A user who passes
|
||||
// `_G` explicitly as the table target is opting in deliberately and we
|
||||
// don't second-guess it.
|
||||
if (targetIdx == Game::Lua::GLOBALS_INDEX) {
|
||||
const char *name = Game::Lua::ToString(L, nameIdx);
|
||||
if (IsUnhookable(name)) {
|
||||
Game::Lua::Error(L,
|
||||
"hooksecurefunc: function is unhookable");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch target[name] — the original function. `gettable` uses
|
||||
// metamethods, so frame methods (resolved via the frame
|
||||
// metatable's __index) come back as callable closures, exactly
|
||||
// what we want to wrap.
|
||||
Game::Lua::PushValue(L, nameIdx);
|
||||
Game::Lua::GetTable(L, targetIdx);
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::GetTable(L, 1); // [t, name, hookfunc, orig]
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Error(L, "hooksecurefunc: target field is not a function");
|
||||
Game::Lua::Error(L, "hooksecurefunc(): %s is not a function", name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Push callback as the second upvalue; orig is already on top
|
||||
// from the GetTable above.
|
||||
Game::Lua::PushValue(L, callbackIdx);
|
||||
|
||||
// Build the wrapper as a C closure with (orig, callback) as the
|
||||
// two upvalues. PushCClosure pops the upvalues from the stack
|
||||
// and pushes the closure.
|
||||
Game::Lua::PushCClosure(L, &HookWrapper, 2);
|
||||
|
||||
// Install: target[name] = wrapper. SetTable expects the value
|
||||
// on top and the key just below; push them in that order.
|
||||
Game::Lua::PushValue(L, nameIdx);
|
||||
Game::Lua::PushValue(L, -2); // duplicate the closure
|
||||
Game::Lua::SetTable(L, targetIdx);
|
||||
|
||||
Game::Lua::Insert(L, 3); // [t, name, orig, hookfunc]
|
||||
PushWrapper(L); // [t, name, wrapper]
|
||||
Game::Lua::RawSet(L, 1); // t[name] = wrapper
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -185,7 +168,13 @@ int __fastcall Script_HookSecureFunc(void *L) {
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("hooksecurefunc", &Script_HookSecureFunc);
|
||||
}
|
||||
// 4.3.4 registers it from FrameScript_Initialize, which runs for the glue
|
||||
// state (Client.cpp) as well as the world (GameUI.cpp).
|
||||
static void RegisterGlueFunctions() {
|
||||
Game::Lua::RegisterGlueFunction("hooksecurefunc", &Script_HookSecureFunc);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
static const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlueFunctions};
|
||||
|
||||
} // namespace HookSecureFunc
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The one hook wrapper `hooksecurefunc` and `Frame:HookScript` both install —
|
||||
// 4.3.4 uses its FrameScript_HookWrapper for both, so a hooked function and a
|
||||
// hooked script behave the same way.
|
||||
namespace HookSecureFunc {
|
||||
|
||||
// The wrapper closure body. Upvalue 1 = the original, upvalue 2 = the hook.
|
||||
// Runs the original with the wrapper's args (its errors propagate), then the
|
||||
// hook with the same args under the engine's script error handler, and
|
||||
// returns the original's results. Exposed so readers can recognize the
|
||||
// closure by its C function.
|
||||
int __fastcall Wrapper(void *L);
|
||||
|
||||
// Pops the hook (top) and the original (below it) and pushes the wrapper
|
||||
// closure over them.
|
||||
void PushWrapper(void *L);
|
||||
|
||||
} // namespace HookSecureFunc
|
||||
+2987
-279
File diff suppressed because it is too large
Load Diff
+18
-14
@@ -25,6 +25,7 @@
|
||||
// macro: "macro", macroIndex (1-based)
|
||||
// bag item: "item", nil (see note below)
|
||||
// item-by-id: "item", itemID
|
||||
// equipment set: "equipmentset", setName
|
||||
//
|
||||
// The subType is always "spell" for spell-type entries — the engine
|
||||
// helper hardcodes pet=0 for entries on this table. Pet-bar actions
|
||||
@@ -47,6 +48,8 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Slot.h"
|
||||
#include "equipmentset/Data.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -62,19 +65,6 @@ uint32_t ReadEntry(int slot0) {
|
||||
return base[slot0];
|
||||
}
|
||||
|
||||
// Returns 1-based macro slot index if `key` matches any entry in the
|
||||
// macro-slot map, or 0 if no match. `key` is the action entry with the
|
||||
// `0x40000000` type bit stripped (i.e. `entry & 0xBFFFFFFF`).
|
||||
int FindMacroSlot(uint32_t key) {
|
||||
auto *map = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MACRO_SLOT_MAP));
|
||||
for (int i = 0; i < Offsets::MACRO_SLOT_MAP_COUNT; ++i) {
|
||||
if (map[i] == key)
|
||||
return i + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetActionInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetActionInfo(slot)");
|
||||
@@ -117,7 +107,7 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
|
||||
if (type == Offsets::ACTION_TYPE_BAG_OR_MACRO) {
|
||||
const uint32_t key = entry & Offsets::ACTION_PAYLOAD_MASK_BAG_OR_MACRO;
|
||||
const int macroSlot = FindMacroSlot(key);
|
||||
const int macroSlot = Action::Slot::MacroSlotForID(key);
|
||||
if (macroSlot > 0) {
|
||||
Game::Lua::PushString(L, "macro");
|
||||
Game::Lua::PushNumber(L, static_cast<double>(macroSlot));
|
||||
@@ -138,6 +128,20 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (type == Offsets::ACTION_TYPE_EQUIPMENT_SET) {
|
||||
// 3.3.5 `Script_GetActionInfo` (`FUN_005a8f10`): the type and the
|
||||
// set's name.
|
||||
const EquipmentSet::Set *set = EquipmentSet::Data::FindByID(
|
||||
entry & ~static_cast<uint32_t>(Offsets::ACTION_TYPE_EQUIPMENT_SET));
|
||||
if (set == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushString(L, "equipmentset");
|
||||
Game::Lua::PushString(L, set->name.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Unknown type tag — surface as nil so callers don't get garbage.
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetActionCooldown` / `GetActionCount` / `IsConsumableAction` for macro
|
||||
// slots whose `#showtooltip` / `#show` resolved to an ITEM. A spell
|
||||
// resolution needs none of this — the engine reads a macro slot's spell from
|
||||
// its primary-spell cache, which `Macro::ShowTooltip` writes — but the engine
|
||||
// has no per-macro item field, so the item forms of these three are answered
|
||||
// here, each with the engine's own helper for the item-by-ID slot case:
|
||||
//
|
||||
// - cooldown → `FUN_ITEM_QUERY_COOLDOWN` via `Item::Cooldown::PushCooldown`
|
||||
// (the same triple `GetContainerItemCooldown` returns);
|
||||
// - count → `FUN_ACTION_ITEM_COUNT`, the value `UseAction` caches per
|
||||
// item-by-ID slot;
|
||||
// - consumable → the rule of the inner `IsConsumableAction` (`0x004E5250`):
|
||||
// ammo / thrown, or any on-use spell slot with negative
|
||||
// charges.
|
||||
//
|
||||
// Registered in front of the engine globals (last registration wins) and a
|
||||
// strict superset of them: every other slot tail-calls the engine function
|
||||
// with the untouched stack.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Cooldown.h"
|
||||
#include "item/Record.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::ItemState {
|
||||
|
||||
namespace {
|
||||
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
using ItemCount_t = uint32_t(__fastcall *)(uint32_t itemID);
|
||||
|
||||
int Engine(uintptr_t fn, void *L) {
|
||||
return reinterpret_cast<ScriptFn_t>(fn)(L);
|
||||
}
|
||||
|
||||
// True (and fills `info`) when stack[1] is a slot whose macro resolved to an item.
|
||||
bool ItemTarget(void *L, Macro::ShowTooltip::Info *info) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return false;
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
return Macro::ShowTooltip::ForSlot(slot0, info) &&
|
||||
info->target == Macro::ShowTooltip::Target::Item;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetActionCooldown(void *L) {
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (!ItemTarget(L, &info))
|
||||
return Engine(Offsets::FUN_SCRIPT_GET_ACTION_COOLDOWN, L);
|
||||
Item::Cooldown::PushCooldown(L, info.itemID);
|
||||
return 3;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetActionCount(void *L) {
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (!ItemTarget(L, &info))
|
||||
return Engine(Offsets::FUN_SCRIPT_GET_ACTION_COUNT, L);
|
||||
const uint32_t count = reinterpret_cast<ItemCount_t>(Offsets::FUN_ACTION_ITEM_COUNT)(
|
||||
static_cast<uint32_t>(info.itemID));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(count));
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool IsConsumable(int itemID) {
|
||||
const uint8_t *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
const uint32_t invType = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
if (invType == Offsets::INVTYPE_AMMO || invType == Offsets::INVTYPE_THROWN)
|
||||
return true;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i) {
|
||||
const uint32_t spellID = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_ID + i * 4);
|
||||
const uint32_t trigger = Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_TRIGGER + i * 4);
|
||||
const int32_t charges = Game::Read<int32_t>(record, Offsets::OFF_ITEMSTATS_SPELL_CHARGES + i * 4);
|
||||
if (spellID != 0 && trigger == Offsets::ITEM_SPELLTRIGGER_ON_USE && charges < 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsConsumableAction(void *L) {
|
||||
Macro::ShowTooltip::Info info;
|
||||
if (!ItemTarget(L, &info))
|
||||
return Engine(Offsets::FUN_SCRIPT_IS_CONSUMABLE_ACTION, L);
|
||||
if (IsConsumable(info.itemID))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetActionCooldown", &Script_GetActionCooldown);
|
||||
Game::Lua::RegisterGlobalFunction("GetActionCount", &Script_GetActionCount);
|
||||
Game::Lua::RegisterGlobalFunction("IsConsumableAction", &Script_IsConsumableAction);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::ItemState
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "action/Slot.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Action::Slot {
|
||||
|
||||
int MacroSlotForID(uint32_t macroID) {
|
||||
if (macroID == 0)
|
||||
return 0;
|
||||
auto *map = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MACRO_SLOT_MAP));
|
||||
for (int i = 0; i < Offsets::MACRO_SLOT_MAP_COUNT; ++i) {
|
||||
if (map[i] == macroID)
|
||||
return i + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t MacroIDForSlot(int slot0) {
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
auto *table = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE));
|
||||
const uint32_t entry = table[slot0];
|
||||
if (entry == 0 || (entry & Offsets::ACTION_TYPE_MASK) != Offsets::ACTION_TYPE_BAG_OR_MACRO)
|
||||
return 0;
|
||||
const uint32_t key = entry & Offsets::ACTION_PAYLOAD_MASK_BAG_OR_MACRO;
|
||||
return MacroSlotForID(key) != 0 ? key : 0;
|
||||
}
|
||||
|
||||
} // namespace Action::Slot
|
||||
@@ -0,0 +1,36 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Action-slot ↔ macro decoding shared by every module that has to answer
|
||||
// "which macro sits in this action slot" (`GetActionInfo`, the
|
||||
// `#showtooltip` display overrides). The action table stores macroIDs under
|
||||
// the ambiguous `0x40000000` tag; the macro-slot map says which of those ids
|
||||
// are macros (the rest are bag items). Pure C — no Lua stack.
|
||||
|
||||
namespace Action::Slot {
|
||||
|
||||
// 1-based macro slot holding `macroID` — the same linear scan of
|
||||
// `VAR_MACRO_SLOT_MAP` the engine's `FUN_MACRO_ID_TO_SLOT` does — or 0 when
|
||||
// no macro has that id (0 is never a macroID).
|
||||
int MacroSlotForID(uint32_t macroID);
|
||||
|
||||
// The macroID in action slot `slot0` (0-based, `< ACTION_TABLE_MAX_SLOTS`),
|
||||
// or 0 when the slot is empty, isn't `0x40000000`-tagged, or holds a bag
|
||||
// item rather than a macro.
|
||||
uint32_t MacroIDForSlot(int slot0);
|
||||
|
||||
} // namespace Action::Slot
|
||||
@@ -0,0 +1,33 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The `ActionButtonUseKeyDown` cvar — registered by 4.3.4's engine
|
||||
// (GameUI_CVar.cpp, "Activate the action button on a keydown") and read only
|
||||
// by FrameXML's action-button key handlers, which this project's addon ports
|
||||
// (Util/ActionButton.lua). The engine half is the registration. The default
|
||||
// is 0, keeping 1.12's action buttons firing on key release; 4.3.4 shipped 1.
|
||||
|
||||
#include "Game.h"
|
||||
#include "cvar/Factory.h"
|
||||
|
||||
namespace Action::UseKeyDown {
|
||||
|
||||
namespace {
|
||||
|
||||
void Register() { CVar::Factory::Register("ActionButtonUseKeyDown", "0"); }
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::UseKeyDown
|
||||
@@ -137,7 +137,7 @@ bool ExtractTocVersion(const char *content, size_t size,
|
||||
size_t n = 0;
|
||||
if (outSize == 0)
|
||||
return false;
|
||||
if (!AddOns::Toc::FindValue(content, size, "## Version:", &v, &n) || n == 0) {
|
||||
if (!AddOns::Toc::FindValue(content, size, "Version", &v, &n) || n == 0) {
|
||||
out[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
+24
-10
@@ -26,6 +26,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/LoadState.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -251,19 +252,23 @@ int __fastcall Script_GetAddOnSecurity(void *L) {
|
||||
|
||||
// `C_AddOns.IsAddOnLoaded(indexOrName)` → `(loadedOrLoading, loaded)`.
|
||||
//
|
||||
// Modern WoW distinguishes "load-in-progress" from "fully loaded" —
|
||||
// matters for LoD addons whose load is split across multiple
|
||||
// `LoadAddOn` callbacks. Vanilla 1.12's `FUN_0051F240` is fully
|
||||
// synchronous: the loaded byte at `entry+0x18` flips from 0 → 1
|
||||
// inside a single call, so the "loading" state is never observable
|
||||
// from Lua. We return the same boolean twice — `loadedOrLoading`
|
||||
// and `loaded` are identical here.
|
||||
// Modern WoW distinguishes "load-in-progress" from "fully loaded", and
|
||||
// the two really are distinguishable here: the engine's loaded byte
|
||||
// (`entry+0x18`) is set at the START of a load, before the addon's files
|
||||
// run, so the byte alone answers `loadedOrLoading`, not `loaded`. The
|
||||
// window is reachable from Lua — an addon's own file-scope code, and any
|
||||
// dependency pulled in during it, run while the byte already reads set.
|
||||
//
|
||||
// So `loadedOrLoading` is the engine's byte, and `loaded` additionally
|
||||
// requires the addon not to be mid-load, which `AddOns::LoadState`
|
||||
// tracks from the loader itself.
|
||||
//
|
||||
// Resolves both numeric indices (via `FUN_ADDON_GET_BY_INDEX`) and
|
||||
// string names. `IS_LOADED` accepts an entry pointer as its name arg
|
||||
// because the entry's first 12 bytes are the inline NUL-terminated
|
||||
// directory name (same as the other per-field accessors), so we can
|
||||
// feed it either an entry or a Lua-side string transparently.
|
||||
// feed it either an entry or a Lua-side string transparently — and that
|
||||
// same pointer is the name `LoadState` matches on.
|
||||
int __fastcall Script_IsAddOnLoaded(void *L) {
|
||||
const uint8_t *entry = ResolveAddOnName(L);
|
||||
if (entry == nullptr) {
|
||||
@@ -273,8 +278,11 @@ int __fastcall Script_IsAddOnLoaded(void *L) {
|
||||
}
|
||||
auto isLoaded = reinterpret_cast<EntryByteFn_t>(
|
||||
Offsets::FUN_ADDON_IS_LOADED);
|
||||
const bool loaded = isLoaded(entry) != 0;
|
||||
Game::Lua::PushBool(L, loaded);
|
||||
const bool loadedOrLoading = isLoaded(entry) != 0;
|
||||
const bool loaded =
|
||||
loadedOrLoading &&
|
||||
!AddOns::LoadState::IsLoading(reinterpret_cast<const char *>(entry));
|
||||
Game::Lua::PushBool(L, loadedOrLoading);
|
||||
Game::Lua::PushBool(L, loaded);
|
||||
return 2;
|
||||
}
|
||||
@@ -385,6 +393,12 @@ static void RegisterLuaFunctions() {
|
||||
&Script_DoesAddOnExist);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "IsAddOnLoaded",
|
||||
&Script_IsAddOnLoaded);
|
||||
// The stock global, mirrored into the namespace — the engine's own
|
||||
// handler already has the Lua C ABI and the modern return shape.
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_AddOns", "LoadAddOn",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_LOAD_ADDON)));
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnOptionalDependencies",
|
||||
&Script_GetAddOnOptionalDependencies);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Tracks in-progress addon loads so `C_AddOns.IsAddOnLoaded` can report
|
||||
// `loadedOrLoading` and `loaded` separately. See `LoadState.h` for why the
|
||||
// engine's own byte can't answer both.
|
||||
//
|
||||
// Loads nest — an addon's dependencies load inside its own call — so this
|
||||
// keeps a stack rather than a single name. The name is copied rather than
|
||||
// stored by pointer: `LoadAddOn` passes a Lua string, which does not
|
||||
// outlive the call.
|
||||
|
||||
#include "addons/LoadState.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace AddOns::LoadState {
|
||||
|
||||
namespace {
|
||||
|
||||
// Dependency chains are shallow; anything deeper simply isn't tracked
|
||||
// (the depth counter still balances, so the stack can't desync).
|
||||
constexpr int kMaxDepth = 16;
|
||||
constexpr int kMaxName = 64;
|
||||
|
||||
char g_stack[kMaxDepth][kMaxName];
|
||||
int g_depth = 0;
|
||||
|
||||
bool EqualsIgnoreCase(const char *a, const char *b) {
|
||||
for (; *a != '\0' && *b != '\0'; ++a, ++b) {
|
||||
char ca = *a;
|
||||
char cb = *b;
|
||||
if (ca >= 'A' && ca <= 'Z')
|
||||
ca = static_cast<char>(ca - 'A' + 'a');
|
||||
if (cb >= 'A' && cb <= 'Z')
|
||||
cb = static_cast<char>(cb - 'A' + 'a');
|
||||
if (ca != cb)
|
||||
return false;
|
||||
}
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
using LoadOne_t = unsigned int(__fastcall *)(const char *name, char flag,
|
||||
void *progressCtx);
|
||||
LoadOne_t s_loadOne_o = nullptr;
|
||||
|
||||
unsigned int __fastcall LoadOne_h(const char *name, char flag, void *progressCtx) {
|
||||
const int slot = g_depth;
|
||||
if (slot >= 0 && slot < kMaxDepth)
|
||||
Common::BoundedCopy(g_stack[slot], name, kMaxName);
|
||||
++g_depth;
|
||||
|
||||
const unsigned int result = s_loadOne_o(name, flag, progressCtx);
|
||||
|
||||
--g_depth;
|
||||
if (g_depth < 0)
|
||||
g_depth = 0; // defensive: never let the stack run negative
|
||||
return result;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookLoadOne{
|
||||
Offsets::FUN_ADDON_LOAD_ONE,
|
||||
reinterpret_cast<void *>(&LoadOne_h),
|
||||
reinterpret_cast<void **>(&s_loadOne_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsLoading(const char *name) {
|
||||
if (name == nullptr || *name == '\0')
|
||||
return false;
|
||||
const int depth = (g_depth < kMaxDepth) ? g_depth : kMaxDepth;
|
||||
for (int i = 0; i < depth; ++i)
|
||||
if (EqualsIgnoreCase(g_stack[i], name))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace AddOns::LoadState
|
||||
@@ -0,0 +1,33 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// `AddOns::LoadState` — tracks which addons are mid-load.
|
||||
//
|
||||
// The engine's own "loaded" byte (`entry+0x18`) is set at the START of a
|
||||
// load, before the addon's files run, so on its own it can't tell
|
||||
// "loading" from "loaded". That distinction is observable: an addon's
|
||||
// file-scope code, and every dependency pulled in during it, execute
|
||||
// while the byte already reads set.
|
||||
//
|
||||
// A co-hook on the per-addon loader keeps a stack of the names currently
|
||||
// being loaded, which is the missing half of
|
||||
// `C_AddOns.IsAddOnLoaded`'s `(loadedOrLoading, loaded)` pair.
|
||||
namespace AddOns::LoadState {
|
||||
|
||||
// True while `name` is inside the engine's per-addon load call. Matching
|
||||
// is case-insensitive, as addon-name matching is everywhere else.
|
||||
bool IsLoading(const char *name);
|
||||
|
||||
} // namespace AddOns::LoadState
|
||||
@@ -12,17 +12,17 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_AddOns.GetAddOnLocalTable(name)` — the public, gated view of an addon's
|
||||
// private namespace table (the `local _, addonTable = ...` second vararg the
|
||||
// transpiler injects; see luasyntax/AddonNamespace.h).
|
||||
// private namespace table (the `local _, addonTable = ...` second vararg each
|
||||
// addon file is called with; see luasyntax/AddonNamespace.h).
|
||||
//
|
||||
// Retail added this in 11.0 alongside the `## AllowAddOnTableAccess` TOC
|
||||
// directive: one addon can read another's local table ONLY when the target
|
||||
// opts in. We mirror that gate:
|
||||
// 1. the named addon is currently LOADED, and
|
||||
// 2. its `.toc` declares `## AllowAddOnTableAccess: 1`.
|
||||
// Either check failing returns nil. The internal preamble path (an addon
|
||||
// receiving its OWN table) is a different, ungated function (`__addonns`) —
|
||||
// an addon never needs permission for its own namespace.
|
||||
// Either check failing returns nil. An addon receiving its OWN table is a
|
||||
// different, ungated path (its files are called with it at load) — an addon
|
||||
// never needs permission for its own namespace.
|
||||
//
|
||||
// Directive spelling verified against the 1.15.8 Classic Era binary (both the
|
||||
// `AllowAddOnTableAccess:` TOC key and the `GetAddOnLocalTable` name are
|
||||
@@ -77,7 +77,7 @@ bool TocAllowsTableAccess(const char *name) {
|
||||
size_t n = 0;
|
||||
bool allowed = false;
|
||||
if (AddOns::Toc::FindValue(static_cast<const char *>(buf), size,
|
||||
"## AllowAddOnTableAccess:", &v, &n)) {
|
||||
"AllowAddOnTableAccess", &v, &n)) {
|
||||
int value = 0;
|
||||
bool anyDigit = false;
|
||||
for (size_t k = 0; k < n && v[k] >= '0' && v[k] <= '9'; ++k) {
|
||||
|
||||
+21
-63
@@ -16,16 +16,15 @@
|
||||
// a stock 1.12 client, each fixed here with the engine's own machinery:
|
||||
//
|
||||
// 1. FILE VISIBILITY. Every relative-path read resolves through a
|
||||
// loose-file hash index built ONCE at boot (see Offsets.h,
|
||||
// `FUN_VFS_INDEX_SUBTREE`); files created after boot — a new
|
||||
// addon's TOC, a new .lua in an existing addon, a freshly written
|
||||
// SavedVariables file — are invisible until restart. The boot
|
||||
// indexer is dedup-safe and works on subtrees, so we re-run it per
|
||||
// /reload on the only two subtrees whose contents can change what
|
||||
// a /reload loads: `Interface\AddOns` and `WTF\Account`. New files
|
||||
// in EXISTING addons need nothing more — the per-addon loader
|
||||
// `FUN_0051F240` re-reads the TOC from disk on every load pass, so
|
||||
// once the files are visible they load.
|
||||
// loose-file hash index (see Offsets.h, `FUN_VFS_INDEX_CLEAR`);
|
||||
// a file created after the index was built — a new addon's TOC, a
|
||||
// new .lua in an existing addon — is invisible until it is rebuilt.
|
||||
// We call the engine's own clear, the same one the engine calls
|
||||
// after writing a new SavedVariables file: the next lookup rebuilds
|
||||
// the index from disk as boot does, so new files appear and deleted
|
||||
// ones drop out. New files in EXISTING addons need nothing more —
|
||||
// the per-addon loader `FUN_0051F240` re-reads the TOC from disk on
|
||||
// every load pass, so once the files are visible they load.
|
||||
//
|
||||
// 2. REGISTRY MEMBERSHIP. The addon registry is built once at login;
|
||||
// new folders are never walked. We replay the login scan's disk
|
||||
@@ -78,10 +77,9 @@
|
||||
// registration gets. New entries come from the same parser call
|
||||
// `Addons::Embedded` has exercised every login.
|
||||
//
|
||||
// Thread note: the loose-file index has no lock, but the engine itself
|
||||
// builds it lazily on first access and we mutate it only during the
|
||||
// reload loading screen on the main thread — the same context in which
|
||||
// the engine does all its own file work.
|
||||
// Thread note: the loose-file index has no lock in 1.12. We clear it on
|
||||
// the main thread during the reload loading screen — the same context
|
||||
// in which the engine's own unload pass clears it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -102,11 +100,7 @@ namespace {
|
||||
|
||||
// ── Engine entry points (all verified by disassembly; see Offsets.h) ──
|
||||
|
||||
using FindOpen_t = void *(__stdcall *)(const char *dirPath);
|
||||
using FindClose_t = void(__stdcall *)(void *findBlock);
|
||||
using IndexSubtree_t = void(__fastcall *)(const char *basePath,
|
||||
const char *relSubdir,
|
||||
void *findHandle);
|
||||
using IndexClear_t = void(__cdecl *)();
|
||||
using ScanDiskDirs_t = int(__fastcall *)(const char *basePath,
|
||||
const char *pattern, void *callback,
|
||||
void *userParam, int includeHidden);
|
||||
@@ -163,45 +157,9 @@ uintptr_t ResolveEntryByName(const char *name) {
|
||||
|
||||
// ── Step 1: loose-file index refresh ──────────────────────────────────
|
||||
|
||||
// Register new on-disk files under `relSubdir` with the frozen index,
|
||||
// exactly as the boot walk would have (the indexer skips every file
|
||||
// already present). No-op when the directory doesn't exist —
|
||||
// `FUN_VFS_FIND_OPEN` requires an existing directory and returns NULL.
|
||||
void ReindexSubtree(const char *base, const char *relSubdir) {
|
||||
char dir[260];
|
||||
std::snprintf(dir, sizeof(dir), "%s\\%s", base, relSubdir);
|
||||
auto findOpen = reinterpret_cast<FindOpen_t>(Offsets::FUN_VFS_FIND_OPEN);
|
||||
auto findClose = reinterpret_cast<FindClose_t>(Offsets::FUN_VFS_FIND_CLOSE);
|
||||
void *handle = findOpen(dir);
|
||||
if (handle == nullptr)
|
||||
return;
|
||||
auto index =
|
||||
reinterpret_cast<IndexSubtree_t>(Offsets::FUN_VFS_INDEX_SUBTREE);
|
||||
index(base, relSubdir, handle);
|
||||
findClose(handle);
|
||||
}
|
||||
|
||||
// Drop the loose-file index; the next file lookup rebuilds it from disk.
|
||||
void RefreshLooseFileIndex() {
|
||||
// The boot walk's base choice (`FUN_00646EA0`): the recorded game
|
||||
// dir if it opens as a directory, else ".". Index keys are relative
|
||||
// to this, so it must match or the new keys would never be hit.
|
||||
auto findOpen = reinterpret_cast<FindOpen_t>(Offsets::FUN_VFS_FIND_OPEN);
|
||||
auto findClose = reinterpret_cast<FindClose_t>(Offsets::FUN_VFS_FIND_CLOSE);
|
||||
const char *base =
|
||||
reinterpret_cast<const char *>(Offsets::VAR_VFS_BASE_PATH);
|
||||
void *probe = findOpen(base);
|
||||
if (probe != nullptr)
|
||||
findClose(probe);
|
||||
else
|
||||
base = ".";
|
||||
|
||||
// The only subtrees whose post-boot changes affect what a /reload
|
||||
// loads: addon content, and the SavedVariables the unload pass
|
||||
// wrote moments ago (a first-ever SV file is otherwise invisible —
|
||||
// the stock client loses a freshly installed addon's settings on
|
||||
// /reload for exactly this reason).
|
||||
ReindexSubtree(base, "Interface\\AddOns");
|
||||
ReindexSubtree(base, "WTF\\Account");
|
||||
reinterpret_cast<IndexClear_t>(Offsets::FUN_VFS_INDEX_CLEAR)();
|
||||
}
|
||||
|
||||
// ── Step 1.5: `##` metadata refresh (evict + re-register on change) ──
|
||||
@@ -437,13 +395,13 @@ void RebuildDisplayArray() {
|
||||
}
|
||||
|
||||
void Run() {
|
||||
// Registry populated (login scan ran) and loose index built — both
|
||||
// always true by the first in-world LoadScriptFunctions, but these
|
||||
// are the states the steps below mutate, so gate explicitly.
|
||||
// Registry populated (login scan ran) — always true by the first
|
||||
// in-world LoadScriptFunctions, but it is the state the steps below
|
||||
// mutate, so gate explicitly. No gate on the index being built: the
|
||||
// unload pass clears it whenever it wrote a new SV file, and the
|
||||
// clear below is safe either way.
|
||||
if (*reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_INITIALIZED)) == 0 ||
|
||||
*reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_VFS_INDEX_READY)) == 0)
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_INITIALIZED)) == 0)
|
||||
return;
|
||||
|
||||
RefreshLooseFileIndex();
|
||||
|
||||
@@ -35,18 +35,19 @@
|
||||
// The engine STILL runs its own SavedVariables load at the normal step, right
|
||||
// after the files. Left alone, that re-load would overwrite any value the
|
||||
// addon wrote to its SavedVariable at file scope before ADDON_LOADED. So we
|
||||
// also co-hook FUN_LUA_LOAD_FILE and suppress that one redundant re-load: each
|
||||
// path we pre-load is remembered, and the engine's imminent load of that exact
|
||||
// path is skipped once. This makes the behavior match true
|
||||
// LoadSavedVariablesFirst — file-scope reads AND writes both survive. Our own
|
||||
// early load calls the trampoline directly, so it is never self-suppressed.
|
||||
// FUN_LUA_LOAD_FILE loads FILES only (not RunScript string execution), so the
|
||||
// extra hook is a cold, addon-load-time path.
|
||||
// also suppress that one redundant re-load, through Script::LoadFile's shared
|
||||
// FUN_LUA_LOAD_FILE hook: each path we pre-load is remembered, and the
|
||||
// engine's imminent load of that exact path is skipped once. This makes the
|
||||
// behavior match true LoadSavedVariablesFirst — file-scope reads AND writes
|
||||
// both survive. Our own early load calls Script::LoadFile::Original, so it is
|
||||
// never self-suppressed. FUN_LUA_LOAD_FILE loads FILES only (not RunScript
|
||||
// string execution), so this is a cold, addon-load-time path.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "script/LoadFile.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -67,14 +68,12 @@ using AddOns::EngineIO::FileExistsFn;
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
using LoadTocFilesFn = uint32_t(__fastcall *)(char *tocPath, int *a2, int *a3);
|
||||
using LuaLoadFileFn = uint32_t(__fastcall *)(const char *path, void *a2, void *a3);
|
||||
using NameFn = const char *(*)(); // no-arg readers (return in EAX)
|
||||
|
||||
LoadTocFilesFn g_orig = nullptr;
|
||||
LuaLoadFileFn g_origLuaLoad = nullptr;
|
||||
|
||||
// Full SV paths we pre-loaded whose imminent engine re-load must be skipped
|
||||
// once. Populated in LoadSavedVarsEarly, drained by LuaLoad_h. Only ever holds
|
||||
// once. Populated in LoadSavedVarsEarly, drained by SuppressReload. Only ever holds
|
||||
// a handful of entries (one addon's SV files, briefly). Main-thread only.
|
||||
std::vector<std::string> g_pendingSuppress;
|
||||
|
||||
@@ -148,18 +147,18 @@ SvFlags ReadSvFlags(const char *tocPath) {
|
||||
|
||||
const char *v = nullptr;
|
||||
size_t n = 0;
|
||||
if (AddOns::Toc::FindValue(b, size, "## LoadSavedVariablesFirst:", &v, &n))
|
||||
if (AddOns::Toc::FindValue(b, size, "LoadSavedVariablesFirst", &v, &n))
|
||||
f.first = NonzeroFlag(v, n);
|
||||
// Gate each SV file on the DECLARATION, not mere file existence — mirroring
|
||||
// the engine, which loads the account file iff `## SavedVariables` is
|
||||
// declared and the per-char file iff `## SavedVariablesPerCharacter` is.
|
||||
// (The trailing `:` keeps "## SavedVariables:" from matching the
|
||||
// "## SavedVariablesPerCharacter:" line.) Otherwise a stale SV file from a
|
||||
// (FindValue requires `:` right after the key, so "SavedVariables" never
|
||||
// matches the "## SavedVariablesPerCharacter:" line.) Otherwise a stale SV file from a
|
||||
// scope the addon no longer declares would be executed early — Lua the
|
||||
// engine never runs — and queued for a suppression the engine never drains.
|
||||
f.account = AddOns::Toc::FindValue(b, size, "## SavedVariables:", &v, &n) && n > 0;
|
||||
f.account = AddOns::Toc::FindValue(b, size, "SavedVariables", &v, &n) && n > 0;
|
||||
f.perChar =
|
||||
AddOns::Toc::FindValue(b, size, "## SavedVariablesPerCharacter:", &v, &n) && n > 0;
|
||||
AddOns::Toc::FindValue(b, size, "SavedVariablesPerCharacter", &v, &n) && n > 0;
|
||||
|
||||
reinterpret_cast<SMemFreeFn>(static_cast<uintptr_t>(Offsets::FUN_STORM_SMEM_FREE))(
|
||||
buf, __FILE__, __LINE__, 0);
|
||||
@@ -173,12 +172,10 @@ bool FileExists(const char *path) {
|
||||
|
||||
// Load a SavedVariables file we found, then mark its path so the engine's own
|
||||
// re-load of it (right after the addon's files) is suppressed — preserving any
|
||||
// file-scope write. Calls the trampoline directly so this load is never itself
|
||||
// suppressed by our LuaLoad_h hook.
|
||||
// file-scope write. Calls the engine loader directly so this load is never
|
||||
// itself suppressed by SuppressReload.
|
||||
void RunLuaFile(const char *path) {
|
||||
if (g_origLuaLoad == nullptr)
|
||||
return;
|
||||
g_origLuaLoad(path, nullptr, nullptr);
|
||||
Script::LoadFile::Original(path, nullptr, nullptr);
|
||||
g_pendingSuppress.emplace_back(path);
|
||||
}
|
||||
|
||||
@@ -241,18 +238,16 @@ uint32_t __fastcall LoadTocFiles_h(char *tocPath, int *a2, int *a3) {
|
||||
// Skip the engine's one redundant SavedVariables re-load per pre-loaded path.
|
||||
// Every other file load passes straight through. Match is by full path
|
||||
// (case-insensitive); a path is suppressed exactly once, so nothing else is
|
||||
// affected. Our own early load bypasses this via the trampoline.
|
||||
uint32_t __fastcall LuaLoad_h(const char *path, void *a2, void *a3) {
|
||||
if (path != nullptr) {
|
||||
for (size_t i = 0; i < g_pendingSuppress.size(); ++i) {
|
||||
if (SamePathCI(g_pendingSuppress[i].c_str(), path)) {
|
||||
g_pendingSuppress.erase(g_pendingSuppress.begin() +
|
||||
static_cast<std::ptrdiff_t>(i));
|
||||
return 0; // suppress — the addon's file-scope SV writes survive
|
||||
}
|
||||
// affected. Our own early load bypasses this through Original.
|
||||
bool SuppressReload(const char *path) {
|
||||
for (size_t i = 0; i < g_pendingSuppress.size(); ++i) {
|
||||
if (SamePathCI(g_pendingSuppress[i].c_str(), path)) {
|
||||
g_pendingSuppress.erase(g_pendingSuppress.begin() +
|
||||
static_cast<std::ptrdiff_t>(i));
|
||||
return true; // suppress — the addon's file-scope SV writes survive
|
||||
}
|
||||
}
|
||||
return g_origLuaLoad(path, a2, a3);
|
||||
return false;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{
|
||||
@@ -260,10 +255,7 @@ const Game::HookAutoRegister _hook{
|
||||
reinterpret_cast<void *>(&LoadTocFiles_h),
|
||||
reinterpret_cast<void **>(&g_orig)};
|
||||
|
||||
const Game::HookAutoRegister _hookLua{
|
||||
Offsets::FUN_LUA_LOAD_FILE,
|
||||
reinterpret_cast<void *>(&LuaLoad_h),
|
||||
reinterpret_cast<void **>(&g_origLuaLoad)};
|
||||
const Script::LoadFile::AutoSuppress _suppress{&SuppressReload};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
+50
-27
@@ -33,35 +33,58 @@ inline bool EqCI(const char *s, size_t n, const char *lit) {
|
||||
return lit[n] == '\0';
|
||||
}
|
||||
|
||||
// Scans an in-memory TOC buffer for `directive` (the full line prefix, e.g.
|
||||
// "## Version:") at a line start, case-insensitively, and returns the value
|
||||
// that follows via out-params: [*valStart, *valStart + *valLen), trimmed of
|
||||
// leading and trailing spaces/tabs and excluding the CR/LF/EOF terminator.
|
||||
// Returns false (out-params untouched) when the directive is absent. Pure —
|
||||
// reads no files; the caller owns the buffer (via FUN_FILE_READ, or an embedded
|
||||
// copy). Shared by addons/LocalTable.cpp (AllowAddOnTableAccess) and
|
||||
// addons/Embedded.cpp (Version) so the line-scan lives in one place.
|
||||
inline bool FindValue(const char *buf, size_t size, const char *directive,
|
||||
// Scans an in-memory TOC buffer for the `## <key>:` directive and returns the
|
||||
// first one's value via out-params: [*valStart, *valStart + *valLen), trimmed
|
||||
// of leading and trailing spaces/tabs and excluding the CR/LF/EOF terminator.
|
||||
// `key` is the bare name ("Version", "SavedVariables"). Returns false
|
||||
// (out-params untouched) when the directive is absent.
|
||||
//
|
||||
// Matches lines the way the engine's TOC parser `FUN_TOC_PARSER` (0x0051C9B0)
|
||||
// does, so a directive the engine reads is never one we miss: a UTF-8 BOM at
|
||||
// the start of the file is skipped; lines break at CR or LF; a line counts
|
||||
// when it starts with "##", then any number of spaces, then `key`
|
||||
// case-insensitively, IMMEDIATELY followed by ':' (the engine compares
|
||||
// against literals like "SavedVariables:", which is also why
|
||||
// "SavedVariables" never matches a "SavedVariablesPerCharacter:" line).
|
||||
//
|
||||
// Pure — reads no files; the caller owns the buffer (via FUN_FILE_READ, or an
|
||||
// embedded copy). Shared by every addon module that reads a TOC directive so
|
||||
// the line-scan lives in one place.
|
||||
inline bool FindValue(const char *buf, size_t size, const char *key,
|
||||
const char **valStart, size_t *valLen) {
|
||||
const size_t dlen = std::strlen(directive);
|
||||
if (dlen == 0)
|
||||
const size_t klen = std::strlen(key);
|
||||
if (klen == 0)
|
||||
return false;
|
||||
for (size_t i = 0; i + dlen <= size; ++i) {
|
||||
const bool atLineStart = (i == 0) || buf[i - 1] == '\n';
|
||||
if (!atLineStart || _strnicmp(buf + i, directive, dlen) != 0)
|
||||
continue;
|
||||
const char *p = buf + i + dlen;
|
||||
const char *end = buf + size;
|
||||
while (p < end && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
const char *v = p;
|
||||
while (p < end && *p != '\r' && *p != '\n')
|
||||
++p;
|
||||
while (p > v && (p[-1] == ' ' || p[-1] == '\t'))
|
||||
--p;
|
||||
*valStart = v;
|
||||
*valLen = static_cast<size_t>(p - v);
|
||||
return true;
|
||||
const char *end = buf + size;
|
||||
const char *line = buf;
|
||||
if (size >= 3 && static_cast<unsigned char>(buf[0]) == 0xEF &&
|
||||
static_cast<unsigned char>(buf[1]) == 0xBB &&
|
||||
static_cast<unsigned char>(buf[2]) == 0xBF)
|
||||
line += 3;
|
||||
while (line < end) {
|
||||
const char *eol = line;
|
||||
while (eol < end && *eol != '\r' && *eol != '\n')
|
||||
++eol;
|
||||
const char *p = line;
|
||||
if (eol - p >= 2 && p[0] == '#' && p[1] == '#') {
|
||||
p += 2;
|
||||
while (p < eol && *p == ' ')
|
||||
++p;
|
||||
if (static_cast<size_t>(eol - p) > klen && EqCI(p, klen, key) &&
|
||||
p[klen] == ':') {
|
||||
p += klen + 1;
|
||||
while (p < eol && (*p == ' ' || *p == '\t'))
|
||||
++p;
|
||||
const char *v = p;
|
||||
p = eol;
|
||||
while (p > v && (p[-1] == ' ' || p[-1] == '\t'))
|
||||
--p;
|
||||
*valStart = v;
|
||||
*valLen = static_cast<size_t>(p - v);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
line = eol + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// API documentation: the record of every Lua registration plus its
|
||||
// descriptor, exported on demand as Blizzard `APIDocumentation` tables.
|
||||
//
|
||||
// C_APIDocumentation.GetSystems() -> sorted array of system Names
|
||||
// C_APIDocumentation.GetSystem(name) -> one System table (Blizzard shape), or nil
|
||||
// _classicapi_UndocumentedAPI() -> array of registrations lacking a
|
||||
// descriptor, and its count
|
||||
//
|
||||
// The registrars in Game.cpp call `Record*` (see Documentation.h). Recording
|
||||
// runs once per Lua state — the first in-game pass and the first glue pass —
|
||||
// because the set of registrations is static: every module registers the
|
||||
// same names on every `/reload`. The recorded list is never materialized
|
||||
// into Lua wholesale; `GetSystem` builds ONE system's table per call, which
|
||||
// is how Blizzard's browser loads too (its generated data addon is
|
||||
// LoadOnDemand). Eager export would create thousands of tables on every
|
||||
// `/reload` for a feature used from a slash command.
|
||||
//
|
||||
// System grouping is derived, never authored, except for two strings:
|
||||
// - `C_Spell.X` -> System "Spell", Namespace "C_Spell" (strip `C_`)
|
||||
// - `table.insert` -> System "table", Namespace "table"
|
||||
// - a global -> `doc->system` ("SpellGlobals"); a global whose
|
||||
// descriptor names no system has no home and is
|
||||
// listed by the coverage function
|
||||
// - a frame method -> Blizzard's widget system for its registry
|
||||
// ("SimpleTextureAPI", Type "ScriptObject")
|
||||
// - an enum / structure /
|
||||
// event -> the System named in its declaration
|
||||
// A system's `Environment` is "All" when its functions were recorded in both
|
||||
// Lua states, else "Game" or "Glue"; a function whose own environment is
|
||||
// narrower than its system's gets a Documentation line saying so.
|
||||
|
||||
#include "api/Documentation.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Api::Documentation {
|
||||
|
||||
using Game::Doc::Field;
|
||||
using Game::Doc::FieldList;
|
||||
using Game::Doc::Function;
|
||||
using Game::Doc::Method;
|
||||
using Game::Doc::Structure;
|
||||
// NOT `using Game::Doc::Event` — that name would shadow the global `Event`
|
||||
// namespace for the whole file, so `Event::Custom::…` would resolve to the
|
||||
// type. Same trap as `Time::Tick` vs the `Tick` namespace (see CLAUDE.md).
|
||||
using EventDoc = Game::Doc::Event;
|
||||
|
||||
// Head of the self-registering `Structure` list. Constant-initialized, so it
|
||||
// is valid before any `Structure` constructor runs, whatever the cross-TU
|
||||
// static-init order.
|
||||
const Structure *g_structureHead = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
enum Kind : uint8_t {
|
||||
KIND_GLOBAL, // RegisterGlobalFunction / RegisterGlueFunction / RegisterGlobalAlias
|
||||
KIND_TABLE, // RegisterTableFunction
|
||||
KIND_FRAME_METHOD, // RegisterFrameMethods, one per entry
|
||||
KIND_ENUM, // RegisterIntegerEnum
|
||||
KIND_ORPHAN_METHOD_DOC // a Doc::Method naming no entry in its table
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
Kind kind;
|
||||
unsigned env; // Env bits the registration was recorded in
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum")
|
||||
const char *name; // registered name / method name / enum sub-name
|
||||
const void *registry; // frame methods only
|
||||
const Function *doc; // nullptr = undocumented
|
||||
const char *enumSystem;
|
||||
const Game::Lua::EnumIntegerEntry *enumEntries;
|
||||
int enumCount;
|
||||
};
|
||||
|
||||
std::vector<Entry> g_entries;
|
||||
Env g_pass = ENV_NONE;
|
||||
unsigned g_sealed = 0;
|
||||
int g_recordedThisPass = 0;
|
||||
|
||||
constexpr const char *kDebugPrefix = "_classicapi_";
|
||||
|
||||
bool IsDebugName(const char *name) {
|
||||
return std::strncmp(name, kDebugPrefix, std::strlen(kDebugPrefix)) == 0;
|
||||
}
|
||||
|
||||
bool SameText(const char *a, const char *b) {
|
||||
if (a == nullptr || b == nullptr)
|
||||
return a == b;
|
||||
return std::strcmp(a, b) == 0;
|
||||
}
|
||||
|
||||
bool SameTextNoCase(const char *a, const char *b) {
|
||||
if (a == nullptr || b == nullptr)
|
||||
return a == b;
|
||||
return _stricmp(a, b) == 0;
|
||||
}
|
||||
|
||||
Entry *Find(Kind kind, const char *table, const char *name, const void *registry) {
|
||||
for (Entry &e : g_entries) {
|
||||
if (e.kind == kind && e.registry == registry && SameText(e.table, table) &&
|
||||
SameText(e.name, name))
|
||||
return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Record(Kind kind, const char *table, const char *name, const void *registry,
|
||||
const Function *doc) {
|
||||
if (g_pass == ENV_NONE || name == nullptr)
|
||||
return;
|
||||
++g_recordedThisPass;
|
||||
if (Entry *e = Find(kind, table, name, registry)) {
|
||||
e->env |= g_pass;
|
||||
if (e->doc == nullptr)
|
||||
e->doc = doc;
|
||||
return;
|
||||
}
|
||||
Entry e{};
|
||||
e.kind = kind;
|
||||
e.env = g_pass;
|
||||
e.table = table;
|
||||
e.name = name;
|
||||
e.registry = registry;
|
||||
e.doc = doc;
|
||||
g_entries.push_back(e);
|
||||
}
|
||||
|
||||
// ---- System resolution ------------------------------------------------------
|
||||
|
||||
// Blizzard's names for the widget-method systems, keyed by the engine
|
||||
// registry each `FrameMethodEntry[]` is bound to.
|
||||
struct WidgetSystem {
|
||||
uintptr_t registry;
|
||||
const char *system;
|
||||
};
|
||||
const WidgetSystem kWidgetSystems[] = {
|
||||
{Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY, "GameTooltipAPI"},
|
||||
{Offsets::VAR_REGION_METHOD_REGISTRY, "ScriptRegionAPI"},
|
||||
{Offsets::VAR_FRAME_METHOD_REGISTRY, "SimpleFrameAPI"},
|
||||
{Offsets::VAR_TEXTURE_METHOD_REGISTRY, "SimpleTextureAPI"},
|
||||
{Offsets::VAR_FONTSTRING_METHOD_REGISTRY, "SimpleFontStringAPI"},
|
||||
{Offsets::VAR_EDITBOX_METHOD_REGISTRY, "SimpleEditBoxAPI"},
|
||||
{Offsets::VAR_MODEL_METHOD_REGISTRY, "SimpleModelAPI"},
|
||||
};
|
||||
|
||||
const char *WidgetSystemFor(const void *registry) {
|
||||
for (const WidgetSystem &w : kWidgetSystems)
|
||||
if (reinterpret_cast<const void *>(w.registry) == registry)
|
||||
return w.system;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// "C_Spell" -> "Spell"; anything else is its own system name.
|
||||
const char *SystemNameForTable(const char *table) {
|
||||
if (table[0] == 'C' && table[1] == '_' && table[2] != '\0')
|
||||
return table + 2;
|
||||
return table;
|
||||
}
|
||||
|
||||
// The System an entry contributes a function to, or nullptr when it has
|
||||
// none (an undocumented global, a frame method on an unmapped registry).
|
||||
const char *SystemOf(const Entry &e) {
|
||||
switch (e.kind) {
|
||||
case KIND_TABLE:
|
||||
return SystemNameForTable(e.table);
|
||||
case KIND_GLOBAL:
|
||||
return e.doc != nullptr ? e.doc->system : nullptr;
|
||||
case KIND_FRAME_METHOD:
|
||||
return WidgetSystemFor(e.registry);
|
||||
case KIND_ENUM:
|
||||
return e.enumSystem;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
struct SystemInfo {
|
||||
const char *name;
|
||||
const char *ns; // nullptr for global and widget systems
|
||||
const char *type; // "System" or "ScriptObject"
|
||||
unsigned env; // Env bits; 0 when nothing recorded (structures only)
|
||||
};
|
||||
|
||||
// Merge one entry's contribution into the system list.
|
||||
void AddSystem(std::vector<SystemInfo> &systems, const char *name, const char *ns,
|
||||
const char *type, unsigned env) {
|
||||
if (name == nullptr)
|
||||
return;
|
||||
for (SystemInfo &s : systems) {
|
||||
if (SameTextNoCase(s.name, name)) {
|
||||
if (s.ns == nullptr)
|
||||
s.ns = ns;
|
||||
if (type != nullptr)
|
||||
s.type = type;
|
||||
s.env |= env;
|
||||
return;
|
||||
}
|
||||
}
|
||||
systems.push_back(SystemInfo{name, ns, type != nullptr ? type : "System", env});
|
||||
}
|
||||
|
||||
std::vector<SystemInfo> CollectSystems() {
|
||||
std::vector<SystemInfo> systems;
|
||||
for (const Entry &e : g_entries) {
|
||||
switch (e.kind) {
|
||||
case KIND_TABLE:
|
||||
AddSystem(systems, SystemNameForTable(e.table), e.table, "System", e.env);
|
||||
break;
|
||||
case KIND_GLOBAL:
|
||||
AddSystem(systems, SystemOf(e), nullptr, "System", e.env);
|
||||
break;
|
||||
case KIND_FRAME_METHOD:
|
||||
AddSystem(systems, SystemOf(e), nullptr, "ScriptObject", e.env);
|
||||
break;
|
||||
case KIND_ENUM:
|
||||
AddSystem(systems, e.enumSystem, nullptr, nullptr, 0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const Structure *s = g_structureHead; s != nullptr; s = s->next)
|
||||
AddSystem(systems, s->system, nullptr, nullptr, 0);
|
||||
const int events = Event::Custom::ReservedCount();
|
||||
for (int i = 0; i < events; ++i) {
|
||||
const EventDoc *doc = Event::Custom::ReservedDoc(i);
|
||||
if (doc != nullptr)
|
||||
AddSystem(systems, doc->system, nullptr, nullptr, 0);
|
||||
}
|
||||
return systems;
|
||||
}
|
||||
|
||||
const SystemInfo *FindSystem(const std::vector<SystemInfo> &systems, const char *query) {
|
||||
for (const SystemInfo &s : systems)
|
||||
if (SameTextNoCase(s.name, query) || SameTextNoCase(s.ns, query))
|
||||
return &s;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *EnvironmentName(unsigned env) {
|
||||
if (env == (ENV_GAME | ENV_GLUE) || env == 0)
|
||||
return "All";
|
||||
return env == ENV_GLUE ? "Glue" : "Game";
|
||||
}
|
||||
|
||||
// ---- Export builders (each leaves exactly what its comment says) -------------
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// Sets `Documentation = { line1 [, line2 [, line3]] }` on the table at
|
||||
// stack[-1]; no-op when every line is null.
|
||||
void SetDocumentation(void *L, const char *l1, const char *l2 = nullptr,
|
||||
const char *l3 = nullptr) {
|
||||
if (l1 == nullptr && l2 == nullptr && l3 == nullptr)
|
||||
return;
|
||||
Lua::PushString(L, "Documentation");
|
||||
Lua::NewTable(L);
|
||||
int n = 0;
|
||||
for (const char *line : {l1, l2, l3}) {
|
||||
if (line == nullptr)
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
Lua::PushString(L, line);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// A Lua literal from a descriptor's `defaultValue`, pushed as the value it
|
||||
// denotes: `true`/`false` -> boolean, a quoted string -> string, else number.
|
||||
void PushDefault(void *L, const char *literal) {
|
||||
if (std::strcmp(literal, "true") == 0) {
|
||||
Lua::PushBool(L, true);
|
||||
} else if (std::strcmp(literal, "false") == 0) {
|
||||
Lua::PushBool(L, false);
|
||||
} else if (literal[0] == '"') {
|
||||
const size_t len = std::strlen(literal);
|
||||
Lua::PushLString(L, literal + 1, static_cast<unsigned>(len >= 2 ? len - 2 : 0));
|
||||
} else {
|
||||
Lua::PushNumber(L, std::strtod(literal, nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
// Pushes one field record: { Name, Type, Nilable [, Default] [, Documentation] }.
|
||||
void PushField(void *L, const Field &f) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", f.name);
|
||||
Lua::SetFieldString(L, "Type", f.type);
|
||||
Lua::SetFieldBool(L, "Nilable", f.nilable);
|
||||
if (f.defaultValue != nullptr) {
|
||||
Lua::PushString(L, "Default");
|
||||
PushDefault(L, f.defaultValue);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
SetDocumentation(L, f.doc);
|
||||
}
|
||||
|
||||
// Sets `stack[-1][key] = { field records }`; no-op for an empty list.
|
||||
void SetFieldArray(void *L, const char *key, FieldList list) {
|
||||
if (list.count <= 0)
|
||||
return;
|
||||
Lua::PushString(L, key);
|
||||
Lua::NewTable(L);
|
||||
for (int i = 0; i < list.count; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
PushField(L, list.data[i]);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
constexpr const char *kExtensionNote = "ClassicAPI extension - not part of the Blizzard API.";
|
||||
constexpr const char *kGlueOnlyNote = "Available only on the glue screen.";
|
||||
constexpr const char *kGameOnlyNote = "Available only in-game.";
|
||||
// An undocumented function lists with no arguments, which is indistinguishable
|
||||
// from a function that genuinely takes none. Say which it is, so an empty
|
||||
// signature is never read as a real one.
|
||||
constexpr const char *kUndocumentedNote =
|
||||
"Not documented yet. Its arguments and return values are not listed here.";
|
||||
|
||||
// Pushes one function record. An undocumented entry still lists by name so
|
||||
// `/classicapi <System> list` shows the whole surface; its shape arrives with
|
||||
// the descriptor.
|
||||
void PushFunction(void *L, const Entry &e, unsigned systemEnv) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.name);
|
||||
Lua::SetFieldString(L, "Type", "Function");
|
||||
const char *envNote = nullptr;
|
||||
if (e.env != systemEnv && e.env != 0)
|
||||
envNote = (e.env == ENV_GLUE) ? kGlueOnlyNote : kGameOnlyNote;
|
||||
if (e.doc == nullptr) {
|
||||
SetDocumentation(L, kUndocumentedNote, envNote);
|
||||
return;
|
||||
}
|
||||
SetDocumentation(L, e.doc->summary, e.doc->extension ? kExtensionNote : nullptr, envNote);
|
||||
SetFieldArray(L, "Arguments", e.doc->args);
|
||||
SetFieldArray(L, "Returns", e.doc->rets);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Enumeration", NumValues, MinValue, MaxValue,
|
||||
// Fields = { { Name, Type = <enum>, EnumValue } } }.
|
||||
void PushEnumeration(void *L, const Entry &e) {
|
||||
int minValue = 0, maxValue = 0;
|
||||
for (int i = 0; i < e.enumCount; ++i) {
|
||||
const int v = e.enumEntries[i].value;
|
||||
if (i == 0 || v < minValue)
|
||||
minValue = v;
|
||||
if (i == 0 || v > maxValue)
|
||||
maxValue = v;
|
||||
}
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.name);
|
||||
Lua::SetFieldString(L, "Type", "Enumeration");
|
||||
Lua::SetFieldNumber(L, "NumValues", e.enumCount);
|
||||
Lua::SetFieldNumber(L, "MinValue", minValue);
|
||||
Lua::SetFieldNumber(L, "MaxValue", maxValue);
|
||||
Lua::PushString(L, "Fields");
|
||||
Lua::NewTable(L);
|
||||
for (int i = 0; i < e.enumCount; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.enumEntries[i].key);
|
||||
Lua::SetFieldString(L, "Type", e.name);
|
||||
Lua::SetFieldNumber(L, "EnumValue", e.enumEntries[i].value);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Structure" [, Documentation], Fields = { … } }.
|
||||
void PushStructure(void *L, const Structure &s) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", s.name);
|
||||
Lua::SetFieldString(L, "Type", "Structure");
|
||||
SetDocumentation(L, s.summary);
|
||||
SetFieldArray(L, "Fields", s.fields);
|
||||
}
|
||||
|
||||
// "LOSS_OF_CONTROL_ADDED" -> "LossOfControlAdded" (Blizzard's event Name).
|
||||
void PascalCase(const char *literal, char *out, size_t cap) {
|
||||
size_t o = 0;
|
||||
bool upper = true;
|
||||
for (const char *p = literal; *p != '\0' && o + 1 < cap; ++p) {
|
||||
if (*p == '_') {
|
||||
upper = true;
|
||||
continue;
|
||||
}
|
||||
const unsigned char c = static_cast<unsigned char>(*p);
|
||||
out[o++] = static_cast<char>(upper ? std::toupper(c) : std::tolower(c));
|
||||
upper = false;
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Event", LiteralName [, Documentation] [, Payload] }.
|
||||
void PushEvent(void *L, const char *literal, const EventDoc &doc) {
|
||||
char name[128];
|
||||
PascalCase(literal, name, sizeof name);
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", name);
|
||||
Lua::SetFieldString(L, "Type", "Event");
|
||||
Lua::SetFieldString(L, "LiteralName", literal);
|
||||
SetDocumentation(L, doc.summary);
|
||||
SetFieldArray(L, "Payload", doc.payload);
|
||||
}
|
||||
|
||||
// Pushes the whole System table for `sys`.
|
||||
void PushSystem(void *L, const SystemInfo &sys) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", sys.name);
|
||||
Lua::SetFieldString(L, "Type", sys.type);
|
||||
if (sys.ns != nullptr)
|
||||
Lua::SetFieldString(L, "Namespace", sys.ns);
|
||||
Lua::SetFieldString(L, "Environment", EnvironmentName(sys.env));
|
||||
|
||||
Lua::PushString(L, "Functions");
|
||||
Lua::NewTable(L);
|
||||
int n = 0;
|
||||
for (const Entry &e : g_entries) {
|
||||
if (e.kind != KIND_GLOBAL && e.kind != KIND_TABLE && e.kind != KIND_FRAME_METHOD)
|
||||
continue;
|
||||
if (!SameTextNoCase(SystemOf(e), sys.name) || IsDebugName(e.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushFunction(L, e, sys.env);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
|
||||
Lua::PushString(L, "Events");
|
||||
Lua::NewTable(L);
|
||||
n = 0;
|
||||
const int events = Event::Custom::ReservedCount();
|
||||
for (int i = 0; i < events; ++i) {
|
||||
const EventDoc *doc = Event::Custom::ReservedDoc(i);
|
||||
if (doc == nullptr || !SameTextNoCase(doc->system, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushEvent(L, Event::Custom::ReservedName(i), *doc);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
|
||||
Lua::PushString(L, "Tables");
|
||||
Lua::NewTable(L);
|
||||
n = 0;
|
||||
for (const Structure *s = g_structureHead; s != nullptr; s = s->next) {
|
||||
if (!SameTextNoCase(s->system, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushStructure(L, *s);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
for (const Entry &e : g_entries) {
|
||||
if (e.kind != KIND_ENUM || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushEnumeration(L, e);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// ---- Coverage ---------------------------------------------------------------
|
||||
|
||||
std::vector<std::string> Undocumented() {
|
||||
std::vector<std::string> out;
|
||||
const std::vector<SystemInfo> systems = CollectSystems();
|
||||
for (const Entry &e : g_entries) {
|
||||
if (IsDebugName(e.name))
|
||||
continue;
|
||||
switch (e.kind) {
|
||||
case KIND_GLOBAL:
|
||||
if (e.doc == nullptr) {
|
||||
out.push_back(e.name);
|
||||
} else if (e.doc->system == nullptr) {
|
||||
out.push_back(std::string(e.name) + " (doc has no system)");
|
||||
} else if (const SystemInfo *s = FindSystem(systems, e.doc->system);
|
||||
s != nullptr && s->ns != nullptr) {
|
||||
out.push_back(std::string(e.name) + " (system '" + e.doc->system +
|
||||
"' is namespaced)");
|
||||
}
|
||||
break;
|
||||
case KIND_TABLE:
|
||||
if (e.doc == nullptr)
|
||||
out.push_back(std::string(e.table) + "." + e.name);
|
||||
break;
|
||||
case KIND_FRAME_METHOD: {
|
||||
const char *sys = WidgetSystemFor(e.registry);
|
||||
if (sys == nullptr)
|
||||
out.push_back(std::string(e.name) + " (method registry not mapped)");
|
||||
else if (e.doc == nullptr)
|
||||
out.push_back(std::string(sys) + ":" + e.name);
|
||||
break;
|
||||
}
|
||||
case KIND_ORPHAN_METHOD_DOC: {
|
||||
const char *sys = WidgetSystemFor(e.registry);
|
||||
out.push_back(std::string(sys != nullptr ? sys : "?") + ":" + e.name +
|
||||
" (doc for unregistered method)");
|
||||
break;
|
||||
}
|
||||
case KIND_ENUM:
|
||||
if (e.enumSystem == nullptr)
|
||||
out.push_back(std::string(e.table) + "." + e.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const int events = Event::Custom::ReservedCount();
|
||||
for (int i = 0; i < events; ++i)
|
||||
if (Event::Custom::ReservedDoc(i) == nullptr)
|
||||
out.push_back(std::string("event ") + Event::Custom::ReservedName(i));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- Lua surface ------------------------------------------------------------
|
||||
|
||||
int __fastcall Script_GetSystems(void *L) {
|
||||
std::vector<SystemInfo> systems = CollectSystems();
|
||||
std::sort(systems.begin(), systems.end(), [](const SystemInfo &a, const SystemInfo &b) {
|
||||
return _stricmp(a.name, b.name) < 0;
|
||||
});
|
||||
Lua::NewTable(L);
|
||||
for (size_t i = 0; i < systems.size(); ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::PushString(L, systems[i].name);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSystem(void *L) {
|
||||
if (!Lua::IsString(L, 1)) {
|
||||
Lua::Error(L, "Usage: C_APIDocumentation.GetSystem(name)");
|
||||
return 0;
|
||||
}
|
||||
const char *query = Lua::ToString(L, 1);
|
||||
const std::vector<SystemInfo> systems = CollectSystems();
|
||||
const SystemInfo *sys = FindSystem(systems, query);
|
||||
if (sys == nullptr)
|
||||
return 0; // nil
|
||||
Lua::CheckStack(L, 32);
|
||||
PushSystem(L, *sys);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_UndocumentedAPI(void *L) {
|
||||
const std::vector<std::string> names = Undocumented();
|
||||
Lua::NewTable(L);
|
||||
for (size_t i = 0; i < names.size(); ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::PushString(L, names[i].c_str());
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::PushNumber(L, static_cast<double>(names.size()));
|
||||
return 2;
|
||||
}
|
||||
|
||||
const Field kGetSystemsRets[] = {
|
||||
Game::Doc::Req("systems", "table", "Array of system Names, sorted without regard to case."),
|
||||
};
|
||||
const Function kGetSystems{
|
||||
"Returns the name of every documented system.",
|
||||
{}, kGetSystemsRets, nullptr, true};
|
||||
|
||||
const Field kGetSystemArgs[] = {
|
||||
Game::Doc::Req("system", "string", "A system Name or Namespace; case does not matter."),
|
||||
};
|
||||
const Field kGetSystemRets[] = {
|
||||
Game::Doc::Opt("system", "table", nullptr,
|
||||
"The system in Blizzard's APIDocumentation table shape, or nil when unknown."),
|
||||
};
|
||||
const Function kGetSystem{
|
||||
"Returns one system's documentation table.",
|
||||
kGetSystemArgs, kGetSystemRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_APIDocumentation", "GetSystems", &Script_GetSystems,
|
||||
&kGetSystems);
|
||||
Game::Lua::RegisterTableFunction("C_APIDocumentation", "GetSystem", &Script_GetSystem,
|
||||
&kGetSystem);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_UndocumentedAPI", &Script_UndocumentedAPI);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- Recording (called from Game.cpp) ----------------------------------------
|
||||
|
||||
void BeginPass(Env env) {
|
||||
g_pass = (g_sealed & env) != 0 ? ENV_NONE : env;
|
||||
g_recordedThisPass = 0;
|
||||
}
|
||||
|
||||
void EndPass() {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
g_sealed |= g_pass;
|
||||
const Env done = g_pass;
|
||||
g_pass = ENV_NONE;
|
||||
const std::vector<std::string> missing = Undocumented();
|
||||
Debug::Log::Printf("[doc] %s pass: %d registrations recorded, %d undocumented",
|
||||
done == ENV_GLUE ? "glue" : "game", g_recordedThisPass,
|
||||
static_cast<int>(missing.size()));
|
||||
}
|
||||
|
||||
void RecordGlobal(const char *name, const Function *doc) {
|
||||
Record(KIND_GLOBAL, nullptr, name, nullptr, doc);
|
||||
}
|
||||
|
||||
void RecordTable(const char *table, const char *name, const Function *doc) {
|
||||
Record(KIND_TABLE, table, name, nullptr, doc);
|
||||
}
|
||||
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Method *docs, int docCount) {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const Function *doc = nullptr;
|
||||
for (int d = 0; d < docCount; ++d) {
|
||||
if (std::strcmp(docs[d].name, table[i].name) == 0) {
|
||||
doc = docs[d].doc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Record(KIND_FRAME_METHOD, nullptr, table[i].name, registry, doc);
|
||||
}
|
||||
for (int d = 0; d < docCount; ++d) {
|
||||
bool matched = false;
|
||||
for (int i = 0; i < count && !matched; ++i)
|
||||
matched = std::strcmp(docs[d].name, table[i].name) == 0;
|
||||
if (!matched)
|
||||
Record(KIND_ORPHAN_METHOD_DOC, nullptr, docs[d].name, registry, docs[d].doc);
|
||||
}
|
||||
}
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
if (g_pass == ENV_NONE || sub == nullptr)
|
||||
return;
|
||||
++g_recordedThisPass;
|
||||
if (Entry *e = Find(KIND_ENUM, parent, sub, nullptr)) {
|
||||
e->env |= g_pass;
|
||||
if (e->enumSystem == nullptr)
|
||||
e->enumSystem = system;
|
||||
return;
|
||||
}
|
||||
Entry e{};
|
||||
e.kind = KIND_ENUM;
|
||||
e.env = g_pass;
|
||||
e.table = parent;
|
||||
e.name = sub;
|
||||
e.enumSystem = system;
|
||||
e.enumEntries = entries;
|
||||
e.enumCount = count;
|
||||
g_entries.push_back(e);
|
||||
}
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
// The Structure constructor lives here so the list head is private to this
|
||||
// TU; declared in Game.h beside the other descriptor types.
|
||||
Game::Doc::Structure::Structure(const char *n, const char *s, FieldList f, const char *sum)
|
||||
: name(n), system(s), fields(f), summary(sum), next(Api::Documentation::g_structureHead) {
|
||||
Api::Documentation::g_structureHead = this;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
// Recording side of the API documentation. The registrars in Game.cpp call
|
||||
// these; `Documentation.cpp` keeps the list, resolves systems, and exports
|
||||
// Blizzard-shaped `APIDocumentation` tables through `C_APIDocumentation`.
|
||||
// The descriptor model is the `Game::Doc` block in Game.h.
|
||||
namespace Api::Documentation {
|
||||
|
||||
enum Env : unsigned { ENV_NONE = 0, ENV_GAME = 1, ENV_GLUE = 2 };
|
||||
|
||||
// Bracket one `Run*Registrations()` pass. Recording happens only inside a
|
||||
// pass, and only the FIRST pass per environment records — every later pass
|
||||
// (each `/reload`, each logout→login) is a no-op. `EndPass` seals the
|
||||
// environment and logs one summary line to the debug log.
|
||||
void BeginPass(Env env);
|
||||
void EndPass();
|
||||
|
||||
void RecordGlobal(const char *name, const Game::Doc::Function *doc);
|
||||
void RecordTable(const char *table, const char *name, const Game::Doc::Function *doc);
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Game::Doc::Method *docs, int docCount);
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system);
|
||||
|
||||
} // namespace Api::Documentation
|
||||
+4
-4
@@ -21,10 +21,10 @@
|
||||
// tokens separated by `|` and/or whitespace, each optionally negated with a
|
||||
// leading `!`. Whole-token matching, so `RAID_PLAYER_DISPELLABLE` is not
|
||||
// mistaken for `PLAYER` and `!PLAYER` is a negation rather than a match.
|
||||
// Honored: `HELPFUL` / `HARMFUL` (the aura's flag-nibble polarity — see
|
||||
// Data.h; NOT the slot range, which misreads debuffs spilled into buff
|
||||
// slots), `PLAYER` /
|
||||
// `!PLAYER` (caster == / != the local player, from the Aura::Source cache),
|
||||
// Honored: `HELPFUL` / `HARMFUL` (the aura's real polarity, read however this
|
||||
// server encodes it — see `Aura::Data::IsSlotHarmful`), `PLAYER` /
|
||||
// `!PLAYER` (caster is / isn't the local player or their pet, from the
|
||||
// Aura::Source cache),
|
||||
// `DISPELLABLE` / `!DISPELLABLE` (dispel type is / isn't one a
|
||||
// dispel/purge/steal can remove — Spell.dbc Dispel ∈ Magic/Curse/Disease/
|
||||
// Poison, matching the server's DISPEL_ALL_MASK), and `CROWD_CONTROL` /
|
||||
|
||||
+72
-8
@@ -22,6 +22,7 @@
|
||||
#include "spell/CrowdControl.h"
|
||||
#include "spell/IsSelfBuff.h"
|
||||
#include "time/Clock.h"
|
||||
#include "turtle/Detect.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -272,14 +273,59 @@ bool IsSlotPopulated(const uint8_t *unit, int slot) {
|
||||
return IsVisible(SpellRecord(spellID));
|
||||
}
|
||||
|
||||
// Every nibble Turtle's SetAuraFlag can write for an occupied slot: HELPFUL,
|
||||
// HELPFUL|CANCELABLE, or HARMFUL alone. Nothing else is reachable there — a
|
||||
// negative aura gets exactly HARMFUL, and a positive one always carries HELPFUL.
|
||||
bool IsTurtlePolarityNibble(uint8_t nibble) {
|
||||
return nibble == Offsets::UNIT_AURA_FLAG_HELPFUL ||
|
||||
nibble == (Offsets::UNIT_AURA_FLAG_HELPFUL | Offsets::UNIT_AURA_FLAG_CANCELABLE) ||
|
||||
nibble == Offsets::UNIT_AURA_FLAG_HARMFUL;
|
||||
}
|
||||
|
||||
// Latched for the process once a nibble proves the flags are not polarity here.
|
||||
// A server does not change its encoding mid-session, so this never resets.
|
||||
bool g_nibbleContradicted = false;
|
||||
|
||||
bool IsSlotHarmful(const uint8_t *unit, int slot) {
|
||||
if (slot < 0 || slot >= Offsets::UNIT_AURA_TOTAL)
|
||||
return false;
|
||||
|
||||
// Normally the slot range IS the polarity: an aura is written to 0..31 when
|
||||
// helpful and 32..47 when harmful, and nothing ever crosses over. The flag
|
||||
// nibble cannot be used here, because in a stock descriptor those bits mean
|
||||
// something else entirely — one bit per spell effect index that carries an
|
||||
// aura, plus a cancelable bit. Reading bit 0x08 as "harmful" would then be
|
||||
// reading "effect index 0 has an aura", which is true of nearly every aura,
|
||||
// and would report almost every BUFF as a debuff.
|
||||
if (!Turtle::Detected())
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
|
||||
// Turtle lets a debuff spill into a buff slot once the harmful range is
|
||||
// full, so the range stops being reliable — and it repurposes those same
|
||||
// nibble bits to carry the real polarity (0x04 helpful, 0x08 harmful) so
|
||||
// the information survives the spill. There, the nibble is the answer.
|
||||
if (g_nibbleContradicted)
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
|
||||
auto *desc = Descriptor(unit);
|
||||
if (desc == nullptr)
|
||||
return false;
|
||||
const uint8_t byte = *(desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS + slot / 2);
|
||||
const uint8_t nibble = (byte >> ((slot & 1) * 4)) & 0xF;
|
||||
|
||||
// Fail safe. Detection says this server SHOULD be writing polarity here, so
|
||||
// a nibble it could not have produced means that assumption is stale — the
|
||||
// server changed how it writes the flags. Fall back to the slot range for
|
||||
// good rather than keep reading bits whose meaning we no longer know: the
|
||||
// range is merely incomplete for spilled debuffs, whereas a misread nibble
|
||||
// reports nearly every buff as a debuff. Revocation only, never promotion —
|
||||
// nothing here can turn nibble reading ON, because the agreeing case is not
|
||||
// provable (an all-0x08 run is equally consistent with single-effect auras
|
||||
// under the other encoding).
|
||||
if (nibble != 0 && !IsTurtlePolarityNibble(nibble)) {
|
||||
g_nibbleContradicted = true;
|
||||
return slot >= Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
}
|
||||
return (nibble & Offsets::UNIT_AURA_FLAG_HARMFUL) != 0;
|
||||
}
|
||||
|
||||
@@ -315,16 +361,36 @@ int SlotInFilterOrder(Filter filter, int i) {
|
||||
return i;
|
||||
}
|
||||
|
||||
// The caster set the `PLAYER` filter selects: the player AND the player's
|
||||
// pet. Modern defines the token as "auras that were cast by the player, or
|
||||
// by the player's pet or vehicle" (`AuraUtil.AuraFilters.Player`) — there
|
||||
// are no vehicles here, so the pair is player + pet. A pet's cast carries
|
||||
// the PET's guid in the Aura::Source cache (that is who SMSG_SPELL_GO
|
||||
// names), so a plain player compare drops e.g. a Hunter's own Serpent
|
||||
// Sting off a `HARMFUL|PLAYER` query. The pet guid is a live engine global,
|
||||
// so this stays two compares with no object resolve — it runs per slot.
|
||||
// A caster of 0 is a cache miss, never a match (see CasterMode).
|
||||
bool IsPlayerOrPetCaster(uint64_t casterGuid) {
|
||||
if (casterGuid == 0)
|
||||
return false;
|
||||
if (casterGuid == Unit::Identity::PlayerGuid())
|
||||
return true;
|
||||
const uint64_t pet =
|
||||
Game::Read<uint64_t>(static_cast<uintptr_t>(Offsets::VAR_PET_GUID));
|
||||
return pet != 0 && casterGuid == pet;
|
||||
}
|
||||
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
return player != 0 && Attribute(UnitGuid(unit), spellID, slot).caster == player;
|
||||
return IsPlayerOrPetCaster(
|
||||
Attribute(UnitGuid(unit), spellID, slot).caster);
|
||||
}
|
||||
|
||||
// Applies the PLAYER / !PLAYER caster restriction. `isPlayerCast` is the
|
||||
// per-aura "was this cast by the local player" answer (false on a cache miss).
|
||||
// per-aura "was this cast by the player or their pet" answer (false on a
|
||||
// cache miss).
|
||||
bool CasterMatches(CasterMode caster, bool isPlayerCast) {
|
||||
switch (caster) {
|
||||
case CasterMode::PlayerOnly: return isPlayerCast;
|
||||
@@ -398,9 +464,8 @@ bool SlotMatchesFilter(const uint8_t *unit, int slot, Filter filter,
|
||||
// no slot to attribute by and the cache is consulted by (guid, spellID) alone.
|
||||
// A miss counts as "not the player" (same as IsPlayerCast).
|
||||
bool GroupIsPlayerCast(uint64_t guid, uint32_t spellID) {
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
return player != 0 &&
|
||||
Attribute(guid, spellID, Aura::Source::SLOT_UNBOUND).caster == player;
|
||||
return IsPlayerOrPetCaster(
|
||||
Attribute(guid, spellID, Aura::Source::SLOT_UNBOUND).caster);
|
||||
}
|
||||
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
@@ -801,8 +866,7 @@ bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
// Applies `match` to a cache entry. The entry carries its caster, so the caster
|
||||
// test is a plain GUID compare — no scan, nothing to defer.
|
||||
bool CachedMatches(const Aura::Source::CachedAura &c, const Match &match) {
|
||||
return MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(),
|
||||
c.spellId);
|
||||
return MatchesAura(match, IsPlayerOrPetCaster(c.casterGuid), c.spellId);
|
||||
}
|
||||
|
||||
// The eligible cache-fallback entries for `unit` under `filter`, in `Enumerate`
|
||||
|
||||
+40
-24
@@ -20,17 +20,28 @@
|
||||
// `unit + OFF_CGUNIT_OBJECT_FIELDS`; the layout is documented in
|
||||
// `Offsets.h` under `OFF_UNIT_FIELD_AURA*`.
|
||||
//
|
||||
// Slots are absolute 0..47. The server SEATS positive auras in 0..31 and
|
||||
// negative ones in 32..47, but that is a preference, not a rule: once the 16
|
||||
// debuff slots are full it parks further debuffs in 0..31 (and sets
|
||||
// UNIT_FLAG_AURAS_VISIBLE so the client renders them). An aura's real polarity
|
||||
// is the per-slot flag nibble (UNIT_AURA_FLAG_HELPFUL / _HARMFUL), which the
|
||||
// server writes for every visible aura — that is what `Filter` selects on
|
||||
// here. The engine's own UnitBuff/UnitDebuff split by slot range and so report
|
||||
// a spilled debuff as a buff; that is a 1.12 UI limitation we deliberately do
|
||||
// not mirror (retail's contract is the aura's actual polarity). A polarity
|
||||
// walk visits its home range first, then the other range, so indices stay
|
||||
// exactly what they were in the common no-spill case.
|
||||
// Slots are absolute 0..47, and how polarity is read out of them DEPENDS ON THE
|
||||
// SERVER. `IsSlotHarmful` owns that decision; nothing else should look at either
|
||||
// signal directly.
|
||||
//
|
||||
// Normally the slot range IS the polarity: positive auras are written to 0..31
|
||||
// and negative ones to 32..47 by a strict either/or, so nothing crosses over.
|
||||
// The flag nibble is no help — in a stock descriptor those bits record which
|
||||
// spell effect indices carry an aura, not whether the aura is good or bad.
|
||||
//
|
||||
// Turtle changes both halves. Once the 16 harmful slots are full it spills
|
||||
// further debuffs into 0..31 (setting UNIT_FLAG_AURAS_VISIBLE so the client
|
||||
// still renders them), which makes the range unreliable — and it repurposes the
|
||||
// nibble to carry the true polarity so the information survives the spill.
|
||||
// There, the nibble is authoritative. See `Offsets::UNIT_AURA_FLAG_HARMFUL`.
|
||||
//
|
||||
// Either way `Filter` selects on the aura's REAL polarity, so a spilled debuff
|
||||
// is reported as a debuff. The engine's own UnitBuff/UnitDebuff split purely by
|
||||
// slot range and would call it a buff; that is a 1.12 UI limitation we
|
||||
// deliberately do not mirror, since retail's contract is actual polarity. A
|
||||
// polarity walk visits its home range first and then the other range, so
|
||||
// indices are unchanged wherever no spill has happened — which is everywhere,
|
||||
// on a server that does not spill.
|
||||
//
|
||||
// Callers that take a 1-based Lua index into "buffs" or "debuffs" translate
|
||||
// to the absolute 0..47 slot before calling in.
|
||||
@@ -52,11 +63,13 @@ enum class Filter { Helpful, Harmful };
|
||||
enum class Emit { Table, Positional };
|
||||
|
||||
// Caster restriction from the `PLAYER` / `!PLAYER` aura filter tokens.
|
||||
// `Any` = no restriction; `PlayerOnly` = only auras the local player cast
|
||||
// (`PLAYER`); `NotPlayer` = only auras NOT cast by the local player
|
||||
// (`!PLAYER`). Caster attribution comes from the `Aura::Source` cache; a
|
||||
// cache miss counts as "not the player" (so `PlayerOnly` excludes it and
|
||||
// `NotPlayer` includes it — matching `IsPlayerCast`'s miss semantics).
|
||||
// `Any` = no restriction; `PlayerOnly` = only auras the local player OR the
|
||||
// player's pet cast (`PLAYER`); `NotPlayer` = the complement (`!PLAYER`).
|
||||
// The pet is part of the set by definition — modern documents the token as
|
||||
// "cast by the player, or by the player's pet or vehicle". Caster
|
||||
// attribution comes from the `Aura::Source` cache; a cache miss counts as
|
||||
// "not the player" (so `PlayerOnly` excludes it and `NotPlayer` includes
|
||||
// it — matching `IsPlayerCast`'s miss semantics).
|
||||
enum class CasterMode { Any, PlayerOnly, NotPlayer };
|
||||
|
||||
// Restriction from the `DISPELLABLE` / `!DISPELLABLE` aura filter tokens.
|
||||
@@ -93,9 +106,10 @@ uint32_t ReadSpellID(const uint8_t *unit, int slot);
|
||||
// to decide whether to surface an aura through Lua.
|
||||
bool IsSlotPopulated(const uint8_t *unit, int slot);
|
||||
|
||||
// The aura's polarity from its flag nibble: true iff the slot carries
|
||||
// UNIT_AURA_FLAG_HARMFUL. False for an empty slot or a null unit. This, not
|
||||
// the slot range, is what `Filter` selects on (see the file header).
|
||||
// The aura's real polarity, read the way this server encodes it: the slot range
|
||||
// normally, the flag nibble on Turtle (see the file header). False for an empty
|
||||
// slot or a null unit. This is what `Filter` selects on, and the ONE place the
|
||||
// two encodings are chosen between.
|
||||
bool IsSlotHarmful(const uint8_t *unit, int slot);
|
||||
|
||||
// The engine's TOOLTIP visibility gate for a slot: occupied, visible nibble,
|
||||
@@ -136,13 +150,15 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// True iff the aura at `slot` was cast by the local player, per the
|
||||
// `Aura::Source` cache. False on a cache miss (caster unknown) — so a
|
||||
// PLAYER-filtered query excludes auras whose cast we didn't observe.
|
||||
// True iff the aura at `slot` was cast by the local player or by the
|
||||
// player's pet, per the `Aura::Source` cache. False on a cache miss (caster
|
||||
// unknown) — so a PLAYER-filtered query excludes auras whose cast we didn't
|
||||
// observe.
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot);
|
||||
|
||||
// Applies a `CasterMode` to a per-aura "was cast by the local player" answer.
|
||||
// `Any` → always true; `PlayerOnly` → the answer; `NotPlayer` → its negation.
|
||||
// Applies a `CasterMode` to a per-aura "was cast by the player or their pet"
|
||||
// answer. `Any` → always true; `PlayerOnly` → the answer; `NotPlayer` → its
|
||||
// negation.
|
||||
bool CasterMatches(CasterMode caster, bool isPlayerCast);
|
||||
|
||||
// True iff the spell's dispel type is one a dispel/purge/steal can remove —
|
||||
|
||||
+140
-24
@@ -28,6 +28,7 @@
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "totem/Tracker.h"
|
||||
#include "unit/TokenEvents.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -638,11 +639,9 @@ void SignalAuraChanged(uint64_t guid) {
|
||||
}
|
||||
|
||||
void FlushAuraSignals() {
|
||||
using Broadcast_t = void(__fastcall *)(uint64_t *guid, uint32_t eventCode);
|
||||
auto broadcast = reinterpret_cast<Broadcast_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_UNIT_EVENT_BROADCAST));
|
||||
// The engine broadcast plus ClassicAPI's tokens (see Unit::TokenEvents).
|
||||
for (int i = 0; i < g_pendingSignalCount; ++i)
|
||||
broadcast(&g_pendingSignals[i], Offsets::UNIT_EVENT_UNIT_AURA);
|
||||
Unit::TokenEvents::Broadcast(&g_pendingSignals[i], Offsets::UNIT_EVENT_UNIT_AURA);
|
||||
g_pendingSignalCount = 0;
|
||||
}
|
||||
|
||||
@@ -1406,6 +1405,114 @@ int32_t ConsumeTriggeredApplication(uint64_t victim, const uint8_t *rec,
|
||||
// keeps the parse short and robust.
|
||||
constexpr int kMaxTargets = 16;
|
||||
|
||||
// ---- Triggered auras (SPELL_EFFECT_TRIGGER_SPELL) -------------------------
|
||||
//
|
||||
// An effect of type TRIGGER_SPELL makes the server cast EffectTriggerSpell[i]
|
||||
// as a side effect of the cast — Pounce 9005 → Pounce Bleed 9007 (Spell.dbc:
|
||||
// Effect = {6, 64, 80}, EffectTriggerSpell = {0, 9007, 0}). When the
|
||||
// triggered spell's own SMSG_SPELL_GO never reaches the client, its aura is
|
||||
// seated caster-less by OnAuraAdded. The trigger's SPELL_GO names the caster,
|
||||
// so attribution goes through it, read off the DBC link rather than a table.
|
||||
//
|
||||
// A hit by the TRIGGER doesn't prove the triggered aura landed: a target
|
||||
// immune to bleed takes Pounce's stun but not its bleed. So a fresh
|
||||
// application is ARMED, and only the application hook — the descriptor proof
|
||||
// that it landed — consumes the arm (same shape as the judgement arm above).
|
||||
// An instance this caster already holds on the target is refreshed in place
|
||||
// as well: the server's re-apply reuses the descriptor slot, so no
|
||||
// OnAuraAdded fires (the remove+add RefreshTriggeredExisting mirrors).
|
||||
//
|
||||
// Any caster: nothing here needs the caster's talents, and a remote caster
|
||||
// gets the base duration exactly as their directly cast auras do.
|
||||
|
||||
struct TrigAuraArm {
|
||||
uint64_t victim; // 0 = empty
|
||||
uint64_t caster;
|
||||
uint32_t spellId; // the triggered aura
|
||||
uint32_t untilMs;
|
||||
};
|
||||
constexpr int kTrigAuraArmMax = 8;
|
||||
constexpr uint32_t kTrigAuraArmTtlMs = 2000;
|
||||
TrigAuraArm g_trigAuraArms[kTrigAuraArmMax];
|
||||
|
||||
void ArmTriggeredAura(uint64_t victim, uint32_t spellId, uint64_t caster,
|
||||
uint32_t now) {
|
||||
TrigAuraArm *slot = nullptr;
|
||||
for (auto &a : g_trigAuraArms) {
|
||||
if (a.victim == victim && a.spellId == spellId && a.caster == caster) {
|
||||
slot = &a; // re-trigger inside the TTL — refresh in place
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr &&
|
||||
(a.victim == 0 || Time::Clock::Reached(now, a.untilMs)))
|
||||
slot = &a;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
slot = &g_trigAuraArms[0]; // all live — steal the first
|
||||
*slot = {victim, caster, spellId, now + kTrigAuraArmTtlMs};
|
||||
}
|
||||
|
||||
// The caster armed for `spellId` landing on `victim`, or 0. One-shot.
|
||||
uint64_t ConsumeTriggeredAura(uint64_t victim, uint32_t spellId, uint32_t now) {
|
||||
if (victim == 0)
|
||||
return 0;
|
||||
for (auto &a : g_trigAuraArms) {
|
||||
if (a.victim != victim || a.spellId != spellId ||
|
||||
Time::Clock::Reached(now, a.untilMs))
|
||||
continue;
|
||||
const uint64_t caster = a.caster;
|
||||
a = {};
|
||||
return caster;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Duration of an aura cast by `caster`. Combo-point finishers (Rupture,
|
||||
// Kidney Shot, …) have their real duration computed at cast time from the
|
||||
// combo points spent — the base-duration helper can't know it.
|
||||
// ComboDuration mirrors the server's computation from the CP snapshot its
|
||||
// send hook captured; 0 means "not combo-scaled", fall through to the base
|
||||
// path. 0 overall = unknown.
|
||||
uint32_t CastDurationMs(const uint8_t *rec, uint32_t spellId,
|
||||
bool casterIsPlayer) {
|
||||
const uint32_t comboMs =
|
||||
casterIsPlayer ? ComboDuration::TryComboScaledMs(rec, spellId) : 0;
|
||||
return comboMs != 0 ? comboMs : SpellDurationMs(rec, casterIsPlayer);
|
||||
}
|
||||
|
||||
void ApplyTriggeredAuras(uint64_t caster, uint32_t spellId, const uint8_t *rec,
|
||||
const uint64_t *targets, int numTargets) {
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
const auto *effects = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT);
|
||||
const auto *triggered = reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_TRIGGER_SPELL);
|
||||
const bool casterIsPlayer = (caster == Unit::Identity::PlayerGuid());
|
||||
const uint32_t now = NowMs();
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (effects[i] != Offsets::SPELL_EFFECT_TRIGGER_SPELL ||
|
||||
triggered[i] <= 0)
|
||||
continue;
|
||||
const auto auraId = static_cast<uint32_t>(triggered[i]);
|
||||
if (auraId == spellId)
|
||||
continue;
|
||||
const uint8_t *auraRec = Spell::Lookup::RecordForID(triggered[i]);
|
||||
if (!SpellAppliesAura(auraRec))
|
||||
continue;
|
||||
const uint32_t durationMs =
|
||||
CastDurationMs(auraRec, auraId, casterIsPlayer);
|
||||
const uint32_t expirationMs = durationMs > 0 ? now + durationMs : 0;
|
||||
for (int t = 0; t < numTargets; ++t) {
|
||||
ArmTriggeredAura(targets[t], auraId, caster, now);
|
||||
if (FindByCaster(targets[t], auraId, caster) != nullptr &&
|
||||
DescriptorListsAura(targets[t], auraId))
|
||||
StoreFromCast(targets[t], auraId, caster, expirationMs,
|
||||
durationMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The self-contained SPELL_GO processing (succeeded events, server-side
|
||||
// duration edits, aura-source caching). None of it reads engine descriptor
|
||||
// state, so it's order-independent of the engine's own SPELL_GO handler —
|
||||
@@ -1455,19 +1562,23 @@ void HandleSpellGo(uint64_t caster, uint32_t spellId, const uint64_t *targets,
|
||||
for (int i = 0; i < numTargets; ++i)
|
||||
ArmTriggeredApplications(targets[i], spellId, rec);
|
||||
|
||||
// The units this cast's auras land on: the hit list, or the caster itself
|
||||
// when the packet carries none (self-cast with a caster-implicit target).
|
||||
// Shared by the triggered auras and the cast's own aura below.
|
||||
const uint64_t *auraTargets = targets;
|
||||
int numAuraTargets = numTargets;
|
||||
if (numTargets == 0) {
|
||||
auraTargets = &caster;
|
||||
numAuraTargets = 1;
|
||||
}
|
||||
|
||||
ApplyTriggeredAuras(caster, spellId, rec, auraTargets, numAuraTargets);
|
||||
|
||||
if (!SpellAppliesAura(rec))
|
||||
return;
|
||||
|
||||
const bool casterIsPlayer = (caster == Unit::Identity::PlayerGuid());
|
||||
// Combo-point finishers (Rupture, Kidney Shot, …) have their real
|
||||
// duration computed at cast time from the combo points spent — the
|
||||
// base-duration helper can't know it. ComboDuration mirrors the
|
||||
// server's computation from the CP snapshot its send hook captured;
|
||||
// 0 means "not combo-scaled", fall through to the base path.
|
||||
uint32_t durationMs =
|
||||
casterIsPlayer ? ComboDuration::TryComboScaledMs(rec, spellId) : 0;
|
||||
if (durationMs == 0)
|
||||
durationMs = SpellDurationMs(rec, casterIsPlayer);
|
||||
const uint32_t durationMs = CastDurationMs(rec, spellId, casterIsPlayer);
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
|
||||
// Let the application hooks recover the player-talented duration for this
|
||||
@@ -1476,13 +1587,8 @@ void HandleSpellGo(uint64_t caster, uint32_t spellId, const uint64_t *targets,
|
||||
if (casterIsPlayer)
|
||||
RememberPlayerCast(spellId);
|
||||
|
||||
if (numTargets == 0) {
|
||||
// No explicit hit list (self-cast with caster-implicit target).
|
||||
StoreFromCast(caster, spellId, caster, expirationMs, durationMs);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < numTargets; ++i)
|
||||
StoreFromCast(targets[i], spellId, caster, expirationMs, durationMs);
|
||||
for (int i = 0; i < numAuraTargets; ++i)
|
||||
StoreFromCast(auraTargets[i], spellId, caster, expirationMs, durationMs);
|
||||
}
|
||||
|
||||
// SMSG_SPELL_GO parse (funnel subscriber). At the leaf handler the engine has
|
||||
@@ -1515,10 +1621,10 @@ const Net::PacketDispatch::AutoSubscribe _spellGoSub{&SpellGoSub};
|
||||
|
||||
// ---- Aura-application co-hooks (timing for proc / triggered auras) -------
|
||||
|
||||
// Classify by the slot's flag nibble (UNIT_AURA_FLAG_HARMFUL), not its range:
|
||||
// the server parks debuffs in buff slots once the 16 are full, and the nibble
|
||||
// is what records their real polarity. The descriptor write pass has already
|
||||
// landed the flags when the application hooks fire (see Aura::Data).
|
||||
// Defer to `Aura::Data::IsSlotHarmful` rather than testing the range here — how
|
||||
// polarity is encoded differs between servers, and that function is the one
|
||||
// place that decides. The descriptor write pass has already landed both the
|
||||
// slot and its flags by the time the application hooks fire.
|
||||
int8_t KindForSlot(const void *unit, int slot) {
|
||||
return Aura::Data::IsSlotHarmful(static_cast<const uint8_t *>(unit), slot)
|
||||
? KIND_HARMFUL
|
||||
@@ -1565,6 +1671,16 @@ void StampApplication(void *unit, uint32_t spellId, int slot) {
|
||||
durationMs = 1;
|
||||
}
|
||||
}
|
||||
// A triggered-aura arm (Pounce → Pounce Bleed): the server cast this aura
|
||||
// off a trigger whose SPELL_GO named the caster, and this application is
|
||||
// the proof it landed. Target-scoped, so it goes before the spell-scoped
|
||||
// recent-cast window below, for the same reason as the nettle arm.
|
||||
if (caster == 0) {
|
||||
caster = ConsumeTriggeredAura(unitGuid, spellId, NowMs());
|
||||
if (caster != 0)
|
||||
durationMs =
|
||||
SpellDurationMs(rec, caster == Unit::Identity::PlayerGuid());
|
||||
}
|
||||
if (caster == 0) {
|
||||
const bool byPlayer = WasRecentPlayerCast(spellId);
|
||||
durationMs = SpellDurationMs(rec, byPlayer);
|
||||
|
||||
@@ -389,15 +389,23 @@ void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "gsub", &Script_string_gsub);
|
||||
Game::Lua::RegisterTableFunction("string", "reverse", &Script_string_reverse);
|
||||
Game::Lua::RegisterTableFunction("string", "split", &Script_strsplit);
|
||||
// The bare `gsub` global (1.12 exposes the string functions as globals
|
||||
// too) must match `string.gsub`, so addons calling either form get the
|
||||
// table-replacement upgrade.
|
||||
Game::Lua::RegisterGlobalFunction("gsub", &Script_string_gsub);
|
||||
Game::Lua::RegisterGlobalFunction("strsplit", &Script_strsplit);
|
||||
Game::Lua::RegisterGlobalFunction("strjoin", &Script_strjoin);
|
||||
Game::Lua::RegisterGlobalFunction("strtrim", &Script_strtrim);
|
||||
Game::Lua::RegisterTableFunction("string", "trim", &Script_strtrim);
|
||||
Game::Lua::RegisterTableFunction("string", "join", &Script_strjoin);
|
||||
// 4.3.4's compat.lua binds each short global and its string-table twin
|
||||
// to ONE function by assignment (`gsub = str.gsub`, `strrev =
|
||||
// str.reverse`, `str.trim = strtrim`, `str.split = strsplit`, `str.join =
|
||||
// strjoin`), so `strtrim == string.trim`. A second Register* call would
|
||||
// mint a second closure over the same C function, so the globals alias
|
||||
// the table entries by value instead. The string-table half is what lets
|
||||
// `(" x "):trim()` work and libraries capture it at file scope
|
||||
// (AceConfigCmd-3.0: `strtrim = string.trim`); the bare `gsub` gets the
|
||||
// table-replacement upgrade by being the same function.
|
||||
Game::Lua::RegisterGlobalAlias("gsub", "string", "gsub");
|
||||
Game::Lua::RegisterGlobalAlias("strsplit", "string", "split");
|
||||
Game::Lua::RegisterGlobalAlias("strjoin", "string", "join");
|
||||
Game::Lua::RegisterGlobalAlias("strtrim", "string", "trim");
|
||||
Game::Lua::RegisterGlobalAlias("strrev", "string", "reverse");
|
||||
Game::Lua::RegisterGlobalFunction("strreplace", &Script_strreplace);
|
||||
Game::Lua::RegisterGlobalFunction("strrev", &Script_string_reverse);
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing these; RegisterTableFunction
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `success, ... = xpcall(f, msgh [, arg1, ...])` — call `f` under `msgh`,
|
||||
// forwarding the trailing arguments to it.
|
||||
//
|
||||
// 1.12's Lua takes only `xpcall(f, msgh)`. Its implementation is explicit
|
||||
// about it: `lua_settop(L, 2)` throws any further arguments away, then
|
||||
// `lua_pcall(L, 0, ...)` calls `f` with none. So the argument-forwarding form
|
||||
// doesn't merely fail — it silently calls `f` with nothing, and a function
|
||||
// that reads its parameters sees nil. (Note this is NOT a 5.0-vs-5.1
|
||||
// difference: both take `(f, msgh)`. Forwarding arrived later.)
|
||||
//
|
||||
// Callers work around it with a capturing closure —
|
||||
// `xpcall(function() return f(a, b) end, msgh)` — which is what
|
||||
// `ExecuteFrameScript` in the embedded addon does. That stays correct; this
|
||||
// just removes the need for it.
|
||||
//
|
||||
// Why a C function and not a Lua wrapper over the original: a Lua wrapper
|
||||
// would allocate a closure AND an `arg` table on every call (see the vararg
|
||||
// note in FunctionUtil.lua), putting an allocation on any error-guarded path,
|
||||
// including per-frame ones. `HookSecureFunc`'s `kUnhookableNames` also lists
|
||||
// `xpcall` precisely because shadowing a core primitive with a Lua closure
|
||||
// diverges from what C-side callers get. Replacing it with a real C function
|
||||
// has neither problem, and matches how `select` and `unpack` are backported
|
||||
// in this directory.
|
||||
//
|
||||
// A two-argument call is bit-for-bit the old behavior (zero forwarded args),
|
||||
// so this is purely additive for existing code.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::XPCall {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_xpcall(void *L) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (top < 2) {
|
||||
Game::Lua::Error(L, "bad argument #2 to 'xpcall' (value expected)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
|
||||
// `lua_pcall` wants the callee and its arguments contiguous on top, and
|
||||
// the handler at a fixed index. Incoming order is
|
||||
// `f(1), msgh(2), a1(3)..aN(top)`, so the handler sits between the two —
|
||||
// shuffle it below `f`:
|
||||
//
|
||||
// PushValue(2) → f, msgh, a1..aN, msgh
|
||||
// Insert(1) → msgh, f, msgh, a1..aN (top moved down to index 1)
|
||||
// Remove(3) → msgh, f, a1..aN
|
||||
//
|
||||
// 5.2 does the same reshuffle with a single `lua_rotate`, which this Lua
|
||||
// has no equivalent of.
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::Insert(L, 1);
|
||||
Game::Lua::Remove(L, 3);
|
||||
|
||||
const int status =
|
||||
Game::Lua::PCall(L, /*nargs*/ top - 2, Game::Lua::MULTRET, /*errfunc*/ 1);
|
||||
|
||||
// The stack now holds the handler plus either f's results or the handler's
|
||||
// return. Both cases want the status boolean in front of them, and the
|
||||
// handler itself dropped from the return set — which returning
|
||||
// `GetTop - 1` does, since results are taken from the top.
|
||||
Game::Lua::PushBool(L, status == 0);
|
||||
Game::Lua::Insert(L, 2);
|
||||
return Game::Lua::GetTop(L) - 1;
|
||||
}
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("xpcall", &Script_xpcall);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("xpcall", &Script_xpcall);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::XPCall
|
||||
+217
-499
@@ -12,391 +12,189 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Backports the direct-action and override binding APIs introduced in 2.0.
|
||||
// Normal bindings are stored by vanilla's SetBinding implementation using the
|
||||
// later-client SPELL/ITEM/MACRO/CLICK command strings. The resolved-command
|
||||
// hook executes those four command families; every native binding command is
|
||||
// passed through unchanged.
|
||||
// The binding API 4.3.4 has and 1.12 lacks (decomp Ui/UIBindings.cpp,
|
||||
// UIBindings_Scripts.cpp):
|
||||
//
|
||||
// Override bindings live in a separate owner-scoped layer in front of the
|
||||
// native key lookup. They are deliberately never written to the saved binding
|
||||
// table, so clearing an owner immediately reveals the next override or the
|
||||
// unchanged native binding underneath it.
|
||||
// SetBindingSpell / Item / Macro / Click — "SPELL x" etc. into the
|
||||
// current set through the engine's own SetBinding.
|
||||
// SetOverrideBinding[Spell / Item / Macro / Click], ClearOverrideBindings
|
||||
// — owner-scoped bindings in front of the saved ones, never saved.
|
||||
// GetBindingAction(key [, checkOverride]) — sees overrides when asked.
|
||||
//
|
||||
// Overrides — 4.3.4's OVERRIDEKEYBINDING: per key (case-insensitive), two
|
||||
// lists (normal, priority) of {owner, command}; setting an owner's binding
|
||||
// unlinks its old one from both and links the new one at the head of its
|
||||
// priority's list, so the newest priority binding wins, then the newest
|
||||
// normal one. 4.3.4 consults them inside its key lookup, before the saved
|
||||
// binding, for every key it tries. 1.12's key dispatch (FUN_BINDING_KEY_DISPATCH)
|
||||
// tries two keys — the pressed one, then the pressed one minus its first
|
||||
// modifier — so the co-hook tries an override, then the saved binding
|
||||
// (FUN_BINDINGS_GET_COMMAND_ACTION), for each in turn, and lets the engine
|
||||
// run a saved binding it finds.
|
||||
//
|
||||
// Around whatever runs, the key-state override (Input::Modifier) hides the
|
||||
// matched key's own modifiers, as 4.3.4's ExecKey does. Direct actions
|
||||
// (Bindings::Command) run only for a key: the command co-hook acts while the
|
||||
// manager's executing flag — set by the key dispatch, not by RunBinding — is
|
||||
// up, which is 4.3.4's ExecKey / ExecCommand split.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "macro/Execute.h"
|
||||
#include "bindings/Command.h"
|
||||
#include "input/Modifier.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <list>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Bindings::Api {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char kSpellPrefix[] = "SPELL ";
|
||||
constexpr const char kItemPrefix[] = "ITEM ";
|
||||
constexpr const char kMacroPrefix[] = "MACRO ";
|
||||
constexpr const char kClickPrefix[] = "CLICK ";
|
||||
constexpr const char kDefaultMouseButton[] = "LeftButton";
|
||||
using KeyDispatch_t = int(__fastcall *)(void *mgr, void *edx, const char *key, int isDown);
|
||||
using CommandExecute_t = int(__fastcall *)(void *mgr, void *edx, const char *cmd, int isDown);
|
||||
using GetCommandAction_t = const char *(__thiscall *)(void *mgr, const char *key);
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
using ToBool_t = int(__fastcall *)(void *L, int idx, int dflt);
|
||||
|
||||
using ScriptSetBinding_t = int(__fastcall *)(void *L);
|
||||
using KeyDispatch_t = int(__thiscall *)(void *mgr, const char *key, int isDown);
|
||||
using CommandExecute_t = int(__thiscall *)(void *mgr, const char *cmd, int isDown);
|
||||
KeyDispatch_t g_keyDispatchOriginal = nullptr;
|
||||
CommandExecute_t g_commandExecuteOriginal = nullptr;
|
||||
|
||||
KeyDispatch_t KeyDispatch_o = nullptr;
|
||||
CommandExecute_t CommandExecute_o = nullptr;
|
||||
// --- override table (OVERRIDEKEYBINDING) ------------------------------------
|
||||
|
||||
enum class CommandType {
|
||||
None,
|
||||
Spell,
|
||||
Item,
|
||||
Macro,
|
||||
Click,
|
||||
};
|
||||
|
||||
struct ParsedCommand {
|
||||
CommandType type = CommandType::None;
|
||||
const char *argument = nullptr;
|
||||
};
|
||||
|
||||
struct OverrideEntry {
|
||||
const void *owner = nullptr;
|
||||
std::string key;
|
||||
struct Entry {
|
||||
const void *owner;
|
||||
std::string command;
|
||||
bool priority = false;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
std::vector<OverrideEntry> g_overrides;
|
||||
std::unordered_map<std::string, std::string> g_activeOverrides;
|
||||
uint64_t g_nextSequence = 0;
|
||||
struct OverrideKey {
|
||||
std::string key;
|
||||
std::list<Entry> lists[2]; // [0] normal, [1] priority; head = newest
|
||||
};
|
||||
|
||||
// Normalizes a binding key to the engine's own canonical form so a stored
|
||||
// override matches the string FUN_BINDING_KEY_DISPATCH builds at keypress. The
|
||||
// engine's key-string builder (FUN_004b6630) always emits modifiers in the
|
||||
// fixed order ALT-CTRL-SHIFT- ahead of the base key, regardless of the order
|
||||
// the caller passed them; vanilla SetBinding never reorders, so the override
|
||||
// layer does it here. Both the store and the lookup run through this, so they
|
||||
// stay consistent while also matching the engine's keypress form.
|
||||
std::string CanonicalKey(const char *key) {
|
||||
std::string result = key ? key : "";
|
||||
for (char &character : result) {
|
||||
if (character >= 'a' && character <= 'z')
|
||||
character = static_cast<char>(character - 'a' + 'A');
|
||||
std::vector<OverrideKey> g_overrides;
|
||||
|
||||
OverrideKey *FindKey(const char *key) {
|
||||
for (OverrideKey &entry : g_overrides)
|
||||
if (_stricmp(entry.key.c_str(), key) == 0)
|
||||
return &entry;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// OVERRIDEKEYBINDING::SetOverrideBinding.
|
||||
void SetOverride(OverrideKey &binding, const void *owner, bool priority, const char *command) {
|
||||
for (auto &list : binding.lists)
|
||||
list.remove_if([owner](const Entry &entry) { return entry.owner == owner; });
|
||||
if (command != nullptr && *command != '\0')
|
||||
binding.lists[priority ? 1 : 0].push_front(Entry{owner, command});
|
||||
}
|
||||
|
||||
// OVERRIDEKEYBINDING::GetActiveBinding.
|
||||
const char *ActiveOverride(const char *key) {
|
||||
const OverrideKey *binding = FindKey(key);
|
||||
if (binding == nullptr)
|
||||
return nullptr;
|
||||
for (int i = 1; i >= 0; --i)
|
||||
if (!binding->lists[i].empty())
|
||||
return binding->lists[i].front().command.c_str();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// CGUIBindings::SetOverrideBinding.
|
||||
void SetOverrideBinding(const void *owner, bool priority, const char *key, const char *command) {
|
||||
OverrideKey *binding = FindKey(key);
|
||||
if (binding == nullptr) {
|
||||
g_overrides.push_back(OverrideKey{key, {}});
|
||||
binding = &g_overrides.back();
|
||||
}
|
||||
|
||||
// Strip the three modifier prefixes in whatever order/repetition they
|
||||
// appear, then re-emit them in the engine's fixed order. Prefix-stripping
|
||||
// (rather than splitting on '-') keeps the minus base key correct, e.g.
|
||||
// "SHIFT--" -> modifier SHIFT + base "-".
|
||||
bool alt = false, ctrl = false, shift = false;
|
||||
size_t base = 0;
|
||||
for (bool matched = true; matched;) {
|
||||
matched = false;
|
||||
if (result.compare(base, 4, "ALT-") == 0) {
|
||||
alt = true;
|
||||
base += 4;
|
||||
matched = true;
|
||||
} else if (result.compare(base, 5, "CTRL-") == 0) {
|
||||
ctrl = true;
|
||||
base += 5;
|
||||
matched = true;
|
||||
} else if (result.compare(base, 6, "SHIFT-") == 0) {
|
||||
shift = true;
|
||||
base += 6;
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alt && !ctrl && !shift)
|
||||
return result;
|
||||
|
||||
std::string canonical;
|
||||
if (alt)
|
||||
canonical += "ALT-";
|
||||
if (ctrl)
|
||||
canonical += "CTRL-";
|
||||
if (shift)
|
||||
canonical += "SHIFT-";
|
||||
canonical += result.substr(base);
|
||||
|
||||
return canonical;
|
||||
SetOverride(*binding, owner, priority, command);
|
||||
}
|
||||
|
||||
std::string MakeCommand(const char *prefix, const char *arg) {
|
||||
std::string cmd = prefix;
|
||||
cmd += arg;
|
||||
return cmd;
|
||||
// --- key dispatch -------------------------------------------------------------
|
||||
|
||||
void *Manager() { return Game::Read<void *>(Offsets::VAR_BINDINGS_MANAGER); }
|
||||
|
||||
const char *SavedCommand(void *mgr, const char *key) {
|
||||
return reinterpret_cast<GetCommandAction_t>(Offsets::FUN_BINDINGS_GET_COMMAND_ACTION)(mgr,
|
||||
key);
|
||||
}
|
||||
|
||||
std::string MakeClickCommand(const char *btnName, const char *mouseBtn) {
|
||||
std::string cmd = MakeCommand(kClickPrefix, btnName);
|
||||
if (mouseBtn != nullptr) {
|
||||
cmd += ':';
|
||||
cmd += mouseBtn;
|
||||
}
|
||||
|
||||
return cmd;
|
||||
int ExecuteCommand(void *mgr, const char *command, int isDown) {
|
||||
if (Command::IsDirectAction(command))
|
||||
return Command::RunKey(command, isDown);
|
||||
return g_commandExecuteOriginal(mgr, nullptr, command, isDown);
|
||||
}
|
||||
|
||||
ParsedCommand ParseCommand(const char *cmd) {
|
||||
if (cmd == nullptr)
|
||||
return {};
|
||||
|
||||
struct PrefixEntry {
|
||||
const char *prefix;
|
||||
size_t length;
|
||||
CommandType type;
|
||||
};
|
||||
|
||||
static constexpr PrefixEntry prefixes[] = {
|
||||
{kSpellPrefix, sizeof(kSpellPrefix) - 1, CommandType::Spell},
|
||||
{kItemPrefix, sizeof(kItemPrefix) - 1, CommandType::Item},
|
||||
{kMacroPrefix, sizeof(kMacroPrefix) - 1, CommandType::Macro},
|
||||
{kClickPrefix, sizeof(kClickPrefix) - 1, CommandType::Click},
|
||||
};
|
||||
|
||||
for (const PrefixEntry &entry : prefixes) {
|
||||
if (std::strncmp(cmd, entry.prefix, entry.length) == 0)
|
||||
return {entry.type, cmd + entry.length};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
bool CallGlobalString(void *L, const char *func, const char *arg) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (!Game::Lua::PushGlobalFunction(L, func)) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, arg);
|
||||
const bool ok = Game::Lua::PCall(L, 1, 0, 0) == 0;
|
||||
Game::Lua::SetTop(L, top);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool CallTableString(void *L, const char *table, const char *func, const char *arg) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushString(L, table);
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int tableIndex = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushString(L, func);
|
||||
Game::Lua::GetTable(L, tableIndex);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return false;
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, arg);
|
||||
const bool ok = Game::Lua::PCall(L, 1, 0, 0) == 0;
|
||||
Game::Lua::SetTop(L, top);
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
void InvokeButtonClick(void *L, const char *btnName, size_t btnNameLength, const char *mouseBtn) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
|
||||
Game::Lua::PushLString(L, btnName, static_cast<unsigned int>(btnNameLength));
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
return;
|
||||
}
|
||||
|
||||
const int frameIndex = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushString(L, "Click");
|
||||
Game::Lua::GetTable(L, frameIndex);
|
||||
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::PushValue(L, frameIndex);
|
||||
Game::Lua::PushString(L, mouseBtn);
|
||||
Game::Lua::PCall(L, 2, 0, 0);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
void ExecuteClick(void *L, const char *arg) {
|
||||
const char *separator = std::strrchr(arg, ':');
|
||||
if (separator == arg || (separator != nullptr && separator[1] == '\0'))
|
||||
return;
|
||||
|
||||
const char *mouseBtn = kDefaultMouseButton;
|
||||
size_t btnNameLength = std::strlen(arg);
|
||||
if (separator != nullptr) {
|
||||
btnNameLength = static_cast<size_t>(separator - arg);
|
||||
mouseBtn = separator + 1;
|
||||
}
|
||||
|
||||
if (btnNameLength == 0)
|
||||
return;
|
||||
|
||||
InvokeButtonClick(L, arg, btnNameLength, mouseBtn);
|
||||
}
|
||||
|
||||
// Returns true for every recognized direct-action command, including malformed
|
||||
// or stale commands. Vanilla cannot execute these namespaces, so a bad command
|
||||
// is consumed as a no-op instead of falling through to an unrelated handler.
|
||||
bool ExecuteSpecialCommand(const char *cmd, int isDown) {
|
||||
const ParsedCommand parsed = ParseCommand(cmd);
|
||||
if (parsed.type == CommandType::None)
|
||||
return false;
|
||||
|
||||
if (!isDown || parsed.argument == nullptr || *parsed.argument == '\0')
|
||||
return true;
|
||||
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return true;
|
||||
|
||||
switch (parsed.type) {
|
||||
case CommandType::Spell:
|
||||
CallGlobalString(L, "CastSpellByName", parsed.argument);
|
||||
break;
|
||||
case CommandType::Item:
|
||||
CallTableString(L, "C_Item", "UseItemByName", parsed.argument);
|
||||
break;
|
||||
case CommandType::Macro:
|
||||
Macro::Execute::Saved(L, parsed.argument);
|
||||
break;
|
||||
case CommandType::Click:
|
||||
ExecuteClick(L, parsed.argument);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const OverrideEntry *FindEffectiveOverride(const std::string &key) {
|
||||
const OverrideEntry *best = nullptr;
|
||||
for (const OverrideEntry &entry : g_overrides) {
|
||||
if (entry.key != key)
|
||||
continue;
|
||||
|
||||
if (best == nullptr || entry.priority > best->priority ||
|
||||
(entry.priority == best->priority && entry.sequence > best->sequence))
|
||||
best = &entry;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
void SetOverride(const void *owner, bool priority, const std::string &key, const std::string &cmd) {
|
||||
auto existing =
|
||||
std::find_if(g_overrides.begin(), g_overrides.end(),
|
||||
[&](const OverrideEntry &entry) {
|
||||
return entry.owner == owner && entry.key == key;
|
||||
});
|
||||
|
||||
if (existing == g_overrides.end()) {
|
||||
g_overrides.push_back(
|
||||
{owner, key, cmd, priority, ++g_nextSequence});
|
||||
return;
|
||||
}
|
||||
|
||||
existing->command = cmd;
|
||||
existing->priority = priority;
|
||||
existing->sequence = ++g_nextSequence;
|
||||
}
|
||||
|
||||
void RemoveOverride(const void *owner, const std::string &key) {
|
||||
g_overrides.erase(std::remove_if(g_overrides.begin(), g_overrides.end(),
|
||||
[&](const OverrideEntry &entry) {
|
||||
return entry.owner == owner &&
|
||||
entry.key == key;
|
||||
}),
|
||||
g_overrides.end());
|
||||
}
|
||||
|
||||
int ExecuteCommand(void *mgr, const char *cmd, int isDown) {
|
||||
if (ExecuteSpecialCommand(cmd, isDown))
|
||||
return 1;
|
||||
|
||||
return CommandExecute_o(mgr, cmd, isDown);
|
||||
}
|
||||
|
||||
int __fastcall CommandExecute_h(void *mgr, void *, const char *cmd, int isDown) {
|
||||
return ExecuteCommand(mgr, cmd, isDown);
|
||||
}
|
||||
|
||||
// Runs an override-resolved command with the same execution context the engine
|
||||
// wraps around a native keypress. FUN_BINDING_KEY_DISPATCH saves the frame-
|
||||
// script exec-context, zeroes it for the nested command, marks the manager as
|
||||
// mid-command (`+0xD8`), then restores the context and floors the nesting
|
||||
// depth. Native-table bindings already run inside this (the engine sets it up
|
||||
// before calling FUN_BINDING_COMMAND_EXECUTE, which we co-hook); overrides
|
||||
// bypass that path, so we mirror it here. Without it, a native Bindings.xml
|
||||
// command dispatched through an override would run without the context its
|
||||
// handler expects. Special SPELL/ITEM/MACRO/CLICK commands run harmlessly
|
||||
// inside the same wrapper.
|
||||
int DispatchOverrideCommand(void *mgr, const char *cmd, int isDown) {
|
||||
volatile uint32_t *context =
|
||||
reinterpret_cast<volatile uint32_t *>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
volatile uint32_t *depth = reinterpret_cast<volatile uint32_t *>(
|
||||
Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH);
|
||||
volatile uint32_t *executing =
|
||||
Game::Ptr<volatile uint32_t>(mgr, Offsets::OFF_BINDING_MANAGER_EXECUTING);
|
||||
|
||||
const uint32_t savedContext = *context;
|
||||
*depth = *depth + 1;
|
||||
if (*depth != 0)
|
||||
*context = 0;
|
||||
|
||||
*executing = 1;
|
||||
const int result = ExecuteCommand(mgr, cmd, isDown);
|
||||
*executing = 0;
|
||||
|
||||
if (*depth != 0)
|
||||
*context = savedContext;
|
||||
if (static_cast<int32_t>(*depth) - 1 > 0)
|
||||
*depth = *depth - 1;
|
||||
else
|
||||
*depth = 0;
|
||||
|
||||
// An override's command, run in the frame the engine dispatch gives a saved
|
||||
// one: exec context saved and cleared, depth bumped, the manager's executing
|
||||
// flag up (FUN_BINDING_KEY_DISPATCH, see Offsets.h).
|
||||
int RunOverride(void *mgr, const char *command, int isDown) {
|
||||
const std::string copy = command; // the command may clear its own override
|
||||
auto &context = Game::Ref<uint32_t>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
auto &depth = Game::Ref<int32_t>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH);
|
||||
auto &executing = Game::Ref<uint32_t>(mgr, Offsets::OFF_BINDING_MANAGER_EXECUTING);
|
||||
const uint32_t saved = context;
|
||||
if (++depth != 0)
|
||||
context = 0;
|
||||
executing = 1;
|
||||
const int result = ExecuteCommand(mgr, copy.c_str(), isDown);
|
||||
executing = 0;
|
||||
if (depth != 0)
|
||||
context = saved;
|
||||
depth = depth - 1 > 0 ? depth - 1 : 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
int __fastcall KeyDispatch_h(void *mgr, void *, const char *key, int isDown) {
|
||||
const std::string canonicalKey = CanonicalKey(key);
|
||||
int __fastcall KeyDispatch_h(void *mgr, void *edx, const char *key, int isDown) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
return g_keyDispatchOriginal(mgr, edx, key, isDown);
|
||||
|
||||
// Keep a key-down paired with its override command even if that override is
|
||||
// cleared while the command executes. The newly revealed native binding
|
||||
// must not receive only the key-up half of the same physical press.
|
||||
if (!isDown) {
|
||||
auto active = g_activeOverrides.find(canonicalKey);
|
||||
if (active != g_activeOverrides.end()) {
|
||||
DispatchOverrideCommand(mgr, active->second.c_str(), 0);
|
||||
g_activeOverrides.erase(active);
|
||||
return 1;
|
||||
// The engine's two keys: as pressed, then without its first modifier.
|
||||
const char *dash = std::strchr(key, '-');
|
||||
const char *candidates[2] = {key, (dash != nullptr && dash[1] != '\0') ? dash + 1 : nullptr};
|
||||
for (const char *candidate : candidates) {
|
||||
if (candidate == nullptr)
|
||||
break;
|
||||
if (const char *command = ActiveOverride(candidate)) {
|
||||
const Input::Modifier::KeyStateOverride keyState(candidate);
|
||||
return RunOverride(mgr, command, isDown);
|
||||
}
|
||||
if (SavedCommand(mgr, candidate) != nullptr) {
|
||||
const Input::Modifier::KeyStateOverride keyState(candidate);
|
||||
return g_keyDispatchOriginal(mgr, edx, key, isDown);
|
||||
}
|
||||
}
|
||||
return g_keyDispatchOriginal(mgr, edx, key, isDown);
|
||||
}
|
||||
|
||||
const OverrideEntry *entry = FindEffectiveOverride(canonicalKey);
|
||||
if (entry == nullptr)
|
||||
return KeyDispatch_o(mgr, key, isDown);
|
||||
// Direct actions only for a key (executing flag up); RunBinding reaches the
|
||||
// engine's command table, which has none of them — as in 4.3.4.
|
||||
int __fastcall CommandExecute_h(void *mgr, void *edx, const char *command, int isDown) {
|
||||
if (Game::Read<uint32_t>(mgr, Offsets::OFF_BINDING_MANAGER_EXECUTING) != 0 &&
|
||||
Command::IsDirectAction(command))
|
||||
return Command::RunKey(command, isDown);
|
||||
return g_commandExecuteOriginal(mgr, edx, command, isDown);
|
||||
}
|
||||
|
||||
if (isDown)
|
||||
g_activeOverrides[canonicalKey] = entry->command;
|
||||
// --- Lua ----------------------------------------------------------------------
|
||||
|
||||
return DispatchOverrideCommand(mgr, entry->command.c_str(), isDown);
|
||||
bool LooseBool(void *L, int idx) {
|
||||
return reinterpret_cast<ToBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, 0) != 0;
|
||||
}
|
||||
|
||||
const char *OptString(void *L, int idx) {
|
||||
return Game::Lua::IsString(L, idx) ? Game::Lua::ToString(L, idx) : nullptr;
|
||||
}
|
||||
|
||||
// SetBinding(key, command) through the engine's own Script_SetBinding: its
|
||||
// key normalization, current-set write, UPDATE_BINDINGS and 1/nil result.
|
||||
int SetBinding(void *L, const std::string &key, const std::string &command) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushString(L, key.c_str());
|
||||
Game::Lua::PushString(L, command.c_str());
|
||||
return reinterpret_cast<ScriptFn_t>(Offsets::FUN_SCRIPT_SET_BINDING)(L);
|
||||
}
|
||||
|
||||
int SetBindingCommand(void *L, const char *prefix, const char *usage) {
|
||||
@@ -404,216 +202,136 @@ int SetBindingCommand(void *L, const char *prefix, const char *usage) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::string key = Game::Lua::ToString(L, 1);
|
||||
const std::string command = MakeCommand(prefix, Game::Lua::ToString(L, 2));
|
||||
|
||||
// Preserve the stock SetBinding return value, binding-set selection,
|
||||
// key normalization, UPDATE_BINDINGS event, and SaveBindings behavior.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushString(L, key.c_str());
|
||||
Game::Lua::PushString(L, command.c_str());
|
||||
auto setBinding =
|
||||
reinterpret_cast<ScriptSetBinding_t>(Offsets::FUN_SCRIPT_SET_BINDING);
|
||||
|
||||
return setBinding(L);
|
||||
}
|
||||
|
||||
const char *OptionalMouseButton(void *L, int index, const char *usage) {
|
||||
if (Game::Lua::GetTop(L) < index || Game::Lua::Type(L, index) == Game::Lua::TYPE_NIL)
|
||||
return nullptr;
|
||||
|
||||
if (!Game::Lua::IsString(L, index)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return Game::Lua::ToString(L, index);
|
||||
return SetBinding(L, Game::Lua::ToString(L, 1),
|
||||
std::string(prefix) + Game::Lua::ToString(L, 2));
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingSpell(void *L) {
|
||||
return SetBindingCommand(L, kSpellPrefix, "Usage: SetBindingSpell(key, spell)");
|
||||
return SetBindingCommand(L, "SPELL ", "Usage: SetBindingSpell(\"KEY\", \"spellname\")");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingItem(void *L) {
|
||||
return SetBindingCommand(L, kItemPrefix, "Usage: SetBindingItem(key, item)");
|
||||
return SetBindingCommand(L, "ITEM ", "Usage: SetBindingItem(\"KEY\", \"itemname\")");
|
||||
}
|
||||
int __fastcall Script_SetBindingMacro(void *L) {
|
||||
return SetBindingCommand(L, "MACRO ",
|
||||
"Usage: SetBindingMacro(\"KEY\", \"macroname\"|macroid)");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingMacro(void *L) {
|
||||
return SetBindingCommand(L, kMacroPrefix, "Usage: SetBindingMacro(key, macro)");
|
||||
std::string ClickCommand(const char *button, const char *mouseButton) {
|
||||
return std::string("CLICK ") + button + ":" +
|
||||
(mouseButton != nullptr ? mouseButton : "LeftButton");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBindingClick(void *L) {
|
||||
constexpr const char *usage = "Usage: SetBindingClick(key, buttonName [, mouseButton])";
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
Game::Lua::Error(
|
||||
L, "Usage: SetBindingClick(\"KEY\", \"buttonName\"[, \"mouseButton\"])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::string key = Game::Lua::ToString(L, 1);
|
||||
const char *buttonName = Game::Lua::ToString(L, 2);
|
||||
const char *mouseButton = OptionalMouseButton(L, 3, usage);
|
||||
const std::string command = MakeClickCommand(buttonName, mouseButton);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::PushString(L, key.c_str());
|
||||
Game::Lua::PushString(L, command.c_str());
|
||||
auto setBinding =
|
||||
reinterpret_cast<ScriptSetBinding_t>(Offsets::FUN_SCRIPT_SET_BINDING);
|
||||
|
||||
return setBinding(L);
|
||||
return SetBinding(L, Game::Lua::ToString(L, 1),
|
||||
ClickCommand(Game::Lua::ToString(L, 2), OptString(L, 3)));
|
||||
}
|
||||
|
||||
bool ReadOverrideHeader(void *L, const char *usage, const void **owner,
|
||||
bool *priority, std::string *key) {
|
||||
*owner = Game::Lua::ResolveObject(L, 1);
|
||||
if (*owner == nullptr || Game::Lua::Type(L, 2) != Game::Lua::TYPE_BOOLEAN ||
|
||||
!Game::Lua::IsString(L, 3)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return false;
|
||||
}
|
||||
// The owner argument: a frame, as 4.3.4 requires.
|
||||
const void *Owner(void *L) { return Game::Lua::ResolveFrame(L, 1, /*raiseError*/ false); }
|
||||
|
||||
*priority = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
*key = CanonicalKey(Game::Lua::ToString(L, 3));
|
||||
return true;
|
||||
}
|
||||
|
||||
int SetOverrideBindingCommand(void *L, const char *prefix, const char *usage) {
|
||||
const void *owner = nullptr;
|
||||
bool priority = false;
|
||||
|
||||
std::string key;
|
||||
if (!ReadOverrideHeader(L, usage, &owner, &priority, &key))
|
||||
return 0;
|
||||
|
||||
if (!Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
int __fastcall Script_SetOverrideBinding(void *L) {
|
||||
const void *owner = Owner(L);
|
||||
if (owner == nullptr || !Game::Lua::IsString(L, 3)) {
|
||||
Game::Lua::Error(L, "Usage: SetOverrideBinding(owner, isPriority, \"KEY\"[, \"COMMAND\"])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
SetOverride(owner, priority, key,
|
||||
MakeCommand(prefix, Game::Lua::ToString(L, 4)));
|
||||
|
||||
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), OptString(L, 4));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBinding(void *L) {
|
||||
constexpr const char *usage = "Usage: SetOverrideBinding(owner, isPriority, key [, command])";
|
||||
const void *owner = nullptr;
|
||||
bool priority = false;
|
||||
|
||||
std::string key;
|
||||
if (!ReadOverrideHeader(L, usage, &owner, &priority, &key))
|
||||
return 0;
|
||||
|
||||
if (Game::Lua::GetTop(L) < 4 ||
|
||||
Game::Lua::Type(L, 4) == Game::Lua::TYPE_NIL) {
|
||||
RemoveOverride(owner, key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!Game::Lua::IsString(L, 4)) {
|
||||
int SetOverrideCommand(void *L, const char *prefix, const char *usage) {
|
||||
const void *owner = Owner(L);
|
||||
if (owner == nullptr || !Game::Lua::IsString(L, 3) || !Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
SetOverride(owner, priority, key, Game::Lua::ToString(L, 4));
|
||||
const std::string command = std::string(prefix) + Game::Lua::ToString(L, 4);
|
||||
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), command.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingSpell(void *L) {
|
||||
return SetOverrideBindingCommand(
|
||||
L, kSpellPrefix,
|
||||
"Usage: SetOverrideBindingSpell(owner, isPriority, key, spell)");
|
||||
return SetOverrideCommand(
|
||||
L, "SPELL ", "Usage: SetOverrideBindingSpell(owner, isPriority, \"KEY\", \"spellname\")");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingItem(void *L) {
|
||||
return SetOverrideBindingCommand(
|
||||
L, kItemPrefix,
|
||||
"Usage: SetOverrideBindingItem(owner, isPriority, key, item)");
|
||||
return SetOverrideCommand(
|
||||
L, "ITEM ", "Usage: SetOverrideBindingItem(owner, isPriority, \"KEY\", \"itemname\")");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingMacro(void *L) {
|
||||
return SetOverrideBindingCommand(
|
||||
L, kMacroPrefix,
|
||||
"Usage: SetOverrideBindingMacro(owner, isPriority, key, macro)");
|
||||
return SetOverrideCommand(
|
||||
L, "MACRO ",
|
||||
"Usage: SetOverrideBindingMacro(owner, isPriority, \"KEY\", \"macroname\"|macroid)");
|
||||
}
|
||||
|
||||
int __fastcall Script_SetOverrideBindingClick(void *L) {
|
||||
constexpr const char *usage =
|
||||
"Usage: SetOverrideBindingClick(owner, "
|
||||
"isPriority, key, buttonName [, mouseButton])";
|
||||
|
||||
const void *owner = nullptr;
|
||||
bool priority = false;
|
||||
|
||||
std::string key;
|
||||
if (!ReadOverrideHeader(L, usage, &owner, &priority, &key))
|
||||
return 0;
|
||||
|
||||
if (!Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
const void *owner = Owner(L);
|
||||
if (owner == nullptr || !Game::Lua::IsString(L, 3) || !Game::Lua::IsString(L, 4)) {
|
||||
Game::Lua::Error(L, "Usage: SetOverrideBindingClick(owner, isPriority, \"KEY\", "
|
||||
"\"buttonName\"[, \"mouseButton\"])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *buttonName = Game::Lua::ToString(L, 4);
|
||||
const char *mouseButton = OptionalMouseButton(L, 5, usage);
|
||||
SetOverride(owner, priority, key,
|
||||
MakeClickCommand(buttonName, mouseButton));
|
||||
|
||||
const std::string command = ClickCommand(Game::Lua::ToString(L, 4), OptString(L, 5));
|
||||
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), command.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_ClearOverrideBindings(void *L) {
|
||||
const void *owner = Game::Lua::ResolveObject(L, 1);
|
||||
const void *owner = Owner(L);
|
||||
if (owner == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: ClearOverrideBindings(owner)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_overrides.erase(std::remove_if(g_overrides.begin(), g_overrides.end(),
|
||||
[&](const OverrideEntry &entry) {
|
||||
return entry.owner == owner;
|
||||
}),
|
||||
g_overrides.end());
|
||||
for (OverrideKey &binding : g_overrides)
|
||||
SetOverride(binding, owner, false, nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
// A fresh FrameScript state makes every owner pointer from the previous
|
||||
// state stale. Permanent bindings remain in vanilla's binding table.
|
||||
g_overrides.clear();
|
||||
g_activeOverrides.clear();
|
||||
g_nextSequence = 0;
|
||||
// GetBindingAction(key [, checkOverride]): 4.3.4's GetCommandAction checks the
|
||||
// override first when asked; the saved binding is the engine's own answer.
|
||||
int __fastcall Script_GetBindingAction(void *L) {
|
||||
if (Game::Lua::IsString(L, 1) && LooseBool(L, 2)) {
|
||||
if (const char *command = ActiveOverride(Game::Lua::ToString(L, 1))) {
|
||||
Game::Lua::PushString(L, command);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return reinterpret_cast<ScriptFn_t>(Offsets::FUN_SCRIPT_GET_BINDING_ACTION)(L);
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingSpell", &Script_SetBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingItem", &Script_SetBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingMacro", &Script_SetBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingClick", &Script_SetBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBinding",
|
||||
&Script_SetOverrideBinding);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingSpell",
|
||||
&Script_SetOverrideBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingItem",
|
||||
&Script_SetOverrideBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingMacro",
|
||||
&Script_SetOverrideBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingClick",
|
||||
&Script_SetOverrideBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("ClearOverrideBindings",
|
||||
&Script_ClearOverrideBindings);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBinding", &Script_SetOverrideBinding);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingSpell", &Script_SetOverrideBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingItem", &Script_SetOverrideBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingMacro", &Script_SetOverrideBindingMacro);
|
||||
Game::Lua::RegisterGlobalFunction("SetOverrideBindingClick", &Script_SetOverrideBindingClick);
|
||||
Game::Lua::RegisterGlobalFunction("ClearOverrideBindings", &Script_ClearOverrideBindings);
|
||||
Game::Lua::RegisterGlobalFunction("GetBindingAction", &Script_GetBindingAction);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
const Game::HookAutoRegister _hookKeyDispatch{
|
||||
Offsets::FUN_BINDING_KEY_DISPATCH, reinterpret_cast<void *>(&KeyDispatch_h),
|
||||
reinterpret_cast<void **>(&KeyDispatch_o)};
|
||||
// Owners are frames of the Lua state being torn down; their overrides go with it.
|
||||
void PrepareForReload() { g_overrides.clear(); }
|
||||
const Game::ReloadAutoRegister _reload{&PrepareForReload};
|
||||
|
||||
const Game::HookAutoRegister _hookKeyDispatch{Offsets::FUN_BINDING_KEY_DISPATCH,
|
||||
reinterpret_cast<void *>(&KeyDispatch_h),
|
||||
reinterpret_cast<void **>(&g_keyDispatchOriginal)};
|
||||
|
||||
const Game::HookAutoRegister _hookCommandExecute{
|
||||
Offsets::FUN_BINDING_COMMAND_EXECUTE,
|
||||
reinterpret_cast<void *>(&CommandExecute_h),
|
||||
reinterpret_cast<void **>(&CommandExecute_o)};
|
||||
Offsets::FUN_BINDING_COMMAND_EXECUTE, reinterpret_cast<void *>(&CommandExecute_h),
|
||||
reinterpret_cast<void **>(&g_commandExecuteOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -15,4 +15,24 @@
|
||||
<Binding name="AOELOOT">
|
||||
C_Loot.LootAllCorpses()
|
||||
</Binding>
|
||||
<!-- 4.3.4's modified-click actions and defaults (FrameXML\Bindings.xml).
|
||||
Read by src/bindings/ModifiedClick.cpp. -->
|
||||
<ModifiedClick action="SELFCAST" default="ALT"/>
|
||||
<ModifiedClick action="FOCUSCAST" default="NONE"/>
|
||||
<ModifiedClick action="AUTOLOOTTOGGLE" default="SHIFT"/>
|
||||
<ModifiedClick action="MAILAUTOLOOTTOGGLE" default="SHIFT"/>
|
||||
<ModifiedClick action="STICKYCAMERA" default="CTRL"/>
|
||||
<ModifiedClick action="CHATLINK" default="SHIFT-BUTTON1"/>
|
||||
<ModifiedClick action="DRESSUP" default="CTRL-BUTTON1"/>
|
||||
<ModifiedClick action="SOCKETITEM" default="SHIFT-BUTTON2"/>
|
||||
<ModifiedClick action="SPLITSTACK" default="SHIFT"/>
|
||||
<ModifiedClick action="PICKUPACTION" default="SHIFT"/>
|
||||
<ModifiedClick action="PICKUPITEM" default="SHIFT"/>
|
||||
<ModifiedClick action="COMPAREITEMS" default="SHIFT"/>
|
||||
<ModifiedClick action="OPENALLBAGS" default="SHIFT"/>
|
||||
<ModifiedClick action="QUESTWATCHTOGGLE" default="SHIFT"/>
|
||||
<ModifiedClick action="TOKENWATCHTOGGLE" default="SHIFT"/>
|
||||
<ModifiedClick action="SHOWITEMFLYOUT" default="ALT"/>
|
||||
<ModifiedClick action="SHOWMULTICASTFLYOUT" default="ALT"/>
|
||||
<ModifiedClick action="TRADESEARCHADD" default="SHIFT"/>
|
||||
</Bindings>
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The SPELL / ITEM / MACRO / CLICK binding commands — 4.3.4's
|
||||
// CGUIBindings::ExecKey branches (decomp UIBindings.cpp), on 1.12's own
|
||||
// primitives:
|
||||
//
|
||||
// SPELL x on release: the spellbook slot named x (FUN_RESOLVE_SPELL_NAME_TO_SLOT)
|
||||
// cast through FUN_SPELL_CAST_DISPATCH at you when SELFCAST is held,
|
||||
// at your focus when FOCUSCAST is, else with no explicit target.
|
||||
// ITEM x on release: the item named x you carry (equipment, then bags)
|
||||
// used through FUN_ITEM_USE with the same target.
|
||||
// MACRO x on release: macro index x when x is all digits, else the macro
|
||||
// named x, run through FUN_MACRO_RUN_BODY.
|
||||
// CLICK f:b press and release: the frame named f gets a mouse down and a
|
||||
// mouse up for button b.
|
||||
//
|
||||
// The release timing is 4.3.4's (and vanilla action buttons'): the command
|
||||
// runs when the key comes back up.
|
||||
//
|
||||
// CLICK: 4.3.4's frames take a `btn` argument on OnLayerMouseDown / Up that
|
||||
// marks a virtual click — no hit test, and a button clicks for ANY press
|
||||
// (down) or release (up) bit it is registered for. 1.12's take only the mouse
|
||||
// event and hit-test it, so the virtual branch is built from the button's own
|
||||
// pieces, in 4.3.4's order: the frame's OnMouseDown / OnMouseUp script with
|
||||
// the button name, then the Button state machine (click / double-click
|
||||
// vmethods, SetButtonState). Only frames that are Buttons get the second part,
|
||||
// as only CSimpleButton overrides the handlers 4.3.4 runs.
|
||||
|
||||
#include "bindings/Command.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "bindings/ModifiedClick.h"
|
||||
#include "frame/ClickFrame.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace Bindings::Command {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char kSpell[] = "SPELL ";
|
||||
constexpr const char kItem[] = "ITEM ";
|
||||
constexpr const char kMacro[] = "MACRO ";
|
||||
constexpr const char kClick[] = "CLICK ";
|
||||
|
||||
bool HasPrefix(const char *command, const char *prefix) {
|
||||
return _strnicmp(command, prefix, std::strlen(prefix)) == 0;
|
||||
}
|
||||
|
||||
// The target 4.3.4 picks for SPELL / ITEM: you for SELFCAST, your focus for
|
||||
// FOCUSCAST, else none (the engine then uses your target).
|
||||
uint64_t ModifiedTarget() {
|
||||
if (ModifiedClick::IsModifiedClick("SELFCAST"))
|
||||
return Unit::Identity::PlayerGuid();
|
||||
if (ModifiedClick::IsModifiedClick("FOCUSCAST"))
|
||||
return Unit::Focus::Get();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CastSpell(const char *name) {
|
||||
using NameToSlot_t = int(__fastcall *)(const char *name, int *outBookType);
|
||||
using CastDispatch_t = void(__fastcall *)(unsigned slot, int bookType, unsigned guidLo,
|
||||
unsigned guidHi);
|
||||
int book = 0;
|
||||
const int slot =
|
||||
reinterpret_cast<NameToSlot_t>(Offsets::FUN_RESOLVE_SPELL_NAME_TO_SLOT)(name, &book);
|
||||
if (slot < 0)
|
||||
return;
|
||||
const uint64_t target = ModifiedTarget();
|
||||
reinterpret_cast<CastDispatch_t>(Offsets::FUN_SPELL_CAST_DISPATCH)(
|
||||
static_cast<unsigned>(slot), book, static_cast<unsigned>(target),
|
||||
static_cast<unsigned>(target >> 32));
|
||||
}
|
||||
|
||||
void UseItem(const char *name) {
|
||||
using UseItem_t = unsigned(__thiscall *)(const void *item, const uint64_t *targetGuid,
|
||||
int flag);
|
||||
const Item::Arg::Resolved byName{0, 0, name};
|
||||
Item::Location::ByGUIDResult found{};
|
||||
if (!Item::Location::FindByArgNoLua(byName, &found) || found.item == nullptr)
|
||||
return;
|
||||
const uint64_t target = ModifiedTarget();
|
||||
reinterpret_cast<UseItem_t>(Offsets::FUN_ITEM_USE)(found.item, &target, 0);
|
||||
}
|
||||
|
||||
void RunMacro(const char *name) {
|
||||
using SlotToEntry_t = const uint8_t *(__fastcall *)(unsigned slot);
|
||||
using NameToSlot_t = uint32_t(__fastcall *)(const char *name);
|
||||
using RunBody_t = void(__fastcall *)(const uint8_t *entry);
|
||||
const char *digit = name;
|
||||
while (*digit >= '0' && *digit <= '9')
|
||||
++digit;
|
||||
const uint32_t slot =
|
||||
(*name != '\0' && *digit == '\0')
|
||||
? static_cast<uint32_t>(std::atoi(name) - 1)
|
||||
: reinterpret_cast<NameToSlot_t>(Offsets::FUN_MACRO_NAME_TO_SLOT)(name);
|
||||
const uint8_t *entry =
|
||||
reinterpret_cast<SlotToEntry_t>(Offsets::FUN_MACRO_SLOT_TO_ENTRY)(slot);
|
||||
if (entry != nullptr)
|
||||
reinterpret_cast<RunBody_t>(Offsets::FUN_MACRO_RUN_BODY)(entry);
|
||||
}
|
||||
|
||||
// StringToMouseButton: the click mask for a button name, 0 for any other
|
||||
// (the click vmethod then names it "UNKNOWN").
|
||||
uint32_t MaskForButton(const char *button) {
|
||||
static const struct {
|
||||
uintptr_t name;
|
||||
uint32_t mask;
|
||||
} kButtons[] = {
|
||||
{Offsets::VAR_BUTTON_NAME_LEFT, 0x01}, {Offsets::VAR_BUTTON_NAME_RIGHT, 0x04},
|
||||
{Offsets::VAR_BUTTON_NAME_MIDDLE, 0x02}, {Offsets::VAR_BUTTON_NAME_4, 0x08},
|
||||
{Offsets::VAR_BUTTON_NAME_5, 0x10},
|
||||
};
|
||||
for (const auto &entry : kButtons)
|
||||
if (_stricmp(reinterpret_cast<const char *>(entry.name), button) == 0)
|
||||
return entry.mask;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The frame's OnMouseDown / OnMouseUp, fired as the engine fires them.
|
||||
void FireMouseScript(void *frame, uintptr_t slotOffset, const char *button) {
|
||||
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
|
||||
uint32_t *slot = Game::Ptr<uint32_t>(frame, slotOffset);
|
||||
if (slot[0] == 0)
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
const int top = L != nullptr ? Game::Lua::GetTop(L) : 0;
|
||||
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
|
||||
frame, slot, reinterpret_cast<const char *>(Offsets::VAR_SCRIPT_FMT_S), button);
|
||||
if (L != nullptr)
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
using ButtonMethod_t = void(__thiscall *)(void *button, uint32_t arg, int flag);
|
||||
|
||||
void CallButton(void *button, uintptr_t slot, uint32_t arg) {
|
||||
auto *vtable = Game::Read<uintptr_t *>(button, 0);
|
||||
reinterpret_cast<ButtonMethod_t>(vtable[slot / sizeof(uintptr_t)])(button, arg, 0);
|
||||
}
|
||||
|
||||
int &ButtonState(void *button) { return Game::Ref<int>(button, Offsets::OFF_BUTTON_STATE); }
|
||||
bool StateLocked(void *button) {
|
||||
return Game::Read<int>(button, Offsets::OFF_BUTTON_STATE_LOCKED) != 0;
|
||||
}
|
||||
|
||||
// The button's click under the engine's "in click" flag.
|
||||
template <typename Fn> void InClick(Fn fn) {
|
||||
auto &flag = Game::Ref<int>(Offsets::VAR_BUTTON_IN_CLICK);
|
||||
flag = 1;
|
||||
fn();
|
||||
flag = 0;
|
||||
}
|
||||
|
||||
// CSimpleButton::OnLayerMouseDown with a `btn`.
|
||||
void VirtualMouseDown(void *frame, const char *button) {
|
||||
FireMouseScript(frame, Offsets::OFF_FRAME_ONMOUSEDOWN_SLOT, button);
|
||||
if (!Game::Lua::IsA(frame, Offsets::VAR_BUTTON_LUA_TYPE_ID) ||
|
||||
ButtonState(frame) == Offsets::BUTTON_STATE_DISABLED)
|
||||
return;
|
||||
if (Game::Read<uint32_t>(frame, Offsets::OFF_BUTTON_CLICK_MASK) & 0xFF)
|
||||
InClick([&] { CallButton(frame, Offsets::VTBL_BUTTON_CLICK, MaskForButton(button)); });
|
||||
if (!StateLocked(frame) && ButtonState(frame) != Offsets::BUTTON_STATE_DISABLED)
|
||||
CallButton(frame, Offsets::VTBL_BUTTON_SET_STATE, Offsets::BUTTON_STATE_PUSHED);
|
||||
}
|
||||
|
||||
// CSimpleButton::OnLayerMouseUp with a `btn`.
|
||||
void VirtualMouseUp(void *frame, const char *button) {
|
||||
FireMouseScript(frame, Offsets::OFF_FRAME_ONMOUSEUP_SLOT, button);
|
||||
if (!Game::Lua::IsA(frame, Offsets::VAR_BUTTON_LUA_TYPE_ID) ||
|
||||
ButtonState(frame) != Offsets::BUTTON_STATE_PUSHED)
|
||||
return;
|
||||
if (Game::Read<uint32_t>(frame, Offsets::OFF_BUTTON_CLICK_MASK) & 0xFF00) {
|
||||
InClick([&] {
|
||||
const uint32_t now = Time::Clock::NowMs();
|
||||
auto &last = Game::Ref<uint32_t>(frame, Offsets::OFF_BUTTON_LAST_CLICK_MS);
|
||||
const uint32_t mask = MaskForButton(button);
|
||||
if (Game::Read<uint32_t>(frame, Offsets::OFF_BUTTON_ONDOUBLECLICK_HANDLER) != 0 &&
|
||||
last != 0 && Time::Clock::Elapsed(last, now) <= Offsets::BUTTON_DOUBLE_CLICK_MS) {
|
||||
CallButton(frame, Offsets::VTBL_BUTTON_DOUBLE_CLICK, mask);
|
||||
last = 0;
|
||||
} else {
|
||||
CallButton(frame, Offsets::VTBL_BUTTON_CLICK, mask);
|
||||
last = now;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!StateLocked(frame) && ButtonState(frame) != Offsets::BUTTON_STATE_DISABLED)
|
||||
CallButton(frame, Offsets::VTBL_BUTTON_SET_STATE, Offsets::BUTTON_STATE_NORMAL);
|
||||
}
|
||||
|
||||
int Click(const char *target, int isDown) {
|
||||
const char *separator = std::strchr(target, ':');
|
||||
if (separator == nullptr)
|
||||
return 0;
|
||||
void *frame = Frame::ClickFrame::Find(Game::Lua::State(), target,
|
||||
static_cast<size_t>(separator - target));
|
||||
if (frame == nullptr || frame == Game::Read<void *>(Offsets::VAR_GAME_STATE_PTR))
|
||||
return 0; // no such frame, or the world frame
|
||||
const char *button = separator + 1;
|
||||
if (isDown)
|
||||
VirtualMouseDown(frame, button);
|
||||
else
|
||||
VirtualMouseUp(frame, button);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsDirectAction(const char *command) {
|
||||
return command != nullptr && (HasPrefix(command, kSpell) || HasPrefix(command, kItem) ||
|
||||
HasPrefix(command, kMacro) || HasPrefix(command, kClick));
|
||||
}
|
||||
|
||||
int RunKey(const char *command, int isDown) {
|
||||
if (HasPrefix(command, kSpell)) {
|
||||
if (!isDown)
|
||||
CastSpell(command + sizeof kSpell - 1);
|
||||
return 1;
|
||||
}
|
||||
if (HasPrefix(command, kItem)) {
|
||||
if (!isDown)
|
||||
UseItem(command + sizeof kItem - 1);
|
||||
return 1;
|
||||
}
|
||||
if (HasPrefix(command, kMacro)) {
|
||||
if (!isDown)
|
||||
RunMacro(command + sizeof kMacro - 1);
|
||||
return 1;
|
||||
}
|
||||
return Click(command + sizeof kClick - 1, isDown);
|
||||
}
|
||||
|
||||
} // namespace Bindings::Command
|
||||
@@ -0,0 +1,29 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Bindings::Command {
|
||||
|
||||
// True for the direct-action command families a key binding can hold:
|
||||
// "SPELL x", "ITEM x", "MACRO x", "CLICK frame:button" (case-insensitive
|
||||
// prefix, as 4.3.4 tests them).
|
||||
bool IsDirectAction(const char *command);
|
||||
|
||||
// Runs a direct-action command for a key press or release, as 4.3.4's
|
||||
// CGUIBindings::ExecKey does. Returns the engine's handled flag: 1 for SPELL,
|
||||
// ITEM and MACRO, 1 for CLICK when the frame exists, else 0. Only call it for
|
||||
// IsDirectAction commands.
|
||||
int RunKey(const char *command, int isDown);
|
||||
|
||||
} // namespace Bindings::Command
|
||||
@@ -0,0 +1,456 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Modified clicks — 4.3.4's MODIFIEDCLICK system (decomp Ui/MODIFIEDCLICK.cpp,
|
||||
// UIBindings.cpp, UIBindings_Scripts.cpp), which 1.12's binding manager lacks.
|
||||
//
|
||||
// An action ("SELFCAST", "CHATLINK", …) is bound to a modifier set and/or a
|
||||
// mouse button ("ALT", "SHIFT-BUTTON1", "NONE"), once per binding set (0
|
||||
// default, 1 account, 2 character, 3 current), exactly like key bindings. 1.12
|
||||
// has 4.3.4's binding sets, file format and load / save / copy steps, so each
|
||||
// gains the modified-click half 4.3.4 has, through a co-hook on the step:
|
||||
//
|
||||
// - Load (Bindings.xml): `<ModifiedClick action="X" default="Y"/>` defines
|
||||
// an action and its default. The engine loader warns on the node it does
|
||||
// not know; we register the actions from the same file first.
|
||||
// - LoadBindings (a binding file into a set): the set starts from the
|
||||
// defaults, then `modifiedclick BINDING ACTION` lines apply.
|
||||
// - SaveBindings: `modifiedclick` lines for every action not left at its
|
||||
// default.
|
||||
// - CopyBindings: the set's bindings are copied with the key bindings.
|
||||
//
|
||||
// The actions live as long as the binding manager: the engine builds it on
|
||||
// every world load and /reload, so the table is cleared with the Lua state.
|
||||
// Key state comes from Input::Modifier (the left/right bits 4.3.4's
|
||||
// CGGameUI::IsKeyDown reads) and the clicked button from Frame::ClickEvents
|
||||
// (4.3.4's CSimpleTop::m_mouseButtonName).
|
||||
|
||||
#include "bindings/ModifiedClick.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "frame/ClickEvents.h"
|
||||
#include "input/Modifier.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Bindings::ModifiedClick {
|
||||
|
||||
namespace {
|
||||
|
||||
// 4.3.4's modifier bits, = Input::Modifier's left/right key bits (and what
|
||||
// Input::Modifier::ParseModifiers returns).
|
||||
constexpr uint32_t kLShift = 0x01, kRShift = 0x02, kLCtrl = 0x04, kRCtrl = 0x08,
|
||||
kLAlt = 0x10, kRAlt = 0x20;
|
||||
constexpr int kKeyCount = 6;
|
||||
|
||||
// MODIFIEDCLICK_DATA. Flag 1 = the set holds the default (never saved).
|
||||
constexpr uint32_t kFlagDefault = 0x1;
|
||||
struct Data {
|
||||
uint32_t flags = 0;
|
||||
uint32_t modifiers = 0;
|
||||
const char *button = nullptr;
|
||||
};
|
||||
|
||||
struct Click {
|
||||
std::string action;
|
||||
Data data[Offsets::BINDING_SET_COUNT];
|
||||
};
|
||||
|
||||
std::vector<Click> g_clicks;
|
||||
|
||||
Click *Find(const char *action) {
|
||||
if (action == nullptr)
|
||||
return nullptr;
|
||||
for (Click &click : g_clicks)
|
||||
if (_stricmp(click.action.c_str(), action) == 0)
|
||||
return &click;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool KeyDown(int key) { return (Input::Modifier::CurrentMask() & (1u << key)) != 0; }
|
||||
|
||||
bool PrefixIs(const char *s, const char *prefix) {
|
||||
return _strnicmp(s, prefix, std::strlen(prefix)) == 0;
|
||||
}
|
||||
|
||||
// CGUIBindings::AddMetaPrefix, appended to `out`.
|
||||
void AddMetaPrefix(uint32_t modifiers, std::string &out) {
|
||||
const auto side = [&](uint32_t left, uint32_t right, const char *both, const char *l,
|
||||
const char *r) {
|
||||
if ((modifiers & (left | right)) == (left | right))
|
||||
out += both;
|
||||
else if (modifiers & left)
|
||||
out += l;
|
||||
else if (modifiers & right)
|
||||
out += r;
|
||||
};
|
||||
side(kLAlt, kRAlt, "ALT-", "LALT-", "RALT-");
|
||||
side(kLCtrl, kRCtrl, "CTRL-", "LCTRL-", "RCTRL-");
|
||||
side(kLShift, kRShift, "SHIFT-", "LSHIFT-", "RSHIFT-");
|
||||
}
|
||||
|
||||
// BUTTONn <-> mouse-button name (IndexToMouseButton / MouseButtonToIndex).
|
||||
// 1..5 are the engine's own name literals, the strings GetMouseButtonClicked
|
||||
// and the click scope hold.
|
||||
const char *const kExtraButtons[] = {"Button6", "Button7", "Button8", "Button9"};
|
||||
|
||||
const char *ButtonForIndex(int index) {
|
||||
switch (index) {
|
||||
case 1: return reinterpret_cast<const char *>(Offsets::VAR_BUTTON_NAME_LEFT);
|
||||
case 2: return reinterpret_cast<const char *>(Offsets::VAR_BUTTON_NAME_RIGHT);
|
||||
case 3: return reinterpret_cast<const char *>(Offsets::VAR_BUTTON_NAME_MIDDLE);
|
||||
case 4: return reinterpret_cast<const char *>(Offsets::VAR_BUTTON_NAME_4);
|
||||
case 5: return reinterpret_cast<const char *>(Offsets::VAR_BUTTON_NAME_5);
|
||||
case 6: case 7: case 8: case 9: return kExtraButtons[index - 6];
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int IndexForButton(const char *button) {
|
||||
for (int index = 1; index <= 9; ++index)
|
||||
if (_stricmp(ButtonForIndex(index), button) == 0)
|
||||
return index;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// MODIFIEDCLICK::SetBinding.
|
||||
bool SetBinding(Click &click, int set, const char *binding) {
|
||||
const char *button = nullptr;
|
||||
uint32_t modifiers = 0;
|
||||
if (binding != nullptr && *binding != '\0' && _stricmp(binding, "NONE") != 0) {
|
||||
modifiers = Input::Modifier::ParseModifiers(binding);
|
||||
if (PrefixIs(binding, "BUTTON")) {
|
||||
binding += 6;
|
||||
button = ButtonForIndex(std::atoi(binding));
|
||||
++binding;
|
||||
}
|
||||
if (*binding != '\0')
|
||||
return false;
|
||||
}
|
||||
Data &data = click.data[set];
|
||||
if (set != Offsets::BINDING_SET_DEFAULT)
|
||||
data.flags &= ~kFlagDefault;
|
||||
else
|
||||
data.flags |= kFlagDefault;
|
||||
data.button = button;
|
||||
data.modifiers = modifiers;
|
||||
return true;
|
||||
}
|
||||
|
||||
// MODIFIEDCLICK::GetBinding.
|
||||
std::string GetBinding(const Click &click, int set) {
|
||||
const Data &data = click.data[set];
|
||||
if (data.modifiers == 0 && data.button == nullptr)
|
||||
return "NONE";
|
||||
std::string out;
|
||||
AddMetaPrefix(data.modifiers, out);
|
||||
if (data.button != nullptr)
|
||||
out += "BUTTON" + std::to_string(IndexForButton(data.button));
|
||||
else
|
||||
out.pop_back(); // the prefix's trailing '-'
|
||||
return out;
|
||||
}
|
||||
|
||||
// MODIFIEDCLICK::IsActive.
|
||||
bool IsActive(const Click &click, int set) {
|
||||
const Data &data = click.data[set];
|
||||
if (data.modifiers != 0) {
|
||||
int key = 0;
|
||||
while (!((1u << key) & data.modifiers) || !KeyDown(key)) {
|
||||
if (++key >= kKeyCount)
|
||||
return false;
|
||||
}
|
||||
if (data.button == nullptr)
|
||||
return true;
|
||||
} else if (data.button == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const char *current = Frame::ClickEvents::CurrentButton();
|
||||
return current == nullptr || std::strcmp(current, data.button) == 0;
|
||||
}
|
||||
|
||||
// One side of a modifier test: left only, right only, or either.
|
||||
bool SideHeld(uint32_t modifiers, uint32_t left, uint32_t right, int leftKey, int rightKey) {
|
||||
const uint32_t bits = modifiers & (left | right);
|
||||
if (bits == left)
|
||||
return KeyDown(leftKey);
|
||||
if (bits == right || (bits == (left | right) && !KeyDown(leftKey)))
|
||||
return KeyDown(rightKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Bindings.xml: <ModifiedClick action default> ---------------------------
|
||||
|
||||
using LoadFile_t = int(__fastcall *)(void *mgr, void *edx, const char *path, void *md5,
|
||||
void *status);
|
||||
LoadFile_t g_loadFileOriginal = nullptr;
|
||||
|
||||
using TreeLoad_t = void *(__fastcall *)(const char *data, uint32_t size);
|
||||
using TreeFree_t = void(__fastcall *)(void *tree);
|
||||
|
||||
// CGUIBindings::LoadModifiedClick for every root node of the file.
|
||||
void LoadModifiedClicks(const char *path, void *status) {
|
||||
void *data = nullptr;
|
||||
size_t size = 0;
|
||||
if (!reinterpret_cast<AddOns::EngineIO::FileReadFn>(Offsets::FUN_FILE_READ)(0, path, &data,
|
||||
&size, 0, 1, 0))
|
||||
return;
|
||||
void *tree = reinterpret_cast<TreeLoad_t>(Offsets::FUN_XML_TREE_LOAD)(
|
||||
static_cast<const char *>(data), static_cast<uint32_t>(size));
|
||||
reinterpret_cast<AddOns::EngineIO::SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE)(data, __FILE__,
|
||||
__LINE__, 0);
|
||||
if (tree == nullptr)
|
||||
return; // the engine loader reports the parse error
|
||||
const void *root = Game::Read<const void *>(tree, 0);
|
||||
for (const void *node = root ? Xml::FirstChild(root) : nullptr; node != nullptr;
|
||||
node = Xml::NextSibling(node)) {
|
||||
if (!Xml::TagIs(node, "ModifiedClick"))
|
||||
continue;
|
||||
const char *action = Xml::Attr(node, "action");
|
||||
if (action == nullptr || *action == '\0') {
|
||||
Xml::Warn(status, "Found modified click with no action in %s", path);
|
||||
continue;
|
||||
}
|
||||
if (Find(action) != nullptr) {
|
||||
Xml::Warn(status, "Modified click %s is defined more than once in %s", action, path);
|
||||
continue;
|
||||
}
|
||||
g_clicks.push_back(Click{action, {}});
|
||||
const char *binding = Xml::Attr(node, "default");
|
||||
if (binding != nullptr && *binding != '\0')
|
||||
SetBinding(g_clicks.back(), Offsets::BINDING_SET_DEFAULT, binding);
|
||||
}
|
||||
reinterpret_cast<TreeFree_t>(Offsets::FUN_XML_TREE_FREE)(tree);
|
||||
}
|
||||
|
||||
int __fastcall LoadFile_h(void *mgr, void *edx, const char *path, void *md5, void *status) {
|
||||
if (path != nullptr)
|
||||
LoadModifiedClicks(path, status != nullptr
|
||||
? status
|
||||
: reinterpret_cast<void *>(Offsets::VAR_BINDINGS_LOAD_STATUS));
|
||||
return g_loadFileOriginal(mgr, edx, path, md5, status);
|
||||
}
|
||||
|
||||
// --- binding files: modifiedclick lines -----------------------------------
|
||||
|
||||
using LoadSet_t = void(__fastcall *)(int set, const char *text);
|
||||
LoadSet_t g_loadSetOriginal = nullptr;
|
||||
|
||||
using Tokenize_t = void(__stdcall *)(const char **cursor, char *out, unsigned outSize,
|
||||
const char *delims, int *outQuoted);
|
||||
|
||||
// The modified-click half of 4.3.4's LoadBindings: start from the defaults,
|
||||
// then apply `modifiedclick BINDING ACTION` lines.
|
||||
void __fastcall LoadSet_h(int set, const char *text) {
|
||||
g_loadSetOriginal(set, text);
|
||||
if (set < 0 || set >= Offsets::BINDING_SET_COUNT)
|
||||
return;
|
||||
if (set != Offsets::BINDING_SET_DEFAULT)
|
||||
for (Click &click : g_clicks)
|
||||
click.data[set] = click.data[Offsets::BINDING_SET_DEFAULT];
|
||||
|
||||
auto tokenize = reinterpret_cast<Tokenize_t>(Offsets::FUN_STORM_STR_TOKENIZE);
|
||||
const char *lineDelims = reinterpret_cast<const char *>(Offsets::VAR_MACRO_LINE_DELIMS);
|
||||
const char *spaceDelims = reinterpret_cast<const char *>(Offsets::VAR_STR_SPACE_DELIM);
|
||||
char line[1024];
|
||||
char key[32];
|
||||
const char *cursor = text;
|
||||
while (cursor != nullptr && *cursor != '\0') {
|
||||
tokenize(&cursor, line, sizeof line, lineDelims, nullptr);
|
||||
const char *p = line;
|
||||
while (*p == ' ' || *p == '\t')
|
||||
++p;
|
||||
if (!PrefixIs(p, "modifiedclick "))
|
||||
continue;
|
||||
const char *action = p + 14;
|
||||
tokenize(&action, key, sizeof key, spaceDelims, nullptr);
|
||||
if (action != nullptr)
|
||||
if (Click *click = Find(action))
|
||||
SetBinding(*click, set, key);
|
||||
}
|
||||
}
|
||||
|
||||
using SaveSet_t = void(__fastcall *)(void *mgr, void *edx, int set, void *file);
|
||||
SaveSet_t g_saveSetOriginal = nullptr;
|
||||
|
||||
using WriteFile_t = int(__fastcall *)(void *file, const void *buffer, uint32_t size,
|
||||
uint32_t *written);
|
||||
|
||||
void __fastcall SaveSet_h(void *mgr, void *edx, int set, void *file) {
|
||||
g_saveSetOriginal(mgr, edx, set, file);
|
||||
if (set < 0 || set >= Offsets::BINDING_SET_COUNT)
|
||||
return;
|
||||
auto write = reinterpret_cast<WriteFile_t>(Offsets::FUN_OS_WRITE_FILE);
|
||||
for (const Click &click : g_clicks) {
|
||||
if (click.data[set].flags & kFlagDefault)
|
||||
continue;
|
||||
const std::string line =
|
||||
"modifiedclick " + GetBinding(click, set) + " " + click.action + "\r\n";
|
||||
uint32_t written = 0;
|
||||
write(file, line.data(), static_cast<uint32_t>(line.size()), &written);
|
||||
}
|
||||
}
|
||||
|
||||
using CopySet_t = void(__fastcall *)(void *mgr, void *edx, int source, int dest);
|
||||
CopySet_t g_copySetOriginal = nullptr;
|
||||
|
||||
void __fastcall CopySet_h(void *mgr, void *edx, int source, int dest) {
|
||||
g_copySetOriginal(mgr, edx, source, dest);
|
||||
if (source < 0 || source >= Offsets::BINDING_SET_COUNT || dest < 0 ||
|
||||
dest >= Offsets::BINDING_SET_COUNT)
|
||||
return;
|
||||
for (Click &click : g_clicks)
|
||||
click.data[dest] = click.data[source];
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _loadFileHook{Offsets::FUN_BINDINGS_LOAD_FILE,
|
||||
reinterpret_cast<void *>(&LoadFile_h),
|
||||
reinterpret_cast<void **>(&g_loadFileOriginal)};
|
||||
const Game::HookAutoRegister _loadSetHook{Offsets::FUN_BINDINGS_LOAD_SET,
|
||||
reinterpret_cast<void *>(&LoadSet_h),
|
||||
reinterpret_cast<void **>(&g_loadSetOriginal)};
|
||||
const Game::HookAutoRegister _saveSetHook{Offsets::FUN_BINDINGS_SAVE_SET,
|
||||
reinterpret_cast<void *>(&SaveSet_h),
|
||||
reinterpret_cast<void **>(&g_saveSetOriginal)};
|
||||
const Game::HookAutoRegister _copySetHook{Offsets::FUN_BINDINGS_COPY_SET,
|
||||
reinterpret_cast<void *>(&CopySet_h),
|
||||
reinterpret_cast<void **>(&g_copySetOriginal)};
|
||||
|
||||
// The binding manager is rebuilt with every Lua state; so are its actions.
|
||||
void PrepareForReload() { g_clicks.clear(); }
|
||||
const Game::ReloadAutoRegister _reload{&PrepareForReload};
|
||||
|
||||
// --- Lua --------------------------------------------------------------------
|
||||
|
||||
int __fastcall Script_GetNumModifiedClickActions(void *L) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_clicks.size()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetModifiedClickAction(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetModifiedClickAction(index)");
|
||||
return 0;
|
||||
}
|
||||
const int index = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
Game::Lua::PushString(L, index >= 0 && index < static_cast<int>(g_clicks.size())
|
||||
? g_clicks[index].action.c_str()
|
||||
: nullptr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetModifiedClick(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: SetModifiedClick(\"action\", \"binding\")");
|
||||
return 0;
|
||||
}
|
||||
const char *action = Game::Lua::ToString(L, 1);
|
||||
const char *binding = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
Click *click = Find(action);
|
||||
if (click == nullptr || !SetBinding(*click, Offsets::BINDING_SET_CURRENT, binding)) {
|
||||
char message[256];
|
||||
std::snprintf(message, sizeof message,
|
||||
"SetModifiedClick(): Unknown action (%s) or binding (%s)", action,
|
||||
binding != nullptr ? binding : "(null)");
|
||||
Game::Lua::Error(L, "%s", message);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetModifiedClick(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetModifiedClick(\"action\")");
|
||||
return 0;
|
||||
}
|
||||
const Click *click = Find(Game::Lua::ToString(L, 1));
|
||||
if (click == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushString(L, GetBinding(*click, Offsets::BINDING_SET_CURRENT).c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsModifiedClick(void *L) {
|
||||
const char *action = Game::Lua::IsString(L, 1) ? Game::Lua::ToString(L, 1) : nullptr;
|
||||
if (IsModifiedClick(action))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kIndexArgs[] = {Game::Doc::Req("index", "luaIndex")};
|
||||
const Game::Doc::Field kActionArgs[] = {Game::Doc::Req("action", "string")};
|
||||
const Game::Doc::Field kSetArgs[] = {Game::Doc::Req("action", "string"),
|
||||
Game::Doc::Opt("binding", "string")};
|
||||
const Game::Doc::Field kIsArgs[] = {Game::Doc::Opt("action", "string")};
|
||||
const Game::Doc::Field kCountRets[] = {Game::Doc::Req("count", "number")};
|
||||
const Game::Doc::Field kActionRets[] = {Game::Doc::Opt("action", "string")};
|
||||
const Game::Doc::Field kBindingRets[] = {Game::Doc::Opt("binding", "string")};
|
||||
const Game::Doc::Field kIsRets[] = {Game::Doc::Opt("isModified", "number")};
|
||||
|
||||
const Game::Doc::Function kGetNum{"Returns the number of modified-click actions.", {},
|
||||
kCountRets, "BindingsGlobals"};
|
||||
const Game::Doc::Function kGetAction{"Returns the name of the modified-click action at an index.",
|
||||
kIndexArgs, kActionRets, "BindingsGlobals"};
|
||||
const Game::Doc::Function kSet{"Sets the modifier keys and mouse button for an action.",
|
||||
kSetArgs, {}, "BindingsGlobals"};
|
||||
const Game::Doc::Function kGet{"Returns the modifier keys and mouse button set for an action.",
|
||||
kActionArgs, kBindingRets, "BindingsGlobals"};
|
||||
const Game::Doc::Function kIs{"Returns 1 when the keys and mouse button for an action are held.",
|
||||
kIsArgs, kIsRets, "BindingsGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetNumModifiedClickActions",
|
||||
&Script_GetNumModifiedClickActions, &kGetNum);
|
||||
Game::Lua::RegisterGlobalFunction("GetModifiedClickAction", &Script_GetModifiedClickAction,
|
||||
&kGetAction);
|
||||
Game::Lua::RegisterGlobalFunction("SetModifiedClick", &Script_SetModifiedClick, &kSet);
|
||||
Game::Lua::RegisterGlobalFunction("GetModifiedClick", &Script_GetModifiedClick, &kGet);
|
||||
Game::Lua::RegisterGlobalFunction("IsModifiedClick", &Script_IsModifiedClick, &kIs);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsModifiedClick(const char *action) {
|
||||
if (action != nullptr) {
|
||||
const char *rest = action;
|
||||
const uint32_t modifiers = Input::Modifier::ParseModifiers(rest);
|
||||
if (modifiers == 0) {
|
||||
const Click *click = Find(action);
|
||||
return click != nullptr && IsActive(*click, Offsets::BINDING_SET_CURRENT);
|
||||
}
|
||||
return SideHeld(modifiers, kLShift, kRShift, 0, 1) &&
|
||||
SideHeld(modifiers, kLCtrl, kRCtrl, 2, 3) &&
|
||||
SideHeld(modifiers, kLAlt, kRAlt, 4, 5);
|
||||
}
|
||||
if ((Input::Modifier::CurrentMask() & 0x3F) != 0)
|
||||
return true;
|
||||
for (const Click &click : g_clicks)
|
||||
if (IsActive(click, Offsets::BINDING_SET_CURRENT))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Bindings::ModifiedClick
|
||||
@@ -0,0 +1,24 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Bindings::ModifiedClick {
|
||||
|
||||
// 4.3.4's CGUIBindings::IsModifiedClick. `action` null: any modifier key is
|
||||
// held or any modified-click action is active. A modifier string ("SHIFT",
|
||||
// "LCTRL-ALT"): those keys are held. Otherwise: the named action's binding in
|
||||
// the current set is active.
|
||||
bool IsModifiedClick(const char *action);
|
||||
|
||||
} // namespace Bindings::ModifiedClick
|
||||
@@ -0,0 +1,153 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The `%` codes in sent chat, brought to 4.3.4's set.
|
||||
//
|
||||
// SendChatMessage runs its text through FUN_CHAT_PARSE_TEXT (4.3.4's
|
||||
// ChatParserParseText) before the drunk filter. 1.12's knows `%t` / `%n`
|
||||
// only: the target's name — the unit object's name, else its name-cache
|
||||
// name, else TARGET_TOKEN_NOT_FOUND. 4.3.4's (QuestTextParser.cpp) has two
|
||||
// cases, both resolved the same way and both prefixed `{rtN}` when the unit
|
||||
// carries a raid marker and a name was found:
|
||||
//
|
||||
// - `%t` / `%T` / `%n` / `%N` — the target (1.12 lacks only the prefix);
|
||||
// - `%f` / `%F` — the focus, or FOCUS_TOKEN_NOT_FOUND (1.12 lacks the code).
|
||||
//
|
||||
// The co-hook rewrites a copy of the text and hands it to the original: `%f`
|
||||
// becomes the focus text, and a `%t` / `%n` pair gets the target's `{rtN}` in
|
||||
// front of it, so the engine still produces the target's name. The copy walks
|
||||
// `%` codes the way the original does — a code pair stays together, any other
|
||||
// `%` consumes only itself — so text after a `%` is never read differently.
|
||||
// The `{rtN}` prefix renders through Chat::RaidMarkers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/RaidTarget.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Chat::ChatTokens {
|
||||
|
||||
namespace {
|
||||
|
||||
using Parse_t = bool(__fastcall *)(const char *text, char *out, int size);
|
||||
Parse_t g_parseOriginal = nullptr;
|
||||
|
||||
const char *GlobalText(const char *name) {
|
||||
using GetText_t = const char *(__fastcall *)(const char *name, int edx, int gender);
|
||||
return reinterpret_cast<GetText_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRAMESCRIPT_GET_TEXT))(name, -1, 0);
|
||||
}
|
||||
|
||||
// A unit's name the way the `%t` case finds the target's: the unit object's
|
||||
// name, else the name cache. Null when neither has one.
|
||||
const char *UnitName(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
if (void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0)) {
|
||||
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
|
||||
const char *name =
|
||||
reinterpret_cast<GetName_t>(static_cast<uintptr_t>(Offsets::FUN_OBJECT_GET_NAME))(
|
||||
unit, nullptr);
|
||||
if (name != nullptr && *name != '\0')
|
||||
return name;
|
||||
}
|
||||
return Unit::Identity::NameForGuid(guid);
|
||||
}
|
||||
|
||||
// `{rtN}` for a unit that carries raid marker N, else nothing.
|
||||
void AppendMarker(std::string &out, uint64_t guid) {
|
||||
for (int mark = 1; mark <= Unit::RaidTarget::kMarkCount; ++mark) {
|
||||
if (Unit::RaidTarget::GetGUIDByMark(mark) == guid) {
|
||||
char prefix[8];
|
||||
std::snprintf(prefix, sizeof prefix, "{rt%d}", mark);
|
||||
out += prefix;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsMarked(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return false;
|
||||
for (int mark = 1; mark <= Unit::RaidTarget::kMarkCount; ++mark)
|
||||
if (Unit::RaidTarget::GetGUIDByMark(mark) == guid)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsFocusCode(char c) { return c == 'f' || c == 'F'; }
|
||||
bool IsTargetCode(char c) { return c == 't' || c == 'T' || c == 'n' || c == 'N'; }
|
||||
|
||||
uint64_t TargetGuid() {
|
||||
return Game::Read<uint64_t>(Offsets::VAR_CURRENT_SELECTION_GUID_LO);
|
||||
}
|
||||
|
||||
bool __fastcall Parse_h(const char *text, char *out, int size) {
|
||||
if (text == nullptr)
|
||||
return g_parseOriginal(text, out, size);
|
||||
|
||||
// Leave the original's input untouched unless a code needs us: any `%f`,
|
||||
// or a `%t` / `%n` while the target is marked and named.
|
||||
const uint64_t target = TargetGuid();
|
||||
const bool targetPrefix = IsMarked(target) && UnitName(target) != nullptr;
|
||||
bool any = false;
|
||||
for (const char *p = std::strchr(text, '%'); p != nullptr; p = std::strchr(p + 1, '%'))
|
||||
if (IsFocusCode(p[1]) || (targetPrefix && IsTargetCode(p[1]))) {
|
||||
any = true;
|
||||
break;
|
||||
}
|
||||
if (!any)
|
||||
return g_parseOriginal(text, out, size);
|
||||
|
||||
std::string s;
|
||||
for (const char *p = text; *p != '\0';) {
|
||||
if (*p != '%') {
|
||||
s += *p++;
|
||||
continue;
|
||||
}
|
||||
const char c = p[1];
|
||||
if (IsFocusCode(c)) {
|
||||
const uint64_t focus = Unit::Focus::Get();
|
||||
if (const char *name = UnitName(focus)) {
|
||||
AppendMarker(s, focus);
|
||||
s += name;
|
||||
} else {
|
||||
s += GlobalText("FOCUS_TOKEN_NOT_FOUND");
|
||||
}
|
||||
p += 2;
|
||||
} else if (IsTargetCode(c)) {
|
||||
if (targetPrefix)
|
||||
AppendMarker(s, target);
|
||||
s.append(p, 2); // the original's own code — keep the pair together
|
||||
p += 2;
|
||||
} else {
|
||||
s += *p++; // the original consumes only the '%' here
|
||||
}
|
||||
}
|
||||
return g_parseOriginal(s.c_str(), out, size);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _parseHook{Offsets::FUN_CHAT_PARSE_TEXT,
|
||||
reinterpret_cast<void *>(&Parse_h),
|
||||
reinterpret_cast<void **>(&g_parseOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Chat::ChatTokens
|
||||
+30
-20
@@ -11,28 +11,32 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Inline-texture (`|T`) chat anti-spoof.
|
||||
// Inline-texture (`|T`, `|A`) chat anti-spoof.
|
||||
//
|
||||
// Vanilla neutralizes player-*typed* escapes in chat (a typed `|cff…|r`,
|
||||
// `|Hitem…|h` or `|T…|t` shows as raw text) — only *trusted* text renders them
|
||||
// (addon `AddMessage`, shift-clicked links). Our Text::InlineTexture backport
|
||||
// breaks that parity for `|T` alone: it detects the neutralized `||T` form and
|
||||
// draws the icon anyway, so a player could `/say` `|Tpath|t` and have a real
|
||||
// icon appear in everyone's chat AND in the speech bubble over their head.
|
||||
// breaks that parity for the two texture markers: it detects the neutralized
|
||||
// `||T` / `||A` form and draws the art anyway, so a player could `/say`
|
||||
// `|Tpath|t` and have a real icon appear in everyone's chat AND in the speech
|
||||
// bubble over their head.
|
||||
//
|
||||
// This restores vanilla's behavior for `|T` by defanging it in the chat path,
|
||||
// while leaving `|c`/`|r`/`|H`/`|h` exactly as vanilla (already raw for typed
|
||||
// input). It CANNOT be done in the text emitter: addon `|T` and player `|T`
|
||||
// reach the emitter as byte-identical `||T` (vanilla doubles the unrecognized
|
||||
// `|T` for both), so the only place they're distinguishable is the source —
|
||||
// server-delivered chat flows through the chat dispatcher, addon `print` does
|
||||
// not. Chat::Dispatch owns that hook and calls this on the message.
|
||||
// This restores vanilla's behavior for both markers by defanging them in the
|
||||
// chat path, while leaving `|c`/`|r`/`|H`/`|h` exactly as vanilla (already raw
|
||||
// for typed input). It CANNOT be done in the text emitter: addon `|T` and player
|
||||
// `|T` reach the emitter as byte-identical `||T` (vanilla doubles the
|
||||
// unrecognized escape for both), so the only place they're distinguishable is
|
||||
// the source — server-delivered chat flows through the chat dispatcher, addon
|
||||
// `print` does not. Chat::Dispatch owns that hook and calls this on the message.
|
||||
//
|
||||
// Defang = replace the `|` of each `|T` with a space, breaking the pipe→T
|
||||
// Defang = replace the `|` of each opener with a space, breaking the pipe→letter
|
||||
// adjacency the inline-texture detector keys on (`|T` and `||T` both contain the
|
||||
// `|T` substring, so this catches the doubled form too). The path text stays
|
||||
// readable, the closer `|t` is inert without an opener, and every other escape
|
||||
// is untouched.
|
||||
// `|T` substring, so this catches the doubled form too). The payload text stays
|
||||
// readable, the closer is inert without an opener, and every other escape is
|
||||
// untouched.
|
||||
//
|
||||
// ANY new marker the emitter learns MUST be added here in the same change, or it
|
||||
// becomes a chat-spoof vector.
|
||||
|
||||
#include "IconFilter.h"
|
||||
|
||||
@@ -40,10 +44,16 @@ namespace Chat::IconFilter {
|
||||
|
||||
namespace {
|
||||
|
||||
// True if `s` contains an inline-texture opener (`|T`) anywhere.
|
||||
// True if `s` contains an inline-texture opener anywhere. Both markers count:
|
||||
// `|T` names a texture path and `|A` names an atlas, and either one would render
|
||||
// art from player-typed text if it survived to the fontstring.
|
||||
bool IsIconOpener(const char *s) {
|
||||
return s[0] == '|' && (s[1] == 'T' || s[1] == 'A');
|
||||
}
|
||||
|
||||
bool HasIconEscape(const char *s) {
|
||||
for (; *s != '\0'; ++s)
|
||||
if (s[0] == '|' && s[1] == 'T')
|
||||
if (IsIconOpener(s))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@@ -68,12 +78,12 @@ const char *Sanitize(const char *msg, char *buf, size_t bufSize) {
|
||||
// opener survives for the inline-texture detector to fire on.
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; msg[i] != '\0' && j + 1 < bufSize;) {
|
||||
if (msg[i] == '|' && msg[i + 1] == 'T') {
|
||||
if (IsIconOpener(msg + i)) {
|
||||
const bool doubledPipe = (i > 0 && msg[i - 1] == '|');
|
||||
buf[j++] = doubledPipe ? '|' : ' '; // keep pipe only if it completes `||`
|
||||
if (j + 1 < bufSize)
|
||||
buf[j++] = ' '; // blank the `T`
|
||||
i += 2; // consumed `|T`
|
||||
buf[j++] = ' '; // blank the `T` / `A`
|
||||
i += 2; // consumed the opener
|
||||
} else {
|
||||
buf[j++] = msg[i++];
|
||||
}
|
||||
|
||||
+7
-16
@@ -29,10 +29,9 @@
|
||||
// unchanged. This is the pre-7.2.5 "walk WorldFrame children" pattern the
|
||||
// wiki describes, but handed the exact set instead of the whole world.
|
||||
//
|
||||
// The engine keeps active bubbles on an intrusive Storm `TSList` whose
|
||||
// head is `VAR_CHAT_BUBBLE_LIST_HEAD` (`0x00B6F038`); each node's forward
|
||||
// link sits at `+0x318` and the list terminator carries the low-bit
|
||||
// sentinel. We mirror the engine's own walk in the per-frame update
|
||||
// The engine keeps active bubbles on an intrusive Storm `TSList`
|
||||
// (`VAR_CHAT_BUBBLE_LIST_HEAD` / `OFF_CHAT_BUBBLE_LINK_NEXT` in Offsets.h);
|
||||
// the list terminator carries the low-bit sentinel. We mirror the engine's own walk in the per-frame update
|
||||
// `FUN_004b1060` / teardown `FUN_004b0f80` verbatim. Bubbles whose owner
|
||||
// unit has despawned are pruned on the engine's next update tick, so a
|
||||
// just-orphaned bubble can linger one frame — harmless, and addons that
|
||||
@@ -42,32 +41,24 @@
|
||||
// frames, so every bubble is returned regardless.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "ui/FrameObject.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ChatBubble::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
// Storm TSList of live CGChatBubbleFrames. Head pointer, per-node
|
||||
// forward link, and the end-of-list low-bit sentinel — see the engine
|
||||
// walks at 0x004b1060 / 0x004b0f80.
|
||||
constexpr uintptr_t kListHead = 0x00B6F038;
|
||||
constexpr int kOffNextLink = 0x318;
|
||||
|
||||
} // namespace
|
||||
|
||||
static int __fastcall Script_GetAllChatBubbles(void *L) {
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
uintptr_t node = *reinterpret_cast<const uintptr_t *>(kListHead);
|
||||
uintptr_t node = *reinterpret_cast<const uintptr_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CHAT_BUBBLE_LIST_HEAD));
|
||||
int nextIndex = 1;
|
||||
while (node != 0 && (node & 1) == 0) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextIndex++));
|
||||
UI::FrameObject::Push(L, reinterpret_cast<void *>(node));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
node = *reinterpret_cast<const uintptr_t *>(node + kOffNextLink);
|
||||
node = *reinterpret_cast<const uintptr_t *>(node + Offsets::OFF_CHAT_BUBBLE_LINK_NEXT);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// 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_ClassColor.GetClassColor(className)` — a class's color as a ColorMixin.
|
||||
//
|
||||
// Modern clients keep the class colors in the engine and let FrameXML read
|
||||
// them out through this function. Neither half of that exists here: 1.12's
|
||||
// `ChrClasses.dbc` has no color column (17 columns, all accounted for), and
|
||||
// the binary carries no class-color table — searched it for the class float
|
||||
// triples and for the hex strings, no hits.
|
||||
//
|
||||
// What this client does have is `RAID_CLASS_COLORS`, defined by FrameXML in
|
||||
// `Fonts.xml` and given the mixin by `!!!ClassicAPI/Util/Color*.lua`. Every
|
||||
// UI on the client already colors classes from it, so this reads that table
|
||||
// instead of carrying a second copy of the nine colors — the same choice
|
||||
// `C_UnitAuras.GetAuraDispelTypeColor` makes with the `DEBUFF_TYPE_*_COLOR`
|
||||
// globals. One source of truth, one shared object per class, and a class
|
||||
// another addon recolors stays recolored here (this client's stand-in for
|
||||
// CUSTOM_CLASS_COLORS).
|
||||
//
|
||||
// The lookup goes through metamethods, so a `RAID_CLASS_COLORS` that answers
|
||||
// for unknown keys answers here too — the pfUI shim in `Util/AddOnCompat.lua`
|
||||
// hands back grey. A name that is not a string, and a missing table, both
|
||||
// return nil.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace Classes::Color {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_GetClassColor(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_STRING) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, "RAID_CLASS_COLORS");
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Index with the argument itself rather than with a `const char *` read
|
||||
// off the stack: the string stays anchored at index 1, so nothing it
|
||||
// points at can be collected while we look it up.
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::GetTable(L, -2);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("className", "string", "A class file name, such as \"WARRIOR\"."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("classColor", "colorRGB", nullptr,
|
||||
"The color for that class, or nil when the name is not a class."),
|
||||
};
|
||||
const Game::Doc::Function kGetClassColor{
|
||||
"The color a class is drawn in.", kArgs, kRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_ClassColor", "GetClassColor",
|
||||
&Script_GetClassColor, &kGetClassColor);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Classes::Color
|
||||
@@ -86,13 +86,18 @@ std::string StripMarkup(const char *s, unsigned len) {
|
||||
++i;
|
||||
break; // the |h is dropped by the 'h' case on the next iteration
|
||||
case 't': // stray texture closer — drop
|
||||
case 'a': // stray atlas closer — drop
|
||||
i += 2;
|
||||
break;
|
||||
case 'T': // texture opener — skip through to the next |t
|
||||
case 'A': // atlas opener — skip through to the next |a
|
||||
{
|
||||
const char closer = (s[i + 1] == 'A') ? 'a' : 't';
|
||||
i += 2;
|
||||
while (i < len && !(s[i] == '|' && i + 1 < len && s[i + 1] == 't'))
|
||||
while (i < len && !(s[i] == '|' && i + 1 < len && s[i + 1] == closer))
|
||||
++i;
|
||||
break; // the |t is dropped by the 't' case on the next iteration
|
||||
break; // the closer is dropped by the 't'/'a' case next iteration
|
||||
}
|
||||
default: // unknown sequence — keep the pipe literally
|
||||
out.push_back('|');
|
||||
++i;
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
// 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/>.
|
||||
|
||||
// `PLAYER_SWING(swingDuration, swingType)` and `Enum.PlayerSwingType` — the
|
||||
// Classic-beta swing-timer events, backported from packet data 1.12 already
|
||||
// receives. See CLAUDE.md's "Backport the Classic swing-timer API" entry for
|
||||
// the full derivation; short version here.
|
||||
//
|
||||
// This module keeps its own swing-timer STATE rather than relaying packets
|
||||
// 1:1, because the server mutates the swing timer from several places that
|
||||
// don't map to one packet each:
|
||||
//
|
||||
// - a white hit lands -> SMSG_ATTACKERSTATEUPDATE, attacker == us
|
||||
// - an on-next-swing ability replaces it -> SMSG_SPELL_GO, Attributes & ON_NEXT_SWING
|
||||
// - a cast-time spell is cast -> SMSG_SPELL_GO, InterruptFlags & AUTOATTACK
|
||||
// - autorepeat (Auto Shot / Shoot) fires -> SMSG_SPELL_GO, AttributesEx2 & AUTOREPEAT
|
||||
// - starting a fresh melee attack -> SMSG_ATTACKSTART (off-hand only)
|
||||
// - a weapon is swapped mid-combat -> no packet; polled off the invMgr GUIDs
|
||||
// - the LOCAL PLAYER parries an attack -> SMSG_ATTACKERSTATEUPDATE, victim == us,
|
||||
// targetState == PARRY (no separate packet —
|
||||
// the server just clips OUR remaining time)
|
||||
//
|
||||
// Extra attacks (Windfury, Sword Specialization) send N packets for one
|
||||
// logical reset; firing straight off the packet would emit N `PLAYER_SWING`s
|
||||
// for the same instant. Instead every mutation above just marks a swing type
|
||||
// "dirty" and a WorldTick subscriber fires at most one event per type per
|
||||
// frame, with the CURRENT remaining time — so N packets in one frame still
|
||||
// produce exactly one event, and a parry-haste correction that lands in the
|
||||
// same frame as the original reset collapses into it for free.
|
||||
|
||||
#include "combat/Swing.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "net/SendObserver.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Combat::Swing {
|
||||
|
||||
namespace {
|
||||
|
||||
using Net::CDataStore;
|
||||
using Time::Clock::NowMs;
|
||||
|
||||
// ---- Per-swing-type state -----------------------------------------------
|
||||
|
||||
struct State {
|
||||
uint32_t endMs = 0;
|
||||
bool active = false; // at least one reset has happened this session
|
||||
bool dirty = false; // needs a PLAYER_SWING fire on the next WorldTick
|
||||
};
|
||||
State g_swing[3];
|
||||
|
||||
void MarkReset(SwingType type, uint32_t durationMs) {
|
||||
if (durationMs == 0)
|
||||
return; // no weapon for this hand right now -- nothing to report
|
||||
State &s = g_swing[type];
|
||||
s.endMs = NowMs() + durationMs;
|
||||
s.active = true;
|
||||
s.dirty = true;
|
||||
}
|
||||
|
||||
bool PlayerInCombat() {
|
||||
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
|
||||
if (desc == nullptr)
|
||||
return false;
|
||||
const uint32_t flags =
|
||||
*reinterpret_cast<const uint32_t *>(desc + Offsets::OFF_UNIT_FIELD_FLAGS);
|
||||
return (flags & Offsets::UNIT_FLAG_IN_COMBAT) != 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t AttackTimeMs(SwingType type) {
|
||||
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
const int off = type == MAIN_HAND
|
||||
? Offsets::OFF_UNIT_FIELD_BASEATTACKTIME
|
||||
: type == OFF_HAND
|
||||
? Offsets::OFF_UNIT_FIELD_OFFHANDATTACKTIME
|
||||
: Offsets::OFF_UNIT_FIELD_RANGEDATTACKTIME;
|
||||
return *reinterpret_cast<const uint32_t *>(desc + off);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Reads a unit's bounding radius (the same field `Unit::Range`'s
|
||||
// `UnitInRange`/`UnitDistanceSquared` and `Combat::SwingRange` read) off its
|
||||
// descriptor. 0 for a null object or an unpopulated descriptor.
|
||||
float BoundingRadius(void *obj) {
|
||||
if (obj == nullptr)
|
||||
return 0.0f;
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(obj) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (fields == nullptr)
|
||||
return 0.0f;
|
||||
return *reinterpret_cast<const float *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool InMeleeRange(void *player, void *target) {
|
||||
float playerPos[3];
|
||||
float targetPos[3];
|
||||
if (!Unit::Position::Read(player, playerPos) || !Unit::Position::Read(target, targetPos))
|
||||
return false;
|
||||
|
||||
const float leeway =
|
||||
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_LEEWAY));
|
||||
const float floorRange =
|
||||
Game::Read<float>(static_cast<uintptr_t>(Offsets::VAR_MELEE_REACH_MIN));
|
||||
float maxRange = BoundingRadius(player) + BoundingRadius(target) + leeway;
|
||||
if (maxRange < floorRange)
|
||||
maxRange = floorRange;
|
||||
|
||||
const float dx = playerPos[0] - targetPos[0];
|
||||
const float dy = playerPos[1] - targetPos[1];
|
||||
return (dx * dx + dy * dy) < (maxRange * maxRange);
|
||||
}
|
||||
|
||||
namespace {
|
||||
AutoSwingResetOverride *g_overrides = nullptr;
|
||||
} // namespace
|
||||
|
||||
AutoSwingResetOverride::AutoSwingResetOverride(SwingResetOverride fn)
|
||||
: fn(fn), next(g_overrides) {
|
||||
g_overrides = this;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- Non-triggered-cast gate ---------------------------------------------
|
||||
//
|
||||
// The server's swing-reset rule is `!triggered && InterruptFlags & AUTOATTACK`.
|
||||
// SMSG_SPELL_GO carries no triggered bit, so we approximate "not triggered"
|
||||
// by "we ourselves sent CMSG_CAST_SPELL for this id recently" — a proc/aura
|
||||
// trigger never originates from a client packet. Mirrors the capture-ring
|
||||
// pattern in `Aura::ComboDuration` (co-hooking the same NetClient send
|
||||
// funnel via `Net::SendObserver`), simplified: presence within a TTL is
|
||||
// all that's needed here, no payload to carry.
|
||||
struct SentCast {
|
||||
uint32_t spellId;
|
||||
uint32_t tMs;
|
||||
};
|
||||
constexpr int kSentCastCount = 8;
|
||||
SentCast g_sentCasts[kSentCastCount];
|
||||
int g_sentCastCursor = 0;
|
||||
constexpr uint32_t kSentCastTtlMs = 3000; // worst-case send -> SPELL_GO roundtrip
|
||||
|
||||
void RememberSentCast(uint32_t spellId) {
|
||||
g_sentCasts[g_sentCastCursor] = {spellId, NowMs()};
|
||||
g_sentCastCursor = (g_sentCastCursor + 1) % kSentCastCount;
|
||||
}
|
||||
|
||||
bool WasNonTriggeredCast(uint32_t spellId) {
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &c : g_sentCasts) {
|
||||
if (c.spellId == spellId && now - c.tMs < kSentCastTtlMs) {
|
||||
c.spellId = 0; // consume -- a queued re-cast needs its own capture
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnSend(uint32_t opcode, CDataStore *packet) {
|
||||
if (opcode != Offsets::OP_CMSG_CAST_SPELL)
|
||||
return;
|
||||
const uint32_t spellId = Net::Read<uint32_t>(packet);
|
||||
if (spellId != 0)
|
||||
RememberSentCast(spellId);
|
||||
}
|
||||
|
||||
const Net::SendObserver::AutoSubscribe _sendSub{&OnSend};
|
||||
|
||||
// ---- Parry haste ----------------------------------------------------------
|
||||
|
||||
constexpr uint32_t kDefaultUnarmedDelayMs = 2000;
|
||||
constexpr int kInvSlotMainHand1Based = 16;
|
||||
constexpr int kInvSlotOffHand1Based = 17;
|
||||
|
||||
// The UNHASTED weapon swing time for a hand. The server's own
|
||||
// `Unit::GetAttackTime()` unwinds the live percent-mod to recover this, but
|
||||
// the percent-mod factor never crosses the wire — the descriptor field only
|
||||
// ever carries the CURRENT (already-hasted) value. Read it from the weapon's
|
||||
// own item data instead: a static property of the item, independent of any
|
||||
// haste tracking. `slot1Based` is 16 (main hand) or 17 (off hand); on an
|
||||
// item-cache miss (rare — see the item-cache-race notes in CLAUDE.md) we
|
||||
// fall back to the live descriptor value rather than guessing.
|
||||
uint32_t UnhastedWeaponDelayMs(int slot1Based, SwingType fallbackType) {
|
||||
const uint8_t *cgItem = Item::Location::ResolveEquipmentSlot(slot1Based);
|
||||
if (cgItem != nullptr) {
|
||||
const uint8_t *instance = Item::InstanceBlock(cgItem);
|
||||
if (instance != nullptr) {
|
||||
const uint32_t itemID = *reinterpret_cast<const uint32_t *>(
|
||||
instance + Offsets::OFF_INSTANCE_BLOCK_ITEM_ID);
|
||||
const uint8_t *stats = Item::PeekRecord(itemID);
|
||||
if (stats != nullptr) {
|
||||
const uint32_t delay = *reinterpret_cast<const uint32_t *>(
|
||||
stats + Offsets::OFF_ITEMSTATS_DELAY);
|
||||
if (delay > 0)
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
const uint32_t live = AttackTimeMs(fallbackType);
|
||||
if (live > 0)
|
||||
return live;
|
||||
}
|
||||
return kDefaultUnarmedDelayMs;
|
||||
}
|
||||
|
||||
// Mirrors the server's parry-haste correction (tortoise-wow's
|
||||
// `VICTIMSTATE_PARRY` block, reached from the ATTACKER's damage path when
|
||||
// WE are the victim and parried): whichever of our hands has less time
|
||||
// remaining is cut to 20% of its unhasted weapon delay if it was between
|
||||
// 20% and 60% remaining, or has 40% of the delay subtracted if it was past
|
||||
// 60% remaining. Under 20% remaining, the hand is left alone (already about
|
||||
// to swing) — matches the server's `if/else if`, no `else` branch.
|
||||
void ApplyParryHaste() {
|
||||
State &main = g_swing[MAIN_HAND];
|
||||
State &off = g_swing[OFF_HAND];
|
||||
const uint32_t now = NowMs();
|
||||
const uint32_t mainRemaining = Time::Clock::Remaining(now, main.endMs);
|
||||
const uint32_t offRemaining = Time::Clock::Remaining(now, off.endMs);
|
||||
|
||||
const bool useOff = off.active && offRemaining < mainRemaining;
|
||||
State &target = useOff ? off : main;
|
||||
if (!target.active)
|
||||
return;
|
||||
|
||||
const uint32_t remaining = useOff ? offRemaining : mainRemaining;
|
||||
const uint32_t unhastedDelay = UnhastedWeaponDelayMs(
|
||||
useOff ? kInvSlotOffHand1Based : kInvSlotMainHand1Based,
|
||||
useOff ? OFF_HAND : MAIN_HAND);
|
||||
|
||||
const float p20 = static_cast<float>(unhastedDelay) * 0.20f;
|
||||
const float p60 = 3.0f * p20;
|
||||
const auto r = static_cast<float>(remaining);
|
||||
|
||||
uint32_t newRemaining = remaining;
|
||||
if (r > p20 && r <= p60)
|
||||
newRemaining = static_cast<uint32_t>(p20);
|
||||
else if (r > p60)
|
||||
newRemaining = static_cast<uint32_t>(r - 2.0f * p20);
|
||||
|
||||
if (newRemaining != remaining) {
|
||||
target.endMs = now + newRemaining;
|
||||
target.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Ranged wind-up compensation ------------------------------------------
|
||||
|
||||
using GetCastTime_t = int(__fastcall *)(int spellID, int unit, int flag);
|
||||
|
||||
// Auto Shot / Shoot are re-cast by the server as TRIGGERED spells, so the
|
||||
// per-shot SMSG_SPELL_START is skipped and SMSG_SPELL_GO is the only packet
|
||||
// — arriving at the END of the ~0.5s wind-up, well after the server actually
|
||||
// reset the timer. Subtract the engine's own cast-time helper (the same one
|
||||
// `Spell::Cast` uses for the cast bar) so the ranged bar still ends on time
|
||||
// instead of running ~0.5s long.
|
||||
void HandleAutorepeatReset(uint32_t spellId) {
|
||||
const uint32_t rangedTime = AttackTimeMs(RANGED);
|
||||
if (rangedTime == 0)
|
||||
return;
|
||||
auto getCastTime = reinterpret_cast<GetCastTime_t>(Offsets::FUN_GET_CAST_TIME);
|
||||
int castTime = getCastTime(static_cast<int>(spellId), 0, 0);
|
||||
if (castTime < 0)
|
||||
castTime = 0;
|
||||
const auto windUp = static_cast<uint32_t>(castTime);
|
||||
MarkReset(RANGED, windUp >= rangedTime ? 0 : rangedTime - windUp);
|
||||
}
|
||||
|
||||
// ---- SMSG_SPELL_GO dispatch -----------------------------------------------
|
||||
|
||||
bool ForcedSwingReset(uint32_t spellId) {
|
||||
for (auto *r = g_overrides; r != nullptr; r = r->next)
|
||||
if (r->fn(spellId))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnSpellGo(uint32_t spellId) {
|
||||
if (spellId == 0)
|
||||
return;
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT, spellId);
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
|
||||
const uint32_t attr =
|
||||
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES);
|
||||
if (attr & Offsets::SPELL_ATTR_ON_NEXT_SWING) {
|
||||
// Replaces the next white hit -- not also evaluated as a cast-reset
|
||||
// spell below (an on-next-swing ability's InterruptFlags is unrelated).
|
||||
MarkReset(MAIN_HAND, AttackTimeMs(MAIN_HAND));
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t attrEx2 = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX2);
|
||||
if (attrEx2 & Offsets::SPELL_ATTR_EX2_AUTOREPEAT_FLAG) {
|
||||
HandleAutorepeatReset(spellId);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t interrupt = *reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_INTERRUPT_FLAGS);
|
||||
if ((interrupt & Offsets::SPELL_INTERRUPT_FLAG_AUTOATTACK) &&
|
||||
(ForcedSwingReset(spellId) ||
|
||||
!(attrEx2 & Offsets::SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS)) &&
|
||||
WasNonTriggeredCast(spellId)) {
|
||||
MarkReset(MAIN_HAND, AttackTimeMs(MAIN_HAND));
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Incoming packets -------------------------------------------------
|
||||
|
||||
void OnPacket(uint32_t opcode, CDataStore *packet) {
|
||||
if (packet == nullptr)
|
||||
return;
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
if (player == 0)
|
||||
return;
|
||||
|
||||
if (opcode == Offsets::SMSG_ATTACKERSTATEUPDATE) {
|
||||
const uint32_t hitInfo = Net::Read<uint32_t>(packet);
|
||||
const uint64_t attacker = Net::ReadPackedGuid(packet);
|
||||
const uint64_t victim = Net::ReadPackedGuid(packet);
|
||||
Net::Read<uint32_t>(packet); // totalDamage -- unused here
|
||||
const uint8_t subCount = Net::Read<uint8_t>(packet);
|
||||
for (uint8_t i = 0; i < subCount; ++i) {
|
||||
Net::Read<uint32_t>(packet); // school
|
||||
Net::Read<float>(packet); // coefficient
|
||||
Net::Read<uint32_t>(packet); // damage
|
||||
Net::Read<uint32_t>(packet); // absorb
|
||||
Net::Read<int32_t>(packet); // resist
|
||||
}
|
||||
const uint32_t targetState = Net::Read<uint32_t>(packet);
|
||||
|
||||
if (attacker == player) {
|
||||
const SwingType hand =
|
||||
(hitInfo & Offsets::HITINFO_LEFTSWING) ? OFF_HAND : MAIN_HAND;
|
||||
MarkReset(hand, AttackTimeMs(hand));
|
||||
} else if (victim == player && targetState == Offsets::VICTIMSTATE_PARRY) {
|
||||
ApplyParryHaste();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (opcode == Offsets::SMSG_ATTACKSTART) {
|
||||
const uint64_t attacker = Net::Read<uint64_t>(packet);
|
||||
const uint64_t victim = Net::Read<uint64_t>(packet);
|
||||
if (attacker == player) {
|
||||
// Unit::Attack() resets the off-hand timer the INSTANT an
|
||||
// attack is declared, even out of range -- the server just
|
||||
// silently re-arms it every 100ms (DelayAutoAttacks(), no
|
||||
// packet) until the target is actually reachable. Firing here
|
||||
// unconditionally would promise "swing lands in Xs" while
|
||||
// stuck out of range, which never comes true until the real
|
||||
// first swing's SMSG_ATTACKERSTATEUPDATE resets it for real.
|
||||
// Gate on actually being in melee range so this only fires
|
||||
// when the promised time is meaningful.
|
||||
auto *victimObj = Object::ByGuid(
|
||||
Offsets::TYPEMASK_UNIT | Offsets::TYPEMASK_PLAYER, victim);
|
||||
auto *playerObj = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (InMeleeRange(playerObj, victimObj))
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (opcode == Offsets::SMSG_SPELL_GO) {
|
||||
Net::ReadPackedGuid(packet); // itemGuid -- unused here
|
||||
const uint64_t caster = Net::ReadPackedGuid(packet);
|
||||
const uint32_t spellId = Net::Read<uint32_t>(packet);
|
||||
if (caster == player)
|
||||
OnSpellGo(spellId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
// ---- Weapon swap mid-combat (polled -- no packet, no observer needed) ---
|
||||
//
|
||||
// A weapon equipped while `IsInCombat()` resets that hand's timer
|
||||
// server-side (`Player::_ApplyWeaponDependentAuraMods` -> `SetAttackTime`
|
||||
// with `resetTimer = IsInCombat()`). There's no dedicated packet for this;
|
||||
// `Player::Equipment` already tracks the same invMgr GUIDs via a real
|
||||
// descriptor observer for `PLAYER_EQUIPMENT_CHANGED`, but that's a one-way
|
||||
// Lua event fire with nothing to subscribe to in C++. Only 3 slots matter
|
||||
// here, so a plain 3-GUID compare on the existing WorldTick pass is simpler
|
||||
// than standing up a second observer registration for the same fields.
|
||||
|
||||
uint64_t g_lastMainGuid = 0;
|
||||
uint64_t g_lastOffGuid = 0;
|
||||
uint64_t g_lastRangedGuid = 0;
|
||||
bool g_weaponSnapValid = false;
|
||||
|
||||
void PollWeaponSwap() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
const uint8_t *invMgr = player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
const uint32_t count =
|
||||
*reinterpret_cast<const uint32_t *>(invMgr + Offsets::OFF_INVMGR_SLOT_COUNT);
|
||||
if (count < 18) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
const uint64_t *guids = *reinterpret_cast<const uint64_t *const *>(
|
||||
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 0-based invMgr slots: 15 = main hand (Lua 16), 16 = off hand (17),
|
||||
// 17 = ranged (18) -- same `slot0 = luaSlot - 1` convention documented
|
||||
// in Player::Equipment.
|
||||
const uint64_t mainGuid = guids[15];
|
||||
const uint64_t offGuid = guids[16];
|
||||
const uint64_t rangedGuid = guids[17];
|
||||
|
||||
if (g_weaponSnapValid && PlayerInCombat()) {
|
||||
if (mainGuid != g_lastMainGuid)
|
||||
MarkReset(MAIN_HAND, AttackTimeMs(MAIN_HAND));
|
||||
if (offGuid != g_lastOffGuid)
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
if (rangedGuid != g_lastRangedGuid)
|
||||
MarkReset(RANGED, AttackTimeMs(RANGED));
|
||||
}
|
||||
|
||||
g_lastMainGuid = mainGuid;
|
||||
g_lastOffGuid = offGuid;
|
||||
g_lastRangedGuid = rangedGuid;
|
||||
g_weaponSnapValid = true;
|
||||
}
|
||||
|
||||
// ---- PLAYER_SWING fire (WorldTick, at most once per type per frame) -----
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("swingDuration", "number",
|
||||
"Seconds from now until this swing type's next attack."),
|
||||
Game::Doc::Req("swingType", "PlayerSwingType", "Which attack this reset applies to."),
|
||||
};
|
||||
const Game::Doc::Event kPlayerSwingDoc{
|
||||
"SwingTimer",
|
||||
"Fires whenever the player's melee or ranged auto-attack timer resets, "
|
||||
"with the seconds remaining until the next swing.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve _evt{"PLAYER_SWING", &kPlayerSwingDoc};
|
||||
|
||||
void OnWorldTick() {
|
||||
PollWeaponSwap();
|
||||
|
||||
const int slot = _evt.Slot();
|
||||
const uint32_t now = NowMs();
|
||||
for (int t = 0; t < 3; ++t) {
|
||||
State &s = g_swing[t];
|
||||
if (!s.dirty)
|
||||
continue;
|
||||
s.dirty = false;
|
||||
if (slot < 0)
|
||||
continue;
|
||||
const uint32_t remaining = Time::Clock::Remaining(now, s.endMs);
|
||||
if (remaining == 0)
|
||||
continue; // elapsed before we got to fire it -- nothing to show
|
||||
Event::Custom::Fire(slot, "%f%d", static_cast<double>(remaining) * 0.001, t);
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
// ---- Enum.PlayerSwingType --------------------------------------------------
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kSwingTypeEntries[] = {
|
||||
{"MainHand", MAIN_HAND},
|
||||
{"OffHand", OFF_HAND},
|
||||
{"Ranged", RANGED},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "PlayerSwingType", kSwingTypeEntries,
|
||||
sizeof(kSwingTypeEntries) / sizeof(kSwingTypeEntries[0]), "SwingTimer");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Combat::Swing
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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>
|
||||
|
||||
// `Enum.PlayerSwingType` values, matching the `_classic_beta_`
|
||||
// `SwingTimerDocumentation.lua` contract exactly (MainHand=0, OffHand=1,
|
||||
// Ranged=2). Shared between `Combat::Swing` (PLAYER_SWING) and
|
||||
// `Combat::SwingRange` (C_SwingTimer / PLAYER_SWING_RANGE_UPDATE).
|
||||
|
||||
namespace Combat::Swing {
|
||||
|
||||
enum SwingType : int { MAIN_HAND = 0, OFF_HAND = 1, RANGED = 2 };
|
||||
|
||||
// Live read of the player's current (already-hasted) swing time for `type`,
|
||||
// in ms — `UNIT_FIELD_BASEATTACKTIME` / `_OFFHAND` / `UNIT_FIELD_RANGEDATTACKTIME`
|
||||
// off the player's own descriptor, the same fields `UnitAttackSpeed` /
|
||||
// `UnitRangedDamage` read. 0 when the player object isn't resolvable or no
|
||||
// weapon is equipped for that hand (no off-hand weapon, no ranged weapon) —
|
||||
// callers treat 0 as "this swing type doesn't apply right now".
|
||||
uint32_t AttackTimeMs(SwingType type);
|
||||
|
||||
// True if `player` and `target` (raw CGUnit* object pointers, e.g. from
|
||||
// `Unit::Position::ResolveToken` or `Object::ByGuid`) are within pure 2D
|
||||
// (X/Y) melee reach of each other -- the server's own auto-attack distance
|
||||
// test (`Unit::CanReachWithMeleeAutoAttackAtPosition` /
|
||||
// `WorldObject::CanReachWithMeleeSpellAttack`, both explicitly 2D; see
|
||||
// `Offsets::VAR_MELEE_REACH_LEEWAY`'s comment for why the generic 3D spell-
|
||||
// range core can't be reused here). False if either pointer is null or
|
||||
// either position can't be read. Shared by `Combat::Swing` (to decide
|
||||
// whether an attack-start reset is close enough to promise a real swing
|
||||
// time) and `Combat::SwingRange` (the melee `C_SwingTimer` check).
|
||||
bool InMeleeRange(void *player, void *target);
|
||||
|
||||
// Extension point for server-specific exceptions to the "does casting this
|
||||
// spell reset the caster's melee swing timer" rule. The stock rule is
|
||||
// data-driven off the client's own `Spell.dbc` (`InterruptFlags &
|
||||
// SPELL_INTERRUPT_FLAG_AUTOATTACK`, suppressed by `AttributesEx2 &
|
||||
// SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS`) — see `src/turtle/SwingReset.cpp`
|
||||
// for the motivating case: every rank of Slam in Turtle's OWN client DBC
|
||||
// carries the suppression flag, but Turtle's server doesn't honor it for
|
||||
// Slam (verified in-game by packet timestamps: both melee timers delayed
|
||||
// after a Slam cast), contradicting both the client data and the public
|
||||
// tortoise-wow reference source. A registered predicate returning true
|
||||
// forces the reset regardless of the `AttributesEx2` suppression bit — it
|
||||
// does NOT bypass the `InterruptFlags` gate itself, since every known case
|
||||
// so far already has that bit set.
|
||||
//
|
||||
// Declare a file-scope `static const Combat::Swing::AutoSwingResetOverride
|
||||
// _r{&Predicate};` — same static-init chaining as every other
|
||||
// `AutoSubscribe`-style registrar in this codebase.
|
||||
using SwingResetOverride = bool (*)(uint32_t spellId);
|
||||
struct AutoSwingResetOverride {
|
||||
explicit AutoSwingResetOverride(SwingResetOverride fn);
|
||||
SwingResetOverride fn;
|
||||
AutoSwingResetOverride *next;
|
||||
};
|
||||
|
||||
} // namespace Combat::Swing
|
||||
@@ -0,0 +1,260 @@
|
||||
// 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_SwingTimer.EnableRangeCheck` / `IsTargetWithinSwingRange` and
|
||||
// `PLAYER_SWING_RANGE_UPDATE(swingType, isInRange, checksRange)`.
|
||||
//
|
||||
// RANGED reuses the engine's generic range core: `Spell::Range::PlayerVsUnit`
|
||||
// wraps `FUN_SPELL_RANGE_CHECK` -> `FUN_006e3480`, feeding it Auto Shot/Shoot
|
||||
// as a pure geometry probe (it doesn't check spell knowledge). That core
|
||||
// computes full 3D (X/Y/Z) distance, which matches the SERVER's generic
|
||||
// spell-range gate (`Spell::CheckRange`'s non-combat-range path ->
|
||||
// `WorldObject::GetCombatDistance`, also 3D) closely enough for ranged.
|
||||
//
|
||||
// MELEE does NOT reuse it — verified wrong in-game (rejected a swing at
|
||||
// 5.08yd 3D distance from a stationary target's center while hits were
|
||||
// landing). The server's actual melee-attack gate
|
||||
// (`Unit::CanReachWithMeleeAutoAttackAtPosition`,
|
||||
// `WorldObject::CanReachWithMeleeSpellAttack`) is explicitly 2D — its own
|
||||
// source comment says "melee spells ignore Z-axis checks" — so any Z offset
|
||||
// between the two units (sloped ground, stairs, model-origin differences)
|
||||
// eats into a distance budget the server never charges for. The GENERIC
|
||||
// engine range core was never asked to gate a real auto-attack decision
|
||||
// before this feature existed, so there's no existing client mechanism for
|
||||
// that exact semantic to mirror; `Combat::Swing::InMeleeRange` reproduces the
|
||||
// SERVER's formula instead (using the engine's own live reach constants
|
||||
// rather than hardcoding them) — shared with `Combat::Swing`, which also
|
||||
// needs it to decide whether an attack-start off-hand reset is close enough
|
||||
// to promise a real swing time.
|
||||
//
|
||||
// What this module adds on top of the geometry: the "no weapon for this
|
||||
// swing type" and "target can't be attacked" gates Blizzard's contract also
|
||||
// requires (both report as `isInRange == nil`, i.e. `checksRange == false`),
|
||||
// and turning the per-frame poll into a change-driven
|
||||
// `PLAYER_SWING_RANGE_UPDATE`.
|
||||
|
||||
#include "combat/Swing.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "spell/Range.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Combat::SwingRange {
|
||||
|
||||
namespace {
|
||||
|
||||
using Combat::Swing::SwingType;
|
||||
using Combat::Swing::RANGED;
|
||||
|
||||
enum class RangeState { NoCheck, InRange, OutOfRange };
|
||||
|
||||
bool g_enabled[3] = {false, false, false};
|
||||
RangeState g_lastState[3] = {RangeState::NoCheck, RangeState::NoCheck, RangeState::NoCheck};
|
||||
|
||||
using UnitCanAttack_t = bool(__thiscall *)(void *attacker, void *target);
|
||||
|
||||
// Pure geometry probe for Ranged -- the two ranged-weapon families.
|
||||
constexpr int kAutoShotSpellId = 75; // bow/gun/crossbow, RangeIndex 114 (8-35yd)
|
||||
constexpr int kShootSpellId = 5019; // wand, RangeIndex 4 (0-30yd)
|
||||
|
||||
// While actively auto-shooting, the exact spell in flight is the truest
|
||||
// probe; otherwise pick by the equipped ranged weapon's ammo type (bows/
|
||||
// guns/crossbows consume ammo, wands don't -- the same distinction the
|
||||
// server itself uses to route SPELL_ATTR_EX2_AUTOREPEAT_FLAG casts).
|
||||
int RangedProbeSpellId() {
|
||||
const auto active = Game::Read<uint32_t>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTIVE_AUTO_REPEAT_SPELL));
|
||||
if (active != 0)
|
||||
return static_cast<int>(active);
|
||||
|
||||
const uint8_t *cgItem = Item::Location::ResolveEquipmentSlot(18); // ranged slot
|
||||
if (cgItem != nullptr) {
|
||||
const uint8_t *instance = Item::InstanceBlock(cgItem);
|
||||
if (instance != nullptr) {
|
||||
const uint32_t itemID = *reinterpret_cast<const uint32_t *>(
|
||||
instance + Offsets::OFF_INSTANCE_BLOCK_ITEM_ID);
|
||||
const uint8_t *stats = Item::PeekRecord(itemID);
|
||||
if (stats != nullptr) {
|
||||
const uint32_t ammoType = *reinterpret_cast<const uint32_t *>(
|
||||
stats + Offsets::OFF_ITEMSTATS_AMMO_TYPE);
|
||||
return ammoType != 0 ? kAutoShotSpellId : kShootSpellId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return kAutoShotSpellId;
|
||||
}
|
||||
|
||||
RangeState Evaluate(SwingType type) {
|
||||
if (Combat::Swing::AttackTimeMs(type) == 0)
|
||||
return RangeState::NoCheck; // no weapon equipped for this swing type
|
||||
|
||||
void *player = Unit::Position::ResolveToken("player");
|
||||
void *target = Unit::Position::ResolveToken("target");
|
||||
if (player == nullptr || target == nullptr)
|
||||
return RangeState::NoCheck; // no current target
|
||||
|
||||
auto canAttack = reinterpret_cast<UnitCanAttack_t>(Offsets::FUN_UNIT_CAN_ATTACK);
|
||||
if (!canAttack(player, target))
|
||||
return RangeState::NoCheck; // target can't be attacked
|
||||
|
||||
if (type != RANGED)
|
||||
return Combat::Swing::InMeleeRange(player, target) ? RangeState::InRange
|
||||
: RangeState::OutOfRange;
|
||||
|
||||
const int result = Spell::Range::PlayerVsUnit(RangedProbeSpellId(), "target");
|
||||
if (result < 0)
|
||||
return RangeState::NoCheck;
|
||||
return result != 0 ? RangeState::InRange : RangeState::OutOfRange;
|
||||
}
|
||||
|
||||
// --- C_SwingTimer.EnableRangeCheck(swingType, enable) ----------------------
|
||||
|
||||
int __fastcall Script_EnableRangeCheck(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_SwingTimer.EnableRangeCheck(swingType, enable)");
|
||||
return 0;
|
||||
}
|
||||
const int type = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (type < 0 || type > 2)
|
||||
return 0;
|
||||
const bool enable = Game::Lua::ToBoolean(L, 2) != 0;
|
||||
g_enabled[type] = enable;
|
||||
// Seed the change-detector without firing -- callers are expected to
|
||||
// read the current state via IsTargetWithinSwingRange right after
|
||||
// enabling (exactly what SwingTimerMixin:UpdateRangeState does), not
|
||||
// wait for a synthetic first event.
|
||||
g_lastState[type] = enable ? Evaluate(static_cast<SwingType>(type)) : RangeState::NoCheck;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- C_SwingTimer.IsTargetWithinSwingRange(swingType) -> isInRange ---------
|
||||
|
||||
int __fastcall Script_IsTargetWithinSwingRange(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_SwingTimer.IsTargetWithinSwingRange(swingType)");
|
||||
return 0;
|
||||
}
|
||||
const int type = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (type < 0 || type > 2) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const RangeState s = Evaluate(static_cast<SwingType>(type));
|
||||
if (s == RangeState::NoCheck)
|
||||
Game::Lua::PushNil(L);
|
||||
else
|
||||
Game::Lua::PushBool(L, s == RangeState::InRange);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- PLAYER_SWING_RANGE_UPDATE(swingType, isInRange, checksRange) ----------
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("swingType", "PlayerSwingType", "Which attack's range changed."),
|
||||
Game::Doc::Req("isInRange", "bool",
|
||||
"Whether the current target is within auto attack range. "
|
||||
"Should not be used if checksRange is false."),
|
||||
Game::Doc::Req("checksRange", "bool",
|
||||
"False when no range check could be made, for example there "
|
||||
"is no current target."),
|
||||
};
|
||||
const Game::Doc::Event kRangeUpdateDoc{
|
||||
"SwingTimer",
|
||||
"Fires when the current target moves in or out of range for a swing type "
|
||||
"that has EnableRangeCheck on.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve _evt{"PLAYER_SWING_RANGE_UPDATE", &kRangeUpdateDoc};
|
||||
|
||||
// The engine's printf-style event dispatcher has no `%b` and a plain `%d`
|
||||
// with `0` pushes the NUMBER 0 (truthy in Lua) -- so each bool picks
|
||||
// between `%d`+`1` and `%s`+NULL (tail-jumps to `lua_pushnil`), the same
|
||||
// 1/nil idiom `Event::Custom::FireIdSuccess` uses. `isInRange` is only ever
|
||||
// true when `checksRange` is also true (see `Evaluate`), so three shapes
|
||||
// cover every reachable state.
|
||||
void FireRangeUpdate(int slot, int type, bool isInRange, bool checksRange) {
|
||||
if (isInRange)
|
||||
Event::Custom::Fire(slot, "%d%d%d", type, 1, 1);
|
||||
else if (checksRange)
|
||||
Event::Custom::Fire(slot, "%d%s%d", type, static_cast<const char *>(nullptr), 1);
|
||||
else
|
||||
Event::Custom::Fire(slot, "%d%s%s", type, static_cast<const char *>(nullptr),
|
||||
static_cast<const char *>(nullptr));
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
const int slot = _evt.Slot();
|
||||
for (int t = 0; t < 3; ++t) {
|
||||
if (!g_enabled[t])
|
||||
continue;
|
||||
const RangeState cur = Evaluate(static_cast<SwingType>(t));
|
||||
if (cur == g_lastState[t])
|
||||
continue;
|
||||
g_lastState[t] = cur;
|
||||
if (slot < 0)
|
||||
continue;
|
||||
FireRangeUpdate(slot, t, cur == RangeState::InRange, cur != RangeState::NoCheck);
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
// --- Registration -----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kEnableArgs[] = {
|
||||
Game::Doc::Req("swingType", "PlayerSwingType"),
|
||||
Game::Doc::Req("enable", "bool",
|
||||
"True if changes in range for the swing type should dispatch "
|
||||
"PLAYER_SWING_RANGE_UPDATE. False if the swing type no longer "
|
||||
"needs the event."),
|
||||
};
|
||||
const Game::Doc::Function kEnableDoc{
|
||||
"Used with PLAYER_SWING_RANGE_UPDATE to be informed when the current target "
|
||||
"enters or leaves range for a swing type.",
|
||||
kEnableArgs};
|
||||
|
||||
const Game::Doc::Field kIsInRangeArgs[] = {
|
||||
Game::Doc::Req("swingType", "PlayerSwingType"),
|
||||
};
|
||||
const Game::Doc::Field kIsInRangeRets[] = {
|
||||
Game::Doc::Opt("isInRange", "bool", nullptr,
|
||||
"Nil when no range check could be made, for example there is "
|
||||
"no target, the target cannot be attacked, or no weapon is "
|
||||
"equipped for the swing type. Nil must not be treated as out "
|
||||
"of range."),
|
||||
};
|
||||
const Game::Doc::Function kIsInRangeDoc{
|
||||
"Whether the current target is within range of the player's auto attack for "
|
||||
"the given swing type. Auto attacks only ever apply to the current target, "
|
||||
"so no other unit can be queried.",
|
||||
kIsInRangeArgs, kIsInRangeRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_SwingTimer", "EnableRangeCheck",
|
||||
&Script_EnableRangeCheck, &kEnableDoc);
|
||||
Game::Lua::RegisterTableFunction("C_SwingTimer", "IsTargetWithinSwingRange",
|
||||
&Script_IsTargetWithinSwingRange, &kIsInRangeDoc);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Combat::SwingRange
|
||||
@@ -0,0 +1,123 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "CommandLine.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace Config::CommandLine {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsSpace(char c) { return c == ' ' || c == '\t'; }
|
||||
|
||||
char Lower(char c) { return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c; }
|
||||
|
||||
// Stops at `a`'s terminator rather than reading `len` bytes unconditionally, so
|
||||
// a switch name at the very end of the command line cannot read past it.
|
||||
bool EqualsIgnoreCase(const char *a, const char *b, size_t len) {
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (a[i] == '\0' || Lower(a[i]) != Lower(b[i]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t Length(const char *s) {
|
||||
size_t n = 0;
|
||||
while (s[n] != '\0')
|
||||
++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Start of the value token for `-name`, or null when the switch is absent.
|
||||
const char *FindValue(const char *name) {
|
||||
const char *cmdLine = ::GetCommandLineA();
|
||||
if (cmdLine == nullptr || name == nullptr)
|
||||
return nullptr;
|
||||
const size_t nameLen = Length(name);
|
||||
if (nameLen == 0)
|
||||
return nullptr;
|
||||
|
||||
for (const char *p = cmdLine; *p != '\0'; ++p) {
|
||||
if (*p != '-' && *p != '/')
|
||||
continue;
|
||||
// Only at a token boundary, so a path containing the name cannot match.
|
||||
if (p != cmdLine && !IsSpace(p[-1]) && p[-1] != '"')
|
||||
continue;
|
||||
if (!EqualsIgnoreCase(p + 1, name, nameLen))
|
||||
continue;
|
||||
|
||||
const char *after = p + 1 + nameLen;
|
||||
if (*after == '=') {
|
||||
++after;
|
||||
} else if (IsSpace(*after)) {
|
||||
while (IsSpace(*after))
|
||||
++after;
|
||||
} else {
|
||||
continue; // a longer switch that merely starts with `name`
|
||||
}
|
||||
return after;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Walks the token at `p`, honoring double quotes so a value containing spaces
|
||||
// arrives whole. `emit` takes each character and returns whether it was kept;
|
||||
// the walk stops as a failure the moment one is not.
|
||||
template <typename Emit> bool ReadToken(const char *p, Emit emit) {
|
||||
const bool quoted = (*p == '"');
|
||||
if (quoted)
|
||||
++p;
|
||||
size_t n = 0;
|
||||
while (*p != '\0' && (quoted ? (*p != '"') : !IsSpace(*p))) {
|
||||
if (!emit(*p++))
|
||||
return false;
|
||||
++n;
|
||||
}
|
||||
return n > 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Value(const char *name, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
const char *p = FindValue(name);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
|
||||
size_t n = 0;
|
||||
const bool ok = ReadToken(p, [&](char c) {
|
||||
if (n + 1 >= outSize)
|
||||
return false;
|
||||
out[n++] = c;
|
||||
return true;
|
||||
});
|
||||
out[n] = '\0';
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool Value(const char *name, std::string &out) {
|
||||
out.clear();
|
||||
const char *p = FindValue(name);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
return ReadToken(p, [&](char c) {
|
||||
out.push_back(c);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Config::CommandLine
|
||||
@@ -0,0 +1,42 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
// Launch-switch parsing, shared by every module that reads one.
|
||||
//
|
||||
// A switch is written `-name value` or `-name=value`, and the `/name` form
|
||||
// Windows tools also accept is honored. The value may be double-quoted so it can
|
||||
// contain spaces, which a Lua snippet passed on the command line always will.
|
||||
// The name is matched case-insensitively and only at a token boundary, so a path
|
||||
// that happens to contain it cannot be mistaken for the switch itself.
|
||||
//
|
||||
// One parser for all of them on purpose: a second copy would drift, and the
|
||||
// quoting and boundary rules are exactly where that drift would go unnoticed.
|
||||
|
||||
namespace Config::CommandLine {
|
||||
|
||||
// Copies the value of `-name` into `out`. False when the switch is absent, its
|
||||
// value is empty, or the value does not fit -- a value too long to hold is a
|
||||
// failure rather than a silent truncation, since a half-copied filename or
|
||||
// snippet would be acted on as though it were complete.
|
||||
bool Value(const char *name, char *out, size_t outSize);
|
||||
|
||||
// Same, bounded only by the command line itself. For values with no natural
|
||||
// length, such as a script body.
|
||||
bool Value(const char *name, std::string &out);
|
||||
|
||||
} // namespace Config::CommandLine
|
||||
@@ -0,0 +1,68 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
// `-config <name>` — a launch switch that swaps which settings file the client
|
||||
// uses, so one install can hold several profiles.
|
||||
//
|
||||
// The engine already names the config file through a single pointer: boot passes
|
||||
// `"Config.wtf"` to the config initializer, which keeps it and uses it to both
|
||||
// read the file at startup and write it at exit. Supplying a different string is
|
||||
// therefore the whole feature — no second code path, and no way for reading and
|
||||
// writing to disagree. See `Offsets::PATCH_CONFIG_FILENAME_PTR`.
|
||||
|
||||
#include "FileSwitch.h"
|
||||
|
||||
#include "CommandLine.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Config::FileSwitch {
|
||||
|
||||
namespace {
|
||||
|
||||
// The engine holds this pointer for the life of the process and reads through it
|
||||
// again at exit to save, so the storage has to outlive everything — hence a
|
||||
// file-static buffer rather than anything owned by a scope.
|
||||
char g_configName[64];
|
||||
|
||||
// A filename is required to be a plain name. The engine builds the save path as
|
||||
// `WTF\` + this, and a subdirectory that does not exist makes the save fail
|
||||
// silently, which would throw away a session's settings with nothing to show for
|
||||
// it. Refusing the switch is the loud failure that a lost config file is not.
|
||||
bool IsPlainFilename(const char *name) {
|
||||
if (*name == '\0')
|
||||
return false;
|
||||
for (const char *p = name; *p != '\0'; ++p) {
|
||||
if (*p == '\\' || *p == '/' || *p == ':')
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Apply() {
|
||||
if (!Config::CommandLine::Value("config", g_configName, sizeof g_configName))
|
||||
return;
|
||||
if (!IsPlainFilename(g_configName))
|
||||
return;
|
||||
|
||||
void *target = reinterpret_cast<void *>(Offsets::PATCH_CONFIG_FILENAME_PTR);
|
||||
DWORD previous = 0;
|
||||
if (!::VirtualProtect(target, sizeof(const char *), PAGE_EXECUTE_READWRITE, &previous))
|
||||
return;
|
||||
*reinterpret_cast<const char **>(target) = g_configName;
|
||||
::VirtualProtect(target, sizeof(const char *), previous, &previous);
|
||||
}
|
||||
|
||||
} // namespace Config::FileSwitch
|
||||
@@ -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
|
||||
|
||||
// `-config <name>` — makes the client read and write `WTF\<name>` in place of
|
||||
// `WTF\Config.wtf`, so several settings profiles can live side by side.
|
||||
namespace Config::FileSwitch {
|
||||
|
||||
// Reads the switch from the process command line and, when present, points the
|
||||
// engine's config filename at it.
|
||||
//
|
||||
// MUST be called from DllMain, and is the one thing in this project that is.
|
||||
// The engine loads the config during boot, several calls before the point where
|
||||
// VanillaFixes invokes our `Load` export, so by the time the normal
|
||||
// initialization path runs the file has already been read — see
|
||||
// `Offsets::PATCH_CONFIG_FILENAME_PTR` for the full call ordering. DllMain is
|
||||
// early enough because VanillaFixes injects every DLL while the game process is
|
||||
// still suspended and resumes it only afterwards.
|
||||
//
|
||||
// This does not install a hook. It is a single 4-byte store into an immediate
|
||||
// operand, so it costs none of what makes hooking from DllMain a problem: no
|
||||
// trampoline, no thread suspension, no loader-lock work, and nothing for another
|
||||
// DLL's hook to collide with.
|
||||
void Apply();
|
||||
|
||||
} // namespace Config::FileSwitch
|
||||
@@ -0,0 +1,170 @@
|
||||
// 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 from the launch command line:
|
||||
//
|
||||
// -gluescript <code> run at the glue screens
|
||||
// -gluescriptFile <path> the file's contents, run at the glue screens
|
||||
// -gamescript <code> run once in-game
|
||||
// -gamescriptFile <path> the file's contents, run once in-game
|
||||
//
|
||||
// None of these exist in 1.12: the strings do not appear anywhere in the
|
||||
// binary, so there is nothing to parse them and nothing to un-gate. The whole
|
||||
// feature is therefore ours -- read the command line, and hand the source to
|
||||
// the engine's own compile-and-run funnel at the right moment.
|
||||
//
|
||||
// WHEN. Registration time is too early for either state. The in-game module
|
||||
// callback runs from FUN_LOAD_SCRIPT_FUNCTIONS, before FrameXML is loaded, and
|
||||
// the glue callback runs from FUN_LOAD_GLUE_SCRIPT_FUNCTIONS, before GlueXML
|
||||
// is. A script that ran there could not touch a frame, call a FrameXML
|
||||
// function, or see an addon, which is most of what anyone would write one for.
|
||||
// So each script is armed at registration and run from a tick, where the UI is
|
||||
// demonstrably up:
|
||||
//
|
||||
// glue -> Tick::FrameTick, whose hook is the UI render root and so fires at
|
||||
// the login and character-select screens. Armed by the glue module
|
||||
// callback, which runs once per glue boot -- initial launch and every
|
||||
// return from the world on logout -- so the glue script runs each
|
||||
// time the glue screens come up, matching what "run at the glue
|
||||
// screens" says. A script that logs in automatically therefore keeps
|
||||
// working after a logout, which is the point of it.
|
||||
// game -> Tick::WorldTick, which fires only in the world, so its first tick
|
||||
// is past FrameXML, past the addon load pass, and past enter-world
|
||||
// init. Latched for the life of the process, never re-armed on
|
||||
// /reload or on a second login, because "run once in-game" says once.
|
||||
//
|
||||
// HOW. FUN_LUA_RUN_STRING resolves the Lua state itself, so one call reaches
|
||||
// whichever state is current and the two paths differ only in when they fire.
|
||||
// It also installs the FrameScript error handler as the pcall's errfunc, so a
|
||||
// faulty script reports itself the way any script error does instead of failing
|
||||
// silently. Because every chunk goes through the shared compile chokepoint,
|
||||
// these scripts get the same 5.1 syntax support the rest of the client's Lua
|
||||
// has.
|
||||
//
|
||||
// A `*File` is read with Win32 rather than through the engine's file layer, on
|
||||
// purpose. The engine resolves relative paths against a loose-file index built
|
||||
// once at boot, so it cannot see a file created afterwards and cannot open an
|
||||
// absolute path at all -- and an absolute path is exactly what someone puts in
|
||||
// a shortcut. Reading it ourselves also means both switches share one execution
|
||||
// path, so inline code and file contents cannot diverge in behaviour.
|
||||
|
||||
#include "CommandLine.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "Game.h"
|
||||
#include "tick/FrameTick.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace Config::LaunchScript {
|
||||
|
||||
namespace {
|
||||
|
||||
using RunString_t = int(__fastcall *)(const char *source, uint32_t size, const char *chunkName,
|
||||
void *errCtx);
|
||||
|
||||
// A chunk name carries a leading `=` or `@` to tell Lua how to present it in an
|
||||
// error. That marker is for Lua, not for a reader, so the log drops it.
|
||||
const char *Displayed(const std::string &chunkName) {
|
||||
const char *s = chunkName.c_str();
|
||||
return (*s == '=' || *s == '@') ? s + 1 : s;
|
||||
}
|
||||
|
||||
void Run(const std::string &source, const std::string &chunkName) {
|
||||
if (source.empty())
|
||||
return;
|
||||
const int ok = reinterpret_cast<RunString_t>(Offsets::FUN_LUA_RUN_STRING)(
|
||||
source.c_str(), static_cast<uint32_t>(source.size()), chunkName.c_str(), nullptr);
|
||||
// The engine's error handler has already reported the detail to the user by
|
||||
// now; this only records that the switch was the source of it.
|
||||
if (ok == 0)
|
||||
Debug::Log::Printf("[launchscript] %s did not run to completion", Displayed(chunkName));
|
||||
}
|
||||
|
||||
// Whole-file read. Refuses a file larger than the compiler's own size parameter
|
||||
// can describe, which is the engine's bound rather than one of ours.
|
||||
bool ReadWholeFile(const char *path, std::string &out) {
|
||||
out.clear();
|
||||
const HANDLE file = ::CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE)
|
||||
return false;
|
||||
|
||||
bool ok = false;
|
||||
LARGE_INTEGER size{};
|
||||
if (::GetFileSizeEx(file, &size) != 0 && size.QuadPart >= 0 &&
|
||||
size.QuadPart <= static_cast<long long>(0xFFFFFFFFu)) {
|
||||
if (size.QuadPart == 0) {
|
||||
ok = true; // an empty file is not a failure, just nothing to run
|
||||
} else {
|
||||
out.assign(static_cast<size_t>(size.QuadPart), '\0');
|
||||
DWORD read = 0;
|
||||
ok = ::ReadFile(file, &out[0], static_cast<DWORD>(out.size()), &read, nullptr) != 0 &&
|
||||
read == out.size();
|
||||
}
|
||||
}
|
||||
::CloseHandle(file);
|
||||
if (!ok)
|
||||
out.clear();
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Runs the inline switch and then the file switch, so a launch that passes both
|
||||
// gets both, in the order they are written above.
|
||||
void RunPair(const char *codeSwitch, const char *fileSwitch) {
|
||||
std::string code;
|
||||
if (Config::CommandLine::Value(codeSwitch, code))
|
||||
Run(code, std::string("=-") + codeSwitch); // `=` shows the switch, not the source
|
||||
|
||||
std::string path;
|
||||
if (!Config::CommandLine::Value(fileSwitch, path))
|
||||
return;
|
||||
std::string contents;
|
||||
if (!ReadWholeFile(path.c_str(), contents)) {
|
||||
Debug::Log::Printf("[launchscript] -%s could not read '%s'", fileSwitch, path.c_str());
|
||||
return;
|
||||
}
|
||||
Run(contents, "@" + path); // `@` shows the path, so errors carry line numbers
|
||||
}
|
||||
|
||||
bool g_glueArmed = false;
|
||||
bool g_gameRan = false;
|
||||
|
||||
void OnFrameTick() {
|
||||
if (!g_glueArmed)
|
||||
return;
|
||||
g_glueArmed = false;
|
||||
RunPair("gluescript", "gluescriptFile");
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
if (g_gameRan)
|
||||
return;
|
||||
g_gameRan = true;
|
||||
RunPair("gamescript", "gamescriptFile");
|
||||
}
|
||||
|
||||
void ArmGlue() { g_glueArmed = true; }
|
||||
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&ArmGlue};
|
||||
const Tick::FrameTick::AutoSubscribe _frameTick{&OnFrameTick};
|
||||
const Tick::WorldTick::AutoSubscribe _worldTick{&OnWorldTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Config::LaunchScript
|
||||
@@ -0,0 +1,197 @@
|
||||
// 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/>.
|
||||
|
||||
// `ConsoleGetAllCommands()` — every console command and console variable the
|
||||
// client currently has, as `ConsoleCommandInfo` tables.
|
||||
//
|
||||
// A plain global, not a `C_Console` member: the wiki files this under
|
||||
// `C_Console.GetAllCommands`, but on a live client that namespace does not
|
||||
// exist and `ConsoleGetAllCommands` is the real name.
|
||||
//
|
||||
// The engine keeps them in one registry, filled by
|
||||
// FUN_CONSOLE_COMMAND_REGISTER: a Storm intrusive list for iteration plus a
|
||||
// by-name hash for dispatch. This walks the list the same way the `help`
|
||||
// command does, which is the only other thing that enumerates it.
|
||||
//
|
||||
// ONE LIST, NOT TWO. Commands and CVars do not live separately. The CVar
|
||||
// registrar's last act is to register the CVar as a console command as well —
|
||||
// `FUN_CONSOLE_COMMAND_REGISTER(cvar->name, <shared handler>, categoryId,
|
||||
// help)` — which is what makes `/console <cvarName> <value>` work. So the
|
||||
// command list already holds every CVar, and walking the CVar list too (the one
|
||||
// `cvarlist` and the config saver use) would report each one twice.
|
||||
//
|
||||
// That also gives an exact `commandType`: every CVar shares one handler
|
||||
// address, so a node pointing at it is a CVar and anything else is a command.
|
||||
// It is a pointer comparison against a value the registrar itself writes, not a
|
||||
// name or category heuristic.
|
||||
//
|
||||
// FIELDS WE CANNOT FILL. `scriptContents` and `scriptParameters` describe
|
||||
// console macros and scripts, which this client has no notion of — there is no
|
||||
// registration path that could produce one. They are always the empty string,
|
||||
// so a caller reading them gets a string rather than a nil surprise. `category`
|
||||
// is the engine's own number, and the engine's table (debug, graphics, console,
|
||||
// combat, game, default, net, sound, gm) is Enum.ConsoleCategory's 0..8 exactly,
|
||||
// so it needs no translation.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Console::Commands {
|
||||
|
||||
namespace {
|
||||
|
||||
// Enum.ConsoleCommandType. Macro (2) and Script (3) exist in the contract but
|
||||
// cannot occur here.
|
||||
constexpr int kCommandTypeCvar = 0;
|
||||
constexpr int kCommandTypeCommand = 1;
|
||||
|
||||
using ConsoleWrite_t = void(__fastcall *)(const char *line, int colorType);
|
||||
|
||||
// A list node, or 0 once the walk reaches the end. The low bit tags the
|
||||
// sentinel, the same convention the addon registry and the parser lists use.
|
||||
bool IsNode(uintptr_t node) { return node != 0 && (node & 1) == 0; }
|
||||
|
||||
char Lower(char c) { return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c; }
|
||||
|
||||
bool StartsWithIgnoreCase(const char *name, const char *prefix) {
|
||||
for (size_t i = 0; prefix[i] != '\0'; ++i) {
|
||||
if (name[i] == '\0' || Lower(name[i]) != Lower(prefix[i]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetStringField(void *L, const char *key, const char *value) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::PushString(L, value != nullptr ? value : "");
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
void SetIntField(void *L, const char *key, int value) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(value));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// `ConsoleGetAllCommands() -> ConsoleCommandInfo[]`
|
||||
int __fastcall Script_GetAllCommands(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const uintptr_t linkOffset =
|
||||
Game::Read<uint32_t>(Offsets::VAR_CONSOLE_COMMAND_LINK_OFFSET);
|
||||
uintptr_t node = Game::Read<uint32_t>(Offsets::VAR_CONSOLE_COMMAND_LIST_HEAD);
|
||||
|
||||
int count = 0;
|
||||
while (IsNode(node)) {
|
||||
const auto *name =
|
||||
Game::Read<const char *>(node + Offsets::OFF_CONSOLE_COMMAND_NAME);
|
||||
const auto *help =
|
||||
Game::Read<const char *>(node + Offsets::OFF_CONSOLE_COMMAND_HELP);
|
||||
const uintptr_t handler =
|
||||
Game::Read<uint32_t>(node + Offsets::OFF_CONSOLE_COMMAND_HANDLER);
|
||||
const int category =
|
||||
Game::Read<int32_t>(node + Offsets::OFF_CONSOLE_COMMAND_CATEGORY);
|
||||
|
||||
// Step before building the entry: the walk stays correct even if a
|
||||
// field read below is skipped.
|
||||
node = Game::Read<uint32_t>(node + linkOffset + 4);
|
||||
|
||||
if (name == nullptr)
|
||||
continue; // a node the registrar never finished; nothing to report
|
||||
|
||||
Game::Lua::PushNumber(L, ++count);
|
||||
Game::Lua::NewTable(L);
|
||||
SetStringField(L, "command", name);
|
||||
SetStringField(L, "help", help);
|
||||
SetIntField(L, "category", category);
|
||||
SetIntField(L, "commandType",
|
||||
handler == Offsets::FUN_CONSOLE_CVAR_COMMAND_HANDLER
|
||||
? kCommandTypeCvar
|
||||
: kCommandTypeCommand);
|
||||
SetStringField(L, "scriptContents", "");
|
||||
SetStringField(L, "scriptParameters", "");
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Both enums are Blizzard's, stated in full rather than derived from the
|
||||
// engine's own category table. They are an API contract, not a description of
|
||||
// what this client happens to have: an addon asking for a value we can never
|
||||
// return still deserves the number instead of a nil. `Macro` and `Script` have
|
||||
// no registration path here, and `Reveal` / `None` are not among the engine's
|
||||
// nine categories -- everything else lines up, which is the point.
|
||||
// `ConsolePrintAllMatchingCommands(prefix)` — write every command whose name
|
||||
// starts with `prefix` to the console, the way its own tab completion lists
|
||||
// candidates. Case-insensitive, matching how the engine resolves a command
|
||||
// name.
|
||||
//
|
||||
// Prints nothing for an empty prefix rather than dumping all 500-odd entries;
|
||||
// ConsoleGetAllCommands is the way to ask for everything.
|
||||
int __fastcall Script_ConsolePrintAllMatchingCommands(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
return 0;
|
||||
const char *prefix = Game::Lua::ToString(L, 1);
|
||||
if (prefix == nullptr || prefix[0] == '\0')
|
||||
return 0;
|
||||
|
||||
auto write = reinterpret_cast<ConsoleWrite_t>(Offsets::FUN_CONSOLE_WRITE);
|
||||
const uintptr_t linkOffset =
|
||||
Game::Read<uint32_t>(Offsets::VAR_CONSOLE_COMMAND_LINK_OFFSET);
|
||||
uintptr_t node = Game::Read<uint32_t>(Offsets::VAR_CONSOLE_COMMAND_LIST_HEAD);
|
||||
while (IsNode(node)) {
|
||||
const auto *name =
|
||||
Game::Read<const char *>(node + Offsets::OFF_CONSOLE_COMMAND_NAME);
|
||||
node = Game::Read<uint32_t>(node + linkOffset + 4);
|
||||
if (name != nullptr && StartsWithIgnoreCase(name, prefix))
|
||||
write(name, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kCommandTypeEntries[] = {
|
||||
{"Cvar", kCommandTypeCvar},
|
||||
{"Command", kCommandTypeCommand},
|
||||
{"Macro", 2},
|
||||
{"Script", 3},
|
||||
};
|
||||
|
||||
// The engine's table at VAR_CONSOLE_CATEGORY_TABLE holds ids 0..8 named debug,
|
||||
// graphics, console, combat, game, default, net, sound, gm, in that order --
|
||||
// so a `category` read off a command node is already one of these.
|
||||
const Game::Lua::EnumIntegerEntry kCategoryEntries[] = {
|
||||
{"Debug", 0}, {"Graphics", 1}, {"Console", 2}, {"Combat", 3},
|
||||
{"Game", 4}, {"Default", 5}, {"Net", 6}, {"Sound", 7},
|
||||
{"Gm", 8}, {"Reveal", 9}, {"None", 10},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetAllCommands", &Script_GetAllCommands);
|
||||
Game::Lua::RegisterGlobalFunction("ConsolePrintAllMatchingCommands",
|
||||
&Script_ConsolePrintAllMatchingCommands);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCommandType", kCommandTypeEntries,
|
||||
sizeof(kCommandTypeEntries) /
|
||||
sizeof(kCommandTypeEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "ConsoleCategory", kCategoryEntries,
|
||||
sizeof(kCategoryEntries) /
|
||||
sizeof(kCategoryEntries[0]));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Console::Commands
|
||||
@@ -0,0 +1,240 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The developer console, as Lua functions: running commands, writing lines,
|
||||
// and reading the console's own state and appearance.
|
||||
//
|
||||
// `ConsoleExec` is the one with real reach. Nothing in this client's Lua could
|
||||
// run a console command before, so every entry ConsoleGetAllCommands returns
|
||||
// was visible and unreachable at once. It is the engine's own executor, the
|
||||
// same one that replays each `SET` line of Config.wtf at boot.
|
||||
//
|
||||
// THAT INCLUDES `set`, AND `set` IS NOT SetCVar. The console's set handler
|
||||
// looks a cvar up with the unfiltered lookup and performs no read-only check,
|
||||
// while Script_SetCVar tests flag bit 0x4 first and refuses. So
|
||||
// `ConsoleExec("set <name> <value>")` can write a cvar that
|
||||
// `SetCVar` rejects, and can reach cvars no Lua getter can see (Offsets.h at
|
||||
// CVAR_FLAG_READ_ONLY has the mechanism). That is inherent to exposing the
|
||||
// console rather than something added here -- an addon already runs arbitrary
|
||||
// Lua -- but it does mean the read-only guarantee C_CVar.GetCVarInfo reports
|
||||
// stops being absolute once this exists. Deliberate: a console you cannot run
|
||||
// commands through is not a console.
|
||||
//
|
||||
// Everything else is state the console module already keeps. The colours are
|
||||
// its own nine-entry table, the font height its own global, and the toggle key
|
||||
// its own byte, so each is read or written where the engine reads or writes it
|
||||
// rather than being modelled again here.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Console::Shell {
|
||||
|
||||
namespace {
|
||||
|
||||
using ConsoleExec_t = void(__fastcall *)(const char *line, int addToHistory);
|
||||
using ConsoleWrite_t = void(__fastcall *)(const char *line, int colorType);
|
||||
using ConsoleSetKey_t = void(__fastcall *)(int keyCode);
|
||||
|
||||
// `ConsoleExec(command)` — run a console command line.
|
||||
//
|
||||
// Passes addToHistory = 1 so a command run from Lua appears in the console's
|
||||
// own history, which is what someone stepping through the console afterwards
|
||||
// expects to find.
|
||||
int __fastcall Script_ConsoleExec(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
return 0;
|
||||
reinterpret_cast<ConsoleExec_t>(Offsets::FUN_CONSOLE_EXEC)(Game::Lua::ToString(L, 1), 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `ConsoleEcho(message [, colorType])` — write one line to the console.
|
||||
//
|
||||
// The engine no-ops cleanly when the graphics device is not up, so this is safe
|
||||
// to call at any time, and the line is kept whether or not the console is
|
||||
// currently open.
|
||||
int __fastcall Script_ConsoleEcho(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
return 0;
|
||||
int colorType = 0;
|
||||
if (Game::Lua::IsNumber(L, 2)) {
|
||||
colorType = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
if (colorType < 0 || colorType >= Offsets::CONSOLE_COLOR_COUNT)
|
||||
colorType = 0;
|
||||
}
|
||||
reinterpret_cast<ConsoleWrite_t>(Offsets::FUN_CONSOLE_WRITE)(Game::Lua::ToString(L, 1),
|
||||
colorType);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `ConsoleIsActive()` — whether the console is open right now.
|
||||
//
|
||||
// False when the client was started without `-console`, since the toggle key
|
||||
// is gated on that and the console can then never open.
|
||||
int __fastcall Script_ConsoleIsActive(void *L) {
|
||||
const uint32_t enabled = Game::Read<uint32_t>(Offsets::VAR_CONSOLE_ENABLED);
|
||||
const uint32_t visible = Game::Read<uint32_t>(Offsets::VAR_CONSOLE_VISIBLE);
|
||||
Game::Lua::PushBool(L, enabled != 0 && visible != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `ConsoleGetColorFromType(colorType)` -> r, g, b, a
|
||||
//
|
||||
// The colour the console draws that type of line in, as four 0..1 components to
|
||||
// match every other colour in this API. Nil for a type outside the table.
|
||||
int __fastcall Script_ConsoleGetColorFromType(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int type = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (type < 0 || type >= Offsets::CONSOLE_COLOR_COUNT) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const uint32_t argb = *reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_CONSOLE_COLORS + static_cast<uintptr_t>(type) * sizeof(uint32_t));
|
||||
Game::Lua::PushNumber(L, ((argb >> 16) & 0xFF) / 255.0);
|
||||
Game::Lua::PushNumber(L, ((argb >> 8) & 0xFF) / 255.0);
|
||||
Game::Lua::PushNumber(L, (argb & 0xFF) / 255.0);
|
||||
Game::Lua::PushNumber(L, ((argb >> 24) & 0xFF) / 255.0);
|
||||
return 4;
|
||||
}
|
||||
|
||||
// `ConsoleGetFontHeight()` — the console's text height, as a fraction of the
|
||||
// screen height.
|
||||
int __fastcall Script_ConsoleGetFontHeight(void *L) {
|
||||
Game::Lua::PushNumber(L, *reinterpret_cast<const float *>(Offsets::VAR_CONSOLE_FONT_HEIGHT));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// There is deliberately no ConsoleSetFontHeight. Writing the height global
|
||||
// alone changes nothing, since the font was already built at the old size, and
|
||||
// the only thing that rebuilds it is the console's font init — which has no
|
||||
// code callers at all, just one entry in a table at 0x0080E170. It runs once,
|
||||
// during startup, before a single console line exists.
|
||||
//
|
||||
// Re-running it crashes, on a later frame rather than in the call: it frees the
|
||||
// old font handle out from under the cached, font-dependent draw object every
|
||||
// existing line holds at +0x20, and the next console render walks those. The
|
||||
// console's own shutdown frees both together, which is what that pairing is
|
||||
// for. Making a setter safe would mean replaying that teardown across every
|
||||
// line, to change a text size nothing asks to change.
|
||||
|
||||
// `SetConsoleKey(keyCode)` — the key that opens and closes the console.
|
||||
//
|
||||
// Takes the engine's own key code, the value its key events carry, rather than
|
||||
// a key name: the console module stores and compares exactly that, and nothing
|
||||
// in it translates names.
|
||||
int __fastcall Script_SetConsoleKey(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0;
|
||||
reinterpret_cast<ConsoleSetKey_t>(Offsets::FUN_CONSOLE_SET_KEY)(
|
||||
static_cast<int>(Game::Lua::ToNumber(L, 1)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `CalculateStringEditDistance(a, b)` — Levenshtein distance, the number of
|
||||
// single-character insertions, deletions or substitutions between two strings.
|
||||
// The console uses it to suggest a command when one is mistyped.
|
||||
//
|
||||
// Two rolling rows rather than a full matrix, so the cost is bounded by the
|
||||
// shorter string; the longer one only ever drives the loop.
|
||||
int __fastcall Script_CalculateStringEditDistance(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const char *a = Game::Lua::ToString(L, 1);
|
||||
const char *b = Game::Lua::ToString(L, 2);
|
||||
|
||||
size_t lenA = 0, lenB = 0;
|
||||
while (a[lenA] != '\0')
|
||||
++lenA;
|
||||
while (b[lenB] != '\0')
|
||||
++lenB;
|
||||
if (lenA == 0 || lenB == 0) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(lenA + lenB));
|
||||
return 1;
|
||||
}
|
||||
// Index the rows by the shorter string so the buffers stay small whichever
|
||||
// way round the arguments came in.
|
||||
if (lenB > lenA) {
|
||||
const char *t = a;
|
||||
a = b;
|
||||
b = t;
|
||||
const size_t n = lenA;
|
||||
lenA = lenB;
|
||||
lenB = n;
|
||||
}
|
||||
constexpr size_t kMaxRow = 256; // beyond this the answer is of no practical use
|
||||
if (lenB >= kMaxRow) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t prev[kMaxRow], curr[kMaxRow];
|
||||
for (size_t j = 0; j <= lenB; ++j)
|
||||
prev[j] = j;
|
||||
for (size_t i = 1; i <= lenA; ++i) {
|
||||
curr[0] = i;
|
||||
for (size_t j = 1; j <= lenB; ++j) {
|
||||
const size_t cost = (a[i - 1] == b[j - 1]) ? 0u : 1u;
|
||||
size_t best = prev[j] + 1; // deletion
|
||||
if (curr[j - 1] + 1 < best)
|
||||
best = curr[j - 1] + 1; // insertion
|
||||
if (prev[j - 1] + cost < best)
|
||||
best = prev[j - 1] + cost; // substitution
|
||||
curr[j] = best;
|
||||
}
|
||||
for (size_t j = 0; j <= lenB; ++j)
|
||||
prev[j] = curr[j];
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(prev[lenB]));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleExec", &Script_ConsoleExec);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleEcho", &Script_ConsoleEcho);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleIsActive", &Script_ConsoleIsActive);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetColorFromType",
|
||||
&Script_ConsoleGetColorFromType);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetFontHeight", &Script_ConsoleGetFontHeight);
|
||||
Game::Lua::RegisterGlobalFunction("SetConsoleKey", &Script_SetConsoleKey);
|
||||
Game::Lua::RegisterGlobalFunction("CalculateStringEditDistance",
|
||||
&Script_CalculateStringEditDistance);
|
||||
}
|
||||
|
||||
// The console exists before login, so the same surface is registered on the
|
||||
// glue state.
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("ConsoleExec", &Script_ConsoleExec);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleEcho", &Script_ConsoleEcho);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleIsActive", &Script_ConsoleIsActive);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleGetColorFromType",
|
||||
&Script_ConsoleGetColorFromType);
|
||||
Game::Lua::RegisterGlueFunction("ConsoleGetFontHeight", &Script_ConsoleGetFontHeight);
|
||||
Game::Lua::RegisterGlueFunction("SetConsoleKey", &Script_SetConsoleKey);
|
||||
Game::Lua::RegisterGlueFunction("CalculateStringEditDistance",
|
||||
&Script_CalculateStringEditDistance);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
const Game::GlueModuleAutoRegister _glueAutoreg{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Console::Shell
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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.AutoStoreItem(srcBag, srcSlot [, dstBag])` — move an item
|
||||
// and let the SERVER choose the destination slot. A ClassicAPI-only
|
||||
// extension, like its siblings `SwapItems` and `MoveItem`; the modern
|
||||
// API has no equivalent, because it has no need for one.
|
||||
//
|
||||
// The reason to want it: the server does not just find an empty slot.
|
||||
// It first merges the stack into existing non-full stacks of the same
|
||||
// item, splitting it across several of them when that is what fits, and
|
||||
// only places a remainder if any is left over. So this is a one-call
|
||||
// stack consolidator that needs no stack-size lookup — the caller never
|
||||
// has to know what fits. A merge computed on this side has to size
|
||||
// every stack first, and `C_Item.GetItemMaxStackSizeByID` is nil until
|
||||
// that item's data has arrived, so a cold item cache silently skips
|
||||
// exactly the stacks it could not size.
|
||||
//
|
||||
// `dstBag` defaults to 0, which means "anywhere in the main inventory
|
||||
// that it fits" rather than "the backpack" — on the wire the backpack
|
||||
// IS the player container, so the two cannot be distinguished. Pass
|
||||
// 1..4 to confine the item to one equipped bag. Bank destinations are
|
||||
// rejected (see [[item/Swap.h]] `AutoStore`).
|
||||
//
|
||||
// Returns true once the packet is away — like the other two, the send
|
||||
// is fire-and-forget and the server confirms through the normal
|
||||
// BAG_UPDATE / SMSG_INVENTORY_CHANGE_FAILURE flow.
|
||||
|
||||
#include "Game.h"
|
||||
#include "item/Swap.h"
|
||||
|
||||
namespace Container::AutoStoreItem {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_C_Container_AutoStoreItem(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.AutoStoreItem(srcBag, srcSlot [, dstBag])");
|
||||
return 0;
|
||||
}
|
||||
// Read every arg before calling AutoStore — its source lookup goes
|
||||
// through ResolveBag, which stomps the Lua stack via PackBagSlot.
|
||||
const int srcBag = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int srcSlot = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
const int dstBag = Game::Lua::IsNumber(L, 3)
|
||||
? static_cast<int>(Game::Lua::ToNumber(L, 3))
|
||||
: 0;
|
||||
|
||||
const bool ok = Item::Swap::AutoStore(L, srcBag, srcSlot, dstBag);
|
||||
Game::Lua::PushBool(L, ok);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "AutoStoreItem",
|
||||
&Script_C_Container_AutoStoreItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::AutoStoreItem
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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.GetContainerItemEquipmentSetInfo(containerIndex, slotIndex)`
|
||||
// — whether the item in a bag slot belongs to an equipment set, and the
|
||||
// names of the sets it belongs to:
|
||||
//
|
||||
// inSet, setList = C_Container.GetContainerItemEquipmentSetInfo(bag, slot)
|
||||
//
|
||||
// The lookup is by the item's per-instance GUID against the GUIDs
|
||||
// `EquipmentSet::Data` saved into each set, so it names the one physical
|
||||
// item that was saved rather than every copy of that itemID — a spare
|
||||
// of the same item sitting in the next slot reports `false`. The set's
|
||||
// reserved empty / ignored slot markers can never match a real GUID,
|
||||
// and `Data::SetsContainingItem` rejects them outright.
|
||||
//
|
||||
// `setList` is 4.3.4's `GetSetList`: the set names in set order, joined by
|
||||
// PLAYER_LIST_DELIMITER — the shape a caller formats straight into a
|
||||
// tooltip line. It is `nil`
|
||||
// rather than an empty string when the item is in no set, so the usual
|
||||
// `if inSet then … setList … end` guard is all a caller needs.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "equipmentset/Text.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace Container::EquipmentSetInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
uint64_t ItemGuid(const uint8_t *cgItem) {
|
||||
auto *instance = Item::InstanceBlock(cgItem);
|
||||
if (instance == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
instance + Offsets::OFF_INSTANCE_BLOCK_GUID);
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Container_GetContainerItemEquipmentSetInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.GetContainerItemEquipmentSetInfo(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));
|
||||
|
||||
// `ResolveBag` stomps stack slots 1 and 2 through the engine's
|
||||
// `PackBagSlot`, so both arguments are already in locals above and
|
||||
// nothing we need survives on the stack across it.
|
||||
const uint64_t guid =
|
||||
ItemGuid(Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
|
||||
const std::string setList = EquipmentSet::Text::SetList(guid);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (setList.empty()) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Game::Lua::PushBool(L, true);
|
||||
Game::Lua::PushString(L, setList.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Container", "GetContainerItemEquipmentSetInfo",
|
||||
&Script_C_Container_GetContainerItemEquipmentSetInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::EquipmentSetInfo
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Container::FreeSlots {
|
||||
|
||||
@@ -115,6 +116,46 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `C_Container.GetContainerFreeSlots(bagID)` — which slots are empty,
|
||||
// as an array of slot numbers in ascending order, rather than
|
||||
// `GetContainerNumFreeSlots`'s count of them. A full bag returns an
|
||||
// empty table; a bag that isn't there returns nothing, which is how
|
||||
// callers tell "no free slots" from "no such bag".
|
||||
//
|
||||
// `bagID` is 0..4 like the rest of this file (0 = backpack, 1..4 =
|
||||
// equipped bag slots); bank and keyring indices are the "no such bag"
|
||||
// case.
|
||||
int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.GetContainerFreeSlots(bagID)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
if (info.slotCount <= 0)
|
||||
return 0; // nothing — no such bag
|
||||
|
||||
// Collect before building the table. Each `ResolveBag` stomps stack
|
||||
// slots 1 and 2, so the table cannot be on the stack during the walk.
|
||||
std::vector<int> freeSlots;
|
||||
freeSlots.reserve(static_cast<size_t>(info.slotCount));
|
||||
for (int slot = 1; slot <= info.slotCount; ++slot) {
|
||||
if (Item::Location::ResolveBag(L, bagID, slot) == nullptr)
|
||||
freeSlots.push_back(slot);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
for (size_t i = 0; i < freeSlots.size(); ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(freeSlots[i]));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `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
|
||||
@@ -146,6 +187,8 @@ int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
|
||||
&Script_C_Container_GetContainerNumFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerFreeSlots",
|
||||
&Script_C_Container_GetContainerFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
|
||||
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
|
||||
}
|
||||
|
||||
@@ -49,32 +49,20 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Icon.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.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).
|
||||
@@ -137,7 +125,7 @@ int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
if (record != nullptr) {
|
||||
const uint32_t displayInfoID = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
haveIcon = BuildIconPath(displayInfoID, iconPath, sizeof(iconPath));
|
||||
haveIcon = Item::Icon::PathForDisplayInfoID(displayInfoID, iconPath, sizeof(iconPath));
|
||||
quality = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
haveQuality = true;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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.GetContainerItemQuestInfo(containerIndex, slotIndex)` —
|
||||
// the quest state of a bag slot, as one table:
|
||||
//
|
||||
// { isQuestItem = bool, questID = number|nil, isActive = bool }
|
||||
//
|
||||
// This is what draws the two overlays on a bag button: a `questID` the
|
||||
// player has not accepted yet gets the exclamation mark, and either an
|
||||
// accepted `questID` or a plain quest item gets the question-mark
|
||||
// border.
|
||||
//
|
||||
// The three fields come from three places:
|
||||
//
|
||||
// isQuestItem the item's class is Quest — an item that exists to be
|
||||
// a quest objective.
|
||||
// questID the quest the item BEGINS (`m_startQuest` in the
|
||||
// cached item record). Absent, not zero, when the item
|
||||
// starts nothing — callers test `if questInfo.questID`.
|
||||
// isActive that started quest is already in the quest log.
|
||||
//
|
||||
// The table is always returned, even for an empty slot or an item whose
|
||||
// data has not arrived yet, so callers can index it without a nil test.
|
||||
// An uncached item warms the cache, so the next bag update draws the
|
||||
// overlay the first hover could not.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "quest/Log.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Container::QuestInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
// ItemClass.dbc row 12. Only this file cares which class means "quest",
|
||||
// so the code stays local per the single-use rule in the offsets policy.
|
||||
constexpr uint32_t kItemClassQuest = 12;
|
||||
|
||||
int __fastcall Script_C_Container_GetContainerItemQuestInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_Container.GetContainerItemQuestInfo(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));
|
||||
|
||||
// Resolve before building the table: `ResolveBag` stomps stack
|
||||
// slots 1 and 2 through the engine's `PackBagSlot`, so nothing we
|
||||
// want to keep may be on the stack across this call.
|
||||
const int itemID =
|
||||
Item::ID::FromCGItem(Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
|
||||
bool isQuestItem = false;
|
||||
uint32_t startQuest = 0;
|
||||
if (itemID != 0) {
|
||||
auto *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
// Cold cache. Report the empty answer now and fetch, so the
|
||||
// overlay is right on the next bag update.
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
} else {
|
||||
isQuestItem = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CLASS) ==
|
||||
kItemClassQuest;
|
||||
startQuest = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_START_QUEST);
|
||||
}
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldBool(L, "isQuestItem", isQuestItem);
|
||||
if (startQuest > 0) {
|
||||
Game::Lua::SetFieldNumber(L, "questID",
|
||||
static_cast<double>(startQuest));
|
||||
Game::Lua::SetFieldBool(
|
||||
L, "isActive", Quest::Log::IsOnQuest(static_cast<int>(startQuest)));
|
||||
} else {
|
||||
// `questID` stays absent rather than 0 — the field is nilable and
|
||||
// every caller tests it for truth.
|
||||
Game::Lua::SetFieldBool(L, "isActive", false);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemQuestInfo",
|
||||
&Script_C_Container_GetContainerItemQuestInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::QuestInfo
|
||||
@@ -0,0 +1,954 @@
|
||||
// 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.SortBags()` — arrange the player's bags.
|
||||
//
|
||||
// Two phases, because every item move is a server round trip and the
|
||||
// second phase has to read slot contents the first phase changed.
|
||||
//
|
||||
// PHASE ONE consolidates partial stacks, and hands that entirely to the
|
||||
// server: one `CMSG_AUTOSTORE_BAG_ITEM` per redundant stack, which
|
||||
// merges into existing stacks of the same item and splits across several
|
||||
// of them when that is what fits. Nothing here computes what fits, so
|
||||
// nothing here needs an item's max stack size — which matters, because
|
||||
// that number is unavailable until the item's data has arrived from the
|
||||
// server, and a merge that sizes stacks first silently skips the ones it
|
||||
// could not size.
|
||||
//
|
||||
// PHASE TWO places items. It runs on the next `Bag::UpdateDelayed` — the
|
||||
// engine's own "a bag changed this frame" signal — so the merges are
|
||||
// settled before anything is read. It plans the whole layout up front
|
||||
// and then tracks each item's live position as swaps fire, rather than
|
||||
// re-reading a layout it is in the middle of mutating. Our swap is
|
||||
// fire-and-forget and sets no client-side item lock, so the whole batch
|
||||
// goes out in one frame.
|
||||
//
|
||||
// Neither phase may touch the Lua stack: phase two runs from a bag-update
|
||||
// callback, outside any Lua call, where the engine may have its own
|
||||
// values on the stack. Hence the `*From` senders and the direct invMgr
|
||||
// walk rather than `ResolveBag`.
|
||||
//
|
||||
// THE ORDERING IS OURS. Retail computes it in code we cannot read, and
|
||||
// its categories (per-bag assignment flags, expansion filters) have no
|
||||
// counterpart here, so this is a defensible order rather than a
|
||||
// reproduction of Blizzard's: hearthstone, gear, consumables, reagents,
|
||||
// trade goods, quest items, everything else by quality, junk last. Within
|
||||
// a category, by item class, subclass, name, then fuller stacks first.
|
||||
// Gear adds two keys so like pieces sit together: the equip slot right
|
||||
// after the class (kSlotOrder), and quality, best first, before the name.
|
||||
//
|
||||
// An item whose data has NOT arrived is pinned: left where it is, with
|
||||
// its slot withheld from the destination pool so nothing else is planned
|
||||
// into it. It cannot be ranked without a class, quality and name, and
|
||||
// guessing scatters it. A later call places it once the data lands. Phase
|
||||
// one is unaffected, since it needs only the item ID.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "bag/UpdateDelayed.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/Swap.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Container::SortBags {
|
||||
|
||||
namespace {
|
||||
|
||||
// The two sets this can arrange. Both go through the same snapshot,
|
||||
// comparator and placement; they differ only in how phase one merges
|
||||
// (see ConsolidateViaServer / ConsolidateLocally).
|
||||
constexpr int kInventoryBags[] = {0, 1, 2, 3, 4};
|
||||
constexpr int kBankBags[] = {-1, 5, 6, 7, 8, 9, 10};
|
||||
|
||||
// Fill direction, persisted like every other engine setting: the cvar
|
||||
// carries it to Config.wtf and `GetCVar("sortBagsRightToLeft")` reads it,
|
||||
// so it survives a session the way the retail setting does.
|
||||
CVar::Factory::Handle g_rightToLeftCVar = nullptr;
|
||||
|
||||
bool RightToLeft() {
|
||||
return CVar::Factory::GetInt(g_rightToLeftCVar, 0) != 0;
|
||||
}
|
||||
|
||||
// The two containers a sort can be told to leave alone. An equipped bag
|
||||
// is excluded per-bag through its own `DisableAutoSort` slot flag; the
|
||||
// backpack and the main bank container have no such slot, so each gets
|
||||
// its own setting. Persisted as cvars for the same reason the fill
|
||||
// direction is.
|
||||
CVar::Factory::Handle g_backpackAutosortCVar = nullptr;
|
||||
CVar::Factory::Handle g_bankAutosortCVar = nullptr;
|
||||
|
||||
bool BackpackAutosortDisabled() {
|
||||
return CVar::Factory::GetInt(g_backpackAutosortCVar, 0) != 0;
|
||||
}
|
||||
|
||||
bool BankAutosortDisabled() {
|
||||
return CVar::Factory::GetInt(g_bankAutosortCVar, 0) != 0;
|
||||
}
|
||||
|
||||
// Merge packets sent by the most recent phase one, reset per sort.
|
||||
int g_mergePackets = 0;
|
||||
|
||||
// The source slots phase one asked the server to empty. Phase two counts
|
||||
// how many are still occupied when it runs: any at all means it is
|
||||
// planning against a half-merged snapshot, because the server's replies
|
||||
// did not all arrive in the bag update that woke it.
|
||||
struct SourceSlot {
|
||||
int bag, slot;
|
||||
};
|
||||
std::vector<SourceSlot> g_mergeSources;
|
||||
|
||||
// Item.dbc `m_class` values we rank on. Per-module codes, the way
|
||||
// `Item::BagFamily` keeps its own.
|
||||
constexpr uint32_t kClassConsumable = 0;
|
||||
constexpr uint32_t kClassWeapon = 2;
|
||||
constexpr uint32_t kClassArmor = 4;
|
||||
constexpr uint32_t kClassReagent = 5;
|
||||
constexpr uint32_t kClassTradeGoods = 7;
|
||||
constexpr uint32_t kClassQuest = 12;
|
||||
|
||||
constexpr uint32_t kQualityPoor = 0;
|
||||
constexpr uint32_t kQualityUncommon = 2;
|
||||
constexpr uint32_t kQualityRare = 3;
|
||||
constexpr uint32_t kQualityEpic = 4;
|
||||
|
||||
// Primary key. Lower sorts nearer the first slot.
|
||||
enum Category : uint8_t {
|
||||
kCatHearthstone = 0,
|
||||
kCatGear,
|
||||
kCatConsumable,
|
||||
kCatReagent,
|
||||
kCatTradeGoods,
|
||||
kCatQuest,
|
||||
kCatOtherEpic,
|
||||
kCatOtherRare,
|
||||
kCatOtherUncommon,
|
||||
kCatOtherPlain,
|
||||
kCatJunk,
|
||||
};
|
||||
|
||||
uint8_t CategoryFor(uint32_t itemID, uint32_t itemClass, uint32_t quality) {
|
||||
if (itemID == Offsets::HEARTHSTONE_ITEM_ID)
|
||||
return kCatHearthstone;
|
||||
if (quality == kQualityPoor)
|
||||
return kCatJunk;
|
||||
if (itemClass == kClassWeapon || itemClass == kClassArmor)
|
||||
return kCatGear;
|
||||
if (itemClass == kClassConsumable)
|
||||
return kCatConsumable;
|
||||
if (itemClass == kClassReagent)
|
||||
return kCatReagent;
|
||||
if (itemClass == kClassTradeGoods)
|
||||
return kCatTradeGoods;
|
||||
if (itemClass == kClassQuest)
|
||||
return kCatQuest;
|
||||
if (quality >= kQualityEpic)
|
||||
return kCatOtherEpic;
|
||||
if (quality == kQualityRare)
|
||||
return kCatOtherRare;
|
||||
if (quality == kQualityUncommon)
|
||||
return kCatOtherUncommon;
|
||||
return kCatOtherPlain;
|
||||
}
|
||||
|
||||
// Gear's slot key: Item.dbc `m_inventoryType` (INVTYPE_*) in the order its
|
||||
// pieces sort. Not the raw number, which does not follow the character
|
||||
// pane: robe (20) belongs beside chest (5), and cloak (16) beside
|
||||
// shoulder. Not the equipment slot either, which lumps shields, off-hand
|
||||
// weapons and held items into one. The order is Baganator's (its
|
||||
// Sorting/ItemFields.lua): weapons by hand, then ranged and ammo, then
|
||||
// armor head to foot, then jewelry, shirt and tabard.
|
||||
constexpr uint8_t kSlotOrder[] = {
|
||||
17, 13, 21, 14, 23, 26, 22, 15, 25, 24, 27, 28, // weapons, ranged, relic
|
||||
1, 3, 16, 5, 20, 9, 10, 6, 7, 8, // head .. feet
|
||||
2, 11, 12, 4, 19, 18, 0, // jewelry, shirt, tabard, bag, none
|
||||
};
|
||||
|
||||
uint8_t SlotRank(uint32_t inventoryType) {
|
||||
for (uint8_t i = 0; i < sizeof kSlotOrder; ++i)
|
||||
if (kSlotOrder[i] == inventoryType)
|
||||
return i;
|
||||
return static_cast<uint8_t>(sizeof kSlotOrder); // unknown: after all known
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
const void *item; // CGItem — stable across the swap batch
|
||||
int curBag, curSlot; // live position, updated as swaps fire
|
||||
|
||||
// Destination, or unassigned. The unassigned sentinel is destSLOT,
|
||||
// never destBag: slots are 1-based so a negative one is impossible,
|
||||
// whereas bagID -1 is the main bank. Testing destBag < 0 here treats
|
||||
// every item bound for the main bank as unplaced and silently drops
|
||||
// its move.
|
||||
int destBag, destSlot;
|
||||
uint32_t itemID;
|
||||
uint32_t family; // the item's own bag family (0 = fits anywhere)
|
||||
uint32_t itemClass, subClass, quality, count;
|
||||
const char *name;
|
||||
uint8_t category;
|
||||
uint8_t slotRank; // SlotRank(m_inventoryType)
|
||||
};
|
||||
|
||||
struct Cell {
|
||||
int bag, slot;
|
||||
};
|
||||
|
||||
char Lower(char c) {
|
||||
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
|
||||
}
|
||||
|
||||
// A missing name sorts after every present one. Substituting a
|
||||
// placeholder string instead (libbagsort uses "zzz") wedges the unnamed
|
||||
// item in among items actually named with those letters; this keeps it
|
||||
// deterministic and at the end. In practice a nameless item is pinned
|
||||
// before it reaches here, so this is the belt to that braces.
|
||||
int CompareNames(const char *a, const char *b) {
|
||||
if (a == b)
|
||||
return 0;
|
||||
if (a == nullptr)
|
||||
return 1;
|
||||
if (b == nullptr)
|
||||
return -1;
|
||||
for (size_t i = 0;; ++i) {
|
||||
const char ca = Lower(a[i]), cb = Lower(b[i]);
|
||||
if (ca != cb)
|
||||
return ca < cb ? -1 : 1;
|
||||
if (ca == '\0')
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool Precedes(const Entry &a, const Entry &b) {
|
||||
if (a.category != b.category)
|
||||
return a.category < b.category;
|
||||
if (a.itemClass != b.itemClass)
|
||||
return a.itemClass < b.itemClass;
|
||||
const bool gear = a.category == kCatGear;
|
||||
if (gear && a.slotRank != b.slotRank)
|
||||
return a.slotRank < b.slotRank;
|
||||
if (a.subClass != b.subClass)
|
||||
return a.subClass < b.subClass;
|
||||
if (gear && a.quality != b.quality)
|
||||
return a.quality > b.quality; // best first
|
||||
const int byName = CompareNames(a.name, b.name);
|
||||
if (byName != 0)
|
||||
return byName < 0;
|
||||
if (a.count != b.count)
|
||||
return a.count > b.count; // fuller stacks first
|
||||
return a.itemID < b.itemID; // total order, so the result is stable
|
||||
}
|
||||
|
||||
// A bag's storage, addressed without Lua: the invMgr holding its GUID
|
||||
// array, the linear index its first slot sits at, and the family it
|
||||
// accepts. The backpack lives inside the player's own invMgr; bags 1..4
|
||||
// each have one of their own.
|
||||
struct BagView {
|
||||
const uint8_t *invMgr;
|
||||
int firstLinear;
|
||||
int slotCount;
|
||||
uint32_t family;
|
||||
};
|
||||
|
||||
// The backpack and the main bank both live inside the player's own
|
||||
// invMgr, at different linear bases. Equipped bags and bank bags each
|
||||
// have an invMgr of their own, reached through the bag item.
|
||||
bool ViewForBag(int bagID, BagView *out) {
|
||||
if (bagID == 0) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, Offsets::INVMGR_BACKPACK_FIRST_SLOT,
|
||||
Offsets::BACKPACK_NUM_SLOTS, 0};
|
||||
return true;
|
||||
}
|
||||
if (bagID == -1) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT -
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1,
|
||||
0};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve the bag item that owns the container. Bank bags resolve
|
||||
// only once the bank window has been opened this session, which is
|
||||
// the right gate: the server refuses bank moves without it anyway,
|
||||
// so a bank sort attempted with the bank closed finds no cells and
|
||||
// does nothing rather than firing moves that would all be rejected.
|
||||
int equipSlot = 0;
|
||||
if (bagID >= 1 && bagID <= 4)
|
||||
equipSlot = Offsets::INVSLOT_BAG1 + bagID - 1;
|
||||
else if (bagID >= 5 && bagID <= 10)
|
||||
equipSlot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT + 1 + (bagID - 5);
|
||||
else
|
||||
return false;
|
||||
|
||||
auto *bagItem = Item::Location::ResolveEquipmentSlot(equipSlot);
|
||||
if (bagItem == nullptr)
|
||||
return false;
|
||||
auto *bagInv = static_cast<const uint8_t *>(
|
||||
Item::Location::ContainerInventory(bagItem));
|
||||
if (bagInv == nullptr)
|
||||
return false;
|
||||
const int slots = static_cast<int>(
|
||||
Game::Read<uint32_t>(bagInv, Offsets::OFF_INVMGR_SLOT_COUNT));
|
||||
if (slots <= 0)
|
||||
return false;
|
||||
|
||||
// A specialty bag only accepts its own family, and Turtle leaves the
|
||||
// raw field empty on bags — `BitmaskForRecord` derives it from the
|
||||
// container subclass, so a quiver reports a family here rather than
|
||||
// reading as general-purpose. Shared with `C_Item.GetItemFamily`, so
|
||||
// a bag and the items it holds agree.
|
||||
uint32_t family = 0;
|
||||
const int bagItemID = Item::ID::FromCGItem(bagItem);
|
||||
if (bagItemID > 0) {
|
||||
auto *rec = Item::PeekRecord(static_cast<uint32_t>(bagItemID));
|
||||
if (rec != nullptr)
|
||||
family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
}
|
||||
*out = {bagInv, 0, slots, family};
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t *ItemAt(const BagView &v, int slotIndex0) {
|
||||
auto *guids = *reinterpret_cast<const uint64_t *const *>(
|
||||
v.invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr)
|
||||
return nullptr;
|
||||
const uint64_t guid = guids[v.firstLinear + slotIndex0];
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
return static_cast<const uint8_t *>(
|
||||
Object::ByGuid(Offsets::TYPEMASK_ITEM, guid, "SortBags", 0));
|
||||
}
|
||||
|
||||
uint32_t StackCount(const uint8_t *item) {
|
||||
auto *fields = Item::ObjectFields(item);
|
||||
if (fields == nullptr)
|
||||
return 1;
|
||||
const uint32_t n =
|
||||
Game::Read<uint32_t>(fields, Offsets::OFF_DESCRIPTOR_STACK_COUNT);
|
||||
return n == 0 ? 1 : n;
|
||||
}
|
||||
|
||||
// --- phase one: ask the server to consolidate -------------------------------
|
||||
|
||||
// Returns true if any packet went out. Every stack after the first of
|
||||
// its item ID is offered to the server, which merges what fits and
|
||||
// rehomes the rest.
|
||||
//
|
||||
// When the item's record is cached we skip the ones that provably cannot
|
||||
// stack, purely to save a packet. When it is NOT cached we send anyway:
|
||||
// the alternative is silently skipping a merge we could not size, which
|
||||
// is the failure this whole design avoids.
|
||||
// One stack per group of the same item, as gathered for either merge.
|
||||
struct Stack {
|
||||
const uint8_t *item;
|
||||
int bag, slot;
|
||||
uint32_t count;
|
||||
};
|
||||
struct Group {
|
||||
uint32_t itemID;
|
||||
uint32_t maxStack; // 0 when the item's data has not arrived
|
||||
std::vector<Stack> stacks;
|
||||
};
|
||||
|
||||
// Collects every stackable item across `bags`, grouped by item ID.
|
||||
// `maxStack` is 0 for an item whose data has not arrived; the callers
|
||||
// differ in what they do about that.
|
||||
std::vector<Group> GatherGroups(const int *bags, int bagCount) {
|
||||
std::vector<Group> groups;
|
||||
for (int b = 0; b < bagCount; ++b) {
|
||||
const int bag = bags[b];
|
||||
BagView view;
|
||||
if (!ViewForBag(bag, &view))
|
||||
continue;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
const uint8_t *item = ItemAt(view, i);
|
||||
if (item == nullptr)
|
||||
continue;
|
||||
const int itemID = Item::ID::FromCGItem(item);
|
||||
if (itemID <= 0)
|
||||
continue;
|
||||
const uint32_t id = static_cast<uint32_t>(itemID);
|
||||
|
||||
uint32_t maxStack = 0;
|
||||
auto *rec = Item::PeekRecord(id);
|
||||
if (rec != nullptr) {
|
||||
maxStack =
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_ITEMSTATS_STACKABLE);
|
||||
if (maxStack <= 1)
|
||||
continue; // cannot stack at all, so never a merge candidate
|
||||
}
|
||||
|
||||
Group *group = nullptr;
|
||||
for (auto &candidate : groups) {
|
||||
if (candidate.itemID == id) {
|
||||
group = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (group == nullptr) {
|
||||
groups.push_back({id, maxStack, {}});
|
||||
group = &groups.back();
|
||||
}
|
||||
group->stacks.push_back({item, bag, i + 1, StackCount(item)});
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
// Why this is computed here rather than delegated to the server's own
|
||||
// autostore, which can merge a stack into several destinations for one
|
||||
// packet and would otherwise be cheaper:
|
||||
//
|
||||
// `CMSG_AUTOSTORE_BAG_ITEM` names its destination with a container byte,
|
||||
// and the only value the engine's builder will emit for "the player" is
|
||||
// `INVENTORY_SLOT_BAG_0` (255). Server-side that is NOT the search-every-
|
||||
// bag path — that one is `NULL_BAG` (0), which the builder cannot
|
||||
// express. 255 takes the specific-container branch, which searches the
|
||||
// keyring and the backpack and nothing else. So autostore can never
|
||||
// merge a stack in one equipped bag with a stack in another, which is
|
||||
// most of the real cases.
|
||||
//
|
||||
// Addressing both endpoints explicitly costs a packet per transfer
|
||||
// instead of per emptied source — occasionally one more, when a stack
|
||||
// has to be split across several destinations — and works everywhere.
|
||||
|
||||
// Bank consolidation has to be computed here. `CMSG_AUTOSTORE_BANK_ITEM`
|
||||
// always CROSSES the inventory/bank boundary — a bank-sourced item
|
||||
// travels out to the bags — so there is no "find this a fuller stack
|
||||
// inside the bank" packet to delegate to. This is the one place the
|
||||
// protocol forces a client-side merge, and it carries the cost that
|
||||
// comes with that: it needs each item's max stack size, which is
|
||||
// unavailable until that item's data has arrived, so a stack we cannot
|
||||
// size is left alone.
|
||||
//
|
||||
// Two rolling pointers per item: stacks fullest-first, then poured from
|
||||
// the back into the front until they meet.
|
||||
bool Consolidate(const int *bags, int bagCount) {
|
||||
std::vector<Group> groups = GatherGroups(bags, bagCount);
|
||||
|
||||
bool fired = false;
|
||||
for (auto &group : groups) {
|
||||
if (group.stacks.size() < 2)
|
||||
continue;
|
||||
|
||||
// Pack fully, not just enough to free a slot: 9 and 2 with a cap
|
||||
// of 10 become 10 and 1, even though that still occupies two
|
||||
// slots. Topping stacks up is the point of consolidating.
|
||||
//
|
||||
// This settles after one pass and does not re-fire on the next
|
||||
// call, because a maximally packed group has every stack full
|
||||
// except the last — so `space == 0` walks `lo` straight up to
|
||||
// `hi` and nothing is sent.
|
||||
std::sort(group.stacks.begin(), group.stacks.end(),
|
||||
[](const Stack &a, const Stack &b) { return a.count > b.count; });
|
||||
size_t lo = 0, hi = group.stacks.size() - 1;
|
||||
while (lo < hi) {
|
||||
const uint32_t space = group.maxStack - group.stacks[lo].count;
|
||||
if (space == 0) {
|
||||
++lo;
|
||||
continue;
|
||||
}
|
||||
const bool wholeStack = space >= group.stacks[hi].count;
|
||||
const uint32_t moved =
|
||||
wholeStack ? group.stacks[hi].count : space;
|
||||
|
||||
// Choose the packet from OUR model, never from the item's
|
||||
// live stack count. The whole batch goes out in one frame,
|
||||
// so no reply has arrived and every descriptor still holds
|
||||
// its original count while the model has moved on. Asking
|
||||
// the descriptor would send a split for what is really the
|
||||
// last of a stack, and the server rejects a split that would
|
||||
// empty its source ("Couldn't split those items").
|
||||
const bool sent =
|
||||
wholeStack
|
||||
? Item::Swap::ContainersFrom(
|
||||
group.stacks[hi].item, group.stacks[hi].bag,
|
||||
group.stacks[hi].slot, group.stacks[lo].bag,
|
||||
group.stacks[lo].slot)
|
||||
: Item::Swap::MoveCountFrom(
|
||||
group.stacks[hi].item, group.stacks[hi].bag,
|
||||
group.stacks[hi].slot, group.stacks[lo].bag,
|
||||
group.stacks[lo].slot, static_cast<int>(moved));
|
||||
if (sent) {
|
||||
fired = true;
|
||||
++g_mergePackets;
|
||||
// Only a whole-stack transfer empties its source, and
|
||||
// `mergesPending` means "sources that should be empty by
|
||||
// now but are not".
|
||||
if (wholeStack)
|
||||
g_mergeSources.push_back(
|
||||
{group.stacks[hi].bag, group.stacks[hi].slot});
|
||||
}
|
||||
group.stacks[lo].count += moved;
|
||||
group.stacks[hi].count -= moved;
|
||||
if (group.stacks[hi].count == 0)
|
||||
--hi; // that stack is gone
|
||||
else
|
||||
++lo; // this one is full
|
||||
}
|
||||
}
|
||||
return fired;
|
||||
}
|
||||
|
||||
// --- phase two: plan a layout and fire the swaps ----------------------------
|
||||
|
||||
// Counters from the most recent Place(), read back by
|
||||
// `_classicapi_SortBagsStats()`. They exist to tell apart the three ways
|
||||
// a pass can fail to converge: swaps the engine refused to send, items
|
||||
// the planner found no cell for, and a plan that was complete and fully
|
||||
// sent yet still left the bags unsorted (which would mean a swap did
|
||||
// something other than swap).
|
||||
struct Stats {
|
||||
int entries;
|
||||
int cells;
|
||||
int pinned;
|
||||
int planned;
|
||||
int unplanned;
|
||||
int swapsSent;
|
||||
int swapsFailed;
|
||||
int sameItemAvoided;
|
||||
int generalCells; // destination cells in general-purpose containers
|
||||
int specialtyCells; // cells confined to one bag family
|
||||
int familyItems; // items that want a specialty bag
|
||||
int mergesPending; // phase-one sources still occupied at plan time
|
||||
};
|
||||
Stats g_lastRun{};
|
||||
|
||||
void Place(const int *bags, int bagCount) {
|
||||
Stats stats{};
|
||||
std::vector<Entry> items;
|
||||
std::vector<Cell> general;
|
||||
|
||||
// Vanilla items carry at most one family bit, so a flat list keyed by
|
||||
// family is enough — no bitmask intersection needed.
|
||||
struct FamilyPool {
|
||||
uint32_t family;
|
||||
std::vector<Cell> cells;
|
||||
size_t next;
|
||||
};
|
||||
std::vector<FamilyPool> specialty;
|
||||
|
||||
for (int b = 0; b < bagCount; ++b) {
|
||||
const int bag = bags[b];
|
||||
BagView view;
|
||||
if (!ViewForBag(bag, &view))
|
||||
continue;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
const int slot = i + 1;
|
||||
const uint8_t *item = ItemAt(view, i);
|
||||
|
||||
const uint8_t *rec = nullptr;
|
||||
int itemID = 0;
|
||||
if (item != nullptr) {
|
||||
itemID = Item::ID::FromCGItem(item);
|
||||
if (itemID > 0)
|
||||
rec = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (rec == nullptr) {
|
||||
++stats.pinned;
|
||||
continue; // pinned: unrankable, and its slot is withheld
|
||||
}
|
||||
}
|
||||
|
||||
if (view.family == 0) {
|
||||
general.push_back({bag, slot});
|
||||
} else {
|
||||
FamilyPool *pool = nullptr;
|
||||
for (auto &p : specialty) {
|
||||
if (p.family == view.family) {
|
||||
pool = &p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pool == nullptr) {
|
||||
specialty.push_back({view.family, {}, 0});
|
||||
pool = &specialty.back();
|
||||
}
|
||||
pool->cells.push_back({bag, slot});
|
||||
}
|
||||
|
||||
if (item == nullptr)
|
||||
continue; // an empty slot contributes a cell and nothing else
|
||||
|
||||
Entry e{};
|
||||
e.item = item;
|
||||
e.curBag = bag;
|
||||
e.curSlot = slot;
|
||||
e.destBag = -1;
|
||||
e.destSlot = -1;
|
||||
e.itemID = static_cast<uint32_t>(itemID);
|
||||
e.family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
e.itemClass = Game::Read<uint32_t>(rec, Offsets::OFF_ITEMSTATS_CLASS);
|
||||
e.subClass = Game::Read<uint32_t>(rec, Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
e.quality = Game::Read<uint32_t>(rec, Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
e.slotRank = SlotRank(
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE));
|
||||
e.count = StackCount(item);
|
||||
e.name = *reinterpret_cast<const char *const *>(
|
||||
rec + Offsets::OFF_ITEMSTATS_NAME);
|
||||
e.category = CategoryFor(e.itemID, e.itemClass, e.quality);
|
||||
items.push_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
stats.entries = static_cast<int>(items.size());
|
||||
stats.generalCells = static_cast<int>(general.size());
|
||||
for (const auto &p : specialty)
|
||||
stats.specialtyCells += static_cast<int>(p.cells.size());
|
||||
stats.cells = stats.generalCells + stats.specialtyCells;
|
||||
for (const auto &e : items)
|
||||
if (e.family != 0)
|
||||
++stats.familyItems;
|
||||
|
||||
// Any source phase one asked the server to empty that is still
|
||||
// occupied means this snapshot predates some of the merges.
|
||||
for (const auto &src : g_mergeSources) {
|
||||
for (const auto &e : items) {
|
||||
if (e.curBag == src.bag && e.curSlot == src.slot) {
|
||||
++stats.mergesPending;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items.empty()) {
|
||||
g_lastRun = stats;
|
||||
return;
|
||||
}
|
||||
|
||||
std::sort(items.begin(), items.end(), Precedes);
|
||||
|
||||
// Right-to-left is expressed entirely as reversed cell lists: the
|
||||
// passes below still fill from the "front" and the "far end", those
|
||||
// ends have just swapped which physical slot they mean. So the
|
||||
// sorted run starts at the last slot of the last bag and junk
|
||||
// collects at the first slot of the first, both flipping together.
|
||||
if (RightToLeft()) {
|
||||
std::reverse(general.begin(), general.end());
|
||||
for (auto &p : specialty)
|
||||
std::reverse(p.cells.begin(), p.cells.end());
|
||||
}
|
||||
|
||||
// Forward pass for everything but junk: a matching specialty bag
|
||||
// first, overflowing into the general cells. Junk then fills the
|
||||
// general cells from the far end, so it collects away from the rest
|
||||
// and the two passes cannot claim the same cell.
|
||||
size_t genFront = 0;
|
||||
size_t genBack = general.size();
|
||||
for (auto &e : items) {
|
||||
if (e.category == kCatJunk)
|
||||
continue;
|
||||
Cell *cell = nullptr;
|
||||
if (e.family != 0) {
|
||||
for (auto &p : specialty) {
|
||||
if (p.family == e.family && p.next < p.cells.size()) {
|
||||
cell = &p.cells[p.next++];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cell == nullptr && genFront < genBack)
|
||||
cell = &general[genFront++];
|
||||
if (cell == nullptr)
|
||||
continue; // nowhere left to put it; leave it where it is
|
||||
e.destBag = cell->bag;
|
||||
e.destSlot = cell->slot;
|
||||
}
|
||||
for (auto it = items.rbegin(); it != items.rend(); ++it) {
|
||||
if (it->category != kCatJunk)
|
||||
continue;
|
||||
if (genBack <= genFront)
|
||||
break;
|
||||
const Cell &cell = general[--genBack];
|
||||
it->destBag = cell.bag;
|
||||
it->destSlot = cell.slot;
|
||||
}
|
||||
|
||||
// NEVER plan a swap between two stacks of the same item. The server's
|
||||
// swap handler tries a MERGE first and only falls through to a real
|
||||
// swap if the merge is impossible: if the two counts fit one stack it
|
||||
// collapses them into a single slot, and if they do not it
|
||||
// redistributes the counts and leaves both items where they are.
|
||||
// Neither is a swap, so the bookkeeping below would be wrong from
|
||||
// that point on and every later move in the batch would compound it.
|
||||
//
|
||||
// Two stacks of one item are interchangeable, though, so this costs
|
||||
// nothing: within each same-item run, hand each destination cell to
|
||||
// whichever member already occupies it. The run is contiguous because
|
||||
// identical items agree on every key ahead of count.
|
||||
for (size_t i = 0; i < items.size();) {
|
||||
size_t j = i;
|
||||
while (j < items.size() && items[j].itemID == items[i].itemID)
|
||||
++j;
|
||||
for (size_t m = i; m < j; ++m) {
|
||||
for (size_t n = i; n < j; ++n) {
|
||||
if (n == m)
|
||||
continue; // its own destination matching is just "home"
|
||||
if (items[n].destSlot < 0)
|
||||
continue; // unassigned; nothing to trade
|
||||
if (items[n].destBag == items[m].curBag &&
|
||||
items[n].destSlot == items[m].curSlot) {
|
||||
std::swap(items[m].destBag, items[n].destBag);
|
||||
std::swap(items[m].destSlot, items[n].destSlot);
|
||||
++stats.sameItemAvoided;
|
||||
break; // m now targets the slot it is already in
|
||||
}
|
||||
}
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
|
||||
// Fire the batch. Each entry carries its live position, so the plan
|
||||
// stays valid as slots shuffle and nothing re-reads the bags. The
|
||||
// occupant of a destination takes the mover's old slot, which is what
|
||||
// the engine's swap does anyway.
|
||||
for (auto &e : items) {
|
||||
if (e.destSlot < 0) { // see the sentinel note on Entry
|
||||
++stats.unplanned;
|
||||
continue;
|
||||
}
|
||||
++stats.planned;
|
||||
if (e.destBag == e.curBag && e.destSlot == e.curSlot)
|
||||
continue;
|
||||
Entry *occupant = nullptr;
|
||||
for (auto &o : items) {
|
||||
if (&o != &e && o.curBag == e.destBag && o.curSlot == e.destSlot) {
|
||||
occupant = &o;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const int fromBag = e.curBag, fromSlot = e.curSlot;
|
||||
if (!Item::Swap::ContainersFrom(e.item, fromBag, fromSlot, e.destBag,
|
||||
e.destSlot)) {
|
||||
++stats.swapsFailed;
|
||||
continue;
|
||||
}
|
||||
++stats.swapsSent;
|
||||
e.curBag = e.destBag;
|
||||
e.curSlot = e.destSlot;
|
||||
if (occupant != nullptr) {
|
||||
occupant->curBag = fromBag;
|
||||
occupant->curSlot = fromSlot;
|
||||
}
|
||||
}
|
||||
g_lastRun = stats;
|
||||
}
|
||||
|
||||
// --- sequencing -------------------------------------------------------------
|
||||
|
||||
enum Phase { kIdle, kAwaitingMerge };
|
||||
Phase g_phase = kIdle;
|
||||
|
||||
// Which set the in-flight sort is arranging, so the deferred placement
|
||||
// pass knows what to walk. Points at one of the two static lists, so
|
||||
// there is nothing to own or free.
|
||||
const int *g_activeBags = nullptr;
|
||||
int g_activeBagCount = 0;
|
||||
|
||||
void OnBagUpdate() {
|
||||
if (g_phase != kAwaitingMerge)
|
||||
return;
|
||||
// Clear the phase BEFORE placing: the swaps we are about to fire will
|
||||
// signal this callback again next frame, and we must be idle by then
|
||||
// or the sort would restart forever.
|
||||
g_phase = kIdle;
|
||||
Place(g_activeBags, g_activeBagCount);
|
||||
}
|
||||
|
||||
const Bag::UpdateDelayed::AutoSubscribe _bagSub{&OnBagUpdate};
|
||||
|
||||
// Shared entry point. Bags and bank differ only in the list.
|
||||
void StartSort(const int *bags, int bagCount) {
|
||||
if (g_phase != kIdle)
|
||||
return; // a sort is already in flight
|
||||
g_activeBags = bags;
|
||||
g_activeBagCount = bagCount;
|
||||
g_mergePackets = 0;
|
||||
g_mergeSources.clear();
|
||||
if (Consolidate(bags, bagCount))
|
||||
g_phase = kAwaitingMerge; // let the merges land, then place
|
||||
else
|
||||
Place(bags, bagCount); // nothing to merge; the layout reads clean
|
||||
}
|
||||
|
||||
// `C_Container.SortBags()` / `C_Container.SortBankBags()` — no
|
||||
// arguments, no return, matching the modern signatures. Fire-and-forget:
|
||||
// the work spans at least two frames. A call while a sort is already in
|
||||
// flight is ignored rather than queued, so a held-down keybind cannot
|
||||
// stack them up — and neither can sorting bags and bank at once.
|
||||
// Runs a sort over `bags`, dropping `excluded` when its autosort setting
|
||||
// says to leave that container alone. Dropping a bag from the set is all
|
||||
// it takes: the snapshot, the comparator and the placement only ever see
|
||||
// the bags they are handed, so an excluded bag is neither read from nor
|
||||
// written to.
|
||||
void StartSortExcluding(const int *bags, int count, int excluded,
|
||||
bool excludeIt) {
|
||||
if (!excludeIt) {
|
||||
StartSort(bags, count);
|
||||
return;
|
||||
}
|
||||
std::vector<int> kept;
|
||||
kept.reserve(static_cast<size_t>(count));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (bags[i] != excluded)
|
||||
kept.push_back(bags[i]);
|
||||
}
|
||||
if (!kept.empty())
|
||||
StartSort(kept.data(), static_cast<int>(kept.size()));
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Container_SortBags(void * /*L*/) {
|
||||
StartSortExcluding(kInventoryBags,
|
||||
sizeof(kInventoryBags) / sizeof(kInventoryBags[0]),
|
||||
/*excluded*/ 0, BackpackAutosortDisabled());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Container_SortBankBags(void * /*L*/) {
|
||||
StartSortExcluding(kBankBags, sizeof(kBankBags) / sizeof(kBankBags[0]),
|
||||
/*excluded*/ -1, BankAutosortDisabled());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `_classicapi_SortBagsStats()` -> entries, cells, pinned, planned,
|
||||
// unplanned, swapsSent, swapsFailed, sameItemAvoided — from the last
|
||||
// placement pass. Diagnostic only; not part of the API surface.
|
||||
int __fastcall Script_SortBagsStats(void *L) {
|
||||
Game::Lua::PushNumber(L, g_lastRun.entries);
|
||||
Game::Lua::PushNumber(L, g_lastRun.cells);
|
||||
Game::Lua::PushNumber(L, g_lastRun.pinned);
|
||||
Game::Lua::PushNumber(L, g_lastRun.planned);
|
||||
Game::Lua::PushNumber(L, g_lastRun.unplanned);
|
||||
Game::Lua::PushNumber(L, g_lastRun.swapsSent);
|
||||
Game::Lua::PushNumber(L, g_lastRun.swapsFailed);
|
||||
Game::Lua::PushNumber(L, g_lastRun.sameItemAvoided);
|
||||
Game::Lua::PushNumber(L, g_lastRun.generalCells);
|
||||
Game::Lua::PushNumber(L, g_lastRun.specialtyCells);
|
||||
Game::Lua::PushNumber(L, g_lastRun.familyItems);
|
||||
Game::Lua::PushNumber(L, g_mergePackets);
|
||||
Game::Lua::PushNumber(L, g_lastRun.mergesPending);
|
||||
return 13;
|
||||
}
|
||||
|
||||
// `_classicapi_SortBagsBags(which)` -> bagID, slotCount, family for each
|
||||
// bag that resolves in that set (0 = the inventory set, 1 = the bank
|
||||
// set). A bag that does not resolve is skipped entirely, so a missing ID
|
||||
// in the output is itself the answer. Diagnostic only.
|
||||
int __fastcall Script_SortBagsBags(void *L) {
|
||||
const bool bank = Game::Lua::IsNumber(L, 1) &&
|
||||
static_cast<int>(Game::Lua::ToNumber(L, 1)) == 1;
|
||||
const int *bags = bank ? kBankBags : kInventoryBags;
|
||||
const int count = bank
|
||||
? sizeof(kBankBags) / sizeof(kBankBags[0])
|
||||
: sizeof(kInventoryBags) / sizeof(kInventoryBags[0]);
|
||||
int pushed = 0;
|
||||
for (int b = 0; b < count; ++b) {
|
||||
BagView view;
|
||||
if (!ViewForBag(bags[b], &view))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, bags[b]);
|
||||
Game::Lua::PushNumber(L, view.slotCount);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(view.family));
|
||||
pushed += 3;
|
||||
}
|
||||
return pushed;
|
||||
}
|
||||
|
||||
// `C_Container.SetSortBagsRightToLeft(enable)` — no return, matching the
|
||||
// modern signature. Takes effect on the next sort; it does not re-sort.
|
||||
int __fastcall Script_SetSortBagsRightToLeft(void *L) {
|
||||
CVar::Factory::SetInt(g_rightToLeftCVar,
|
||||
Game::Lua::ToBoolean(L, 1) != 0 ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `C_Container.GetSortBagsRightToLeft()` -> isEnabled
|
||||
int __fastcall Script_GetSortBagsRightToLeft(void *L) {
|
||||
Game::Lua::PushBool(L, RightToLeft());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Container.SetBackpackAutosortDisabled(disable)` /
|
||||
// `C_Container.SetBankAutosortDisabled(disable)` — keep that container
|
||||
// out of `SortBags` / `SortBankBags`. No return, matching the modern
|
||||
// signatures; the paired getters report the stored setting.
|
||||
int __fastcall Script_SetBackpackAutosortDisabled(void *L) {
|
||||
CVar::Factory::SetInt(g_backpackAutosortCVar,
|
||||
Game::Lua::ToBoolean(L, 1) != 0 ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetBackpackAutosortDisabled(void *L) {
|
||||
Game::Lua::PushBool(L, BackpackAutosortDisabled());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetBankAutosortDisabled(void *L) {
|
||||
CVar::Factory::SetInt(g_bankAutosortCVar,
|
||||
Game::Lua::ToBoolean(L, 1) != 0 ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetBankAutosortDisabled(void *L) {
|
||||
Game::Lua::PushBool(L, BankAutosortDisabled());
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
// Dedups by name, so re-registering each reload is harmless.
|
||||
g_rightToLeftCVar = CVar::Factory::Register("sortBagsRightToLeft", "0");
|
||||
g_backpackAutosortCVar =
|
||||
CVar::Factory::Register("backpackAutosortDisabled", "0");
|
||||
g_bankAutosortCVar = CVar::Factory::Register("bankAutosortDisabled", "0");
|
||||
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SortBags",
|
||||
&Script_C_Container_SortBags);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SetSortBagsRightToLeft",
|
||||
&Script_SetSortBagsRightToLeft);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetSortBagsRightToLeft",
|
||||
&Script_GetSortBagsRightToLeft);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SortBankBags",
|
||||
&Script_C_Container_SortBankBags);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SetBackpackAutosortDisabled",
|
||||
&Script_SetBackpackAutosortDisabled);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetBackpackAutosortDisabled",
|
||||
&Script_GetBackpackAutosortDisabled);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SetBankAutosortDisabled",
|
||||
&Script_SetBankAutosortDisabled);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetBankAutosortDisabled",
|
||||
&Script_GetBankAutosortDisabled);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_SortBagsStats",
|
||||
&Script_SortBagsStats);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_SortBagsBags",
|
||||
&Script_SortBagsBags);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::SortBags
|
||||
+110
-152
@@ -11,120 +11,99 @@
|
||||
// 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).
|
||||
// The `coroutine` library, as 4.3.4 ships it (stock Lua 5.1's lbaselib.c
|
||||
// co_funcs, opened by luaopen_base in FrameScript_Initialize for the glue AND
|
||||
// the world state). WoW 1.12.1 links the C-level machinery (`lua_resume`,
|
||||
// `lua_yield`, `lua_newthread`, threads as type tag 8) but never registers
|
||||
// the table, so the six functions below are 5.1's, built on the engine's own
|
||||
// 5.0 primitives. Where this build's 5.0 differs from 5.1, the engine's own
|
||||
// predicate is mirrored (see CoStatus / AuxResume).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.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)
|
||||
// lua_State / CallInfo / global_State fields — all single-consumer, local:
|
||||
// L->top @ +0x08, L->l_G @ +0x10, L->ci @ +0x14, L->base_ci @ +0x28;
|
||||
// CallInfo->state @ +0x08, CI_YIELD = 0x10 — the bit lua_yield
|
||||
// (0x006F6860) ORs in and the resume body (0x006F67B0) tests;
|
||||
// G->mainthread @ +0x50 — f_luaopen (0x006F6D70) stores the opening L.
|
||||
static constexpr uintptr_t OFF_L_TOP = 0x08;
|
||||
static constexpr uintptr_t OFF_L_G = 0x10;
|
||||
static constexpr uintptr_t OFF_L_CI = 0x14;
|
||||
static constexpr uintptr_t OFF_L_BASE_CI = 0x28;
|
||||
static constexpr uintptr_t OFF_CI_STATE = 0x08;
|
||||
static constexpr uint32_t CI_YIELD = 0x10;
|
||||
static constexpr uintptr_t OFF_G_MAINTHREAD = 0x50;
|
||||
|
||||
// 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);
|
||||
}
|
||||
// This build's TValue: {tt @ +0, ctx @ +4, value @ +8}, 16 bytes.
|
||||
static constexpr uintptr_t kTValueSize = 0x10;
|
||||
|
||||
// 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_RUN = 0;
|
||||
static constexpr int CO_SUS = 1;
|
||||
static constexpr int CO_NOR = 2; // resumed another coroutine
|
||||
static constexpr int CO_DEAD = 3;
|
||||
|
||||
static const char *const kStatusNames[] = {
|
||||
"running",
|
||||
"suspended",
|
||||
"normal",
|
||||
"dead",
|
||||
};
|
||||
static const char *const kStatusNames[] = {"running", "suspended", "normal", "dead"};
|
||||
// 5.1's `"cannot resume %s coroutine"` with each status name, indexed the
|
||||
// same way (never used for CO_SUS).
|
||||
static const char *const kCannotResume[] = {
|
||||
"cannot resume running coroutine", nullptr,
|
||||
"cannot resume normal coroutine", "cannot resume dead coroutine"};
|
||||
|
||||
// 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".
|
||||
// 5.1's costatus, on this build's 5.0 state. 5.1 asks `lua_status(co) ==
|
||||
// LUA_YIELD`; this build's lua_yield never writes L->status, it sets CI_YIELD
|
||||
// on the current CallInfo, so ask exactly what the engine's resume body asks:
|
||||
// ci == base_ci, empty stack -> dead (returned, or errored and unwound)
|
||||
// ci == base_ci, function set -> suspended (created, never resumed)
|
||||
// ci > base_ci, CI_YIELD -> suspended (paused in yield)
|
||||
// ci > base_ci, no CI_YIELD -> normal (mid-resume of another coroutine)
|
||||
static bool HasFrames(void *co) {
|
||||
return Game::Read<const void *>(co, OFF_L_CI) !=
|
||||
Game::Read<const void *>(co, OFF_L_BASE_CI);
|
||||
}
|
||||
static bool IsYielded(void *co) {
|
||||
return HasFrames(co) &&
|
||||
(Game::Read<uint32_t>(Game::Read<const void *>(co, OFF_L_CI), OFF_CI_STATE) &
|
||||
CI_YIELD) != 0;
|
||||
}
|
||||
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;
|
||||
return CO_RUN;
|
||||
if (!HasFrames(co))
|
||||
return Game::Lua::GetTop(co) == 0 ? CO_DEAD : CO_SUS;
|
||||
return IsYielded(co) ? CO_SUS : CO_NOR;
|
||||
}
|
||||
|
||||
// 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).
|
||||
// 5.1's auxresume. Returns the result count (moved onto L) or -1 with the
|
||||
// error message on top of L.
|
||||
//
|
||||
// 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");
|
||||
// This build's lua_resume returns 1 for BOTH a yield and a caught error (5.1
|
||||
// separates them), so the yield is recognized by IsYielded: a yielded body
|
||||
// is still on co's call stack with CI_YIELD set, an errored one has been
|
||||
// unwound to base_ci (leaving the message on the stack, which CoStatus
|
||||
// alone would mistake for a never-resumed coroutine).
|
||||
static int AuxResume(void *L, void *co, int narg) {
|
||||
const int status = CoStatus(L, co);
|
||||
if (Game::Lua::CheckStack(co, narg) == 0)
|
||||
Game::Lua::Error(L, "too many arguments to resume");
|
||||
if (status != CO_SUS) {
|
||||
Game::Lua::PushString(L, kCannotResume[status]);
|
||||
return -1;
|
||||
}
|
||||
if (Game::Lua::CheckStack(co, nargs) == 0) {
|
||||
Game::Lua::PushString(L, "too many arguments to resume 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) {
|
||||
Game::Lua::XMove(L, co, narg);
|
||||
const int s = Game::Lua::Resume(co, narg);
|
||||
if (s == 0 || IsYielded(co)) {
|
||||
const int nres = Game::Lua::GetTop(co);
|
||||
if (Game::Lua::CheckStack(L, nres) == 0) {
|
||||
Game::Lua::PushString(L, "too many results to resume coroutine");
|
||||
return -1;
|
||||
}
|
||||
if (Game::Lua::CheckStack(L, nres + 1) == 0)
|
||||
Game::Lua::Error(L, "too many results to resume");
|
||||
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);
|
||||
Game::Lua::XMove(co, L, 1); // the error message
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -133,14 +112,10 @@ static int __fastcall Script_Coroutine_Create(void *L) {
|
||||
Game::Lua::ArgError(L, 1, "Lua function expected");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::NewThread(L); // [arg1, NL]
|
||||
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
|
||||
Game::Lua::PushValue(L, 1); // [arg1, NL, fn]
|
||||
Game::Lua::XMove(L, NL, 1); // [arg1, NL]; NL has fn
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -149,106 +124,89 @@ static int __fastcall Script_Coroutine_Yield(void *L) {
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Resume(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_THREAD) {
|
||||
void *co = Game::Lua::Type(L, 1) == Game::Lua::TYPE_THREAD
|
||||
? Game::Lua::ToThread(L, 1)
|
||||
: nullptr;
|
||||
if (co == nullptr) {
|
||||
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);
|
||||
const int r = AuxResume(L, co, Game::Lua::GetTop(L) - 1);
|
||||
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;
|
||||
return 2; // false + error message
|
||||
}
|
||||
// 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;
|
||||
return r + 1; // true + resume's returns
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Status(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_THREAD) {
|
||||
void *co = Game::Lua::Type(L, 1) == Game::Lua::TYPE_THREAD
|
||||
? Game::Lua::ToThread(L, 1)
|
||||
: nullptr;
|
||||
if (co == nullptr) {
|
||||
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.
|
||||
// 5.1's auxwrap: a string error gets the caller's position prefixed
|
||||
// (luaL_where(L, 1)) — which is what LUA_ERROR, really luaL_error, does —
|
||||
// and any other error object is re-raised untouched with the raw lua_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);
|
||||
const int r = AuxResume(L, co, Game::Lua::GetTop(L));
|
||||
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");
|
||||
if (Game::Lua::IsString(L, -1))
|
||||
Game::Lua::Error(L, "%s", Game::Lua::ToString(L, -1));
|
||||
using RawError_t = int(__fastcall *)(void *L);
|
||||
reinterpret_cast<RawError_t>(Offsets::LUA_RAW_ERROR)(L);
|
||||
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
|
||||
Script_Coroutine_Create(L); // leaves the thread on top
|
||||
Game::Lua::PushCClosure(L, &WrapAux, 1); // captures it as upvalue 1
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `coroutine.running()` — Lua 5.1 addition: the running coroutine, or
|
||||
// nil when called from the main thread. The 5.0 C API has no
|
||||
// `lua_pushthread` (also a 5.1 addition), so the thread TValue is
|
||||
// written to the stack raw: this build's TValue is 16 bytes with
|
||||
// {tt @ +0, value @ +8} (see LuaSyntax::StringMethods for the layout
|
||||
// derivation), tag 8 = thread, and the value is the running `lua_State`
|
||||
// itself — exactly what `lua_newthread` stores and what LUA_TO_POINTER
|
||||
// reads back at tvalue[2]. No stack-overflow check needed: every C
|
||||
// function is guaranteed LUA_MINSTACK free slots and this pushes one.
|
||||
// The running thread is trivially GC-alive, so no anchoring either.
|
||||
// Main-thread detection: module callbacks run on the state's main
|
||||
// thread, so the L captured at registration IS the main thread;
|
||||
// coroutine bodies run on their own lua_State.
|
||||
static void *g_mainL = nullptr;
|
||||
static constexpr uintptr_t OFF_L_TOP = 0x8; // L->top (push helpers' [ecx+8])
|
||||
|
||||
// 5.1's corunning: the running coroutine, or nil on the main thread. 5.0's
|
||||
// C API has no lua_pushthread, so the thread is pushed the way the engine's
|
||||
// own pushes write a TValue: {tt = thread, ctx = the current exec context,
|
||||
// value = L}, then top += one slot. Every C function has LUA_MINSTACK free
|
||||
// slots, and the running thread is trivially alive.
|
||||
static int __fastcall Script_Coroutine_Running(void *L) {
|
||||
if (L == g_mainL) {
|
||||
const void *G = Game::Read<const void *>(L, OFF_L_G);
|
||||
if (L == Game::Read<const void *>(G, OFF_G_MAINTHREAD)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
uint8_t *top = Game::Read<uint8_t *>(L, OFF_L_TOP);
|
||||
*reinterpret_cast<int *>(top) = Game::Lua::TYPE_THREAD;
|
||||
*reinterpret_cast<void **>(top + 8) = L;
|
||||
Game::Ref<uint8_t *>(L, OFF_L_TOP) = top + 16;
|
||||
Game::Ref<int>(top, 0) = Game::Lua::TYPE_THREAD;
|
||||
Game::Ref<int>(top, 4) = Game::Read<int>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
Game::Ref<void *>(top, 8) = L;
|
||||
Game::Ref<uint8_t *>(L, OFF_L_TOP) = top + kTValueSize;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
g_mainL = Game::Lua::State(); // the state's main thread (see Running)
|
||||
// Both states: 4.3.4 opens the library from FrameScript_Initialize, which
|
||||
// runs for the glue state (Client.cpp) and the world (GameUI.cpp).
|
||||
static void RegisterFunctions() {
|
||||
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", "running", &Script_Coroutine_Running);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "status", &Script_Coroutine_Status);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "wrap", &Script_Coroutine_Wrap);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "running", &Script_Coroutine_Running);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "yield", &Script_Coroutine_Yield);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterFunctions};
|
||||
static const Game::GlueModuleAutoRegister _autoregGlue{&RegisterFunctions};
|
||||
|
||||
} // namespace Coroutine
|
||||
|
||||
+58
-49
@@ -242,56 +242,15 @@ 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 char *english = nullptr;
|
||||
const char *localized = nullptr;
|
||||
if (!FactionGroupForRace(raceID, &english, &localized))
|
||||
return 0; // unknown race or no named group -> nil
|
||||
|
||||
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 = Game::Read<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 = Game::Read<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 = Game::Read<const uint8_t *>(
|
||||
Offsets::VAR_FACTIONGROUP_RECORDS);
|
||||
const int fgCount =
|
||||
Game::Read<int>(Offsets::VAR_FACTIONGROUP_COUNT);
|
||||
const int locale = Game::Read<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 = Game::Read<int>(
|
||||
row, Offsets::OFF_FACTIONGROUP_BIT);
|
||||
if ((mask & (1u << (bit & 0x1F))) == 0)
|
||||
continue;
|
||||
const char *loc = Game::Read<const char *>(
|
||||
row, Offsets::OFF_FACTIONGROUP_NAMES + locale * 4);
|
||||
if (loc == nullptr || *loc == '\0')
|
||||
continue; // "Player" / "Monster" have empty localized names
|
||||
const char *eng = Game::Read<const char *>(
|
||||
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
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "name", localized);
|
||||
Game::Lua::SetFieldString(L, "groupTag", english);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_CreatureInfo.GetCreatureTypeInfo(creatureTypeID)` — CreatureTypeInfo
|
||||
@@ -368,6 +327,56 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool FactionGroupForRace(int raceID, const char **english, const char **localized) {
|
||||
if (raceID <= 0)
|
||||
return false;
|
||||
|
||||
const uint8_t *crRec = DBC::Record(Offsets::VAR_CHRRACES_RECORDS,
|
||||
Offsets::VAR_CHRRACES_COUNT,
|
||||
static_cast<uint32_t>(raceID));
|
||||
if (crRec == nullptr)
|
||||
return false; // unknown race
|
||||
const int ftId = Game::Read<int>(
|
||||
crRec, Offsets::OFF_CHRRACES_FACTION_TEMPLATE);
|
||||
if (ftId <= 0)
|
||||
return false;
|
||||
|
||||
const uint8_t *ftRec = DBC::Record(Offsets::VAR_FACTIONTEMPLATE_RECORDS,
|
||||
Offsets::VAR_FACTIONTEMPLATE_COUNT,
|
||||
static_cast<uint32_t>(ftId));
|
||||
if (ftRec == nullptr)
|
||||
return false;
|
||||
const uint32_t mask = Game::Read<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 = Game::Read<const uint8_t *>(
|
||||
Offsets::VAR_FACTIONGROUP_RECORDS);
|
||||
const int fgCount =
|
||||
Game::Read<int>(Offsets::VAR_FACTIONGROUP_COUNT);
|
||||
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
if (fgBase == nullptr)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < fgCount; ++i) {
|
||||
const uint8_t *row = fgBase + i * Offsets::FACTIONGROUP_STRIDE;
|
||||
const int bit = Game::Read<int>(
|
||||
row, Offsets::OFF_FACTIONGROUP_BIT);
|
||||
if ((mask & (1u << (bit & 0x1F))) == 0)
|
||||
continue;
|
||||
const char *loc = Game::Read<const char *>(
|
||||
row, Offsets::OFF_FACTIONGROUP_NAMES + locale * 4);
|
||||
if (loc == nullptr || *loc == '\0')
|
||||
continue; // "Player" / "Monster" have empty localized names
|
||||
*english = Game::Read<const char *>(
|
||||
row, Offsets::OFF_FACTIONGROUP_ENGLISH);
|
||||
*localized = loc;
|
||||
return true;
|
||||
}
|
||||
return false; // no named group
|
||||
}
|
||||
|
||||
// Public accessor for Model::DisplayInfo's SetCreature — the same peek
|
||||
// GetCreatureInfoByID uses, reduced to the display-ID field. 0 = uncached.
|
||||
uint32_t DisplayID(uint32_t creatureID) {
|
||||
|
||||
@@ -24,4 +24,16 @@ namespace Creature::Info {
|
||||
// entry-ID → model mapping vanilla has no DBC for.
|
||||
uint32_t DisplayID(uint32_t creatureID);
|
||||
|
||||
// The faction group a player race belongs to, resolved the way
|
||||
// `Script_UnitFactionGroup` does it for a player whose object isn't in the
|
||||
// world (its name-cache branch, which reads the race): ChrRaces -> FactionTemplate
|
||||
// group mask -> the first FactionGroup row whose bit is set and whose
|
||||
// localized name is non-empty (skipping "Player" / "Monster"). On success
|
||||
// `english` is the locale-independent tag ("Alliance") and `localized` the
|
||||
// display name; both point into DBC string storage. False for an unknown or
|
||||
// non-positive race, or a race with no named group. Backs
|
||||
// `C_CreatureInfo.GetFactionInfo` and `UnitFactionGroup("player")` before the
|
||||
// player object spawns.
|
||||
bool FactionGroupForRace(int raceID, const char **english, const char **localized);
|
||||
|
||||
} // namespace Creature::Info
|
||||
|
||||
+21
-25
@@ -15,10 +15,11 @@
|
||||
// money string (e.g. "12<gold> 34<silver> 56<copper>"), a C++ backport of the
|
||||
// retail C function.
|
||||
//
|
||||
// Behaviour derived from 3.3.5's `Script_GetCoinTextureString` (FUN_00510d00 →
|
||||
// formatter FUN_007e7e10): split the copper amount into gold / silver / copper,
|
||||
// emit only the non-zero denominations in that order, space-separated; a zero
|
||||
// amount shows "0" copper. Each denomination is built from a localized
|
||||
// Behaviour mirrors 3.3.5's `Script_GetCoinTextureString` (FUN_00510d00 →
|
||||
// formatter FUN_007e7e10, `GetCurrencyTextureString` in the decomp): the icon
|
||||
// height defaults to 14 and is taken as given otherwise; split the copper
|
||||
// amount into gold / silver / copper, emit only the non-zero denominations in
|
||||
// that order, separated by one space; a zero amount shows "0" copper. Each denomination is built from a localized
|
||||
// GlobalString — `GOLD_AMOUNT_TEXTURE` / `SILVER_AMOUNT_TEXTURE` /
|
||||
// `COPPER_AMOUNT_TEXTURE` — exactly as the engine does (its formatter
|
||||
// `FUN_0076f070` is a C-format over the same GlobalString). Those strings don't
|
||||
@@ -43,38 +44,36 @@ namespace Currency::CoinText {
|
||||
|
||||
namespace {
|
||||
|
||||
// Icon size. Retail defaults fontHeight to 0 = "use the display font's height",
|
||||
// and the inline-texture renderer now honours 0 (it resolves the coin to the
|
||||
// line's font height at draw time), so coins match the surrounding text instead
|
||||
// of a fixed size. Callers may still pass an explicit height.
|
||||
constexpr int kDefaultHeight = 0;
|
||||
// Icon size when the caller passes none — 3.3.5's `int32_t height = 14`. The
|
||||
// inline-texture renderer scales a written size by the line's font against a
|
||||
// 14-unit reference (4.3.4's ParseEmbeddedTexture), so this follows the text.
|
||||
constexpr int kDefaultHeight = 14;
|
||||
|
||||
// Fallbacks byte-identical to the backported GlobalStrings, used only if the
|
||||
// locale layer somehow isn't loaded when this is first called.
|
||||
// Format: count |T sheet : height=0 : width=0 : offsetX=0 : offsetY=0 : texW=4 :
|
||||
// Format: count |T sheet : height : width : offsetX=2 : offsetY=0 : texW=4 :
|
||||
// texH=1 : left : right : top=0 : bottom=1 |t (left:right selects the coin cell).
|
||||
// height/width 0 = size to the font. offsetX stays 0: the inline-texture
|
||||
// renderer's font-relative pad spaces the coin from its digits (the historical
|
||||
// "coin sits on the digit" bug was the emitter's lazy pen read-back, fixed in
|
||||
// the DLL). Never grow a per-coin offset here.
|
||||
// offsetX 2 is retail's (3.3.5 / 4.3.4 GlobalStrings: `UI-GoldIcon:%d:%d:2:0`):
|
||||
// it shifts only the drawn coin, leaving room against the digits before it.
|
||||
constexpr const char *kGoldFallback =
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
constexpr const char *kSilverFallback =
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:1:2:0:1|t";
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:1:2:0:1|t";
|
||||
// The three coin arts occupy IDENTICAL pixels within their sheet cells (x 0-14,
|
||||
// y 1-14 of 16 — confirmed by decoding the DXT3 alpha of the exported
|
||||
// UI-MoneyIcons.blp); only the left:right cell select differs (gold 0:1,
|
||||
// silver 1:2, copper 2:3).
|
||||
constexpr const char *kCopperFallback =
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:2:3:0:1|t";
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:2:3:0:1|t";
|
||||
|
||||
// Formats one denomination from its localized GlobalString and appends it to
|
||||
// `out`. Reads `_G[globalName]` the way retail reads the coin GlobalStrings; the
|
||||
// format consumes (count, iconHeight, iconWidth). No separator between
|
||||
// denominations — the renderer's symmetric per-icon pad already spaces each coin
|
||||
// from the surrounding digits, so a space here would double the gap.
|
||||
// format consumes (count, iconHeight, iconWidth). Denominations are separated
|
||||
// by one space, as the engine's formatter packs them.
|
||||
void AppendDenom(void *L, std::string &out, const char *globalName, const char *fallback,
|
||||
int count, int height) {
|
||||
if (!out.empty())
|
||||
out += ' ';
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushLocalizedString(L, globalName, fallback); // [.., fmt]
|
||||
const char *fmt = Game::Lua::ToString(L, -1);
|
||||
@@ -105,11 +104,8 @@ int __fastcall Script_GetCoinTextureString(void *L) {
|
||||
amount = kMaxCopper;
|
||||
|
||||
int height = kDefaultHeight;
|
||||
if (Game::Lua::IsNumber(L, 2)) {
|
||||
const int h = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
if (h > 0)
|
||||
height = h;
|
||||
}
|
||||
if (Game::Lua::IsNumber(L, 2))
|
||||
height = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
const int gold = static_cast<int>(amount / 10000);
|
||||
const int silver = static_cast<int>((amount % 10000) / 100);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user