Name | Type | is_array | initial_value |
do; local _, codeLoc = pcall(error, "", 2) --get line number where DebugUtils begins.
--[[
-------------------------
-- | Debug Utils 2.3 | --
-------------------------
--> 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 nil 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 nil 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.first()
* - Re-prints the very first error on screen that was thrown during map runtime and prevents further error messages and nil-warnings from being printed afterwards.
* - Useful, if a constant stream of error messages hinders you from reading any of them.
* - IngameConsole will still output error messages afterwards for code executed in it.
* 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 nil-globals |
* ----------------------------
* - DebugUtils will print warnings on screen, if you read any global variable in your code that contains nil.
* - This feature is meant to spot any case where you forgot to initialize a variable with a value or misspelled a variable or function name, such as calling CraeteUnit instead of CreateUnit.
* - By default, warnings are disabled for globals that have been initialized with any value (including nil). I.e. you can disable nil-warnings by explicitly setting MyGlobalVariable = nil. This behaviour can be changed in the settings.
*
* Debug.disableNilWarningsFor(variableName:string)
* - Manually disables nil-warnings for the specified global variable.
* - Variable must be inputted as string, e.g. Debug.disableNilWarningsFor("MyGlobalVariable").
*
* -----------------
* | 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 = {
--Ingame Error Messages
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).
--Ingame Console and -exec
, ALLOW_INGAME_CODE_EXECUTION = true ---Set to true to enable IngameConsole and -exec command.
--Warnings for nil globals
, WARNING_FOR_NIL_GLOBALS = true ---Set to true to print warnings upon accessing nil-globals (i.e. globals containing no value).
, SHOW_TRACE_FOR_NIL_WARNINGS = false ---Set to true to include a stack trace into nil-warnings.
, EXCLUDE_BJ_GLOBALS_FROM_NIL_WARNINGS = false ---Set to true to exclude bj_ variables from nil-warnings.
, EXCLUDE_INITIALIZED_GLOBALS_FROM_NIL_WARNINGS = true ---Set to true to disable warnings for initialized globals, (i.e. nil globals that held a value at some point will be treated intentionally nilled and no longer prompt warnings).
--Print Caching
, USE_PRINT_CACHE = true ---Set to true to let print()-calls during loading screen be cached until the game starts.
, PRINT_DURATION = nil ---@type float Adjusts the duration in seconds that values printed by print() last on screen. Set to nil to use default duration (which depends on string length).
--Name Caching
, 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).
--Colors
, colors = {
error = 'ff5555' ---@type string Color to be applied to error messages
, log = '888888' ---@type string Color to be applied to logged messages through Debug.log().
, nilWarning = 'ffffff' ---@type string Color to be applied to nil-warnings.
}
}
--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.
, globalWarningExclusions = {} ---@type table<string,boolean> contains global variable names that should be excluded from warnings.
, printErrors_yn = true ---@type boolean Set to false to disable error messages. Used by Debug.first.
, firstError = nil ---@type string? contains the first error that was thrown. Used by Debug.first.
}
}
--localization
local settings, paramLog, nameCache, nameDepths, autoIndexedTables, nameCacheMirror, sourceMap, printCache, data = Debug.settings, Debug.data.paramLog, Debug.data.nameCache, Debug.data.nameDepths, Debug.data.autoIndexedTables, Debug.data.nameCacheMirror, Debug.data.sourceMap, Debug.data.printCache, Debug.data
--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)
--Original error message and stack trace.
local toPrint = "|cff" .. settings.colors.error .. "ERROR at " .. errorMsg .. "|r"
if settings.SHOW_TRACE_ON_ERROR then
toPrint = toPrint .. "\n|cff" .. settings.colors.error .. "Traceback (most recent call first):|r\n|cff" .. settings.colors.error .. getStackTrace(startDepth,200) .. "|r"
end
--Also print entries from param log, if there are any.
for location, loggedParams in pairs(paramLog) do
toPrint = toPrint .. "\n|cff" .. settings.colors.log .. "Logged at " .. convertToLocalErrorMsg(location) .. loggedParams .. "|r"
paramLog[location] = nil
end
data.firstError = data.firstError or toPrint
if data.printErrors_yn then --don't print error, if execution of Debug.firstError() has disabled it.
print(toPrint)
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
---Re-prints the very first error that occured during map runtime and prevents any further error messages and nil-warnings from being printed afterwards (for the rest of the game).
---Use this, if a constant stream of error messages hinders you from reading any of them.
---IngameConsole will still output error messages afterwards for code executed in it.
function Debug.first()
data.printErrors_yn = false
print(data.firstError)
end
----------------------------------
--| Undeclared Global Warnings |--
----------------------------------
--Utility function here. _G metatable behaviour is defined in Gamestart section further below.
local globalWarningExclusions = Debug.data.globalWarningExclusions
---Disables nil-warnings for the specified variable.
---@param variableName string
function Debug.disableNilWarningsFor(variableName)
globalWarningExclusions[variableName] = true
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 nil-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
--------------------------------
--| Warnings for Nil Globals |--
--------------------------------
--Exclude initialized globals from warnings, even if initialized with nil.
--These warning exclusions take effect immediately, while the warnings take effect at game start (see code after MarkGameStarted below).
if settings.WARNING_FOR_NIL_GLOBALS and settings.EXCLUDE_INITIALIZED_GLOBALS_FROM_NIL_WARNINGS then
local existingNewIndex = getmetatable(_G).__newindex
local isTable_yn = (type(existingNewIndex) == 'table')
getmetatable(_G).__newindex = function(t,k,v)
--First, exclude the initialized global from future warnings
globalWarningExclusions[k] = true
--Second, execute existing newindex, if there is one in place.
if existingNewIndex then
if isTable_yn then
existingNewIndex[k] = v
else
existingNewIndex(t,k,v)
end
else
rawset(t,k,v)
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_NIL_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.
--Don't show warning, if the variable name has been actively excluded or if it's a bj_ variable (and those are excluded).
if data.printErrors_yn and (not globalWarningExclusions[k]) and ((not settings.EXCLUDE_BJ_GLOBALS_FROM_NIL_WARNINGS) or 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("|cff" .. settings.colors.nilWarning .. "Trying to read nil global at " .. getStackTrace(4,4) .. ": " .. tostring(k) .. "|r"
.. (settings.SHOW_TRACE_FOR_NIL_WARNINGS and "\n|cff" .. settings.colors.nilWarning .. "Traceback (most recent call first):|r\n|cff" .. settings.colors.nilWarning .. getStackTrace(4,200) .. "|r" 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
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 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
OnInit.root( function()
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 "FileIO" end
--[[
FileIO v1a
Provides functionality to read and write files, optimized with lua functionality in mind.
API:
FileIO.Save(filename, data)
- Write string data to a file
FileIO.Load(filename) -> string?
- Read string data from a file. Returns nil if file doesn't exist.
FileIO.SaveAsserted(filename, data, onFail?) -> bool
- Saves the file and checks that it was saved successfully.
If it fails, passes (filename, data, loadResult) to onFail.
FileIO.enabled : bool
- field that indicates that files can be accessed correctly.
Optional requirements:
DebugUtils by Eikonium @ https://www.hiveworkshop.com/threads/330758/
Total Initialization by Bribe @ https://www.hiveworkshop.com/threads/317099/
Inspired by:
- TriggerHappy's Codeless Save and Load @ https://www.hiveworkshop.com/threads/278664/
- ScrewTheTrees's Codeless Save/Sync concept @ https://www.hiveworkshop.com/threads/325749/
- Luashine's LUA variant of TH's FileIO @ https://www.hiveworkshop.com/threads/307568/post-3519040
- HerlySQR's LUA variant of TH's Save/Load @ https://www.hiveworkshop.com/threads/331536/post-3565884
Updated: 8 Mar 2023
--]]
OnInit("FileIO", function()
local RAW_PREFIX = ']]i([['
local RAW_SUFFIX = ']])--[['
local RAW_SIZE = 256 - #RAW_PREFIX - #RAW_SUFFIX
local LOAD_ABILITY = FourCC('ANdc')
local LOAD_EMPTY_KEY = '!@#$, empty data'
local function open(filename)
name = filename
PreloadGenClear()
Preload('")\nendfunction\n//!beginusercode\nlocal p={} local i=function(s) table.insert(p,s) end--[[')
end
local function write(s)
for i = 1, #s, RAW_SIZE do
Preload(RAW_PREFIX .. s:sub(i, i + RAW_SIZE - 1) .. RAW_SUFFIX)
end
end
local function close()
Preload(']]BlzSetAbilityTooltip(' ..
LOAD_ABILITY .. ', table.concat(p), 0)\n//!endusercode\nfunction a takes nothing returns nothing\n//')
PreloadGenEnd(name)
name = nil
end
---
---@param filename string
---@param data string
local function savefile(filename, data)
open(filename)
write(data)
close()
end
---@param filename string
---@return string?
local function loadfile(filename)
local s = BlzGetAbilityTooltip(LOAD_ABILITY, 0)
BlzSetAbilityTooltip(LOAD_ABILITY, LOAD_EMPTY_KEY, 0)
Preloader(filename)
local loaded = BlzGetAbilityTooltip(LOAD_ABILITY, 0)
BlzSetAbilityTooltip(LOAD_ABILITY, s, 0)
if loaded == LOAD_EMPTY_KEY then
return nil
end
return loaded
end
---@param filename string
---@param data string
---@param onFail function?
---@return boolean
local function saveAsserted(filename, data, onFail)
savefile(filename, data)
local res = loadfile(filename)
if res == data then
return true
end
if onFail then
onFail(filename, data, res)
end
return false
end
local fileIO_enabled = saveAsserted('TestFileIO.pld', 'FileIO is Enabled')
FileIO = {
Save = savefile,
Load = loadfile,
SaveAsserted = saveAsserted,
enabled = fileIO_enabled,
}
end)
if Debug then Debug.endFile() end
if Debug and Debug.beginFile then Debug.beginFile("SyncedTable") end
--[[
---------------------------
-- | SyncedTable v1.1b | --
---------------------------
by Eikonium
--> https://www.hiveworkshop.com/threads/syncedtable.353715/
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| A Lua table with a multiplayer-safe pairs()-iteration, i.e. it will not desync unlike native pairs().
| Syncedtables achieve this safe pairs() by looping in the same order that elements got inserted into it (can change upon element removal), which is deterministic and identical across clients.
| You can do everything with a SyncedTable that you could do with a normal Lua table, with the few restrictions listed below.
| SyncedTables are pretty quick on adding and removing elements as well as looping over them, but they add some overhead to table creation. Thus, only create a SyncedTable instead of a normal one, if you intend to loop over it via pairs() in multiplayer.
|
| -------
| | API |
| -------
| SyncedTable.create([existingTable]) --> SyncedTable
| - Creates a new SyncedTable, i.e. a Lua-table with a multiplayer-synchronized pairs()-iteration.
| - Specifying an existing table will add all of its (key,value)-pairs to the new SyncedTable and <-sort its keys to derive the initial loop order.
| Hence, creation will throw an error, if existingTable contains keys of the same type that can not be <-sorted.
| Specifying an existing table is a convenience feature. Using it on big tables will hurt performance.
| - Typically, you create empty SyncedTables and add elements one-by-one.
| - Example:
| local PlayerColors = SyncedTable.create()
| PlayerColors[Player(0)] = "FF0303"
| PlayerColors[Player(1)] = "0042FF"
| PlayerColors[Player(2)] = "1CE6B9"
| for player, color in pairs(PlayerColors) do -- loop order will be the same for all clients in a multiplayer game (not that it matters in this particular example)
| print("|cff" .. color .. GetPlayerName(player) .. "|r")
| end
| SyncedTable.isSyncedTable(table) --> boolean
| - Returns true, if the specified table is a SyncedTable, and false otherwise.
| ----------------
| | Restrictions |
| ----------------
| - Detaching a SyncedTable's metatable or overwriting it's __index, __newindex or __pairs metamethods will stop it from working. You can however add any other metamethod to it's existing metatable.
| - You can't use next(S)==nil on a SyncedTable S to check, whether it is empty or not. You can however use pairs(S)(S)==nil to check the same (although this isn't super performant).
--]]-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- disable sumneko extension warnings for imported resource
---@diagnostic disable
do
---Comparison function for sorting a set of objects having a natural order. Primarily sorts by type, secondarily by <-relation.
---Used, if an existing table is specified during SyncedTable creation.
---@param a any
---@param b any
---@return boolean
local function comparisonFunc(a,b)
local t1,t2 = type(a), type(b)
if t1 == t2 then
return a<b
end
return t1 < t2
end
--Help data structures for SyncedTable loops, shared between all existing SyncedTables.
local recycleStack = {} --State-tables are stored here to prevent garbage collection, at least up to MAX_STACK_SIZE
local stackSize = 0 --Current number of tables stored in recycleStack
local MAX_STACK_SIZE = 128 --Max number of tables that can be stored in recycleStack
---@class SyncedTable : table
SyncedTable = {}
---Creates a table with a multiplayer-synchronized pairs-function, i.e. you can iterate over it via pairs(table) without fearing desyncs.
---After creation, you can use it like any other table.
---The implementation adds overhead to creating the table, adding and removing elements, but keeps the loop itself very performant. So you should only used syncedTables, if you plan to iterate over it.
---You are both allowed to add and remove elements during a pairs()-loop.
---Specifying an existing table as input parameter will add its elements to the new SyncedTable. This only works for input tables, where all keys are sortable via the "<"-relation, i.e. numbers, strings and objects listening to some __lt-metamethod.
---@param existingTable? table any lua table, whose elements you want to add to the new SyncedTable. The table is required to only contain keys that can be sorted via the '<'-relation. E.g. you might write SyncedTable.create{x = 10, y = 3}.
---@return SyncedTable
function SyncedTable.create(existingTable)
local new = {}
local metatable = {class = SyncedTable}
local data = {}
--orderedKeys and keyToIndex don't need to be weak tables. They reference keys if and only if those keys are used in data.
local orderedKeys = {} --array of all keys, defining loop order.
local keyToIndex = {} --mirrored orderedKeys, i.e. keyToIndex[key] = int <=> orderedKeys[int] = key. This is used to speed up the process of removing (key, value)-pairs from the syncedTable (to prevent array search in orderendKeys).
local numKeys = 0
--If existingTable was provided, register all keys from the existing table to the keyToIndex and orderedKeys help tables.
if existingTable then
--prepare orderedKeys array by sorting all existing keys
for k,v in pairs(existingTable) do
numKeys = numKeys + 1
orderedKeys[numKeys] = k --> the resulting orderedKeys is asynchronous at this point
data[k] = v
end
table.sort(orderedKeys, comparisonFunc) --result is synchronous for all players
--fill keyToIndex accordingly
for i = 1, numKeys do
keyToIndex[orderedKeys[i]] = i
end
end
--Catch read action
metatable.__index = function(t, key)
return data[key]
end
--Catch write action
metatable.__newindex = function(t, key, value)
--Case 1: User tries to remove an existing (key,value) pair by writing table[key] = nil.
if data[key]~=nil and value == nil then
--swap last element to the slot being removed (in the iteration order array)
local i = keyToIndex[key] --slot of the key, which is getting removed
keyToIndex[orderedKeys[numKeys]] = i --first set last slot to i
keyToIndex[key] = nil --afterwards nil current key (has to be afterwards, when i == numKeys)
orderedKeys[i] = orderedKeys[numKeys] --i refers to the old keyToIndex[key]
orderedKeys[numKeys] = nil
numKeys = numKeys - 1
--Case 2: User tries to add a new key to the table (i.e. table[key] doesn't yet exist and both key and value are not nil)
elseif data[key]==nil and key ~= nil and value ~= nil then
numKeys = numKeys + 1
keyToIndex[key] = numKeys
orderedKeys[numKeys] = key
end
--Case 3: User tries to change an existing key to a different non-nil value (i.e. table[existingKey] = value ~= nil)
-- -> no action necessary apart from the all cases line
--Case 4: User tries to set table[nil]=value or table[key]=nil for a non-existent key (would be case 1 for an existent key)
-- -> don't do anything.
--In all cases, do the following:
data[key] = value --doesn't have any effect for case 4.
end
--- State-based iterator function that is used the retreive the next loop element within a SyncedTable loop.
--- The iteration loops through orderedKeys[] in ascending order, saving the current position and key in the state-table.
---@param state {loopCounter:integer, lastKey:any} holds loop metadata
---@return any key, any value
local function iterator(state)
if state.lastKey == orderedKeys[state.loopCounter] then --check, if the last iterated key is still in place. If not, it has been removed in the last part of the iteration.
state.loopCounter = state.loopCounter + 1 --only increase i, when the last iterated key is still part of the table. Otherwise use the same i again. This allows the removal of (key,value)-pairs inside the pairs()-iteration.
end
local currentKey = orderedKeys[state.loopCounter]
state.lastKey = currentKey
--If the loop is finished and the recycleStack is not full, empty and recycle the state-table.
--If the recycleStack is full, the state-table will not be recycled and instead garbage collected (no further action required)
if currentKey == nil and stackSize < MAX_STACK_SIZE then
state.loopCounter = nil --state.lastKey is already nil at this point
stackSize = stackSize + 1
recycleStack[stackSize] = state
end
return currentKey, data[currentKey] -- (key,value)
end
--- Metamethod to define the pairs-loop for a SyncedTable. Runs every time a new loop is initiated.
--- Fetches a new state-table and returns it together with the above iterator.
---@param t SyncedTable
---@return function iterator
---@return integer state loopId of the new loop
metatable.__pairs = function(t)
local state --structure to hold loop information
if stackSize > 0 then --recycled table available -> pop
state = recycleStack[stackSize]
recycleStack[stackSize] = nil
stackSize = stackSize - 1
else
state = {}
end
state.loopCounter = 1 --current position within orderedKeys
return iterator, state, nil
end
setmetatable(new, metatable)
return new
end
---Returns true, if the input argument is a SyncedTable, and false otherwise.
---@param anyObject any
---@return boolean isSyncedTable
SyncedTable.isSyncedTable = function(anyObject)
local metatable = getmetatable(anyObject)
return metatable and metatable['class'] == SyncedTable
end
--Allows writing SyncedTable() instead of SyncedTable.create().
setmetatable(SyncedTable, {__call = function(func, t)
return SyncedTable.create(t)
end})
end
if Debug and Debug.endFile then Debug.endFile() end
if Debug and Debug.beginFile then Debug.beginFile("LogUtils") end
--[[
LogUtils v1.0.0 by Tomotz
This library provides a log file for the game.
Features beyond what FileIO offers:
- Log is saved to a memory buffer, allowing you to write to the file over and over again without losing the old data.
- Writing lines to the log only when the game is in replay to reduce performance impact
- Maximum log length - when reached, will open a new log file (and memory buffer) to avoid long writes
- Adds the player id to the log name to allow multiple logs from a single game on the same computer (for map testing)
Interface:
LogWrite(line)
Appends a line to the current log file (also flushes everything in the memory buffer to the file)
LogWriteNoFlush(line)
Appends a line to the memory buffer without writing it to the file.
This should only be used if you know you will eventually use a LogWrite to flush this line
LogWriteNoFlushReplay(line)
LogWriteReplay(line)
These functions are the same as the ones above, except they will only write the line when a replay of the game is played.
Note that the normal variants of these functions will write in both replay and normal modes.
These functions should be used when you worry about game performace impact. If you write many/very long lines to
the log, it might hurt performance
Installation instructions:
Copy the code to your map.
Credits:
TriggerHappy GameStatus (Replay Detection) https://www.hiveworkshop.com/threads/gamestatus-replay-detection.293176/
* A big block of code was taken from there to allow detecting that the game is run in replay mode.
Requires:
FileIO (lua) by Trokkin - https://www.hiveworkshop.com/threads/fileio-lua-optimized.347049/
TotalInitialization by Bribe - https://www.hiveworkshop.com/threads/total-initialization.317099/
Updated: 24 Dec 2024
--]]
do
LIBRARY_LogUtils = true
local SCOPE_PREFIX = "LogUtils_" ---@type string requires TotalInitialization, FileIO
-- Configurations:
-- Base name for the log files.
-- The logs are saved to Documents\Warcraft III\CustomMapData
-- the relative path where the log files are kept. Should end with \\
local RAW_LOG_PATH = "Savegames\\TestMap\\"
-- Raw name for the log file. Will append the player index to the start on any mode, and "replay" to the end on replay mode.
-- Will also add the log index (in case there are multiple log files in the same run) and .txt to the end
local RAW_LOG_NAME = "last_game_log"
-- The maximum length of the memory buffer. If too long, writes to the log file can take a long time
-- (as the whole buffer is flushed on every write) and hinder performances
local MAX_BUFF_LEN = 50000 ---@type integer -- Theoretically, FileIO supports 999999
-- Maximum number of log files that will be created in a single game (will stop logging if threshold was reached)
local MAX_FILES = 10 ---@type integer
-- Same as MAX_FILES, but for replay logs
local MAX_FILESREPLAY = 50 ---@type integer
-- If true, will try writing the map name to the long on init (using TRIGSTR_001)
local WRITE_MAP_NAME = true ---@type boolean
-- Useful globals that tells you if the game is offline (single player I think), online or replay.
-- The first 3 are constants and should not be changed
GAME_STATUS_OFFLINE = 0 ---@type integer
GAME_STATUS_ONLINE = 1 ---@type integer
GAME_STATUS_REPLAY = 2 ---@type integer
GameStatus = 0 ---@type integer -- one of GAME_STATUS_OFFLINE, GAME_STATUS_ONLINE, GAME_STATUS_REPLAY
-- Local variables for internal use
local LogFileName ---@type string
local WriteBuffer = "\n" ---@type string
local FileIdx = 0 ---@type integer
local IsFlashed = true ---@type boolean
---@param text string
local function WriteAndFlush(text)
if (FileIdx < MAX_FILES) or ((GameStatus == GAME_STATUS_REPLAY) and (FileIdx < MAX_FILESREPLAY)) then
FileIO.Save(LogFileName, text)
end
end
local function CreateNewLogFile()
LogFileName = RAW_LOG_PATH .. GetPlayerId(GetLocalPlayer()) .. "_" .. RAW_LOG_NAME .. "_"
if GameStatus == GAME_STATUS_REPLAY then
LogFileName = LogFileName .. "replay_"
end
LogFileName = LogFileName .. tostring(FileIdx) .. ".txt"
FileIdx = FileIdx + 1
WriteAndFlush("\nLog file is empty")
end
-- sets GameStatus variable which decides if game is online, offline or replay
local function SetGameStatus()
local firstPlayer ---@type player
local u ---@type unit
local selected ---@type boolean
-- find an actual player
firstPlayer = Player(0)
while not ((GetPlayerController(firstPlayer) == MAP_CONTROL_USER and GetPlayerSlotState(firstPlayer) == PLAYER_SLOT_STATE_PLAYING)) do
firstPlayer = Player(GetPlayerId(firstPlayer) + 1)
end
-- force the player to select a dummy unit
u = CreateUnit(firstPlayer, FourCC('hfoo'), 0, 0, 0)
SelectUnit(u, true)
selected = IsUnitSelected(u, firstPlayer)
RemoveUnit(u)
if (selected) then
-- detect if replay or offline game
if (ReloadGameCachesFromDisk()) then
GameStatus = GAME_STATUS_OFFLINE
else
GameStatus = GAME_STATUS_REPLAY
end
else
-- if the unit wasn't selected instantly, the game is online
GameStatus = GAME_STATUS_ONLINE
end
end
local function init()
SetGameStatus()
CreateNewLogFile()
-- write map name to log
if WRITE_MAP_NAME then
LogWrite(GetLocalizedString("TRIGSTR_001"))
end
end
-- Writes a line to the WriteBuffer to be logged
---@param line string
function LogWriteNoFlush(line)
local full_line = line .. "\n" ---@type string
if StringLength(WriteBuffer) + StringLength(full_line) > MAX_BUFF_LEN then
if not IsFlashed then
WriteAndFlush(WriteBuffer)
end
CreateNewLogFile()
WriteBuffer = full_line
else
WriteBuffer = WriteBuffer .. full_line
end
IsFlashed = false
end
-- Writes a line to the WriteBuffer to be logged only in replay mode
---@param line string
function LogWriteNoFlushReplay(line)
if GameStatus == GAME_STATUS_REPLAY then
LogWriteNoFlush(line)
end
end
-- Writes a line to the current log
---@param line string
function LogWrite(line)
LogWriteNoFlush(line)
WriteAndFlush(WriteBuffer)
IsFlashed = true
end
-- Writes a line to the log only in replay mode
---@param line string
function LogWriteReplay(line)
if GameStatus == GAME_STATUS_REPLAY then
LogWrite(line)
end
end
OnInit(init)
end
if Debug and Debug.endFile then Debug.endFile() end
if Debug and Debug.beginFile then Debug.beginFile("DebugDumps") end
--[[
DebugDumps v1.2.0 by Tomotz
This library is meant to help with debugging desyncs.
It dumps the stacktrace and names of any functions that are currently in progress or ended not long ago.
(which can be triggered when a player leaves the game).
It's main purpose is to help investigating desyncs by letting the developer know what functions recently started/finished
execution, and what functions are still in progress. This info can be dumped when a player leaves the game.
Features:
- Save a list of functions that recently started/ended execution
- Save all locations of the current context switching functions (functions that release execution and allow other threads to run)
TriggerSleepAction (TSA), ExecuteFunc, TriggerExecute, TriggerEvaluate calls. Will save those from the time the function released
execution, until the thread returns to that point in the code.
Saves all coroutins that have yielded and weren't resumed yet
- Save all timers that are currently counting (started and weren't paused/destroyed) including the location in the code where they
were started
- Dump all the above info to a file
- For each timer/context switch, will dump the name of the function that used the TSA/CS function, and the traceback where this
call was performed.
Interface:
- LogRecentFuncs()
Dumps the recent functions to the log file
- DebugDumpsEndRecording()
- DebugDumpsStartRecording()
Not required to use. If you to skip wrapping some functions, you can use `DebugDumpsEndRecording` at the start of such
function block, and DebugDumpsStartRecording at the end, causing all functions in between no to be recorded
Installation instructions:
- DebugDumps should be copied to your map and put above any part of your code you wish to be included in the dumps.
(but after all the libraries it depends on which of course will not be included in the dumps)
Performance:
I did not see a notable performance impact on my map from all the wrapping functions, but I'm sure it has some performance impact.
It shouldn't be very hard to implement that the code here will only be ran in replay mode if necessary.
Known issues:
- If the same function is called multiple times, it will appear multiple times in the log (reducing the amount of overall unique functions in the log)
- Does not dump traces from local functions.
- Does not dump traces from table functions.
* I think this one should be solvable by adding `table` recursive behavior to __newindex, but I didn't really needed it so didn't implement it.
- These last two cases might also cause a wrong function name to appear in the log in the case TSA or another context switching function
is used in a local/table function.
- Cannot dump anything that happens before OnInit (or anything in itself/the libraries it depends on)
Requires:
TotalInitialization by Bribe - https://www.hiveworkshop.com/threads/total-initialization.317099/
SyncedTable by Eikonium - https://www.hiveworkshop.com/threads/syncedtable.353715/
Hook by Bribe - https://www.hiveworkshop.com/threads/hook.339153/
LogUtils (By me) or any implementations of LogWrite and LogWriteNoFlush - https://www.hiveworkshop.com/threads/logutils.357625/
For LogUtils - FileIO (lua) by Trokkin - https://www.hiveworkshop.com/threads/fileio-lua-optimized.347049/
--]]
-- Configurations:
-- Maximum number of recently ended functions to print.
local RECENT_END_MAX = 50
-- A table for functions that we don't want to dump to the recent function trace.
-- Note that they could still appear on Current timers/Context switches.
---@type table<string>
local EXCLUDE_LIST = {
["RegisterFastTimer"] = true
}
-- If true, will dump the recent functions whenever a player leaves the game
local DUMP_ON_PLAYER_LEAVE = true
-- Local variables for internal use
--functions that recently ended
---@type table<integer, string>
local recent_end_functions = {}
local cur_func_end_idx = 0
local next_func_end_idx = 1
local recent_funcs_end_full = false
---@type string -- the last user function seen in the current thread
local last_function_seen = ""
---@type SyncedTable<string, integer>
local cur_functions = SyncedTable.create()
---@type SyncedTable<timer, string>
local cur_timers = SyncedTable.create()
---@type SyncedTable<thread, {trace:string, prev_func:string}>
local cur_coroutines = SyncedTable.create()
---@type SyncedTable<string, string>
local recent_TSA = SyncedTable.create()
---@type Hook.property
DebugWrapHook = nil
function LogRecentFuncs()
LogWriteNoFlush("###Current funcs###")
for funcName, count in pairs(cur_functions) do
LogWriteNoFlush(funcName .. ": " .. count)
end
LogWriteNoFlush("###Current coroutines###")
for tid, cr_data in pairs(cur_coroutines) do
LogWriteNoFlush(cr_data.prev_func .. "(" .. tostring(tid) .. ")")
end
LogWriteNoFlush("###Current timers###")
for _, traceback in pairs(cur_timers) do
LogWriteNoFlush(traceback)
end
LogWriteNoFlush("###Current Context Switches###")
for traceback, funcType in pairs(recent_TSA) do
LogWriteNoFlush(funcType .. ": " .. traceback)
end
LogWriteNoFlush("###Recently ended funcs###")
if recent_funcs_end_full then
for i = next_func_end_idx, RECENT_END_MAX do
LogWriteNoFlush(recent_end_functions[i])
end
end
for i = 1, next_func_end_idx - 1 do
LogWriteNoFlush(recent_end_functions[i])
end
LogWrite("######")
end
local function init()
local function TimerStartHook(hook, whichTimer, timeout, periodic, handlerFunc)
cur_timers[whichTimer] = Debug.traceback() .. " (" .. last_function_seen .. ")"
-- save the last function seen from the caller, and restore it at the end of the run
local prev_function = last_function_seen
local WrapperFunc = function()
last_function_seen = prev_function
if handlerFunc ~= nil then
handlerFunc()
end
end
hook.next(whichTimer, timeout, periodic, WrapperFunc)
last_function_seen = prev_function
end
local function StopTimerHook(hook, whichTimer)
cur_timers[whichTimer] = nil
hook.next(whichTimer)
end
Hook.add("TimerStart", TimerStartHook)
Hook.add("PauseTimer", StopTimerHook)
Hook.add("DestroyTimer", StopTimerHook)
---@param hook Hook.property
local function CoroutineYieldHook(hook, ...)
cur_coroutines[coroutine.running()] = {
trace = Debug.traceback() .. " (" .. last_function_seen .. ")",
prev_func =
last_function_seen
}
hook.next(...)
end
---@param hook Hook.property
---@param cur_thread thread
local function CoroutineResumeHook(hook, cur_thread, ...)
thread_data = cur_coroutines[cur_thread]
if thread_data ~= nil then
last_function_seen = thread_data.prev_func
cur_coroutines[cur_thread] = nil
end
hook.next(cur_thread, ...)
end
Hook.add("yield", CoroutineYieldHook, 0, coroutine)
Hook.add("resume", CoroutineResumeHook, 0, coroutine)
---@param hook Hook.property
---@param func_type string
local function WrapContextSwitchFunc(hook, func_type, ...)
local tb = Debug.traceback() .. " (" .. last_function_seen .. ")"
-- save the last function seen from the caller, and restore it at the end of the run
local prev_function = last_function_seen
recent_TSA[tb] = func_type
hook.next(...)
last_function_seen = prev_function
recent_TSA[tb] = nil
end
---@param hook Hook.property
local function TriggerSleepActionHook(hook, ...)
WrapContextSwitchFunc(hook, "TSA", ...)
end
---@param hook Hook.property
local function ExecuteFuncHook(hook, ...)
WrapContextSwitchFunc(hook, "ExFunc", ...)
end
---@param hook Hook.property
local function TriggerExecuteHook(hook, ...)
WrapContextSwitchFunc(hook, "TrigEx", ...)
end
---@param hook Hook.property
local function TriggerEvaluateHook(hook, ...)
WrapContextSwitchFunc(hook, "TrigEval", ...)
end
Hook.add("TriggerSleepAction", TriggerSleepActionHook)
Hook.add("ExecuteFunc", ExecuteFuncHook)
Hook.add("TriggerExecute", TriggerExecuteHook)
Hook.add("TriggerEvaluate", TriggerEvaluateHook)
if DUMP_ON_PLAYER_LEAVE then
local player_leaves_trig = CreateTrigger()
for i = 0, GetBJMaxPlayers() - 1 do
TriggerRegisterPlayerEvent(player_leaves_trig, Player(i), EVENT_PLAYER_LEAVE)
end
TriggerAddAction(player_leaves_trig, LogRecentFuncs)
end
end
local function OnStart(funcName)
if cur_functions[funcName] == nil then
cur_functions[funcName] = 0
end
cur_functions[funcName] = cur_functions[funcName] + 1
end
local function OnEnd(funcName)
cur_functions[funcName] = cur_functions[funcName] - 1
if cur_functions[funcName] == 0 then
cur_functions[funcName] = nil
end
if recent_end_functions[cur_func_end_idx] ~= funcName then
recent_end_functions[next_func_end_idx] = funcName
cur_func_end_idx = next_func_end_idx
next_func_end_idx = ModuloInteger(next_func_end_idx, RECENT_END_MAX) + 1
if next_func_end_idx == 1 then
recent_funcs_end_full = true
end
end
end
local function WrapFunction(func, funcName)
return function(...)
-- save the last function seen from the caller, and restore it at the end of the run
local prev_function = last_function_seen
last_function_seen = funcName
if EXCLUDE_LIST[funcName] == nil then
OnStart(funcName)
end
local result = { func(...) } -- Capture all return values
if EXCLUDE_LIST[funcName] == nil then
OnEnd(funcName)
end
last_function_seen = prev_function
return table.unpack(result) -- Return all results
end
end
---@param hook Hook.property
---@param _G table
---@param key string
---@param value unknown
local function __newindex(hook, _G, key, value)
if type(value) == "function" then
value = WrapFunction(value, key)
end
hook.next(_G, key, value)
end
function DebugDumpsStartRecording()
if DebugWrapHook == nil then
DebugWrapHook = Hook.add('__newindex', __newindex, 0, _G, rawset)
end
end
function DebugDumpsEndRecording()
if DebugWrapHook ~= nil then
Hook.delete(DebugWrapHook)
DebugWrapHook = nil
end
end
OnInit(init)
if Debug and Debug.endFile then Debug.endFile() end
DebugDumpsStartRecording()
if Debug and Debug.beginFile then Debug.beginFile("MainCode") end
local Counter = 0 ---@type integer
local ExecutedTrigger ---@type trigger
function CoroutineYielder()
-- no one should resume this yield, so the function will apear in the log
coroutine.yield()
end
function CoroutineStarted()
co = coroutine.create(CoroutineYielder)
coroutine.resume(co)
end
function ExecutedFunctionWithTSA()
print("Running ExecutedFunctionWithTSA")
TriggerSleepAction(10)
end
function DumpRecentWrap()
--function that you should see in the "recent funcs" section of the log
print("Dumping recent functions to log")
TriggerExecute(ExecutedTrigger)
LogRecentFuncs()
end
function CrashingFun()
print("Crashing")
error()
print("Crashing end")
end
function CrashTest()
-- print("Crash test ExecuteFunc")
-- ExecuteFunc("CrashingFun")
-- TriggerSleepAction(1)
-- print("TimerCrashTest")
-- local tt = CreateTimer()
-- TimerStart(tt, 1, false, CrashingFun)
-- TriggerSleepAction(2)
-- TimerStart(tt, 1, false, function() print("timer ok") end)
-- print("Crash test end")
-- print("Timer reset test")
-- local tt = CreateTimer()
-- TimerStart(tt, 2, false, function() print("timer 1 ok") end)
-- TriggerSleepAction(1)
-- TimerStart(tt, 2, false, function() print("timer 2 ok") end)
-- print("Timer reset test end")
print("no func test")
local tt = CreateTimer()
TimerStart(tt, 1, false, gsfsgsgf)
TriggerSleepAction(2)
TimerStart(tt, 1, false, function() print("timer 2 ok") end)
print("Timer reset test end")
end
function RegisterFastTimer()
TimerStart(CreateTimer(), 0.03, true, function() Counter = Counter + 1 end)
end
function RegisterSlowTimer()
TimerStart(CreateTimer(), 10, true,
function()
print("Type -d to dump recent functions to log. Type -s to trigger a desyncing function. -c for crash test. Counter =" ..
Counter)
end)
end
function CreateUnitForPlayer()
print("Desyncing. This can take up to 15 seconds I think")
if GetTriggerPlayer() == GetLocalPlayer() then
CreateUnit(GetTriggerPlayer(), FourCC('hfoo'), 0, 0, 0)
end
end
function RegisterEvents()
local dumpTrig = CreateTrigger()
for i = 0, GetBJMaxPlayers() - 1 do
TriggerRegisterPlayerChatEvent(dumpTrig, Player(i), "-d", true)
end
TriggerAddAction(dumpTrig, DumpRecentWrap)
local desyncTrig = CreateTrigger()
for i = 0, GetBJMaxPlayers() - 1 do
TriggerRegisterPlayerChatEvent(desyncTrig, Player(i), "-s", true)
end
TriggerAddAction(desyncTrig, CreateUnitForPlayer)
local crash_trigger = CreateTrigger()
for i = 0, GetBJMaxPlayers() - 1 do
TriggerRegisterPlayerChatEvent(crash_trigger, Player(i), "-c", true)
end
TriggerAddAction(crash_trigger, CrashTest)
ExecutedTrigger = CreateTrigger()
TriggerAddAction(ExecutedTrigger, ExecutedFunctionWithTSA)
RegisterFastTimer()
RegisterSlowTimer()
CoroutineStarted()
end
OnInit(RegisterEvents)
if Debug and Debug.endFile then Debug.endFile() end
-- DebugWrapEnd
DebugDumpsEndRecording()
end)