feat(api): describe the API in the code, browse it with /classicapi

docs/API.md is 18.7k lines and its anchors are signature slugs, so editing a
signature breaks the 281 internal and 88 README links into it. Blizzard
solved the same problem by shipping the API as data plus a browser addon,
and warcraft.wiki.gg renders those same tables. One set of descriptors can
feed all three.

Each registration now carries one next to it: typed arguments and returns,
nilability, defaults, and a sentence. Events, enumerations and returned table
shapes are described the same way. No Script_* body changed.

Game.h gains namespace Game::Doc (Field with Req/Opt/Vararg, FieldList,
Function, Method, Structure, Event) and a trailing descriptor argument on all
six Game::Lua::Register* plus Event::Custom::AutoReserve.

api/Documentation.cpp records every registration on the first pass of each
Lua state and seals it, so /reload re-registers but records nothing and the
environment comes from which pass saw the name. Export is lazy: nothing is
materialized until C_APIDocumentation.GetSystem asks for one system, because
eager export would build thousands of tables per reload for a slash command.
_classicapi_UndocumentedAPI lists what still has no descriptor.

AddOns/!!!ClassicAPI/APIDocumentation/ ports Blizzard_APIDocumentation's eight
files. Named /classicapi, not /api: the browser holds only what ClassicAPI
adds, so /api would have implied the whole 1.12 surface. Two deliberate
differences from the retail source, both noted in the file headers: a system
search also matches its namespace, since that is the name people type; and an
undocumented function says so, because an empty signature otherwise reads as
a real one.

LinkUtil gains the SetItemRef to LinkUtil.ProcessLink dispatch. That handler
registry was dead code; a new link type now registers a handler instead of
stacking another SetItemRef override. Also defines assertsafe there, which
was undefined and only mattered once the function went live.

Documented: the C_Spell and C_SpellBook surface, the spell globals, both
spellbook enumerations, the 13 UNIT_SPELLCAST_* events, GameTooltip as the
widget-method example and C_Glue as the glue and mixed-environment example.
66 of 692 function registrations. tools/New-ApiDocSkeletons.ps1 emits
paste-ready skeletons to make the rest of the sweep tractable.

