Name | Type | is_array | initial_value |
do; local _, codeLoc = pcall(error, "", 2) --get line number where DebugUtils begins.
--[[
--------------------------
-- | Debug Utils 2.1a | --
--------------------------
--> https://www.hiveworkshop.com/threads/lua-debug-utils-incl-ingame-console.353720/
- by Eikonium, with special thanks to:
- @Bribe, for pretty table print, showing that xpcall's message handler executes before the stack unwinds and useful suggestions like name caching and stack trace improvements.
- @Jampion, for useful suggestions like print caching and applying Debug.try to all code entry points
- @Luashine, for useful feedback and building "WC3 Debug Console Paste Helper" (https://github.com/Luashine/wc3-debug-console-paste-helper#readme)
- @HerlySQR, for showing a way to get a stack trace in Wc3 (https://www.hiveworkshop.com/threads/lua-getstacktrace.340841/)
- @Macadamia, for showing a way to print warnings upon accessing undeclared globals, where this all started with (https://www.hiveworkshop.com/threads/lua-very-simply-trick-to-help-lua-users-track-syntax-errors.326266/)
-----------------------------------------------------------------------------------------------------------------------------
| Provides debugging utility for Wc3-maps using Lua. |
| |
| Including: |
| 1. Automatic ingame error messages upon running erroneous code from triggers or timers. |
| 2. Ingame Console that allows you to execute code via Wc3 ingame chat. |
| 3. Automatic warnings upon reading undeclared globals (which also triggers after misspelling globals) |
| 4. Debug-Library functions for manual error handling. |
| 5. Caching of loading screen print messages until game start (which simplifies error handling during loading screen) |
| 6. Overwritten tostring/print-functions to show the actual string-name of an object instead of the memory position. |
| 7. Conversion of war3map.lua-error messages to local file error messages. |
| 8. Other useful debug utility (table.print and Debug.wc3Type) |
-----------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Installation: |
| |
| 1. Copy the code (DebugUtils.lua, StringWidth.lua and IngameConsole.lua) into your map. Use script files (Ctrl+U) in your trigger editor, not text-based triggers! |
| 2. Order the files: DebugUtils above StringWidth above IngameConsole. Make sure they are above ALL other scripts (crucial for local line number feature). |
| 3. Adjust the settings in the settings-section further below to receive the debug environment that fits your needs. |
| |
| Deinstallation: |
| |
| - Debug Utils is meant to provide debugging utility and as such, shall be removed or invalidated from the map closely before release. |
| - Optimally delete the whole Debug library. If that isn't suitable (because you have used library functions at too many places), you can instead replace Debug Utils |
| by the following line of code that will invalidate all Debug functionality (without breaking your code): |
| Debug = setmetatable({try = function(...) return select(2,pcall(...)) end}, {__index = function(t,k) return DoNothing end}); try = Debug.try |
| - If that is also not suitable for you (because your systems rely on the Debug functionality to some degree), at least set ALLOW_INGAME_CODE_EXECUTION to false. |
| - Be sure to test your map thoroughly after removing Debug Utils. |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
* Documentation and API-Functions:
*
* - All automatic functionality provided by Debug Utils can be deactivated using the settings directly below the documentation.
*
* -------------------------
* | Ingame Code Execution |
* -------------------------
* - Debug Utils provides the ability to run code via chat command from within Wc3, if you have conducted step 3 from the installation section.
* - You can either open the ingame console by typing "-console" into the chat, or directly execute code by typing "-exec <code>".
* - See IngameConsole script for further documentation.
*
* ------------------
* | Error Handling |
* ------------------
* - Debug Utils automatically applies error handling (i.e. Debug.try) to code executed by your triggers and timers (error handling means that error messages are printed on screen, if anything doesn't run properly).
* - You can still use the below library functions for manual debugging.
*
* Debug.try(funcToExecute, ...) -> ...
* - Help function for debugging another function (funcToExecute) that prints error messages on screen, if funcToExecute fails to execute.
* - DebugUtils will automatically apply this to all code run by your triggers and timers, so you rarely need to apply this manually (maybe on code run in the Lua root).
* - Calls funcToExecute with the specified parameters (...) in protected mode (which means that following code will continue to run even if funcToExecute fails to execute).
* - If the call is successful, returns the specified function's original return values (so p1 = Debug.try(Player, 0) will work fine).
* - If the call is unsuccessful, prints an error message on screen (including stack trace and parameters you have potentially logged before the error occured)
* - By default, the error message consists of a line-reference to war3map.lua (which you can look into by forcing a syntax error in WE or by exporting it from your map via File -> Export Script).
* You can get more helpful references to local script files instead, see section about "Local script references".
* - Example: Assume you have a code line like "func(param1,param2)", which doesn't work and you want to know why.
* Option 1: Change it to "Debug.try(func, param1, param2)", i.e. separate the function from the parameters.
* Option 2: Change it to "Debug.try(function() return func(param1, param2) end)", i.e. pack it into an anonymous function (optionally skip the return statement).
* Debug.log(...)
* - Logs the specified parameters to the Debug-log. The Debug-log will be printed upon the next error being catched by Debug.try, Debug.assert or Debug.throwError.
* - The Debug-log will only hold one set of parameters per code-location. That means, if you call Debug.log() inside any function, only the params saved within the latest call of that function will be kept.
* Debug.throwError(...)
* - Prints an error message including document, line number, stack trace, previously logged parameters and all specified parameters on screen. Parameters can have any type.
* - In contrast to Lua's native error function, this can be called outside of protected mode and doesn't halt code execution.
* Debug.assert(condition:boolean, errorMsg:string, ...) -> ...
* - Prints the specified error message including document, line number, stack trace and previously logged parameters on screen, IF the specified condition fails (i.e. resolves to false/nil).
* - Returns ..., IF the specified condition holds.
* - This works exactly like Lua's native assert, except that it also works outside of protected mode and does not halt code execution.
* Debug.traceback() -> string
* - Returns the stack trace at the position where this is called. You need to manually print it.
* Debug.getLine([depth: integer]) -> integer?
* - Returns the line in war3map.lua, where this function is executed.
* - You can specify a depth d >= 1 to instead return the line, where the d-th function in the stack trace was called. I.e. depth = 2 will return the line of execution of the function that calls Debug.getLine.
* - Due to Wc3's limited stack trace ability, this might sometimes return nil for depth >= 3, so better apply nil-checks on the result.
* Debug.getLocalErrorMsg(errorMsg:string) -> string
* - Takes an error message containing a file and a linenumber and converts war3map.lua-lines to local document lines as defined by uses of Debug.beginFile() and Debug.endFile().
* - Error Msg must be formatted like "<document>:<linenumber><Rest>".
*
* -----------------------------------
* | Warnings for undeclared globals |
* -----------------------------------
* - DebugUtils will print warnings on screen, if you read an undeclared global variable.
* - This is technically the case, when you misspelled on a function name, like calling CraeteUnit instead of CreateUnit.
* - Keep in mind though that the same warning will pop up after reading a global that was intentionally nilled. If you don't like this, turn of this feature in the settings.
*
* -----------------
* | Print Caching |
* -----------------
* - DebugUtils caches print()-calls occuring during loading screen and delays them to after game start.
* - This also applies to loading screen error messages, so you can wrap erroneous parts of your Lua root in Debug.try-blocks and see the message after game start.
*
* -------------------------
* | Local File Stacktrace |
* -------------------------
* - By default, error messages and stack traces printed by the error handling functionality of Debug Utils contain references to war3map.lua (a big file just appending all your local scripts).
* - The Debug-library provides the two functions below to index your local scripts, activating local file names and line numbers (matching those in your IDE) instead of the war3map.lua ones.
* - This allows you to inspect errors within your IDE (VSCode) instead of the World Editor.
*
* Debug.beginFile(fileName: string [, depth: integer])
* - Tells the Debug library that the specified file begins exactly here (i.e. in the line, where this is called).
* - Using this improves stack traces of error messages. "war3map.lua"-references between <here> and the next Debug.endFile() will be converted to file-specific references.
* - All war3map.lua-lines located between the call of Debug.beginFile(fileName) and the next call of Debug.beginFile OR Debug.endFile are treated to be part of "fileName".
* - !!! To be called in the Lua root in Line 1 of every document you wish to track. Line 1 means exactly line 1, before any comment! This way, the line shown in the trace will exactly match your IDE.
* - Depth can be ignored, except if you want to use a custom wrapper around Debug.beginFile(), in which case you need to set the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.beginFile().
* Debug.endFile([depth: integer])
* - Ends the current file that was previously begun by using Debug.beginFile(). War3map.lua-lines after this will not be converted until the next instance of Debug.beginFile().
* - The next call of Debug.beginFile() will also end the previous one, so using Debug.endFile() is optional. Mainly recommended to use, if you prefer to have war3map.lua-references in a certain part of your script (such as within GUI triggers).
* - Depth can be ignored, except if you want to use a custom wrapper around Debug.endFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.endFile().
*
* ----------------
* | Name Caching |
* ----------------
* - DebugUtils overwrites the tostring-function so that it prints the name of a non-primitive object (if available) instead of its memory position. The same applies to print().
* - For instance, print(CreateUnit) will show "function: CreateUnit" on screen instead of "function: 0063A698".
* - The table holding all those names is referred to as "Name Cache".
* - All names of objects in global scope will automatically be added to the Name Cache both within Lua root and again at game start (to get names for overwritten natives and your own objects).
* - New names entering global scope will also automatically be added, even after game start. The same applies to subtables of _G up to a depth of Debug.settings.NAME_CACHE_DEPTH.
* - Objects within subtables will be named after their parent tables and keys. For instance, the name of the function within T = {{bla = function() end}} is "T[1].bla".
* - The automatic adding doesn't work for objects saved into existing variables/keys after game start (because it's based on __newindex metamethod which simply doesn't trigger)
* - You can manually add names to the name cache by using the following API-functions:
*
* Debug.registerName(whichObject:any, name:string)
* - Adds the specified object under the specified name to the name cache, letting tostring and print output "<type>: <name>" going foward.
* - The object must be non-primitive, i.e. this won't work on strings, numbers and booleans.
* - This will overwrite existing names for the specified object with the specified name.
* Debug.registerNamesFrom(parentTable:table [, parentTableName:string] [, depth])
* - Adds names for all values from within the specified parentTable to the name cache.
* - Names for entries will be like "<parentTableName>.<key>" or "<parentTableName>[<key>]" (depending on the key type), using the existing name of the parentTable from the name cache.
* - You can optionally specify a parentTableName to use that for the entry naming instead of the existing name. Doing so will also register that name for the parentTable, if it doesn't already has one.
* - Specifying the empty string as parentTableName will suppress it in the naming and just register all values as "<key>". Note that only string keys will be considered this way.
* - In contrast to Debug.registerName(), this function will NOT overwrite existing names, but just add names for new objects.
* Debug.oldTostring(object:any) -> string
* - The old tostring-function in case you still need outputs like "function: 0063A698".
*
* -----------------
* | Other Utility |
* -----------------
*
* Debug.wc3Type(object:any) -> string
* - Returns the Warcraft3-type of the input object. E.g. Debug.wc3Type(Player(0)) will return "player".
* - Returns type(object), if used on Lua-objects.
* table.tostring(whichTable [, depth:integer] [, pretty_yn:boolean])
* - Creates a list of all (key,value)-pairs from the specified table. Also lists subtable entries up to the specified depth (unlimited, if not specified).
* - E.g. for T = {"a", 5, {7}}, table.tostring(T) would output '{(1, "a"), (2, 5), (3, {(1, 7)})}' (if using concise style, i.e. pretty_yn being nil or false).
* - Not specifying a depth can potentially lead to a stack overflow for self-referential tables (e.g X = {}; X[1] = X). Choose a sensible depth to prevent this (in doubt start with 1 and test upwards).
* - Supports pretty style by setting pretty_yn to true. Pretty style is linebreak-separated, uses indentations and has other visual improvements. Use it on small tables only, because Wc3 can't show that many linebreaks at once.
* - All of the following is valid syntax: table.tostring(T), table.tostring(T, depth), table.tostring(T, pretty_yn) or table.tostring(T, depth, pretty_yn).
* - table.tostring is not multiplayer-synced.
* table.print(whichTable [, depth:integer] [, pretty_yn:boolean])
* - Prints table.tostring(...).
*
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------]]
-- disable sumneko extension warnings for imported resource
---@diagnostic disable
----------------
--| Settings |--
----------------
Debug = {
--BEGIN OF SETTINGS--
settings = {
SHOW_TRACE_ON_ERROR = true ---Set to true to show a stack trace on every error in addition to the regular message (msg sources: automatic error handling, Debug.try, Debug.throwError, ...)
, INCLUDE_DEBUGUTILS_INTO_TRACE = true ---Set to true to include lines from Debug Utils into the stack trace. Those show the source of error handling, which you might consider redundant.
, USE_TRY_ON_TRIGGERADDACTION = true ---Set to true for automatic error handling on TriggerAddAction (applies Debug.try on every trigger action).
, USE_TRY_ON_CONDITION = true ---Set to true for automatic error handling on boolexpressions created via Condition() or Filter() (essentially applies Debug.try on every trigger condition).
, USE_TRY_ON_TIMERSTART = true ---Set to true for automatic error handling on TimerStart (applies Debug.try on every timer callback).
, USE_TRY_ON_ENUMFUNCS = true ---Set to true for automatic error handling on ForGroup, ForForce, EnumItemsInRect and EnumDestructablesInRect (applies Debug.try on every enum callback)
, USE_TRY_ON_COROUTINES = true ---Set to true for improved stack traces on errors within coroutines (applies Debug.try on coroutine.create and coroutine.wrap). This lets stack traces point to the erroneous function executed within the coroutine (instead of the function creating the coroutine).
, ALLOW_INGAME_CODE_EXECUTION = true ---Set to true to enable IngameConsole and -exec command.
, WARNING_FOR_UNDECLARED_GLOBALS = false ---Set to true to print warnings upon accessing undeclared globals (i.e. globals with nil-value). This is technically the case after having misspelled on a function name (like CraeteUnit instead of CreateUnit).
, SHOW_TRACE_FOR_UNDECLARED_GLOBALS = false ---Set to true to include a stack trace into undeclared global warnings. Only takes effect, if WARNING_FOR_UNDECLARED_GLOBALS is also true.
, USE_PRINT_CACHE = true ---Set to true to let print()-calls during loading screen be cached until the game starts.
, PRINT_DURATION = nil ---Adjust the duration in seconds that values printed by print() last on screen. Set to nil to use default duration (which depends on string length).
, USE_NAME_CACHE = true ---Set to true to let tostring/print output the string-name of an object instead of its memory location (except for booleans/numbers/strings). E.g. print(CreateUnit) will output "function: CreateUnit" instead of "function: 0063A698".
, AUTO_REGISTER_NEW_NAMES = true ---Automatically adds new names from global scope (and subtables of _G up to NAME_CACHE_DEPTH) to the name cache by adding metatables with the __newindex metamethod to ALL tables accessible from global scope.
, NAME_CACHE_DEPTH = 4 ---Set to 0 to only affect globals. Experimental feature: Set to an integer > 0 to also cache names for subtables of _G (up to the specified depth). Warning: This will alter the __newindex metamethod of subtables of _G (but not break existing functionality).
}
--END OF SETTINGS--
--START OF CODE--
, data = {
nameCache = {} ---@type table<any,string> contains the string names of any object in global scope (random for objects that have multiple names)
, nameCacheMirror = {} ---@type table<string,any> contains the (name,object)-pairs of all objects in the name cache. Used to prevent name duplicates that might otherwise occur upon reassigning globals.
, nameDepths = {} ---@type table<any,integer> contains the depth of the name used by by any object in the name cache (i.e. the depth within the parentTable).
, autoIndexedTables = {} ---@type table<table,boolean> contains (t,true), if DebugUtils already set a __newindex metamethod for name caching in t. Prevents double application.
, paramLog = {} ---@type table<string,string> saves logged information per code location. to be filled by Debug.log(), to be printed by Debug.try()
, sourceMap = {{firstLine= 1,file='DebugUtils'}} ---@type table<integer,{firstLine:integer,file:string,lastLine?:integer}> saves lines and file names of all documents registered via Debug.beginFile().
, printCache = {n=0} ---@type string[] contains the strings that were attempted to print during loading screen.
}
}
--localization
local settings, paramLog, nameCache, nameDepths, autoIndexedTables, nameCacheMirror, sourceMap, printCache = Debug.settings, Debug.data.paramLog, Debug.data.nameCache, Debug.data.nameDepths, Debug.data.autoIndexedTables, Debug.data.nameCacheMirror, Debug.data.sourceMap, Debug.data.printCache
--Write DebugUtils first line number to sourceMap:
---@diagnostic disable-next-line
Debug.data.sourceMap[1].firstLine = tonumber(codeLoc:match(":\x25d+"):sub(2,-1))
-------------------------------------------------
--| File Indexing for local Error Msg Support |--
-------------------------------------------------
-- Functions for war3map.lua -> local file conversion for error messages.
---Returns the line number in war3map.lua, where this is called (for depth = 0).
---Choose a depth d > 0 to instead return the line, where the d-th function in the stack leading to this call is executed.
---@param depth? integer default: 0.
---@return number?
function Debug.getLine(depth)
depth = depth or 0
local _, location = pcall(error, "", depth + 3) ---@diagnostic disable-next-line
local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000:..."
return tonumber(line and line:sub(2,-1)) --check if line is nil before applying string.sub to prevent errors (nil can result from string.match above, although it should never do so in our case)
end
---Tells the Debug library that the specified file begins exactly here (i.e. in the line, where this is called).
---
---Using this improves stack traces of error messages. Stack trace will have "war3map.lua"-references between this and the next Debug.endFile() converted to file-specific references.
---
---To be called in the Lua root in Line 1 of every file you wish to track! Line 1 means exactly line 1, before any comment! This way, the line shown in the trace will exactly match your IDE.
---
---If you want to use a custom wrapper around Debug.beginFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.beginFile().
---@param fileName string
---@param depth? integer default: 0. Set to 1, if you call this from a wrapper (and use the wrapper in line 1 of every document).
---@param lastLine? integer Ignore this. For compatibility with Total Initialization.
function Debug.beginFile(fileName, depth, lastLine)
depth, fileName = depth or 0, fileName or '' --filename is not actually optional, we just default to '' to prevent crashes.
local line = Debug.getLine(depth + 1)
if line then --for safety reasons. we don't want to add a non-existing line to the sourceMap
table.insert(sourceMap, {firstLine = line, file = fileName, lastLine = lastLine}) --automatically sorted list, because calls of Debug.beginFile happen logically in the order of the map script.
end
end
---Tells the Debug library that the file previously started with Debug.beginFile() ends here.
---This is in theory optional to use, as the next call of Debug.beginFile will also end the previous. Still good practice to always use this in the last line of every file.
---If you want to use a custom wrapper around Debug.endFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.endFile().
---@param depth? integer
function Debug.endFile(depth)
depth = depth or 0
local line = Debug.getLine(depth + 1)
sourceMap[#sourceMap].lastLine = line
end
---Takes an error message containing a file and a linenumber and converts both to local file and line as saved to Debug.sourceMap.
---@param errorMsg string must be formatted like "<document>:<linenumber><RestOfMsg>".
---@return string convertedMsg a string of the form "<localDocument>:<localLinenumber><RestOfMsg>"
function Debug.getLocalErrorMsg(errorMsg)
local startPos, endPos = errorMsg:find(":\x25d*") --start and end position of line number. The part before that is the document, part after the error msg.
if startPos and endPos then --can be nil, if input string was not of the desired form "<document>:<linenumber><RestOfMsg>".
local document, line, rest = errorMsg:sub(1, startPos), tonumber(errorMsg:sub(startPos+1, endPos)), errorMsg:sub(endPos+1, -1) --get error line in war3map.lua
if document == 'war3map.lua:' and line then --only convert war3map.lua-references to local position. Other files such as Blizzard.j.lua are not converted (obiously).
for i = #sourceMap, 1, -1 do --find local file containing the war3map.lua error line.
if line >= sourceMap[i].firstLine then --war3map.lua line is part of sourceMap[i].file
if not sourceMap[i].lastLine or line <= sourceMap[i].lastLine then --if lastLine is given, we must also check for it
return sourceMap[i].file .. ":" .. (line - sourceMap[i].firstLine + 1) .. rest
else --if line is larger than firstLine and lastLine of sourceMap[i], it is not part of a tracked file -> return global war3map.lua position.
break --prevent return within next step of the loop ("line >= sourceMap[i].firstLine" would be true again, but wrong file)
end
end
end
end
end
return errorMsg
end
local convertToLocalErrorMsg = Debug.getLocalErrorMsg
----------------------
--| Error Handling |--
----------------------
local concat
---Applies tostring() on all input params and concatenates them 4-space-separated.
---@param firstParam any
---@param ... any
---@return string
concat = function(firstParam, ...)
if select('#', ...) == 0 then
return tostring(firstParam)
end
return tostring(firstParam) .. ' ' .. concat(...)
end
---Returns the stack trace between the specified startDepth and endDepth.
---The trace lists file names and line numbers. File name is only listed, if it has changed from the previous traced line.
---The previous file can also be specified as an input parameter to suppress the first file name in case it's identical.
---@param startDepth integer
---@param endDepth integer
---@return string trace
local function getStackTrace(startDepth, endDepth)
local trace, separator = "", ""
local _, currentFile, lastFile, tracePiece, lastTracePiece
for loopDepth = startDepth, endDepth do --get trace on different depth level
_, tracePiece = pcall(error, "", loopDepth) ---@type boolean, string
tracePiece = convertToLocalErrorMsg(tracePiece)
if #tracePiece > 0 and lastTracePiece ~= tracePiece then --some trace pieces can be empty, but there can still be valid ones beyond that
currentFile = tracePiece:match("^.-:")
--Hide DebugUtils in the stack trace (except main reference), if settings.INCLUDE_DEBUGUTILS_INTO_TRACE is set to true.
if settings.INCLUDE_DEBUGUTILS_INTO_TRACE or (loopDepth == startDepth) or currentFile ~= "DebugUtils:" then
trace = trace .. separator .. ((currentFile == lastFile) and tracePiece:match(":\x25d+"):sub(2,-1) or tracePiece:match("^.-:\x25d+"))
lastFile, lastTracePiece, separator = currentFile, tracePiece, " <- "
end
end
end
return trace
end
---Message Handler to be used by the try-function below.
---Adds stack trace plus formatting to the message and prints it.
---@param errorMsg string
---@param startDepth? integer default: 4 for use in xpcall
local function errorHandler(errorMsg, startDepth)
startDepth = startDepth or 4 --xpcall doesn't specify this param, so it must default to 4 for this case
errorMsg = convertToLocalErrorMsg(errorMsg)
--Print original error message and stack trace.
print("|cffff5555ERROR at " .. errorMsg .. "|r")
if settings.SHOW_TRACE_ON_ERROR then
print("|cffff5555Traceback (most recent call first):|r")
print("|cffff5555" .. getStackTrace(startDepth,200) .. "|r")
end
--Also print entries from param log, if there are any.
for location, loggedParams in pairs(paramLog) do
print("|cff888888Logged at " .. convertToLocalErrorMsg(location) .. loggedParams .. "|r")
paramLog[location] = nil
end
end
---Tries to execute the specified function with the specified parameters in protected mode and prints an error message (including stack trace), if unsuccessful.
---
---Example use: Assume you have a code line like "CreateUnit(0,1,2)", which doesn't work and you want to know why.
---* Option 1: Change it to "Debug.try(CreateUnit, 0, 1, 2)", i.e. separate the function from the parameters.
---* Option 2: Change it to "Debug.try(function() return CreateUnit(0,1,2) end)", i.e. pack it into an anonymous function. You can skip the "return", if you don't need the return values.
---When no error occured, the try-function will return all values returned by the input function.
---When an error occurs, try will print the resulting error and stack trace.
---@param funcToExecute function the function to call in protected mode
---@param ... any params for the input-function
---@return ... any
function Debug.try(funcToExecute, ...)
return select(2, xpcall(funcToExecute, errorHandler,...))
end
---@diagnostic disable-next-line lowercase-global
try = Debug.try
---Prints "ERROR:" and the specified error objects on the Screen. Also prints the stack trace leading to the error. You can specify as many arguments as you wish.
---
---In contrast to Lua's native error function, this can be called outside of protected mode and doesn't halt code execution.
---@param ... any objects/errormessages to be printed (doesn't have to be strings)
function Debug.throwError(...)
errorHandler(getStackTrace(4,4) .. ": " .. concat(...), 5)
end
---Prints the specified error message, if the specified condition fails (i.e. if it resolves to false or nil).
---
---Returns all specified arguments after the errorMsg, if the condition holds.
---
---In contrast to Lua's native assert function, this can be called outside of protected mode and doesn't halt code execution (even in case of condition failure).
---@param condition any actually a boolean, but you can use any object as a boolean.
---@param errorMsg string the message to be printed, if the condition fails
---@param ... any will be returned, if the condition holds
function Debug.assert(condition, errorMsg, ...)
if condition then
return ...
else
errorHandler(getStackTrace(4,4) .. ": " .. errorMsg, 5)
end
end
---Returns the stack trace at the code position where this function is called.
---The returned string includes war3map.lua/blizzard.j.lua code positions of all functions from the stack trace in the order of execution (most recent call last). It does NOT include function names.
---@return string
function Debug.traceback()
return getStackTrace(3,200)
end
---Saves the specified parameters to the debug log at the location where this function is called. The Debug-log will be printed for all affected locations upon the try-function catching an error.
---The log is unique per code location: Parameters logged at code line x will overwrite the previous ones logged at x. Parameters logged at different locations will all persist and be printed.
---@param ... any save any information, for instance the parameters of the function call that you are logging.
function Debug.log(...)
local _, location = pcall(error, "", 3) ---@diagnostic disable-next-line: need-check-nil
paramLog[location or ''] = concat(...)
end
------------------------------------
--| Name Caching (API-functions) |--
------------------------------------
--Help-table. The registerName-functions below shall not work on call-by-value-types, i.e. booleans, strings and numbers (renaming a value of any primitive type doesn't make sense).
local skipType = {boolean = true, string = true, number = true, ['nil'] = true}
--Set weak keys to nameCache and nameDepths and weak values for nameCacheMirror to prevent garbage collection issues
setmetatable(nameCache, {__mode = 'k'})
setmetatable(nameDepths, getmetatable(nameCache))
setmetatable(nameCacheMirror, {__mode = 'v'})
---Removes the name from the name cache, if already used for any object (freeing it for the new object). This makes sure that a name is always unique.
---This doesn't solve the
---@param name string
local function removeNameIfNecessary(name)
if nameCacheMirror[name] then
nameCache[nameCacheMirror[name]] = nil
nameCacheMirror[name] = nil
end
end
---Registers a name for the specified object, which will be the future output for tostring(whichObject).
---You can overwrite existing names for whichObject by using this.
---@param whichObject any
---@param name string
function Debug.registerName(whichObject, name)
if not skipType[type(whichObject)] then
removeNameIfNecessary(name)
nameCache[whichObject] = name
nameCacheMirror[name] = whichObject
nameDepths[name] = 0
end
end
---Registers a new name to the nameCache as either just <key> (if parentTableName is the empty string), <table>.<key> (if parentTableName is given and string key doesn't contain whitespace) or <name>[<key>] notation (for other keys in existing tables).
---Only string keys without whitespace support <key>- and <table>.<key>-notation. All other keys require a parentTableName.
---@param parentTableName string | '""' empty string suppresses <table>-affix.
---@param key any
---@param object any only call-be-ref types allowed
---@param parentTableDepth? integer
local function addNameToCache(parentTableName, key, object, parentTableDepth)
parentTableDepth = parentTableDepth or -1
--Don't overwrite existing names for the same object, don't add names for primitive types.
if nameCache[object] or skipType[type(object)] then
return
end
local name
--apply dot-syntax for string keys without whitespace
if type(key) == 'string' and not string.find(key, "\x25s") then
if parentTableName == "" then
name = key
nameDepths[object] = 0
else
name = parentTableName .. "." .. key
nameDepths[object] = parentTableDepth + 1
end
--apply bracket-syntax for all other keys. This requires a parentTableName.
elseif parentTableName ~= "" then
name = type(key) == 'string' and ('"' .. key .. '"') or key
name = parentTableName .. "[" .. tostring(name) .. "]"
nameDepths[object] = parentTableDepth + 1
end
--Stop in cases without valid name (like parentTableName = "" and key = [1])
if name then
removeNameIfNecessary(name)
nameCache[object] = name
nameCacheMirror[name] = object
end
end
---Registers all call-by-reference objects in the given parentTable to the nameCache.
---Automatically filters out primitive objects and already registed Objects.
---@param parentTable table
---@param parentTableName? string
local function registerAllObjectsInTable(parentTable, parentTableName)
parentTableName = parentTableName or nameCache[parentTable] or ""
--Register all call-by-ref-objects in parentTable
for key, object in pairs(parentTable) do
addNameToCache(parentTableName, key, object, nameDepths[parentTable])
end
end
---Adds names for all values of the specified parentTable to the name cache. Names will be "<parentTableName>.<key>" or "<parentTableName>[<key>]", depending on the key type.
---
---Example: Given a table T = {f = function() end, [1] = {}}, tostring(T.f) and tostring(T[1]) will output "function: T.f" and "table: T[1]" respectively after running Debug.registerNamesFrom(T).
---The name of T itself must either be specified as an input parameter OR have previously been registered. It can also be suppressed by inputting the empty string (so objects will just display by their own names).
---The names of objects in global scope are automatically registered during loading screen.
---@param parentTable table base table of which all entries shall be registered (in the Form parentTableName.objectName).
---@param parentTableName? string|'""' Nil: takes <parentTableName> as previously registered. Empty String: Skips <parentTableName> completely. String <s>: Objects will show up as "<s>.<objectName>".
---@param depth? integer objects within sub-tables up to the specified depth will also be added. Default: 1 (only elements of whichTable). Must be >= 1.
---@overload fun(parentTable:table, depth:integer)
function Debug.registerNamesFrom(parentTable, parentTableName, depth)
--Support overloaded definition fun(parentTable:table, depth:integer)
if type(parentTableName) == 'number' then
depth = parentTableName
parentTableName = nil
end
--Apply default values
depth = depth or 1
parentTableName = parentTableName or nameCache[parentTable] or ""
--add name of T in case it hasn't already
if not nameCache[parentTable] and parentTableName ~= "" then
Debug.registerName(parentTable, parentTableName)
end
--Register all call-by-ref-objects in parentTable. To be preferred over simple recursive approach to ensure that top level names are preferred.
registerAllObjectsInTable(parentTable, parentTableName)
--if depth > 1 was specified, also register Names from subtables.
if depth > 1 then
for _, object in pairs(parentTable) do
if type(object) == 'table' then
Debug.registerNamesFrom(object, nil, depth - 1)
end
end
end
end
-------------------------------------------
--| Name Caching (Loading Screen setup) |--
-------------------------------------------
---Registers all existing object names from global scope and Lua incorporated libraries to be used by tostring() overwrite below.
local function registerNamesFromGlobalScope()
--Add all names from global scope to the name cache.
Debug.registerNamesFrom(_G, "")
--Add all names of Warcraft-enabled Lua libraries as well:
--Could instead add a depth to the function call above, but we want to ensure that these libraries are added even if the user has chosen depth 0.
for _, lib in ipairs({coroutine, math, os, string, table, utf8, Debug}) do
Debug.registerNamesFrom(lib)
end
--Add further names that are not accessible from global scope:
--Player(i)
for i = 0, GetBJMaxPlayerSlots() - 1 do
Debug.registerName(Player(i), "Player(" .. i .. ")")
end
end
--Set empty metatable to _G. __index is added when game starts (for "attempt to read undeclared global"-errors), __newindex is added right below (for building the name cache).
setmetatable(_G, getmetatable(_G) or {}) --getmetatable(_G) should always return nil provided that DebugUtils is the topmost script file in the trigger editor, but we still include this for safety-
-- Save old tostring into Debug Library before overwriting it.
Debug.oldTostring = tostring
if settings.USE_NAME_CACHE then
local oldTostring = tostring
tostring = function(obj) --new tostring(CreateUnit) prints "function: CreateUnit"
--tostring of non-primitive object is NOT guaranteed to be like "<type>:<hex>", because it might have been changed by some __tostring-metamethod.
if settings.USE_NAME_CACHE then --return names from name cache only if setting is enabled. This allows turning it off during runtime (via Ingame Console) to revert to old tostring.
return nameCache[obj] and ((oldTostring(obj):match("^.-: ") or (oldTostring(obj) .. ": ")) .. nameCache[obj]) or oldTostring(obj)
end
return Debug.oldTostring(obj)
end
--Add names to Debug.data.objectNames within Lua root. Called below the other Debug-stuff to get the overwritten versions instead of the original ones.
registerNamesFromGlobalScope()
--Prepare __newindex-metamethod to automatically add new names to the name cache
if settings.AUTO_REGISTER_NEW_NAMES then
local nameRegisterNewIndex
---__newindex to be used for _G (and subtables up to a certain depth) to automatically register new names to the nameCache.
---Tables in global scope will use their own name. Subtables of them will use <parentName>.<childName> syntax.
---Global names don't support container[key]-notation (because "_G[...]" is probably not desired), so we only register string type keys instead of using prettyTostring.
---@param t table
---@param k any
---@param v any
---@param skipRawset? boolean set this to true when combined with another __newindex. Suppresses rawset(t,k,v) (because the other __newindex is responsible for that).
nameRegisterNewIndex = function(t,k,v, skipRawset)
local parentDepth = nameDepths[t] or 0
--Make sure the parent table has an existing name before using it as part of the child name
if t == _G or nameCache[t] then
local existingName = nameCache[v]
if not existingName then
addNameToCache((t == _G and "") or nameCache[t], k, v, parentDepth)
end
--If v is a table and the parent table has a valid name, inherit __newindex to v's existing metatable (or create a new one), if that wasn't already done.
if type(v) == 'table' and nameDepths[v] < settings.NAME_CACHE_DEPTH then
if not existingName then
--If v didn't have a name before, also add names for elements contained in v by construction (like v = {x = function() end} ).
Debug.registerNamesFrom(v, settings.NAME_CACHE_DEPTH - nameDepths[v])
end
--Apply __newindex to new tables.
if not autoIndexedTables[v] then
autoIndexedTables[v] = true
local mt = getmetatable(v)
if not mt then
mt = {}
setmetatable(v, mt) --only use setmetatable when we are sure there wasn't any before to prevent issues with "__metatable"-metamethod.
end
---@diagnostic disable-next-line: assign-type-mismatch
local existingNewIndex = mt.__newindex
local isTable_yn = (type(existingNewIndex) == 'table')
--If mt has an existing __newindex, add the name-register effect to it (effectively create a new __newindex using the old)
if existingNewIndex then
mt.__newindex = function(t,k,v)
nameRegisterNewIndex(t,k,v, true) --setting t[k] = v might not be desired in case of existing newindex. Skip it and let existingNewIndex make the decision.
if isTable_yn then
existingNewIndex[k] = v
else
return existingNewIndex(t,k,v)
end
end
else
--If mt doesn't have an existing __newindex, add one that adds the object to the name cache.
mt.__newindex = nameRegisterNewIndex
end
end
end
end
--Set t[k] = v.
if not skipRawset then
rawset(t,k,v)
end
end
--Apply metamethod to _G.
local existingNewIndex = getmetatable(_G).__newindex --should always be nil provided that DebugUtils is the topmost script in your trigger editor. Still included for safety.
local isTable_yn = (type(existingNewIndex) == 'table')
if existingNewIndex then
getmetatable(_G).__newindex = function(t,k,v)
nameRegisterNewIndex(t,k,v, true)
if isTable_yn then
existingNewIndex[k] = v
else
existingNewIndex(t,k,v)
end
end
else
getmetatable(_G).__newindex = nameRegisterNewIndex
end
end
end
------------------------------------------------------
--| Native Overwrite for Automatic Error Handling |--
------------------------------------------------------
--A table to store the try-wrapper for each function. This avoids endless re-creation of wrapper functions within the hooks below.
--Weak keys ensure that garbage collection continues as normal.
local tryWrappers = setmetatable({}, {__mode = 'k'}) ---@type table<function,function>
local try = Debug.try
---Takes a function and returns a wrapper executing the same function within Debug.try.
---Wrappers are permanently stored (until the original function is garbage collected) to ensure that they don't have to be created twice for the same function.
---@param func? function
---@return function
local function getTryWrapper(func)
if func then
tryWrappers[func] = tryWrappers[func] or function(...) return try(func, ...) end
end
return tryWrappers[func] --returns nil for func = nil (important for TimerStart overwrite below)
end
--Overwrite TriggerAddAction, TimerStart, Condition, Filter and Enum natives to let them automatically apply Debug.try.
--Also overwrites coroutine.create and coroutine.wrap to let stack traces point to the function executed within instead of the function creating the coroutine.
if settings.USE_TRY_ON_TRIGGERADDACTION then
local originalTriggerAddAction = TriggerAddAction
TriggerAddAction = function(whichTrigger, actionFunc)
return originalTriggerAddAction(whichTrigger, getTryWrapper(actionFunc))
end
end
if settings.USE_TRY_ON_TIMERSTART then
local originalTimerStart = TimerStart
TimerStart = function(whichTimer, timeout, periodic, handlerFunc)
originalTimerStart(whichTimer, timeout, periodic, getTryWrapper(handlerFunc))
end
end
if settings.USE_TRY_ON_CONDITION then
local originalCondition = Condition
Condition = function(func)
return originalCondition(getTryWrapper(func))
end
Filter = Condition
end
if settings.USE_TRY_ON_ENUMFUNCS then
local originalForGroup = ForGroup
ForGroup = function(whichGroup, callback)
originalForGroup(whichGroup, getTryWrapper(callback))
end
local originalForForce = ForForce
ForForce = function(whichForce, callback)
originalForForce(whichForce, getTryWrapper(callback))
end
local originalEnumItemsInRect = EnumItemsInRect
EnumItemsInRect = function(r, filter, actionfunc)
originalEnumItemsInRect(r, filter, getTryWrapper(actionfunc))
end
local originalEnumDestructablesInRect = EnumDestructablesInRect
EnumDestructablesInRect = function(r, filter, actionFunc)
originalEnumDestructablesInRect(r, filter, getTryWrapper(actionFunc))
end
end
if settings.USE_TRY_ON_COROUTINES then
local originalCoroutineCreate = coroutine.create
---@diagnostic disable-next-line: duplicate-set-field
coroutine.create = function(f)
return originalCoroutineCreate(getTryWrapper(f))
end
local originalCoroutineWrap = coroutine.wrap
---@diagnostic disable-next-line: duplicate-set-field
coroutine.wrap = function(f)
return originalCoroutineWrap(getTryWrapper(f))
end
end
------------------------------------------
--| Cache prints during Loading Screen |--
------------------------------------------
-- Apply the duration as specified in the settings.
if settings.PRINT_DURATION then
local display, getLocalPlayer, dur = DisplayTimedTextToPlayer, GetLocalPlayer, settings.PRINT_DURATION
print = function(...) ---@diagnostic disable-next-line: param-type-mismatch
display(getLocalPlayer(), 0, 0, dur, concat(...))
end
end
-- Delay loading screen prints to after game start.
if settings.USE_PRINT_CACHE then
local oldPrint = print
--loading screen print will write the values into the printCache
print = function(...)
if bj_gameStarted then
oldPrint(...)
else --during loading screen only: concatenate input arguments 4-space-separated, implicitely apply tostring on each, cache to table
---@diagnostic disable-next-line
printCache.n = printCache.n + 1
printCache[printCache.n] = concat(...)
end
end
end
-------------------------
--| Modify Game Start |--
-------------------------
local originalMarkGameStarted = MarkGameStarted
--Hook certain actions into the start of the game.
MarkGameStarted = function()
originalMarkGameStarted()
if settings.WARNING_FOR_UNDECLARED_GLOBALS then
local existingIndex = getmetatable(_G).__index
local isTable_yn = (type(existingIndex) == 'table')
getmetatable(_G).__index = function(t, k) --we made sure that _G has a metatable further above.
if string.sub(tostring(k),1,3) ~= 'bj_' then --prevents intentionally nilled bj-variables from triggering the check within Blizzard.j-functions, like bj_cineFadeFinishTimer.
print("Trying to read undeclared global at " .. getStackTrace(4,4) .. ": " .. tostring(k)
.. (settings.SHOW_TRACE_FOR_UNDECLARED_GLOBALS and "\nTraceback (most recent call first):\n" .. getStackTrace(4,200) or ""))
end
if existingIndex then
if isTable_yn then
return existingIndex[k]
end
return existingIndex(t,k)
end
return rawget(t,k)
end
end
--Add names to Debug.data.objectNames again to ensure that overwritten natives also make it to the name cache.
--Overwritten natives have a new value, but the old key, so __newindex didn't trigger. But we can be sure that objectNames[v] doesn't yet exist, so adding again is safe.
if settings.USE_NAME_CACHE then
for _,v in pairs(_G) do
nameCache[v] = nil
end
registerNamesFromGlobalScope()
end
--Print messages that have been cached during loading screen.
if settings.USE_PRINT_CACHE then
--Note that we don't restore the old print. The overwritten variant only applies caching behaviour to loading screen prints anyway and "unhooking" always adds other risks.
for _, str in ipairs(printCache) do
print(str)
end ---@diagnostic disable-next-line: cast-local-type
XXX = printCache
printCache = nil --frees reference for the garbage collector
end
--Create triggers listening to "-console" and "-exec" chat input.
if settings.ALLOW_INGAME_CODE_EXECUTION and IngameConsole then
IngameConsole.createTriggers()
end
end
---------------------
--| Other Utility |--
---------------------
do
---Returns the type of a warcraft object as string, e.g. "unit" upon inputting a unit.
---@param input any
---@return string
function Debug.wc3Type(input)
local typeString = type(input)
if typeString == 'userdata' then
typeString = tostring(input) --tostring returns the warcraft type plus a colon and some hashstuff.
return typeString:sub(1, (typeString:find(":", nil, true) or 0) -1) --string.find returns nil, if the argument is not found, which would break string.sub. So we need to replace by 0.
else
return typeString
end
end
Wc3Type = Debug.wc3Type --for backwards compatibility
local conciseTostring, prettyTostring
---Translates a table into a comma-separated list of its (key,value)-pairs. Also translates subtables up to the specified depth.
---E.g. {"a", 5, {7}} will display as '{(1, "a"), (2, 5), (3, {(1, 7)})}'.
---@param object any
---@param depth? integer default: unlimited. Unlimited depth will throw a stack overflow error on self-referential tables.
---@return string
conciseTostring = function (object, depth)
depth = depth or -1
if type(object) == 'string' then
return '"' .. object .. '"'
elseif depth ~= 0 and type(object) == 'table' then
local elementArray = {}
local keyAsString
for k,v in pairs(object) do
keyAsString = type(k) == 'string' and ('"' .. tostring(k) .. '"') or tostring(k)
table.insert(elementArray, '(' .. keyAsString .. ', ' .. conciseTostring(v, depth -1) .. ')')
end
return '{' .. table.concat(elementArray, ', ') .. '}'
end
return tostring(object)
end
---Creates a list of all (key,value)-pairs from the specified table. Also lists subtable entries up to the specified depth.
---Major differences to concise print are:
--- * Format: Linebreak-formatted instead of one-liner, uses "[key] = value" instead of "(key,value)"
--- * Will also unpack tables used as keys
--- * Also includes the table's memory position as returned by tostring(table).
--- * Tables referenced multiple times will only be unpacked upon first encounter and abbreviated on subsequent encounters
--- * As a consequence, pretty version can be executed with unlimited depth on self-referential tables.
---@param object any
---@param depth? integer default: unlimited.
---@param constTable table
---@param indent string
---@return string
prettyTostring = function(object, depth, constTable, indent)
depth = depth or -1
local objType = type(object)
if objType == "string" then
return '"'..object..'"' --wrap the string in quotes.
elseif objType == 'table' and depth ~= 0 then
if not constTable[object] then
constTable[object] = tostring(object):gsub(":","")
if next(object)==nil then
return constTable[object]..": {}"
else
local mappedKV = {}
for k,v in pairs(object) do
table.insert(mappedKV, '\n ' .. indent ..'[' .. prettyTostring(k, depth - 1, constTable, indent .. " ") .. '] = ' .. prettyTostring(v, depth - 1, constTable, indent .. " "))
end
return constTable[object]..': {'.. table.concat(mappedKV, ',') .. '\n'..indent..'}'
end
end
end
return constTable[object] or tostring(object)
end
---Creates a list of all (key,value)-pairs from the specified table. Also lists subtable entries up to the specified depth.
---Supports concise style and pretty style.
---Concise will display {"a", 5, {7}} as '{(1, "a"), (2, 5), (3, {(1, 7)})}'.
---Pretty is linebreak-separated, so consider table size before converting. Pretty also abbreviates tables referenced multiple times.
---Can be called like table.tostring(T), table.tostring(T, depth), table.tostring(T, pretty_yn) or table.tostring(T, depth, pretty_yn).
---table.tostring is not multiplayer-synced.
---@param whichTable table
---@param depth? integer default: unlimited
---@param pretty_yn? boolean default: false (concise)
---@return string
---@overload fun(whichTable:table, pretty_yn?:boolean):string
function table.tostring(whichTable, depth, pretty_yn)
--reassign input params, if function was called as table.tostring(whichTable, pretty_yn)
if type(depth) == 'boolean' then
pretty_yn = depth
depth = -1
end
return pretty_yn and prettyTostring(whichTable, depth, {}, "") or conciseTostring(whichTable, depth)
end
---Prints a list of (key,value)-pairs contained in the specified table and its subtables up to the specified depth.
---Supports concise style and pretty style. Pretty is linebreak-separated, so consider table size before printing.
---Can be called like table.print(T), table.print(T, depth), table.print(T, pretty_yn) or table.print(T, depth, pretty_yn).
---@param whichTable table
---@param depth? integer default: unlimited
---@param pretty_yn? boolean default: false (concise)
---@overload fun(whichTable:table, pretty_yn?:boolean)
function table.print(whichTable, depth, pretty_yn)
print(table.tostring(whichTable, depth, pretty_yn))
end
end
end
Debug.endFile()
if Debug and Debug.beginFile then Debug.beginFile("IngameConsole") end
--[[
--------------------------
----| Ingame Console |----
--------------------------
/**********************************************
* Allows you to use the following ingame commands:
* "-exec <code>" to execute any code ingame.
* "-console" to start an ingame console interpreting any further chat input as code and showing both return values of function calls and error messages. Furthermore, the print function will print
* directly to the console after it got started. You can still look up all print messages in the F12-log.
***********************
* -------------------
* |Using the console|
* -------------------
* Any (well, most) chat input by any player after starting the console is interpreted as code and directly executed. You can enter terms (like 4+5 or just any variable name), function calls (like print("bla"))
* and set-statements (like y = 5). If the code has any return values, all of them are printed to the console. Erroneous code will print an error message.
* Chat input starting with a hyphen is being ignored by the console, i.e. neither executed as code nor printed to the console. This allows you to still use other chat commands like "-exec" without prompting errors.
***********************
* ------------------
* |Multiline-Inputs|
* ------------------
* You can prevent a chat input from being immediately executed by preceeding it with the '>' character. All lines entered this way are halted, until any line not starting with '>' is being entered.
* The first input without '>' will execute all halted lines (and itself) in one chunk.
* Example of a chat input (the console will add an additional '>' to every line):
* >function a(x)
* >return x
* end
***********************
* Note that multiline inputs don't accept pure term evaluations, e.g. the following input is not supported and will prompt an error, while the same lines would have worked as two single-line inputs:
* >x = 5
* x
***********************
* -------------------
* |Reserved Keywords|
* -------------------
* The following keywords have a reserved functionality, i.e. are direct commands for the console and will not be interpreted as code:
* - 'help' - will show a list of all reserved keywords along very short explanations.
* - 'exit' - will shut down the console
* - 'share' - will share the players console with every other player, allowing others to read and write into it. Will force-close other players consoles, if they have one active.
* - 'clear' - will clear all text from the console, except the word 'clear'
* - 'lasttrace' - will show the stack trace of the latest error that occured within IngameConsole
* - 'show' - will show the console, after it was accidently hidden (you can accidently hide it by showing another multiboard, while the console functionality is still up and running).
* - 'printtochat' - will let the print function return to normal behaviour (i.e. print to the chat instead of the console).
* - 'printtoconsole'- will let the print function print to the console (which is default behaviour).
* - 'autosize on' - will enable automatic console resize depending on the longest string in the display. This is turned on by default.
* - 'autosize off' - will disable automatic console resize and instead linebreak long strings into multiple lines.
* - 'textlang eng' - lets the console use english Wc3 text language font size to compute linebreaks (look in your Blizzard launcher settings to find out)
* - 'textlang ger' - lets the console use german Wc3 text language font size to compute linebreaks (look in your Blizzard launcher settings to find out)
***********************
* --------------
* |Paste Helper|
* --------------
* @Luashine has created a tool that simplifies pasting multiple lines of code from outside Wc3 into the IngameConsole.
* This is particularly useful, when you want to execute a large chunk of testcode containing several linebreaks.
* Goto: https://github.com/Luashine/wc3-debug-console-paste-helper#readme
*
*************************************************/
--]]
----------------
--| Settings |--
----------------
---@class IngameConsole
IngameConsole = {
--Settings
numRows = 20 ---@type integer Number of Rows of the console (multiboard), excluding the title row. So putting 20 here will show 21 rows, first being the title row.
, autosize = true ---@type boolean Defines, whether the width of the main Column automatically adjusts with the longest string in the display.
, currentWidth = 0.5 ---@type number Current and starting Screen Share of the console main column.
, mainColMinWidth = 0.3 ---@type number Minimum Screen share of the console main column.
, mainColMaxWidth = 0.8 ---@type number Maximum Scren share of the console main column.
, tsColumnWidth = 0.06 ---@type number Screen Share of the Timestamp Column
, linebreakBuffer = 0.008 ---@type number Screen Share that is added to longest string in display to calculate the screen share for the console main column. Compensates for the small inaccuracy of the String Width function.
, maxLinebreaks = 8 ---@type integer Defines the maximum amount of linebreaks, before the remaining output string will be cut and not further displayed.
, printToConsole = true ---@type boolean defines, if the print function should print to the console or to the chat
, sharedConsole = false ---@type boolean defines, if the console is displayed to each player at the same time (accepting all players input) or if all players much start their own console.
, showTraceOnError = false ---@type boolean defines, if the console shows a trace upon printing errors. Usually not too useful within console, because you have just initiated the erroneous call.
, textLanguage = 'eng' ---@type string text language of your Wc3 installation, which influences font size (look in the settings of your Blizzard launcher). Currently only supports 'eng' and 'ger'.
, colors = {
timestamp = "bbbbbb" ---@type string Timestamp Color
, singleLineInput = "ffffaa" ---@type string Color to be applied to single line console inputs
, multiLineInput = "ffcc55" ---@type string Color to be applied to multi line console inputs
, returnValue = "00ffff" ---@type string Color applied to return values
, error = "ff5555" ---@type string Color to be applied to errors resulting of function calls
, keywordInput = "ff00ff" ---@type string Color to be applied to reserved keyword inputs (console reserved keywords)
, info = "bbbbbb" ---@type string Color to be applied to info messages from the console itself (for instance after creation or after printrestore)
}
--Privates
, numCols = 2 ---@type integer Number of Columns of the console (multiboard). Adjusting this requires further changes on code base.
, player = nil ---@type player player for whom the console is being created
, currentLine = 0 ---@type integer Current Output Line of the console.
, inputload = '' ---@type string Input Holder for multi-line-inputs
, output = {} ---@type string[] Array of all output strings
, outputTimestamps = {} ---@type string[] Array of all output string timestamps
, outputWidths = {} ---@type number[] remembers all string widths to allow for multiboard resize
, trigger = nil ---@type trigger trigger processing all inputs during console lifetime
, multiboard = nil ---@type multiboard
, timer = nil ---@type timer gets started upon console creation to measure timestamps
, errorHandler = nil ---@type fun(errorMsg:string):string error handler to be used within xpcall. We create one per console to make it compatible with console-specific settings.
, lastTrace = '' ---@type string trace of last error occured within console. To be printed via reserved keyword "lasttrace"
--Statics
, keywords = {} ---@type table<string,function> saves functions to be executed for all reserved keywords
, playerConsoles = {} ---@type table<player,IngameConsole> Consoles currently being active. up to one per player.
, originalPrint = print ---@type function original print function to restore, after the console gets closed.
}
IngameConsole.__index = IngameConsole
IngameConsole.__name = 'IngameConsole'
------------------------
--| Console Creation |--
------------------------
---Creates and opens up a new console.
---@param consolePlayer player player for whom the console is being created
---@return IngameConsole
function IngameConsole.create(consolePlayer)
local new = {} ---@type IngameConsole
setmetatable(new, IngameConsole)
---setup Object data
new.player = consolePlayer
new.output = {}
new.outputTimestamps = {}
new.outputWidths = {}
--Timer
new.timer = CreateTimer()
TimerStart(new.timer, 3600., true, nil) --just to get TimeElapsed for printing Timestamps.
--Trigger to be created after short delay, because otherwise it would fire on "-console" input immediately and lead to stack overflow.
new:setupTrigger()
--Multiboard
new:setupMultiboard()
--Create own error handler per console to be compatible with console-specific settings
new:setupErrorHandler()
--Share, if settings say so
if IngameConsole.sharedConsole then
new:makeShared() --we don't have to exit other players consoles, because we look for the setting directly in the class and there just logically can't be other active consoles.
end
--Welcome Message
new:out('info', 0, false, "Console started. Any further chat input will be executed as code, except when beginning with \x22-\x22.")
return new
end
---Creates the multiboard used for console display.
function IngameConsole:setupMultiboard()
self.multiboard = CreateMultiboard()
MultiboardSetRowCount(self.multiboard, self.numRows + 1) --title row adds 1
MultiboardSetColumnCount(self.multiboard, self.numCols)
MultiboardSetTitleText(self.multiboard, "Console")
local mbitem
for col = 1, self.numCols do
for row = 1, self.numRows + 1 do --Title row adds 1
mbitem = MultiboardGetItem(self.multiboard, row -1, col -1)
MultiboardSetItemStyle(mbitem, true, false)
MultiboardSetItemValueColor(mbitem, 255, 255, 255, 255) -- Colors get applied via text color code
MultiboardSetItemWidth(mbitem, (col == 1 and self.tsColumnWidth) or self.currentWidth )
MultiboardReleaseItem(mbitem)
end
end
mbitem = MultiboardGetItem(self.multiboard, 0, 0)
MultiboardSetItemValue(mbitem, "|cffffcc00Timestamp|r")
MultiboardReleaseItem(mbitem)
mbitem = MultiboardGetItem(self.multiboard, 0, 1)
MultiboardSetItemValue(mbitem, "|cffffcc00Line|r")
MultiboardReleaseItem(mbitem)
self:showToOwners()
end
---Creates the trigger that responds to chat events.
function IngameConsole:setupTrigger()
self.trigger = CreateTrigger()
TriggerRegisterPlayerChatEvent(self.trigger, self.player, "", false) --triggers on any input of self.player
TriggerAddCondition(self.trigger, Condition(function() return string.sub(GetEventPlayerChatString(),1,1) ~= '-' end)) --console will not react to entered stuff starting with '-'. This still allows to use other chat orders like "-exec".
TriggerAddAction(self.trigger, function() self:processInput(GetEventPlayerChatString()) end)
end
---Creates an Error Handler to be used by xpcall below.
---Adds stack trace plus formatting to the message.
function IngameConsole:setupErrorHandler()
self.errorHandler = function(errorMsg)
errorMsg = Debug.getLocalErrorMsg(errorMsg)
local _, tracePiece, lastFile = nil, "", errorMsg:match("^.-:") or "<unknown>" -- errors on objects created within Ingame Console don't have a file and linenumber. Consider "x = {}; x[nil] = 5".
local fullMsg = errorMsg .. "\nTraceback (most recent call first):\n" .. (errorMsg:match("^.-:\x25d+") or "<unknown>")
--Get Stack Trace. Starting at depth 5 ensures that "error", "messageHandler", "xpcall" and the input error message are not included.
for loopDepth = 5, 50 do --get trace on depth levels up to 50
---@diagnostic disable-next-line: cast-local-type, assign-type-mismatch
_, tracePiece = pcall(error, "", loopDepth) ---@type boolean, string
tracePiece = Debug.getLocalErrorMsg(tracePiece)
if #tracePiece > 0 then --some trace pieces can be empty, but there can still be valid ones beyond that
fullMsg = fullMsg .. " <- " .. ((tracePiece:match("^.-:") == lastFile) and tracePiece:match(":\x25d+"):sub(2,-1) or tracePiece:match("^.-:\x25d+"))
lastFile = tracePiece:match("^.-:")
end
end
self.lastTrace = fullMsg
return "ERROR: " .. (self.showTraceOnError and fullMsg or errorMsg)
end
end
---Shares this console with all players.
function IngameConsole:makeShared()
local player
for i = 0, GetBJMaxPlayers() -1 do
player = Player(i)
if (GetPlayerSlotState(player) == PLAYER_SLOT_STATE_PLAYING) and (IngameConsole.playerConsoles[player] ~= self) then --second condition ensures that the player chat event is not added twice for the same player.
IngameConsole.playerConsoles[player] = self
TriggerRegisterPlayerChatEvent(self.trigger, player, "", false) --triggers on any input
end
end
self.sharedConsole = true
end
---------------------
--| In |--
---------------------
---Processes a chat string. Each input will be printed. Incomplete multiline-inputs will be halted until completion. Completed inputs will be converted to a function and executed. If they have an output, it will be printed.
---@param inputString string
function IngameConsole:processInput(inputString)
--if the input is a reserved keyword, conduct respective actions and skip remaining actions.
if IngameConsole.keywords[inputString] then --if the input string is a reserved keyword
self:out('keywordInput', 1, false, inputString)
IngameConsole.keywords[inputString](self) --then call the method with the same name. IngameConsole.keywords["exit"](self) is just self.keywords:exit().
return
end
--if the input is a multi-line-input, queue it into the string buffer (inputLoad), but don't yet execute anything
if string.sub(inputString, 1, 1) == '>' then --multiLineInput
inputString = string.sub(inputString, 2, -1)
self:out('multiLineInput',2, false, inputString)
self.inputload = self.inputload .. inputString .. '\n'
else --if the input is either singleLineInput OR the last line of multiLineInput, execute the whole thing.
self:out(self.inputload == '' and 'singleLineInput' or 'multiLineInput', 1, false, inputString)
self.inputload = self.inputload .. inputString
local loadedFunc, errorMsg = load("return " .. self.inputload) --adds return statements, if possible (works for term statements)
if loadedFunc == nil then
loadedFunc, errorMsg = load(self.inputload)
end
self.inputload = '' --empty inputload before execution of pcall. pcall can break (rare case, can for example be provoked with metatable.__tostring = {}), which would corrupt future console inputs.
--manually catch case, where the input did not define a proper Lua statement (i.e. loadfunc is nil)
local results = loadedFunc and table.pack(xpcall(loadedFunc, self.errorHandler)) or {false, "Input is not a valid Lua-statement: " .. errorMsg}
--output error message (unsuccessful case) or return values (successful case)
if not results[1] then --results[1] is the error status that pcall always returns. False stands for: error occured.
self:out('error', 0, true, results[2]) -- second result of pcall is the error message in case an error occured
elseif results.n > 1 then --Check, if there was at least one valid output argument. We check results.n instead of results[2], because we also get nil as a proper return value this way.
self:out('returnValue', 0, true, table.unpack(results, 2, results.n))
end
end
end
----------------------
--| Out |--
----------------------
-- split color codes, split linebreaks, print lines separately, print load-errors, update string width, update text, error handling with stack trace.
---Duplicates Color coding around linebreaks to make each line printable separately.
---Operates incorrectly on lookalike color codes invalidated by preceeding escaped vertical bar (like "||cffffcc00bla|r").
---Also operates incorrectly on multiple color codes, where the first is missing the end sequence (like "|cffffcc00Hello |cff0000ffWorld|r")
---@param inputString string
---@return string, integer
function IngameConsole.spreadColorCodes(inputString)
local replacementTable = {} --remembers all substrings to be replaced and their replacements.
for foundInstance, color in inputString:gmatch("((|c\x25x\x25x\x25x\x25x\x25x\x25x\x25x\x25x).-|r)") do
replacementTable[foundInstance] = foundInstance:gsub("(\r?\n)", "|r\x251" .. color)
end
return inputString:gsub("((|c\x25x\x25x\x25x\x25x\x25x\x25x\x25x\x25x).-|r)", replacementTable)
end
---Concatenates all inputs to one string, spreads color codes around line breaks and prints each line to the console separately.
---@param colorTheme? '"timestamp"'| '"singleLineInput"' | '"multiLineInput"' | '"result"' | '"keywordInput"' | '"info"' | '"error"' | '"returnValue"' Decides about the color to be applied. Currently accepted: 'timestamp', 'singleLineInput', 'multiLineInput', 'result', nil. (nil equals no colorTheme, i.e. white color)
---@param numIndentations integer Number of '>' chars that shall preceed the output
---@param hideTimestamp boolean Set to false to hide the timestamp column and instead show a "->" symbol.
---@param ... any the things to be printed in the console.
function IngameConsole:out(colorTheme, numIndentations, hideTimestamp, ...)
local inputs = table.pack(...)
for i = 1, inputs.n do
inputs[i] = tostring(inputs[i]) --apply tostring on every input param in preparation for table.concat
end
--Concatenate all inputs (4-space-separated)
local printOutput = table.concat(inputs, ' ', 1, inputs.n)
printOutput = printOutput:find("(\r?\n)") and IngameConsole.spreadColorCodes(printOutput) or printOutput
local substrStart, substrEnd = 1, 1
local numLinebreaks, completePrint = 0, true
repeat
substrEnd = (printOutput:find("(\r?\n)", substrStart) or 0) - 1
numLinebreaks, completePrint = self:lineOut(colorTheme, numIndentations, hideTimestamp, numLinebreaks, printOutput:sub(substrStart, substrEnd))
hideTimestamp = true
substrStart = substrEnd + 2
until substrEnd == -1 or numLinebreaks > self.maxLinebreaks
if substrEnd ~= -1 or not completePrint then
self:lineOut('info', 0, false, 0, "Previous value not entirely printed after exceeding maximum number of linebreaks. Consider adjusting 'IngameConsole.maxLinebreaks'.")
end
self:updateMultiboard()
end
---Prints the given string to the console with the specified colorTheme and the specified number of indentations.
---Only supports one-liners (no \n) due to how multiboards work. Will add linebreaks though, if the one-liner doesn't fit into the given multiboard space.
---@param colorTheme? '"timestamp"'| '"singleLineInput"' | '"multiLineInput"' | '"result"' | '"keywordInput"' | '"info"' | '"error"' | '"returnValue"' Decides about the color to be applied. Currently accepted: 'timestamp', 'singleLineInput', 'multiLineInput', 'result', nil. (nil equals no colorTheme, i.e. white color)
---@param numIndentations integer Number of greater '>' chars that shall preceed the output
---@param hideTimestamp boolean Set to false to hide the timestamp column and instead show a "->" symbol.
---@param numLinebreaks integer
---@param printOutput string the line to be printed in the console.
---@return integer numLinebreaks, boolean hasPrintedEverything returns true, if everything could be printed. Returns false otherwise (can happen for very long strings).
function IngameConsole:lineOut(colorTheme, numIndentations, hideTimestamp, numLinebreaks, printOutput)
--add preceeding greater chars
printOutput = ('>'):rep(numIndentations) .. printOutput
--Print a space instead of the empty string. This allows the console to identify, if the string has already been fully printed (see while-loop below).
if printOutput == '' then
printOutput = ' '
end
--Compute Linebreaks.
local linebreakWidth = ((self.autosize and self.mainColMaxWidth) or self.currentWidth )
local partialOutput = nil
local maxPrintableCharPosition
local printWidth
while string.len(printOutput) > 0 and numLinebreaks <= self.maxLinebreaks do --break, if the input string has reached length 0 OR when the maximum number of linebreaks would be surpassed.
--compute max printable substring (in one multiboard line)
maxPrintableCharPosition, printWidth = IngameConsole.getLinebreakData(printOutput, linebreakWidth - self.linebreakBuffer, self.textLanguage)
--adds timestamp to the first line of any output
if numLinebreaks == 0 then
partialOutput = printOutput:sub(1, numIndentations) .. ((IngameConsole.colors[colorTheme] and "|cff" .. IngameConsole.colors[colorTheme] .. printOutput:sub(numIndentations + 1, maxPrintableCharPosition) .. "|r") or printOutput:sub(numIndentations + 1, maxPrintableCharPosition)) --Colorize the output string, if a color theme was specified. IngameConsole.colors[colorTheme] can be nil.
table.insert(self.outputTimestamps, "|cff" .. IngameConsole.colors['timestamp'] .. ((hideTimestamp and ' ->') or IngameConsole.formatTimerElapsed(TimerGetElapsed(self.timer))) .. "|r")
else
partialOutput = (IngameConsole.colors[colorTheme] and "|cff" .. IngameConsole.colors[colorTheme] .. printOutput:sub(1, maxPrintableCharPosition) .. "|r") or printOutput:sub(1, maxPrintableCharPosition) --Colorize the output string, if a color theme was specified. IngameConsole.colors[colorTheme] can be nil.
table.insert(self.outputTimestamps, ' ..') --need a dummy entry in the timestamp list to make it line-progress with the normal output.
end
numLinebreaks = numLinebreaks + 1
--writes output string and width to the console tables.
table.insert(self.output, partialOutput)
table.insert(self.outputWidths, printWidth + self.linebreakBuffer) --remember the Width of this printed string to adjust the multiboard size in case. 0.5 percent is added to avoid the case, where the multiboard width is too small by a tiny bit, thus not showing some string without spaces.
--compute remaining string to print
printOutput = string.sub(printOutput, maxPrintableCharPosition + 1, -1) --remaining string until the end. Returns empty string, if there is nothing left
end
self.currentLine = #self.output
return numLinebreaks, string.len(printOutput) == 0 --printOutput is the empty string, if and only if everything has been printed
end
---Lets the multiboard show the recently printed lines.
function IngameConsole:updateMultiboard()
local startIndex = math.max(self.currentLine - self.numRows, 0) --to be added to loop counter to get to the index of output table to print
local outputIndex = 0
local maxWidth = 0.
local mbitem
for i = 1, self.numRows do --doesn't include title row (index 0)
outputIndex = i + startIndex
mbitem = MultiboardGetItem(self.multiboard, i, 0)
MultiboardSetItemValue(mbitem, self.outputTimestamps[outputIndex] or '')
MultiboardReleaseItem(mbitem)
mbitem = MultiboardGetItem(self.multiboard, i, 1)
MultiboardSetItemValue(mbitem, self.output[outputIndex] or '')
MultiboardReleaseItem(mbitem)
maxWidth = math.max(maxWidth, self.outputWidths[outputIndex] or 0.) --looping through non-defined widths, so need to coalesce with 0
end
--Adjust Multiboard Width, if necessary.
maxWidth = math.min(math.max(maxWidth, self.mainColMinWidth), self.mainColMaxWidth)
if self.autosize and self.currentWidth ~= maxWidth then
self.currentWidth = maxWidth
for i = 1, self.numRows +1 do
mbitem = MultiboardGetItem(self.multiboard, i-1, 1)
MultiboardSetItemWidth(mbitem, maxWidth)
MultiboardReleaseItem(mbitem)
end
self:showToOwners() --reshow multiboard to update item widths on the frontend
end
end
---Shows the multiboard to all owners (one or all players)
function IngameConsole:showToOwners()
if self.sharedConsole or GetLocalPlayer() == self.player then
MultiboardDisplay(self.multiboard, true)
MultiboardMinimize(self.multiboard, false)
end
end
---Formats the elapsed time as "mm: ss. hh" (h being a hundreds of a sec)
function IngameConsole.formatTimerElapsed(elapsedInSeconds)
return string.format("\x2502d: \x2502.f. \x2502.f", elapsedInSeconds // 60, math.fmod(elapsedInSeconds, 60.) // 1, math.fmod(elapsedInSeconds, 1) * 100)
end
---Computes the max printable substring for a given string and a given linebreakWidth (regarding a single line of console).
---Returns both the substrings last char position and its total width in the multiboard.
---@param stringToPrint string the string supposed to be printed in the multiboard console.
---@param linebreakWidth number the maximum allowed width in one line of the console, before a string must linebreak
---@param textLanguage string 'ger' or 'eng'
---@return integer maxPrintableCharPosition, number printWidth
function IngameConsole.getLinebreakData(stringToPrint, linebreakWidth, textLanguage)
local loopWidth = 0.
local bytecodes = table.pack(string.byte(stringToPrint, 1, -1))
for i = 1, bytecodes.n do
loopWidth = loopWidth + string.charMultiboardWidth(bytecodes[i], textLanguage)
if loopWidth > linebreakWidth then
return i-1, loopWidth - string.charMultiboardWidth(bytecodes[i], textLanguage)
end
end
return bytecodes.n, loopWidth
end
-------------------------
--| Reserved Keywords |--
-------------------------
---Exits the Console
---@param self IngameConsole
function IngameConsole.keywords.exit(self)
DestroyMultiboard(self.multiboard)
DestroyTrigger(self.trigger)
DestroyTimer(self.timer)
IngameConsole.playerConsoles[self.player] = nil
if next(IngameConsole.playerConsoles) == nil then --set print function back to original, when no one has an active console left.
print = IngameConsole.originalPrint
end
end
---Lets the console print to chat
---@param self IngameConsole
function IngameConsole.keywords.printtochat(self)
self.printToConsole = false
self:out('info', 0, false, "The print function will print to the normal chat.")
end
---Lets the console print to itself (default)
---@param self IngameConsole
function IngameConsole.keywords.printtoconsole(self)
self.printToConsole = true
self:out('info', 0, false, "The print function will print to the console.")
end
---Shows the console in case it was hidden by another multiboard before
---@param self IngameConsole
function IngameConsole.keywords.show(self)
self:showToOwners() --might be necessary to do, if another multiboard has shown up and thereby hidden the console.
self:out('info', 0, false, "Console is showing.")
end
---Prints all available reserved keywords plus explanations.
---@param self IngameConsole
function IngameConsole.keywords.help(self)
self:out('info', 0, false, "The Console currently reserves the following keywords:")
self:out('info', 0, false, "'help' shows the text you are currently reading.")
self:out('info', 0, false, "'exit' closes the console.")
self:out('info', 0, false, "'lasttrace' shows the stack trace of the latest error that occured within IngameConsole.")
self:out('info', 0, false, "'share' allows other players to read and write into your console, but also force-closes their own consoles.")
self:out('info', 0, false, "'clear' clears all text from the console.")
self:out('info', 0, false, "'show' shows the console. Sensible to use, when displaced by another multiboard.")
self:out('info', 0, false, "'printtochat' lets Wc3 print text to normal chat again.")
self:out('info', 0, false, "'printtoconsole' lets Wc3 print text to the console (default).")
self:out('info', 0, false, "'autosize on' enables automatic console resize depending on the longest line in the display.")
self:out('info', 0, false, "'autosize off' retains the current console size.")
self:out('info', 0, false, "'textlang eng' will use english text installation font size to compute linebreaks (default).")
self:out('info', 0, false, "'textlang ger' will use german text installation font size to compute linebreaks.")
self:out('info', 0, false, "Preceeding a line with '>' prevents immediate execution, until a line not starting with '>' has been entered.")
end
---Clears the display of the console.
---@param self IngameConsole
function IngameConsole.keywords.clear(self)
self.output = {}
self.outputTimestamps = {}
self.outputWidths = {}
self.currentLine = 0
self:out('keywordInput', 1, false, 'clear') --we print 'clear' again. The keyword was already printed by self:processInput, but cleared immediately after.
end
---Shares the console with other players in the same game.
---@param self IngameConsole
function IngameConsole.keywords.share(self)
for _, console in pairs(IngameConsole.playerConsoles) do
if console ~= self then
IngameConsole.keywords['exit'](console) --share was triggered during console runtime, so there potentially are active consoles of others players that need to exit.
end
end
self:makeShared()
self:showToOwners() --showing it to the other players.
self:out('info', 0,false, "The console of player " .. GetConvertedPlayerId(self.player) .. " is now shared with all players.")
end
---Enables auto-sizing of console (will grow and shrink together with text size)
---@param self IngameConsole
IngameConsole.keywords["autosize on"] = function(self)
self.autosize = true
self:out('info', 0,false, "The console will now change size depending on its content.")
end
---Disables auto-sizing of console
---@param self IngameConsole
IngameConsole.keywords["autosize off"] = function(self)
self.autosize = false
self:out('info', 0,false, "The console will retain the width that it currently has.")
end
---Lets linebreaks be computed by german font size
---@param self IngameConsole
IngameConsole.keywords["textlang ger"] = function(self)
self.textLanguage = 'ger'
self:out('info', 0,false, "Linebreaks will now compute with respect to german text installation font size.")
end
---Lets linebreaks be computed by english font size
---@param self IngameConsole
IngameConsole.keywords["textlang eng"] = function(self)
self.textLanguage = 'eng'
self:out('info', 0,false, "Linebreaks will now compute with respect to english text installation font size.")
end
---Prints the stack trace of the latest error that occured within IngameConsole.
---@param self IngameConsole
IngameConsole.keywords["lasttrace"] = function(self)
self:out('error', 0,false, self.lastTrace)
end
--------------------
--| Main Trigger |--
--------------------
do
--Actions to be executed upon typing -exec
local function execCommand_Actions()
local input = string.sub(GetEventPlayerChatString(),7,-1)
print("Executing input: |cffffff44" .. input .. "|r")
--try preceeding the input by a return statement (preparation for printing below)
local loadedFunc, errorMsg = load("return ".. input)
if not loadedFunc then --if that doesn't produce valid code, try without return statement
loadedFunc, errorMsg = load(input)
end
--execute loaded function in case the string defined a valid function. Otherwise print error.
if errorMsg then
print("|cffff5555Invalid Lua-statement: " .. Debug.getLocalErrorMsg(errorMsg) .. "|r")
else
---@diagnostic disable-next-line: param-type-mismatch
local results = table.pack(Debug.try(loadedFunc))
if results[1] ~= nil or results.n > 1 then
for i = 1, results.n do
results[i] = tostring(results[i])
end
--concatenate all function return values to one colorized string
print("|cff00ffff" .. table.concat(results, ' ', 1, results.n) .. "|r")
end
end
end
local function execCommand_Condition()
return string.sub(GetEventPlayerChatString(), 1, 6) == "-exec "
end
local function startIngameConsole()
--if the triggering player already has a console, show that console and stop executing further actions
if IngameConsole.playerConsoles[GetTriggerPlayer()] then
IngameConsole.playerConsoles[GetTriggerPlayer()]:showToOwners()
return
end
--create Ingame Console object
IngameConsole.playerConsoles[GetTriggerPlayer()] = IngameConsole.create(GetTriggerPlayer())
--overwrite print function
print = function(...)
IngameConsole.originalPrint(...) --the new print function will also print "normally", but clear the text immediately after. This is to add the message to the F12-log.
if IngameConsole.playerConsoles[GetLocalPlayer()] and IngameConsole.playerConsoles[GetLocalPlayer()].printToConsole then
ClearTextMessages() --clear text messages for all players having an active console
end
for player, console in pairs(IngameConsole.playerConsoles) do
if console.printToConsole and (player == console.player) then --player == console.player ensures that the console only prints once, even if the console was shared among all players
console:out(nil, 0, false, ...)
end
end
end
end
---Creates the triggers listening to "-console" and "-exec" chat input.
---Being executed within DebugUtils (MarkGameStart overwrite).
function IngameConsole.createTriggers()
--Exec
local execTrigger = CreateTrigger()
TriggerAddCondition(execTrigger, Condition(execCommand_Condition))
TriggerAddAction(execTrigger, execCommand_Actions)
--Real Console
local consoleTrigger = CreateTrigger()
TriggerAddAction(consoleTrigger, startIngameConsole)
--Events
for i = 0, GetBJMaxPlayers() -1 do
TriggerRegisterPlayerChatEvent(execTrigger, Player(i), "-exec ", false)
TriggerRegisterPlayerChatEvent(consoleTrigger, Player(i), "-console", true)
end
end
end
--[[
used by Ingame Console to determine multiboard size
every unknown char will be treated as having default width (see constants below)
--]]
do
----------------------------
----| String Width API |----
----------------------------
local multiboardCharTable = {} ---@type table -- saves the width in screen percent (on 1920 pixel width resolutions) that each char takes up, when displayed in a multiboard.
local DEFAULT_MULTIBOARD_CHAR_WIDTH = 1. / 128. ---@type number -- used for unknown chars (where we didn't define a width in the char table)
local MULTIBOARD_TO_PRINT_FACTOR = 1. / 36. ---@type number -- 36 is actually the lower border (longest width of a non-breaking string only consisting of the letter "i")
---Returns the width of a char in a multiboard, when inputting a char (string of length 1) and 0 otherwise.
---also returns 0 for non-recorded chars (like ` and ´ and ß and § and €)
---@param char string | integer integer bytecode representations of chars are also allowed, i.e. the results of string.byte().
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.charMultiboardWidth(char, textlanguage)
return multiboardCharTable[textlanguage or 'eng'][char] or DEFAULT_MULTIBOARD_CHAR_WIDTH
end
---returns the width of a string in a multiboard (i.e. output is in screen percent)
---unknown chars will be measured with default width (see constants above)
---@param multichar string
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.multiboardWidth(multichar, textlanguage)
local chartable = table.pack(multichar:byte(1,-1)) --packs all bytecode char representations into a table
local charWidth = 0.
for i = 1, chartable.n do
charWidth = charWidth + string.charMultiboardWidth(chartable[i], textlanguage)
end
return charWidth
end
---The function should match the following criteria: If the value returned by this function is smaller than 1.0, than the string fits into a single line on screen.
---The opposite is not necessarily true (but should be true in the majority of cases): If the function returns bigger than 1.0, the string doesn't necessarily break.
---@param char string | integer integer bytecode representations of chars are also allowed, i.e. the results of string.byte().
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.charPrintWidth(char, textlanguage)
return string.charMultiboardWidth(char, textlanguage) * MULTIBOARD_TO_PRINT_FACTOR
end
---The function should match the following criteria: If the value returned by this function is smaller than 1.0, than the string fits into a single line on screen.
---The opposite is not necessarily true (but should be true in the majority of cases): If the function returns bigger than 1.0, the string doesn't necessarily break.
---@param multichar string
---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@return number
function string.printWidth(multichar, textlanguage)
return string.multiboardWidth(multichar, textlanguage) * MULTIBOARD_TO_PRINT_FACTOR
end
----------------------------------
----| String Width Internals |----
----------------------------------
---@param charset '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@param char string|integer either the char or its bytecode
---@param lengthInScreenWidth number
local function setMultiboardCharWidth(charset, char, lengthInScreenWidth)
multiboardCharTable[charset] = multiboardCharTable[charset] or {}
multiboardCharTable[charset][char] = lengthInScreenWidth
end
---numberPlacements says how often the char can be placed in a multiboard column, before reaching into the right bound.
---@param charset '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
---@param char string|integer either the char or its bytecode
---@param numberPlacements integer
local function setMultiboardCharWidthBase80(charset, char, numberPlacements)
setMultiboardCharWidth(charset, char, 0.8 / numberPlacements) --1-based measure. 80./numberPlacements would result in Screen Percent.
setMultiboardCharWidth(charset, char:byte(1,-1), 0.8 / numberPlacements)
end
-- Set Char Width for all printable ascii chars in screen width (1920 pixels). Measured on a 80percent screen width multiboard column by counting the number of chars that fit into it.
-- Font size differs by text install language and patch (1.32- vs. 1.33+)
if BlzGetUnitOrderCount then --identifies patch 1.33+
--German font size for patch 1.33+
setMultiboardCharWidthBase80('ger', "a", 144)
setMultiboardCharWidthBase80('ger', "b", 131)
setMultiboardCharWidthBase80('ger', "c", 144)
setMultiboardCharWidthBase80('ger', "d", 120)
setMultiboardCharWidthBase80('ger', "e", 131)
setMultiboardCharWidthBase80('ger', "f", 240)
setMultiboardCharWidthBase80('ger', "g", 120)
setMultiboardCharWidthBase80('ger', "h", 131)
setMultiboardCharWidthBase80('ger', "i", 288)
setMultiboardCharWidthBase80('ger', "j", 288)
setMultiboardCharWidthBase80('ger', "k", 144)
setMultiboardCharWidthBase80('ger', "l", 288)
setMultiboardCharWidthBase80('ger', "m", 85)
setMultiboardCharWidthBase80('ger', "n", 131)
setMultiboardCharWidthBase80('ger', "o", 120)
setMultiboardCharWidthBase80('ger', "p", 120)
setMultiboardCharWidthBase80('ger', "q", 120)
setMultiboardCharWidthBase80('ger', "r", 206)
setMultiboardCharWidthBase80('ger', "s", 160)
setMultiboardCharWidthBase80('ger', "t", 206)
setMultiboardCharWidthBase80('ger', "u", 131)
setMultiboardCharWidthBase80('ger', "v", 131)
setMultiboardCharWidthBase80('ger', "w", 96)
setMultiboardCharWidthBase80('ger', "x", 144)
setMultiboardCharWidthBase80('ger', "y", 131)
setMultiboardCharWidthBase80('ger', "z", 144)
setMultiboardCharWidthBase80('ger', "A", 103)
setMultiboardCharWidthBase80('ger', "B", 120)
setMultiboardCharWidthBase80('ger', "C", 111)
setMultiboardCharWidthBase80('ger', "D", 103)
setMultiboardCharWidthBase80('ger', "E", 144)
setMultiboardCharWidthBase80('ger', "F", 160)
setMultiboardCharWidthBase80('ger', "G", 96)
setMultiboardCharWidthBase80('ger', "H", 96)
setMultiboardCharWidthBase80('ger', "I", 240)
setMultiboardCharWidthBase80('ger', "J", 240)
setMultiboardCharWidthBase80('ger', "K", 120)
setMultiboardCharWidthBase80('ger', "L", 144)
setMultiboardCharWidthBase80('ger', "M", 76)
setMultiboardCharWidthBase80('ger', "N", 96)
setMultiboardCharWidthBase80('ger', "O", 90)
setMultiboardCharWidthBase80('ger', "P", 131)
setMultiboardCharWidthBase80('ger', "Q", 90)
setMultiboardCharWidthBase80('ger', "R", 120)
setMultiboardCharWidthBase80('ger', "S", 131)
setMultiboardCharWidthBase80('ger', "T", 144)
setMultiboardCharWidthBase80('ger', "U", 103)
setMultiboardCharWidthBase80('ger', "V", 120)
setMultiboardCharWidthBase80('ger', "W", 76)
setMultiboardCharWidthBase80('ger', "X", 111)
setMultiboardCharWidthBase80('ger', "Y", 120)
setMultiboardCharWidthBase80('ger', "Z", 120)
setMultiboardCharWidthBase80('ger', "1", 144)
setMultiboardCharWidthBase80('ger', "2", 120)
setMultiboardCharWidthBase80('ger', "3", 120)
setMultiboardCharWidthBase80('ger', "4", 120)
setMultiboardCharWidthBase80('ger', "5", 120)
setMultiboardCharWidthBase80('ger', "6", 120)
setMultiboardCharWidthBase80('ger', "7", 131)
setMultiboardCharWidthBase80('ger', "8", 120)
setMultiboardCharWidthBase80('ger', "9", 120)
setMultiboardCharWidthBase80('ger', "0", 120)
setMultiboardCharWidthBase80('ger', ":", 288)
setMultiboardCharWidthBase80('ger', ";", 288)
setMultiboardCharWidthBase80('ger', ".", 288)
setMultiboardCharWidthBase80('ger', "#", 120)
setMultiboardCharWidthBase80('ger', ",", 288)
setMultiboardCharWidthBase80('ger', " ", 286) --space
setMultiboardCharWidthBase80('ger', "'", 180)
setMultiboardCharWidthBase80('ger', "!", 180)
setMultiboardCharWidthBase80('ger', "$", 131)
setMultiboardCharWidthBase80('ger', "&", 90)
setMultiboardCharWidthBase80('ger', "/", 180)
setMultiboardCharWidthBase80('ger', "(", 240)
setMultiboardCharWidthBase80('ger', ")", 240)
setMultiboardCharWidthBase80('ger', "=", 120)
setMultiboardCharWidthBase80('ger', "?", 144)
setMultiboardCharWidthBase80('ger', "^", 144)
setMultiboardCharWidthBase80('ger', "<", 144)
setMultiboardCharWidthBase80('ger', ">", 144)
setMultiboardCharWidthBase80('ger', "-", 180)
setMultiboardCharWidthBase80('ger', "+", 120)
setMultiboardCharWidthBase80('ger', "*", 180)
setMultiboardCharWidthBase80('ger', "|", 287) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('ger', "~", 111)
setMultiboardCharWidthBase80('ger', "{", 240)
setMultiboardCharWidthBase80('ger', "}", 240)
setMultiboardCharWidthBase80('ger', "[", 240)
setMultiboardCharWidthBase80('ger', "]", 240)
setMultiboardCharWidthBase80('ger', "_", 144)
setMultiboardCharWidthBase80('ger', "\x25", 103) --percent
setMultiboardCharWidthBase80('ger', "\x5C", 205) --backslash
setMultiboardCharWidthBase80('ger', "\x22", 120) --double quotation mark
setMultiboardCharWidthBase80('ger', "\x40", 90) --at sign
setMultiboardCharWidthBase80('ger', "\x60", 144) --Gravis (Accent)
--English font size for patch 1.33+
setMultiboardCharWidthBase80('eng', "a", 144)
setMultiboardCharWidthBase80('eng', "b", 120)
setMultiboardCharWidthBase80('eng', "c", 131)
setMultiboardCharWidthBase80('eng', "d", 120)
setMultiboardCharWidthBase80('eng', "e", 120)
setMultiboardCharWidthBase80('eng', "f", 240)
setMultiboardCharWidthBase80('eng', "g", 120)
setMultiboardCharWidthBase80('eng', "h", 120)
setMultiboardCharWidthBase80('eng', "i", 288)
setMultiboardCharWidthBase80('eng', "j", 288)
setMultiboardCharWidthBase80('eng', "k", 144)
setMultiboardCharWidthBase80('eng', "l", 288)
setMultiboardCharWidthBase80('eng', "m", 80)
setMultiboardCharWidthBase80('eng', "n", 120)
setMultiboardCharWidthBase80('eng', "o", 111)
setMultiboardCharWidthBase80('eng', "p", 111)
setMultiboardCharWidthBase80('eng', "q", 111)
setMultiboardCharWidthBase80('eng', "r", 206)
setMultiboardCharWidthBase80('eng', "s", 160)
setMultiboardCharWidthBase80('eng', "t", 206)
setMultiboardCharWidthBase80('eng', "u", 120)
setMultiboardCharWidthBase80('eng', "v", 144)
setMultiboardCharWidthBase80('eng', "w", 90)
setMultiboardCharWidthBase80('eng', "x", 131)
setMultiboardCharWidthBase80('eng', "y", 144)
setMultiboardCharWidthBase80('eng', "z", 144)
setMultiboardCharWidthBase80('eng', "A", 103)
setMultiboardCharWidthBase80('eng', "B", 120)
setMultiboardCharWidthBase80('eng', "C", 103)
setMultiboardCharWidthBase80('eng', "D", 96)
setMultiboardCharWidthBase80('eng', "E", 131)
setMultiboardCharWidthBase80('eng', "F", 160)
setMultiboardCharWidthBase80('eng', "G", 96)
setMultiboardCharWidthBase80('eng', "H", 90)
setMultiboardCharWidthBase80('eng', "I", 240)
setMultiboardCharWidthBase80('eng', "J", 240)
setMultiboardCharWidthBase80('eng', "K", 120)
setMultiboardCharWidthBase80('eng', "L", 131)
setMultiboardCharWidthBase80('eng', "M", 76)
setMultiboardCharWidthBase80('eng', "N", 90)
setMultiboardCharWidthBase80('eng', "O", 85)
setMultiboardCharWidthBase80('eng', "P", 120)
setMultiboardCharWidthBase80('eng', "Q", 85)
setMultiboardCharWidthBase80('eng', "R", 120)
setMultiboardCharWidthBase80('eng', "S", 131)
setMultiboardCharWidthBase80('eng', "T", 144)
setMultiboardCharWidthBase80('eng', "U", 96)
setMultiboardCharWidthBase80('eng', "V", 120)
setMultiboardCharWidthBase80('eng', "W", 76)
setMultiboardCharWidthBase80('eng', "X", 111)
setMultiboardCharWidthBase80('eng', "Y", 120)
setMultiboardCharWidthBase80('eng', "Z", 111)
setMultiboardCharWidthBase80('eng', "1", 103)
setMultiboardCharWidthBase80('eng', "2", 111)
setMultiboardCharWidthBase80('eng', "3", 111)
setMultiboardCharWidthBase80('eng', "4", 111)
setMultiboardCharWidthBase80('eng', "5", 111)
setMultiboardCharWidthBase80('eng', "6", 111)
setMultiboardCharWidthBase80('eng', "7", 111)
setMultiboardCharWidthBase80('eng', "8", 111)
setMultiboardCharWidthBase80('eng', "9", 111)
setMultiboardCharWidthBase80('eng', "0", 111)
setMultiboardCharWidthBase80('eng', ":", 288)
setMultiboardCharWidthBase80('eng', ";", 288)
setMultiboardCharWidthBase80('eng', ".", 288)
setMultiboardCharWidthBase80('eng', "#", 103)
setMultiboardCharWidthBase80('eng', ",", 288)
setMultiboardCharWidthBase80('eng', " ", 286) --space
setMultiboardCharWidthBase80('eng', "'", 360)
setMultiboardCharWidthBase80('eng', "!", 288)
setMultiboardCharWidthBase80('eng', "$", 131)
setMultiboardCharWidthBase80('eng', "&", 120)
setMultiboardCharWidthBase80('eng', "/", 180)
setMultiboardCharWidthBase80('eng', "(", 206)
setMultiboardCharWidthBase80('eng', ")", 206)
setMultiboardCharWidthBase80('eng', "=", 111)
setMultiboardCharWidthBase80('eng', "?", 180)
setMultiboardCharWidthBase80('eng', "^", 144)
setMultiboardCharWidthBase80('eng', "<", 111)
setMultiboardCharWidthBase80('eng', ">", 111)
setMultiboardCharWidthBase80('eng', "-", 160)
setMultiboardCharWidthBase80('eng', "+", 111)
setMultiboardCharWidthBase80('eng', "*", 144)
setMultiboardCharWidthBase80('eng', "|", 479) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('eng', "~", 144)
setMultiboardCharWidthBase80('eng', "{", 160)
setMultiboardCharWidthBase80('eng', "}", 160)
setMultiboardCharWidthBase80('eng', "[", 206)
setMultiboardCharWidthBase80('eng', "]", 206)
setMultiboardCharWidthBase80('eng', "_", 120)
setMultiboardCharWidthBase80('eng', "\x25", 103) --percent
setMultiboardCharWidthBase80('eng', "\x5C", 180) --backslash
setMultiboardCharWidthBase80('eng', "\x22", 180) --double quotation mark
setMultiboardCharWidthBase80('eng', "\x40", 85) --at sign
setMultiboardCharWidthBase80('eng', "\x60", 206) --Gravis (Accent)
else
--German font size up to patch 1.32
setMultiboardCharWidthBase80('ger', "a", 144)
setMultiboardCharWidthBase80('ger', "b", 144)
setMultiboardCharWidthBase80('ger', "c", 144)
setMultiboardCharWidthBase80('ger', "d", 131)
setMultiboardCharWidthBase80('ger', "e", 144)
setMultiboardCharWidthBase80('ger', "f", 240)
setMultiboardCharWidthBase80('ger', "g", 120)
setMultiboardCharWidthBase80('ger', "h", 144)
setMultiboardCharWidthBase80('ger', "i", 360)
setMultiboardCharWidthBase80('ger', "j", 288)
setMultiboardCharWidthBase80('ger', "k", 144)
setMultiboardCharWidthBase80('ger', "l", 360)
setMultiboardCharWidthBase80('ger', "m", 90)
setMultiboardCharWidthBase80('ger', "n", 144)
setMultiboardCharWidthBase80('ger', "o", 131)
setMultiboardCharWidthBase80('ger', "p", 131)
setMultiboardCharWidthBase80('ger', "q", 131)
setMultiboardCharWidthBase80('ger', "r", 206)
setMultiboardCharWidthBase80('ger', "s", 180)
setMultiboardCharWidthBase80('ger', "t", 206)
setMultiboardCharWidthBase80('ger', "u", 144)
setMultiboardCharWidthBase80('ger', "v", 131)
setMultiboardCharWidthBase80('ger', "w", 96)
setMultiboardCharWidthBase80('ger', "x", 144)
setMultiboardCharWidthBase80('ger', "y", 131)
setMultiboardCharWidthBase80('ger', "z", 144)
setMultiboardCharWidthBase80('ger', "A", 103)
setMultiboardCharWidthBase80('ger', "B", 131)
setMultiboardCharWidthBase80('ger', "C", 120)
setMultiboardCharWidthBase80('ger', "D", 111)
setMultiboardCharWidthBase80('ger', "E", 144)
setMultiboardCharWidthBase80('ger', "F", 180)
setMultiboardCharWidthBase80('ger', "G", 103)
setMultiboardCharWidthBase80('ger', "H", 103)
setMultiboardCharWidthBase80('ger', "I", 288)
setMultiboardCharWidthBase80('ger', "J", 240)
setMultiboardCharWidthBase80('ger', "K", 120)
setMultiboardCharWidthBase80('ger', "L", 144)
setMultiboardCharWidthBase80('ger', "M", 80)
setMultiboardCharWidthBase80('ger', "N", 103)
setMultiboardCharWidthBase80('ger', "O", 96)
setMultiboardCharWidthBase80('ger', "P", 144)
setMultiboardCharWidthBase80('ger', "Q", 90)
setMultiboardCharWidthBase80('ger', "R", 120)
setMultiboardCharWidthBase80('ger', "S", 144)
setMultiboardCharWidthBase80('ger', "T", 144)
setMultiboardCharWidthBase80('ger', "U", 111)
setMultiboardCharWidthBase80('ger', "V", 120)
setMultiboardCharWidthBase80('ger', "W", 76)
setMultiboardCharWidthBase80('ger', "X", 111)
setMultiboardCharWidthBase80('ger', "Y", 120)
setMultiboardCharWidthBase80('ger', "Z", 120)
setMultiboardCharWidthBase80('ger', "1", 288)
setMultiboardCharWidthBase80('ger', "2", 131)
setMultiboardCharWidthBase80('ger', "3", 144)
setMultiboardCharWidthBase80('ger', "4", 120)
setMultiboardCharWidthBase80('ger', "5", 144)
setMultiboardCharWidthBase80('ger', "6", 131)
setMultiboardCharWidthBase80('ger', "7", 144)
setMultiboardCharWidthBase80('ger', "8", 131)
setMultiboardCharWidthBase80('ger', "9", 131)
setMultiboardCharWidthBase80('ger', "0", 131)
setMultiboardCharWidthBase80('ger', ":", 480)
setMultiboardCharWidthBase80('ger', ";", 360)
setMultiboardCharWidthBase80('ger', ".", 480)
setMultiboardCharWidthBase80('ger', "#", 120)
setMultiboardCharWidthBase80('ger', ",", 360)
setMultiboardCharWidthBase80('ger', " ", 288) --space
setMultiboardCharWidthBase80('ger', "'", 480)
setMultiboardCharWidthBase80('ger', "!", 360)
setMultiboardCharWidthBase80('ger', "$", 160)
setMultiboardCharWidthBase80('ger', "&", 96)
setMultiboardCharWidthBase80('ger', "/", 180)
setMultiboardCharWidthBase80('ger', "(", 288)
setMultiboardCharWidthBase80('ger', ")", 288)
setMultiboardCharWidthBase80('ger', "=", 160)
setMultiboardCharWidthBase80('ger', "?", 180)
setMultiboardCharWidthBase80('ger', "^", 144)
setMultiboardCharWidthBase80('ger', "<", 160)
setMultiboardCharWidthBase80('ger', ">", 160)
setMultiboardCharWidthBase80('ger', "-", 144)
setMultiboardCharWidthBase80('ger', "+", 160)
setMultiboardCharWidthBase80('ger', "*", 206)
setMultiboardCharWidthBase80('ger', "|", 480) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('ger', "~", 144)
setMultiboardCharWidthBase80('ger', "{", 240)
setMultiboardCharWidthBase80('ger', "}", 240)
setMultiboardCharWidthBase80('ger', "[", 240)
setMultiboardCharWidthBase80('ger', "]", 288)
setMultiboardCharWidthBase80('ger', "_", 144)
setMultiboardCharWidthBase80('ger', "\x25", 111) --percent
setMultiboardCharWidthBase80('ger', "\x5C", 206) --backslash
setMultiboardCharWidthBase80('ger', "\x22", 240) --double quotation mark
setMultiboardCharWidthBase80('ger', "\x40", 103) --at sign
setMultiboardCharWidthBase80('ger', "\x60", 240) --Gravis (Accent)
--English Font size up to patch 1.32
setMultiboardCharWidthBase80('eng', "a", 144)
setMultiboardCharWidthBase80('eng', "b", 120)
setMultiboardCharWidthBase80('eng', "c", 131)
setMultiboardCharWidthBase80('eng', "d", 120)
setMultiboardCharWidthBase80('eng', "e", 131)
setMultiboardCharWidthBase80('eng', "f", 240)
setMultiboardCharWidthBase80('eng', "g", 120)
setMultiboardCharWidthBase80('eng', "h", 131)
setMultiboardCharWidthBase80('eng', "i", 360)
setMultiboardCharWidthBase80('eng', "j", 288)
setMultiboardCharWidthBase80('eng', "k", 144)
setMultiboardCharWidthBase80('eng', "l", 360)
setMultiboardCharWidthBase80('eng', "m", 80)
setMultiboardCharWidthBase80('eng', "n", 131)
setMultiboardCharWidthBase80('eng', "o", 120)
setMultiboardCharWidthBase80('eng', "p", 120)
setMultiboardCharWidthBase80('eng', "q", 120)
setMultiboardCharWidthBase80('eng', "r", 206)
setMultiboardCharWidthBase80('eng', "s", 160)
setMultiboardCharWidthBase80('eng', "t", 206)
setMultiboardCharWidthBase80('eng', "u", 131)
setMultiboardCharWidthBase80('eng', "v", 144)
setMultiboardCharWidthBase80('eng', "w", 90)
setMultiboardCharWidthBase80('eng', "x", 131)
setMultiboardCharWidthBase80('eng', "y", 144)
setMultiboardCharWidthBase80('eng', "z", 144)
setMultiboardCharWidthBase80('eng', "A", 103)
setMultiboardCharWidthBase80('eng', "B", 120)
setMultiboardCharWidthBase80('eng', "C", 103)
setMultiboardCharWidthBase80('eng', "D", 103)
setMultiboardCharWidthBase80('eng', "E", 131)
setMultiboardCharWidthBase80('eng', "F", 160)
setMultiboardCharWidthBase80('eng', "G", 103)
setMultiboardCharWidthBase80('eng', "H", 96)
setMultiboardCharWidthBase80('eng', "I", 288)
setMultiboardCharWidthBase80('eng', "J", 240)
setMultiboardCharWidthBase80('eng', "K", 120)
setMultiboardCharWidthBase80('eng', "L", 131)
setMultiboardCharWidthBase80('eng', "M", 76)
setMultiboardCharWidthBase80('eng', "N", 96)
setMultiboardCharWidthBase80('eng', "O", 85)
setMultiboardCharWidthBase80('eng', "P", 131)
setMultiboardCharWidthBase80('eng', "Q", 85)
setMultiboardCharWidthBase80('eng', "R", 120)
setMultiboardCharWidthBase80('eng', "S", 131)
setMultiboardCharWidthBase80('eng', "T", 144)
setMultiboardCharWidthBase80('eng', "U", 103)
setMultiboardCharWidthBase80('eng', "V", 120)
setMultiboardCharWidthBase80('eng', "W", 76)
setMultiboardCharWidthBase80('eng', "X", 111)
setMultiboardCharWidthBase80('eng', "Y", 120)
setMultiboardCharWidthBase80('eng', "Z", 111)
setMultiboardCharWidthBase80('eng', "1", 206)
setMultiboardCharWidthBase80('eng', "2", 131)
setMultiboardCharWidthBase80('eng', "3", 131)
setMultiboardCharWidthBase80('eng', "4", 111)
setMultiboardCharWidthBase80('eng', "5", 131)
setMultiboardCharWidthBase80('eng', "6", 120)
setMultiboardCharWidthBase80('eng', "7", 131)
setMultiboardCharWidthBase80('eng', "8", 111)
setMultiboardCharWidthBase80('eng', "9", 120)
setMultiboardCharWidthBase80('eng', "0", 111)
setMultiboardCharWidthBase80('eng', ":", 360)
setMultiboardCharWidthBase80('eng', ";", 360)
setMultiboardCharWidthBase80('eng', ".", 360)
setMultiboardCharWidthBase80('eng', "#", 103)
setMultiboardCharWidthBase80('eng', ",", 360)
setMultiboardCharWidthBase80('eng', " ", 288) --space
setMultiboardCharWidthBase80('eng', "'", 480)
setMultiboardCharWidthBase80('eng', "!", 360)
setMultiboardCharWidthBase80('eng', "$", 131)
setMultiboardCharWidthBase80('eng', "&", 120)
setMultiboardCharWidthBase80('eng', "/", 180)
setMultiboardCharWidthBase80('eng', "(", 240)
setMultiboardCharWidthBase80('eng', ")", 240)
setMultiboardCharWidthBase80('eng', "=", 111)
setMultiboardCharWidthBase80('eng', "?", 180)
setMultiboardCharWidthBase80('eng', "^", 144)
setMultiboardCharWidthBase80('eng', "<", 131)
setMultiboardCharWidthBase80('eng', ">", 131)
setMultiboardCharWidthBase80('eng', "-", 180)
setMultiboardCharWidthBase80('eng', "+", 111)
setMultiboardCharWidthBase80('eng', "*", 180)
setMultiboardCharWidthBase80('eng', "|", 480) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
setMultiboardCharWidthBase80('eng', "~", 144)
setMultiboardCharWidthBase80('eng', "{", 240)
setMultiboardCharWidthBase80('eng', "}", 240)
setMultiboardCharWidthBase80('eng', "[", 240)
setMultiboardCharWidthBase80('eng', "]", 240)
setMultiboardCharWidthBase80('eng', "_", 120)
setMultiboardCharWidthBase80('eng', "\x25", 103) --percent
setMultiboardCharWidthBase80('eng', "\x5C", 180) --backslash
setMultiboardCharWidthBase80('eng', "\x22", 206) --double quotation mark
setMultiboardCharWidthBase80('eng', "\x40", 96) --at sign
setMultiboardCharWidthBase80('eng', "\x60", 206) --Gravis (Accent)
end
end
if Debug and Debug.endFile then Debug.endFile() end
---@diagnostic disable: undefined-global
if Debug then Debug.beginFile 'TotalInitialization' end
--[[——————————————————————————————————————————————————————
Total Initialization version 5.3.1
Created by: Bribe
Contributors: Eikonium, HerlySQR, Tasyen, Luashine, Forsakn
Inspiration: Almia, ScorpioT1000, Troll-Brain
Hosted at: https://github.com/BribeFromTheHive/Lua/blob/master/TotalInitialization.lua
Debug library hosted at: https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/
————————————————————————————————————————————————————————————]]
---Calls the user's initialization function during the map's loading process. The first argument should either be the init function,
---or it should be the string to give the initializer a name (works similarly to a module name/identically to a vJass library name).
---
---To use requirements, call `Require.strict 'LibraryName'` or `Require.optional 'LibraryName'`. Alternatively, the OnInit callback
---function can take the `Require` table as a single parameter: `OnInit(function(import) import.strict 'ThisIsTheSameAsRequire' end)`.
---
-- - `OnInit.global` or just `OnInit` is called after InitGlobals and is the standard point to initialize.
-- - `OnInit.trig` is called after InitCustomTriggers, and is useful for removing hooks that should only apply to GUI events.
-- - `OnInit.map` is the last point in initialization before the loading screen is completed.
-- - `OnInit.final` occurs immediately after the loading screen has disappeared, and the game has started.
---@class OnInit
--
--Simple Initialization without declaring a library name:
---@overload async fun(initCallback: Initializer.Callback)
--
--Advanced initialization with a library name and an optional third argument to signal to Eikonium's DebugUtils that the file has ended.
---@overload async fun(libraryName: string, initCallback: Initializer.Callback, debugLineNum?: integer)
--
--A way to yield your library to allow other libraries in the same initialization sequence to load, then resume once they have loaded.
---@overload async fun(customInitializerName: string)
OnInit = {}
---@alias Initializer.Callback fun(require?: Requirement | {[string]: Requirement}):...?
---@alias Requirement async fun(reqName: string, source?: table): unknown
-- `Require` will yield the calling `OnInit` initialization function until the requirement (referenced as a string) exists. It will check the
-- global API (for example, does 'GlobalRemap' exist) and then check for any named OnInit resources which might use that same string as its name.
--
-- Due to the way Sumneko's syntax highlighter works, the return value will only be linted for defined @class objects (and doesn't work for regular
-- globals like `TimerStart`). I tried to request the functionality here: https://github.com/sumneko/lua-language-server/issues/1792 , however it
-- was closed. Presumably, there are other requests asking for it, but I wouldn't count on it.
--
-- To declare a requirement, use: `Require.strict 'SomeLibrary'` or (if you don't care about the missing linting functionality) `Require 'SomeLibrary'`
--
-- To optionally require something, use any other suffix (such as `.optionally` or `.nonstrict`): `Require.optional 'SomeLibrary'`
--
---@class Require: { [string]: Requirement }
---@overload async fun(reqName: string, source?: table): string
Require = {}
do
local library = {} --You can change this to false if you don't use `Require` nor the `OnInit.library` API.
--CONFIGURABLE LEGACY API FUNCTION:
---@param _ENV table
---@param OnInit any
local function assignLegacyAPI(_ENV, OnInit)
OnGlobalInit = OnInit; OnTrigInit = OnInit.trig; OnMapInit = OnInit.map; OnGameStart = OnInit.final --Global Initialization Lite API
--OnMainInit = OnInit.main; OnLibraryInit = OnInit.library; OnGameInit = OnInit.final --short-lived experimental API
--onGlobalInit = OnInit; onTriggerInit = OnInit.trig; onInitialization = OnInit.map; onGameStart = OnInit.final --original Global Initialization API
--OnTriggerInit = OnInit.trig; OnInitialization = OnInit.map --Forsakn's Ordered Indices API
end
--END CONFIGURABLES
local _G, rawget, insert =
_G, rawget, table.insert
local initFuncQueue = {}
---@param name string
---@param continue? function
local function runInitializers(name, continue)
--print('running:', name, tostring(initFuncQueue[name]))
if initFuncQueue[name] then
for _,func in ipairs(initFuncQueue[name]) do
coroutine.wrap(func)(Require)
end
initFuncQueue[name] = nil
end
if library then
library:resume()
end
if continue then
continue()
end
end
local function initEverything()
---@param hookName string
---@param continue? function
local function hook(hookName, continue)
local hookedFunc = rawget(_G, hookName)
if hookedFunc then
rawset(_G, hookName,
function()
hookedFunc()
runInitializers(hookName, continue)
end
)
else
runInitializers(hookName, continue)
end
end
hook(
'InitGlobals',
function()
hook(
'InitCustomTriggers',
function()
hook('RunInitializationTriggers')
end
)
end
)
hook(
'MarkGameStarted',
function()
if library then
for _,func in ipairs(library.queuedInitializerList) do
func(nil, true) --run errors for missing requirements.
end
for _,func in pairs(library.yieldedModuleMatrix) do
func(true) --run errors for modules that aren't required.
end
end
OnInit = nil
Require = nil
end
)
end
---@param initName string
---@param libraryName string | Initializer.Callback
---@param func? Initializer.Callback
---@param debugLineNum? integer
---@param incDebugLevel? boolean
local function addUserFunc(initName, libraryName, func, debugLineNum, incDebugLevel)
if not func then
---@cast libraryName Initializer.Callback
func = libraryName
else
assert(type(libraryName) == 'string')
if debugLineNum and Debug then
Debug.beginFile(libraryName, incDebugLevel and 3 or 2)
Debug.data.sourceMap[#Debug.data.sourceMap].lastLine = debugLineNum
end
if library then
func = library:create(libraryName, func)
end
end
assert(type(func) == 'function')
--print('adding user func: ' , initName , libraryName, debugLineNum, incDebugLevel)
initFuncQueue[initName] = initFuncQueue[initName] or {}
insert(initFuncQueue[initName], func)
if initName == 'root' or initName == 'module' then
runInitializers(initName)
end
end
---@param name string
local function createInit(name)
---@async
---@param libraryName string --Assign your callback a unique name, allowing other OnInit callbacks can use it as a requirement.
---@param userInitFunc Initializer.Callback --Define a function to be called at the chosen point in the initialization process. It can optionally take the `Require` object as a parameter. Its optional return value(s) are passed to a requiring library via the `Require` object (defaults to `true`).
---@param debugLineNum? integer --If the Debug library is present, you can call Debug.getLine() for this parameter (which should coincide with the last line of your script file). This will neatly tie-in with OnInit's built-in Debug library functionality to define a starting line and an ending line for your module.
---@overload async fun(userInitFunc: Initializer.Callback)
return function(libraryName, userInitFunc, debugLineNum)
addUserFunc(name, libraryName, userInitFunc, debugLineNum)
end
end
OnInit.global = createInit 'InitGlobals' -- Called after InitGlobals, and is the standard point to initialize.
OnInit.trig = createInit 'InitCustomTriggers' -- Called after InitCustomTriggers, and is useful for removing hooks that should only apply to GUI events.
OnInit.map = createInit 'RunInitializationTriggers' -- Called last in the script's loading screen sequence. Runs after the GUI "Map Initialization" events have run.
OnInit.final = createInit 'MarkGameStarted' -- Called immediately after the loading screen has disappeared, and the game has started.
do
---@param self table
---@param libraryNameOrInitFunc function | string
---@param userInitFunc function
---@param debugLineNum number
local function __call(
self,
libraryNameOrInitFunc,
userInitFunc,
debugLineNum
)
if userInitFunc or type(libraryNameOrInitFunc) == 'function' then
addUserFunc(
'InitGlobals', --Calling OnInit directly defaults to OnInit.global (AKA OnGlobalInit)
libraryNameOrInitFunc,
userInitFunc,
debugLineNum,
true
)
elseif library then
library:declare(libraryNameOrInitFunc) --API handler for OnInit "Custom initializer"
else
error(
"Bad OnInit args: "..
tostring(libraryNameOrInitFunc) .. ", " ..
tostring(userInitFunc)
)
end
end
setmetatable(OnInit --[[@as table]], { __call = __call })
end
do --if you don't need the initializers for 'root', 'config' and 'main', you can delete this do...end block.
local gmt = getmetatable(_G) or
getmetatable(setmetatable(_G, {}))
local rawIndex = gmt.__newindex or rawset
local hookMainAndConfig
---@param _G table
---@param key string
---@param fnOrDiscard unknown
function hookMainAndConfig(_G, key, fnOrDiscard)
if key == 'main' or key == 'config' then
---@cast fnOrDiscard function
if key == 'main' then
runInitializers 'root'
end
rawIndex(_G, key, function()
if key == 'config' then
fnOrDiscard()
elseif gmt.__newindex == hookMainAndConfig then
gmt.__newindex = rawIndex --restore the original __newindex if no further hooks on __newindex exist.
end
runInitializers(key)
if key == 'main' then
fnOrDiscard()
end
end)
else
rawIndex(_G, key, fnOrDiscard)
end
end
gmt.__newindex = hookMainAndConfig
OnInit.root = createInit 'root' -- Runs immediately during the Lua root, but is yieldable (allowing requirements) and pcalled.
OnInit.config = createInit 'config' -- Runs when `config` is called. Credit to @Luashine: https://www.hiveworkshop.com/threads/inject-main-config-from-we-trigger-code-like-jasshelper.338201/
OnInit.main = createInit 'main' -- Runs when `main` is called. Idea from @Tasyen: https://www.hiveworkshop.com/threads/global-initialization.317099/post-3374063
end
if library then
library.queuedInitializerList = {}
library.customDeclarationList = {}
library.yieldedModuleMatrix = {}
library.moduleValueMatrix = {}
function library:pack(name, ...)
self.moduleValueMatrix[name] = table.pack(...)
end
function library:resume()
if self.queuedInitializerList[1] then
local continue, tempQueue, forceOptional
::initLibraries::
repeat
continue=false
self.queuedInitializerList, tempQueue =
{}, self.queuedInitializerList
for _,func in ipairs(tempQueue) do
if func(forceOptional) then
continue=true --Something was initialized; therefore further systems might be able to initialize.
else
insert(self.queuedInitializerList, func) --If the queued initializer returns false, that means its requirement wasn't met, so we re-queue it.
end
end
until not continue or not self.queuedInitializerList[1]
if self.customDeclarationList[1] then
self.customDeclarationList, tempQueue =
{}, self.customDeclarationList
for _,func in ipairs(tempQueue) do
func() --unfreeze any custom initializers.
end
elseif not forceOptional then
forceOptional = true
else
return
end
goto initLibraries
end
end
local function declareName(name, initialValue)
assert(type(name) == 'string')
assert(library.moduleValueMatrix[name] == nil)
library.moduleValueMatrix[name] =
initialValue and { true, n = 1 }
end
function library:create(name, userFunc)
assert(type(userFunc) == 'function')
declareName(name, false) --declare itself as a non-loaded library.
return function()
self:pack(name, userFunc(Require)) --pack return values to allow multiple values to be communicated.
if self.moduleValueMatrix[name].n == 0 then
self:pack(name, true) --No values were returned; therefore simply package the value as `true`
end
end
end
---@async
function library:declare(name)
declareName(name, true) --declare itself as a loaded library.
local co = coroutine.running()
insert(
self.customDeclarationList,
function()
coroutine.resume(co)
end
)
coroutine.yield() --yields the calling function until after all currently-queued initializers have run.
end
local processRequirement
---@async
function processRequirement(
optional,
requirement,
explicitSource
)
if type(optional) == 'string' then
optional, requirement, explicitSource =
true, optional, requirement --optional requirement (processed by the __index method)
else
optional = false --strict requirement (processed by the __call method)
end
local source = explicitSource or _G
assert(type(source)=='table')
assert(type(requirement)=='string')
::reindex::
local subSource, subReq =
requirement:match("([\x25w_]+)\x25.(.+)") --Check if user is requiring using "table.property" syntax
if subSource and subReq then
source,
requirement =
processRequirement(subSource, source), --If the container is nil, yield until it is not.
subReq
if type(source)=='table' then
explicitSource = source
goto reindex --check for further nested properties ("table.property.subProperty.anyOthers").
else
return --The source table for the requirement wasn't found, so disregard the rest (this only happens with optional requirements).
end
end
local function loadRequirement(unpack)
local package = rawget(source, requirement) --check if the requirement exists in the host table.
if not package and not explicitSource then
if library.yieldedModuleMatrix[requirement] then
library.yieldedModuleMatrix[requirement]() --load module if it exists
end
package = library.moduleValueMatrix[requirement] --retrieve the return value from the module.
if unpack and type(package)=='table' then
return table.unpack(package, 1, package.n) --using unpack allows any number of values to be returned by the required library.
end
end
return package
end
local co, loaded
local function checkReqs(forceOptional, printErrors)
if not loaded then
loaded = loadRequirement()
loaded = loaded or optional and
(loaded==nil or forceOptional)
if loaded then
if co then coroutine.resume(co) end --resume only if it was yielded in the first place.
return loaded
elseif printErrors then
coroutine.resume(co, true)
end
end
end
if not checkReqs() then --only yield if the requirement doesn't already exist.
co = coroutine.running()
insert(library.queuedInitializerList, checkReqs)
if coroutine.yield() then
error("Missing Requirement: "..requirement) --handle the error within the user's function to get an accurate stack trace via the `try` function.
end
end
return loadRequirement(true)
end
---@type Requirement
function Require.strict(name, explicitSource)
return processRequirement(nil, name, explicitSource)
end
setmetatable(Require --[[@as table]], {
__call = processRequirement,
__index = function()
return processRequirement
end
})
local module = createInit 'module'
--- `OnInit.module` will only call the OnInit function if the module is required by another resource, rather than being called at a pre-
--- specified point in the loading process. It works similarly to Go, in that including modules in your map that are not actually being
--- required will throw an error message.
---@param name string
---@param func fun(require?: Initializer.Callback):any
---@param debugLineNum? integer
OnInit.module = function(name, func, debugLineNum)
if func then
local userFunc = func
func = function(require)
local co = coroutine.running()
library.yieldedModuleMatrix[name] =
function(failure)
library.yieldedModuleMatrix[name] = nil
coroutine.resume(co, failure)
end
if coroutine.yield() then
error("Module declared but not required: "..name)
end
return userFunc(require)
end
end
module(name, func, debugLineNum)
end
end
if assignLegacyAPI then --This block handles legacy code.
---Allows packaging multiple requirements into one table and queues the initialization for later.
---@deprecated
---@param initList string | table
---@param userFunc function
function OnInit.library(initList, userFunc)
local typeOf = type(initList)
assert(typeOf=='table' or typeOf=='string')
assert(type(userFunc) == 'function')
local function caller(use)
if typeOf=='string' then
use(initList)
else
for _,initName in ipairs(initList) do
use(initName)
end
if initList.optional then
for _,initName in ipairs(initList.optional) do
use.lazily(initName)
end
end
end
end
if initList.name then
OnInit(initList.name, caller)
else
OnInit(caller)
end
end
local legacyTable = {}
assignLegacyAPI(legacyTable, OnInit)
for key,func in pairs(legacyTable) do
rawset(_G, key, func)
end
OnInit.final(function()
for key in pairs(legacyTable) do
rawset(_G, key, nil)
end
end)
end
initEverything()
end
if Debug then Debug.endFile() end
do
OnInit.global("AbilityFields", function()
ABILITY_FIELD_OF_STRING = {
abpx = ABILITY_IF_BUTTON_POSITION_NORMAL_X,
abpy = ABILITY_IF_BUTTON_POSITION_NORMAL_Y,
aubx = ABILITY_IF_BUTTON_POSITION_ACTIVATED_X,
auby = ABILITY_IF_BUTTON_POSITION_ACTIVATED_Y,
arpx = ABILITY_IF_BUTTON_POSITION_RESEARCH_X,
arpy = ABILITY_IF_BUTTON_POSITION_RESEARCH_Y,
amsp = ABILITY_IF_MISSILE_SPEED,
atac = ABILITY_IF_TARGET_ATTACHMENTS,
acac = ABILITY_IF_CASTER_ATTACHMENTS,
apri = ABILITY_IF_PRIORITY,
alev = ABILITY_IF_LEVELS,
arlv = ABILITY_IF_REQUIRED_LEVEL,
alsk = ABILITY_IF_LEVEL_SKIP_REQUIREMENT,
aher = ABILITY_BF_HERO_ABILITY,
aite = ABILITY_BF_ITEM_ABILITY,
achd = ABILITY_BF_CHECK_DEPENDENCIES,
amac = ABILITY_RF_ARF_MISSILE_ARC,
anam = ABILITY_SF_NAME,
auar = ABILITY_SF_ICON_ACTIVATED,
arar = ABILITY_SF_ICON_RESEARCH,
aefs = ABILITY_SF_EFFECT_SOUND,
aefl = ABILITY_SF_EFFECT_SOUND_LOOPING,
amcs = ABILITY_ILF_MANA_COST,
hbz1 = ABILITY_ILF_NUMBER_OF_WAVES,
hbz3 = ABILITY_ILF_NUMBER_OF_SHARDS,
hmt1 = ABILITY_ILF_NUMBER_OF_UNITS_TELEPORTED,
hwe2 = ABILITY_ILF_SUMMONED_UNIT_COUNT_HWE2,
omi1 = ABILITY_ILF_NUMBER_OF_IMAGES,
uan1 = ABILITY_ILF_NUMBER_OF_CORPSES_RAISED_UAN1,
eme2 = ABILITY_ILF_MORPHING_FLAGS,
nrg5 = ABILITY_ILF_STRENGTH_BONUS_NRG5,
nrg6 = ABILITY_ILF_DEFENSE_BONUS_NRG6,
ocl2 = ABILITY_ILF_NUMBER_OF_TARGETS_HIT,
ofs1 = ABILITY_ILF_DETECTION_TYPE_OFS1,
osf2 = ABILITY_ILF_NUMBER_OF_SUMMONED_UNITS_OSF2,
efn1 = ABILITY_ILF_NUMBER_OF_SUMMONED_UNITS_EFN1,
hre1 = ABILITY_ILF_NUMBER_OF_CORPSES_RAISED_HRE1,
hca4 = ABILITY_ILF_STACK_FLAGS,
ndp2 = ABILITY_ILF_MINIMUM_NUMBER_OF_UNITS,
ndp3 = ABILITY_ILF_MAXIMUM_NUMBER_OF_UNITS_NDP3,
nrc2 = ABILITY_ILF_NUMBER_OF_UNITS_CREATED_NRC2,
ams3 = ABILITY_ILF_SHIELD_LIFE,
ams4 = ABILITY_ILF_MANA_LOSS_AMS4,
bgm1 = ABILITY_ILF_GOLD_PER_INTERVAL_BGM1,
bgm3 = ABILITY_ILF_MAX_NUMBER_OF_MINERS,
car1 = ABILITY_ILF_CARGO_CAPACITY,
dev3 = ABILITY_ILF_MAXIMUM_CREEP_LEVEL_DEV3,
dev1 = ABILITY_ILF_MAX_CREEP_LEVEL_DEV1,
egm1 = ABILITY_ILF_GOLD_PER_INTERVAL_EGM1,
fae1 = ABILITY_ILF_DEFENSE_REDUCTION,
fla1 = ABILITY_ILF_DETECTION_TYPE_FLA1,
fla3 = ABILITY_ILF_FLARE_COUNT,
gld1 = ABILITY_ILF_MAX_GOLD,
gld3 = ABILITY_ILF_MINING_CAPACITY,
gyd1 = ABILITY_ILF_MAXIMUM_NUMBER_OF_CORPSES_GYD1,
har1 = ABILITY_ILF_DAMAGE_TO_TREE,
har2 = ABILITY_ILF_LUMBER_CAPACITY,
har3 = ABILITY_ILF_GOLD_CAPACITY,
inf2 = ABILITY_ILF_DEFENSE_INCREASE_INF2,
neu2 = ABILITY_ILF_INTERACTION_TYPE,
ndt1 = ABILITY_ILF_GOLD_COST_NDT1,
ndt2 = ABILITY_ILF_LUMBER_COST_NDT2,
ndt3 = ABILITY_ILF_DETECTION_TYPE_NDT3,
poi4 = ABILITY_ILF_STACKING_TYPE_POI4,
poa5 = ABILITY_ILF_STACKING_TYPE_POA5,
ply1 = ABILITY_ILF_MAXIMUM_CREEP_LEVEL_PLY1,
pos1 = ABILITY_ILF_MAXIMUM_CREEP_LEVEL_POS1,
prg1 = ABILITY_ILF_MOVEMENT_UPDATE_FREQUENCY_PRG1,
prg2 = ABILITY_ILF_ATTACK_UPDATE_FREQUENCY_PRG2,
prg6 = ABILITY_ILF_MANA_LOSS_PRG6,
rai1 = ABILITY_ILF_UNITS_SUMMONED_TYPE_ONE,
rai2 = ABILITY_ILF_UNITS_SUMMONED_TYPE_TWO,
ucb5 = ABILITY_ILF_MAX_UNITS_SUMMONED,
rej3 = ABILITY_ILF_ALLOW_WHEN_FULL_REJ3,
rpb5 = ABILITY_ILF_MAXIMUM_UNITS_CHARGED_TO_CASTER,
rpb6 = ABILITY_ILF_MAXIMUM_UNITS_AFFECTED,
roa2 = ABILITY_ILF_DEFENSE_INCREASE_ROA2,
roa7 = ABILITY_ILF_MAX_UNITS_ROA7,
roo1 = ABILITY_ILF_ROOTED_WEAPONS,
roo2 = ABILITY_ILF_UPROOTED_WEAPONS,
roo4 = ABILITY_ILF_UPROOTED_DEFENSE_TYPE,
sal2 = ABILITY_ILF_ACCUMULATION_STEP,
esn4 = ABILITY_ILF_NUMBER_OF_OWLS,
spo4 = ABILITY_ILF_STACKING_TYPE_SPO4,
sod1 = ABILITY_ILF_NUMBER_OF_UNITS,
spa1 = ABILITY_ILF_SPIDER_CAPACITY,
wha2 = ABILITY_ILF_INTERVALS_BEFORE_CHANGING_TREES,
iagi = ABILITY_ILF_AGILITY_BONUS,
iint = ABILITY_ILF_INTELLIGENCE_BONUS,
istr = ABILITY_ILF_STRENGTH_BONUS_ISTR,
iatt = ABILITY_ILF_ATTACK_BONUS,
idef = ABILITY_ILF_DEFENSE_BONUS_IDEF,
isn1 = ABILITY_ILF_SUMMON_1_AMOUNT,
isn2 = ABILITY_ILF_SUMMON_2_AMOUNT,
ixpg = ABILITY_ILF_EXPERIENCE_GAINED,
ihpg = ABILITY_ILF_HIT_POINTS_GAINED_IHPG,
impg = ABILITY_ILF_MANA_POINTS_GAINED_IMPG,
ihp2 = ABILITY_ILF_HIT_POINTS_GAINED_IHP2,
imp2 = ABILITY_ILF_MANA_POINTS_GAINED_IMP2,
idic = ABILITY_ILF_DAMAGE_BONUS_DICE,
iarp = ABILITY_ILF_ARMOR_PENALTY_IARP,
iob5 = ABILITY_ILF_ENABLED_ATTACK_INDEX_IOB5,
ilev = ABILITY_ILF_LEVELS_GAINED,
ilif = ABILITY_ILF_MAX_LIFE_GAINED,
iman = ABILITY_ILF_MAX_MANA_GAINED,
igol = ABILITY_ILF_GOLD_GIVEN,
ilum = ABILITY_ILF_LUMBER_GIVEN,
ifa1 = ABILITY_ILF_DETECTION_TYPE_IFA1,
icre = ABILITY_ILF_MAXIMUM_CREEP_LEVEL_ICRE,
imvb = ABILITY_ILF_MOVEMENT_SPEED_BONUS,
ihpr = ABILITY_ILF_HIT_POINTS_REGENERATED_PER_SECOND,
isib = ABILITY_ILF_SIGHT_RANGE_BONUS,
icfd = ABILITY_ILF_DAMAGE_PER_DURATION,
icfm = ABILITY_ILF_MANA_USED_PER_SECOND,
icfx = ABILITY_ILF_EXTRA_MANA_REQUIRED,
idet = ABILITY_ILF_DETECTION_RADIUS_IDET,
idim = ABILITY_ILF_MANA_LOSS_PER_UNIT_IDIM,
idid = ABILITY_ILF_DAMAGE_TO_SUMMONED_UNITS_IDID,
irec = ABILITY_ILF_MAXIMUM_NUMBER_OF_UNITS_IREC,
ircd = ABILITY_ILF_DELAY_AFTER_DEATH_SECONDS,
irc2 = ABILITY_ILF_RESTORED_LIFE,
irc3 = ABILITY_ILF_RESTORED_MANA__1_FOR_CURRENT,
ihps = ABILITY_ILF_HIT_POINTS_RESTORED,
imps = ABILITY_ILF_MANA_POINTS_RESTORED,
itpm = ABILITY_ILF_MAXIMUM_NUMBER_OF_UNITS_ITPM,
cad1 = ABILITY_ILF_NUMBER_OF_CORPSES_RAISED_CAD1,
wrs3 = ABILITY_ILF_TERRAIN_DEFORMATION_DURATION_MS,
uds1 = ABILITY_ILF_MAXIMUM_UNITS,
det1 = ABILITY_ILF_DETECTION_TYPE_DET1,
nsp1 = ABILITY_ILF_GOLD_COST_PER_STRUCTURE,
nsp2 = ABILITY_ILF_LUMBER_COST_PER_USE,
nsp3 = ABILITY_ILF_DETECTION_TYPE_NSP3,
uls1 = ABILITY_ILF_NUMBER_OF_SWARM_UNITS,
uls3 = ABILITY_ILF_MAX_SWARM_UNITS_PER_TARGET,
nba2 = ABILITY_ILF_NUMBER_OF_SUMMONED_UNITS_NBA2,
nch1 = ABILITY_ILF_MAXIMUM_CREEP_LEVEL_NCH1,
nsi1 = ABILITY_ILF_ATTACKS_PREVENTED,
efk3 = ABILITY_ILF_MAXIMUM_NUMBER_OF_TARGETS_EFK3,
esv1 = ABILITY_ILF_NUMBER_OF_SUMMONED_UNITS_ESV1,
exh1 = ABILITY_ILF_MAXIMUM_NUMBER_OF_CORPSES_EXH1,
inv1 = ABILITY_ILF_ITEM_CAPACITY,
spl2 = ABILITY_ILF_MAXIMUM_NUMBER_OF_TARGETS_SPL2,
irl3 = ABILITY_ILF_ALLOW_WHEN_FULL_IRL3,
idc3 = ABILITY_ILF_MAXIMUM_DISPELLED_UNITS,
imo1 = ABILITY_ILF_NUMBER_OF_LURES,
ict1 = ABILITY_ILF_NEW_TIME_OF_DAY_HOUR,
ict2 = ABILITY_ILF_NEW_TIME_OF_DAY_MINUTE,
mec1 = ABILITY_ILF_NUMBER_OF_UNITS_CREATED_MEC1,
spb3 = ABILITY_ILF_MINIMUM_SPELLS,
spb4 = ABILITY_ILF_MAXIMUM_SPELLS,
gra3 = ABILITY_ILF_DISABLED_ATTACK_INDEX,
gra4 = ABILITY_ILF_ENABLED_ATTACK_INDEX_GRA4,
gra5 = ABILITY_ILF_MAXIMUM_ATTACKS,
npr1 = ABILITY_ILF_BUILDING_TYPES_ALLOWED_NPR1,
nsa1 = ABILITY_ILF_BUILDING_TYPES_ALLOWED_NSA1,
iaa1 = ABILITY_ILF_ATTACK_MODIFICATION,
npa5 = ABILITY_ILF_SUMMONED_UNIT_COUNT_NPA5,
igl1 = ABILITY_ILF_UPGRADE_LEVELS,
ndo2 = ABILITY_ILF_NUMBER_OF_SUMMONED_UNITS_NDO2,
nst1 = ABILITY_ILF_BEASTS_PER_SECOND,
ncl2 = ABILITY_ILF_TARGET_TYPE,
ncl3 = ABILITY_ILF_OPTIONS,
nab3 = ABILITY_ILF_ARMOR_PENALTY_NAB3,
nhs6 = ABILITY_ILF_WAVE_COUNT_NHS6,
ntm3 = ABILITY_ILF_MAX_CREEP_LEVEL_NTM3,
ncs3 = ABILITY_ILF_MISSILE_COUNT,
nlm3 = ABILITY_ILF_SPLIT_ATTACK_COUNT,
nlm6 = ABILITY_ILF_GENERATION_COUNT,
nvc1 = ABILITY_ILF_ROCK_RING_COUNT,
nvc2 = ABILITY_ILF_WAVE_COUNT_NVC2,
tau1 = ABILITY_ILF_PREFER_HOSTILES_TAU1,
tau2 = ABILITY_ILF_PREFER_FRIENDLIES_TAU2,
tau3 = ABILITY_ILF_MAX_UNITS_TAU3,
tau4 = ABILITY_ILF_NUMBER_OF_PULSES,
hwe1 = ABILITY_ILF_SUMMONED_UNIT_TYPE_HWE1,
uin4 = ABILITY_ILF_SUMMONED_UNIT_UIN4,
osf1 = ABILITY_ILF_SUMMONED_UNIT_OSF1,
efnu = ABILITY_ILF_SUMMONED_UNIT_TYPE_EFNU,
nbau = ABILITY_ILF_SUMMONED_UNIT_TYPE_NBAU,
ntou = ABILITY_ILF_SUMMONED_UNIT_TYPE_NTOU,
esvu = ABILITY_ILF_SUMMONED_UNIT_TYPE_ESVU,
nef1 = ABILITY_ILF_SUMMONED_UNIT_TYPES,
ndou = ABILITY_ILF_SUMMONED_UNIT_TYPE_NDOU,
emeu = ABILITY_ILF_ALTERNATE_FORM_UNIT_EMEU,
aplu = ABILITY_ILF_PLAGUE_WARD_UNIT_TYPE,
btl1 = ABILITY_ILF_ALLOWED_UNIT_TYPE_BTL1,
cha1 = ABILITY_ILF_NEW_UNIT_TYPE,
ent1 = ABILITY_ILF_RESULTING_UNIT_TYPE_ENT1,
gydu = ABILITY_ILF_CORPSE_UNIT_TYPE,
loa1 = ABILITY_ILF_ALLOWED_UNIT_TYPE_LOA1,
raiu = ABILITY_ILF_UNIT_TYPE_FOR_LIMIT_CHECK,
stau = ABILITY_ILF_WARD_UNIT_TYPE_STAU,
iobu = ABILITY_ILF_EFFECT_ABILITY,
ndc2 = ABILITY_ILF_CONVERSION_UNIT,
nsl1 = ABILITY_ILF_UNIT_TO_PRESERVE,
chl1 = ABILITY_ILF_UNIT_TYPE_ALLOWED,
ussu = ABILITY_ILF_SWARM_UNIT_TYPE,
coau = ABILITY_ILF_RESULTING_UNIT_TYPE_COAU,
exhu = ABILITY_ILF_UNIT_TYPE_EXHU,
hwdu = ABILITY_ILF_WARD_UNIT_TYPE_HWDU,
imou = ABILITY_ILF_LURE_UNIT_TYPE,
ipmu = ABILITY_ILF_UNIT_TYPE_IPMU,
nsyu = ABILITY_ILF_FACTORY_UNIT_ID,
nfyu = ABILITY_ILF_SPAWN_UNIT_ID_NFYU,
nvcu = ABILITY_ILF_DESTRUCTIBLE_ID,
iglu = ABILITY_ILF_UPGRADE_TYPE,
acas = ABILITY_RLF_CASTING_TIME,
adur = ABILITY_RLF_DURATION_NORMAL,
ahdu = ABILITY_RLF_DURATION_HERO,
acdn = ABILITY_RLF_COOLDOWN,
aare = ABILITY_RLF_AREA_OF_EFFECT,
aran = ABILITY_RLF_CAST_RANGE,
hbz2 = ABILITY_RLF_DAMAGE_HBZ2,
hbz4 = ABILITY_RLF_BUILDING_REDUCTION_HBZ4,
hbz5 = ABILITY_RLF_DAMAGE_PER_SECOND_HBZ5,
hbz6 = ABILITY_RLF_MAXIMUM_DAMAGE_PER_WAVE,
hab1 = ABILITY_RLF_MANA_REGENERATION_INCREASE,
hmt2 = ABILITY_RLF_CASTING_DELAY,
oww1 = ABILITY_RLF_DAMAGE_PER_SECOND_OWW1,
oww2 = ABILITY_RLF_MAGIC_DAMAGE_REDUCTION_OWW2,
ocr1 = ABILITY_RLF_CHANCE_TO_CRITICAL_STRIKE,
ocr2 = ABILITY_RLF_DAMAGE_MULTIPLIER_OCR2,
ocr3 = ABILITY_RLF_DAMAGE_BONUS_OCR3,
ocr4 = ABILITY_RLF_CHANCE_TO_EVADE_OCR4,
omi2 = ABILITY_RLF_DAMAGE_DEALT_PERCENT_OMI2,
omi3 = ABILITY_RLF_DAMAGE_TAKEN_PERCENT_OMI3,
omi4 = ABILITY_RLF_ANIMATION_DELAY,
owk1 = ABILITY_RLF_TRANSITION_TIME,
owk2 = ABILITY_RLF_MOVEMENT_SPEED_INCREASE_PERCENT_OWK2,
owk3 = ABILITY_RLF_BACKSTAB_DAMAGE,
udc1 = ABILITY_RLF_AMOUNT_HEALED_DAMAGED_UDC1,
udp1 = ABILITY_RLF_LIFE_CONVERTED_TO_MANA,
udp2 = ABILITY_RLF_LIFE_CONVERTED_TO_LIFE,
uau1 = ABILITY_RLF_MOVEMENT_SPEED_INCREASE_PERCENT_UAU1,
uau2 = ABILITY_RLF_LIFE_REGENERATION_INCREASE_PERCENT,
eev1 = ABILITY_RLF_CHANCE_TO_EVADE_EEV1,
eim1 = ABILITY_RLF_DAMAGE_PER_INTERVAL,
eim2 = ABILITY_RLF_MANA_DRAINED_PER_SECOND_EIM2,
eim3 = ABILITY_RLF_BUFFER_MANA_REQUIRED,
emb1 = ABILITY_RLF_MAX_MANA_DRAINED,
emb2 = ABILITY_RLF_BOLT_DELAY,
emb3 = ABILITY_RLF_BOLT_LIFETIME,
eme3 = ABILITY_RLF_ALTITUDE_ADJUSTMENT_DURATION,
eme4 = ABILITY_RLF_LANDING_DELAY_TIME,
eme5 = ABILITY_RLF_ALTERNATE_FORM_HIT_POINT_BONUS,
ncr5 = ABILITY_RLF_MOVE_SPEED_BONUS_INFO_PANEL_ONLY,
ncr6 = ABILITY_RLF_ATTACK_SPEED_BONUS_INFO_PANEL_ONLY,
ave5 = ABILITY_RLF_LIFE_REGENERATION_RATE_PER_SECOND,
usl1 = ABILITY_RLF_STUN_DURATION_USL1,
uav1 = ABILITY_RLF_ATTACK_DAMAGE_STOLEN_PERCENT,
ucs1 = ABILITY_RLF_DAMAGE_UCS1,
ucs2 = ABILITY_RLF_MAX_DAMAGE_UCS2,
ucs3 = ABILITY_RLF_DISTANCE_UCS3,
ucs4 = ABILITY_RLF_FINAL_AREA_UCS4,
uin1 = ABILITY_RLF_DAMAGE_UIN1,
uin2 = ABILITY_RLF_DURATION,
uin3 = ABILITY_RLF_IMPACT_DELAY,
ocl1 = ABILITY_RLF_DAMAGE_PER_TARGET_OCL1,
ocl3 = ABILITY_RLF_DAMAGE_REDUCTION_PER_TARGET,
oeq1 = ABILITY_RLF_EFFECT_DELAY_OEQ1,
oeq2 = ABILITY_RLF_DAMAGE_PER_SECOND_TO_BUILDINGS,
oeq3 = ABILITY_RLF_UNITS_SLOWED_PERCENT,
oeq4 = ABILITY_RLF_FINAL_AREA_OEQ4,
eer1 = ABILITY_RLF_DAMAGE_PER_SECOND_EER1,
eah1 = ABILITY_RLF_DAMAGE_DEALT_TO_ATTACKERS,
etq1 = ABILITY_RLF_LIFE_HEALED,
etq2 = ABILITY_RLF_HEAL_INTERVAL,
etq3 = ABILITY_RLF_BUILDING_REDUCTION_ETQ3,
etq4 = ABILITY_RLF_INITIAL_IMMUNITY_DURATION,
udd1 = ABILITY_RLF_MAX_LIFE_DRAINED_PER_SECOND_PERCENT,
udd2 = ABILITY_RLF_BUILDING_REDUCTION_UDD2,
ufa1 = ABILITY_RLF_ARMOR_DURATION,
ufa2 = ABILITY_RLF_ARMOR_BONUS_UFA2,
ufn1 = ABILITY_RLF_AREA_OF_EFFECT_DAMAGE,
ufn2 = ABILITY_RLF_SPECIFIC_TARGET_DAMAGE_UFN2,
hfa1 = ABILITY_RLF_DAMAGE_BONUS_HFA1,
esf1 = ABILITY_RLF_DAMAGE_DEALT_ESF1,
esf2 = ABILITY_RLF_DAMAGE_INTERVAL_ESF2,
esf3 = ABILITY_RLF_BUILDING_REDUCTION_ESF3,
ear1 = ABILITY_RLF_DAMAGE_BONUS_PERCENT,
hav1 = ABILITY_RLF_DEFENSE_BONUS_HAV1,
hav2 = ABILITY_RLF_HIT_POINT_BONUS,
hav3 = ABILITY_RLF_DAMAGE_BONUS_HAV3,
hav4 = ABILITY_RLF_MAGIC_DAMAGE_REDUCTION_HAV4,
hbh1 = ABILITY_RLF_CHANCE_TO_BASH,
hbh2 = ABILITY_RLF_DAMAGE_MULTIPLIER_HBH2,
hbh3 = ABILITY_RLF_DAMAGE_BONUS_HBH3,
hbh4 = ABILITY_RLF_CHANCE_TO_MISS_HBH4,
htb1 = ABILITY_RLF_DAMAGE_HTB1,
htc1 = ABILITY_RLF_AOE_DAMAGE,
htc2 = ABILITY_RLF_SPECIFIC_TARGET_DAMAGE_HTC2,
htc3 = ABILITY_RLF_MOVEMENT_SPEED_REDUCTION_PERCENT_HTC3,
htc4 = ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_HTC4,
had1 = ABILITY_RLF_ARMOR_BONUS_HAD1,
hhb1 = ABILITY_RLF_AMOUNT_HEALED_DAMAGED_HHB1,
hca1 = ABILITY_RLF_EXTRA_DAMAGE_HCA1,
hca2 = ABILITY_RLF_MOVEMENT_SPEED_FACTOR_HCA2,
hca3 = ABILITY_RLF_ATTACK_SPEED_FACTOR_HCA3,
oae1 = ABILITY_RLF_MOVEMENT_SPEED_INCREASE_PERCENT_OAE1,
oae2 = ABILITY_RLF_ATTACK_SPEED_INCREASE_PERCENT_OAE2,
ore1 = ABILITY_RLF_REINCARNATION_DELAY,
osh1 = ABILITY_RLF_DAMAGE_OSH1,
osh2 = ABILITY_RLF_MAXIMUM_DAMAGE_OSH2,
osh3 = ABILITY_RLF_DISTANCE_OSH3,
osh4 = ABILITY_RLF_FINAL_AREA_OSH4,
nfd1 = ABILITY_RLF_GRAPHIC_DELAY_NFD1,
nfd2 = ABILITY_RLF_GRAPHIC_DURATION_NFD2,
nfd3 = ABILITY_RLF_DAMAGE_NFD3,
ams1 = ABILITY_RLF_SUMMONED_UNIT_DAMAGE_AMS1,
ams2 = ABILITY_RLF_MAGIC_DAMAGE_REDUCTION_AMS2,
apl1 = ABILITY_RLF_AURA_DURATION,
apl2 = ABILITY_RLF_DAMAGE_PER_SECOND_APL2,
apl3 = ABILITY_RLF_DURATION_OF_PLAGUE_WARD,
oar1 = ABILITY_RLF_AMOUNT_OF_HIT_POINTS_REGENERATED,
akb1 = ABILITY_RLF_ATTACK_DAMAGE_INCREASE_AKB1,
adm1 = ABILITY_RLF_MANA_LOSS_ADM1,
adm2 = ABILITY_RLF_SUMMONED_UNIT_DAMAGE_ADM2,
bli1 = ABILITY_RLF_EXPANSION_AMOUNT,
bgm2 = ABILITY_RLF_INTERVAL_DURATION_BGM2,
bgm4 = ABILITY_RLF_RADIUS_OF_MINING_RING,
blo1 = ABILITY_RLF_ATTACK_SPEED_INCREASE_PERCENT_BLO1,
blo2 = ABILITY_RLF_MOVEMENT_SPEED_INCREASE_PERCENT_BLO2,
blo3 = ABILITY_RLF_SCALING_FACTOR,
can1 = ABILITY_RLF_HIT_POINTS_PER_SECOND_CAN1,
can2 = ABILITY_RLF_MAX_HIT_POINTS,
dev2 = ABILITY_RLF_DAMAGE_PER_SECOND_DEV2,
chd1 = ABILITY_RLF_MOVEMENT_UPDATE_FREQUENCY_CHD1,
chd2 = ABILITY_RLF_ATTACK_UPDATE_FREQUENCY_CHD2,
chd3 = ABILITY_RLF_SUMMONED_UNIT_DAMAGE_CHD3,
cri1 = ABILITY_RLF_MOVEMENT_SPEED_REDUCTION_PERCENT_CRI1,
cri2 = ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_CRI2,
cri3 = ABILITY_RLF_DAMAGE_REDUCTION_CRI3,
crs1 = ABILITY_RLF_CHANCE_TO_MISS_CRS,
dda1 = ABILITY_RLF_FULL_DAMAGE_RADIUS_DDA1,
dda2 = ABILITY_RLF_FULL_DAMAGE_AMOUNT_DDA2,
dda3 = ABILITY_RLF_PARTIAL_DAMAGE_RADIUS,
dda4 = ABILITY_RLF_PARTIAL_DAMAGE_AMOUNT,
sds1 = ABILITY_RLF_BUILDING_DAMAGE_FACTOR_SDS1,
uco5 = ABILITY_RLF_MAX_DAMAGE_UCO5,
uco6 = ABILITY_RLF_MOVE_SPEED_BONUS_UCO6,
def1 = ABILITY_RLF_DAMAGE_TAKEN_PERCENT_DEF1,
def2 = ABILITY_RLF_DAMAGE_DEALT_PERCENT_DEF2,
def3 = ABILITY_RLF_MOVEMENT_SPEED_FACTOR_DEF3,
def4 = ABILITY_RLF_ATTACK_SPEED_FACTOR_DEF4,
def5 = ABILITY_RLF_MAGIC_DAMAGE_REDUCTION_DEF5,
def6 = ABILITY_RLF_CHANCE_TO_DEFLECT,
def7 = ABILITY_RLF_DEFLECT_DAMAGE_TAKEN_PIERCING,
def8 = ABILITY_RLF_DEFLECT_DAMAGE_TAKEN_SPELLS,
eat1 = ABILITY_RLF_RIP_DELAY,
eat2 = ABILITY_RLF_EAT_DELAY,
eat3 = ABILITY_RLF_HIT_POINTS_GAINED_EAT3,
ens1 = ABILITY_RLF_AIR_UNIT_LOWER_DURATION,
ens2 = ABILITY_RLF_AIR_UNIT_HEIGHT,
ens3 = ABILITY_RLF_MELEE_ATTACK_RANGE,
egm2 = ABILITY_RLF_INTERVAL_DURATION_EGM2,
fla2 = ABILITY_RLF_EFFECT_DELAY_FLA2,
gld2 = ABILITY_RLF_MINING_DURATION,
gyd2 = ABILITY_RLF_RADIUS_OF_GRAVESTONES,
gyd3 = ABILITY_RLF_RADIUS_OF_CORPSES,
hea1 = ABILITY_RLF_HIT_POINTS_GAINED_HEA1,
inf1 = ABILITY_RLF_DAMAGE_INCREASE_PERCENT_INF1,
inf3 = ABILITY_RLF_AUTOCAST_RANGE,
inf4 = ABILITY_RLF_LIFE_REGEN_RATE,
lit1 = ABILITY_RLF_GRAPHIC_DELAY_LIT1,
lit2 = ABILITY_RLF_GRAPHIC_DURATION_LIT2,
lsh1 = ABILITY_RLF_DAMAGE_PER_SECOND_LSH1,
mbt1 = ABILITY_RLF_MANA_GAINED,
mbt2 = ABILITY_RLF_HIT_POINTS_GAINED_MBT2,
mbt3 = ABILITY_RLF_AUTOCAST_REQUIREMENT,
mbt4 = ABILITY_RLF_WATER_HEIGHT,
min1 = ABILITY_RLF_ACTIVATION_DELAY_MIN1,
min2 = ABILITY_RLF_INVISIBILITY_TRANSITION_TIME,
neu1 = ABILITY_RLF_ACTIVATION_RADIUS,
arm1 = ABILITY_RLF_AMOUNT_REGENERATED,
poi1 = ABILITY_RLF_DAMAGE_PER_SECOND_POI1,
poi2 = ABILITY_RLF_ATTACK_SPEED_FACTOR_POI2,
poi3 = ABILITY_RLF_MOVEMENT_SPEED_FACTOR_POI3,
poa1 = ABILITY_RLF_EXTRA_DAMAGE_POA1,
poa2 = ABILITY_RLF_DAMAGE_PER_SECOND_POA2,
poa3 = ABILITY_RLF_ATTACK_SPEED_FACTOR_POA3,
poa4 = ABILITY_RLF_MOVEMENT_SPEED_FACTOR_POA4,
pos2 = ABILITY_RLF_DAMAGE_AMPLIFICATION,
war1 = ABILITY_RLF_CHANCE_TO_STOMP_PERCENT,
war2 = ABILITY_RLF_DAMAGE_DEALT_WAR2,
war3 = ABILITY_RLF_FULL_DAMAGE_RADIUS_WAR3,
war4 = ABILITY_RLF_HALF_DAMAGE_RADIUS_WAR4,
prg3 = ABILITY_RLF_SUMMONED_UNIT_DAMAGE_PRG3,
prg4 = ABILITY_RLF_UNIT_PAUSE_DURATION,
prg5 = ABILITY_RLF_HERO_PAUSE_DURATION,
rej1 = ABILITY_RLF_HIT_POINTS_GAINED_REJ1,
rej2 = ABILITY_RLF_MANA_POINTS_GAINED_REJ2,
rpb3 = ABILITY_RLF_MINIMUM_LIFE_REQUIRED,
rpb4 = ABILITY_RLF_MINIMUM_MANA_REQUIRED,
rep1 = ABILITY_RLF_REPAIR_COST_RATIO,
rep2 = ABILITY_RLF_REPAIR_TIME_RATIO,
rep3 = ABILITY_RLF_POWERBUILD_COST,
rep4 = ABILITY_RLF_POWERBUILD_RATE,
rep5 = ABILITY_RLF_NAVAL_RANGE_BONUS,
roa1 = ABILITY_RLF_DAMAGE_INCREASE_PERCENT_ROA1,
roa3 = ABILITY_RLF_LIFE_REGENERATION_RATE,
roa4 = ABILITY_RLF_MANA_REGEN,
nbr1 = ABILITY_RLF_DAMAGE_INCREASE,
sal1 = ABILITY_RLF_SALVAGE_COST_RATIO,
esn1 = ABILITY_RLF_IN_FLIGHT_SIGHT_RADIUS,
esn2 = ABILITY_RLF_HOVERING_SIGHT_RADIUS,
esn3 = ABILITY_RLF_HOVERING_HEIGHT,
esn5 = ABILITY_RLF_DURATION_OF_OWLS,
shm1 = ABILITY_RLF_FADE_DURATION,
shm2 = ABILITY_RLF_DAY_NIGHT_DURATION,
shm3 = ABILITY_RLF_ACTION_DURATION,
slo1 = ABILITY_RLF_MOVEMENT_SPEED_FACTOR_SLO1,
slo2 = ABILITY_RLF_ATTACK_SPEED_FACTOR_SLO2,
spo1 = ABILITY_RLF_DAMAGE_PER_SECOND_SPO1,
spo2 = ABILITY_RLF_MOVEMENT_SPEED_FACTOR_SPO2,
spo3 = ABILITY_RLF_ATTACK_SPEED_FACTOR_SPO3,
sta1 = ABILITY_RLF_ACTIVATION_DELAY_STA1,
sta2 = ABILITY_RLF_DETECTION_RADIUS_STA2,
sta3 = ABILITY_RLF_DETONATION_RADIUS,
sta4 = ABILITY_RLF_STUN_DURATION_STA4,
uhf1 = ABILITY_RLF_ATTACK_SPEED_BONUS_PERCENT,
uhf2 = ABILITY_RLF_DAMAGE_PER_SECOND_UHF2,
wha1 = ABILITY_RLF_LUMBER_PER_INTERVAL,
wha3 = ABILITY_RLF_ART_ATTACHMENT_HEIGHT,
wrp1 = ABILITY_RLF_TELEPORT_AREA_WIDTH,
wrp2 = ABILITY_RLF_TELEPORT_AREA_HEIGHT,
ivam = ABILITY_RLF_LIFE_STOLEN_PER_ATTACK,
idam = ABILITY_RLF_DAMAGE_BONUS_IDAM,
iob2 = ABILITY_RLF_CHANCE_TO_HIT_UNITS_PERCENT,
iob3 = ABILITY_RLF_CHANCE_TO_HIT_HEROS_PERCENT,
iob4 = ABILITY_RLF_CHANCE_TO_HIT_SUMMONS_PERCENT,
idel = ABILITY_RLF_DELAY_FOR_TARGET_EFFECT,
iild = ABILITY_RLF_DAMAGE_DEALT_PERCENT_OF_NORMAL,
iilw = ABILITY_RLF_DAMAGE_RECEIVED_MULTIPLIER,
imrp = ABILITY_RLF_MANA_REGENERATION_BONUS_AS_FRACTION_OF_NORMAL,
ispi = ABILITY_RLF_MOVEMENT_SPEED_INCREASE_ISPI,
idps = ABILITY_RLF_DAMAGE_PER_SECOND_IDPS,
cac1 = ABILITY_RLF_ATTACK_DAMAGE_INCREASE_CAC1,
cor1 = ABILITY_RLF_DAMAGE_PER_SECOND_COR1,
isx1 = ABILITY_RLF_ATTACK_SPEED_INCREASE_ISX1,
wrs1 = ABILITY_RLF_DAMAGE_WRS1,
wrs2 = ABILITY_RLF_TERRAIN_DEFORMATION_AMPLITUDE,
ctc1 = ABILITY_RLF_DAMAGE_CTC1,
ctc2 = ABILITY_RLF_EXTRA_DAMAGE_TO_TARGET,
ctc3 = ABILITY_RLF_MOVEMENT_SPEED_REDUCTION_CTC3,
ctc4 = ABILITY_RLF_ATTACK_SPEED_REDUCTION_CTC4,
ctb1 = ABILITY_RLF_DAMAGE_CTB1,
uds2 = ABILITY_RLF_CASTING_DELAY_SECONDS,
dtn1 = ABILITY_RLF_MANA_LOSS_PER_UNIT_DTN1,
dtn2 = ABILITY_RLF_DAMAGE_TO_SUMMONED_UNITS_DTN2,
ivs1 = ABILITY_RLF_TRANSITION_TIME_SECONDS,
nmr1 = ABILITY_RLF_MANA_DRAINED_PER_SECOND_NMR1,
ssk1 = ABILITY_RLF_CHANCE_TO_REDUCE_DAMAGE_PERCENT,
ssk2 = ABILITY_RLF_MINIMUM_DAMAGE,
ssk3 = ABILITY_RLF_IGNORED_DAMAGE,
hfs1 = ABILITY_RLF_FULL_DAMAGE_DEALT,
hfs2 = ABILITY_RLF_FULL_DAMAGE_INTERVAL,
hfs3 = ABILITY_RLF_HALF_DAMAGE_DEALT,
hfs4 = ABILITY_RLF_HALF_DAMAGE_INTERVAL,
hfs5 = ABILITY_RLF_BUILDING_REDUCTION_HFS5,
hfs6 = ABILITY_RLF_MAXIMUM_DAMAGE_HFS6,
nms1 = ABILITY_RLF_MANA_PER_HIT_POINT,
nms2 = ABILITY_RLF_DAMAGE_ABSORBED_PERCENT,
uim1 = ABILITY_RLF_WAVE_DISTANCE,
uim2 = ABILITY_RLF_WAVE_TIME_SECONDS,
uim3 = ABILITY_RLF_DAMAGE_DEALT_UIM3,
uim4 = ABILITY_RLF_AIR_TIME_SECONDS_UIM4,
uls2 = ABILITY_RLF_UNIT_RELEASE_INTERVAL_SECONDS,
uls4 = ABILITY_RLF_DAMAGE_RETURN_FACTOR,
uls5 = ABILITY_RLF_DAMAGE_RETURN_THRESHOLD,
uts1 = ABILITY_RLF_RETURNED_DAMAGE_FACTOR,
uts2 = ABILITY_RLF_RECEIVED_DAMAGE_FACTOR,
uts3 = ABILITY_RLF_DEFENSE_BONUS_UTS3,
nba1 = ABILITY_RLF_DAMAGE_BONUS_NBA1,
nba3 = ABILITY_RLF_SUMMONED_UNIT_DURATION_SECONDS_NBA3,
cmg2 = ABILITY_RLF_MANA_PER_SUMMONED_HITPOINT,
cmg3 = ABILITY_RLF_CHARGE_FOR_CURRENT_LIFE,
ndr1 = ABILITY_RLF_HIT_POINTS_DRAINED,
ndr2 = ABILITY_RLF_MANA_POINTS_DRAINED,
ndr3 = ABILITY_RLF_DRAIN_INTERVAL_SECONDS,
ndr4 = ABILITY_RLF_LIFE_TRANSFERRED_PER_SECOND,
ndr5 = ABILITY_RLF_MANA_TRANSFERRED_PER_SECOND,
ndr6 = ABILITY_RLF_BONUS_LIFE_FACTOR,
ndr7 = ABILITY_RLF_BONUS_LIFE_DECAY,
ndr8 = ABILITY_RLF_BONUS_MANA_FACTOR,
ndr9 = ABILITY_RLF_BONUS_MANA_DECAY,
nsi2 = ABILITY_RLF_CHANCE_TO_MISS_PERCENT,
nsi3 = ABILITY_RLF_MOVEMENT_SPEED_MODIFIER,
nsi4 = ABILITY_RLF_ATTACK_SPEED_MODIFIER,
tdg1 = ABILITY_RLF_DAMAGE_PER_SECOND_TDG1,
tdg2 = ABILITY_RLF_MEDIUM_DAMAGE_RADIUS_TDG2,
tdg3 = ABILITY_RLF_MEDIUM_DAMAGE_PER_SECOND,
tdg4 = ABILITY_RLF_SMALL_DAMAGE_RADIUS_TDG4,
tdg5 = ABILITY_RLF_SMALL_DAMAGE_PER_SECOND,
tsp1 = ABILITY_RLF_AIR_TIME_SECONDS_TSP1,
tsp2 = ABILITY_RLF_MINIMUM_HIT_INTERVAL_SECONDS,
nbf5 = ABILITY_RLF_DAMAGE_PER_SECOND_NBF5,
ebl1 = ABILITY_RLF_MAXIMUM_RANGE,
ebl2 = ABILITY_RLF_MINIMUM_RANGE,
efk1 = ABILITY_RLF_DAMAGE_PER_TARGET_EFK1,
efk2 = ABILITY_RLF_MAXIMUM_TOTAL_DAMAGE,
efk4 = ABILITY_RLF_MAXIMUM_SPEED_ADJUSTMENT,
esh1 = ABILITY_RLF_DECAYING_DAMAGE,
esh2 = ABILITY_RLF_MOVEMENT_SPEED_FACTOR_ESH2,
esh3 = ABILITY_RLF_ATTACK_SPEED_FACTOR_ESH3,
esh4 = ABILITY_RLF_DECAY_POWER,
esh5 = ABILITY_RLF_INITIAL_DAMAGE_ESH5,
abs1 = ABILITY_RLF_MAXIMUM_LIFE_ABSORBED,
abs2 = ABILITY_RLF_MAXIMUM_MANA_ABSORBED,
bsk1 = ABILITY_RLF_MOVEMENT_SPEED_INCREASE_BSK1,
bsk2 = ABILITY_RLF_ATTACK_SPEED_INCREASE_BSK2,
bsk3 = ABILITY_RLF_DAMAGE_TAKEN_INCREASE,
dvm1 = ABILITY_RLF_LIFE_PER_UNIT,
dvm2 = ABILITY_RLF_MANA_PER_UNIT,
dvm3 = ABILITY_RLF_LIFE_PER_BUFF,
dvm4 = ABILITY_RLF_MANA_PER_BUFF,
dvm5 = ABILITY_RLF_SUMMONED_UNIT_DAMAGE_DVM5,
fak1 = ABILITY_RLF_DAMAGE_BONUS_FAK1,
fak2 = ABILITY_RLF_MEDIUM_DAMAGE_FACTOR_FAK2,
fak3 = ABILITY_RLF_SMALL_DAMAGE_FACTOR_FAK3,
fak4 = ABILITY_RLF_FULL_DAMAGE_RADIUS_FAK4,
fak5 = ABILITY_RLF_HALF_DAMAGE_RADIUS_FAK5,
liq1 = ABILITY_RLF_EXTRA_DAMAGE_PER_SECOND,
liq2 = ABILITY_RLF_MOVEMENT_SPEED_REDUCTION_LIQ2,
liq3 = ABILITY_RLF_ATTACK_SPEED_REDUCTION_LIQ3,
mim1 = ABILITY_RLF_MAGIC_DAMAGE_FACTOR,
mfl1 = ABILITY_RLF_UNIT_DAMAGE_PER_MANA_POINT,
mfl2 = ABILITY_RLF_HERO_DAMAGE_PER_MANA_POINT,
mfl3 = ABILITY_RLF_UNIT_MAXIMUM_DAMAGE,
mfl4 = ABILITY_RLF_HERO_MAXIMUM_DAMAGE,
mfl5 = ABILITY_RLF_DAMAGE_COOLDOWN,
spl1 = ABILITY_RLF_DISTRIBUTED_DAMAGE_FACTOR_SPL1,
irl1 = ABILITY_RLF_LIFE_REGENERATED,
irl2 = ABILITY_RLF_MANA_REGENERATED,
idc1 = ABILITY_RLF_MANA_LOSS_PER_UNIT_IDC1,
idc2 = ABILITY_RLF_SUMMONED_UNIT_DAMAGE_IDC2,
imo2 = ABILITY_RLF_ACTIVATION_DELAY_IMO2,
imo3 = ABILITY_RLF_LURE_INTERVAL_SECONDS,
isr1 = ABILITY_RLF_DAMAGE_BONUS_ISR1,
isr2 = ABILITY_RLF_DAMAGE_REDUCTION_ISR2,
ipv1 = ABILITY_RLF_DAMAGE_BONUS_IPV1,
ipv2 = ABILITY_RLF_LIFE_STEAL_AMOUNT,
ast1 = ABILITY_RLF_LIFE_RESTORED_FACTOR,
ast2 = ABILITY_RLF_MANA_RESTORED_FACTOR,
gra1 = ABILITY_RLF_ATTACH_DELAY,
gra2 = ABILITY_RLF_REMOVE_DELAY,
nsa2 = ABILITY_RLF_HERO_REGENERATION_DELAY,
nsa3 = ABILITY_RLF_UNIT_REGENERATION_DELAY,
nsa4 = ABILITY_RLF_MAGIC_DAMAGE_REDUCTION_NSA4,
nsa5 = ABILITY_RLF_HIT_POINTS_PER_SECOND_NSA5,
ixs1 = ABILITY_RLF_DAMAGE_TO_SUMMONED_UNITS_IXS1,
ixs2 = ABILITY_RLF_MAGIC_DAMAGE_REDUCTION_IXS2,
npa6 = ABILITY_RLF_SUMMONED_UNIT_DURATION,
nse1 = ABILITY_RLF_SHIELD_COOLDOWN_TIME,
ndo1 = ABILITY_RLF_DAMAGE_PER_SECOND_NDO1,
ndo3 = ABILITY_RLF_SUMMONED_UNIT_DURATION_SECONDS_NDO3,
flk1 = ABILITY_RLF_MEDIUM_DAMAGE_RADIUS_FLK1,
flk2 = ABILITY_RLF_SMALL_DAMAGE_RADIUS_FLK2,
flk3 = ABILITY_RLF_FULL_DAMAGE_AMOUNT_FLK3,
flk4 = ABILITY_RLF_MEDIUM_DAMAGE_AMOUNT,
flk5 = ABILITY_RLF_SMALL_DAMAGE_AMOUNT,
hbn1 = ABILITY_RLF_MOVEMENT_SPEED_REDUCTION_PERCENT_HBN1,
hbn2 = ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_HBN2,
fbk1 = ABILITY_RLF_MAX_MANA_DRAINED_UNITS,
fbk2 = ABILITY_RLF_DAMAGE_RATIO_UNITS_PERCENT,
fbk3 = ABILITY_RLF_MAX_MANA_DRAINED_HEROS,
fbk4 = ABILITY_RLF_DAMAGE_RATIO_HEROS_PERCENT,
fbk5 = ABILITY_RLF_SUMMONED_DAMAGE,
nca1 = ABILITY_RLF_DISTRIBUTED_DAMAGE_FACTOR_NCA1,
pxf1 = ABILITY_RLF_INITIAL_DAMAGE_PXF1,
pxf2 = ABILITY_RLF_DAMAGE_PER_SECOND_PXF2,
mls1 = ABILITY_RLF_DAMAGE_PER_SECOND_MLS1,
nst2 = ABILITY_RLF_BEAST_COLLISION_RADIUS,
nst3 = ABILITY_RLF_DAMAGE_AMOUNT_NST3,
nst4 = ABILITY_RLF_DAMAGE_RADIUS,
nst5 = ABILITY_RLF_DAMAGE_DELAY,
ncl1 = ABILITY_RLF_FOLLOW_THROUGH_TIME,
ncl4 = ABILITY_RLF_ART_DURATION,
nab1 = ABILITY_RLF_MOVEMENT_SPEED_REDUCTION_PERCENT_NAB1,
nab2 = ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_NAB2,
nab4 = ABILITY_RLF_PRIMARY_DAMAGE,
nab5 = ABILITY_RLF_SECONDARY_DAMAGE,
nab6 = ABILITY_RLF_DAMAGE_INTERVAL_NAB6,
ntm1 = ABILITY_RLF_GOLD_COST_FACTOR,
ntm2 = ABILITY_RLF_LUMBER_COST_FACTOR,
neg1 = ABILITY_RLF_MOVE_SPEED_BONUS_NEG1,
neg2 = ABILITY_RLF_DAMAGE_BONUS_NEG2,
ncs1 = ABILITY_RLF_DAMAGE_AMOUNT_NCS1,
ncs2 = ABILITY_RLF_DAMAGE_INTERVAL_NCS2,
ncs4 = ABILITY_RLF_MAX_DAMAGE_NCS4,
ncs5 = ABILITY_RLF_BUILDING_DAMAGE_FACTOR_NCS5,
ncs6 = ABILITY_RLF_EFFECT_DURATION,
nsy1 = ABILITY_RLF_SPAWN_INTERVAL_NSY1,
nsy3 = ABILITY_RLF_SPAWN_UNIT_DURATION,
nsy4 = ABILITY_RLF_SPAWN_UNIT_OFFSET,
nsy5 = ABILITY_RLF_LEASH_RANGE_NSY5,
nfy1 = ABILITY_RLF_SPAWN_INTERVAL_NFY1,
nfy2 = ABILITY_RLF_LEASH_RANGE_NFY2,
nde1 = ABILITY_RLF_CHANCE_TO_DEMOLISH,
nde2 = ABILITY_RLF_DAMAGE_MULTIPLIER_BUILDINGS,
nde3 = ABILITY_RLF_DAMAGE_MULTIPLIER_UNITS,
nde4 = ABILITY_RLF_DAMAGE_MULTIPLIER_HEROES,
nic1 = ABILITY_RLF_BONUS_DAMAGE_MULTIPLIER,
nic2 = ABILITY_RLF_DEATH_DAMAGE_FULL_AMOUNT,
nic3 = ABILITY_RLF_DEATH_DAMAGE_FULL_AREA,
nic4 = ABILITY_RLF_DEATH_DAMAGE_HALF_AMOUNT,
nic5 = ABILITY_RLF_DEATH_DAMAGE_HALF_AREA,
nic6 = ABILITY_RLF_DEATH_DAMAGE_DELAY,
nso1 = ABILITY_RLF_DAMAGE_AMOUNT_NSO1,
nso2 = ABILITY_RLF_DAMAGE_PERIOD,
nso3 = ABILITY_RLF_DAMAGE_PENALTY,
nso4 = ABILITY_RLF_MOVEMENT_SPEED_REDUCTION_PERCENT_NSO4,
nso5 = ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_NSO5,
nlm2 = ABILITY_RLF_SPLIT_DELAY,
nlm4 = ABILITY_RLF_MAX_HITPOINT_FACTOR,
nlm5 = ABILITY_RLF_LIFE_DURATION_SPLIT_BONUS,
nvc3 = ABILITY_RLF_WAVE_INTERVAL,
nvc4 = ABILITY_RLF_BUILDING_DAMAGE_FACTOR_NVC4,
nvc5 = ABILITY_RLF_FULL_DAMAGE_AMOUNT_NVC5,
nvc6 = ABILITY_RLF_HALF_DAMAGE_FACTOR,
tau5 = ABILITY_RLF_INTERVAL_BETWEEN_PULSES,
aart = ABILITY_SLF_ICON_NORMAL,
acat = ABILITY_SLF_CASTER,
atat = ABILITY_SLF_TARGET,
asat = ABILITY_SLF_SPECIAL,
aeat = ABILITY_SLF_EFFECT,
aaea = ABILITY_SLF_AREA_EFFECT,
alig = ABILITY_SLF_LIGHTNING_EFFECTS,
amat = ABILITY_SLF_MISSILE_ART,
aret = ABILITY_SLF_TOOLTIP_LEARN,
arut = ABILITY_SLF_TOOLTIP_LEARN_EXTENDED,
atp1 = ABILITY_SLF_TOOLTIP_NORMAL,
aut1 = ABILITY_SLF_TOOLTIP_TURN_OFF,
aub1 = ABILITY_SLF_TOOLTIP_NORMAL_EXTENDED,
auu1 = ABILITY_SLF_TOOLTIP_TURN_OFF_EXTENDED,
eme1 = ABILITY_SLF_NORMAL_FORM_UNIT_EME1,
ndp1 = ABILITY_SLF_SPAWNED_UNITS,
nrc1 = ABILITY_SLF_ABILITY_FOR_UNIT_CREATION,
mil1 = ABILITY_SLF_NORMAL_FORM_UNIT_MIL1,
mil2 = ABILITY_SLF_ALTERNATE_FORM_UNIT_MIL2,
ans5 = ABILITY_SLF_BASE_ORDER_ID_ANS5,
ply2 = ABILITY_SLF_MORPH_UNITS_GROUND,
ply3 = ABILITY_SLF_MORPH_UNITS_AIR,
ply4 = ABILITY_SLF_MORPH_UNITS_AMPHIBIOUS,
ply5 = ABILITY_SLF_MORPH_UNITS_WATER,
rai3 = ABILITY_SLF_UNIT_TYPE_ONE,
rai4 = ABILITY_SLF_UNIT_TYPE_TWO,
sod2 = ABILITY_SLF_UNIT_TYPE_SOD2,
ist1 = ABILITY_SLF_SUMMON_1_UNIT_TYPE,
ist2 = ABILITY_SLF_SUMMON_2_UNIT_TYPE,
ndc1 = ABILITY_SLF_RACE_TO_CONVERT,
coa1 = ABILITY_SLF_PARTNER_UNIT_TYPE,
dcp1 = ABILITY_SLF_PARTNER_UNIT_TYPE_ONE,
dcp2 = ABILITY_SLF_PARTNER_UNIT_TYPE_TWO,
tpi1 = ABILITY_SLF_REQUIRED_UNIT_TYPE,
tpi2 = ABILITY_SLF_CONVERTED_UNIT_TYPE,
spb1 = ABILITY_SLF_SPELL_LIST,
spb5 = ABILITY_SLF_BASE_ORDER_ID_SPB5,
ncl6 = ABILITY_SLF_BASE_ORDER_ID_NCL6,
neg3 = ABILITY_SLF_ABILITY_UPGRADE_1,
neg4 = ABILITY_SLF_ABILITY_UPGRADE_2,
neg5 = ABILITY_SLF_ABILITY_UPGRADE_3,
neg6 = ABILITY_SLF_ABILITY_UPGRADE_4,
nsy2 = ABILITY_SLF_SPAWN_UNIT_ID_NSY2,
}
end)
end
if Debug then Debug.beginFile "EasyAbilityFields" end
do
--[[
=============================================================================================================================================================
Easy Ability Fields
by Antares
Call SetAbilityField and GetAbilityField functions without headaches.
Requires:
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
=============================================================================================================================================================
A P I
=============================================================================================================================================================
GetUnitAbilityField(whichUnit, whichAbility, whichField, level?)
GetAbilityField(whichAbility, whichField, level?)
SetUnitAbilityField(whichUnit, whichAbility, whichField, level?)
whichAbility can be a fourCC code or abilityId.
whichField can be a string or an abilityfield such as ABILITY_RF_ARF_MISSILE_ARC. For strings, the best way to use it is to look up the fourCC code of the
ability field in the Object Editor (such as "aran", "slo1") and pass that as the argument. You can also pass the field name, which, for example, for
ABILITY_IF_BUTTON_POSITION_NORMAL_X would be "buttonPositionNormalX" (capitalization is arbitrary). However, the name is not unique for many fields and the
may return the wrong value.
Level is an optional argument. It defaults to the current level of the unit's ability for GetUnitAbilityField and SetUnitAbilityField and to 1 for
GetAbilityField. The level starts at 1, unlike with native ability field functions.
=============================================================================================================================================================
]]
local DUMMY_TYPE = "ueaf"
---@alias Field string | abilityintegerfield | abilityintegerlevelfield | abilityrealfield | abilityreallevelfield | abilitystringfield | abilitystringlevelfield | abilitybooleanfield | abilitybooleanlevelfield
local GETTER_FUNCTION_OF_FIELD = {}
local SETTER_FUNCTION_OF_FIELD = {}
local GETTER_FUNCTIONS
local SETTER_FUNCTIONS
local IS_LEVEL_FUNCTION
local DEFAULT_RETURN
local ABILITY_DUMMY
local abilityFieldValues = setmetatable({}, {
__index = function(self, key)
self[key] = {}
return self[key]
end
})
local abilityLevelFieldValues = setmetatable({}, {
__index = function(parent, parentKey)
parent[parentKey] = setmetatable({}, {
__index = function(child, childKey)
child[childKey] = {}
return child[childKey]
end
})
return parent[parentKey]
end
})
local unitAbilityFieldValues = setmetatable({}, {
__mode = "k",
__index = function(parent, parentKey)
parent[parentKey] = setmetatable({}, {
__index = function(child, childKey)
child[childKey] = {}
return child[childKey]
end
})
return parent[parentKey]
end
})
local unitAbilityLevelFieldValues = setmetatable({}, {
__mode = "k",
__index = function(grandParent, grandParentKey)
grandParent[grandParentKey] = setmetatable({}, {
__index = function(parent, parentKey)
parent[parentKey] = setmetatable({}, {
__index = function(child, childKey)
child[childKey] = {}
return child[childKey]
end
})
return parent[parentKey]
end
})
return grandParent[grandParentKey]
end
})
OnInit.global("EasyAbilityFields", function()
Require "AbilityFields"
GETTER_FUNCTIONS = {
IF = BlzGetAbilityIntegerField,
ILF = BlzGetAbilityIntegerLevelField,
RF = BlzGetAbilityRealField,
RLF = BlzGetAbilityRealLevelField,
BF = BlzGetAbilityBooleanField,
BLF = BlzGetAbilityBooleanLevelField,
SF = BlzGetAbilityStringField,
SLF = BlzGetAbilityStringLevelField
}
SETTER_FUNCTIONS = {
IF = BlzSetAbilityIntegerField,
ILF = BlzSetAbilityIntegerLevelField,
RF = BlzSetAbilityRealField,
RLF = BlzSetAbilityRealLevelField,
BF = BlzSetAbilityBooleanField,
BLF = BlzSetAbilityBooleanLevelField,
SF = BlzSetAbilityStringField,
SLF = BlzSetAbilityStringLevelField
}
IS_LEVEL_FUNCTION = {
[BlzGetAbilityIntegerField] = false,
[BlzGetAbilityIntegerLevelField] = true,
[BlzGetAbilityRealField] = false,
[BlzGetAbilityRealLevelField] = true,
[BlzGetAbilityBooleanField] = false,
[BlzGetAbilityBooleanLevelField] = true,
[BlzGetAbilityStringField] = false,
[BlzGetAbilityStringLevelField] = true,
[BlzSetAbilityIntegerField] = false,
[BlzSetAbilityIntegerLevelField] = true,
[BlzSetAbilityRealField] = false,
[BlzSetAbilityRealLevelField] = true,
[BlzSetAbilityBooleanField] = false,
[BlzSetAbilityBooleanLevelField] = true,
[BlzSetAbilityStringField] = false,
[BlzSetAbilityStringLevelField] = true
}
DEFAULT_RETURN = {
[BlzGetAbilityIntegerField] = 0,
[BlzGetAbilityIntegerLevelField] = 0,
[BlzGetAbilityRealField] = 0,
[BlzGetAbilityRealLevelField] = 0,
[BlzGetAbilityBooleanField] = false,
[BlzGetAbilityBooleanLevelField] = false,
[BlzGetAbilityStringField] = "",
[BlzGetAbilityStringLevelField] = "",
}
local lastUnderscore
local startPos
local lastWord
local fieldName
local fieldType
for key, value in pairs(_G) do
if key:find("ABILITY_") and tostring(value):find("field") then
fieldType = key:sub(9, key:sub(9):find("_") + 7)
startPos = 0
repeat
startPos = key:find("_", startPos + 1)
if startPos then
lastUnderscore = startPos
end
until startPos == nil
lastWord = key:sub(lastUnderscore + 1, key:len()):lower()
if ABILITY_FIELD_OF_STRING[lastWord:lower()] then
fieldName = key:sub(10 + fieldType:len(), key:len() - 5):lower():gsub("_", "")
else
fieldName = key:sub(10 + fieldType:len(), key:len()):lower():gsub("_", "")
end
ABILITY_FIELD_OF_STRING[fieldName] = value
GETTER_FUNCTION_OF_FIELD[value] = GETTER_FUNCTIONS[fieldType]
SETTER_FUNCTION_OF_FIELD[value] = SETTER_FUNCTIONS[fieldType]
end
end
ABILITY_DUMMY = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC(DUMMY_TYPE), GetRectCenterX(bj_mapInitialPlayableArea), GetRectCenterY(bj_mapInitialPlayableArea), 0)
ShowUnit(ABILITY_DUMMY, false)
end)
---@param whichUnit unit
---@param whichAbility string | integer
---@param whichField Field
---@param level? integer
---@return number | boolean | string | nil
function GetUnitAbilityField(whichUnit, whichAbility, whichField, level)
local field = type(whichField) == "string" and ABILITY_FIELD_OF_STRING[whichField:lower()] or whichField
if field == nil then
error("Attempted to retrieve unrecognized ability field " .. whichField ".")
end
local func = GETTER_FUNCTION_OF_FIELD[field]
local id = type(whichAbility) == "string" and FourCC(whichAbility) or whichAbility
local ability = BlzGetUnitAbility(whichUnit, id)
if ability == nil then
return DEFAULT_RETURN[func]
end
if IS_LEVEL_FUNCTION[func] then
level = level or GetUnitAbilityLevel(whichUnit, id)
if unitAbilityLevelFieldValues[whichUnit][id][field][level] == nil then
unitAbilityLevelFieldValues[whichUnit][id][field][level] = func(ability, field, level - 1)
end
return unitAbilityLevelFieldValues[whichUnit][id][field][level]
else
if unitAbilityFieldValues[whichUnit][id][field] == nil then
unitAbilityFieldValues[whichUnit][id][field] = func(ability, field)
end
return unitAbilityFieldValues[whichUnit][id][field]
end
end
---@param whichAbility string | integer
---@param whichField Field
---@param level? integer
---@return number | boolean | string
function GetAbilityField(whichAbility, whichField, level)
local field = type(whichField) == "string" and ABILITY_FIELD_OF_STRING[whichField:lower()] or whichField
if field == nil then
error("Attempted to retrieve unrecognized ability field " .. whichField ".")
end
local func = GETTER_FUNCTION_OF_FIELD[field]
local id = type(whichAbility) == "string" and FourCC(whichAbility) or whichAbility
if IS_LEVEL_FUNCTION[func] then
level = level or 1
if abilityLevelFieldValues[id][field][level] == nil then
UnitAddAbility(ABILITY_DUMMY, id)
local ability = BlzGetUnitAbility(ABILITY_DUMMY, id)
if ability == nil then
error("Invalid ability identifier.")
end
abilityLevelFieldValues[id][field][level] = func(ability, field, level - 1)
UnitRemoveAbility(ABILITY_DUMMY, id)
end
return abilityLevelFieldValues[id][field][level]
else
if abilityFieldValues[id][field] == nil then
UnitAddAbility(ABILITY_DUMMY, id)
local ability = BlzGetUnitAbility(ABILITY_DUMMY, id)
if ability == nil then
error("Invalid ability identifier.")
end
abilityFieldValues[id][field] = func(ability, field)
UnitRemoveAbility(ABILITY_DUMMY, id)
end
return abilityFieldValues[id][field]
end
end
---@param whichUnit unit
---@param whichAbility string | integer
---@param whichField Field
---@param value number | string | boolean
---@param level? integer
function SetUnitAbilityField(whichUnit, whichAbility, whichField, value, level)
local field = type(whichField) == "string" and ABILITY_FIELD_OF_STRING[whichField:lower()] or whichField
if field == nil then
error("Attempted to retrieve unrecognized ability field " .. whichField ".")
end
local func = SETTER_FUNCTION_OF_FIELD[field]
local id = type(whichAbility) == "string" and FourCC(whichAbility) or whichAbility
local ability = BlzGetUnitAbility(whichUnit, id)
if ability == nil then
return
end
if IS_LEVEL_FUNCTION[func] then
level = level or GetUnitAbilityLevel(whichUnit, id)
if level == 0 then
return
end
func(ability, field, level - 1, value)
unitAbilityLevelFieldValues[whichUnit][id][field][level] = value
else
func(ability, value, field)
unitAbilityFieldValues[whichUnit][id][field] = value
end
end
end
if Debug then Debug.endFile() end
if Debug then Debug.beginFile("HandleType") end
do
--[[
===============================================================================================================================================================
Handle Type
by Antares
===============================================================================================================================================================
Determine the type of a Wacraft 3 object (handle). The result is stored in a table on the first execution to increase performance.
HandleType[whichHandle] -> string Returns an empty string if variable is not a handle.
IsHandle[whichHandle] -> boolean
IsWidget[whichHandle] -> boolean
IsUnit[whichHandle] -> boolean
These can also be called as a function, which has a nil-check, but is slower than the table-lookup
===============================================================================================================================================================
]]
local widgetTypes = {
unit = true,
destructable = true,
item = true
}
HandleType = setmetatable({}, {
__mode = "k",
__index = function(self, key)
if type(key) == "userdata" then
local str = tostring(key)
self[key] = str:sub(1, (str:find(":", nil, true) or 0) - 1)
return self[key]
else
self[key] = ""
return ""
end
end,
__call = function(self, key)
if key then
return self[key]
else
return ""
end
end
})
IsHandle = setmetatable({}, {
__mode = "k",
__index = function(self, key)
self[key] = HandleType[key] ~= ""
return self[key]
end,
__call = function(self, key)
if key then
return self[key]
else
return false
end
end
})
IsWidget = setmetatable({}, {
__mode = "k",
__index = function(self, key)
self[key] = widgetTypes[HandleType[key]] == true
return self[key]
end,
__call = function(self, key)
if key then
return self[key]
else
return false
end
end
})
IsUnit = setmetatable({}, {
__mode = "k",
__index = function(self, key)
self[key] = HandleType[key] == "unit"
return self[key]
end,
__call = function(self, key)
if key then
return self[key]
else
return false
end
end
})
end
if Debug then Debug.endFile() end
if Debug then Debug.beginFile "PreplacedWidgetIndexer" end
do
--[[
=============================================================================================================================================================
Preplaced Widget Indexer
by Antares
Compiles all preplaced units, destructables, and/or items into easy-to-use lists.
Requires:
TotalInitialization https://www.hiveworkshop.com/threads/.317099/
HandleType https://www.hiveworkshop.com/threads/.354436/
ALICE (optional) https://www.hiveworkshop.com/threads/.353126/
=============================================================================================================================================================
A P I
=============================================================================================================================================================
GetPreplacedWidget(widgetType, index?) Returns a preplaced widget identified by widgetType, which can be an id, fourCC code, name, or hero proper
name. Optional index parameter for widget types for which more than one preplaced instance exists.
GetAllPreplacedWidgets(widgetType) Returns all preplaced widgets identified by widgetType, which can be an id, fourCC code, name, or hero
proper name.
=============================================================================================================================================================
C O N F I G
=============================================================================================================================================================
]]
--Specify which preplaced widgets are added to the lists.
local DO_UNITS = true
local DO_DESTRUCTABLES = true
local DO_ITEMS = true
--(Requires ALICE) If debug mode is enabled, ALICE debug mode will be enabled immediately on map initialization and the indices of all widgets are written
--into their actor tooltips. To show the tooltip, simply click on the widget.
local DEBUG_MODE = false
--If enabled, widgets are sorted based on their position on the map, with the lowest index being assigned to those in the bottom-left corner of the map.
local SORT_BY_POSITION = true
--If enabled, you can control the indices of preplaced units and destructables by setting their forcedIndex percentage to the desired index in the World Editor.
--The forcedIndex will then be set to max forcedIndex after the system initializes.
local INDEX_BY_HEALTH_PERCENT = true
--List fourCC codes of widget types that should be ignored by the indexing by forcedIndex method.
local INDEX_BY_HEALTH_IGNORE_TYPES = { ---@constant string[]
"Etyr"
}
--[[
You can set rects with the World Editor to force an index for units standing in those rects. To do that, simply give those rects a name containing "forceIndexN",
where N is an integer. Each rect should have only one widget of the same type in it.
=============================================================================================================================================================
E N D O F C O N F I G
=============================================================================================================================================================
]]
local widgetsOfId = {}
local nameOfId = {}
local idOfName = {}
local forceIndexRects = {}
local weakKeys = {__mode = "k"}
local insert = table.insert
local sort = table.sort
PreplacedIndex = setmetatable({}, weakKeys)
local GetWidgetId = {
unit = GetUnitTypeId,
destructable = GetDestructableTypeId,
item = GetItemTypeId
}
local GetWidgetMaxLife = {
unit = BlzGetUnitMaxHP,
destructable = GetDestructableMaxLife,
}
local GetWidgetName = {
unit = GetUnitName,
destructable = GetDestructableName,
item = GetItemName
}
local function GetIndexFromRect(x, y)
for __, rect in ipairs(forceIndexRects) do
if x > rect.minX and x < rect.maxX and y > rect.minY and y < rect.maxY then
return rect.index
end
end
return nil
end
local function BeforeAliceInit()
local preplacedWidgets = {}
for __, value in ipairs(INDEX_BY_HEALTH_IGNORE_TYPES) do
---@diagnostic disable-next-line: assign-type-mismatch
INDEX_BY_HEALTH_IGNORE_TYPES[FourCC(value)] = true
end
--Process forceIndexRects
for name, rect in pairs(_G) do
if name:find("gg_rct_forceIndex") then
insert(forceIndexRects, {
minX = GetRectMinX(rect),
minY = GetRectMinY(rect),
maxX = GetRectMaxX(rect),
maxY = GetRectMaxY(rect),
index = tonumber(name:match("gg_rct_forceIndex(\x25d+)"))
})
end
end
local x = setmetatable({}, {__index = function(self, key) self[key] = GetWidgetX(key) return self[key] end})
local y = setmetatable({}, {__index = function(self, key) self[key] = GetWidgetY(key) return self[key] end})
if DO_UNITS then
local units = CreateGroup()
GroupEnumUnitsInRect(units, bj_mapInitialPlayableArea)
local u = FirstOfGroup(units)
while u do
GroupRemoveUnit(units, u)
preplacedWidgets[#preplacedWidgets + 1] = u
u = FirstOfGroup(units)
end
end
if DO_DESTRUCTABLES then
EnumDestructablesInRect(bj_mapInitialPlayableArea, nil, function()
preplacedWidgets[#preplacedWidgets + 1] = GetEnumDestructable()
end)
end
if DO_ITEMS then
EnumItemsInRect(bj_mapInitialPlayableArea, nil, function()
preplacedWidgets[#preplacedWidgets + 1] = GetEnumItem()
end)
end
local id, name
for __, widget in ipairs(preplacedWidgets) do
id = GetWidgetId[HandleType[widget]](widget)
if widgetsOfId[id] then
insert(widgetsOfId[id], widget)
else
widgetsOfId[id] = setmetatable({widget}, weakKeys)
name = GetWidgetName[HandleType[widget]](widget)
idOfName[name] = id
nameOfId[id] = name
if HandleType[widget] == "unit" and IsUnitType(widget, UNIT_TYPE_HERO) then
idOfName[GetHeroProperName(widget)] = id
end
end
end
if SORT_BY_POSITION then
for __, widgets in pairs(widgetsOfId) do
if #widgets > 1 then
sort(widgets, function(widgetA, widgetB)
return x[widgetA] + y[widgetA] < x[widgetB] + y[widgetB]
end)
end
end
end
if INDEX_BY_HEALTH_PERCENT then
local indexFromRect, forcedIndex, tempWidget, healthPercent, indexFromHealth
local toHeal = {}
for id, widgets in pairs(widgetsOfId) do
local indices = {}
for index, widget in pairs(widgets) do
::begin::
indexFromRect = GetIndexFromRect(x[widget], y[widget])
healthPercent = HandleType[widget] ~= "item" and (100*GetWidgetLife(widget) + 0.5) // GetWidgetMaxLife[HandleType[widget]](widget) or 100
indexFromHealth = not INDEX_BY_HEALTH_IGNORE_TYPES[id] and healthPercent < 100 and healthPercent or nil
forcedIndex = indexFromRect or indexFromHealth
if forcedIndex then
if indexFromHealth then
insert(toHeal, widget)
end
if index ~= forcedIndex then
if widgets[forcedIndex] then
if indices[forcedIndex] then
print("|cffff0000Warning:|r Two or more widgets of type |cffffcc00" .. nameOfId[id] .. "|r have the same forced index " .. forcedIndex .. ".")
break
end
tempWidget = widgets[forcedIndex]
widgets[forcedIndex] = widget
widgets[index] = tempWidget
widget = tempWidget
indices[forcedIndex] = true
goto begin
else
widgets[forcedIndex] = widget
widgets[index] = nil
end
end
end
end
end
for __, widget in ipairs(toHeal) do
SetWidgetLife(widget, GetWidgetMaxLife[HandleType[widget]](widget))
end
end
for __, widgets in pairs(widgetsOfId) do
for index, widget in pairs(widgets) do
PreplacedIndex[widget] = index
end
end
end
local function AfterAliceInit()
Require "ALICE"
ALICE_TrackVariables("PreplacedIndex")
ALICE_Debug()
end
OnInit.global("PreplacedWidgetIndexer", BeforeAliceInit)
if DEBUG_MODE then
OnInit.final(AfterAliceInit)
end
---Returns a preplaced widget identified by widgetType, which can be an id, fourCC code, name, or hero proper name. Optional index parameter for widget types for which more than one preplaced instance exists.
---@param widgetType string | integer
---@param index? integer
---@return unit | destructable | item
function GetPreplacedWidget(widgetType, index)
if type(widgetType) == "number" then
assert(widgetsOfId[widgetType], "Preplaced widgets with fourCC code " .. string.pack(">I4", widgetType) .. " do not exist.")
return widgetsOfId[widgetType][index or 1]
elseif widgetType:len() == 4 then
local fourCC = FourCC(widgetType)
assert(widgetsOfId[fourCC], "Preplaced widgets with fourCC code " .. fourCC .. " do not exist.")
return widgetsOfId[fourCC][index or 1]
else
assert(idOfName[widgetType], "Preplaced widgets with name " .. widgetType .. " do not exist.")
return widgetsOfId[idOfName[widgetType]][index or 1]
end
end
---Returns all preplaced widgets identified by widgetType, which can be an id, fourCC code, name, or hero proper name.
---@param widgetType string | integer
---@return table
function GetAllPreplacedWidgets(widgetType)
if type(widgetType) == "number" then
return widgetsOfId[widgetType]
elseif widgetType:len() == 4 then
return widgetsOfId[FourCC(widgetType)]
else
assert(idOfName[widgetType], "Preplaced widgets with name " .. widgetType .. " do not exist.")
return widgetsOfId[idOfName[widgetType]]
end
end
end
if Debug then Debug.endFile() end
do
local DUMMY_TYPE = "udum"
local availableList = {}
local activeList = {}
local function RequestDummy(x,y,whichPlayer,abilityId)
local t
local u
if #availableList == 0 then
u = CreateUnit(whichPlayer , FourCC(DUMMY_TYPE) , x , y , 0 )
activeList[#activeList + 1] = u
t = CreateTimer()
else
u = availableList[#availableList]
activeList[#activeList + 1] = u
availableList[#availableList] = nil
SetUnitX(u, x)
SetUnitY(u, y)
end
local index = #activeList
TimerStart( t , 0.25 , false , function()
UnitRemoveAbility(u , abilityId)
if index < #activeList then
activeList[index] = activeList[#activeList]
end
activeList[#activeList] = nil
availableList[#availableList + 1] = u
DestroyTimer(t)
end )
return activeList[#activeList]
end
---@param source unit
---@param target unit
---@param abilityId integer
---@param level integer
---@param orderId string
---@return unit dummy
function DummyTargetUnit(source,target,abilityId,level,orderId)
local x = GetUnitX(source)
local y = GetUnitY(source)
local whichPlayer = GetOwningPlayer(source)
local dummy = RequestDummy(x,y,whichPlayer,abilityId)
UnitAddAbility(dummy,abilityId)
SetUnitAbilityLevel(dummy,abilityId,level)
IssueTargetOrderById(dummy,OrderId(orderId),target)
return dummy
end
end
if Debug then Debug.beginFile "Hook" end
--——————————————————————————————————————
-- Hook version 7.1.0.1
-- Created by: Bribe
-- Contributors: Eikonium, Jampion, MyPad, Wrda
--—————————————————————————————————————————————
---@class Hook.property
---@field next function|Hook.property --Call the next/native function. Also works with any given name (old/native/original/etc.). The args and return values align with the original function.
---@field remove fun(all?: boolean) --Remove the hook. Pass the boolean "true" to remove all hooks.
---@field package tree HookTree --Reference to the tree storing each hook on that particular key in that particular host.
---@field package priority number
---@field package index integer
---@field package hookAsBasicFn? function
----@field package debugId? string
----@field package debugNext? string
---@class Hook: {[integer]: Hook.property, [string]: function}
Hook = {}
do
local looseValuesMT = { __mode = "v" }
local hostKeyTreeMatrix = ---@type table<table, table<any, HookTree>>
setmetatable({
--Already add a hook matrix for _G right away.
[_G] = setmetatable({}, looseValuesMT)
}, looseValuesMT)
---@class HookTree: { [number]: Hook.property }
---@field host table
---@field key unknown --What the function was indexed to (_G items are typically indexed via strings)
---@field hasHookAsBasicFn boolean
---Reindexes a HookTree, inserting or removing a hook and updating the properties of each hook.
---@param tree HookTree
---@param index integer
---@param newHook? table
local function reindexTree(tree, index, newHook)
if newHook then
table.insert(tree, index, newHook)
else
table.remove(tree, index)
end
local top = #tree
local prevHook = tree[index - 1]
-- `table.insert` and `table.remove` shift the elements upwards or downwards,
-- so this loop manually aligns the tree elements with this shift.
for i = index, top do
local currentHook = tree[i]
currentHook.index = i
currentHook.next = (i > 1) and
rawget(prevHook, 'hookAsBasicFn') or
prevHook
--currentHook.debugNext = tostring(currentHook.next)
prevHook = currentHook
end
local topHookBasicFn = rawget(tree[top], 'hookAsBasicFn')
if topHookBasicFn then
if not tree.hasHookAsBasicFn or rawget(tree.host, tree.key) ~= topHookBasicFn then
tree.hasHookAsBasicFn = true
--a different basic function should be called for this hook
--instead of the one that was previously there.
tree.host[tree.key] = topHookBasicFn
end
else
--The below comparison rules out 'nil' and 'true'.
--Q: Why rule out nil?
--A: There is no need to reassign a host hook handler if there is already one in place.
if tree.hasHookAsBasicFn ~= false then
tree.host[tree.key] = function(...)
return tree[#tree](...)
end
end
tree.hasHookAsBasicFn = false
end
end
---@param hookProperty Hook.property
---@param deleteAllHooks? boolean
function Hook.delete(hookProperty, deleteAllHooks)
local tree = hookProperty.tree
hookProperty.tree = nil
if deleteAllHooks or #tree == 1 then
--Reset the host table's native behavior for the hooked key.
tree.host[tree.key] =
(tree[0] ~= DoNothing) and
tree[0] or
nil
hostKeyTreeMatrix[tree.host][tree.key] = nil
else
reindexTree(tree, hookProperty.index)
end
end
---@param hostTableToHook? table
---@param defaultNativeBehavior? function
---@param hookedTableIsMetaTable? boolean
local function setupHostTable(hostTableToHook, defaultNativeBehavior, hookedTableIsMetaTable)
hostTableToHook = hostTableToHook or _G
if hookedTableIsMetaTable or
(defaultNativeBehavior and hookedTableIsMetaTable == nil)
then
hostTableToHook = getmetatable(hostTableToHook) or
getmetatable(setmetatable(hostTableToHook, {}))
end
return hostTableToHook
end
---@param tree HookTree
---@param priority number
local function huntThroughPriorityList(tree, priority)
local index = 1
local topIndex = #tree
repeat
if priority <= tree[index].priority then
break
end
index = index + 1
until index > topIndex
return index
end
---@param hostTableToHook table
---@param key unknown
---@param defaultNativeBehavior? function
---@return HookTree | nil
local function createHookTree(hostTableToHook, key, defaultNativeBehavior)
local nativeFn = rawget(hostTableToHook, key) or
defaultNativeBehavior or
((hostTableToHook ~= _G or type(key) ~= "string") and
DoNothing)
if not nativeFn then
--Logging is used here instead of directly throwing an error, because
--no one can be sure that we're running within a debug-friendly thread.
(Debug and Debug.throwError or print)("Hook Error: No value found for key: " .. tostring(key))
return
end
---@class HookTree
local tree = {
host = hostTableToHook,
key = key,
[0] = nativeFn,
--debugNativeId = tostring(nativeFn)
}
hostKeyTreeMatrix[hostTableToHook][key] = tree
return tree
end
---@param self Hook.property
local function __index(self)
return self.next
end
---@param key unknown Usually `string` (the name of the native you wish to hook)
---@param callbackFn fun(Hook, ...):any The function you want to run when the native is called. The first parameter is type "Hook", and the remaining parameters (and return value(s)) align with the original function.
---@param priority? number Defaults to 0. Hooks are called in order of highest priority down to lowest priority. The native itself has the lowest priority.
---@param hostTableToHook? table Defaults to _G (the table that stores all global variables).
---@param defaultNativeBehavior? function If the native does not exist in the host table, use this default instead.
---@param hookedTableIsMetaTable? boolean Whether to store into the host's metatable instead. Defaults to true if the "default" parameter is given.
---@param hookAsBasicFn? boolean When adding a hook instance, the default behavior is to use the __call metamethod in metatables to govern callbacks. If this is `true`, it will instead use normal function callbacks.
---@return Hook.property
function Hook.add(
key,
callbackFn,
priority,
hostTableToHook,
defaultNativeBehavior,
hookedTableIsMetaTable,
hookAsBasicFn
)
priority = priority or 0
hostTableToHook = setupHostTable(hostTableToHook, defaultNativeBehavior, hookedTableIsMetaTable)
hostKeyTreeMatrix[hostTableToHook] =
hostKeyTreeMatrix[hostTableToHook] or
setmetatable({}, looseValuesMT)
local index = 1
local tree = hostKeyTreeMatrix[hostTableToHook][key]
if tree then
index = huntThroughPriorityList(tree, priority)
else
---@diagnostic disable-next-line: cast-local-type
tree = createHookTree(hostTableToHook, key, defaultNativeBehavior)
if not tree then
return ---@diagnostic disable-line: missing-return-value
end
end
local new = {
priority = priority,
tree = tree
}
function new.remove(deleteAllHooks)
Hook.delete(new, deleteAllHooks)
end
--new.debugId = tostring(callbackFn) .. ' and ' .. tostring(new)
if hookAsBasicFn then
new.hookAsBasicFn = callbackFn
else
setmetatable(new, {
__call = callbackFn,
__index = __index
})
end
reindexTree(tree, index, new)
return new
end
end
---Hook.basic avoids creating a metatable for the hook.
---This is necessary for adding hooks to metatable methods such as __index.
---The main difference versus Hook.add is in the parameters passed to callbackFn;
---Hook.add has a 'self' argument which points to the hook, whereas Hook.basic does not.
---@param key unknown
---@param callbackFn fun(Hook, ...):any
---@param priority? number
---@param hostTableToHook? table
---@param defaultNativeBehavior? function
---@param hookedTableIsMetaTable? boolean
function Hook.basic(key, callbackFn, priority, hostTableToHook, defaultNativeBehavior, hookedTableIsMetaTable)
return Hook.add(key, callbackFn, priority, hostTableToHook, defaultNativeBehavior, hookedTableIsMetaTable, true)
end
---@deprecated
---@see Hook.add for args
function AddHook(...)
local new = Hook.basic(...)
return function(...)
return new.next(...)
end, new.remove
end
setmetatable(Hook, {
__newindex = function(_, key, callback)
Hook.add(key, callback)
end
})
if Debug then Debug.endFile() end
if Debug then Debug.beginFile "FrameDoctor" end
do
--[[
=============================================================================================================================================================
Frame Doctor
by Antares
Automatically repair all broken frames and return them to their last known state after loading a saved game.
Requires:
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
Hook https://www.hiveworkshop.com/threads/hook.339153/
HandleType https://www.hiveworkshop.com/threads/get-handle-type.354436/
=============================================================================================================================================================
IsFrame(var) Returns whether the specified variable is a framehandle or FrameData table.
GetFrame(var) Returns the variable if it is a framehandle or the framehandle stored to a FrameData table.
=============================================================================================================================================================
]]
--Disable for debugging purposes. Use this if you want to keep the IsFrame and GetFrame functions and overwrite BlzDestroyFrame. Otherwise disable in World
--Editor.
local DISABLE = false
--Disable when the map is launched in multiplayer. Make sure that this doesn't break your code because tables are now suddenly framehandles again.
local DISABLE_IN_MULTIPLAYER = true
--===========================================================================================================================================================
local SETTER_FUNCTIONS
local GETTER_FUNCTIONS
local FRAMEPOINTS
local ORIGIN_FRAME_NAMES
local copiedData
local gameWasSaved = false
local tocFiles = {}
local hideOriginFrames = false
local pack = table.pack
local unpack = table.unpack
local dataOfFrame = {} ---@type FrameData[]
local first = {} ---@type FrameData
local last = first ---@type FrameData
local SetPointNative
local SetAbsPointNative
local SetTooltipNative
local AddTextNative
local RegisterFrameEventNative
local GetOriginFrameNative
local GetChildNative
local GetParentNative
local GetFrameByNameNative
local ClearAllPointsNative
local SetVisibleNative
---@class FrameData
local FrameData = {
framehandle = nil, ---@type framehandle
constructor = nil, ---@type function
constructorArgs = nil, ---@type table
setters = nil, ---@type table<function,boolean>
setterArgs = nil, ---@type table[]
positioners = nil, ---@type table<framepointtype,function>
x = nil, ---@type number[]
y = nil, ---@type number[]
anchor = nil, ---@type FrameData[]
anchorFramePoint = nil, ---@type framepointtype[]
clearAllPoints = nil, ---@type boolean
path = nil, ---@type string
addText = nil, ---@type string[]
tooltip = nil, ---@type FrameData
triggers = nil, ---@type trigger[]
triggerEventTypes = nil, ---@type frameeventtype[]
parent = nil, ---@type FrameData
parentIndex = nil, ---@type integer
child = nil, ---@type FrameData
next = nil, ---@type FrameData
previous = nil, ---@type FrameData
}
---@param var FrameData | framehandle
---@return framehandle, FrameData
local function GetFrameAndData(var)
if HandleType[var] == "framehandle" then
local data = dataOfFrame[var]
if data then
return data.framehandle, data
else
return var, dataOfFrame[var]
end
else
return var.framehandle, var
end
end
---@param name string
---@return framehandle
local function GetFrameFromString(name)
local position = name:find("---")
local index = name:sub(position + 3, name:len())
string = name:sub(1, position - 1)
if _G[string] then
return GetOriginFrameNative(_G[string], tonumber(index))
else
return GetFrameByNameNative(string, tonumber(index))
end
end
local function GetFrameFromParent(data)
return GetChildNative(data.parent.framehandle, data.parentIndex)
end
local function GetFrameFromChild(data)
return GetParentNative(data.child.framehandle)
end
---@param frame framehandle
---@param parent? FrameData
---@param index? integer
---@param path? string
---@param constructor? string
---@param constructorArgs? table
---@param child? FrameData
local function RegisterFrame(frame, parent, index, path, constructor, constructorArgs, child)
local data
if copiedData then
data = copiedData
data.framehandle = frame
else
data = {
framehandle = frame,
constructor = _G[constructor],
parent = parent,
parentIndex = index,
child = child,
path = path,
constructorArgs = constructorArgs,
setters = {},
setterArgs = {},
positioners = {},
x = {},
y = {},
anchor = {},
anchorFramePoint = {},
}
last.next = data
data.previous = last
last = data
end
dataOfFrame[frame] = data
return data
end
---@param data FrameData
local function CopyFrame(data)
if data.constructor then
copiedData = data
data.constructor(unpack(data.constructorArgs))
copiedData = nil
elseif data.path then
data.framehandle = GetFrameFromString(data.path)
elseif data.parent then
data.framehandle = GetFrameFromParent(data)
elseif data.child then
data.framehandle = GetFrameFromChild(data)
end
dataOfFrame[data.framehandle] = data
end
---@param data FrameData
local function ModifyFrame(data)
if data.clearAllPoints then
ClearAllPointsNative(data.framehandle)
end
for framePoint, func in pairs(data.positioners) do
if func == SetPointNative then
SetPointNative(data.framehandle, framePoint, data.anchor[framePoint].framehandle, data.anchorFramePoint[framePoint], data.x[framePoint], data.y[framePoint])
else
SetAbsPointNative(data.framehandle, framePoint, data.x[framePoint], data.y[framePoint])
end
end
for setterFunc, __ in pairs(data.setters) do
setterFunc(data.framehandle, unpack(data.setterArgs[setterFunc]))
end
if data.addText then
for __, text in ipairs(data.addText) do
AddTextNative(data.framehandle, text)
end
end
if data.triggers then
for index, trigger in ipairs(data.triggers) do
RegisterFrameEventNative(trigger, data.framehandle, data.triggerEventTypes[index])
end
end
if data.tooltip then
SetTooltipNative(data.framehandle, data.tooltip.framehandle)
end
end
local function RecreateFrames()
local data = first.next
while data do
CopyFrame(data)
data = data.next
end
data = first.next
while data do
ModifyFrame(data)
data = data.next
end
end
local function OnLoad()
gameWasSaved = true
if hideOriginFrames then
BlzHideOriginFrames(true)
end
for __, path in ipairs(tocFiles) do
BlzLoadTOCFile(path)
end
RecreateFrames()
DestroyTimer(GetExpiredTimer())
end
OnInit.global("FrameDoctor", function()
if DISABLE then
return
end
if DISABLE_IN_MULTIPLAYER and not bj_isSinglePlayer then
Hook.add("BlzDestroyFrame", function(self, frame)
BlzFrameSetVisible(frame, false)
end)
return
end
FrameDoctor = true
SetPointNative = BlzFrameSetPoint
SetAbsPointNative = BlzFrameSetAbsPoint
SetTooltipNative = BlzFrameSetTooltip
AddTextNative = BlzFrameAddText
RegisterFrameEventNative = BlzTriggerRegisterFrameEvent
GetOriginFrameNative = BlzGetOriginFrame
GetChildNative = BlzFrameGetChild
GetParentNative = BlzFrameGetParent
GetFrameByNameNative = BlzGetFrameByName
ClearAllPointsNative = BlzFrameClearAllPoints
SetVisibleNative = BlzFrameSetVisible
SETTER_FUNCTIONS = {
BlzDestroyFrame = true,
BlzFrameSetVisible = true,
BlzFrameSetText = true,
BlzFrameSetTextSizeLimit = true,
BlzFrameSetTextColor = true,
BlzFrameSetFocus = true,
BlzFrameSetModel = true,
BlzFrameSetEnable = true,
BlzFrameSetAlpha = true,
BlzFrameSetSpriteAnimate = true,
BlzFrameSetTexture = true,
BlzFrameSetScale = true,
BlzFrameCageMouse = true,
BlzFrameSetValue = true,
BlzFrameSetMinMaxValue = true,
BlzFrameSetStepSize = true,
BlzFrameSetSize = true,
BlzFrameSetVertexColor = true,
BlzFrameSetLevel = true,
BlzFrameSetParent = true,
BlzFrameSetFont = true,
BlzFrameSetTextAlignment = true,
}
GETTER_FUNCTIONS = {
BlzFrameGetText = true,
BlzFrameGetName = true,
BlzFrameGetTextSizeLimit = true,
BlzFrameGetAlpha = true,
BlzFrameGetEnable = true,
BlzFrameGetValue = true,
BlzFrameGetHeight = true,
BlzFrameGetWidth = true,
BlzFrameIsVisible = true,
}
if BlzFrameGetChildrenCount then
GETTER_FUNCTIONS.BlzFrameGetChildrenCount = true
end
FRAMEPOINTS = {
[FRAMEPOINT_BOTTOM] = true,
[FRAMEPOINT_BOTTOMLEFT] = true,
[FRAMEPOINT_BOTTOMRIGHT] = true,
[FRAMEPOINT_CENTER] = true,
[FRAMEPOINT_LEFT] = true,
[FRAMEPOINT_RIGHT] = true,
[FRAMEPOINT_TOP] = true,
[FRAMEPOINT_TOPLEFT] = true,
[FRAMEPOINT_TOPRIGHT] = true
}
ORIGIN_FRAME_NAMES = {
[ORIGIN_FRAME_GAME_UI] = "ORIGIN_FRAME_GAME_UI",
[ORIGIN_FRAME_COMMAND_BUTTON] = "ORIGIN_FRAME_COMMAND_BUTTON",
[ORIGIN_FRAME_HERO_BAR] = "ORIGIN_FRAME_HERO_BAR",
[ORIGIN_FRAME_HERO_BUTTON] = "ORIGIN_FRAME_HERO_BUTTON",
[ORIGIN_FRAME_HERO_HP_BAR] = "ORIGIN_FRAME_HERO_HP_BAR",
[ORIGIN_FRAME_HERO_MANA_BAR] = "ORIGIN_FRAME_HERO_MANA_BAR",
[ORIGIN_FRAME_HERO_BUTTON_INDICATOR] = "ORIGIN_FRAME_HERO_BUTTON_INDICATOR",
[ORIGIN_FRAME_ITEM_BUTTON] = "ORIGIN_FRAME_ITEM_BUTTON",
[ORIGIN_FRAME_MINIMAP] = "ORIGIN_FRAME_MINIMAP",
[ORIGIN_FRAME_MINIMAP_BUTTON] = "ORIGIN_FRAME_MINIMAP_BUTTON",
[ORIGIN_FRAME_SYSTEM_BUTTON] = "ORIGIN_FRAME_SYSTEM_BUTTON",
[ORIGIN_FRAME_TOOLTIP] = "ORIGIN_FRAME_TOOLTIP",
[ORIGIN_FRAME_UBERTOOLTIP] = "ORIGIN_FRAME_UBERTOOLTIP",
[ORIGIN_FRAME_CHAT_MSG] = "ORIGIN_FRAME_CHAT_MSG",
[ORIGIN_FRAME_UNIT_MSG] = "ORIGIN_FRAME_UNIT_MSG",
[ORIGIN_FRAME_TOP_MSG] = "ORIGIN_FRAME_TOP_MSG",
[ORIGIN_FRAME_PORTRAIT] = "ORIGIN_FRAME_PORTRAIT",
[ORIGIN_FRAME_WORLD_FRAME] = "ORIGIN_FRAME_WORLD_FRAME",
}
if ORIGIN_FRAME_SIMPLE_UI_PARENT then
ORIGIN_FRAME_NAMES[ORIGIN_FRAME_SIMPLE_UI_PARENT] = "ORIGIN_FRAME_SIMPLE_UI_PARENT"
ORIGIN_FRAME_NAMES[ORIGIN_FRAME_PORTRAIT_HP_TEXT] = "ORIGIN_FRAME_PORTRAIT_HP_TEXT"
ORIGIN_FRAME_NAMES[ORIGIN_FRAME_PORTRAIT_MANA_TEXT] = "ORIGIN_FRAME_PORTRAIT_MANA_TEXT"
ORIGIN_FRAME_NAMES[ORIGIN_FRAME_UNIT_PANEL_BUFF_BAR] = "ORIGIN_FRAME_UNIT_PANEL_BUFF_BAR"
ORIGIN_FRAME_NAMES[ORIGIN_FRAME_UNIT_PANEL_BUFF_BAR_LABEL] = "ORIGIN_FRAME_UNIT_PANEL_BUFF_BAR_LABEL"
end
local trig = CreateTrigger()
TriggerRegisterGameEvent(trig, EVENT_GAME_LOADED)
TriggerAddAction(trig, function()
TimerStart(CreateTimer(), 0.0, false, OnLoad)
end)
--Functions that return a framehandle have the lowest possible priority so that other hooks don't break because it returns a table.
--Functions that take a framehandle have the highest possible priority so that other hooks don't receive a table.
Hook.add("BlzCreateFrame", function(self, name, ownerVar, priority, createContext)
local ownerFrame, ownerData = GetFrameAndData(ownerVar)
local newFrame = self.old(name, ownerFrame, priority, createContext)
return RegisterFrame(newFrame, nil, nil, nil, "BlzCreateFrame", {name, ownerData, priority, createContext})
end, -2147483648)
Hook.add("BlzCreateSimpleFrame", function(self, name, ownerVar, createContext)
local ownerFrame, ownerData = GetFrameAndData(ownerVar)
local newFrame = self.old(name, ownerFrame, createContext)
return RegisterFrame(newFrame, nil, nil, nil, "BlzCreateSimpleFrame", {name, ownerData, createContext})
end, -2147483648)
Hook.add("BlzCreateFrameByType", function(self, typeName, name, ownerVar, inherits, createContext)
local ownerFrame, ownerData = GetFrameAndData(ownerVar)
local newFrame = self.old(typeName, name, ownerFrame, inherits, createContext)
return RegisterFrame(newFrame, nil, nil, nil, "BlzCreateFrameByType", {typeName, name, ownerData, inherits, createContext})
end, -2147483648)
for func, __ in pairs(SETTER_FUNCTIONS) do
Hook.add(func, function(self, var, ...)
if var == nil then return end
local framehandle, data = GetFrameAndData(var)
self.old(framehandle, ...)
dataOfFrame[framehandle].setters[self.old] = true
dataOfFrame[framehandle].setterArgs[self.old] = pack(...)
end, 2147483647)
end
for func, __ in pairs(GETTER_FUNCTIONS) do
Hook.add(func, function(self, var, ...)
if var == nil then return end
local framehandle, data = GetFrameAndData(var)
return self.old(framehandle, ...)
end, 2147483647)
end
Hook.add("BlzFrameAddText", function(self, var, text)
local framehandle, data = GetFrameAndData(var)
if var == nil then return end
data.addText = data.addText or {}
table.insert(data.addText, text)
self.old(framehandle, text)
end)
Hook.add("BlzFrameSetParent", function(self, var, parentVar)
if var == nil then return end
local framehandle, data = GetFrameAndData(var)
local parent = GetFrameAndData(parentVar)
self.old(framehandle, parent)
end, 2147483647)
Hook.add("BlzFrameSetTooltip", function(self, var, tooltipVar)
if var == nil then return end
local framehandle, data = GetFrameAndData(var)
local tooltip, tooltipData = GetFrameAndData(tooltipVar)
self.old(framehandle, tooltip)
data.tooltip = tooltipData
end, 2147483647)
Hook.add("BlzFrameSetAbsPoint", function(self, var, framePoint, x, y)
if var == nil then return end
local framehandle, data = GetFrameAndData(var)
self.old(framehandle, framePoint, x, y)
data.positioners[framePoint] = self.old
data.x[framePoint] = x
data.y[framePoint] = y
end, 2147483647)
Hook.add("BlzFrameSetPoint", function(self, var, framePoint, anchorVar, anchorFramePoint, x, y)
if var == nil or anchorVar == nil then return end
local framehandle, data = GetFrameAndData(var)
local anchorFrame, anchorData = GetFrameAndData(anchorVar)
self.old(framehandle, framePoint, anchorFrame, anchorFramePoint, x, y)
data.positioners[framePoint] = self.old
data.x[framePoint] = x
data.y[framePoint] = y
data.anchor[framePoint] = anchorData
data.anchorFramePoint[framePoint] = anchorFramePoint
end, 2147483647)
Hook.add("BlzFrameClearAllPoints", function(self, var)
if var == nil then return end
local framehandle, data = GetFrameAndData(var)
self.old(framehandle)
data.clearAllPoints = true
for framePoint, __ in pairs(FRAMEPOINTS) do
data.positioners[framePoint] = nil
end
end, 2147483647)
Hook.add("BlzFrameSetAllPoints", function(self, var, anchorVar)
if var == nil or anchorVar == nil then return end
local framehandle, data = GetFrameAndData(var)
local anchorFrame, anchorData = GetFrameAndData(anchorVar)
self.old(framehandle, anchorFrame)
for framePoint, __ in pairs(FRAMEPOINTS) do
data.positioners[framePoint] = SetPointNative
data.x[framePoint] = 0
data.y[framePoint] = 0
data.anchor[framePoint] = anchorData
data.anchorFramePoint[framePoint] = framePoint
end
end, 2147483647)
Hook.add("BlzFrameClick", function(self, var)
if var == nil then return end
local framehandle, data = GetFrameAndData(var)
self.old(framehandle)
end)
Hook.add("BlzTriggerRegisterFrameEvent", function(self, trigger, var, frameEventType)
if var == nil then return end
local framehandle, data = GetFrameAndData(var)
data.triggers = data.triggers or {}
data.triggerEventTypes = data.triggerEventTypes or {}
table.insert(data.triggers, trigger)
table.insert(data.triggerEventTypes, frameEventType)
return self.old(trigger, framehandle, frameEventType)
end, 2147483647)
Hook.add("BlzGetTriggerFrame", function(self)
local framehandle = self.old()
return dataOfFrame[framehandle] or framehandle
end, -2147483648)
Hook.add("BlzFrameGetParent", function(self, var)
if var == nil then return end
local childFrame, childData = GetFrameAndData(var)
local parentFrame, parentData = GetFrameAndData(self.old(childFrame))
if parentData == nil then
RegisterFrame(parentFrame, nil, nil, nil, nil, nil, childData)
end
return parentData or parentFrame
end, -2147483648)
if BlzFrameGetChild then
Hook.add("BlzFrameGetChild", function(self, var, index)
if var == nil then return end
local parentFrame, parentData = GetFrameAndData(var)
local childFrame, childData = GetFrameAndData(self.old(parentFrame, index))
if childData == nil then
childData = RegisterFrame(childFrame, parentData, index)
end
return childData
end, -2147483648)
end
Hook.add("BlzGetFrameByName", function(self, name, createContext)
local frame, frameData = GetFrameAndData(self.old(name, createContext))
if frameData == nil then
frameData = RegisterFrame(frame, nil, nil, name .. "---" .. createContext)
end
return frameData
end, -2147483648)
Hook.add("BlzGetOriginFrame", function(self, originFrame, createContext)
local frame, frameData = GetFrameAndData(self.old(originFrame, createContext))
if frameData == nil then
frameData = RegisterFrame(frame, nil, nil, ORIGIN_FRAME_NAMES[originFrame] .. "---" .. createContext)
end
return frameData
end, -2147483648)
Hook.add("BlzDestroyFrame", function(self, var)
if var == nil then return end
local frame, frameData = GetFrameAndData(var)
SetVisibleNative(frame, false)
if frameData.next then
frameData.next.previous = frameData.previous
else
last = frameData.previous
end
frameData.previous.next = frameData.next
frameData[frame] = nil
end, 2147483647)
Hook.add("BlzLoadTOCFile", function(self, filePath)
local success = self.old(filePath)
if not gameWasSaved then
table.insert(tocFiles, filePath)
end
return success
end)
Hook.add("BlzHideOriginFrames", function(self, enable)
hideOriginFrames = enable
self.old(enable)
end)
Hook.add("GetHandleId", function(self, var)
if type(var) == "table" and var.framehandle then
return self.old(var.framehandle)
else
return self.old(var)
end
end)
end)
---@param var any
---@return boolean
function IsFrame(var)
if HandleType[var] == "framehandle" then
return true
elseif type(var) == "table" then
return var.framehandle ~= nil
end
return false
end
---@param var framehandle | FrameData
---@return framehandle | nil
function GetFrame(var)
if HandleType[var] == "framehandle" then
return var
elseif type(var) == "table" then
return var.framehandle
end
return nil
end
end
if Debug then Debug.endFile() end
if Debug then Debug.beginFile "TalentDeclaration" end
do
OnInit.global(function()
--Lunar Blessing
CTT.RegisterTalent({
--Add name, tooltip, and icon from an ability.
fourCC = 'Tlub',
tree = "Lunar",
column = 2,
row = 1,
maxPoints = 5,
values = {
manaReg = 0.2
},
onLearn = function(whichPlayer, talentName, parentTree, oldRank, newRank)
if newRank > 0 then
UnitAddAbility(HeroOfPlayer[whichPlayer], FourCC "Alub")
SetUnitAbilityField(HeroOfPlayer[whichPlayer], "Alub", "Imrp", CTT.GetValue(whichPlayer, talentName, "manaReg"))
else
UnitRemoveAbility(HeroOfPlayer[whichPlayer], FourCC "Alub")
end
end,
})
--Improved Scout
CTT.RegisterTalent({
fourCC = 'Towl',
tree = "Lunar",
column = 3,
row = 1,
maxPoints = 5,
values = {
duration = 5
},
onLearn = function(whichPlayer, talentName, parentTree, oldRank, newRank)
local newDuration = math.tointeger(GetAbilityField("AEst", "adur") + CTT.GetValue(whichPlayer, talentName, "duration"))
SetUnitAbilityField(HeroOfPlayer[whichPlayer], "AEst", "adur", newDuration)
if GetLocalPlayer() == whichPlayer then
BlzSetAbilityExtendedTooltip(FourCC "AEst", "Summons an Owl Scout, which can be used to scout. Can see invisible units. Lasts " .. newDuration .. " seconds.", 0)
end
end
})
--Starlight Pillar
CTT.RegisterTalent({
fourCC = 'Tslp',
tree = "Lunar",
column = 2,
row = 2,
requirement = "Lunar Blessing",
values = {
healthReg = 20,
duration = 15
},
onLearn = function(whichPlayer, talentName, parentTree, oldRank, newRank)
if newRank > 0 then
UnitAddAbility(HeroOfPlayer[whichPlayer], FourCC "Apil")
else
UnitRemoveAbility(HeroOfPlayer[whichPlayer], FourCC "Apil")
end
end,
})
--Starforce
CTT.RegisterTalent({
--Add name, tooltip, and icon without using an ability.
name = "Starforce",
tooltip = "Increases all spell damage you deal by !increase,\x25!.",
prelearnTooltip = "Increases all duderized damage you deal by !increase,\x25!.",
icon = "TalentIcons\\Starforce.blp",
tree = "Lunar",
column = 3,
row = 2,
maxPoints = 3,
requirement = "Starlight Pillar",
values = {
increase = {0.04, 0.07, 0.1}
},
})
--Improved Starlight Pillar
CTT.RegisterTalent({
fourCC = 'Tisp',
tree = "Lunar",
column = 2,
row = 3,
maxPoints = 5,
requirement = "Starlight Pillar",
values = {
increase = 0.1
},
onLearn = function(whichPlayer, talentName, parentTree, oldRank, newRank)
local base = CTT.GetValue(whichPlayer, "Starlight Pillar", "healthReg")
local duration = CTT.GetValue(whichPlayer, "Starlight Pillar", "duration")
local increase = CTT.GetValue(whichPlayer, talentName, "increase")
if GetLocalPlayer() == whichPlayer then
BlzSetAbilityExtendedTooltip(FourCC "Apil", "Creates a pillar of starlight at target location that heals a unit bathing in the light by " .. (math.tointeger(base*(1 + increase)) or base*(1 + increase)) .. " health per second. Lasts " .. duration .. " seconds.", 0)
end
end
})
--Starfall
CTT.RegisterTalent({
--Add name, tooltip, and icon without using an ability.
name = "Starfall",
tooltip = "While channeling, calls down waves of falling stars that damage nearby enemy units. Each wave deals !damage! damage. Lasts for !duration! seconds. !cooldown! seconds cooldown.",
icon = "TalentIcons\\Starfall.blp",
tree = "Lunar",
column = 3,
row = 3,
--Retrieve values from the ability fields of the linked ability.
ability = "AEsf",
values = {
damage = "Esf1",
duration = "adur",
cooldown = "acdn"
},
onLearn = function(whichPlayer, talentName, parentTree, oldRank, newRank)
if newRank > 0 then
UnitAddAbility(HeroOfPlayer[whichPlayer], FourCC "AEsf")
else
UnitRemoveAbility(HeroOfPlayer[whichPlayer], FourCC "AEsf")
end
end,
})
--Elune's Protection
CTT.RegisterTalent({
fourCC = 'Tfoc',
tree = "Lunar",
column = 4,
row = 3,
maxPoints = 3,
requirement = "Starfall",
values = {
reduction = 0.3,
duration = 5
}
})
--Trueshot Aura
CTT.RegisterTalent({
name = "Trueshot Aura",
tooltip = "An aura that gives friendly nearby units a !damage,\x25! bonus damage to their ranged attacks.",
icon = "TalentIcons\\TrueshotAura.blp",
tree = "Archery",
column = 2,
row = 1,
maxPoints = 5,
ability = "AEar",
values = {
damage = "Ear1"
},
onLearn = function(whichPlayer, talentName, parentTree, oldRank, newRank)
if newRank == 1 then
UnitAddAbility(HeroOfPlayer[whichPlayer], FourCC "AEar")
elseif newRank > 1 then
SetUnitAbilityLevel(HeroOfPlayer[whichPlayer], FourCC "AEar", newRank)
else
UnitRemoveAbility(HeroOfPlayer[whichPlayer], FourCC "AEar")
end
end,
})
--Tenacity
CTT.RegisterTalent({
fourCC = 'Tten',
tree = "Survival",
column = 2,
row = 1,
maxPoints = 5,
values = {
health = 25
},
onLearn = function(whichPlayer, talentName, parentTree, oldRank, newRank)
if newRank > oldRank then
SetPlayerTechResearched(whichPlayer, FourCC "Rtnc", newRank)
else
BlzDecPlayerTechResearched(whichPlayer, FourCC "Rtnc", oldRank - newRank)
end
end
})
end)
end
if Debug then Debug.endFile() end
if Debug then Debug.beginFile "TalentTree" end
do
--[[
=============================================================================================================================================================
Classic Talent Trees
by Antares
v1.2
A recreation of Clasic World of Warcraft class talent trees.
Requires:
TotalInitialization https://www.hiveworkshop.com/threads/total-initialization.317099/
Handle Type https://www.hiveworkshop.com/threads/get-handle-type.354436/
Easy Ability Fields (optional) https://www.hiveworkshop.com/threads/easy-ability-fields.355859/
For installation guide, tutorials & documentation, see here:
https://www.hiveworkshop.com/threads/.355929/
=============================================================================================================================================================
A P I
=============================================================================================================================================================
• The who parameter can be a player or a unit. Passing a unit is simply a shortcut and will reroute the function to the unit's owning player. Creating
multiple sets of talent trees for different heroes controlled by the same player is not possible.
• Each talent or talent tree is referenced exclusively by its name.
CTT.RegisterTalent(data) Registers a talent from the data provided in the data table. The recognized fields for data table are
fourCC, name, icon, tooltip, tree, column, row, maxPoints, callback, requirement, and values.
For details, see hiveworkshop resource page.
CTT.SetTrees(who, ...) Sets the talent trees for the specified player to the provided list. The talent trees will be arranged
according to their order in the arguments.
CTT.AddTrees(who, ...) Adds the provided list of talent trees to the talents trees of the specified player. The talent trees
will be arranged according to their order in the arguments.
CTT.GrantPoints(who, howMany?) Adds the specified amount, or 1 if not specified, to the player's unspent talent points.
CTT.SetPoints(who, howMany) Sets the unspent talent points of the specified player to the provided amount.
CTT.ForceSelectTalent(who, whichTalent, subtractCost?) Selects the specified talent for a player. If the optional subtractCost argument is not set, the
player will not lose unspent talent points and all requirements of the talent will be ignored.
CTT.ResetAllTalents(who) Resets all talents and refunds their cost for the specified player.
CTT.GetUnspentPoints(who) Returns the amount of unspent talent points the specified player has.
CTT.GetTalentRank(who, whichTalent) Returns the rank of the specified talent for a player.
CTT.HasTalent(who, whichTalent) Returns whether a player has one or more talent points invested in the specified talent.
CTT.IsTalentMaxed(who, whichTalent) Returns whether a player has invested the maximum number of points in the specified talent.
CTT.DisableTalent(who, whichTalent, requirementText?) Disables the specified talent for a player. The requirements text will be inserted on top of the tooltip
while it is disabled. If the optional overwriteRequirements flag is set, the text for other missing
requirements will be removed from the tooltip.
CTT.EnableTalent(who, whichTalent) Enables for the specified player a talent which has been previously disabled with CTT.DisableTalent.
CTT.GetPointsInTree(who, whichTree) Returns the number of talent points a player has invested into the specified talent tree.
CTT.GetTotalPoints(who) Returns the total number of talent points a player has distributed among talents. Does not include
unspent talent points.
CTT.GetValue(who, whichTalent, whichValue) Returns the value with the whichValue key in the values table stored for the specified talent at the
talent rank of a player.
CTT.GetTooltip(who, whichTalent) Returns the tooltip for the specified talent at the talent rank of a player.
CTT.GetTalentPosition(whichTalent) Returns the parent tree, the row, and the column of the specified talent.
CTT.AffectedBy(object) Returns a table containing all talents that affect the specified object. Object can be a fourCC code
or id.
CTT.GetImageSizeRatio(width?, height?) Prints out the optimal image size ratio for the talent tree background images. You can specify either
width or height to get the other.
CTT.Show(who?, enable?) Opens the talent menu for the specified player, or for all players if no player is specified. Set the
optional enable parameter to false to instead force players to close the talent menu.
CTT.IsOpened(who) Returns whether the specified player has the talent tree opened.
CTT.Compile(who) Returns a table containing a sequence with the ranks of each talent for the specified player as well
as the number of unspent talent points as the last entry. Empty slots in the talent trees are included
as a 0.
CTT.LoadBuild(who, data) Loads the specified player's talent build from the provided data table which has been generated with
the Compile function. Returns false if the load was not successful.
CTT.ResetSavedBuild(who, data) Loads the talent build from the provided table and refunds all acquired talents for the specified
player. This function should be used if the talent build load from the Load function was not successful.
=============================================================================================================================================================
C O N F I G
=============================================================================================================================================================
]]
--Define talent trees by their name.
local TALENT_TREES = { ---@constant string[]
"Lunar",
"Archery",
"Survival"
}
--If enabled, the cost of talents is set by the GetTalentCost function. In addition, a cost indicator and symbol will appear in the talent tooltips. You need
--to import an icon called "TalentPoints.blp".
local VARIABLE_TALENT_COST = false
--If enabled, the first talent row is at the bottom instead of the top.
local TALENT_TREE_BOTTOM_TO_TOP = true
--Number of points required to be invested into a talent tree to unlock the next row of talents.
local POINTS_IN_TREE_REQUIRED_PER_ROW = 5
--Number of points required to be invested into any talent to unlock the next row of talents for each tree.
local POINTS_TOTAL_REQUIRED_PER_ROW = 0
--If enabled and VARIABLE_TALENT_COST is enabled, the above values refer to currency invested instead of talent points.
local REQUIRE_CURRENCY_INSTEAD_OF_POINTS = false
--A function that is called whenever a player puts a point into any talent. Called with the arguments (whichPlayer, talentName, parentTree, oldRank, newRank).
local GLOBAL_CALLBACK = nil
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--These constants control the layout and the extents of the talent tree menu.
--x-position of menu center.
local TREE_MENU_X = 0.14
local TREE_MENU_Y = 0.38
local TREE_MENU_WIDTH = 0.28
local TREE_MENU_HEIGHT = 0.395
--x-Position of first talent column relative to left edge of menu.
local TALENT_X_OFFSET = 0.043
--y-Position of first talent row relative to top/bottom edge of menu.
local TALENT_Y_OFFSET = 0.056
local TALENT_VERTICAL_SPACING = 0.0547
local TALENT_HORIZONTAL_SPACING = 0.0547
local TALENT_BUTTON_SIZE = 0.03
local NUMBER_OF_TALENT_ROWS = 6
local NUMBER_OF_TALENT_COLUMNS = 4
local BACKGROUND_HORIZONTAL_INSET = 0.02
local BACKGROUND_VERTICAL_INSET = -0.003
local BLACK_FADE_CORNER_SIZE = 0.02
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--These constants control the talent tooltips. Within these strings, you can use various auto-replacements.
--This text appears when a requirement talent is not maxed out. Use !MAXPOINTS! for the maximum points of the requirement talent and !REQUIREMENT! for the
--requirement talent's name.
local REQUIREMENT_NOT_MET_TEXT = "|cffff0000Requires !MAXPOINTS! point(s) in !REQUIREMENT!|r"
--This text appears when not enough points are invested into the parent tree of the talent. Use !TREEPOINTS! for the required points in the tree and !TREE!
--for the name of the tree.
local NOT_ENOUGH_POINTS_IN_TREE_TEXT = "|cffff0000Requires !TREEPOINTS! point(s) in !TREE! Talents|r"
--This text appears when not enough points are invested into any talent. Use !TOTALPOINTS! to refer to the number of talent points required.
local NOT_ENOUGH_POINTS_TOTAL_TEXT = "|cffff0000Requires !TOTALPOINTS! Talents total|r"
--The header of the talent tooltip.
local TALENT_TITLE = "|cffaaaaaaTalent|r"
--The header of the tooltip for the current rank of the talent. Use !POINTS! for the current rank, and !MAXPOINTS! for the maximum.
local CURRENT_RANK_TEXT = "|cffaaaaffCurrent rank (!POINTS!/!MAXPOINTS!):|r"
--The header of the tooltip for the next rank of the talent. Use !POINTS! for the next rank, and !MAXPOINTS! for the maximum.
local NEXT_RANK_TEXT = "|cffaaaaffNext rank (!POINTS!/!MAXPOINTS!):|r"
--The color of all auto-replacements from a talent's values table.
local TOOLTIP_VALUE_COLOR = "|cffffcc00"
--Increases the height of the tooltip relative to the height of the text box.
local TALENT_TOOLTIP_HEIGHT_BUFFER = 0.035
--Define additional replacements that are automatically made in all tooltips. This table's entries define in pairs the strings that should be replaced and
--strings to replace them with (string 1, replacement 1, string 2, replacement 2, ...).
local REPLACEMENTS = {
"Poisoned", "|cff00ff00Poisoned|r",
}
--If enabled, a list of all objects added with the affects flag is included at the bottom of each tooltip.
local ADD_AFFECTED_OBJECTS_TO_TOOLTIP = true
local AFFECTS_HEADER = "|cffffcc00Affects:|r"
local AFFECTED_ITEM_COLOR = "|cffaaaaaa"
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--These constants control the rank indicators of the talent buttons.
local RANK_INDICATOR_WIDTH = 0.024
local RANK_INDICATOR_HEIGHT = 0.014
local RANK_INDICATOR_HORIZONTAL_OVERLAP = 0.014
local RANK_INDICATOR_VERTICAL_OVERLAP = 0.007
local RANK_INDICATOR_FONT_SIZE = 8
--The color of the rank indicator when a talent cannot be picked.
local UNAVAILABLE_COLOR = "|cffaaaaaa"
--The color of the rank indicator when a talent can be picked.
local AVAILABLE_COLOR = "|cff00ff00"
--The color of the rank indicator when the talent is maxed.
local MAXED_COLOR = "|cffffcc00"
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--These constants control the size of the requirement arrows.
local REQUIREMENT_ARROW_BUTTON_OVERLAP = 0.004
local REQUIREMENT_ARROW_WIDTH = 0.008
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--These constants control the text and layout of the top bar.
local TOP_BAR_HEIGHT = 0.035
local TOP_BAR_TEXT_VERTICAL_SHIFT = 0.007
--Relevant if the border of the bar isn't covered by the talent menu border.
local TOP_BAR_HORIZONTAL_INSET = 0.015
local TOP_BAR_VERTICAL_SHIFT = -0.01
local TOP_BAR_TEXT = "|cffffcc00Talents|r"
local TOP_BAR_FONT_SIZE = 13
local CLOSE_BUTTON_SIZE = 0.023
local CLOSE_BUTTON_HORIZONTAL_SHIFT = -0.001
local CLOSE_BUTTON_VERTICAL_SHIFT = -0.007
local CLOSE_BUTTON_TEXTURE = "TalentMenuClose.blp"
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--These constants control the text and layout of the bottom bar.
local BOTTOM_BAR_HEIGHT = 0.029
local BOTTOM_BAR_TEXT_VERTICAL_SHIFT = -0.007
--The distance between the left edge of the text box and the right edge of the menu.
local BOTTOM_RIGHT_TEXT_HORIZONTAL_INSET = 0.113
--The distance between the left edge of the text box and the left edge of the menu.
local BOTTOM_LEFT_TEXT_HORIZONTAL_INSET = 0.023
--Possible replacements are !TREE!, !UNSPENTPOINTS!, !TREEPOINTS!, !TREECURRENCY!, !TOTALPOINTS!, and !TOTALCURRENCY!.
local BOTTOM_RIGHT_TEXT = "|cffffcc00Unspent Talents:|r !UNSPENTPOINTS!"
local BOTTOM_LEFT_TEXT = "|cffffcc00!TREE! Talents:|r !TREEPOINTS!"
--Relevant if the border of the bar isn't covered by the talent menu border.
local BOTTOM_BAR_HORIZONTAL_INSET = 0.015
local BOTTOM_BAR_VERTICAL_SHIFT = 0.01
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--These constants control the appearance and layout of the talent tree navigator buttons.
local TREE_NAVIGATOR_WIDTH = 0.062
local TREE_NAVIGATOR_HEIGHT = 0.028
local TREE_NAVIGATOR_HORIZONTAL_SHIFT = 0.016
local TREE_NAVIGATOR_VERTICAL_SHIFT = 0.017
local TREE_NAVIGATOR_OVERLAP = 0.005
local TREE_NAVIGATOR_SELECTED_SIZE_INCREASE = 0.005
local TREE_NAVIGATOR_FONT_SIZE = 10
local TREE_NAVIGATOR_SELECTED_COLOR = "|cffffffff"
local TREE_NAVIGATOR_UNSELECTED_COLOR = "|cffaaaaaa"
-------------------------------------------------------------------------------------------------------------------------------------------------------------
--If you have VARIABLE_TALENT_COST enabled, use this function to determine the currency cost of a talent.
---@param talentName string
---@param maxPoints integer
---@param currentLevel integer
---@return integer
local function GetTalentCost(talentName, maxPoints, currentLevel)
if maxPoints == 1 then
return 500
else
return 100
end
end
--Set the icon paths for maxed and disabled versions of talent icons here.
---@param basePath string
---@return string
local function GetTalentMaxedIconPath(basePath)
return basePath .. "Maxed.blp"
end
---@param basePath string
---@return string
local function GetTalentDisabledIconPath(basePath)
return basePath .. "Disabled.blp"
end
--[[
=============================================================================================================================================================
E N D O F C O N F I G
=============================================================================================================================================================
]]
local BACKGROUND_INSET_BOTTOM = BACKGROUND_VERTICAL_INSET + BOTTOM_BAR_HEIGHT + BOTTOM_BAR_VERTICAL_SHIFT
local BACKGROUND_INSET_TOP = BACKGROUND_VERTICAL_INSET + TOP_BAR_HEIGHT - TOP_BAR_VERTICAL_SHIFT
local talentTreeParentFrame = {} ---@type framehandle[]
local talentBackdrop ---@type framehandle
local talentFrame = {} ---@type framehandle[]
local talentIconFrame = {} ---@type framehandle[]
local talentIconClickedFrame = {} ---@type framehandle[]
local talentIconDisabledFrame = {} ---@type framehandle[]
local talentRankBoxFrame = {} ---@type framehandle[]
local talentRankFrame = {} ---@type framehandle[]
local treeNavigatorFrame = {} ---@type framehandle[]
local treeNavigatorTitles = {} ---@type framehandle[]
local treeNavigatorPosition = {} ---@type integer[]
local talentTreePointsSpentText = {} ---@type framehandle[]
local talentTreeUnspentPointsText = {} ---@type framehandle[]
local treeFromNavigator = {} ---@type string[]
local talentTooltipFrame = {} ---@type framehandle[]
local talentTooltipTitleFrame = {} ---@type framehandle[]
local talentTooltipTextFrame = {} ---@type framehandle[]
local talentArrowHeadFrame = {} ---@type framehandle[][]
local talentArrowBodyFrame = {} ---@type framehandle[][]
local talentTooltipIconFrame = {} ---@type framehandle[]
local talentTooltipCostFrame = {} ---@type framehandle[]
local talentFromFrame = {} ---@type string[]
local talentFromPosition = {} ---@type string[][][]
local talentTooltip = {} ---@type string[][]
local talentIconPath = {} ---@type string[]
local talentRequirement = {} ---@type string[][]
local talentRequirementTexture = {} ---@type string[][]
local talentSuccessors = {} ---@type string[][]
local talentParentTree = {} ---@type string[]
local talentColumn = {} ---@type integer[]
local talentRow = {} ---@type integer[]
local talentMaxPoints = {} ---@type integer[]
local talentOnLearn = {} ---@type function[]
local talentValues = {} ---@type number[][][] | string[][][] | boolean[][][]
local talentAffects = {} ---@type integer[][]
local talentAbility = {} ---@type integer[]
local talentTrigger ---@type trigger
local playerUnspentPoints = {} ---@type integer[]
local playerTalentTrees = {} ---@type string[][]
local playerPointsInTalent = {} ---@type integer[][]
local playerPointsInTree = {} ---@type integer[][]
local playerPointsTotal = {} ---@type integer[]
local playerCurrencyInTree = {} ---@type integer[][]
local playerCurrencyTotal = {} ---@type integer[]
local currentTalentTree = {} ---@type string
local playerRowsInTreeEnabled = {} ---@type integer[][]
local playerTalentIsEnabled = {} ---@type boolean[][]
local playerTalentIsHardDisabled = {} ---@type boolean[][]
local playerTalentRequirementText = {} ---@type string[][]
local playerTalentOverwriteText = {} ---@type boolean[][]
local affectedByTalents = {} ---@type string[][]
local localPlayer ---@type player
local buttonPressSound ---@type sound
local talentTreeOpen = {} ---@type boolean[]
local talentTreesInitialized ---@type boolean
local GetOwner ---@type function
local RECOGNIZED_FLAGS = { ---@constant table<string,boolean>
fourCC = true,
tree = true,
column = true,
row = true,
name = true,
tooltip = true,
prelearnTooltip = true,
icon = true,
maxPoints = true,
requirement = true,
onLearn = true,
onUnlearn = true,
values = true,
affects = true,
ability = true
}
local REQUIRED_FLAGS = { ---@constant table<string,boolean>
tree = true,
column = true,
row = true,
}
for __, tree in ipairs(TALENT_TREES) do
TALENT_TREES[tree] = true
talentFromPosition[tree] = {}
for j = 1, NUMBER_OF_TALENT_COLUMNS do
talentFromPosition[tree][j] = {}
end
end
--===========================================================================================================================================================
local function ToUpperCase(__, letter)
return letter:upper()
end
local function ToPascalCase(whichString)
whichString = whichString:gsub("|[cC]\x25x\x25x\x25x\x25x\x25x\x25x\x25x\x25x", "") --remove color codes
whichString = whichString:gsub("|[rR]", "") --remove closing color codes
whichString = whichString:gsub("(\x25s)(\x25a)", ToUpperCase) --remove spaces and convert to upper case after space
whichString = whichString:gsub("[^\x25w]", "") --remove special characters
return string.upper(whichString:sub(1,1)) .. string.sub(whichString,2) --converts first character to upper case
end
local function PressCloseButton()
if not BlzFrameIsVisible(talentBackdrop) then
return
end
local player = GetTriggerPlayer()
if localPlayer == player then
StartSound(buttonPressSound)
end
CTT.Show(player, false)
end
---@param whichTalent string
---@param whichPlayer player
---@return boolean
local function HasAllRequirements(whichTalent, whichPlayer)
if talentRequirement[whichTalent] == nil then
return true
end
for __, requirement in ipairs(talentRequirement[whichTalent]) do
if playerPointsInTalent[whichPlayer][requirement] < talentMaxPoints[requirement] then
return false
end
end
return true
end
---@param whichTalent string
---@param whichPlayer player
---@return boolean
local function ShouldTalentBeEnabled(whichTalent, whichPlayer)
if playerTalentIsHardDisabled[whichPlayer][whichTalent] then
return false
end
if REQUIRE_CURRENCY_INSTEAD_OF_POINTS then
return not playerTalentIsEnabled[whichPlayer][whichTalent]
and HasAllRequirements(whichTalent, whichPlayer)
and (talentRow[whichTalent] - 1)*POINTS_IN_TREE_REQUIRED_PER_ROW <= playerCurrencyInTree[whichPlayer][talentParentTree[whichTalent]]
and (talentRow[whichTalent] - 1)*POINTS_TOTAL_REQUIRED_PER_ROW <= playerCurrencyTotal[whichPlayer]
else
return not playerTalentIsEnabled[whichPlayer][whichTalent]
and HasAllRequirements(whichTalent, whichPlayer)
and (talentRow[whichTalent] - 1)*POINTS_IN_TREE_REQUIRED_PER_ROW <= playerPointsInTree[whichPlayer][talentParentTree[whichTalent]]
and (talentRow[whichTalent] - 1)*POINTS_TOTAL_REQUIRED_PER_ROW <= playerPointsTotal[whichPlayer]
end
end
local function DisableTalent(whichTalent, whichPlayer)
BlzFrameSetEnable(talentFrame[whichTalent], false)
if playerUnspentPoints[whichPlayer] == 0 and playerPointsInTalent[whichPlayer][whichTalent] == 0 then
BlzFrameSetVisible(talentRankBoxFrame[whichTalent], false)
BlzFrameSetVisible(talentRankFrame[whichTalent], false)
if talentRequirement[whichTalent] then
for k = 1, #talentRequirement[whichTalent] do
if talentArrowHeadFrame[whichTalent][k] then
BlzFrameSetTexture(talentArrowHeadFrame[whichTalent][k], "TalentArrowHead" .. talentRequirementTexture[whichTalent][k] .. "Disabled.blp", 0, true)
BlzFrameSetTexture(talentArrowBodyFrame[whichTalent][k], "TalentArrowBody" .. talentRequirementTexture[whichTalent][k] .. "Disabled.blp", 0, true)
end
end
end
elseif playerPointsInTalent[whichPlayer][whichTalent] < talentMaxPoints[whichTalent] then
BlzFrameSetTexture(talentIconDisabledFrame[whichTalent], talentIconPath[whichTalent], 0, true)
end
end
---@param whichPlayer player
local function DisableButtonsOnLastPointSpent(whichPlayer)
if localPlayer == whichPlayer then
for __, tree in ipairs(playerTalentTrees[whichPlayer]) do
for i = 1, NUMBER_OF_TALENT_COLUMNS do
for j = 1, NUMBER_OF_TALENT_ROWS do
if talentFromPosition[tree][i][j] then
DisableTalent(talentFromPosition[tree][i][j], whichPlayer)
end
end
end
end
end
end
---@param whichPlayer player
local function EnableButtonsOnFirstPointGained(whichPlayer)
local talent
if playerTalentTrees[whichPlayer] == nil then
return
end
if localPlayer == whichPlayer then
for __, tree in ipairs(playerTalentTrees[whichPlayer]) do
for i = 1, NUMBER_OF_TALENT_COLUMNS do
for j = 1, NUMBER_OF_TALENT_ROWS do
talent = talentFromPosition[tree][i][j]
if talent then
if playerTalentIsEnabled[whichPlayer][talent] then
if playerPointsInTalent[whichPlayer][talent] == 0 then
BlzFrameSetEnable(talentFrame[talent], true)
BlzFrameSetVisible(talentRankBoxFrame[talent], true)
BlzFrameSetVisible(talentRankFrame[talent], true)
elseif playerPointsInTalent[whichPlayer][talent] < talentMaxPoints[talent] then
BlzFrameSetEnable(talentFrame[talent], true)
BlzFrameSetTexture(talentIconDisabledFrame[talent], GetTalentDisabledIconPath(talentIconPath[talent]:gsub(".blp", "")), 0, true)
end
if talentRequirement[talent] then
for index, requirement in ipairs(talentRequirement[talent]) do
if talentArrowHeadFrame[talent][index] and playerPointsInTalent[whichPlayer][requirement] == talentMaxPoints[requirement] then
BlzFrameSetTexture(talentArrowHeadFrame[talent][index], "TalentArrowHead" .. talentRequirementTexture[talent][index] .. ".blp", 0, true)
BlzFrameSetTexture(talentArrowBodyFrame[talent][index], "TalentArrowBody" .. talentRequirementTexture[talent][index] .. ".blp", 0, true)
end
end
end
else
BlzFrameSetVisible(talentRankBoxFrame[talent], true)
BlzFrameSetVisible(talentRankFrame[talent], true)
end
end
end
end
end
end
end
---@param whichPlayer player
---@param whichTree string
local function SetBottomBarText(whichPlayer, whichTree)
if localPlayer == whichPlayer then
BlzFrameSetText(talentTreePointsSpentText[whichTree] , BOTTOM_LEFT_TEXT
:gsub("!TREE!", whichTree)
:gsub("!TREEPOINTS!", playerPointsInTree[whichPlayer][whichTree])
:gsub("!TREECURRENCY!", playerCurrencyInTree[whichPlayer][whichTree])
:gsub("!TOTALPOINTS!", playerPointsTotal[whichPlayer])
:gsub("!TOTALCURRENCY!", playerCurrencyTotal[whichPlayer])
:gsub("!UNSPENTPOINTS!", playerUnspentPoints[whichPlayer]))
BlzFrameSetText(talentTreeUnspentPointsText[whichTree] , BOTTOM_RIGHT_TEXT
:gsub("!TREE!", whichTree)
:gsub("!TREEPOINTS!", playerPointsInTree[whichPlayer][whichTree])
:gsub("!TREECURRENCY!", playerCurrencyInTree[whichPlayer][whichTree])
:gsub("!TOTALPOINTS!", playerPointsTotal[whichPlayer])
:gsub("!TOTALCURRENCY!", playerCurrencyTotal[whichPlayer])
:gsub("!UNSPENTPOINTS!", playerUnspentPoints[whichPlayer]))
end
end
---@param whichPlayer player
---@param whichTree string
---@param whichNavigator? framehandle
local function SwitchToTree(whichPlayer, whichTree, whichNavigator)
if localPlayer == whichPlayer then
if whichNavigator then
BlzFrameSetEnable(whichNavigator, false)
BlzFrameSetEnable(whichNavigator, true)
local oldTree = currentTalentTree[whichPlayer]
if oldTree then
BlzFrameSetPoint(treeNavigatorFrame[oldTree], FRAMEPOINT_BOTTOMLEFT, talentBackdrop, FRAMEPOINT_BOTTOMLEFT, TREE_NAVIGATOR_HORIZONTAL_SHIFT + (treeNavigatorPosition[oldTree] - 1)*(TREE_NAVIGATOR_WIDTH - TREE_NAVIGATOR_OVERLAP), TREE_NAVIGATOR_VERTICAL_SHIFT - TREE_NAVIGATOR_HEIGHT)
BlzFrameSetPoint(treeNavigatorFrame[oldTree], FRAMEPOINT_TOPRIGHT, talentBackdrop, FRAMEPOINT_BOTTOMLEFT, TREE_NAVIGATOR_HORIZONTAL_SHIFT + treeNavigatorPosition[oldTree]*TREE_NAVIGATOR_WIDTH - (treeNavigatorPosition[oldTree] - 1)*TREE_NAVIGATOR_OVERLAP, TREE_NAVIGATOR_VERTICAL_SHIFT)
BlzFrameSetLevel(treeNavigatorFrame[oldTree], treeNavigatorPosition[oldTree] - 1)
BlzFrameSetText(treeNavigatorTitles[oldTree], TREE_NAVIGATOR_UNSELECTED_COLOR .. oldTree .. "|r")
BlzFrameSetScale(treeNavigatorTitles[oldTree], TREE_NAVIGATOR_FONT_SIZE/10)
BlzFrameSetVisible(talentTreeParentFrame[oldTree], false)
BlzFrameSetEnable(treeNavigatorFrame[oldTree], true)
end
BlzFrameSetPoint(treeNavigatorFrame[whichTree], FRAMEPOINT_BOTTOMLEFT, talentBackdrop, FRAMEPOINT_BOTTOMLEFT, TREE_NAVIGATOR_HORIZONTAL_SHIFT + (treeNavigatorPosition[whichTree] - 1)*(TREE_NAVIGATOR_WIDTH - TREE_NAVIGATOR_OVERLAP) - TREE_NAVIGATOR_SELECTED_SIZE_INCREASE, TREE_NAVIGATOR_VERTICAL_SHIFT - TREE_NAVIGATOR_HEIGHT - TREE_NAVIGATOR_SELECTED_SIZE_INCREASE)
BlzFrameSetPoint(treeNavigatorFrame[whichTree], FRAMEPOINT_TOPRIGHT, talentBackdrop, FRAMEPOINT_BOTTOMLEFT, TREE_NAVIGATOR_HORIZONTAL_SHIFT + treeNavigatorPosition[whichTree]*TREE_NAVIGATOR_WIDTH - (treeNavigatorPosition[whichTree] -1)*TREE_NAVIGATOR_OVERLAP + TREE_NAVIGATOR_SELECTED_SIZE_INCREASE, TREE_NAVIGATOR_VERTICAL_SHIFT)
BlzFrameSetLevel(treeNavigatorFrame[whichTree], treeNavigatorPosition[whichTree] + 1)
BlzFrameSetText(treeNavigatorTitles[whichTree], TREE_NAVIGATOR_SELECTED_COLOR .. whichTree .. "|r")
BlzFrameSetScale(treeNavigatorTitles[whichTree], (TREE_NAVIGATOR_FONT_SIZE/10)*(TREE_NAVIGATOR_WIDTH + 2*TREE_NAVIGATOR_SELECTED_SIZE_INCREASE)/TREE_NAVIGATOR_WIDTH)
if oldTree then
BlzFrameSetVisible(talentTreeParentFrame[whichTree], true)
end
BlzFrameSetEnable(treeNavigatorFrame[whichTree], false)
end
SetBottomBarText(whichPlayer, whichTree)
currentTalentTree[whichPlayer] = whichTree
end
end
local function OnNavigatorClick()
local whichPlayer = GetTriggerPlayer()
local whichNavigator = BlzGetTriggerFrame()
local whichTree = treeFromNavigator[whichNavigator]
SwitchToTree(whichPlayer, whichTree, whichNavigator)
end
---@param whichTalent string
---@param whichPlayer player
local function EnableTalent(whichTalent, whichPlayer)
playerTalentIsEnabled[whichPlayer][whichTalent] = true
if localPlayer == whichPlayer then
if playerPointsInTalent[whichPlayer][whichTalent] < talentMaxPoints[whichTalent] then
BlzFrameSetEnable(talentFrame[whichTalent], true)
BlzFrameSetText(talentRankFrame[whichTalent], AVAILABLE_COLOR .. playerPointsInTalent[whichPlayer][whichTalent] .. "/" .. talentMaxPoints[whichTalent])
end
if talentArrowHeadFrame[whichTalent] then
for index, __ in ipairs(talentRequirement[whichTalent]) do
BlzFrameSetTexture(talentArrowHeadFrame[whichTalent][index], "TalentArrowHead" .. talentRequirementTexture[whichTalent][index] .. ".blp", 0, true)
BlzFrameSetTexture(talentArrowBodyFrame[whichTalent][index], "TalentArrowBody" .. talentRequirementTexture[whichTalent][index] .. ".blp", 0, true)
end
end
end
if talentSuccessors[whichTalent] then
for __, successor in ipairs(talentSuccessors[whichTalent]) do
if ShouldTalentBeEnabled(successor, whichPlayer) then
EnableTalent(successor, whichPlayer)
end
end
end
end
---@param whichPlayer player
---@param whichTalent string
---@return string
local function GetRequirementsText(whichPlayer, whichTalent)
local text = ""
local whichTree = talentParentTree[whichTalent]
if talentRequirement[whichTalent] then
for __, requirement in ipairs(talentRequirement[whichTalent]) do
if requirement and playerPointsInTalent[whichPlayer][requirement] < talentMaxPoints[requirement] then
text = text .. REQUIREMENT_NOT_MET_TEXT:gsub("!REQUIREMENT!", requirement):gsub("!MAXPOINTS!", talentMaxPoints[requirement]):gsub("\x25(s\x25)", talentMaxPoints[requirement] > 1 and "s" or "") .. "|n"
end
end
end
local pointsRequired = (talentRow[whichTalent] - 1)*POINTS_IN_TREE_REQUIRED_PER_ROW
if (REQUIRE_CURRENCY_INSTEAD_OF_POINTS and pointsRequired > playerCurrencyInTree[whichPlayer][whichTree]) or (not REQUIRE_CURRENCY_INSTEAD_OF_POINTS and pointsRequired > playerPointsInTree[whichPlayer][whichTree]) then
text = text .. NOT_ENOUGH_POINTS_IN_TREE_TEXT:gsub("!TREEPOINTS!", pointsRequired):gsub("!TREE!", whichTree):gsub("\x25(s\x25)", pointsRequired > 1 and "s" or "") .. "|n"
end
pointsRequired = (talentRow[whichTalent] - 1)*POINTS_TOTAL_REQUIRED_PER_ROW
if (REQUIRE_CURRENCY_INSTEAD_OF_POINTS and pointsRequired > playerCurrencyTotal[whichPlayer]) or (not REQUIRE_CURRENCY_INSTEAD_OF_POINTS and pointsRequired > playerPointsTotal[whichPlayer]) then
text = text .. NOT_ENOUGH_POINTS_TOTAL_TEXT:gsub("!TOTALPOINTS!", pointsRequired):gsub("\x25(s\x25)", pointsRequired > 1 and "s" or "") .. "|n"
end
return text
end
local function GetAffectsText(whichTalent)
if not ADD_AFFECTED_OBJECTS_TO_TOOLTIP then
return ""
end
if talentAffects[whichTalent] then
local text = "|n|n" .. AFFECTS_HEADER
for __, value in ipairs(talentAffects[whichTalent]) do
text = text .. "|n" .. AFFECTED_ITEM_COLOR .. GetObjectName(value) .. "|r"
end
return text
else
return ""
end
end
---@param whichPlayer player
---@param whichTalent string
---@return string
local function GetTalentTooltip(whichPlayer, whichTalent)
local currentLevel = playerPointsInTalent[whichPlayer][whichTalent]
local maxPoints = talentMaxPoints[whichTalent]
local requirementsText = playerTalentRequirementText[whichPlayer][whichTalent]
local overwrite = playerTalentOverwriteText[whichPlayer][whichTalent]
if talentTooltip[whichTalent][0] and currentLevel == 0 then
return ((requirementsText and requirementsText .. "|n" .. (overwrite and "" or GetRequirementsText(whichPlayer, whichTalent))) or GetRequirementsText(whichPlayer, whichTalent)) .. TALENT_TITLE:gsub("!TREE!", talentParentTree[whichTalent]) .. "|n" .. NEXT_RANK_TEXT:gsub("!POINTS!", 1):gsub("!MAXPOINTS!", maxPoints) .. "|n" .. talentTooltip[whichTalent][0] .. GetAffectsText(whichTalent)
elseif maxPoints == 1 then
return ((requirementsText and requirementsText .. "|n" .. (overwrite and "" or GetRequirementsText(whichPlayer, whichTalent))) or GetRequirementsText(whichPlayer, whichTalent)) .. TALENT_TITLE:gsub("!TREE!", talentParentTree[whichTalent]) .. "|n" .. talentTooltip[whichTalent][1] .. GetAffectsText(whichTalent)
elseif currentLevel == maxPoints then
return ((requirementsText and requirementsText .. "|n" .. (overwrite and "" or GetRequirementsText(whichPlayer, whichTalent))) or GetRequirementsText(whichPlayer, whichTalent)) .. TALENT_TITLE:gsub("!TREE!", talentParentTree[whichTalent]) .. "|n" .. CURRENT_RANK_TEXT:gsub("!POINTS!", currentLevel):gsub("!MAXPOINTS!", maxPoints) .. "|n" .. talentTooltip[whichTalent][maxPoints] .. GetAffectsText(whichTalent)
elseif currentLevel == 0 then
return ((requirementsText and requirementsText .. "|n" .. (overwrite and "" or GetRequirementsText(whichPlayer, whichTalent))) or GetRequirementsText(whichPlayer, whichTalent)) .. TALENT_TITLE:gsub("!TREE!", talentParentTree[whichTalent]) .. "|n" .. NEXT_RANK_TEXT:gsub("!POINTS!", 1):gsub("!MAXPOINTS!", maxPoints) .. "|n" .. talentTooltip[whichTalent][1] .. GetAffectsText(whichTalent)
else
return ((requirementsText and requirementsText .. "|n" .. (overwrite and "" or GetRequirementsText(whichPlayer, whichTalent))) or GetRequirementsText(whichPlayer, whichTalent)) .. TALENT_TITLE:gsub("!TREE!", talentParentTree[whichTalent]) .. "|n" .. CURRENT_RANK_TEXT:gsub("!POINTS!", currentLevel):gsub("!MAXPOINTS!", maxPoints) .. "|n" .. talentTooltip[whichTalent][currentLevel] .. "|n|n" .. NEXT_RANK_TEXT:gsub("!POINTS!", currentLevel + 1):gsub("!MAXPOINTS!", maxPoints) .. "|n" .. talentTooltip[whichTalent][currentLevel + 1] .. GetAffectsText(whichTalent)
end
end
---@param whichPlayer player
---@param whichTalent string
local function SetTalentTooltip(whichPlayer, whichTalent)
if localPlayer == whichPlayer then
BlzFrameSetText(talentTooltipTextFrame[whichTalent], GetTalentTooltip(whichPlayer, whichTalent))
BlzFrameSetSize(talentTooltipTextFrame[whichTalent], 0.28, 0.0)
BlzFrameSetSize(talentTooltipFrame[whichTalent], 0.29, BlzFrameGetHeight(talentTooltipTextFrame[whichTalent]) + TALENT_TOOLTIP_HEIGHT_BUFFER)
end
end
---@param whichPlayer player
---@param whichTalent string
local function ResetTalent(whichPlayer, whichTalent)
if localPlayer == whichPlayer then
BlzFrameSetEnable(talentFrame[whichTalent], false)
BlzFrameSetTexture(talentIconDisabledFrame[whichTalent], GetTalentDisabledIconPath(talentIconPath[whichTalent]:gsub(".blp", "")), 0, true)
BlzFrameSetVisible(talentRankBoxFrame[whichTalent], true)
BlzFrameSetVisible(talentRankFrame[whichTalent], true)
BlzFrameSetText(talentRankFrame[whichTalent], UNAVAILABLE_COLOR .. "0/" .. talentMaxPoints[whichTalent] .. "|r")
if VARIABLE_TALENT_COST then
BlzFrameSetVisible(talentTooltipIconFrame[whichTalent], true)
BlzFrameSetVisible(talentTooltipCostFrame[whichTalent], true)
end
if talentArrowHeadFrame[whichTalent] then
for i = 1, #talentRequirement[whichTalent] do
if talentArrowHeadFrame[whichTalent][i] then
BlzFrameSetTexture(talentArrowHeadFrame[whichTalent][i], "TalentArrowHead" .. talentRequirementTexture[whichTalent][i] .. "Disabled.blp", 0, true)
BlzFrameSetTexture(talentArrowBodyFrame[whichTalent][i], "TalentArrowBody" .. talentRequirementTexture[whichTalent][i] .. "Disabled.blp", 0, true)
end
end
end
end
SetTalentTooltip(whichPlayer, whichTalent)
end
---@param whichPlayer player
---@param whichTree string
---@param whichRow integer
local function EnableRow(whichPlayer, whichTree, whichRow)
local talent
for i = 1, NUMBER_OF_TALENT_COLUMNS do
talent = talentFromPosition[whichTree][i][whichRow]
if talent then
SetTalentTooltip(whichPlayer, talent)
if ShouldTalentBeEnabled(talent, whichPlayer) then
EnableTalent(talent, whichPlayer)
end
end
end
end
---@param whichPlayer player
local function CheckRowsForEnabledTalents(whichPlayer)
for __, tree in ipairs(playerTalentTrees[whichPlayer]) do
if REQUIRE_CURRENCY_INSTEAD_OF_POINTS then
while playerCurrencyInTree[whichPlayer][tree] >= playerRowsInTreeEnabled[whichPlayer][tree]*POINTS_IN_TREE_REQUIRED_PER_ROW and playerCurrencyTotal[whichPlayer] >= playerRowsInTreeEnabled[whichPlayer][tree]*POINTS_TOTAL_REQUIRED_PER_ROW do
playerRowsInTreeEnabled[whichPlayer][tree] = playerRowsInTreeEnabled[whichPlayer][tree] + 1
EnableRow(whichPlayer, tree, playerRowsInTreeEnabled[whichPlayer][tree])
end
else
while playerPointsInTree[whichPlayer][tree] >= playerRowsInTreeEnabled[whichPlayer][tree]*POINTS_IN_TREE_REQUIRED_PER_ROW and playerPointsTotal[whichPlayer] >= playerRowsInTreeEnabled[whichPlayer][tree]*POINTS_TOTAL_REQUIRED_PER_ROW do
playerRowsInTreeEnabled[whichPlayer][tree] = playerRowsInTreeEnabled[whichPlayer][tree] + 1
EnableRow(whichPlayer, tree, playerRowsInTreeEnabled[whichPlayer][tree])
end
end
end
end
---@param whichPlayer player
---@param whichTalent string
local function SelectTalent(whichPlayer, whichTalent)
if playerPointsInTalent[whichPlayer][whichTalent] == talentMaxPoints[whichTalent] then
return
end
local whichTree = talentParentTree[whichTalent]
if REQUIRE_CURRENCY_INSTEAD_OF_POINTS then
local cost = GetTalentCost(whichTalent, talentMaxPoints[whichTalent], playerPointsInTalent[whichPlayer][whichTalent])
playerCurrencyInTree[whichPlayer][whichTree] = playerCurrencyInTree[whichPlayer][whichTree] + cost
playerCurrencyTotal[whichPlayer] = playerCurrencyTotal[whichPlayer]
end
playerPointsInTalent[whichPlayer][whichTalent] = playerPointsInTalent[whichPlayer][whichTalent] + 1
playerPointsInTree[whichPlayer][whichTree] = playerPointsInTree[whichPlayer][whichTree] + 1
playerPointsTotal[whichPlayer] = playerPointsTotal[whichPlayer] + 1
if talentOnLearn[whichTalent] then
talentOnLearn[whichTalent](whichPlayer, whichTalent, whichTree, playerPointsInTalent[whichPlayer][whichTalent] - 1, playerPointsInTalent[whichPlayer][whichTalent])
end
if GLOBAL_CALLBACK then
GLOBAL_CALLBACK(whichPlayer, whichTalent, whichTree, playerPointsInTalent[whichPlayer][whichTalent] - 1, playerPointsInTalent[whichPlayer][whichTalent])
end
if playerPointsInTalent[whichPlayer][whichTalent] == talentMaxPoints[whichTalent] then
if localPlayer == whichPlayer then
BlzFrameSetEnable(talentFrame[whichTalent], false)
BlzFrameSetTexture(talentIconDisabledFrame[whichTalent], GetTalentMaxedIconPath(talentIconPath[whichTalent]:gsub(".blp", "")), 0, true)
BlzFrameSetText(talentRankFrame[whichTalent], MAXED_COLOR .. talentMaxPoints[whichTalent] .. "/" .. talentMaxPoints[whichTalent] .. "|r")
BlzFrameSetVisible(talentTooltipIconFrame[whichTalent], false)
BlzFrameSetVisible(talentTooltipCostFrame[whichTalent], false)
end
if talentSuccessors[whichTalent] then
for __, successor in ipairs(talentSuccessors[whichTalent]) do
SetTalentTooltip(whichPlayer, successor)
if ShouldTalentBeEnabled(successor, whichPlayer) then
EnableTalent(successor, whichPlayer)
end
end
end
else
if localPlayer == whichPlayer then
BlzFrameSetText(talentRankFrame[whichTalent], AVAILABLE_COLOR .. playerPointsInTalent[whichPlayer][whichTalent] .. "/" .. talentMaxPoints[whichTalent] .. "|r")
if VARIABLE_TALENT_COST then
BlzFrameSetText(talentTooltipCostFrame[whichTalent], tostring(GetTalentCost(whichTalent, talentMaxPoints[whichTalent], playerPointsInTalent[whichPlayer][whichTalent])))
end
end
end
SetTalentTooltip(whichPlayer, whichTalent)
SetBottomBarText(whichPlayer, whichTree)
CheckRowsForEnabledTalents(whichPlayer)
if not VARIABLE_TALENT_COST and playerUnspentPoints[whichPlayer] == 0 then
DisableButtonsOnLastPointSpent(whichPlayer)
end
end
local function TalentOnClick()
local whichFrame = BlzGetTriggerFrame()
local whichTalent = talentFromFrame[whichFrame]
local whichPlayer = GetTriggerPlayer()
if localPlayer == whichPlayer then
BlzFrameSetEnable(whichFrame, false)
end
if not playerTalentIsEnabled[whichPlayer][whichTalent] then
return
end
if localPlayer == whichPlayer then
BlzFrameSetEnable(whichFrame, true)
end
local cost = VARIABLE_TALENT_COST and GetTalentCost(whichTalent, talentMaxPoints[whichTalent], playerPointsInTalent[whichPlayer][whichTalent]) or 1
if playerUnspentPoints[whichPlayer] < cost then
return
else
playerUnspentPoints[whichPlayer] = playerUnspentPoints[whichPlayer] - cost
end
SelectTalent(whichPlayer, whichTalent)
end
--===========================================================================================================================================================
--Init
--===========================================================================================================================================================
---Returns textureType, anchorFramepoint, dxBottomLeftHead, dyBottomLeftHead, dxTopRightHead, dyTopRightHead, dxBottomLeftBody, dyBottomLeftBody, dxTopRightBody, dyTopRightBody
---@param whichTalent string
---@param requirement string
---@return string|nil, framepointtype|nil, number|nil, number|nil, number|nil, number|nil, number | nil, number|nil, number|nil, number|nil
local function GetArrowFrameParameters(whichTalent, requirement)
if talentRow[whichTalent] == talentRow[requirement] then
if talentColumn[whichTalent] == talentColumn[requirement] - 1 then
return "Left", FRAMEPOINT_BOTTOMRIGHT,
-REQUIREMENT_ARROW_BUTTON_OVERLAP, TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2,
-REQUIREMENT_ARROW_BUTTON_OVERLAP + REQUIREMENT_ARROW_WIDTH, TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2,
-REQUIREMENT_ARROW_BUTTON_OVERLAP + REQUIREMENT_ARROW_WIDTH, TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2,
TALENT_HORIZONTAL_SPACING - TALENT_BUTTON_SIZE, TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2
elseif talentColumn[whichTalent] == talentColumn[requirement] + 1 then
return "Right", FRAMEPOINT_BOTTOMLEFT,
REQUIREMENT_ARROW_BUTTON_OVERLAP - REQUIREMENT_ARROW_WIDTH, TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2,
REQUIREMENT_ARROW_BUTTON_OVERLAP, TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2,
-TALENT_HORIZONTAL_SPACING + TALENT_BUTTON_SIZE, TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2,
REQUIREMENT_ARROW_BUTTON_OVERLAP - REQUIREMENT_ARROW_WIDTH, TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2
end
elseif talentColumn[whichTalent] == talentColumn[requirement] then
if TALENT_TREE_BOTTOM_TO_TOP then
if talentRow[whichTalent] == talentRow[requirement] + 1 then
return "Up", FRAMEPOINT_BOTTOMLEFT,
TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2, REQUIREMENT_ARROW_BUTTON_OVERLAP - REQUIREMENT_ARROW_WIDTH,
TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2, REQUIREMENT_ARROW_BUTTON_OVERLAP,
TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2, -TALENT_VERTICAL_SPACING + TALENT_BUTTON_SIZE,
TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2, REQUIREMENT_ARROW_BUTTON_OVERLAP - REQUIREMENT_ARROW_WIDTH
elseif talentRow[whichTalent] == talentRow[requirement] + 2 then
return "Up", FRAMEPOINT_BOTTOMLEFT,
TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2, REQUIREMENT_ARROW_BUTTON_OVERLAP - REQUIREMENT_ARROW_WIDTH,
TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2, REQUIREMENT_ARROW_BUTTON_OVERLAP,
TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2, -2*TALENT_VERTICAL_SPACING + TALENT_BUTTON_SIZE - REQUIREMENT_ARROW_WIDTH,
TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2, REQUIREMENT_ARROW_BUTTON_OVERLAP
end
else
if talentRow[whichTalent] == talentRow[requirement] + 1 then
return "Down", FRAMEPOINT_TOPLEFT,
TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2, -REQUIREMENT_ARROW_BUTTON_OVERLAP,
TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2, -REQUIREMENT_ARROW_BUTTON_OVERLAP + REQUIREMENT_ARROW_WIDTH,
TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2, -REQUIREMENT_ARROW_BUTTON_OVERLAP + REQUIREMENT_ARROW_WIDTH,
TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2, TALENT_VERTICAL_SPACING - TALENT_BUTTON_SIZE
elseif talentRow[whichTalent] == talentRow[requirement] + 2 then
return "Down", FRAMEPOINT_TOPLEFT,
TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2, -REQUIREMENT_ARROW_BUTTON_OVERLAP,
TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2, -REQUIREMENT_ARROW_BUTTON_OVERLAP + REQUIREMENT_ARROW_WIDTH,
TALENT_BUTTON_SIZE/2 - REQUIREMENT_ARROW_WIDTH/2, -REQUIREMENT_ARROW_BUTTON_OVERLAP + REQUIREMENT_ARROW_WIDTH,
TALENT_BUTTON_SIZE/2 + REQUIREMENT_ARROW_WIDTH/2, 2*TALENT_VERTICAL_SPACING - TALENT_BUTTON_SIZE
end
end
end
end
---@param whichTree string
local function AddTalentsToTree(whichTree)
local parent = talentTreeParentFrame[whichTree]
for i = 1, NUMBER_OF_TALENT_COLUMNS do
for j = 1, NUMBER_OF_TALENT_ROWS do
local whichTalent = talentFromPosition[whichTree][i][j]
if whichTalent ~= nil then
local x, y
x = TALENT_X_OFFSET + TALENT_HORIZONTAL_SPACING*(i-1)
if TALENT_TREE_BOTTOM_TO_TOP then
y = TALENT_Y_OFFSET + TALENT_VERTICAL_SPACING*(j-1)
else
y = TREE_MENU_HEIGHT - TALENT_BUTTON_SIZE - TALENT_Y_OFFSET - TALENT_VERTICAL_SPACING*(j-1)
end
talentFrame[whichTalent] = BlzCreateFrame("TalentButton", parent, 0, 0)
BlzFrameSetPoint( talentFrame[whichTalent], FRAMEPOINT_BOTTOMLEFT, parent, FRAMEPOINT_BOTTOMLEFT, x, y)
BlzFrameSetPoint( talentFrame[whichTalent], FRAMEPOINT_TOPRIGHT, parent, FRAMEPOINT_BOTTOMLEFT, x + TALENT_BUTTON_SIZE, y + TALENT_BUTTON_SIZE)
BlzTriggerRegisterFrameEvent( talentTrigger, talentFrame[whichTalent], FRAMEEVENT_CONTROL_CLICK)
BlzFrameSetLevel(talentFrame[whichTalent], talentColumn[whichTalent])
talentFromFrame[talentFrame[whichTalent]] = whichTalent
talentIconFrame[whichTalent] = BlzFrameGetChild(talentFrame[whichTalent], 0)
talentIconClickedFrame[whichTalent] = BlzFrameGetChild(talentFrame[whichTalent], 1)
talentIconDisabledFrame[whichTalent] = BlzFrameGetChild(talentFrame[whichTalent], 2)
BlzFrameClearAllPoints(talentIconClickedFrame[whichTalent])
BlzFrameSetPoint(talentIconClickedFrame[whichTalent], FRAMEPOINT_BOTTOMLEFT, talentFrame[whichTalent], FRAMEPOINT_BOTTOMLEFT, 0.001, 0.001)
BlzFrameSetPoint(talentIconClickedFrame[whichTalent], FRAMEPOINT_TOPRIGHT, talentFrame[whichTalent], FRAMEPOINT_TOPRIGHT, -0.001, -0.001)
BlzFrameSetTexture(talentIconFrame[whichTalent], talentIconPath[whichTalent], 0, true)
BlzFrameSetTexture(talentIconClickedFrame[whichTalent], talentIconPath[whichTalent], 0, true)
BlzFrameSetTexture(talentIconDisabledFrame[whichTalent], GetTalentDisabledIconPath(talentIconPath[whichTalent]:gsub(".blp", "")), 0, true)
talentTooltipFrame[whichTalent] = BlzCreateFrame("TalentTooltip", talentFrame[whichTalent], 0, 0)
BlzFrameSetAbsPoint(talentTooltipFrame[whichTalent], FRAMEPOINT_BOTTOMLEFT, math.min(TREE_MENU_X - TREE_MENU_WIDTH/2 + x + TALENT_BUTTON_SIZE, 0.51), TREE_MENU_Y - TREE_MENU_HEIGHT/2 + y + TALENT_BUTTON_SIZE)
BlzFrameSetTooltip(talentFrame[whichTalent], talentTooltipFrame[whichTalent])
BlzFrameSetSize( talentTooltipFrame[whichTalent], 0.29, 0.0)
talentTooltipTitleFrame[whichTalent] = BlzFrameGetChild(talentTooltipFrame[whichTalent], 0)
talentTooltipTextFrame[whichTalent] = BlzFrameGetChild(talentTooltipFrame[whichTalent], 1)
talentTooltipIconFrame[whichTalent] = BlzFrameGetChild(talentTooltipFrame[whichTalent], 2)
talentTooltipCostFrame[whichTalent] = BlzFrameGetChild(talentTooltipFrame[whichTalent], 3)
BlzFrameSetText(talentTooltipTitleFrame[whichTalent], whichTalent)
SetTalentTooltip(localPlayer, whichTalent)
BlzFrameSetSize(talentTooltipTextFrame[whichTalent], 0.28, 0.0)
BlzFrameSetSize(talentTooltipFrame[whichTalent], 0.29, BlzFrameGetHeight(talentTooltipTextFrame[whichTalent]) + TALENT_TOOLTIP_HEIGHT_BUFFER)
talentRankBoxFrame[whichTalent] = BlzFrameGetChild(talentFrame[whichTalent], 3)
BlzFrameSetPoint(talentRankBoxFrame[whichTalent], FRAMEPOINT_BOTTOMLEFT, talentFrame[whichTalent], FRAMEPOINT_BOTTOMRIGHT, -RANK_INDICATOR_HORIZONTAL_OVERLAP, RANK_INDICATOR_VERTICAL_OVERLAP - RANK_INDICATOR_HEIGHT)
BlzFrameSetPoint(talentRankBoxFrame[whichTalent], FRAMEPOINT_TOPRIGHT, talentFrame[whichTalent], FRAMEPOINT_BOTTOMRIGHT, -RANK_INDICATOR_HORIZONTAL_OVERLAP + RANK_INDICATOR_WIDTH, RANK_INDICATOR_VERTICAL_OVERLAP)
talentRankFrame[whichTalent] = BlzFrameGetChild(talentFrame[whichTalent], 4)
BlzFrameSetTextAlignment(talentRankFrame[whichTalent], TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)
BlzFrameSetAllPoints(talentRankFrame[whichTalent], talentRankBoxFrame[whichTalent])
BlzFrameSetScale(talentRankFrame[whichTalent], RANK_INDICATOR_FONT_SIZE/10)
local whichPlayer
for p = 0, 23 do
whichPlayer = Player(p)
if ShouldTalentBeEnabled(whichTalent, whichPlayer) then
BlzFrameSetText(talentRankFrame[whichTalent], AVAILABLE_COLOR .. "0/" .. talentMaxPoints[whichTalent] .. "|r")
playerTalentIsEnabled[whichPlayer][whichTalent] = true
else
BlzFrameSetText(talentRankFrame[whichTalent], UNAVAILABLE_COLOR .. "0/" .. talentMaxPoints[whichTalent] .. "|r")
BlzFrameSetEnable(talentFrame[whichTalent], false)
end
end
if VARIABLE_TALENT_COST then
BlzFrameSetTextAlignment(talentTooltipCostFrame[whichTalent], TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_RIGHT)
BlzFrameSetText(talentTooltipCostFrame[whichTalent], tostring(GetTalentCost(whichTalent, talentMaxPoints[whichTalent], 0)))
else
BlzFrameSetVisible(talentTooltipCostFrame[whichTalent], false)
BlzFrameSetVisible(talentTooltipIconFrame[whichTalent], false)
end
if talentRequirement[whichTalent] then
talentArrowHeadFrame[whichTalent] = {}
talentArrowBodyFrame[whichTalent] = {}
talentRequirementTexture[whichTalent] = {}
for index, requirement in ipairs(talentRequirement[whichTalent]) do
local texture, framepoint, x1h, y1h, x2h, y2h, x1b, y1b, x2b, y2b = GetArrowFrameParameters(whichTalent, requirement)
if texture then
talentRequirementTexture[whichTalent][index] = texture
talentArrowHeadFrame[whichTalent][index] = BlzCreateFrameByType("BACKDROP", "talentArrowHeadFrame", talentFrame[whichTalent], "", 0)
BlzFrameSetTexture(talentArrowHeadFrame[whichTalent][index], "TalentArrowHead" .. texture .. "Disabled.blp", 0, true)
BlzFrameSetPoint(talentArrowHeadFrame[whichTalent][index], FRAMEPOINT_BOTTOMLEFT, talentFrame[whichTalent], framepoint, x1h, y1h)
BlzFrameSetPoint(talentArrowHeadFrame[whichTalent][index], FRAMEPOINT_TOPRIGHT, talentFrame[whichTalent], framepoint, x2h, y2h)
BlzFrameSetEnable(talentArrowHeadFrame[whichTalent][index], false)
talentArrowBodyFrame[whichTalent][index] = BlzCreateFrameByType("BACKDROP", "talentArrowBodyFrame", talentFrame[whichTalent], "", 0)
BlzFrameSetTexture(talentArrowBodyFrame[whichTalent][index], "TalentArrowBody" .. texture .. "Disabled.blp", 0, true)
BlzFrameSetPoint(talentArrowBodyFrame[whichTalent][index], FRAMEPOINT_BOTTOMLEFT, talentFrame[whichTalent], framepoint, x1b, y1b)
BlzFrameSetPoint(talentArrowBodyFrame[whichTalent][index], FRAMEPOINT_TOPRIGHT, talentFrame[whichTalent], framepoint, x2b, y2b)
BlzFrameSetEnable(talentArrowBodyFrame[whichTalent][index], false)
end
end
end
end
end
end
end
---@param whichString string
---@return string
local function ReplaceTooltipStrings(whichString)
for i = 1, #REPLACEMENTS, 2 do
whichString = whichString:gsub(REPLACEMENTS[i], REPLACEMENTS[i+1])
end
return whichString
end
---@param data table
local function CreateTalent(data)
local tree = data.tree
local column = data.column
local row = data.row
local requirement = data.requirement
local learnCallback = data.onLearn
local maxPoints = data.maxPoints or 1
local values = data.values
local fourCC = data.fourCC
local name = data.name
local rawTooltip = data.tooltip
local prelearnTooltip = data.prelearnTooltip
local icon = data.icon
local affects = data.affects
local ability = data.ability
if fourCC then
local abilityId = type(fourCC) == "string" and FourCC(fourCC) or fourCC
name = BlzGetAbilityTooltip(abilityId, 0)
rawTooltip = BlzGetAbilityExtendedTooltip(abilityId, 0)
icon = BlzGetAbilityIcon(abilityId)
end
if not TALENT_TREES[tree] then
print("|cffff0000Warning: |r" .. tree .. " talent tree not recognized.")
return
end
if row > NUMBER_OF_TALENT_ROWS then
print("|cffff0000Warning:|r Talent " .. name .. "'s row is greater than NUMBER_OF_TALENT_ROWS.")
return
end
if column > NUMBER_OF_TALENT_COLUMNS then
print("|cffff0000Warning:|r Talent " .. name .. "'s column is greater than NUMBER_OF_TALENT_COLUMNS.")
return
end
if talentTooltip[name] ~= nil then
print("|cffff0000Warning:|r Talent " .. name .. " multiply declared.")
return
end
if talentFromPosition[tree][column][row] then
print("|cffff0000Warning:|r Multiple talents declared for column " .. column .. ", row " .. row .. " in talent tree " .. tree .. ".")
return
end
if requirement then
if type(requirement) == "table" then
talentRequirement[name] = requirement
else
talentRequirement[name] = {requirement}
end
for __, req in ipairs(talentRequirement[name]) do
if talentTooltip[req] == nil then
print("|cffff0000Warning:|r Requirement talent " .. req .. " not recognized. Defined before requirement?")
return
end
end
end
talentTooltip[name] = {}
if rawTooltip == nil then
print("|cffff0000Warning:|r Invalid ability " .. fourCC .. " specified in RegisterTalent function.")
return
end
rawTooltip = ReplaceTooltipStrings(rawTooltip)
for i = 1, maxPoints do
talentTooltip[name][i] = rawTooltip
end
if prelearnTooltip then
talentTooltip[name][0] = ReplaceTooltipStrings(prelearnTooltip)
end
if values then
talentValues[name] = {}
talentAbility[name] = type(ability) == "string" and FourCC(ability) or ability
local isPercent, isStatic, isFromField, fieldValue
for key, value in pairs(values) do
isPercent = rawTooltip:find("!" .. key .. ",\x25$?\x25\x25") ~= nil
isStatic = rawTooltip:find("!" .. key .. ",\x25\x25?\x25$") ~= nil
isFromField = ability and type(value) == "string" and value:len() == 4
talentValues[name][key] = {}
local v
--Prelearn tooltip is stored at index 0, but uses values of index 1.
for i = 0, maxPoints do
if talentTooltip[name][i] then
v = i < 1 and 1 or i
if type(value) == "table" then
if isPercent then
talentTooltip[name][i] = talentTooltip[name][i]:gsub("!" .. key .. ",\x25\x25!", TOOLTIP_VALUE_COLOR .. math.floor(100*value[v] + 0.5) .. "\x25\x25|r")
else
talentTooltip[name][i] = talentTooltip[name][i]:gsub("!" .. key .. "!", TOOLTIP_VALUE_COLOR .. (math.tointeger(value[v]) or value[v]) .. "|r")
end
talentValues[name][key][i] = value[v]
elseif isFromField then
fieldValue = GetAbilityField(talentAbility[name], value, v)
if isPercent then
talentTooltip[name][i] = talentTooltip[name][i]:gsub("!" .. key .. ",\x25\x25!", TOOLTIP_VALUE_COLOR .. math.floor(100*fieldValue + 0.5) .. "\x25\x25|r")
else
talentTooltip[name][i] = talentTooltip[name][i]:gsub("!" .. key .. "!", TOOLTIP_VALUE_COLOR .. (math.tointeger(fieldValue) or fieldValue) .. "|r")
end
talentValues[name][key][i] = fieldValue
elseif isPercent then
if isStatic then
talentTooltip[name][i] = talentTooltip[name][i]:gsub("!" .. key .. ",\x25\x25\x25$", TOOLTIP_VALUE_COLOR .. math.floor(100*value + 0.5) .. "\x25\x25|r")
talentTooltip[name][i] = talentTooltip[name][i]:gsub("!" .. key .. ",\x25$\x25\x25", TOOLTIP_VALUE_COLOR .. math.floor(100*value + 0.5) .. "\x25\x25|r")
talentValues[name][key][i] = value
else
talentTooltip[name][i] = talentTooltip[name][i]:gsub("!" .. key .. ",\x25\x25!", TOOLTIP_VALUE_COLOR .. math.floor(100*v*value + 0.5) .. "\x25\x25|r")
talentValues[name][key][i] = v*value
end
else
if isStatic then
talentTooltip[name][i] = talentTooltip[name][i]:gsub("!" .. key .. ",\x25$!", TOOLTIP_VALUE_COLOR .. (math.tointeger(value) or value) .. "|r")
talentValues[name][key][i] = value
else
talentTooltip[name][i] = talentTooltip[name][i]:gsub("!" .. key .. "!", TOOLTIP_VALUE_COLOR .. (math.tointeger(v*value) or v*value) .. "|r")
talentValues[name][key][i] = v*value
end
end
end
end
end
end
local id
if affects then
if type(affects) == "table" then
talentAffects[name] = {}
for index, value in ipairs(affects) do
id = type(value) == "string" and FourCC(value) or value
talentAffects[name][index] = id
affectedByTalents[id] = affectedByTalents[id] or {}
table.insert(affectedByTalents[id], name)
end
else
id = type(affects) == "string" and FourCC(affects) or affects
talentAffects[name] = {id}
affectedByTalents[id] = affectedByTalents[id] or {}
table.insert(affectedByTalents[id], name)
end
end
talentFromPosition[tree][column][row] = name
talentIconPath[name] = icon
talentParentTree[name] = tree
talentColumn[name] = column
talentRow[name] = row
talentMaxPoints[name] = maxPoints
if requirement then
for __, req in ipairs(talentRequirement[name]) do
if talentSuccessors[req] == nil then
talentSuccessors[req] = {name}
else
talentSuccessors[req][#talentSuccessors[req] + 1] = name
end
end
end
talentOnLearn[name] = learnCallback
end
---@param treeName string
local function CreateTalentTree(treeName)
talentTreeParentFrame[treeName] = BlzCreateFrame("TalentTreeParentFrame", BlzGetFrameByName("ConsoleUIBackdrop", 0), 0, 0)
BlzFrameSetAbsPoint(talentTreeParentFrame[treeName], FRAMEPOINT_TOPLEFT, TREE_MENU_X - TREE_MENU_WIDTH/2, TREE_MENU_Y + TREE_MENU_HEIGHT/2)
BlzFrameSetAbsPoint(talentTreeParentFrame[treeName], FRAMEPOINT_BOTTOMRIGHT, TREE_MENU_X + TREE_MENU_WIDTH/2, TREE_MENU_Y - TREE_MENU_HEIGHT/2)
BlzFrameSetVisible(talentTreeParentFrame[treeName], false)
BlzFrameSetEnable(talentTreeParentFrame[treeName], false)
BlzFrameSetTexture(talentTreeParentFrame[treeName], "transparentMask.blp", 0, true)
local backdrop = BlzFrameGetChild(talentTreeParentFrame[treeName], 0)
BlzFrameSetTexture(backdrop, "TalentTreeBackground" .. ToPascalCase(treeName), 0, true)
BlzFrameSetPoint(backdrop, FRAMEPOINT_BOTTOMLEFT, talentTreeParentFrame[treeName], FRAMEPOINT_BOTTOMLEFT, BACKGROUND_HORIZONTAL_INSET, BACKGROUND_INSET_BOTTOM)
BlzFrameSetPoint(backdrop, FRAMEPOINT_TOPRIGHT, talentTreeParentFrame[treeName], FRAMEPOINT_TOPRIGHT, -BACKGROUND_HORIZONTAL_INSET, -BACKGROUND_INSET_TOP)
BlzFrameSetEnable(backdrop, false)
local bottomBar = BlzFrameGetChild(talentTreeParentFrame[treeName], 1)
BlzFrameSetPoint(bottomBar, FRAMEPOINT_BOTTOMLEFT, talentTreeParentFrame[treeName], FRAMEPOINT_BOTTOMLEFT, BOTTOM_BAR_HORIZONTAL_INSET, BOTTOM_BAR_VERTICAL_SHIFT)
BlzFrameSetPoint(bottomBar, FRAMEPOINT_TOPRIGHT, talentTreeParentFrame[treeName], FRAMEPOINT_BOTTOMRIGHT, -BOTTOM_BAR_HORIZONTAL_INSET, BOTTOM_BAR_HEIGHT + BOTTOM_BAR_VERTICAL_SHIFT)
BlzFrameSetEnable(bottomBar, false)
local topBar = BlzFrameGetChild(talentTreeParentFrame[treeName], 2)
BlzFrameSetPoint(topBar, FRAMEPOINT_BOTTOMLEFT, talentTreeParentFrame[treeName], FRAMEPOINT_TOPLEFT, TOP_BAR_HORIZONTAL_INSET, TOP_BAR_VERTICAL_SHIFT - TOP_BAR_HEIGHT)
BlzFrameSetPoint(topBar, FRAMEPOINT_TOPRIGHT, talentTreeParentFrame[treeName], FRAMEPOINT_TOPRIGHT, -TOP_BAR_HORIZONTAL_INSET, TOP_BAR_VERTICAL_SHIFT)
BlzFrameSetEnable(topBar, false)
local header = BlzCreateFrameByType("TEXT", "talentMenuHeader", topBar, "", 0)
BlzFrameSetPoint(header, FRAMEPOINT_BOTTOMLEFT, topBar, FRAMEPOINT_BOTTOMLEFT, 0, TOP_BAR_TEXT_VERTICAL_SHIFT)
BlzFrameSetPoint(header, FRAMEPOINT_TOPRIGHT, topBar, FRAMEPOINT_TOPRIGHT, 0, TOP_BAR_TEXT_VERTICAL_SHIFT)
BlzFrameSetTextAlignment(header, TEXT_JUSTIFY_BOTTOM, TEXT_JUSTIFY_CENTER)
BlzFrameSetText(header, TOP_BAR_TEXT)
BlzFrameSetScale(header, TOP_BAR_FONT_SIZE/10)
local closeButton = BlzCreateFrameByType("GLUETEXTBUTTON", "closeButton", header, "ScriptDialogButton", 0)
BlzFrameSetPoint(closeButton, FRAMEPOINT_BOTTOMLEFT, header, FRAMEPOINT_BOTTOMRIGHT, CLOSE_BUTTON_HORIZONTAL_SHIFT - CLOSE_BUTTON_SIZE, CLOSE_BUTTON_VERTICAL_SHIFT)
BlzFrameSetPoint(closeButton, FRAMEPOINT_TOPRIGHT, header, FRAMEPOINT_BOTTOMRIGHT, CLOSE_BUTTON_HORIZONTAL_SHIFT, CLOSE_BUTTON_VERTICAL_SHIFT + CLOSE_BUTTON_SIZE)
local icon = BlzFrameGetChild(closeButton, 0)
local iconClicked = BlzFrameGetChild(closeButton, 1)
BlzFrameSetAllPoints(icon, closeButton)
BlzFrameSetPoint(iconClicked, FRAMEPOINT_BOTTOMLEFT, closeButton, FRAMEPOINT_BOTTOMLEFT, 0.001, 0.001)
BlzFrameSetPoint(iconClicked, FRAMEPOINT_TOPRIGHT, closeButton, FRAMEPOINT_TOPRIGHT, -0.001, -0.001)
BlzFrameSetTexture(icon, CLOSE_BUTTON_TEXTURE, 0, true)
BlzFrameSetTexture(iconClicked, CLOSE_BUTTON_TEXTURE, 0, true)
local closeTrigger = CreateTrigger()
BlzTriggerRegisterFrameEvent(closeTrigger, closeButton, FRAMEEVENT_CONTROL_CLICK)
TriggerAddAction(closeTrigger, PressCloseButton)
local border = BlzFrameGetChild(talentTreeParentFrame[treeName], 3)
BlzFrameSetAbsPoint(border, FRAMEPOINT_TOPLEFT, TREE_MENU_X - TREE_MENU_WIDTH/2, TREE_MENU_Y + TREE_MENU_HEIGHT/2)
BlzFrameSetAbsPoint(border, FRAMEPOINT_BOTTOMRIGHT, TREE_MENU_X + TREE_MENU_WIDTH/2, TREE_MENU_Y - TREE_MENU_HEIGHT/2)
BlzFrameSetEnable(border, false)
talentTreePointsSpentText[treeName] = BlzCreateFrameByType("TEXT", "talentTreePointsSpent" .. treeName, border, "", 0)
BlzFrameSetPoint(talentTreePointsSpentText[treeName], FRAMEPOINT_BOTTOMLEFT, border, FRAMEPOINT_BOTTOMLEFT, BOTTOM_LEFT_TEXT_HORIZONTAL_INSET, BOTTOM_BAR_VERTICAL_SHIFT + BOTTOM_BAR_TEXT_VERTICAL_SHIFT)
BlzFrameSetPoint(talentTreePointsSpentText[treeName], FRAMEPOINT_TOPRIGHT, border, FRAMEPOINT_BOTTOMRIGHT, -BOTTOM_LEFT_TEXT_HORIZONTAL_INSET, BOTTOM_BAR_HEIGHT + BOTTOM_BAR_VERTICAL_SHIFT + BOTTOM_BAR_TEXT_VERTICAL_SHIFT)
BlzFrameSetTextAlignment(talentTreePointsSpentText[treeName], TEXT_JUSTIFY_TOP, TEXT_JUSTIFY_LEFT)
BlzFrameSetEnable(talentTreePointsSpentText[treeName], false)
talentTreeUnspentPointsText[treeName] = BlzCreateFrameByType("TEXT", "talentTreePointsSpent" .. treeName, border, "", 0)
BlzFrameSetPoint(talentTreeUnspentPointsText[treeName], FRAMEPOINT_BOTTOMLEFT, border, FRAMEPOINT_BOTTOMRIGHT, -BOTTOM_RIGHT_TEXT_HORIZONTAL_INSET, BOTTOM_BAR_VERTICAL_SHIFT + BOTTOM_BAR_TEXT_VERTICAL_SHIFT)
BlzFrameSetPoint(talentTreeUnspentPointsText[treeName], FRAMEPOINT_TOPRIGHT, border, FRAMEPOINT_BOTTOMRIGHT, 0, BOTTOM_BAR_HEIGHT + BOTTOM_BAR_VERTICAL_SHIFT + BOTTOM_BAR_TEXT_VERTICAL_SHIFT)
BlzFrameSetTextAlignment(talentTreeUnspentPointsText[treeName], TEXT_JUSTIFY_TOP, TEXT_JUSTIFY_LEFT)
BlzFrameSetEnable(talentTreeUnspentPointsText[treeName], false)
treeNavigatorFrame[treeName] = BlzCreateFrame("TalentNavigator", talentBackdrop, 0, 0)
treeNavigatorTitles[treeName] = BlzFrameGetChild(treeNavigatorFrame[treeName], 3)
BlzFrameSetText(treeNavigatorTitles[treeName], TREE_NAVIGATOR_UNSELECTED_COLOR .. treeName .. "|r")
BlzFrameSetAllPoints(treeNavigatorTitles[treeName], treeNavigatorFrame[treeName])
BlzFrameSetTextAlignment(treeNavigatorTitles[treeName], TEXT_JUSTIFY_MIDDLE, TEXT_JUSTIFY_CENTER)
BlzFrameSetEnable(treeNavigatorTitles[treeName], false)
BlzFrameSetScale(treeNavigatorTitles[treeName], TREE_NAVIGATOR_FONT_SIZE/10)
if BLACK_FADE_CORNER_SIZE > 0 then
local blackFadeFrames = {}
for i = 1, 4 do
blackFadeFrames[i] = BlzCreateFrameByType("BACKDROP", "blackFadeFrame" .. i, talentTreeParentFrame[treeName], "", 0)
BlzFrameSetTexture(blackFadeFrames[i], "TalentTreeFade" .. i .. ".blp", 0, true)
BlzFrameSetEnable(blackFadeFrames[i], false)
end
BlzFrameSetSize(blackFadeFrames[1], BLACK_FADE_CORNER_SIZE, TREE_MENU_HEIGHT - BACKGROUND_INSET_BOTTOM - BACKGROUND_INSET_TOP)
BlzFrameSetPoint(blackFadeFrames[1], FRAMEPOINT_BOTTOMRIGHT, talentTreeParentFrame[treeName], FRAMEPOINT_BOTTOMRIGHT, -BACKGROUND_HORIZONTAL_INSET, BACKGROUND_INSET_BOTTOM)
BlzFrameSetSize(blackFadeFrames[2], TREE_MENU_WIDTH - BACKGROUND_HORIZONTAL_INSET - BACKGROUND_HORIZONTAL_INSET, BLACK_FADE_CORNER_SIZE)
BlzFrameSetPoint(blackFadeFrames[2], FRAMEPOINT_TOPLEFT, talentTreeParentFrame[treeName], FRAMEPOINT_TOPLEFT, BACKGROUND_HORIZONTAL_INSET, -BACKGROUND_INSET_TOP)
BlzFrameSetSize(blackFadeFrames[3], BLACK_FADE_CORNER_SIZE, TREE_MENU_HEIGHT - BACKGROUND_INSET_BOTTOM - BACKGROUND_INSET_TOP)
BlzFrameSetPoint(blackFadeFrames[3], FRAMEPOINT_BOTTOMLEFT, talentTreeParentFrame[treeName], FRAMEPOINT_BOTTOMLEFT, BACKGROUND_HORIZONTAL_INSET, BACKGROUND_INSET_BOTTOM)
BlzFrameSetSize(blackFadeFrames[4], TREE_MENU_WIDTH - BACKGROUND_HORIZONTAL_INSET - BACKGROUND_HORIZONTAL_INSET, BLACK_FADE_CORNER_SIZE)
BlzFrameSetPoint(blackFadeFrames[4], FRAMEPOINT_BOTTOMLEFT, talentTreeParentFrame[treeName], FRAMEPOINT_BOTTOMLEFT, BACKGROUND_HORIZONTAL_INSET, BACKGROUND_INSET_BOTTOM)
end
BlzFrameSetVisible(treeNavigatorFrame[treeName], false)
local trig = CreateTrigger()
BlzTriggerRegisterFrameEvent(trig, treeNavigatorFrame[treeName], FRAMEEVENT_CONTROL_CLICK)
TriggerAddAction(trig, OnNavigatorClick)
treeFromNavigator[treeNavigatorFrame[treeName]] = treeName
end
OnInit.final("TalentTree", function()
localPlayer = GetLocalPlayer()
GetOwner = ALICE_GetOwner or GetOwningPlayer
if not BlzLoadTOCFile("TalentTreeTemplates.toc") then
print("|cffff0000Warning:|r TalentTreeTemplates.toc failed to load.")
return
end
talentBackdrop = BlzCreateFrameByType("FRAME", "heroIconFrame", BlzGetFrameByName("ConsoleUIBackdrop",0), "", 0)
BlzFrameSetAbsPoint(talentBackdrop, FRAMEPOINT_BOTTOMLEFT, TREE_MENU_X - TREE_MENU_WIDTH/2, TREE_MENU_Y - TREE_MENU_HEIGHT/2)
BlzFrameSetAbsPoint(talentBackdrop, FRAMEPOINT_TOPRIGHT, TREE_MENU_X + TREE_MENU_WIDTH/2, TREE_MENU_Y + TREE_MENU_HEIGHT/2)
BlzFrameSetTexture(talentBackdrop, "ReplaceableTextures\\CommandButtons\\BTNHeroPaladin.blp", 0, true)
BlzFrameSetEnable(talentBackdrop, false)
BlzFrameSetVisible(talentBackdrop, false)
talentTrigger = CreateTrigger()
TriggerAddAction(talentTrigger, TalentOnClick)
for i = 1, #TALENT_TREES do
CreateTalentTree(TALENT_TREES[i])
end
local trig = CreateTrigger()
TriggerAddAction(trig, PressCloseButton)
local zeroTable = {__index = function() return 0 end}
local player
for i = 0, 23 do
player = Player(i)
playerPointsInTalent[player] = setmetatable({}, zeroTable)
playerPointsInTree[player] = setmetatable({}, zeroTable)
playerCurrencyInTree[player] = setmetatable({}, zeroTable)
playerRowsInTreeEnabled[player] = setmetatable({}, {__index = function() return 1 end})
playerPointsTotal[player] = 0
playerCurrencyTotal[player] = 0
playerUnspentPoints[player] = 0
playerTalentIsEnabled[player] = {}
playerTalentIsHardDisabled[player] = {}
playerTalentRequirementText[player] = {}
playerTalentOverwriteText[player] = {}
BlzTriggerRegisterPlayerKeyEvent(trig, player, OSKEY_ESCAPE, 0, true)
end
for i = 1, #TALENT_TREES do
AddTalentsToTree(TALENT_TREES[i])
end
buttonPressSound = CreateSound("Sound\\Interface\\BigButtonClick.flac", false, false, false, 0, 0, "DefaultEAXON")
talentTreesInitialized = true
end)
--===========================================================================================================================================================
--API
--===========================================================================================================================================================
CTT = {}
---Registers a talent from the data provided in the data table. The recognized fields for data table are fourCC, name, icon, tooltip, tree, column, row, maxPoints, callback, requirement, and values. For details, see hiveworkshop resource page.
---@param data table
CTT.RegisterTalent = function(data)
if talentTreesInitialized then
print("|cffff0000Warning:|r Register talents only before talent trees are initialized.")
return
end
for key, __ in pairs(data) do
if not RECOGNIZED_FLAGS[key] then
print("|cffff0000Warning:|r Unrecognized flag " .. key .. " in data table passed into RegisterTalent function.")
end
end
for key, __ in ipairs(REQUIRED_FLAGS) do
if not data[key] then
print("|cffff000Warning:|r Required flag " .. key .. " missing in data table passed into RegisterTalent function.")
return
end
end
if not data.fourCC and not (data.name and data.icon and data.tooltip) then
print("|cffff000Warning:|r Data table for RegisterTalent function requires either fourCC or name, tooltip, and icon.")
return
end
CreateTalent(data)
end
---Sets the talent trees for the specified player to the provided list of talent trees. The talent trees will be arranged according to their order in the arguments.
---@param who player | unit
---@vararg string
CTT.SetTrees = function(who, ...)
who = HandleType[who] == "player" and who or GetOwner(who)
if playerTalentTrees[who] == nil then
playerTalentTrees[who] = {}
else
for key, __ in pairs(playerTalentTrees[who]) do
playerTalentTrees[key] = nil
BlzFrameSetVisible(treeNavigatorFrame[key], false)
end
end
local tree
for i = 1, select("#", ...) do
tree = select(i, ...)
if not treeNavigatorFrame[tree] then
print("|cffff0000Warning:|r Attempt to set unrecognized talent tree " .. tree .. " for player.")
return
end
playerTalentTrees[who][i] = tree
BlzFrameSetVisible(treeNavigatorFrame[tree], true)
BlzFrameSetLevel(treeNavigatorFrame[tree], 0)
BlzFrameSetPoint(treeNavigatorFrame[tree], FRAMEPOINT_BOTTOMLEFT, talentBackdrop, FRAMEPOINT_BOTTOMLEFT, TREE_NAVIGATOR_HORIZONTAL_SHIFT + (i-1)*(TREE_NAVIGATOR_WIDTH - TREE_NAVIGATOR_OVERLAP), TREE_NAVIGATOR_VERTICAL_SHIFT - TREE_NAVIGATOR_HEIGHT)
BlzFrameSetPoint(treeNavigatorFrame[tree], FRAMEPOINT_TOPRIGHT, talentBackdrop, FRAMEPOINT_BOTTOMLEFT, TREE_NAVIGATOR_HORIZONTAL_SHIFT + i*TREE_NAVIGATOR_WIDTH - (i-1)*TREE_NAVIGATOR_OVERLAP, TREE_NAVIGATOR_VERTICAL_SHIFT)
if localPlayer == who then
treeNavigatorPosition[tree] = i
end
end
if not VARIABLE_TALENT_COST and playerUnspentPoints[who] == 0 then
DisableButtonsOnLastPointSpent(who)
end
SwitchToTree(who, playerTalentTrees[who][1], treeNavigatorFrame[playerTalentTrees[who][1]])
end
---Adds the provided list of talent trees to the talents trees of the specified player. The talent trees will be arranged according to their order in the arguments.
---@param who player | unit
---@vararg string
CTT.AddTrees = function(who, ...)
who = HandleType[who] == "player" and who or GetOwner(who)
if playerTalentTrees[who] == nil then
playerTalentTrees[who] = {}
end
local tree
for i = 1, select("#", ...) do
tree = select(i, ...)
if not treeNavigatorFrame[tree] then
print("|cffff0000Warning:|r Attempt to set unrecognized talent tree " .. tree .. " for player.")
return
end
playerTalentTrees[who][i] = tree
BlzFrameSetVisible(treeNavigatorFrame[tree], true)
BlzFrameSetLevel(treeNavigatorFrame[tree], 0)
BlzFrameSetPoint(treeNavigatorFrame[tree], FRAMEPOINT_BOTTOMLEFT, talentBackdrop, FRAMEPOINT_BOTTOMLEFT, TREE_NAVIGATOR_HORIZONTAL_SHIFT + (i-1)*(TREE_NAVIGATOR_WIDTH - TREE_NAVIGATOR_OVERLAP), TREE_NAVIGATOR_VERTICAL_SHIFT - TREE_NAVIGATOR_HEIGHT)
BlzFrameSetPoint(treeNavigatorFrame[tree], FRAMEPOINT_TOPRIGHT, talentBackdrop, FRAMEPOINT_BOTTOMLEFT, TREE_NAVIGATOR_HORIZONTAL_SHIFT + i*TREE_NAVIGATOR_WIDTH + (i-1)*TREE_NAVIGATOR_OVERLAP, TREE_NAVIGATOR_VERTICAL_SHIFT)
if localPlayer == who then
treeNavigatorPosition[tree] = i
end
end
if not VARIABLE_TALENT_COST and playerUnspentPoints[who] == 0 then
DisableButtonsOnLastPointSpent(who)
end
if currentTalentTree[who] == nil then
SwitchToTree(who, playerTalentTrees[who][1], treeNavigatorFrame[playerTalentTrees[who][1]])
end
end
---Adds the specified amount, or 1 if not specified, to the player's unspent talent points.
---@param who player | unit
---@param howMany? integer
CTT.GrantPoints = function(who, howMany)
who = HandleType[who] == "player" and who or GetOwner(who)
if not VARIABLE_TALENT_COST and playerUnspentPoints[who] == 0 and howMany > 0 then
EnableButtonsOnFirstPointGained(who)
end
playerUnspentPoints[who] = playerUnspentPoints[who] + (howMany or 1)
if currentTalentTree[who] then
SetBottomBarText(who, currentTalentTree[who])
end
end
---Sets the unspent talent points of the specified player to the provided amount.
---@param who player | unit
---@param howMany integer
CTT.SetPoints = function(who, howMany)
who = HandleType[who] == "player" and who or GetOwner(who)
if not VARIABLE_TALENT_COST and playerUnspentPoints[who] == 0 and howMany > 0 then
EnableButtonsOnFirstPointGained(who)
elseif playerUnspentPoints[who] > 0 and howMany == 0 then
DisableButtonsOnLastPointSpent(who)
end
playerUnspentPoints[who] = howMany
if currentTalentTree[who] then
SetBottomBarText(who, currentTalentTree[who])
end
end
---Selects the specified talent for a player. If the optional subtractCost argument is not set, the player will not lose unspent talent points and all requirements of the talent will be ignored.
---@param who player | unit
---@param whichTalent string
---@param subtractCost? boolean
CTT.ForceSelectTalent = function(who, whichTalent, subtractCost)
who = HandleType[who] == "player" and who or GetOwner(who)
if subtractCost then
local cost = VARIABLE_TALENT_COST and GetTalentCost(whichTalent, talentMaxPoints[whichTalent], playerPointsInTalent[who][whichTalent]) or 1
if playerUnspentPoints[who] < cost then
return
else
playerUnspentPoints[who] = playerUnspentPoints[who] - cost
end
end
SelectTalent(who, whichTalent)
end
---Resets all talents and refunds their cost for the specified player.
---@param who player | unit
CTT.ResetAllTalents = function(who)
who = HandleType[who] == "player" and who or GetOwner(who)
local numPoints = 0
local talent
for __, tree in ipairs(playerTalentTrees[who]) do
for i = 1, NUMBER_OF_TALENT_COLUMNS do
for j = 1, NUMBER_OF_TALENT_ROWS do
talent = talentFromPosition[tree][i][j]
if talent then
if VARIABLE_TALENT_COST then
for k = playerPointsInTalent[who][talent] - 1, 0, -1 do
numPoints = numPoints + GetTalentCost(talent, talentMaxPoints[talent], k)
end
else
numPoints = numPoints + playerPointsInTalent[who][talent]
end
if playerPointsInTalent[who][talent] > 0 and talentOnLearn[talent] then
talentOnLearn[talent](who, talent, tree, playerPointsInTalent[who][talent], 0)
end
playerPointsInTalent[who][talent] = 0
playerTalentIsEnabled[who][talent] = nil
ResetTalent(who, talent)
end
end
end
playerPointsInTree[who][tree] = 0
playerCurrencyInTree[who][tree] = 0
playerRowsInTreeEnabled[who][tree] = 0
end
playerPointsTotal[who] = 0
playerCurrencyTotal[who] = 0
playerUnspentPoints[who] = playerUnspentPoints[who] + numPoints
if who == localPlayer then
for __, tree in ipairs(playerTalentTrees[who]) do
SetBottomBarText(who, tree)
end
end
CheckRowsForEnabledTalents(who)
end
---Returns the amount of unspent talent points the specified player has.
---@param who player | unit
---@return integer
CTT.GetUnspentPoints = function(who)
who = HandleType[who] == "player" and who or GetOwner(who)
return playerUnspentPoints[who]
end
---Returns the rank of the specified talent for a player.
---@param who player | unit
---@param whichTalent string
---@return integer
CTT.GetTalentRank = function(who, whichTalent)
if talentFrame[whichTalent] == nil then
print("|cffff0000Warning:|r Unrecognized talent " .. whichTalent .. ".")
return 0
end
who = HandleType[who] == "player" and who or GetOwner(who)
return playerPointsInTalent[who][whichTalent]
end
---Returns whether a player has one or more talent points invested in the specified talent.
---@param who player | unit
---@param whichTalent string
---@return boolean
CTT.HasTalent = function(who, whichTalent)
if talentFrame[whichTalent] == nil then
print("|cffff0000Warning:|r Unrecognized talent " .. whichTalent .. ".")
return false
end
who = HandleType[who] == "player" and who or GetOwner(who)
return playerPointsInTalent[who][whichTalent] > 0
end
---Returns whether a player has invested the maximum number of points in the specified talent.
---@param who player | unit
---@param whichTalent string
---@return boolean
CTT.IsTalentMaxed = function(who, whichTalent)
if talentFrame[whichTalent] == nil then
print("|cffff0000Warning:|r Unrecognized talent " .. whichTalent .. ".")
return false
end
who = HandleType[who] == "player" and who or GetOwner(who)
return playerPointsInTalent[who][whichTalent] == talentMaxPoints[whichTalent]
end
---Disables the specified talent for a player. The requirements text will be inserted on top of the tooltip while it is disabled. If the optional overwriteRequirements flag is set, the text for other missing requirements will be removed from the tooltip.
---@param who player | unit
---@param whichTalent string
---@param requirementsText? string
---@param overwriteRequirements? boolean
CTT.DisableTalent = function(who, whichTalent, requirementsText, overwriteRequirements)
if talentFrame[whichTalent] == nil then
print("|cffff0000Warning:|r Unrecognized talent " .. whichTalent .. ".")
return
end
who = HandleType[who] == "player" and who or GetOwner(who)
if playerPointsInTalent[who][whichTalent] > 0 then
return
end
if not playerTalentIsHardDisabled[who][whichTalent] then
playerTalentIsHardDisabled[who][whichTalent] = true
playerTalentIsEnabled[who][whichTalent] = nil
BlzFrameSetText(talentRankFrame[whichTalent], UNAVAILABLE_COLOR .. playerPointsInTalent[who][whichTalent] .. "/" .. talentMaxPoints[whichTalent])
DisableTalent(whichTalent, who)
end
playerTalentRequirementText[who][whichTalent] = requirementsText
playerTalentOverwriteText[who][whichTalent] = overwriteRequirements == true
SetTalentTooltip(who, whichTalent)
end
---Enables for the specified player a talent which has been previously disabled with CTT.DisableTalent.
---@param who player | unit
---@param whichTalent string
CTT.EnableTalent = function(who, whichTalent)
if talentFrame[whichTalent] == nil then
print("|cffff0000Warning:|r Unrecognized talent " .. whichTalent .. ".")
return
end
who = HandleType[who] == "player" and who or GetOwner(who)
if not playerTalentIsHardDisabled[who][whichTalent] then
return
end
playerTalentIsHardDisabled[who][whichTalent] = nil
playerTalentRequirementText[who][whichTalent] = nil
playerTalentOverwriteText[who][whichTalent] = nil
SetTalentTooltip(who, whichTalent)
if ShouldTalentBeEnabled(whichTalent, who) then
EnableTalent(whichTalent, who)
end
end
---Returns the number of talent points a player has invested into the specified talent tree.
---@param who player | unit
---@param whichTree string
CTT.GetPointsInTree = function(who, whichTree)
if talentTreeParentFrame[whichTree] == nil then
print("|cffff0000Warning:|r Unrecognized talent tree " .. whichTree .. ".")
return 0
end
who = HandleType[who] == "player" and who or GetOwner(who)
return playerPointsInTree[who][whichTree]
end
---Returns the total number of talent points a player has distributed among talents. Does not include unspent talent points.
---@param who player | unit
---@return integer
CTT.GetTotalPoints = function(who)
who = HandleType[who] == "player" and who or GetOwner(who)
return playerPointsTotal[who]
end
---Returns the value with the whichValue key in the values table stored for the specified talent at the talent rank of a player. Returns 0, "", or false if the talent rank is zero.
---@param who player | unit
---@param whichTalent string
---@param whichValue string
---@return number | string | boolean
CTT.GetValue = function(who, whichTalent, whichValue)
if talentFrame[whichTalent] == nil then
print("|cffff0000Warning:|r Unrecognized talent " .. whichTalent .. ".")
return 0
end
if talentValues[whichTalent][whichValue] == nil then
print("|cffff0000Warning:|r Unrecognized value " .. whichValue .. " for talent " .. whichTalent .. ".")
return 0
end
who = HandleType[who] == "player" and who or GetOwner(who)
local points = playerPointsInTalent[who][whichTalent]
if points == 0 then
local valueType = type(talentValues[whichTalent][whichValue][1])
if valueType == "number" then
return 0
elseif valueType == "string" then
return ""
else
return false
end
else
return talentValues[whichTalent][whichValue][points]
end
end
---Returns the tooltip for the specified talent at the talent rank of a player.
---@param who player | unit
---@param whichTalent any
CTT.GetTooltip = function(who, whichTalent)
if talentFrame[whichTalent] == nil then
print("|cffff0000Warning:|r Unrecognized talent " .. whichTalent .. ".")
return ""
end
who = HandleType[who] == "player" and who or GetOwner(who)
local points = playerPointsInTalent[who][whichTalent]
if points == 0 then
return ""
end
return talentTooltip[whichTalent][points]
end
---Returns the parent tree, the row, and the column of the specified talent.
---@param whichTalent string
---@return string, integer, integer
CTT.GetTalentPosition = function(whichTalent)
if talentFrame[whichTalent] == nil then
print("|cffff0000Warning:|r Unrecognized talent " .. whichTalent .. ".")
return "", 0, 0
end
return talentParentTree[whichTalent], talentRow[whichTalent], talentColumn[whichTalent]
end
---Returns a table containing all talents that affect the specified object. Object can be a fourCC code or id.
---@param object string | integer
---@return table
CTT.AffectedBy = function(object)
local id = type(object) == "string" and FourCC(object) or object
return affectedByTalents[id] or {}
end
---Prints out the optimal image size ratio for the talent tree background images. You can specify either width or height to get the other.
---@param width? integer
---@param height? integer
CTT.GetImageSizeRatio = function(width, height)
local ratio = (TREE_MENU_HEIGHT - BOTTOM_BAR_HEIGHT - TOP_BAR_HEIGHT - BOTTOM_BAR_VERTICAL_SHIFT + TOP_BAR_VERTICAL_SHIFT - 2*BACKGROUND_VERTICAL_INSET) / (TREE_MENU_WIDTH - 2*BACKGROUND_HORIZONTAL_INSET)
local text = "The optimal image size ratio is: |cffffcc00" .. string.format("\x25.3f", ratio) .. "|r."
if width and not height then
text = text .. " At a fixed width of " .. width .. ", this requires a height of |cffffcc00" .. math.floor(width*ratio + 0.5) .. "|r."
elseif height and not width then
text = text .. " At a fixed height of " .. height .. ", this requires a width of |cffffcc00" .. math.floor(height/ratio + 0.5) .. "|r."
elseif height and width then
text = text .. " Do not specify both width and height."
end
print(text)
end
---Opens the talent menu for the specified player, or for all players if no player is specified. Set the optional enable parameter to false to instead force players to close the talent menu.
---@param who? player | unit
---@param enable? boolean
CTT.Show = function(who, enable)
who = who and (HandleType[who] == "player" and who or GetOwner(who))
enable = enable ~= false
if who == nil then
BlzFrameSetVisible(talentBackdrop, enable)
for i = 0, 23 do
who = Player(i)
if currentTalentTree[who] and localPlayer == who then
BlzFrameSetVisible(talentTreeParentFrame[currentTalentTree[who]], enable)
if currentTalentTree[who] then
SetBottomBarText(who, currentTalentTree[who])
end
if #playerTalentTrees[localPlayer] == 1 then
BlzFrameSetVisible(treeNavigatorFrame[playerTalentTrees[localPlayer][1]], false)
end
end
talentTreeOpen[who] = enable
end
else
talentTreeOpen[who] = enable
if who == localPlayer then
BlzFrameSetVisible(talentTreeParentFrame[currentTalentTree[who]], enable)
BlzFrameSetVisible(talentBackdrop, enable)
if currentTalentTree[who] then
SetBottomBarText(who, currentTalentTree[who])
end
if #playerTalentTrees[who] == 1 then
BlzFrameSetVisible(treeNavigatorFrame[playerTalentTrees[who][1]], false)
end
end
end
end
---Returns whether the specified player has the talent tree opened.
---@param who player | unit
---@return boolean
CTT.IsOpened = function(who)
who = who and (HandleType[who] == "player" and who or GetOwner(who))
return talentTreeOpen[who] or false
end
---Returns a table containing a sequence with the ranks of each talent for the specified player as well as the number of unspent talent points as the last entry. Empty slots in the talent trees are included as a 0.
---@param who player | unit
---@return table
CTT.Compile = function(who)
who = HandleType[who] == "player" and who or GetOwner(who)
local returnTable = {}
for __, tree in ipairs(playerTalentTrees[who]) do
for i = 1, NUMBER_OF_TALENT_COLUMNS do
for j = 1, NUMBER_OF_TALENT_ROWS do
returnTable[#returnTable + 1] = playerPointsInTalent[who][talentFromPosition[tree][i][j]] or 0
end
end
end
returnTable[#returnTable + 1] = playerUnspentPoints[who]
return returnTable
end
---Loads the specified player's talent build from the provided data table which has been generated with the Compile function. Returns false if the load was not successful.
---@param who player | unit
---@param data table
---@return boolean
CTT.LoadBuild = function(who, data)
who = HandleType[who] == "player" and who or GetOwner(who)
local expectedLength = #playerTalentTrees[who]*NUMBER_OF_TALENT_COLUMNS*NUMBER_OF_TALENT_ROWS + 1
if expectedLength ~= #data then
return false
end
local index = 1
local mathType = math.type
local entry
for __, tree in ipairs(playerTalentTrees[who]) do
for i = 1, NUMBER_OF_TALENT_COLUMNS do
for j = 1, NUMBER_OF_TALENT_ROWS do
entry = data[index]
if type(entry) ~= "number" or mathType(entry) ~= "integer" or entry < 0 or entry > (talentMaxPoints[talentFromPosition[tree][i][j]] or 0) then
return false
end
index = index + 1
end
end
end
if type(data[#data]) ~= "number" or mathType(data[#data]) ~= "integer" or data[#data] < 0 then
return false
end
CTT.ResetAllTalents(who)
index = 1
for __, tree in ipairs(playerTalentTrees[who]) do
for i = 1, NUMBER_OF_TALENT_COLUMNS do
for j = 1, NUMBER_OF_TALENT_ROWS do
for k = 1, data[index] do
SelectTalent(who, talentFromPosition[tree][i][j])
end
index = index + 1
end
end
end
CTT.SetPoints(who, data[#data])
return true
end
---Loads the talent build from the provided table and refunds all acquired talents for the specified player. This function should be used if the talent build load from the Load function was not successful.
---@param who player | unit
---@param data table
CTT.ResetSavedBuild = function(who, data)
who = HandleType[who] == "player" and who or GetOwner(who)
local unspentTalents = 0
for i = 1, #data do
unspentTalents = unspentTalents + data[i]
end
CTT.ResetAllTalents(who)
CTT.SetPoints(who, unspentTalents)
end
end
if Debug then Debug.endFile() end
do
local counter = 1
function Spawn()
local x, y = GetRandomReal(GetRectMinX(gg_rct_Spawn), GetRectMaxX(gg_rct_Spawn)), GetRandomReal(GetRectMinY(gg_rct_Spawn), GetRectMaxY(gg_rct_Spawn))
local u = CreateUnit(Player(11), FourCC "ugho", x, y, 315)
IssuePointOrder(u, "attack", GetRectCenterX(gg_rct_Gate), GetRectCenterY(gg_rct_Gate))
counter = counter + 1
if ModuloInteger(counter, 20) == 0 and counter >= 40 then
for i = 1, 3 do
u = CreateUnit(Player(11), FourCC "ucry", x, y, 315)
IssuePointOrder(u, "attack", GetRectCenterX(gg_rct_Gate), GetRectCenterY(gg_rct_Gate))
counter = counter + 1
end
end
if counter >= 40 and ModuloInteger(counter, 40) == 10 then
for i = 1, 2 do
u = CreateUnit(Player(11), FourCC "uabo", x, y, 315)
IssuePointOrder(u, "attack", GetRectCenterX(gg_rct_Gate), GetRectCenterY(gg_rct_Gate))
counter = counter + 1
end
end
end
OnInit.final(function()
Require "TalentTree"
CreateFogModifierRectBJ( true, Player(0), FOG_OF_WAR_VISIBLE, GetPlayableMapRect() )
print("Type -spawn to start spawning enemies. Type -giveplx to grant talent points.")
CTT.SetTrees(Player(0), "Lunar", "Archery", "Survival")
HeroOfPlayer = {}
local tyrande = GetPreplacedWidget("Tyrande")
HeroOfPlayer[Player(0)] = tyrande
local trig = CreateTrigger()
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_HERO_LEVEL)
TriggerAddAction(trig, function()
CTT.GrantPoints(GetTriggerUnit(), 1)
end)
trig = CreateTrigger()
TriggerRegisterUnitEvent(trig, tyrande, EVENT_UNIT_SPELL_EFFECT)
TriggerAddAction(trig, function()
if GetSpellAbilityId() == FourCC "Atal" then
CTT.Show(GetOwningPlayer(GetTriggerUnit()))
end
end)
trig = CreateTrigger()
TriggerRegisterUnitEvent(trig, tyrande, EVENT_UNIT_SPELL_EFFECT)
TriggerAddAction(trig, function()
if GetSpellAbilityId() == FourCC "AEsf" then
if CTT.HasTalent(tyrande, "Elune's Protection") then
DummyTargetUnit(tyrande, tyrande, FourCC "Aepd", CTT.GetTalentRank(tyrande, "Elune's Protection"), "unholyfrenzy")
end
end
end)
trig = CreateTrigger()
TriggerRegisterUnitEvent(trig, tyrande, EVENT_UNIT_DAMAGED)
TriggerAddAction(trig, function()
if GetUnitAbilityLevel(tyrande, FourCC "Bepd") > 0 then
BlzSetEventDamage(GetEventDamage()*(1 - CTT.GetValue(tyrande, "Elune's Protection", "reduction")))
end
end)
trig = CreateTrigger()
TriggerRegisterUnitEvent(trig, tyrande, EVENT_UNIT_SPELL_EFFECT)
TriggerAddAction(trig, function()
if GetSpellAbilityId() == FourCC "Apil" then
local x, y = GetSpellTargetX(), GetSpellTargetY()
local pillar = AddSpecialEffect("Abilities\\Spells\\NightElf\\Starfall\\StarfallCaster.mdl", x, y)
local counter = 1
local healEffect
TimerStart(CreateTimer(), 0.1, true, function()
if math.sqrt((GetUnitX(tyrande) - x)^2 + (GetUnitY(tyrande) - y)^2) < 75 then
if healEffect == nil then
healEffect = AddSpecialEffectTarget("Abilities\\Spells\\Other\\ANrm\\ANrmTarget.mdl", tyrande, "origin")
end
SetUnitState(tyrande, UNIT_STATE_LIFE, GetUnitState(tyrande, UNIT_STATE_LIFE) + CTT.GetValue(tyrande, "Starlight Pillar", "healthReg")*(1 + CTT.GetValue(tyrande, "Improved Starlight Pillar", "increase"))/10)
else
if healEffect then
DestroyEffect(healEffect)
healEffect = nil
end
end
counter = counter + 1
if counter >= 150 then
DestroyEffect(pillar)
if healEffect then
DestroyEffect(healEffect)
end
DestroyTimer(GetExpiredTimer())
end
end)
end
end)
trig = CreateTrigger()
TriggerRegisterPlayerChatEvent(trig, Player(0), "-spawn", true)
TriggerAddAction(trig, function()
TimerStart(CreateTimer(), 1.0, true, Spawn)
end)
trig = CreateTrigger()
TriggerRegisterPlayerChatEvent(trig, Player(0), "-giveplx", true)
TriggerAddAction(trig, function()
CTT.GrantPoints(GetTriggerPlayer(), 10)
end)
trig = CreateTrigger()
TriggerRegisterUnitEvent(trig, tyrande, EVENT_UNIT_USE_ITEM)
TriggerAddAction(trig, function()
CTT.ResetAllTalents(GetTriggerUnit())
end)
trig = CreateTrigger()
TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_DAMAGED)
TriggerAddAction(trig, function()
local u = GetEventDamageSource()
if not BlzGetEventIsAttack() and CTT.HasTalent(u, "Starforce") then
BlzSetEventDamage(GetEventDamage()*(1 + CTT.GetValue(u, "Starforce", "increase")))
end
end)
end)
end