mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
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:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// API documentation: the record of every Lua registration plus its
|
||||
// descriptor, exported on demand as Blizzard `APIDocumentation` tables.
|
||||
//
|
||||
// C_APIDocumentation.GetSystems() -> sorted array of system Names
|
||||
// C_APIDocumentation.GetSystem(name) -> one System table (Blizzard shape), or nil
|
||||
// _classicapi_UndocumentedAPI() -> array of registrations lacking a
|
||||
// descriptor, and its count
|
||||
//
|
||||
// The registrars in Game.cpp call `Record*` (see Documentation.h). Recording
|
||||
// runs once per Lua state — the first in-game pass and the first glue pass —
|
||||
// because the set of registrations is static: every module registers the
|
||||
// same names on every `/reload`. The recorded list is never materialized
|
||||
// into Lua wholesale; `GetSystem` builds ONE system's table per call, which
|
||||
// is how Blizzard's browser loads too (its generated data addon is
|
||||
// LoadOnDemand). Eager export would create thousands of tables on every
|
||||
// `/reload` for a feature used from a slash command.
|
||||
//
|
||||
// System grouping is derived, never authored, except for two strings:
|
||||
// - `C_Spell.X` -> System "Spell", Namespace "C_Spell" (strip `C_`)
|
||||
// - `table.insert` -> System "table", Namespace "table"
|
||||
// - a global -> `doc->system` ("SpellGlobals"); a global whose
|
||||
// descriptor names no system has no home and is
|
||||
// listed by the coverage function
|
||||
// - a frame method -> Blizzard's widget system for its registry
|
||||
// ("SimpleTextureAPI", Type "ScriptObject")
|
||||
// - an enum / structure /
|
||||
// event -> the System named in its declaration
|
||||
// A system's `Environment` is "All" when its functions were recorded in both
|
||||
// Lua states, else "Game" or "Glue"; a function whose own environment is
|
||||
// narrower than its system's gets a Documentation line saying so.
|
||||
|
||||
#include "api/Documentation.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Api::Documentation {
|
||||
|
||||
using Game::Doc::Field;
|
||||
using Game::Doc::FieldList;
|
||||
using Game::Doc::Function;
|
||||
using Game::Doc::Method;
|
||||
using Game::Doc::Structure;
|
||||
// NOT `using Game::Doc::Event` — that name would shadow the global `Event`
|
||||
// namespace for the whole file, so `Event::Custom::…` would resolve to the
|
||||
// type. Same trap as `Time::Tick` vs the `Tick` namespace (see CLAUDE.md).
|
||||
using EventDoc = Game::Doc::Event;
|
||||
|
||||
// Head of the self-registering `Structure` list. Constant-initialized, so it
|
||||
// is valid before any `Structure` constructor runs, whatever the cross-TU
|
||||
// static-init order.
|
||||
const Structure *g_structureHead = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
enum Kind : uint8_t {
|
||||
KIND_GLOBAL, // RegisterGlobalFunction / RegisterGlueFunction / RegisterGlobalAlias
|
||||
KIND_TABLE, // RegisterTableFunction
|
||||
KIND_FRAME_METHOD, // RegisterFrameMethods, one per entry
|
||||
KIND_ENUM, // RegisterIntegerEnum
|
||||
KIND_ORPHAN_METHOD_DOC // a Doc::Method naming no entry in its table
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
Kind kind;
|
||||
unsigned env; // Env bits the registration was recorded in
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum")
|
||||
const char *name; // registered name / method name / enum sub-name
|
||||
const void *registry; // frame methods only
|
||||
const Function *doc; // nullptr = undocumented
|
||||
const char *enumSystem;
|
||||
const Game::Lua::EnumIntegerEntry *enumEntries;
|
||||
int enumCount;
|
||||
};
|
||||
|
||||
std::vector<Entry> g_entries;
|
||||
Env g_pass = ENV_NONE;
|
||||
unsigned g_sealed = 0;
|
||||
int g_recordedThisPass = 0;
|
||||
|
||||
constexpr const char *kDebugPrefix = "_classicapi_";
|
||||
|
||||
bool IsDebugName(const char *name) {
|
||||
return std::strncmp(name, kDebugPrefix, std::strlen(kDebugPrefix)) == 0;
|
||||
}
|
||||
|
||||
bool SameText(const char *a, const char *b) {
|
||||
if (a == nullptr || b == nullptr)
|
||||
return a == b;
|
||||
return std::strcmp(a, b) == 0;
|
||||
}
|
||||
|
||||
bool SameTextNoCase(const char *a, const char *b) {
|
||||
if (a == nullptr || b == nullptr)
|
||||
return a == b;
|
||||
return _stricmp(a, b) == 0;
|
||||
}
|
||||
|
||||
Entry *Find(Kind kind, const char *table, const char *name, const void *registry) {
|
||||
for (Entry &e : g_entries) {
|
||||
if (e.kind == kind && e.registry == registry && SameText(e.table, table) &&
|
||||
SameText(e.name, name))
|
||||
return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Record(Kind kind, const char *table, const char *name, const void *registry,
|
||||
const Function *doc) {
|
||||
if (g_pass == ENV_NONE || name == nullptr)
|
||||
return;
|
||||
++g_recordedThisPass;
|
||||
if (Entry *e = Find(kind, table, name, registry)) {
|
||||
e->env |= g_pass;
|
||||
if (e->doc == nullptr)
|
||||
e->doc = doc;
|
||||
return;
|
||||
}
|
||||
Entry e{};
|
||||
e.kind = kind;
|
||||
e.env = g_pass;
|
||||
e.table = table;
|
||||
e.name = name;
|
||||
e.registry = registry;
|
||||
e.doc = doc;
|
||||
g_entries.push_back(e);
|
||||
}
|
||||
|
||||
// ---- System resolution ------------------------------------------------------
|
||||
|
||||
// Blizzard's names for the widget-method systems, keyed by the engine
|
||||
// registry each `FrameMethodEntry[]` is bound to.
|
||||
struct WidgetSystem {
|
||||
uintptr_t registry;
|
||||
const char *system;
|
||||
};
|
||||
const WidgetSystem kWidgetSystems[] = {
|
||||
{Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY, "GameTooltipAPI"},
|
||||
{Offsets::VAR_REGION_METHOD_REGISTRY, "ScriptRegionAPI"},
|
||||
{Offsets::VAR_FRAME_METHOD_REGISTRY, "SimpleFrameAPI"},
|
||||
{Offsets::VAR_TEXTURE_METHOD_REGISTRY, "SimpleTextureAPI"},
|
||||
{Offsets::VAR_FONTSTRING_METHOD_REGISTRY, "SimpleFontStringAPI"},
|
||||
{Offsets::VAR_EDITBOX_METHOD_REGISTRY, "SimpleEditBoxAPI"},
|
||||
{Offsets::VAR_MODEL_METHOD_REGISTRY, "SimpleModelAPI"},
|
||||
};
|
||||
|
||||
const char *WidgetSystemFor(const void *registry) {
|
||||
for (const WidgetSystem &w : kWidgetSystems)
|
||||
if (reinterpret_cast<const void *>(w.registry) == registry)
|
||||
return w.system;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// "C_Spell" -> "Spell"; anything else is its own system name.
|
||||
const char *SystemNameForTable(const char *table) {
|
||||
if (table[0] == 'C' && table[1] == '_' && table[2] != '\0')
|
||||
return table + 2;
|
||||
return table;
|
||||
}
|
||||
|
||||
// The System an entry contributes a function to, or nullptr when it has
|
||||
// none (an undocumented global, a frame method on an unmapped registry).
|
||||
const char *SystemOf(const Entry &e) {
|
||||
switch (e.kind) {
|
||||
case KIND_TABLE:
|
||||
return SystemNameForTable(e.table);
|
||||
case KIND_GLOBAL:
|
||||
return e.doc != nullptr ? e.doc->system : nullptr;
|
||||
case KIND_FRAME_METHOD:
|
||||
return WidgetSystemFor(e.registry);
|
||||
case KIND_ENUM:
|
||||
return e.enumSystem;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
struct SystemInfo {
|
||||
const char *name;
|
||||
const char *ns; // nullptr for global and widget systems
|
||||
const char *type; // "System" or "ScriptObject"
|
||||
unsigned env; // Env bits; 0 when nothing recorded (structures only)
|
||||
};
|
||||
|
||||
// Merge one entry's contribution into the system list.
|
||||
void AddSystem(std::vector<SystemInfo> &systems, const char *name, const char *ns,
|
||||
const char *type, unsigned env) {
|
||||
if (name == nullptr)
|
||||
return;
|
||||
for (SystemInfo &s : systems) {
|
||||
if (SameTextNoCase(s.name, name)) {
|
||||
if (s.ns == nullptr)
|
||||
s.ns = ns;
|
||||
if (type != nullptr)
|
||||
s.type = type;
|
||||
s.env |= env;
|
||||
return;
|
||||
}
|
||||
}
|
||||
systems.push_back(SystemInfo{name, ns, type != nullptr ? type : "System", env});
|
||||
}
|
||||
|
||||
std::vector<SystemInfo> CollectSystems() {
|
||||
std::vector<SystemInfo> systems;
|
||||
for (const Entry &e : g_entries) {
|
||||
switch (e.kind) {
|
||||
case KIND_TABLE:
|
||||
AddSystem(systems, SystemNameForTable(e.table), e.table, "System", e.env);
|
||||
break;
|
||||
case KIND_GLOBAL:
|
||||
AddSystem(systems, SystemOf(e), nullptr, "System", e.env);
|
||||
break;
|
||||
case KIND_FRAME_METHOD:
|
||||
AddSystem(systems, SystemOf(e), nullptr, "ScriptObject", e.env);
|
||||
break;
|
||||
case KIND_ENUM:
|
||||
AddSystem(systems, e.enumSystem, nullptr, nullptr, 0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const Structure *s = g_structureHead; s != nullptr; s = s->next)
|
||||
AddSystem(systems, s->system, nullptr, nullptr, 0);
|
||||
const int events = Event::Custom::ReservedCount();
|
||||
for (int i = 0; i < events; ++i) {
|
||||
const EventDoc *doc = Event::Custom::ReservedDoc(i);
|
||||
if (doc != nullptr)
|
||||
AddSystem(systems, doc->system, nullptr, nullptr, 0);
|
||||
}
|
||||
return systems;
|
||||
}
|
||||
|
||||
const SystemInfo *FindSystem(const std::vector<SystemInfo> &systems, const char *query) {
|
||||
for (const SystemInfo &s : systems)
|
||||
if (SameTextNoCase(s.name, query) || SameTextNoCase(s.ns, query))
|
||||
return &s;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *EnvironmentName(unsigned env) {
|
||||
if (env == (ENV_GAME | ENV_GLUE) || env == 0)
|
||||
return "All";
|
||||
return env == ENV_GLUE ? "Glue" : "Game";
|
||||
}
|
||||
|
||||
// ---- Export builders (each leaves exactly what its comment says) -------------
|
||||
|
||||
namespace Lua = Game::Lua;
|
||||
|
||||
// Sets `Documentation = { line1 [, line2 [, line3]] }` on the table at
|
||||
// stack[-1]; no-op when every line is null.
|
||||
void SetDocumentation(void *L, const char *l1, const char *l2 = nullptr,
|
||||
const char *l3 = nullptr) {
|
||||
if (l1 == nullptr && l2 == nullptr && l3 == nullptr)
|
||||
return;
|
||||
Lua::PushString(L, "Documentation");
|
||||
Lua::NewTable(L);
|
||||
int n = 0;
|
||||
for (const char *line : {l1, l2, l3}) {
|
||||
if (line == nullptr)
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
Lua::PushString(L, line);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// A Lua literal from a descriptor's `defaultValue`, pushed as the value it
|
||||
// denotes: `true`/`false` -> boolean, a quoted string -> string, else number.
|
||||
void PushDefault(void *L, const char *literal) {
|
||||
if (std::strcmp(literal, "true") == 0) {
|
||||
Lua::PushBool(L, true);
|
||||
} else if (std::strcmp(literal, "false") == 0) {
|
||||
Lua::PushBool(L, false);
|
||||
} else if (literal[0] == '"') {
|
||||
const size_t len = std::strlen(literal);
|
||||
Lua::PushLString(L, literal + 1, static_cast<unsigned>(len >= 2 ? len - 2 : 0));
|
||||
} else {
|
||||
Lua::PushNumber(L, std::strtod(literal, nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
// Pushes one field record: { Name, Type, Nilable [, Default] [, Documentation] }.
|
||||
void PushField(void *L, const Field &f) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", f.name);
|
||||
Lua::SetFieldString(L, "Type", f.type);
|
||||
Lua::SetFieldBool(L, "Nilable", f.nilable);
|
||||
if (f.defaultValue != nullptr) {
|
||||
Lua::PushString(L, "Default");
|
||||
PushDefault(L, f.defaultValue);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
SetDocumentation(L, f.doc);
|
||||
}
|
||||
|
||||
// Sets `stack[-1][key] = { field records }`; no-op for an empty list.
|
||||
void SetFieldArray(void *L, const char *key, FieldList list) {
|
||||
if (list.count <= 0)
|
||||
return;
|
||||
Lua::PushString(L, key);
|
||||
Lua::NewTable(L);
|
||||
for (int i = 0; i < list.count; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
PushField(L, list.data[i]);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
constexpr const char *kExtensionNote = "ClassicAPI extension - not part of the Blizzard API.";
|
||||
constexpr const char *kGlueOnlyNote = "Available only on the glue screen.";
|
||||
constexpr const char *kGameOnlyNote = "Available only in-game.";
|
||||
// An undocumented function lists with no arguments, which is indistinguishable
|
||||
// from a function that genuinely takes none. Say which it is, so an empty
|
||||
// signature is never read as a real one.
|
||||
constexpr const char *kUndocumentedNote =
|
||||
"Not documented yet. Its arguments and return values are not listed here.";
|
||||
|
||||
// Pushes one function record. An undocumented entry still lists by name so
|
||||
// `/classicapi <System> list` shows the whole surface; its shape arrives with
|
||||
// the descriptor.
|
||||
void PushFunction(void *L, const Entry &e, unsigned systemEnv) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.name);
|
||||
Lua::SetFieldString(L, "Type", "Function");
|
||||
const char *envNote = nullptr;
|
||||
if (e.env != systemEnv && e.env != 0)
|
||||
envNote = (e.env == ENV_GLUE) ? kGlueOnlyNote : kGameOnlyNote;
|
||||
if (e.doc == nullptr) {
|
||||
SetDocumentation(L, kUndocumentedNote, envNote);
|
||||
return;
|
||||
}
|
||||
SetDocumentation(L, e.doc->summary, e.doc->extension ? kExtensionNote : nullptr, envNote);
|
||||
SetFieldArray(L, "Arguments", e.doc->args);
|
||||
SetFieldArray(L, "Returns", e.doc->rets);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Enumeration", NumValues, MinValue, MaxValue,
|
||||
// Fields = { { Name, Type = <enum>, EnumValue } } }.
|
||||
void PushEnumeration(void *L, const Entry &e) {
|
||||
int minValue = 0, maxValue = 0;
|
||||
for (int i = 0; i < e.enumCount; ++i) {
|
||||
const int v = e.enumEntries[i].value;
|
||||
if (i == 0 || v < minValue)
|
||||
minValue = v;
|
||||
if (i == 0 || v > maxValue)
|
||||
maxValue = v;
|
||||
}
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.name);
|
||||
Lua::SetFieldString(L, "Type", "Enumeration");
|
||||
Lua::SetFieldNumber(L, "NumValues", e.enumCount);
|
||||
Lua::SetFieldNumber(L, "MinValue", minValue);
|
||||
Lua::SetFieldNumber(L, "MaxValue", maxValue);
|
||||
Lua::PushString(L, "Fields");
|
||||
Lua::NewTable(L);
|
||||
for (int i = 0; i < e.enumCount; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.enumEntries[i].key);
|
||||
Lua::SetFieldString(L, "Type", e.name);
|
||||
Lua::SetFieldNumber(L, "EnumValue", e.enumEntries[i].value);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Structure" [, Documentation], Fields = { … } }.
|
||||
void PushStructure(void *L, const Structure &s) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", s.name);
|
||||
Lua::SetFieldString(L, "Type", "Structure");
|
||||
SetDocumentation(L, s.summary);
|
||||
SetFieldArray(L, "Fields", s.fields);
|
||||
}
|
||||
|
||||
// "LOSS_OF_CONTROL_ADDED" -> "LossOfControlAdded" (Blizzard's event Name).
|
||||
void PascalCase(const char *literal, char *out, size_t cap) {
|
||||
size_t o = 0;
|
||||
bool upper = true;
|
||||
for (const char *p = literal; *p != '\0' && o + 1 < cap; ++p) {
|
||||
if (*p == '_') {
|
||||
upper = true;
|
||||
continue;
|
||||
}
|
||||
const unsigned char c = static_cast<unsigned char>(*p);
|
||||
out[o++] = static_cast<char>(upper ? std::toupper(c) : std::tolower(c));
|
||||
upper = false;
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Event", LiteralName [, Documentation] [, Payload] }.
|
||||
void PushEvent(void *L, const char *literal, const EventDoc &doc) {
|
||||
char name[128];
|
||||
PascalCase(literal, name, sizeof name);
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", name);
|
||||
Lua::SetFieldString(L, "Type", "Event");
|
||||
Lua::SetFieldString(L, "LiteralName", literal);
|
||||
SetDocumentation(L, doc.summary);
|
||||
SetFieldArray(L, "Payload", doc.payload);
|
||||
}
|
||||
|
||||
// Pushes the whole System table for `sys`.
|
||||
void PushSystem(void *L, const SystemInfo &sys) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", sys.name);
|
||||
Lua::SetFieldString(L, "Type", sys.type);
|
||||
if (sys.ns != nullptr)
|
||||
Lua::SetFieldString(L, "Namespace", sys.ns);
|
||||
Lua::SetFieldString(L, "Environment", EnvironmentName(sys.env));
|
||||
|
||||
Lua::PushString(L, "Functions");
|
||||
Lua::NewTable(L);
|
||||
int n = 0;
|
||||
for (const Entry &e : g_entries) {
|
||||
if (e.kind != KIND_GLOBAL && e.kind != KIND_TABLE && e.kind != KIND_FRAME_METHOD)
|
||||
continue;
|
||||
if (!SameTextNoCase(SystemOf(e), sys.name) || IsDebugName(e.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushFunction(L, e, sys.env);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
|
||||
Lua::PushString(L, "Events");
|
||||
Lua::NewTable(L);
|
||||
n = 0;
|
||||
const int events = Event::Custom::ReservedCount();
|
||||
for (int i = 0; i < events; ++i) {
|
||||
const EventDoc *doc = Event::Custom::ReservedDoc(i);
|
||||
if (doc == nullptr || !SameTextNoCase(doc->system, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushEvent(L, Event::Custom::ReservedName(i), *doc);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
|
||||
Lua::PushString(L, "Tables");
|
||||
Lua::NewTable(L);
|
||||
n = 0;
|
||||
for (const Structure *s = g_structureHead; s != nullptr; s = s->next) {
|
||||
if (!SameTextNoCase(s->system, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushStructure(L, *s);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
for (const Entry &e : g_entries) {
|
||||
if (e.kind != KIND_ENUM || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushEnumeration(L, e);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// ---- Coverage ---------------------------------------------------------------
|
||||
|
||||
std::vector<std::string> Undocumented() {
|
||||
std::vector<std::string> out;
|
||||
const std::vector<SystemInfo> systems = CollectSystems();
|
||||
for (const Entry &e : g_entries) {
|
||||
if (IsDebugName(e.name))
|
||||
continue;
|
||||
switch (e.kind) {
|
||||
case KIND_GLOBAL:
|
||||
if (e.doc == nullptr) {
|
||||
out.push_back(e.name);
|
||||
} else if (e.doc->system == nullptr) {
|
||||
out.push_back(std::string(e.name) + " (doc has no system)");
|
||||
} else if (const SystemInfo *s = FindSystem(systems, e.doc->system);
|
||||
s != nullptr && s->ns != nullptr) {
|
||||
out.push_back(std::string(e.name) + " (system '" + e.doc->system +
|
||||
"' is namespaced)");
|
||||
}
|
||||
break;
|
||||
case KIND_TABLE:
|
||||
if (e.doc == nullptr)
|
||||
out.push_back(std::string(e.table) + "." + e.name);
|
||||
break;
|
||||
case KIND_FRAME_METHOD: {
|
||||
const char *sys = WidgetSystemFor(e.registry);
|
||||
if (sys == nullptr)
|
||||
out.push_back(std::string(e.name) + " (method registry not mapped)");
|
||||
else if (e.doc == nullptr)
|
||||
out.push_back(std::string(sys) + ":" + e.name);
|
||||
break;
|
||||
}
|
||||
case KIND_ORPHAN_METHOD_DOC: {
|
||||
const char *sys = WidgetSystemFor(e.registry);
|
||||
out.push_back(std::string(sys != nullptr ? sys : "?") + ":" + e.name +
|
||||
" (doc for unregistered method)");
|
||||
break;
|
||||
}
|
||||
case KIND_ENUM:
|
||||
if (e.enumSystem == nullptr)
|
||||
out.push_back(std::string(e.table) + "." + e.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const int events = Event::Custom::ReservedCount();
|
||||
for (int i = 0; i < events; ++i)
|
||||
if (Event::Custom::ReservedDoc(i) == nullptr)
|
||||
out.push_back(std::string("event ") + Event::Custom::ReservedName(i));
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- Lua surface ------------------------------------------------------------
|
||||
|
||||
int __fastcall Script_GetSystems(void *L) {
|
||||
std::vector<SystemInfo> systems = CollectSystems();
|
||||
std::sort(systems.begin(), systems.end(), [](const SystemInfo &a, const SystemInfo &b) {
|
||||
return _stricmp(a.name, b.name) < 0;
|
||||
});
|
||||
Lua::NewTable(L);
|
||||
for (size_t i = 0; i < systems.size(); ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::PushString(L, systems[i].name);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSystem(void *L) {
|
||||
if (!Lua::IsString(L, 1)) {
|
||||
Lua::Error(L, "Usage: C_APIDocumentation.GetSystem(name)");
|
||||
return 0;
|
||||
}
|
||||
const char *query = Lua::ToString(L, 1);
|
||||
const std::vector<SystemInfo> systems = CollectSystems();
|
||||
const SystemInfo *sys = FindSystem(systems, query);
|
||||
if (sys == nullptr)
|
||||
return 0; // nil
|
||||
Lua::CheckStack(L, 32);
|
||||
PushSystem(L, *sys);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_UndocumentedAPI(void *L) {
|
||||
const std::vector<std::string> names = Undocumented();
|
||||
Lua::NewTable(L);
|
||||
for (size_t i = 0; i < names.size(); ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::PushString(L, names[i].c_str());
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::PushNumber(L, static_cast<double>(names.size()));
|
||||
return 2;
|
||||
}
|
||||
|
||||
const Field kGetSystemsRets[] = {
|
||||
Game::Doc::Req("systems", "table", "Array of system Names, sorted without regard to case."),
|
||||
};
|
||||
const Function kGetSystems{
|
||||
"Returns the name of every documented system.",
|
||||
{}, kGetSystemsRets, nullptr, true};
|
||||
|
||||
const Field kGetSystemArgs[] = {
|
||||
Game::Doc::Req("system", "string", "A system Name or Namespace; case does not matter."),
|
||||
};
|
||||
const Field kGetSystemRets[] = {
|
||||
Game::Doc::Opt("system", "table", nullptr,
|
||||
"The system in Blizzard's APIDocumentation table shape, or nil when unknown."),
|
||||
};
|
||||
const Function kGetSystem{
|
||||
"Returns one system's documentation table.",
|
||||
kGetSystemArgs, kGetSystemRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_APIDocumentation", "GetSystems", &Script_GetSystems,
|
||||
&kGetSystems);
|
||||
Game::Lua::RegisterTableFunction("C_APIDocumentation", "GetSystem", &Script_GetSystem,
|
||||
&kGetSystem);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_UndocumentedAPI", &Script_UndocumentedAPI);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- Recording (called from Game.cpp) ----------------------------------------
|
||||
|
||||
void BeginPass(Env env) {
|
||||
g_pass = (g_sealed & env) != 0 ? ENV_NONE : env;
|
||||
g_recordedThisPass = 0;
|
||||
}
|
||||
|
||||
void EndPass() {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
g_sealed |= g_pass;
|
||||
const Env done = g_pass;
|
||||
g_pass = ENV_NONE;
|
||||
const std::vector<std::string> missing = Undocumented();
|
||||
Debug::Log::Printf("[doc] %s pass: %d registrations recorded, %d undocumented",
|
||||
done == ENV_GLUE ? "glue" : "game", g_recordedThisPass,
|
||||
static_cast<int>(missing.size()));
|
||||
}
|
||||
|
||||
void RecordGlobal(const char *name, const Function *doc) {
|
||||
Record(KIND_GLOBAL, nullptr, name, nullptr, doc);
|
||||
}
|
||||
|
||||
void RecordTable(const char *table, const char *name, const Function *doc) {
|
||||
Record(KIND_TABLE, table, name, nullptr, doc);
|
||||
}
|
||||
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Method *docs, int docCount) {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const Function *doc = nullptr;
|
||||
for (int d = 0; d < docCount; ++d) {
|
||||
if (std::strcmp(docs[d].name, table[i].name) == 0) {
|
||||
doc = docs[d].doc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Record(KIND_FRAME_METHOD, nullptr, table[i].name, registry, doc);
|
||||
}
|
||||
for (int d = 0; d < docCount; ++d) {
|
||||
bool matched = false;
|
||||
for (int i = 0; i < count && !matched; ++i)
|
||||
matched = std::strcmp(docs[d].name, table[i].name) == 0;
|
||||
if (!matched)
|
||||
Record(KIND_ORPHAN_METHOD_DOC, nullptr, docs[d].name, registry, docs[d].doc);
|
||||
}
|
||||
}
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
if (g_pass == ENV_NONE || sub == nullptr)
|
||||
return;
|
||||
++g_recordedThisPass;
|
||||
if (Entry *e = Find(KIND_ENUM, parent, sub, nullptr)) {
|
||||
e->env |= g_pass;
|
||||
if (e->enumSystem == nullptr)
|
||||
e->enumSystem = system;
|
||||
return;
|
||||
}
|
||||
Entry e{};
|
||||
e.kind = KIND_ENUM;
|
||||
e.env = g_pass;
|
||||
e.table = parent;
|
||||
e.name = sub;
|
||||
e.enumSystem = system;
|
||||
e.enumEntries = entries;
|
||||
e.enumCount = count;
|
||||
g_entries.push_back(e);
|
||||
}
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
// The Structure constructor lives here so the list head is private to this
|
||||
// TU; declared in Game.h beside the other descriptor types.
|
||||
Game::Doc::Structure::Structure(const char *n, const char *s, FieldList f, const char *sum)
|
||||
: name(n), system(s), fields(f), summary(sum), next(Api::Documentation::g_structureHead) {
|
||||
Api::Documentation::g_structureHead = this;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
// Recording side of the API documentation. The registrars in Game.cpp call
|
||||
// these; `Documentation.cpp` keeps the list, resolves systems, and exports
|
||||
// Blizzard-shaped `APIDocumentation` tables through `C_APIDocumentation`.
|
||||
// The descriptor model is the `Game::Doc` block in Game.h.
|
||||
namespace Api::Documentation {
|
||||
|
||||
enum Env : unsigned { ENV_NONE = 0, ENV_GAME = 1, ENV_GLUE = 2 };
|
||||
|
||||
// Bracket one `Run*Registrations()` pass. Recording happens only inside a
|
||||
// pass, and only the FIRST pass per environment records — every later pass
|
||||
// (each `/reload`, each logout→login) is a no-op. `EndPass` seals the
|
||||
// environment and logs one summary line to the debug log.
|
||||
void BeginPass(Env env);
|
||||
void EndPass();
|
||||
|
||||
void RecordGlobal(const char *name, const Game::Doc::Function *doc);
|
||||
void RecordTable(const char *table, const char *name, const Game::Doc::Function *doc);
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Game::Doc::Method *docs, int docCount);
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system);
|
||||
|
||||
} // namespace Api::Documentation
|
||||
+20
-3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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};
|
||||
|
||||
@@ -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."
|
||||
Reference in New Issue
Block a user