Verified in game: the browser lists, searches, and prints detail; links
render and click through; right-click copies and shift-click dumps.
This commit is contained in:
Brues
2026-09-12 23:20:46 -05:00
parent b4e5f4f2bb
commit 108a91a843
48 changed files with 3473 additions and 97 deletions
+9
View File
@@ -52,3 +52,12 @@ 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
+34
View File
@@ -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
+1
View File
@@ -54,6 +54,7 @@ reference in **[docs/API.md](docs/API.md)**.
|-----------|-------|
| [Action](docs/API.md#action) | `GetActionInfo` |
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded`, `C_AddOns.LoadAddOn` |
| [APIDocumentation](docs/API.md#apidocumentation) | `/classicapi` in-game browser, `C_APIDocumentation.GetSystem`, `C_APIDocumentation.GetSystems` |
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings` |
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
+55
View File
@@ -3860,3 +3860,58 @@ 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.
+98
View File
@@ -26,6 +26,12 @@ build instructions.
- [Conditional and multi-flavor TOC loading](#conditional-and-multi-flavor-toc-loading)
- [SavedVariables loaded first](#savedvariables-loaded-first)
- [APIDocumentation](#apidocumentation)
- [`/classicapi`](#classicapi)
- [`C_APIDocumentation.GetSystems()`](#c_apidocumentationgetsystems)
- [`C_APIDocumentation.GetSystem(system)`](#c_apidocumentationgetsystemsystem)
- [`_classicapi_UndocumentedAPI()`](#_classicapi_undocumentedapi)
- [AuctionHouse](#auctionhouse)
- [`C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid, buyout)`](#c_auctionhousepostitemitemlocation-duration-quantity-numstacks-bid-buyout)
@@ -1289,6 +1295,98 @@ ClassicAPI removes the restart requirement. On every `/reload`:
- **Deleting an addon folder removes it from the addon list** on the
next `/reload`.
## APIDocumentation
ClassicAPI describes its own surface. Each function it adds carries a
typed signature: the name and type of every argument and return value,
which of them can be nil, and one sentence that says what the function
does. Events, enumerations, and the table shapes functions return are
described the same way.
You read this in the game with `/classicapi`, or from code through
`C_APIDocumentation`.
### `/classicapi`
Browse the API from the chat box. `/capi` is the short form.
```
/classicapi usage
/classicapi stats how much is documented
/classicapi system list every system
/classicapi spell one system
/classicapi spell list every function, event and table in a system
/classicapi search cooldown search everything
/classicapi spell search cooldown search one system
```
`search` also answers to `s`. Every search takes a Lua pattern.
Results are clickable. Click a name to print its full signature, with
each argument and return value on its own line. Right-click copies a
ready-to-paste call to the clipboard. Shift-click puts a `/dump` of the
function in the chat box, with the cursor between the parentheses.
Functions are grouped into systems. A system is a namespace such as
`C_Spell`, a group of plain globals such as `SpellGlobals`, or a widget
method set such as `SimpleTextureAPI`. Find a system by its name or its
namespace: `/classicapi spell` and `/classicapi c_spell` both work, and
case does not matter.
The browser holds what ClassicAPI adds. The functions the client already
ships are not in it.
### `C_APIDocumentation.GetSystems()`
Returns an array of system names, sorted.
```lua
local systems = C_APIDocumentation.GetSystems()
-- { "Item", "ItemGlobals", "Spell", "SpellBook", "SpellGlobals", ... }
```
### `C_APIDocumentation.GetSystem(system)`
Returns one system's documentation table, or `nil` if no system has that
name. Match on the name or the namespace; case does not matter. Each
call builds a new table, so the caller may keep it and change it.
```lua
local spell = C_APIDocumentation.GetSystem("C_Spell")
spell.Name -- "Spell"
spell.Namespace -- "C_Spell"
spell.Functions[1].Name
spell.Functions[1].Returns[1].Type
```
| Field | Meaning |
|-------|---------|
| `Name` | The system's name. |
| `Type` | `"System"` for a namespace or a group of globals, `"ScriptObject"` for a widget method set. |
| `Namespace` | The table the functions live in, such as `C_Spell`. Absent for globals and widget methods. |
| `Environment` | `"All"`, `"Game"`, or `"Glue"` — where the functions exist. |
| `Functions` | Array of functions. Each has `Name`, `Type`, and where documented `Documentation`, `Arguments`, and `Returns`. |
| `Events` | Array of events. Each has `Name`, `LiteralName`, and where documented `Documentation` and `Payload`. |
| `Tables` | Array of table shapes. A `Structure` has `Fields`; an `Enumeration` also has `NumValues`, `MinValue`, and `MaxValue`. |
An argument, return value, payload value, or field is described by
`Name`, `Type`, and `Nilable`, plus `Default` and `Documentation` when
they apply. `Type` is a Lua type such as `number` or `string`, or the
name of a table shape in some system's `Tables`.
The layout matches the one Blizzard's own documentation uses, so a tool
written against that data reads this without change.
### `_classicapi_UndocumentedAPI()`
Returns an array of the registered names that carry no description yet,
and its length. Names are being described one namespace at a time, so
this list shrinks with each release.
```lua
local missing, count = _classicapi_UndocumentedAPI()
```
## AuctionHouse
### `C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid, buyout)`
+21 -6
View File
@@ -14,6 +14,7 @@
#include "Game.h"
#include "MinHook.h"
#include "Offsets.h"
#include "api/Documentation.h"
namespace Game {
@@ -259,11 +260,12 @@ void MirrorRegistration(const char *tableName, const char *name) {
}
} // namespace
void RegisterGlobalFunction(const char *name, CFunction func) {
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
@@ -271,23 +273,27 @@ void RegisterGlobalFunction(const char *name, CFunction func) {
// 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) {
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) {
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;
@@ -297,6 +303,7 @@ void RegisterTableFunction(const char *tableName, const char *methodName, CFunct
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
@@ -306,7 +313,8 @@ void RegisterTableFunction(const char *tableName, const char *methodName, CFunct
// 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) {
void RegisterGlobalAlias(const char *alias, const char *tableName, const char *methodName,
const Doc::Function *doc) {
void *L = State();
if (L == nullptr)
return;
@@ -323,10 +331,12 @@ void RegisterGlobalAlias(const char *alias, const char *tableName, const char *m
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;
@@ -340,6 +350,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) {
@@ -477,8 +488,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)
@@ -487,8 +500,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) {
+133 -7
View File
@@ -13,6 +13,7 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace Game {
@@ -78,6 +79,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);
@@ -265,10 +373,21 @@ void *ResolveModel(void *L, int idx = 1, bool raiseError = true);
// `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
@@ -278,7 +397,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
@@ -290,15 +410,17 @@ 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
@@ -306,7 +428,8 @@ void RegisterTableFunction(const char *tableName, const char *methodName,
// 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);
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
@@ -319,8 +442,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).
+690
View File
@@ -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;
}
+40
View File
@@ -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
+20 -3
View File
@@ -39,11 +39,12 @@ namespace {
// GET_ITEM_INFO_RECEIVED) never claimed a slot. Grep `AutoReserve` before
// bumping the reservation count near this ceiling.
constexpr int MAX_RESERVED = 64;
struct ReservedName {
struct Reservation {
const char *name;
int slot; // -1 until claimed
const Game::Doc::Event *doc; // API documentation; nullptr = undocumented
};
ReservedName g_reserved[MAX_RESERVED];
Reservation g_reserved[MAX_RESERVED];
int g_reservedCount = 0;
bool g_writesEnabled = false;
// One-shot latch so a genuinely full event table (no NULL slot left to
@@ -243,13 +244,18 @@ void Grow() {
} // namespace
AutoReserve::AutoReserve(const char *name) {
AutoReserve::AutoReserve(const char *name, const Game::Doc::Event *doc) {
if (name == nullptr)
return;
// Dedup: a name reserved twice resolves both instances to the one entry.
// A descriptor on either instance documents the one reservation (an event
// fired from several modules is declared in a shared header, but a second
// declaration must not silently drop its documentation).
for (int i = 0; i < g_reservedCount; ++i) {
if (std::strcmp(g_reserved[i].name, name) == 0) {
index_ = i;
if (g_reserved[i].doc == nullptr)
g_reserved[i].doc = doc;
return;
}
}
@@ -257,6 +263,7 @@ AutoReserve::AutoReserve(const char *name) {
return; // overflow — index_ stays -1, Slot() reports unclaimed
g_reserved[g_reservedCount].name = name;
g_reserved[g_reservedCount].slot = -1;
g_reserved[g_reservedCount].doc = doc;
index_ = g_reservedCount;
++g_reservedCount;
}
@@ -265,6 +272,16 @@ int AutoReserve::Slot() const {
return index_ >= 0 ? g_reserved[index_].slot : -1;
}
int ReservedCount() { return g_reservedCount; }
const char *ReservedName(int index) {
return (index >= 0 && index < g_reservedCount) ? g_reserved[index].name : nullptr;
}
const Game::Doc::Event *ReservedDoc(int index) {
return (index >= 0 && index < g_reservedCount) ? g_reserved[index].doc : nullptr;
}
int LookupByName(const char *name) {
if (name == nullptr)
return -1;
+15 -1
View File
@@ -13,6 +13,7 @@
#pragma once
#include "Game.h"
#include "Offsets.h"
namespace Event::Custom {
@@ -50,14 +51,27 @@ namespace Event::Custom {
// `RegisterEvent` triggers `RetryClaims`), which `Fire` treats as a no-op.
// Do NOT cache the return across frames — read it at fire time (the read
// is two loads; there is nothing to save).
//
// `doc` is the event's API documentation (its System, a summary, and the
// payload fields — see `Game::Doc::Event`); `Api::Documentation` exports it
// under the System's `Events`. A reservation without one is listed by
// `_classicapi_UndocumentedAPI()`.
struct AutoReserve {
explicit AutoReserve(const char *name);
explicit AutoReserve(const char *name, const Game::Doc::Event *doc = nullptr);
int Slot() const;
private:
int index_ = -1; // into the reservation table; -1 = dropped (overflow)
};
// Read access to the reservation table for the API documentation: the
// number of reserved names, the literal name of the i-th (0-based), and its
// descriptor (nullptr when undocumented). Indices are stable for the
// process lifetime (reservations are only ever appended).
int ReservedCount();
const char *ReservedName(int index);
const Game::Doc::Event *ReservedDoc(int index);
// Walks the live engine event table at `[VAR_EVENT_TABLE_BASE_PTR]`
// looking for an entry whose name strcmps equal to `name`. Returns
// the slot index, or -1 if no entry matches.
+21 -3
View File
@@ -59,19 +59,37 @@ int __fastcall Script_IsOnGlueScreen_False(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kIsFirstLoadRets[] = {
Game::Doc::Req("isFirstLoad", "bool",
"True only while the first login screen since launch is showing."),
};
const Game::Doc::Function kIsFirstLoadThisSession{
"Whether this is the first login screen since the game started.",
{}, kIsFirstLoadRets};
const Game::Doc::Field kIsOnGlueScreenRets[] = {
Game::Doc::Req("isOnGlueScreen", "bool", "True on the login and character screens."),
};
const Game::Doc::Function kIsOnGlueScreen{
"Whether a login or character screen is showing rather than the world.",
{}, kIsOnGlueScreenRets};
void RegisterGlue() {
++g_glueBootCount; // one glue boot per callback invocation
Game::Lua::RegisterTableFunction("C_Glue", "IsFirstLoadThisSession",
&Script_IsFirstLoadThisSession);
&Script_IsFirstLoadThisSession,
&kIsFirstLoadThisSession);
Game::Lua::RegisterTableFunction("C_Glue", "IsOnGlueScreen",
&Script_IsOnGlueScreen_True);
&Script_IsOnGlueScreen_True, &kIsOnGlueScreen);
}
void RegisterInGame() {
// In-world: only IsOnGlueScreen makes sense (and it's always false).
// IsFirstLoadThisSession is a glue concept, so it's not registered here.
Game::Lua::RegisterTableFunction("C_Glue", "IsOnGlueScreen",
&Script_IsOnGlueScreen_False);
&Script_IsOnGlueScreen_False, &kIsOnGlueScreen);
}
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
+17 -1
View File
@@ -224,9 +224,25 @@ int __fastcall Script_C_Spell_CastAtCursor(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kCastAtCursorArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier",
"A spell ID, which casts that exact rank, or a spell name, "
"which casts the highest rank you know unless it ends in \"(Rank N)\"."),
};
const Game::Doc::Field kCastAtCursorRets[] = {
Game::Doc::Req("placed", "bool",
"True when the spell landed at the cursor; false when the cursor was "
"not on the ground, or the spell needs no ground target."),
};
const Game::Doc::Function kCastAtCursor{
"Casts a ground-target spell at the cursor's world position, with no reticle click.",
kCastAtCursorArgs, kCastAtCursorRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "CastAtCursor",
&Script_C_Spell_CastAtCursor);
&Script_C_Spell_CastAtCursor, &kCastAtCursor);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+20 -1
View File
@@ -119,9 +119,28 @@ int __fastcall Script_C_Spell_CastAtUnit(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kCastAtUnitArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier",
"A spell ID, which casts that exact rank, or a spell name, "
"which casts the highest rank you know unless it ends in \"(Rank N)\"."),
Game::Doc::Req("unit", "UnitToken", "The unit to cast the spell at."),
Game::Doc::Opt("placeGroundSpell", "bool", "true",
"False leaves a ground-target spell's reticle for you to click."),
};
const Game::Doc::Field kCastAtUnitRets[] = {
Game::Doc::Req("cast", "bool",
"True when the spell went out at the unit; false when you do not know "
"the spell, or the unit is not a target the spell accepts."),
};
const Game::Doc::Function kCastAtUnit{
"Casts a spell at a unit, dropping a ground-target spell at that unit's feet.",
kCastAtUnitArgs, kCastAtUnitRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "CastAtUnit",
&Script_C_Spell_CastAtUnit);
&Script_C_Spell_CastAtUnit, &kCastAtUnit);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+38 -4
View File
@@ -103,15 +103,49 @@ static int __fastcall Script_IsRangedAutoAttackSpellBookItem(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
static const Game::Doc::Field kSpellArgs[] = {
Game::Doc::Req("spellID", "number", "The spell to test."),
};
static const Game::Doc::Field kSlotArgs[] = {
Game::Doc::Req("slot", "luaIndex", "A spellbook slot, counted from 1."),
Game::Doc::Opt("bookType", "string", "\"spell\"",
"\"pet\" reads the pet book; the spell bank number 1 does the same."),
};
static const Game::Doc::Field kIsMeleeRets[] = {
Game::Doc::Req("isAutoAttack", "bool", "True for the melee auto attack."),
};
static const Game::Doc::Field kIsRangedRets[] = {
Game::Doc::Req("isRangedAutoAttack", "bool", "True for a ranged auto attack."),
};
static const Game::Doc::Function kIsAutoAttackSpell{
"Whether the spell is the melee auto attack that drives your weapon swings.",
kSpellArgs, kIsMeleeRets};
static const Game::Doc::Function kIsRangedAutoAttackSpell{
"Whether the spell is a repeating ranged attack, such as Auto Shot or a wand shot.",
kSpellArgs, kIsRangedRets};
static const Game::Doc::Function kIsAutoAttackSpellBookItem{
"Whether the spellbook slot holds the melee auto attack.",
kSlotArgs, kIsMeleeRets};
static const Game::Doc::Function kIsRangedAutoAttackSpellBookItem{
"Whether the spellbook slot holds a repeating ranged attack.",
kSlotArgs, kIsRangedRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "IsAutoAttackSpell",
&Script_IsAutoAttackSpell);
&Script_IsAutoAttackSpell, &kIsAutoAttackSpell);
Game::Lua::RegisterTableFunction("C_Spell", "IsRangedAutoAttackSpell",
&Script_IsRangedAutoAttackSpell);
&Script_IsRangedAutoAttackSpell,
&kIsRangedAutoAttackSpell);
Game::Lua::RegisterTableFunction("C_SpellBook", "IsAutoAttackSpellBookItem",
&Script_IsAutoAttackSpellBookItem);
&Script_IsAutoAttackSpellBookItem,
&kIsAutoAttackSpellBookItem);
Game::Lua::RegisterTableFunction("C_SpellBook", "IsRangedAutoAttackSpellBookItem",
&Script_IsRangedAutoAttackSpellBookItem);
&Script_IsRangedAutoAttackSpellBookItem,
&kIsRangedAutoAttackSpellBookItem);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+19 -2
View File
@@ -86,13 +86,30 @@ int __fastcall Script_CancelSpellByName(void *L) {
return 0;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kByIDArgs[] = {
Game::Doc::Req("spellID", "number", "The buff to cancel."),
};
const Game::Doc::Function kCancelSpellByID{
"Cancels one of your own buffs by spell ID.", kByIDArgs, {}};
const Game::Doc::Field kByNameArgs[] = {
Game::Doc::Req("name", "string",
"The buff's name, matched without regard to case; a \"(Rank N)\" "
"suffix is not read."),
};
const Game::Doc::Function kCancelSpellByName{
"Cancels the first of your own buffs whose name matches.",
kByNameArgs, {}, "SpellGlobals"};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "CancelSpellByID",
&Script_CancelSpellByID);
&Script_CancelSpellByID, &kCancelSpellByID);
Game::Lua::RegisterGlobalFunction("CancelSpellByName",
&Script_CancelSpellByName);
&Script_CancelSpellByName, &kCancelSpellByName);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+75 -5
View File
@@ -977,6 +977,71 @@ static int __fastcall Script_UnitSpellTargetName(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
static const Game::Doc::Field kUnitArgs[] = {
Game::Doc::Req("unit", "UnitToken", "The unit to read."),
};
// Both cast getters push the same 11 values, and push nothing at all when
// there is no cast — so every return is nilable.
static const Game::Doc::Field kCastingInfoRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Localized spell name; all returns are nil when the unit is not casting."),
Game::Doc::Opt("displayName", "string", nullptr, "Same as name."),
Game::Doc::Opt("textureID", "string", nullptr, "Icon texture path."),
Game::Doc::Opt("startTimeMs", "number", nullptr,
"When the cast began, on the same clock as GetTime times 1000."),
Game::Doc::Opt("endTimeMs", "number", nullptr, "When the cast ends, on the same clock."),
Game::Doc::Opt("isTradeskill", "bool", nullptr, "True for a profession recipe cast."),
Game::Doc::Opt("castID", "string", nullptr,
"The cast GUID the UNIT_SPELLCAST events carry for this cast."),
Game::Doc::Opt("notInterruptible", "bool", nullptr,
"True when no interrupt or silence you know can stop the cast."),
Game::Doc::Opt("castingSpellID", "number", nullptr, "The spell being cast."),
Game::Doc::Opt("castBarID", "number", nullptr, "Always nil."),
Game::Doc::Opt("delayTimeMs", "number", nullptr,
"Pushback the cast has taken so far, in milliseconds."),
};
static const Game::Doc::Function kUnitCastingInfo{
"The regular cast a unit has in progress, with its start and end times.",
kUnitArgs, kCastingInfoRets};
static const Game::Doc::Function kCastingInfo{
"The regular cast the player has in progress, with its start and end times.",
{}, kCastingInfoRets};
// Both channel getters push the same 8 values, and nothing when there is no
// channel. Remote units whose channel we did not see begin report nil times.
static const Game::Doc::Field kChannelInfoRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Localized spell name; all returns are nil when the unit is not channeling."),
Game::Doc::Opt("displayName", "string", nullptr, "Same as name."),
Game::Doc::Opt("textureID", "string", nullptr, "Icon texture path."),
Game::Doc::Opt("startTimeMs", "number", nullptr,
"When the channel began, on the same clock as GetTime times 1000; "
"nil for a unit whose channel you did not see begin."),
Game::Doc::Opt("endTimeMs", "number", nullptr, "When the channel ends, on the same clock."),
Game::Doc::Opt("isTradeskill", "bool", nullptr, "True for a profession recipe cast."),
Game::Doc::Opt("notInterruptible", "bool", nullptr,
"True when no interrupt or silence you know can stop the channel."),
Game::Doc::Opt("spellID", "number", nullptr, "The spell being channeled."),
};
static const Game::Doc::Function kUnitChannelInfo{
"The channel a unit has in progress, with its start and end times.",
kUnitArgs, kChannelInfoRets};
static const Game::Doc::Function kChannelInfo{
"The channel the player has in progress, with its start and end times.",
{}, kChannelInfoRets};
static const Game::Doc::Field kTargetNameRets[] = {
Game::Doc::Opt("targetName", "string", nullptr,
"Nil when the unit is not casting, the spell has no unit target, "
"or the name cannot be resolved."),
};
static const Game::Doc::Function kUnitSpellTargetName{
"The name of the unit that a unit is casting or channeling a spell at.",
kUnitArgs, kTargetNameRets, "SpellGlobals", true};
static void RegisterLuaFunctions() {
// Registered under C_Spell rather than as globals to avoid clobbering
// the global `UnitCastingInfo` / `UnitChannelInfo` names. Addons that
@@ -986,13 +1051,18 @@ static void RegisterLuaFunctions() {
// engine never exposes). Occupying the global makes them adopt our
// player-only version and drop their superior fallback — so we cede
// the global names and expose the functions here instead.
Game::Lua::RegisterTableFunction("C_Spell", "UnitCastingInfo", &Script_UnitCastingInfo);
Game::Lua::RegisterTableFunction("C_Spell", "CastingInfo", &Script_CastingInfo);
Game::Lua::RegisterTableFunction("C_Spell", "UnitChannelInfo", &Script_UnitChannelInfo);
Game::Lua::RegisterTableFunction("C_Spell", "ChannelInfo", &Script_ChannelInfo);
Game::Lua::RegisterTableFunction("C_Spell", "UnitCastingInfo", &Script_UnitCastingInfo,
&kUnitCastingInfo);
Game::Lua::RegisterTableFunction("C_Spell", "CastingInfo", &Script_CastingInfo,
&kCastingInfo);
Game::Lua::RegisterTableFunction("C_Spell", "UnitChannelInfo", &Script_UnitChannelInfo,
&kUnitChannelInfo);
Game::Lua::RegisterTableFunction("C_Spell", "ChannelInfo", &Script_ChannelInfo,
&kChannelInfo);
// A novel ClassicAPI name (no addon ships its own), so it's safe as a
// global — and it matches the `UnitSpellTargetName(unit)` call shape.
Game::Lua::RegisterGlobalFunction("UnitSpellTargetName", &Script_UnitSpellTargetName);
Game::Lua::RegisterGlobalFunction("UnitSpellTargetName", &Script_UnitSpellTargetName,
&kUnitSpellTargetName);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+31 -2
View File
@@ -78,12 +78,41 @@ int __fastcall Script_C_SpellBook_GetSpellBookItemCastCount(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kBySpellArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kBySpellRets[] = {
Game::Doc::Req("castCount", "number",
"Casts the carried reagents allow; 0 for an unknown spell and for a "
"spell that needs no reagents."),
};
const Game::Doc::Function kGetSpellCastCount{
"How many times you can cast the spell with the reagents you carry.",
kBySpellArgs, kBySpellRets};
const Game::Doc::Field kBySlotArgs[] = {
Game::Doc::Req("slotIndex", "luaIndex", "A spellbook slot, counted from 1."),
Game::Doc::Opt("spellBank", "number", "0", "1 reads the pet book; anything else "
"reads the player book."),
};
const Game::Doc::Field kBySlotRets[] = {
Game::Doc::Req("castCount", "number",
"Casts the carried reagents allow; 0 for an empty slot and for a "
"spell that needs no reagents."),
};
const Game::Doc::Function kGetSpellBookItemCastCount{
"How many times you can cast the slot's spell with the reagents you carry.",
kBySlotArgs, kBySlotRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellCastCount",
&Script_C_Spell_GetSpellCastCount);
&Script_C_Spell_GetSpellCastCount,
&kGetSpellCastCount);
Game::Lua::RegisterTableFunction(
"C_SpellBook", "GetSpellBookItemCastCount",
&Script_C_SpellBook_GetSpellBookItemCastCount);
&Script_C_SpellBook_GetSpellBookItemCastCount, &kGetSpellBookItemCastCount);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+76 -13
View File
@@ -31,20 +31,83 @@ namespace Spell::CastEvents {
namespace {
// --- Event documentation ----------------------------------------------------
//
// Every event here carries `(unitTarget, castGUID, spellID, spellName, rank)`
// except SENT, which inserts `target` at argument 2. `spellName` and `rank`
// are a ClassicAPI addition to the retail payload — they save the handler a
// lookup, and a handler written for retail simply ignores them.
const Game::Doc::Field kCastPayload[] = {
Game::Doc::Req("unitTarget", "UnitToken", "The unit that is casting."),
Game::Doc::Req("castGUID", "string",
"Identifies one cast. Every event of that cast carries the same value."),
Game::Doc::Req("spellID", "number"),
Game::Doc::Opt("spellName", "string", nullptr, "The spell's name in the client's language."),
Game::Doc::Opt("rank", "string", nullptr, "The spell's rank text, when it has one."),
};
// The reticle is up before any cast exists, so there is no castGUID to send.
const Game::Doc::Field kReticlePayload[] = {
Game::Doc::Req("unitTarget", "UnitToken", "Always \"player\"."),
Game::Doc::Req("castGUID", "string", "Always empty; no cast exists yet."),
Game::Doc::Req("spellID", "number"),
Game::Doc::Opt("spellName", "string", nullptr, "The spell's name in the client's language."),
Game::Doc::Opt("rank", "string", nullptr, "The spell's rank text, when it has one."),
};
const Game::Doc::Field kSentPayload[] = {
Game::Doc::Req("unitTarget", "UnitToken", "Always \"player\"."),
Game::Doc::Req("target", "string",
"Unit token of the cast's target, or empty for a self or untargeted cast."),
Game::Doc::Req("castGUID", "string",
"Identifies one cast. Every event of that cast carries the same value."),
Game::Doc::Req("spellID", "number"),
Game::Doc::Opt("spellName", "string", nullptr, "The spell's name in the client's language."),
Game::Doc::Opt("rank", "string", nullptr, "The spell's rank text, when it has one."),
};
const Game::Doc::Event kStartDoc{"Spell", "A unit began casting a spell.", kCastPayload};
const Game::Doc::Event kStopDoc{"Spell",
"A cast ended, for any reason. It follows every START.", kCastPayload};
const Game::Doc::Event kDelayedDoc{"Spell",
"A cast's finish time moved. Read the times again.", kCastPayload};
const Game::Doc::Event kChannelStartDoc{"Spell", "A unit began channeling a spell.",
kCastPayload};
const Game::Doc::Event kChannelStopDoc{"Spell",
"A channel ended, whether it finished or was cut short.", kCastPayload};
const Game::Doc::Event kChannelUpdateDoc{"Spell",
"A channel's finish time moved. Read the times again.", kCastPayload};
const Game::Doc::Event kSucceededDoc{"Spell", "A spell took effect.", kCastPayload};
const Game::Doc::Event kInterruptedDoc{"Spell",
"A cast was cut short. Channels do not fire this.", kCastPayload};
const Game::Doc::Event kFailedDoc{"Spell",
"A cast was refused before it began, such as out of range or short of power.",
kCastPayload};
const Game::Doc::Event kFailedQuietDoc{"Spell",
"A cast was refused and the client showed no error.", kCastPayload};
const Game::Doc::Event kSentDoc{"Spell",
"The player asked the server to cast a spell. It is the first event of a cast.",
kSentPayload};
const Game::Doc::Event kReticleTargetDoc{"Spell",
"A spell is waiting for the player to choose a ground target.", kReticlePayload};
const Game::Doc::Event kReticleClearDoc{"Spell",
"The player cancelled a spell that was waiting for a ground target.", kReticlePayload};
// The reservations double as the fire handles (`k*.Slot()` — O(1)).
const Event::Custom::AutoReserve kStart{"UNIT_SPELLCAST_START"};
const Event::Custom::AutoReserve kStop{"UNIT_SPELLCAST_STOP"};
const Event::Custom::AutoReserve kDelayed{"UNIT_SPELLCAST_DELAYED"};
const Event::Custom::AutoReserve kChannelStart{"UNIT_SPELLCAST_CHANNEL_START"};
const Event::Custom::AutoReserve kChannelStop{"UNIT_SPELLCAST_CHANNEL_STOP"};
const Event::Custom::AutoReserve kChannelUpdate{"UNIT_SPELLCAST_CHANNEL_UPDATE"};
const Event::Custom::AutoReserve kSucceeded{"UNIT_SPELLCAST_SUCCEEDED"};
const Event::Custom::AutoReserve kInterrupted{"UNIT_SPELLCAST_INTERRUPTED"};
const Event::Custom::AutoReserve kFailed{"UNIT_SPELLCAST_FAILED"};
const Event::Custom::AutoReserve kFailedQuiet{"UNIT_SPELLCAST_FAILED_QUIET"};
const Event::Custom::AutoReserve kSent{"UNIT_SPELLCAST_SENT"};
const Event::Custom::AutoReserve kReticleTarget{"UNIT_SPELLCAST_RETICLE_TARGET"};
const Event::Custom::AutoReserve kReticleClear{"UNIT_SPELLCAST_RETICLE_CLEAR"};
const Event::Custom::AutoReserve kStart{"UNIT_SPELLCAST_START", &kStartDoc};
const Event::Custom::AutoReserve kStop{"UNIT_SPELLCAST_STOP", &kStopDoc};
const Event::Custom::AutoReserve kDelayed{"UNIT_SPELLCAST_DELAYED", &kDelayedDoc};
const Event::Custom::AutoReserve kChannelStart{"UNIT_SPELLCAST_CHANNEL_START", &kChannelStartDoc};
const Event::Custom::AutoReserve kChannelStop{"UNIT_SPELLCAST_CHANNEL_STOP", &kChannelStopDoc};
const Event::Custom::AutoReserve kChannelUpdate{"UNIT_SPELLCAST_CHANNEL_UPDATE", &kChannelUpdateDoc};
const Event::Custom::AutoReserve kSucceeded{"UNIT_SPELLCAST_SUCCEEDED", &kSucceededDoc};
const Event::Custom::AutoReserve kInterrupted{"UNIT_SPELLCAST_INTERRUPTED", &kInterruptedDoc};
const Event::Custom::AutoReserve kFailed{"UNIT_SPELLCAST_FAILED", &kFailedDoc};
const Event::Custom::AutoReserve kFailedQuiet{"UNIT_SPELLCAST_FAILED_QUIET", &kFailedQuietDoc};
const Event::Custom::AutoReserve kSent{"UNIT_SPELLCAST_SENT", &kSentDoc};
const Event::Custom::AutoReserve kReticleTarget{"UNIT_SPELLCAST_RETICLE_TARGET", &kReticleTargetDoc};
const Event::Custom::AutoReserve kReticleClear{"UNIT_SPELLCAST_RETICLE_CLEAR", &kReticleClearDoc};
// Autoshot spams client-side failures while ramping — nampower filters it
// out of its failure event, so do we.
+26 -1
View File
@@ -70,6 +70,30 @@ int __fastcall Script_C_Spell_GetSpellCooldown(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kSpellCooldownInfoFields[] = {
Game::Doc::Req("startTime", "number",
"When the cooldown began, in seconds on the GetTime clock; 0 when none runs."),
Game::Doc::Req("duration", "number", "Cooldown length in seconds; 0 when none runs."),
Game::Doc::Req("isEnabled", "bool", "False while the cooldown is on hold."),
Game::Doc::Req("modRate", "number", "Always 1."),
};
const Game::Doc::Structure kSpellCooldownInfo{
"SpellCooldownInfo", "Spell", kSpellCooldownInfoFields,
"Cooldown state for one spell."};
const Game::Doc::Field kGetSpellCooldownArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kGetSpellCooldownRets[] = {
Game::Doc::Opt("cooldownInfo", "SpellCooldownInfo", nullptr,
"Nil when the spell cannot be resolved."),
};
const Game::Doc::Function kGetSpellCooldown{
"When a spell's cooldown began and how long it lasts.",
kGetSpellCooldownArgs, kGetSpellCooldownRets};
} // namespace
bool Query(int spellID, uint32_t *startMs, uint32_t *durationMs,
@@ -88,7 +112,8 @@ bool Query(int spellID, uint32_t *startMs, uint32_t *durationMs,
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellCooldown",
&Script_C_Spell_GetSpellCooldown);
&Script_C_Spell_GetSpellCooldown,
&kGetSpellCooldown);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+15 -1
View File
@@ -53,9 +53,23 @@ static int __fastcall Script_GetSpellDescription(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
static const Game::Doc::Field kGetSpellDescriptionArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
static const Game::Doc::Field kGetSpellDescriptionRets[] = {
Game::Doc::Opt("description", "string", nullptr,
"Nil for an unknown spell and for a spell with no description in "
"your language."),
};
static const Game::Doc::Function kGetSpellDescription{
"The spell's description text, with its placeholders filled in at the base rank.",
kGetSpellDescriptionArgs, kGetSpellDescriptionRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellDescription",
&Script_GetSpellDescription);
&Script_GetSpellDescription, &kGetSpellDescription);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+33 -1
View File
@@ -114,9 +114,41 @@ int __fastcall Script_GetSpellEffectInfo(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kSpellEffectInfoFields[] = {
Game::Doc::Req("effect", "number", "The effect code; 0 when the slot is unused."),
Game::Doc::Req("auraName", "number",
"Which aura the effect applies; 0 when it applies none."),
Game::Doc::Req("basePoints", "number", "The effect's stored magnitude."),
Game::Doc::Req("baseDice", "number", "Added to basePoints for the fixed magnitude."),
Game::Doc::Req("dieSides", "number",
"Size of the magnitude's random range; above 1 means the server rolls it."),
Game::Doc::Req("pointsPerLevel", "number", "Magnitude gained per level; can be fractional."),
Game::Doc::Req("dicePerLevel", "number",
"Random range gained per level; can be fractional."),
Game::Doc::Req("miscValue", "number",
"The effect's parameter, such as which stat a stat aura changes."),
};
const Game::Doc::Structure kSpellEffectInfo{
"SpellEffectInfo", "Spell", kSpellEffectInfoFields,
"The stored fields of one spell effect."};
const Game::Doc::Field kGetSpellEffectInfoArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kGetSpellEffectInfoRets[] = {
Game::Doc::Opt("effects", "table", nullptr,
"Three SpellEffectInfo tables, one per effect slot, counted from 1; "
"an unused slot has every field 0. Nil for an unknown spell."),
};
const Game::Doc::Function kGetSpellEffectInfo{
"The stored fields of each of the spell's three effects.",
kGetSpellEffectInfoArgs, kGetSpellEffectInfoRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellEffectInfo",
&Script_GetSpellEffectInfo);
&Script_GetSpellEffectInfo, &kGetSpellEffectInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+16 -1
View File
@@ -64,11 +64,26 @@ int __fastcall Script_GetSpellEffectMechanics(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kGetSpellEffectMechanicsArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kGetSpellEffectMechanicsRets[] = {
Game::Doc::Opt("mechanics", "table", nullptr,
"Three mechanic IDs, one per effect slot, counted from 1; 0 where an "
"effect carries no mechanic. Nil for an unknown spell."),
};
const Game::Doc::Function kGetSpellEffectMechanics{
"The mechanic each of the spell's three effects applies, such as root or bleed.",
kGetSpellEffectMechanicsArgs, kGetSpellEffectMechanicsRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellEffectMechanics",
&Script_GetSpellEffectMechanics);
&Script_GetSpellEffectMechanics,
&kGetSpellEffectMechanics);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+13 -1
View File
@@ -41,9 +41,21 @@ static int __fastcall Script_DoesSpellExist(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
static const Game::Doc::Field kDoesSpellExistArgs[] = {
Game::Doc::Req("spellID", "number", "The spell ID to test."),
};
static const Game::Doc::Field kDoesSpellExistRets[] = {
Game::Doc::Req("exists", "bool", "True when the client holds data for that spell ID."),
};
static const Game::Doc::Function kDoesSpellExist{
"Whether a spell ID names a real spell, as a check before you read more about it.",
kDoesSpellExistArgs, kDoesSpellExistRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "DoesSpellExist",
&Script_DoesSpellExist);
&Script_DoesSpellExist, &kDoesSpellExist);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+30 -2
View File
@@ -58,11 +58,39 @@ static int __fastcall Script_SpellHasRange(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kByIdentifierArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kByIdentifierRets[] = {
Game::Doc::Req("hasRange", "bool",
"True when the spell's minimum or maximum range is above zero."),
};
const Game::Doc::Function kC_SpellHasRange{
"Whether the spell is cast at something at a distance rather than on the caster.",
kByIdentifierArgs, kByIdentifierRets};
const Game::Doc::Field kBySlotArgs[] = {
Game::Doc::Req("slot", "luaIndex", "A 1-based spellbook slot."),
Game::Doc::Opt("bookType", "string", "\"spell\"",
"\"spell\" or \"pet\"; picks the book the slot belongs to."),
};
const Game::Doc::Field kBySlotRets[] = {
Game::Doc::Req("hasRange", "bool",
"True when the spell's minimum or maximum range is above zero; "
"false for an empty slot."),
};
const Game::Doc::Function kSpellHasRange{
"Whether the spellbook slot holds a spell you cast at a distance rather than on "
"the caster.",
kBySlotArgs, kBySlotRets, "SpellGlobals"};
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "SpellHasRange",
&Script_C_Spell_SpellHasRange);
&Script_C_Spell_SpellHasRange, &kC_SpellHasRange);
Game::Lua::RegisterGlobalFunction("SpellHasRange",
&Script_SpellHasRange);
&Script_SpellHasRange, &kSpellHasRange);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+16 -1
View File
@@ -46,9 +46,24 @@ int __fastcall Script_C_Spell_IsSpellInRange(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
Game::Doc::Req("targetUnit", "UnitToken", "The unit to measure the distance to."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("inRange", "bool", nullptr,
"True when in range, false when out of range, nil when the spell or "
"unit gives no answer."),
};
const Game::Doc::Function kIsSpellInRange{
"Whether the player is close enough to the unit to cast the spell on it.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "IsSpellInRange",
&Script_C_Spell_IsSpellInRange);
&Script_C_Spell_IsSpellInRange, &kIsSpellInRange);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+240 -21
View File
@@ -788,36 +788,255 @@ static int __fastcall Script_C_SpellBook_GetSpellBookItemInfo(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
// The globals that share `ResolveLuaArgsToSpellID`: a spell ID, a name, a
// "name(Rank N)", or a link — or a spellbook slot when a bookType follows.
static const Game::Doc::Field kSpellOrSlotArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier",
"A spell ID, name, or link, or a spellbook slot when bookType is given."),
Game::Doc::Opt("bookType", "string", nullptr,
"\"spell\" or \"pet\"; makes the first argument a slot in that book."),
};
// The `C_Spell.*` forms take one identifier; they have no slot form.
static const Game::Doc::Field kSpellIdArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
static const Game::Doc::Field kGetSpellInfoRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Localized spell name; all returns are nil for an unknown spell."),
Game::Doc::Opt("rank", "string", nullptr, "Localized rank text, such as \"Rank 2\"."),
Game::Doc::Opt("icon", "string", nullptr, "Icon texture path."),
Game::Doc::Opt("cost", "number", nullptr, "Base power cost."),
Game::Doc::Opt("isFunnel", "bool", nullptr, "True for a health funnel spell."),
Game::Doc::Opt("powerType", "number", nullptr,
"0 mana, 1 rage, 2 focus, 3 energy, 4 happiness."),
Game::Doc::Opt("castTime", "number", nullptr,
"Base cast time in milliseconds; 0 when instant."),
Game::Doc::Opt("minRange", "number", nullptr, "Minimum range in yards."),
Game::Doc::Opt("maxRange", "number", nullptr, "Maximum range in yards."),
Game::Doc::Opt("spellID", "number", nullptr, "The spell ID the arguments resolved to."),
};
static const Game::Doc::Function kGetSpellInfo{
"Name, rank, icon, cost, cast time and range for any spell, plus its ID.",
kSpellOrSlotArgs, kGetSpellInfoRets, "SpellGlobals"};
static const Game::Doc::Field kSpellInfoFields[] = {
Game::Doc::Opt("name", "string", nullptr, "Localized spell name."),
Game::Doc::Opt("iconID", "string", nullptr,
"Icon texture path; give it to texture:SetTexture."),
Game::Doc::Req("castTime", "number", "Base cast time in milliseconds; 0 when instant."),
Game::Doc::Req("minRange", "number", "Minimum range in yards."),
Game::Doc::Req("maxRange", "number", "Maximum range in yards."),
Game::Doc::Req("spellID", "number", "The spell ID."),
Game::Doc::Opt("rank", "string", nullptr, "Localized rank text, such as \"Rank 2\"."),
Game::Doc::Req("cost", "number", "Base power cost."),
Game::Doc::Req("isFunnel", "bool", "True for a health funnel spell."),
Game::Doc::Req("powerType", "number", "0 mana, 1 rage, 2 focus, 3 energy, 4 happiness."),
};
static const Game::Doc::Structure kSpellInfoStruct{
"SpellInfo", "Spell", kSpellInfoFields, "Static data for one spell."};
static const Game::Doc::Field kCSpellInfoRets[] = {
Game::Doc::Opt("info", "SpellInfo", nullptr, "Nil for an unknown spell."),
};
static const Game::Doc::Function kCSpellGetSpellInfo{
"A table of the spell's name, icon, cast time, range and cost.",
kSpellIdArgs, kCSpellInfoRets};
static const Game::Doc::Field kSpellNameRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Nil for an unknown spell, or one with no name in this locale."),
};
static const Game::Doc::Function kCSpellGetSpellName{
"The localized name of a spell.", kSpellIdArgs, kSpellNameRets};
static const Game::Doc::Field kSpellTextureRets[] = {
Game::Doc::Opt("texture", "string", nullptr,
"Icon texture path; nil when the spell or its icon is unknown."),
};
static const Game::Doc::Function kCSpellGetSpellTexture{
"The icon texture path for a spell.", kSpellIdArgs, kSpellTextureRets};
static const Game::Doc::Field kGetSpellLinkRets[] = {
Game::Doc::Opt("link", "string", nullptr,
"Chat hyperlink for the spell; nil for an unknown spell."),
Game::Doc::Opt("spellID", "number", nullptr, "The spell ID the arguments resolved to."),
};
static const Game::Doc::Function kGetSpellLink{
"The chat hyperlink for a spell, with the spell ID it resolves to.",
kSpellOrSlotArgs, kGetSpellLinkRets, "SpellGlobals"};
static const Game::Doc::Field kCSpellLinkRets[] = {
Game::Doc::Opt("link", "string", nullptr,
"Chat hyperlink for the spell; nil for an unknown spell."),
};
static const Game::Doc::Function kCSpellGetSpellLink{
"The chat hyperlink for a spell.", kSpellIdArgs, kCSpellLinkRets};
static const Game::Doc::Field kFindSlotArgs[] = {
Game::Doc::Req("spellID", "number", "The spell to look for."),
};
static const Game::Doc::Field kFindSlotRets[] = {
Game::Doc::Opt("slot", "luaIndex", nullptr,
"Spellbook slot holding the spell; nil when neither book has it."),
Game::Doc::Opt("bookType", "string", nullptr, "\"spell\" or \"pet\"."),
};
static const Game::Doc::Function kFindSpellBookSlotByID{
"The spellbook slot and book that hold a spell.",
kFindSlotArgs, kFindSlotRets, "SpellGlobals"};
static const Game::Doc::Field kIsPassiveRets[] = {
Game::Doc::Opt("isPassive", "bool", nullptr,
"True when the spell applies itself with no cast; nil for an unknown spell."),
};
static const Game::Doc::Function kIsPassiveSpell{
"Whether a spell is passive, so it needs no cast.",
kSpellOrSlotArgs, kIsPassiveRets, "SpellGlobals"};
static const Game::Doc::Function kCSpellIsSpellPassive{
"Whether a spell is passive, so it needs no cast.",
kSpellIdArgs, kIsPassiveRets};
static const Game::Doc::Field kIsPlayerSpellArgs[] = {
Game::Doc::Req("spellID", "number", "The spell to test."),
};
static const Game::Doc::Field kIsPlayerSpellRets[] = {
Game::Doc::Req("isKnown", "bool", "True when the player knows this exact spell ID."),
};
static const Game::Doc::Function kIsPlayerSpell{
"Whether the player knows the spell, talents, racials and recipes included.",
kIsPlayerSpellArgs, kIsPlayerSpellRets, "SpellGlobals"};
static const Game::Doc::Field kCanDualWieldRets[] = {
Game::Doc::Req("canDualWield", "bool", "True when the player has learned Dual Wield."),
};
static const Game::Doc::Function kCanDualWield{
"Whether the player can hold a weapon in the off hand.",
{}, kCanDualWieldRets, "SpellGlobals"};
static const Game::Doc::Field kIsSpellKnownArgs[] = {
Game::Doc::Req("spellID", "number", "The spell to test."),
Game::Doc::Opt("isPet", "bool", "false", "Search the pet spellbook instead."),
};
static const Game::Doc::Field kIsSpellKnownRets[] = {
Game::Doc::Req("isKnown", "bool", "True when the chosen spellbook holds the spell."),
};
static const Game::Doc::Function kIsSpellKnown{
"Whether the spell has a button in the player's or the pet's spellbook.",
kIsSpellKnownArgs, kIsSpellKnownRets, "SpellGlobals"};
static const Game::Doc::Field kIsHarmfulRets[] = {
Game::Doc::Req("isHarmful", "bool", "True when an effect of the spell aims at an enemy."),
};
static const Game::Doc::Function kIsHarmfulSpell{
"Whether the spell aims at an enemy.",
kSpellOrSlotArgs, kIsHarmfulRets, "SpellGlobals"};
static const Game::Doc::Function kCSpellIsSpellHarmful{
"Whether the spell aims at an enemy.", kSpellIdArgs, kIsHarmfulRets};
static const Game::Doc::Field kIsHelpfulRets[] = {
Game::Doc::Req("isHelpful", "bool",
"True when an effect of the spell aims at yourself or a friendly unit."),
};
static const Game::Doc::Function kIsHelpfulSpell{
"Whether the spell aims at yourself or a friendly unit.",
kSpellOrSlotArgs, kIsHelpfulRets, "SpellGlobals"};
static const Game::Doc::Function kCSpellIsSpellHelpful{
"Whether the spell aims at yourself or a friendly unit.",
kSpellIdArgs, kIsHelpfulRets};
static const Game::Doc::Field kSpellBookItemInfoFields[] = {
Game::Doc::Req("itemType", "SpellBookItemType", "Spell for the player book, PetAction for the pet book."),
Game::Doc::Req("actionID", "number", "The spell ID; the same value as spellID."),
Game::Doc::Req("spellID", "number", "The spell ID in the slot."),
Game::Doc::Opt("name", "string", nullptr, "Localized spell name."),
Game::Doc::Req("subName", "string", "Rank text, such as \"Rank 3\", or an empty string."),
Game::Doc::Opt("iconID", "string", nullptr,
"Icon texture path; give it to texture:SetTexture."),
Game::Doc::Req("isPassive", "bool", "True for a passive spell."),
Game::Doc::Req("isOffSpec", "bool", "Always false."),
};
static const Game::Doc::Structure kSpellBookItemInfoStruct{
"SpellBookItemInfo", "SpellBook", kSpellBookItemInfoFields,
"The spell that fills one spellbook slot."};
static const Game::Doc::Field kGetSpellBookItemInfoArgs[] = {
Game::Doc::Req("slotIndex", "luaIndex", "Slot number, counted across the whole book."),
Game::Doc::Opt("spellBank", "SpellBookSpellBank", "0",
"Which book to read; Player by default."),
};
static const Game::Doc::Field kGetSpellBookItemInfoRets[] = {
Game::Doc::Opt("info", "SpellBookItemInfo", nullptr,
"Nil for an empty slot, or a slot past the end of the book."),
};
static const Game::Doc::Function kGetSpellBookItemInfo{
"A table describing the spell in a spellbook slot.",
kGetSpellBookItemInfoArgs, kGetSpellBookItemInfoRets};
static const Game::Doc::Field kByAuraArgs[] = {
Game::Doc::Req("auraName", "number", "The aura code an effect of the spell must apply."),
};
static const Game::Doc::Field kByAuraRets[] = {
Game::Doc::Req("spellIDs", "table",
"Spell IDs in ascending order; empty when no known spell applies the aura."),
};
static const Game::Doc::Function kGetPlayerSpellsByAura{
"Every spell the player knows that applies the given aura.",
kByAuraArgs, kByAuraRets};
static const Game::Doc::Field kDisenchantRets[] = {
Game::Doc::Req("hasDisenchant", "bool", "True when a known spell disenchants items."),
};
static const Game::Doc::Function kContainsAnyDisenchantSpell{
"Whether the player knows a spell that disenchants items.", {}, kDisenchantRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetSpellInfo", &Script_GetSpellInfo);
Game::Lua::RegisterGlobalFunction("GetSpellLink", &Script_GetSpellLink);
Game::Lua::RegisterGlobalFunction("GetSpellInfo", &Script_GetSpellInfo, &kGetSpellInfo);
Game::Lua::RegisterGlobalFunction("GetSpellLink", &Script_GetSpellLink, &kGetSpellLink);
Game::Lua::RegisterGlobalFunction("FindSpellBookSlotByID",
&Script_FindSpellBookSlotByID);
Game::Lua::RegisterGlobalFunction("IsPassiveSpell", &Script_IsPassiveSpell);
Game::Lua::RegisterGlobalFunction("IsPlayerSpell", &Script_IsPlayerSpell);
Game::Lua::RegisterGlobalFunction("CanDualWield", &Script_CanDualWield);
Game::Lua::RegisterGlobalFunction("IsSpellKnown", &Script_IsSpellKnown);
Game::Lua::RegisterGlobalFunction("IsHarmfulSpell", &Script_IsHarmfulSpell);
Game::Lua::RegisterGlobalFunction("IsHelpfulSpell", &Script_IsHelpfulSpell);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellLink", &Script_C_GetSpellLink);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellInfo", &Script_C_GetSpellInfo);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellName", &Script_C_GetSpellName);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellTexture", &Script_C_GetSpellTexture);
Game::Lua::RegisterTableFunction("C_Spell", "IsSpellPassive", &Script_C_IsSpellPassive);
&Script_FindSpellBookSlotByID,
&kFindSpellBookSlotByID);
Game::Lua::RegisterGlobalFunction("IsPassiveSpell", &Script_IsPassiveSpell,
&kIsPassiveSpell);
Game::Lua::RegisterGlobalFunction("IsPlayerSpell", &Script_IsPlayerSpell,
&kIsPlayerSpell);
Game::Lua::RegisterGlobalFunction("CanDualWield", &Script_CanDualWield, &kCanDualWield);
Game::Lua::RegisterGlobalFunction("IsSpellKnown", &Script_IsSpellKnown, &kIsSpellKnown);
Game::Lua::RegisterGlobalFunction("IsHarmfulSpell", &Script_IsHarmfulSpell,
&kIsHarmfulSpell);
Game::Lua::RegisterGlobalFunction("IsHelpfulSpell", &Script_IsHelpfulSpell,
&kIsHelpfulSpell);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellLink", &Script_C_GetSpellLink,
&kCSpellGetSpellLink);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellInfo", &Script_C_GetSpellInfo,
&kCSpellGetSpellInfo);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellName", &Script_C_GetSpellName,
&kCSpellGetSpellName);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellTexture", &Script_C_GetSpellTexture,
&kCSpellGetSpellTexture);
Game::Lua::RegisterTableFunction("C_Spell", "IsSpellPassive", &Script_C_IsSpellPassive,
&kCSpellIsSpellPassive);
Game::Lua::RegisterTableFunction("C_Spell", "IsSpellHarmful",
&Script_C_Spell_IsSpellHarmful);
&Script_C_Spell_IsSpellHarmful,
&kCSpellIsSpellHarmful);
Game::Lua::RegisterTableFunction("C_Spell", "IsSpellHelpful",
&Script_C_Spell_IsSpellHelpful);
&Script_C_Spell_IsSpellHelpful,
&kCSpellIsSpellHelpful);
Game::Lua::RegisterTableFunction("C_SpellBook", "GetSpellBookItemInfo",
&Script_C_SpellBook_GetSpellBookItemInfo);
&Script_C_SpellBook_GetSpellBookItemInfo,
&kGetSpellBookItemInfo);
Game::Lua::RegisterTableFunction("C_SpellBook", "GetPlayerSpellsByAura",
&Script_GetPlayerSpellsByAura);
&Script_GetPlayerSpellsByAura,
&kGetPlayerSpellsByAura);
Game::Lua::RegisterTableFunction("C_SpellBook", "ContainsAnyDisenchantSpell",
&Script_ContainsAnyDisenchantSpell);
&Script_ContainsAnyDisenchantSpell,
&kContainsAnyDisenchantSpell);
Game::Lua::RegisterIntegerEnum("Enum", "SpellBookSpellBank",
kSpellBookSpellBankEntries, 2);
kSpellBookSpellBankEntries, 2, "SpellBook");
Game::Lua::RegisterIntegerEnum("Enum", "SpellBookItemType",
kSpellBookItemTypeEntries, 5);
kSpellBookItemTypeEntries, 5, "SpellBook");
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+14 -1
View File
@@ -63,11 +63,24 @@ int __fastcall Script_IsCurrentSpell(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("isCurrent", "bool",
"True while the spell is on the cast bar, queued, or channeling."),
};
const Game::Doc::Function kIsCurrentSpell{
"Whether the player is casting, channeling, or has queued this spell.",
kArgs, kRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "IsCurrentSpell",
&Script_IsCurrentSpell);
&Script_IsCurrentSpell, &kIsCurrentSpell);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+13 -1
View File
@@ -83,9 +83,21 @@ static int __fastcall Script_IsSelfBuff(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spellID", "number", "The spell to examine."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("isSelfBuff", "bool",
"True when every effect of the spell targets the caster alone."),
};
const Game::Doc::Function kIsSelfBuff{
"Whether the spell only ever applies to the caster.", kArgs, kRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "IsSelfBuff",
&Script_IsSelfBuff);
&Script_IsSelfBuff, &kIsSelfBuff);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+55 -5
View File
@@ -392,17 +392,67 @@ int __fastcall Script_GetCurrentLevelSpells(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kSpellIDArgs[] = {
Game::Doc::Req("spellID", "number", "The spell to examine."),
};
const Game::Doc::Field kLevelLearnedRets[] = {
Game::Doc::Req("level", "number",
"The level the spell becomes available, or 0 when it has no "
"level requirement."),
};
const Game::Doc::Function kGetSpellLevelLearned{
"The level at which a spell becomes available.", kSpellIDArgs, kLevelLearnedRets};
const Game::Doc::Field kCurrentLevelSpellsArgs[] = {
Game::Doc::Opt("level", "number", nullptr,
"The level to ask about; defaults to the player's own level."),
};
const Game::Doc::Field kCurrentLevelSpellsRets[] = {
Game::Doc::Req("spellIDs", "table",
"A 1-based array of spell IDs, empty when nothing matches."),
};
const Game::Doc::Function kGetCurrentLevelSpells{
"The spells the player's class and race can train at a given level.",
kCurrentLevelSpellsArgs, kCurrentLevelSpellsRets};
const Game::Doc::Field kRequiredTargetLevelRets[] = {
Game::Doc::Opt("level", "number", nullptr,
"The lowest target level, 0 when the spell has no such limit; "
"nil for an unknown spell."),
};
const Game::Doc::Function kGetSpellRequiredTargetLevel{
"The lowest level a target must be for this rank of the spell to apply to it.",
kSpellIDArgs, kRequiredTargetLevelRets, "SpellGlobals"};
const Game::Doc::Field kLevelInfoRets[] = {
Game::Doc::Opt("spellLevel", "number", nullptr,
"The level of this rank; nil for an unknown spell."),
Game::Doc::Opt("baseLevel", "number", nullptr, "The base level of the spell."),
Game::Doc::Opt("maxLevel", "number", nullptr,
"The level at which per-level scaling stops, or 0 for no cap."),
};
const Game::Doc::Function kGetSpellLevelInfo{
"The three level values of a spell: its rank level, base level, and scaling cap.",
kSpellIDArgs, kLevelInfoRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_SpellBook", "GetSpellLevelLearned",
&Script_GetSpellLevelLearned);
&Script_GetSpellLevelLearned,
&kGetSpellLevelLearned);
Game::Lua::RegisterTableFunction("C_SpellBook", "GetCurrentLevelSpells",
&Script_GetCurrentLevelSpells);
&Script_GetCurrentLevelSpells,
&kGetCurrentLevelSpells);
Game::Lua::RegisterGlobalFunction("GetSpellRequiredTargetLevel",
&Script_GetSpellRequiredTargetLevel);
&Script_GetSpellRequiredTargetLevel,
&kGetSpellRequiredTargetLevel);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellLevelInfo",
&Script_GetSpellLevelInfo);
&Script_GetSpellLevelInfo, &kGetSpellLevelInfo);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellRequiredTargetLevel",
&Script_GetSpellRequiredTargetLevel);
&Script_GetSpellRequiredTargetLevel,
&kGetSpellRequiredTargetLevel);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+19 -1
View File
@@ -80,9 +80,27 @@ static int __fastcall Script_GetSpellMechanicByID(void *L) {
return 2;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spellID", "SpellIdentifier",
"A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("mechanicID", "number", nullptr,
"The mechanic the spell applies, 0 when it applies none; "
"nil for an unknown spell."),
Game::Doc::Opt("name", "string", nullptr,
"The English mechanic name, nil when the mechanic has none."),
};
const Game::Doc::Function kGetSpellMechanicByID{
"The crowd-control mechanic a spell applies, with its English name.",
kArgs, kRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellMechanicByID",
&Script_GetSpellMechanicByID);
&Script_GetSpellMechanicByID,
&kGetSpellMechanicByID);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+32 -1
View File
@@ -85,10 +85,41 @@ int __fastcall Script_GetSpellPowerCost(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kSpellPowerCostInfoFields[] = {
Game::Doc::Req("type", "number",
"The power the spell spends: 0 mana, 1 rage, 2 focus, 3 energy, "
"4 happiness."),
Game::Doc::Req("name", "string", "The power token, such as \"MANA\" or \"RAGE\"."),
Game::Doc::Req("cost", "number", "What the cast costs the player now."),
Game::Doc::Req("minCost", "number", "The same value as cost."),
Game::Doc::Req("costPercent", "number",
"The share of the base power pool the cast takes, or 0 for a flat cost."),
Game::Doc::Req("costPerSec", "number", "Always 0."),
Game::Doc::Req("requiredAuraID", "number", "Always 0."),
Game::Doc::Req("hasRequiredAura", "bool", "Always false."),
};
const Game::Doc::Structure kSpellPowerCostInfo{
"SpellPowerCostInfo", "Spell", kSpellPowerCostInfoFields,
"What one cast of a spell costs."};
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("costs", "table", nullptr,
"An array of SpellPowerCostInfo entries; nil when the spell costs "
"nothing or is unknown."),
};
const Game::Doc::Function kGetSpellPowerCost{
"What one cast of the spell costs the player in power.", kArgs, kRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellPowerCost", &Script_GetSpellPowerCost);
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellPowerCost", &Script_GetSpellPowerCost,
&kGetSpellPowerCost);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+30 -2
View File
@@ -109,10 +109,38 @@ static int __fastcall Script_GetSpellRadius(void *L) {
return PushRadiusForSpellID(L, Spell::Lookup::SpellbookSlotToID(slot, bookType));
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kRadiusRets[] = {
Game::Doc::Opt("baseRadius", "number", nullptr,
"The radius in yards for any caster; nil when the spell has no area."),
Game::Doc::Opt("modifiedRadius", "number", nullptr,
"The radius with the local player's talent and item changes applied."),
};
const Game::Doc::Field kByIdentifierArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Function kC_SpellGetSpellRadius{
"The area a spell covers in yards, before and after the player's own changes to it.",
kByIdentifierArgs, kRadiusRets};
const Game::Doc::Field kBySlotArgs[] = {
Game::Doc::Req("slot", "luaIndex", "A 1-based spellbook slot."),
Game::Doc::Opt("bookType", "string", "\"spell\"",
"\"spell\" or \"pet\"; picks the book the slot belongs to."),
};
const Game::Doc::Function kGetSpellRadius{
"The area the spell in a spellbook slot covers in yards, before and after the "
"player's own changes to it.",
kBySlotArgs, kRadiusRets, "SpellGlobals"};
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellRadius",
&Script_C_Spell_GetSpellRadius);
Game::Lua::RegisterGlobalFunction("GetSpellRadius", &Script_GetSpellRadius);
&Script_C_Spell_GetSpellRadius,
&kC_SpellGetSpellRadius);
Game::Lua::RegisterGlobalFunction("GetSpellRadius", &Script_GetSpellRadius,
&kGetSpellRadius);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+23 -1
View File
@@ -60,11 +60,33 @@ int __fastcall Script_C_Spell_GetSpellReagents(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kSpellReagentInfoFields[] = {
Game::Doc::Req("itemID", "number", "The item the cast consumes."),
Game::Doc::Req("count", "number", "How many of that item one cast consumes."),
};
const Game::Doc::Structure kSpellReagentInfo{
"SpellReagentInfo", "Spell", kSpellReagentInfoFields,
"One item a spell consumes when you cast it."};
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("reagents", "table", nullptr,
"An array of SpellReagentInfo entries, empty when the spell uses "
"none; nil for an unknown spell."),
};
const Game::Doc::Function kGetSpellReagents{
"The items a spell consumes each time you cast it.", kArgs, kRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellReagents",
&Script_C_Spell_GetSpellReagents);
&Script_C_Spell_GetSpellReagents,
&kGetSpellReagents);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+33 -3
View File
@@ -90,13 +90,43 @@ static int __fastcall Script_ResetsMeleeSwing(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kSpellArgs[] = {
Game::Doc::Req("spellID", "SpellIdentifier",
"A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kDispelTypeRets[] = {
Game::Doc::Req("dispelType", "number",
"1 magic, 2 curse, 3 disease, 4 poison; 0 when nothing removes it."),
};
const Game::Doc::Function kGetSpellDispelType{
"Which kind of dispel removes the effects of a spell.", kSpellArgs, kDispelTypeRets};
const Game::Doc::Field kNextMeleeRets[] = {
Game::Doc::Req("isNextMelee", "bool",
"True when the ability lands on your next weapon swing."),
};
const Game::Doc::Function kIsNextMeleeSpell{
"Whether the ability waits for your next weapon swing instead of casting.",
kSpellArgs, kNextMeleeRets};
const Game::Doc::Field kResetsSwingRets[] = {
Game::Doc::Req("resetsSwing", "bool",
"True when casting the spell starts the swing timer again."),
};
const Game::Doc::Function kResetsMeleeSwing{
"Whether casting the spell starts your melee swing timer again.",
kSpellArgs, kResetsSwingRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellDispelType",
&Script_GetSpellDispelType);
&Script_GetSpellDispelType, &kGetSpellDispelType);
Game::Lua::RegisterTableFunction("C_Spell", "IsNextMeleeSpell",
&Script_IsNextMeleeSpell);
&Script_IsNextMeleeSpell, &kIsNextMeleeSpell);
Game::Lua::RegisterTableFunction("C_Spell", "ResetsMeleeSwing",
&Script_ResetsMeleeSwing);
&Script_ResetsMeleeSwing, &kResetsMeleeSwing);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+15 -1
View File
@@ -60,9 +60,23 @@ static int __fastcall Script_GetSchoolString(void *L) {
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("schoolMask", "number",
"A damage-school bitmask, such as 4 for fire or 32 for shadow."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("school", "string",
"The localized school name, or \"Unknown\" when the mask names "
"more than one school."),
};
const Game::Doc::Function kGetSchoolString{
"The localized name of a damage school.", kArgs, kRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSchoolString",
&Script_GetSchoolString);
&Script_GetSchoolString, &kGetSchoolString);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+12 -1
View File
@@ -29,6 +29,17 @@ namespace Spell::Subtext {
namespace {
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("subtext", "string", nullptr,
"The rank line, nil when the spell has none."),
};
const Game::Doc::Function kGetSpellSubtext{
"The rank text shown under a spell's name in the spellbook.", kArgs, kRets};
} // namespace
@@ -52,7 +63,7 @@ static int __fastcall Script_GetSpellSubtext(void *L) {
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellSubtext",
&Script_GetSpellSubtext);
&Script_GetSpellSubtext, &kGetSpellSubtext);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+35 -1
View File
@@ -194,11 +194,45 @@ static const Game::Lua::FrameMethodEntry g_methods[] = {
{"HasSpell", &Script_GameTooltipHasSpell},
};
// --- Documentation ----------------------------------------------------------
static const Game::Doc::Field kSpellIDArg[] = {
Game::Doc::Req("spellID", "number", "Any spell ID, learned or not."),
};
static const Game::Doc::Function kSetSpellByID{
"Fills the tooltip with a spell's tooltip.", kSpellIDArg, {}};
static const Game::Doc::Function kAddSpellByID{
"Appends a spell's tooltip to what the tooltip already shows.", kSpellIDArg, {}};
static const Game::Doc::Field kGetSpellRets[] = {
Game::Doc::Opt("name", "string", nullptr, "Nil when the tooltip shows no spell."),
Game::Doc::Opt("rank", "string", nullptr, "Empty for a spell that has no rank."),
Game::Doc::Opt("spellID", "number"),
};
static const Game::Doc::Function kGetSpell{
"The spell the tooltip is showing. Aura tooltips report nothing.", {}, kGetSpellRets};
static const Game::Doc::Field kHasSpellRets[] = {
Game::Doc::Req("hasSpell", "bool", "True while the tooltip shows a spell."),
};
static const Game::Doc::Function kHasSpell{
"Whether the tooltip is showing a spell. Aura tooltips report false.",
{}, kHasSpellRets};
static const Game::Doc::Method g_methodDocs[] = {
{"SetSpellByID", &kSetSpellByID},
{"AddSpellByID", &kAddSpellByID},
{"GetSpell", &kGetSpell},
{"HasSpell", &kHasSpell},
};
static void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])),
g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+29 -2
View File
@@ -210,12 +210,39 @@ int __fastcall Script_C_Spell_IsSpellUsable(void *L) {
return 2;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kLegacyArgs[] = {
Game::Doc::Req("spell", "number", "A spell ID, or a spellbook slot when bookType is given."),
Game::Doc::Opt("bookType", "string", nullptr,
"\"spell\" or \"pet\"; makes the first argument a slot in that book."),
};
const Game::Doc::Field kLegacyRets[] = {
Game::Doc::Opt("usable", "number", nullptr, "1 when the spell can be cast now, else nil."),
Game::Doc::Opt("noMana", "number", nullptr, "1 when power is the only block, else nil."),
};
const Game::Doc::Function kIsUsableSpell{
"Whether the player can cast the spell now, in 1/nil pairs like IsUsableAction; "
"cooldown is not part of the answer.",
kLegacyArgs, kLegacyRets, "SpellGlobals"};
const Game::Doc::Field kModernArgs[] = {
Game::Doc::Req("spell", "SpellIdentifier", "A spell ID, spell link, or spell name."),
};
const Game::Doc::Field kModernRets[] = {
Game::Doc::Req("isUsable", "bool", "True when the spell can be cast now."),
Game::Doc::Req("insufficientPower", "bool", "True when power is the only block."),
};
const Game::Doc::Function kIsSpellUsable{
"Whether the player can cast the spell now; cooldown is not part of the answer.",
kModernArgs, kModernRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("IsUsableSpell", &Script_IsUsableSpell);
Game::Lua::RegisterGlobalFunction("IsUsableSpell", &Script_IsUsableSpell, &kIsUsableSpell);
Game::Lua::RegisterTableFunction("C_Spell", "IsSpellUsable",
&Script_C_Spell_IsSpellUsable);
&Script_C_Spell_IsSpellUsable, &kIsSpellUsable);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+211
View File
@@ -0,0 +1,211 @@
<#
.SYNOPSIS
Emits paste-ready `Game::Doc` descriptor skeletons for registrations that
do not have one yet.
.DESCRIPTION
Every `Game::Lua::Register*` call can carry a documentation descriptor (see
`namespace Game::Doc` in src/Game.h). Roughly 700 registrations still need
one. This script does the mechanical part of each: it finds the registered
names in a source file, recovers the argument list from that file's
`Usage:` strings, recovers a summary from the function's section in
docs/API.md, and writes a skeleton you paste into the file.
It writes ONLY into build/docskel. It never edits src/.
Every guess it makes is marked `// TODO`. A skeleton is a starting point,
not an answer: the `Script_*` body is the authority on the real signature,
and docs/API.md is stale in places. Read the body, fix the skeleton, then
paste it.
.PARAMETER Path
Source files to process. Accepts globs. Defaults to every .cpp under src.
.PARAMETER OutDir
Where to write the skeletons. Defaults to build/docskel.
.EXAMPLE
./tools/New-ApiDocSkeletons.ps1 -Path src/item/*.cpp
Writes build/docskel/item/<file>.skel.cpp for each item module.
#>
[CmdletBinding()]
param(
[string[]] $Path = @('src/**/*.cpp'),
[string] $OutDir = 'build/docskel'
)
$ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent $PSScriptRoot
Set-Location $repo
# ---------------------------------------------------------------------------
# docs/API.md: heading -> first sentence of the paragraph under it.
# Headings look like: ### `C_Spell.GetSpellInfo(spellID)`
# or a combined pair: ### `IsUsableSpell(spell)` / `IsUsableSpell(slot, ...)`
# ---------------------------------------------------------------------------
function Read-ApiSummaries {
$summaries = @{}
$lines = Get-Content 'docs/API.md' -Encoding UTF8
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -notmatch '^###\s+(.+)$') { continue }
$heading = $Matches[1]
# Every `Name(...)` in the heading maps to the same summary.
$names = [regex]::Matches($heading, '`([A-Za-z_][A-Za-z0-9_.:]*)\s*\(') |
ForEach-Object { $_.Groups[1].Value }
if (-not $names) { continue }
# First non-blank prose line after the heading, trimmed to one sentence.
$summary = $null
for ($j = $i + 1; $j -lt [Math]::Min($i + 8, $lines.Count); $j++) {
$line = $lines[$j].Trim()
if (-not $line) { continue }
if ($line -match '^(```|\||>|#|-\s)') { break }
$summary = $line
break
}
if (-not $summary) { continue }
if ($summary -match '^(.+?[.!?])(\s|$)') { $summary = $Matches[1] }
$summary = $summary -replace '\[([^\]]+)\]\([^)]+\)', '$1' `
-replace '[`*]', '' -replace '"', '\"'
foreach ($n in $names) {
$short = ($n -split '\.')[-1]
if (-not $summaries.ContainsKey($short)) { $summaries[$short] = $summary }
}
}
return $summaries
}
# ---------------------------------------------------------------------------
# Argument-name -> Blizzard type guess. Anything unmatched is TODO.
# ---------------------------------------------------------------------------
function Get-TypeGuess([string] $name) {
switch -Regex ($name) {
'^unit' { return 'UnitToken' }
'^(spell|spellIdentifier)$' { return 'SpellIdentifier' }
'^(item|itemIdentifier|itemInfo)$' { return 'ItemInfo' }
'(ID|Id|Index|Count|Slot|Level|Amount|Duration|Rate|Time)$' { return 'number' }
'^(index|slot|count|level|duration)$' { return 'number' }
'^(name|link|text|token|path|texture|locale|type|bookType)$' { return 'string' }
'^(is|has|can|does|should)[A-Z]' { return 'bool' }
default { return 'TODO' }
}
}
# Parse the inside of a `Usage:` signature into Doc::Req / Doc::Opt lines.
# `[, x]` marks the start of the optional tail; `...` is a vararg.
function ConvertTo-Fields([string] $argText) {
$fields = @()
if (-not $argText.Trim()) { return $fields }
$optionalFrom = $argText.IndexOf('[')
$clean = $argText -replace '[\[\]]', ''
$parts = $clean -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
$consumed = 0
foreach ($p in $parts) {
$bare = $p -replace '^"', '' -replace '"$', ''
$consumed += $p.Length
if ($bare -eq '...') {
$fields += ' Game::Doc::Vararg("TODO"), // TODO: type'
continue
}
$type = Get-TypeGuess $bare
$todo = if ($type -eq 'TODO') { ' // TODO: type' } else { '' }
$isOptional = $optionalFrom -ge 0 -and $argText.IndexOf($p) -gt $optionalFrom
if ($isOptional) {
$fields += " Game::Doc::Opt(`"$bare`", `"$type`"),$todo"
} else {
$fields += " Game::Doc::Req(`"$bare`", `"$type`"),$todo"
}
}
return $fields
}
$summaries = Read-ApiSummaries
Write-Host "docs/API.md: $($summaries.Keys.Count) summaries indexed"
$files = @()
foreach ($p in $Path) { $files += Get-ChildItem -Path $p -File -Recurse -ErrorAction SilentlyContinue }
$files = $files | Sort-Object FullName -Unique
$written = 0
foreach ($file in $files) {
$text = Get-Content $file.FullName -Raw
$rel = $file.FullName.Substring($repo.Length + 1) -replace '\\', '/'
# Registrations that already carry a descriptor end with `, &kSomething)`.
$globals = [regex]::Matches($text, 'RegisterGlobalFunction\(\s*"([^"]+)"\s*,\s*&(\w+)\s*\)')
$glue = [regex]::Matches($text, 'RegisterGlueFunction\(\s*"([^"]+)"\s*,\s*&(\w+)\s*\)')
$tables = [regex]::Matches($text, 'RegisterTableFunction\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*&(\w+)\s*\)')
if ($globals.Count + $glue.Count + $tables.Count -eq 0) { continue }
# `Usage: Name(a [, b])` strings, keyed by the bare function name.
$usage = @{}
foreach ($m in [regex]::Matches($text, 'Usage:\s*([A-Za-z_][A-Za-z0-9_.:]*)\s*\(([^)"]*)\)')) {
$short = ($m.Groups[1].Value -split '[.:]')[-1]
if (-not $usage.ContainsKey($short)) { $usage[$short] = $m.Groups[2].Value }
}
$out = New-Object System.Collections.Generic.List[string]
$out.Add("// Descriptor skeletons for $rel")
$out.Add('//')
$out.Add('// Paste into the file''s anonymous namespace, under a')
$out.Add('// // --- Documentation ---')
$out.Add('// banner, then add the `&kName` argument to each Register* call.')
$out.Add('//')
$out.Add('// VERIFY EVERY LINE against the Script_* body. Types and optionality are')
$out.Add('// guessed from the Usage: string; summaries come from docs/API.md, which')
$out.Add('// is stale in places. The body is the authority.')
$out.Add('')
$emit = {
param($luaName, $system)
$short = ($luaName -split '\.')[-1]
# One symbol per REGISTERED NAME, not per short name: a global and a
# table function often share a short name and would collide.
$sym = 'k' + (($luaName -replace '[^A-Za-z0-9]', ''))
$summary = if ($summaries.ContainsKey($short)) { $summaries[$short] } else { 'TODO: one sentence.' }
$argText = if ($usage.ContainsKey($short)) { $usage[$short] } else { $null }
$out.Add("// $luaName")
if ($null -eq $argText) {
$out.Add("// TODO: no Usage: string found - read the Script_* body for the arguments.")
}
$fields = if ($argText) { ConvertTo-Fields $argText } else { @() }
$argsRef = '{}'
if ($fields.Count -gt 0) {
$out.Add("const Game::Doc::Field ${sym}Args[] = {")
$fields | ForEach-Object { $out.Add($_) }
$out.Add('};')
$argsRef = "${sym}Args"
}
$out.Add("const Game::Doc::Field ${sym}Rets[] = {")
$out.Add(' // TODO: one entry per pushed value, in order.')
$out.Add(' Game::Doc::Req("TODO", "TODO"),')
$out.Add('};')
$sysArg = if ($system) { ", `"$system`"" } else { '' }
$out.Add("const Game::Doc::Function $sym{")
$out.Add(" `"$summary`",")
$out.Add(" $argsRef, ${sym}Rets$sysArg};")
$out.Add('')
}
foreach ($m in $globals) { & $emit $m.Groups[1].Value 'TODOGlobals' }
foreach ($m in $glue) { & $emit $m.Groups[1].Value 'TODOGlobals' }
foreach ($m in $tables) { & $emit "$($m.Groups[1].Value).$($m.Groups[2].Value)" $null }
$dest = Join-Path $OutDir ($rel -replace '^src/', '')
$dest = [IO.Path]::ChangeExtension($dest, $null) + 'skel.cpp'
$destDir = Split-Path -Parent $dest
if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Force $destDir | Out-Null }
$noBom = New-Object System.Text.UTF8Encoding($false)
[IO.File]::WriteAllText($dest, ($out -join "`r`n") + "`r`n", $noBom)
$written++
Write-Host " $rel -> $dest"
}
Write-Host ""
Write-Host "$written skeleton file(s) written to $OutDir."
Write-Host "Nothing under src/ was modified."