From 108a91a8438c190afad9cf531d256c6b4e293206 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:20:46 -0500 Subject: [PATCH] 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. --- AddOns/!!!ClassicAPI/!!!ClassicAPI.toc | 9 + .../APIDocumentation/APIDocumentation.lua | 421 +++++++++++ .../APIDocumentation_Bootstrap.lua | 66 ++ .../APIDocumentation/BaseAPIMixin.lua | 115 +++ .../APIDocumentation/EventsAPIMixin.lua | 104 +++ .../APIDocumentation/FieldsAPIMixin.lua | 90 +++ .../APIDocumentation/FunctionsAPIMixin.lua | 121 +++ .../APIDocumentation/SystemsAPIMixin.lua | 159 ++++ .../APIDocumentation/TablesAPIMixin.lua | 74 ++ AddOns/!!!ClassicAPI/Util/LinkUtil.lua | 34 + README.md | 1 + TODO.md | 55 ++ docs/API.md | 98 +++ src/Game.cpp | 27 +- src/Game.h | 140 +++- src/api/Documentation.cpp | 690 ++++++++++++++++++ src/api/Documentation.h | 40 + src/event/Custom.cpp | 23 +- src/event/Custom.h | 16 +- src/glue/Session.cpp | 24 +- src/spell/AtCursor.cpp | 18 +- src/spell/AtUnit.cpp | 21 +- src/spell/AutoAttack.cpp | 42 +- src/spell/Cancel.cpp | 21 +- src/spell/Cast.cpp | 80 +- src/spell/CastCount.cpp | 33 +- src/spell/CastEvents.cpp | 89 ++- src/spell/Cooldown.cpp | 27 +- src/spell/Description.cpp | 16 +- src/spell/EffectInfo.cpp | 34 +- src/spell/EffectMechanic.cpp | 17 +- src/spell/Exists.cpp | 14 +- src/spell/HasRange.cpp | 32 +- src/spell/InRange.cpp | 17 +- src/spell/Info.cpp | 261 ++++++- src/spell/IsCurrent.cpp | 15 +- src/spell/IsSelfBuff.cpp | 14 +- src/spell/Level.cpp | 60 +- src/spell/Mechanic.cpp | 20 +- src/spell/PowerCost.cpp | 33 +- src/spell/Radius.cpp | 32 +- src/spell/Reagents.cpp | 24 +- src/spell/RecordFields.cpp | 36 +- src/spell/SchoolString.cpp | 16 +- src/spell/Subtext.cpp | 13 +- src/spell/Tooltip.cpp | 36 +- src/spell/Usable.cpp | 31 +- tools/New-ApiDocSkeletons.ps1 | 211 ++++++ 48 files changed, 3473 insertions(+), 97 deletions(-) create mode 100644 AddOns/!!!ClassicAPI/APIDocumentation/APIDocumentation.lua create mode 100644 AddOns/!!!ClassicAPI/APIDocumentation/APIDocumentation_Bootstrap.lua create mode 100644 AddOns/!!!ClassicAPI/APIDocumentation/BaseAPIMixin.lua create mode 100644 AddOns/!!!ClassicAPI/APIDocumentation/EventsAPIMixin.lua create mode 100644 AddOns/!!!ClassicAPI/APIDocumentation/FieldsAPIMixin.lua create mode 100644 AddOns/!!!ClassicAPI/APIDocumentation/FunctionsAPIMixin.lua create mode 100644 AddOns/!!!ClassicAPI/APIDocumentation/SystemsAPIMixin.lua create mode 100644 AddOns/!!!ClassicAPI/APIDocumentation/TablesAPIMixin.lua create mode 100644 src/api/Documentation.cpp create mode 100644 src/api/Documentation.h create mode 100644 tools/New-ApiDocSkeletons.ps1 diff --git a/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc b/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc index a8f285c6..d7d0f9e7 100644 --- a/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc +++ b/AddOns/!!!ClassicAPI/!!!ClassicAPI.toc @@ -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 diff --git a/AddOns/!!!ClassicAPI/APIDocumentation/APIDocumentation.lua b/AddOns/!!!ClassicAPI/APIDocumentation/APIDocumentation.lua new file mode 100644 index 00000000..77ac9632 --- /dev/null +++ b/AddOns/!!!ClassicAPI/APIDocumentation/APIDocumentation.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 "); + self:WriteLine(self:GetIndentString() .. "or"); + self:WriteLine(self:GetIndentString() .. "/classicapi s "); + 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 search "); + self:WriteLine(self:GetIndentString() .. "or"); + self:WriteLine(self:GetIndentString() .. "/classicapi s "); + self:WriteLine(self:GetIndentString() .. "Example: /classicapi spell search cooldown"); + self:WriteLine(" "); + + self:WriteLine("List all API in a system"); + self:WriteLine(self:GetIndentString() .. "/classicapi 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(); diff --git a/AddOns/!!!ClassicAPI/APIDocumentation/APIDocumentation_Bootstrap.lua b/AddOns/!!!ClassicAPI/APIDocumentation/APIDocumentation_Bootstrap.lua new file mode 100644 index 00000000..1b1f7a02 --- /dev/null +++ b/AddOns/!!!ClassicAPI/APIDocumentation/APIDocumentation_Bootstrap.lua @@ -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); diff --git a/AddOns/!!!ClassicAPI/APIDocumentation/BaseAPIMixin.lua b/AddOns/!!!ClassicAPI/APIDocumentation/BaseAPIMixin.lua new file mode 100644 index 00000000..e20d45b3 --- /dev/null +++ b/AddOns/!!!ClassicAPI/APIDocumentation/BaseAPIMixin.lua @@ -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 diff --git a/AddOns/!!!ClassicAPI/APIDocumentation/EventsAPIMixin.lua b/AddOns/!!!ClassicAPI/APIDocumentation/EventsAPIMixin.lua new file mode 100644 index 00000000..b27a5160 --- /dev/null +++ b/AddOns/!!!ClassicAPI/APIDocumentation/EventsAPIMixin.lua @@ -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 diff --git a/AddOns/!!!ClassicAPI/APIDocumentation/FieldsAPIMixin.lua b/AddOns/!!!ClassicAPI/APIDocumentation/FieldsAPIMixin.lua new file mode 100644 index 00000000..766d12d4 --- /dev/null +++ b/AddOns/!!!ClassicAPI/APIDocumentation/FieldsAPIMixin.lua @@ -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 diff --git a/AddOns/!!!ClassicAPI/APIDocumentation/FunctionsAPIMixin.lua b/AddOns/!!!ClassicAPI/APIDocumentation/FunctionsAPIMixin.lua new file mode 100644 index 00000000..a082a495 --- /dev/null +++ b/AddOns/!!!ClassicAPI/APIDocumentation/FunctionsAPIMixin.lua @@ -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 diff --git a/AddOns/!!!ClassicAPI/APIDocumentation/SystemsAPIMixin.lua b/AddOns/!!!ClassicAPI/APIDocumentation/SystemsAPIMixin.lua new file mode 100644 index 00000000..23563ede --- /dev/null +++ b/AddOns/!!!ClassicAPI/APIDocumentation/SystemsAPIMixin.lua @@ -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 diff --git a/AddOns/!!!ClassicAPI/APIDocumentation/TablesAPIMixin.lua b/AddOns/!!!ClassicAPI/APIDocumentation/TablesAPIMixin.lua new file mode 100644 index 00000000..af6c90a9 --- /dev/null +++ b/AddOns/!!!ClassicAPI/APIDocumentation/TablesAPIMixin.lua @@ -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 diff --git a/AddOns/!!!ClassicAPI/Util/LinkUtil.lua b/AddOns/!!!ClassicAPI/Util/LinkUtil.lua index fdda28c4..e2aad781 100644 --- a/AddOns/!!!ClassicAPI/Util/LinkUtil.lua +++ b/AddOns/!!!ClassicAPI/Util/LinkUtil.lua @@ -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:...|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 diff --git a/README.md b/README.md index 5dfcc054..c8d7e3d5 100644 --- a/README.md +++ b/README.md @@ -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` | diff --git a/TODO.md b/TODO.md index bfdcbd0f..dc2f258f 100644 --- a/TODO.md +++ b/TODO.md @@ -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. diff --git a/docs/API.md b/docs/API.md index c06fd917..fb5f0afb 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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)` diff --git a/src/Game.cpp b/src/Game.cpp index bdec3e0b..f2de6dc8 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -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( 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( 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(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) { diff --git a/src/Game.h b/src/Game.h index 72355c60..6bcf474b 100644 --- a/src/Game.h +++ b/src/Game.h @@ -13,6 +13,7 @@ #pragma once +#include #include 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 +// `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 + constexpr FieldList(const Field (&a)[N]) : data(a), count(static_cast(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). diff --git a/src/api/Documentation.cpp b/src/api/Documentation.cpp new file mode 100644 index 00000000..163b2f98 --- /dev/null +++ b/src/api/Documentation.cpp @@ -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 . + +// 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 +#include +#include +#include +#include +#include + +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 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(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 &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 CollectSystems() { + std::vector 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 &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(++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(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(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 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 = , 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(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(*p); + out[o++] = static_cast(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(++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(++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(++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(++n)); + PushEnumeration(L, e); + Lua::SetTable(L, -3); + } + Lua::SetTable(L, -3); +} + +// ---- Coverage --------------------------------------------------------------- + +std::vector Undocumented() { + std::vector out; + const std::vector 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 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(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 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 names = Undocumented(); + Lua::NewTable(L); + for (size_t i = 0; i < names.size(); ++i) { + Lua::PushNumber(L, static_cast(i + 1)); + Lua::PushString(L, names[i].c_str()); + Lua::SetTable(L, -3); + } + Lua::PushNumber(L, static_cast(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 missing = Undocumented(); + Debug::Log::Printf("[doc] %s pass: %d registrations recorded, %d undocumented", + done == ENV_GLUE ? "glue" : "game", g_recordedThisPass, + static_cast(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; +} diff --git a/src/api/Documentation.h b/src/api/Documentation.h new file mode 100644 index 00000000..85d51be1 --- /dev/null +++ b/src/api/Documentation.h @@ -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 . + +#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 diff --git a/src/event/Custom.cpp b/src/event/Custom.cpp index 40a982be..7f9b616d 100644 --- a/src/event/Custom.cpp +++ b/src/event/Custom.cpp @@ -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; diff --git a/src/event/Custom.h b/src/event/Custom.h index 9df5af41..4a549b7c 100644 --- a/src/event/Custom.h +++ b/src/event/Custom.h @@ -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. diff --git a/src/glue/Session.cpp b/src/glue/Session.cpp index cae63b84..fe8da9d9 100644 --- a/src/glue/Session.cpp +++ b/src/glue/Session.cpp @@ -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}; diff --git a/src/spell/AtCursor.cpp b/src/spell/AtCursor.cpp index 712a1269..b812c746 100644 --- a/src/spell/AtCursor.cpp +++ b/src/spell/AtCursor.cpp @@ -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}; diff --git a/src/spell/AtUnit.cpp b/src/spell/AtUnit.cpp index 28b4510d..2800c76c 100644 --- a/src/spell/AtUnit.cpp +++ b/src/spell/AtUnit.cpp @@ -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}; diff --git a/src/spell/AutoAttack.cpp b/src/spell/AutoAttack.cpp index 72eb6df5..e2764335 100644 --- a/src/spell/AutoAttack.cpp +++ b/src/spell/AutoAttack.cpp @@ -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}; diff --git a/src/spell/Cancel.cpp b/src/spell/Cancel.cpp index 85285e52..3607adff 100644 --- a/src/spell/Cancel.cpp +++ b/src/spell/Cancel.cpp @@ -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}; diff --git a/src/spell/Cast.cpp b/src/spell/Cast.cpp index 24598013..b4f72306 100644 --- a/src/spell/Cast.cpp +++ b/src/spell/Cast.cpp @@ -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}; diff --git a/src/spell/CastCount.cpp b/src/spell/CastCount.cpp index 1914b504..80b354ee 100644 --- a/src/spell/CastCount.cpp +++ b/src/spell/CastCount.cpp @@ -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}; diff --git a/src/spell/CastEvents.cpp b/src/spell/CastEvents.cpp index 26e4435b..ed906c84 100644 --- a/src/spell/CastEvents.cpp +++ b/src/spell/CastEvents.cpp @@ -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. diff --git a/src/spell/Cooldown.cpp b/src/spell/Cooldown.cpp index c2eafcbd..7caca148 100644 --- a/src/spell/Cooldown.cpp +++ b/src/spell/Cooldown.cpp @@ -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}; diff --git a/src/spell/Description.cpp b/src/spell/Description.cpp index 615ab8ae..30e80a02 100644 --- a/src/spell/Description.cpp +++ b/src/spell/Description.cpp @@ -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}; diff --git a/src/spell/EffectInfo.cpp b/src/spell/EffectInfo.cpp index 9084cb7b..ffde3a03 100644 --- a/src/spell/EffectInfo.cpp +++ b/src/spell/EffectInfo.cpp @@ -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}; diff --git a/src/spell/EffectMechanic.cpp b/src/spell/EffectMechanic.cpp index 22ad2b8b..fa2e25a0 100644 --- a/src/spell/EffectMechanic.cpp +++ b/src/spell/EffectMechanic.cpp @@ -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}; diff --git a/src/spell/Exists.cpp b/src/spell/Exists.cpp index 4305406f..c6e1f7ae 100644 --- a/src/spell/Exists.cpp +++ b/src/spell/Exists.cpp @@ -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}; diff --git a/src/spell/HasRange.cpp b/src/spell/HasRange.cpp index 2b993fd1..ebe44fea 100644 --- a/src/spell/HasRange.cpp +++ b/src/spell/HasRange.cpp @@ -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}; diff --git a/src/spell/InRange.cpp b/src/spell/InRange.cpp index b75dcf49..8e05c071 100644 --- a/src/spell/InRange.cpp +++ b/src/spell/InRange.cpp @@ -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}; diff --git a/src/spell/Info.cpp b/src/spell/Info.cpp index bfe33158..f92b59a8 100644 --- a/src/spell/Info.cpp +++ b/src/spell/Info.cpp @@ -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}; diff --git a/src/spell/IsCurrent.cpp b/src/spell/IsCurrent.cpp index df38abe2..18cca42b 100644 --- a/src/spell/IsCurrent.cpp +++ b/src/spell/IsCurrent.cpp @@ -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}; diff --git a/src/spell/IsSelfBuff.cpp b/src/spell/IsSelfBuff.cpp index 7b9aded1..1f0b2ba3 100644 --- a/src/spell/IsSelfBuff.cpp +++ b/src/spell/IsSelfBuff.cpp @@ -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}; diff --git a/src/spell/Level.cpp b/src/spell/Level.cpp index dfceb450..2d3c1103 100644 --- a/src/spell/Level.cpp +++ b/src/spell/Level.cpp @@ -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}; diff --git a/src/spell/Mechanic.cpp b/src/spell/Mechanic.cpp index 4e589b79..0f7f6cc0 100644 --- a/src/spell/Mechanic.cpp +++ b/src/spell/Mechanic.cpp @@ -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}; diff --git a/src/spell/PowerCost.cpp b/src/spell/PowerCost.cpp index 807cdaf4..1681de9d 100644 --- a/src/spell/PowerCost.cpp +++ b/src/spell/PowerCost.cpp @@ -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}; diff --git a/src/spell/Radius.cpp b/src/spell/Radius.cpp index 67cdb5e9..aa349b46 100644 --- a/src/spell/Radius.cpp +++ b/src/spell/Radius.cpp @@ -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}; diff --git a/src/spell/Reagents.cpp b/src/spell/Reagents.cpp index 6b606ce5..2dc6f596 100644 --- a/src/spell/Reagents.cpp +++ b/src/spell/Reagents.cpp @@ -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}; diff --git a/src/spell/RecordFields.cpp b/src/spell/RecordFields.cpp index ef8b9f64..a2921db2 100644 --- a/src/spell/RecordFields.cpp +++ b/src/spell/RecordFields.cpp @@ -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}; diff --git a/src/spell/SchoolString.cpp b/src/spell/SchoolString.cpp index 60d9189c..7cbfdf24 100644 --- a/src/spell/SchoolString.cpp +++ b/src/spell/SchoolString.cpp @@ -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}; diff --git a/src/spell/Subtext.cpp b/src/spell/Subtext.cpp index edc35bff..53e62a16 100644 --- a/src/spell/Subtext.cpp +++ b/src/spell/Subtext.cpp @@ -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}; diff --git a/src/spell/Tooltip.cpp b/src/spell/Tooltip.cpp index c88011a8..5c27ba8f 100644 --- a/src/spell/Tooltip.cpp +++ b/src/spell/Tooltip.cpp @@ -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(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY), g_methods, - static_cast(sizeof(g_methods) / sizeof(g_methods[0]))); + static_cast(sizeof(g_methods) / sizeof(g_methods[0])), + g_methodDocs, + static_cast(sizeof(g_methodDocs) / sizeof(g_methodDocs[0]))); } static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; diff --git a/src/spell/Usable.cpp b/src/spell/Usable.cpp index 08f99b83..c176cfee 100644 --- a/src/spell/Usable.cpp +++ b/src/spell/Usable.cpp @@ -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}; diff --git a/tools/New-ApiDocSkeletons.ps1 b/tools/New-ApiDocSkeletons.ps1 new file mode 100644 index 00000000..e030a87b --- /dev/null +++ b/tools/New-ApiDocSkeletons.ps1 @@ -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/.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."