• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[Lua] Debug Utils (Ingame Console etc.)

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
Since it looks like you're planning to override "print" to have it wait until the game has started, one mildly useful tidbit would be to have it use "DisplayTimedTextToPlayer", whose fourth parameters allows you to specify how long the message is displayed for:

Lua:
function print(...)
    local sb = table.pack(...)
    for i = 1, sb.n do
        sb[i] = tostring(sb[i])
    end
    DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 60000, table.concat(sb, '    '))
end

@Nestharus had a very robust approach to displaying error messages in vJass: JASS/main.j at master · nestharus/JASS . In his scripts, he's put basically an "unlimited" amount of time to display each message. This works more like a standard console, where the error messages are output to a static state. Obviously, using your debug console already gives you the ability to have everything show up in a neat and orderly place, but this is a simple quality of life improvement for the general 'print' function (which is only meant for debugging, rather than gameplay-relevant content).

The version I've provided above will also be ever-so-slightly more efficient than the current "print" method by reducing the number of calls to the expensive "select" method.
 
Level 20
Joined
Jul 10, 2009
Messages
478
Update to DebugUtils 1.4 (beta)

Doing a beta-test before updating the main post, because it's such a big one. You can download the attached "LuaDebugUtils_v1.4_beta.w3x" and use it in your own project to help me testing.
You can also help me by challenging the design decisions I've listed below.

Big thanks to @Bribe and @Luashine for all the great contributions!! Also a retrospective thanks to @Jampion, who's suggestions from 10 months ago I have implemented now (they also matched some of Bribe's).

General changes
  1. Due to the sheer amount of new API-functions, all of them are now part of the Debug-library. I.e., try becomes Debug.try and so on.
    Design decisions:
    a) Used Debug with capital D instead of Lua's native debug-library to not mess with VSCode's auto-complete suggestions.
    b) Included try = Debug.try for backwards compatibility reasons.
  2. DebugUtils now needs to be the topmost script in your trigger editor (because one of the new features relies on it).
  3. DebugUtils now includes a settings section, where you can enable/disable all of the automatic features.

New Error Handling Library, Stack Trace, Local files
  1. DebugUtils now overwrites TimerStart and applies Debug.try on every callback.
    Design decision: The anonymous callbacks continously created by the overwritten TimerStart and TriggerAddAction are now cached/reused to a table to prevent large-scale recreation, especially for short-duration periodic timers. The table uses weak references, so garbage collection is supposed to continue as normal.
    To Test: How does this interact with other systems applying error handling on timers, like Total Initialization and TimerQueue?
  2. Debug.try now prints a stack trace for every catched error.
    1668804314952.png

    Design decisions:
    a) Most recent call first
    b) Show document names on change only (i.e. 1819, 1827 and 1823 above are all part of war3map.lua)
    c) Don't exclude error handling source from stack trace (because I actually like to see where the error handling comes from).
    d) Stack Trace is calculated up to a depth of 50. Blank entries and immediately recurring entries are skipped.
  3. Error Handling and stack traces now show references to local files instead of war3map.lua-ones for all indexed files.
    The scripts "IngameConsole" and "DebugUtils" are indexed by default, as you can see in the error message above.
    This gets you the same line numbers as in your IDE and allows you to inspect errors in VSCode without forcing syntax errors in the World Editor.
    You can index a file by writing Debug.beginFile(fileName) in its very first line (even before comments) and Debug.endFile() in the very last (the latter being optional, because the next call of Debug.beginFile will also end the previous file).
  4. New or changed library functions:
    • Debug.log(...) (formerly LogStackTrace) saves all arguments to the Debug-log. The Debug-log is printed by Debug.try upon catching an error (i.e. not at all in a clean run).
      Design decision: Debug.log(...) only saves one set of arguments per code-location, i.e. using it within a re-occuring function execution will only show the latest logged instance on error.
    • Debug.throwError(...) throws an error at the point of execution, including stack trace, Debug-log and the specified arguments.
    • Debug.assert(condition, ...) calls Debug.throwError(...), IF the specified condition resolves to false.
    • Debug.traceback() (formelry GetStackTrace) returns the stack trace at the point where it is executed (and you manually need to print it).
    • Debug.getLocalErrorMsg(errorMsg) converts an error messages with references to war3map.lua to an error message with local references. Non-war3map.lua references are left untouched. This can be used for custom message handlers, but probably won't see much use.

Table-print
  • table.tostring(whichTable [, depth] [, pretty_yn]) now has a third parameter applying @Bribe 's pretty version, if set to true. Same for table.print.
    Details in this post.

Ingame Console and Exec
  • Now calculates linebreaks based on Wc3 version 1.33 font size, IF used in Wc3 1.33 (i.e. old version will still apply old font size). This will hopefully fix the multiboard exceeding issue mentioned by @Luashine.
  • Improved both the code and its documentation.
  • The documentation now links to @Luashine 's paste helper.
  • Ingame Console can now properly display strings including linebreaks (like "Hello\nWorld").
    Design decision:
    It will split a string by every linebreak, spread existing color codes around every line and print the parts separately.
    There are some super-rare edge cases, where the color code spreading doesn't work, but that's mainly true for incorrect syntax color codes, so I don't care :p
  • The "Invalid Lua-statement" error has been further improved, delivering details on what was actually wrong with the input-string.
  • Ingame Console now has a setting to turn on stack traces for its error messages.
    Design decisions:
    a) Stack traces for errors from functions executed within the console are turned off by default, because you have actively made that execution and hence know where it comes from.
    b) If you still need the stack trace after an error occured, you can use the new reserved keyword lasttrace to receive it.
  • The -exec command now prints both executed statement and return values for better feedback. Error Messages now include stack trace.

Print Overwrite
  1. DebugUtils now caches prints during loading screen and displays them after game start.
  2. You can now adjust the duration that prints last on screen in the Debug-settings.
    Design decision: Default setting is nil (meaning default Wc3 print duration, depending on string length), because I personally favor looking into the F12-log over showing long prints on screen.

tostring Overwrite / Name Caching

Essentially: tostring will now show globals by their string name instead of their memory position, e.g. print(CreateUnit) will now print "function: CreateUnit" instead of "function: 0063A698".
Same applies to entries of subtables of _G.
Further details and design decisions in the spoiler below.

  1. DebugUtils now overwrites the tostring-function to show the string name of non-primitive objects instead of their memory position. Same applies to print. E.g. print(CreateUnit) will now print "function: CreateUnit" instead of "function: 0063A698". Values without a name (like all locals) remain the old way (mempos).
    For that, DebugUtils builds a Name Cache during loading screen, saving the string name for every object in global scope.
    This also applies to subtables of global scope up to a depth of Debug.settings.NAME_CACHE_DEPTH. Entries in subtables are named after their parenttable plus their own key.
    Design decisions:
    a) The memory position is completely cut from the string. Assumption is that the name is just as unique as the mempos.
    b) Names for values in tables use dot-notation for string keys without whitespace (such as "math.sin") and square bracket notation for other keys (such as "T[1]").
    c) Some particular objects not located in global scope were also given a name (such as Player(i)).
    Known flaw:
    Reassigning a new value to an existing variable will not update the name cache, so the name is still used for the old object, while the new one will show up in memory position style.
  2. New objects are automatically added to the name cache by use of the __newindex metamethod for _G.
    Experimental feature: The __newindex-metamethod inherits to new subtables of _G (again up to a depth of Debug.settings.NAME_CACHE_DEPTH), also automatically adding new objects within subtables to the name cache.
    Design decision:
    a) DebugUtils applies a new metatable (using the same __newindex) to all subtables, essentially doubling tables created (but remember that this only applies to those tables accessible from global scope and only for the first use of a name).
    b) Existing __newindex metamethod's of Tables entering (sub-)global scope will be overwritten by a nested __newindex (which applies both the name caching and the original newindex). I assume this to be the "standard" way for existing metatables, because new tables usually stay local until they have been fully prepared. Even if this assumption fails, all that happens is that this table overwrites the name caching newindex, so their entries will just show up in the old way.
    Known flaws:
    a) Assigning a new metatable or a new __newindex metamethod to any table will break name caching. Nothing we can do about this, though, and not really an issue (remember that values inside will still be printed in the normal way).
    b) Creating a new table into an existing key will not trigger __newindex of the parent table and therefore not inherit the name caching. Again nothing we can do and not really an issue.
  3. Library functions to manually add a name for objects to the name cache that don't have one already (applicable to locals):
    • Debug.registerName(whichObject, name) registers the specified name for the specified non-primitive object to the name cache.
    • Debug.registerNamesFrom(whichTable [, tableName] [, depth]) registers names for all values within the specified parent table to the name cache, again using "<tablename>.<key>" or "<tablename>[<key>]" for the new names.
      Leaving tableName nil will use its existing name from the name cache. Setting tableName to the empty string will suppress it and only name values after their keys.
      You can specify a depth to also register names for values within subtables of the specified table.
      Design decision:

    Debug.registerName will overwrite existing names (because you wouldn't call it, if you wanted to keep the old name), while Debug.registerNamesFrom will not (because name cached objects might be referenced again in the parent table, but you likely want to keep the shorter name already in existence).

Other Changes
  1. Warnings for accessing undeclared globals are now disabled during loading screen (and begin upon game start), to prevent library dependency checks from showing too many warnings in combination with the cached print feature.
  2. Debug.wc3Type (formerly Wc3Type) doesn't translate numbers to real and integer anymore, just calling them number, just like type does.

The new code

This is the new code for DebugUtils:
Lua:
do
    local _, codeLoc = pcall(error, "", 2) --get line number where DebugUtils begins.
--[[
 -------------------------
 -- | Debug Utils 1.4 | --
 -------------------------
 --> https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/
 - by Eikonium, with special thanks to:
    - @Bribe, for useful suggestions like name caching and stack trace improvements, pretty table print and showing that xpcall's message handler executes before the stack unwinds
    - @Jampion, for useful suggestions like print caching and overwriting TriggerAddAction for automated error handling
    - @Luashine, for useful feedback, bug reporting and building "WC3 Code Paste Helper" (https://github.com/Luashine/wc3-debug-console-paste-helper#readme)
    - @HerlySQR, for showing a way to get a stack trace in Wc3
    - @Macadamia, for showing a way to print warnings upon accessing undeclared globals (where this all started with)
-----------------------------------------------------------------------------------------------------------------------------
| 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 any code within Wc3 through ingame chat.                                   |
|   3. Automatic warnings upon reading undeclared globals (which also triggers in case of misspelling globals)              |
|   4. Debug-Library functions for simplified 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 this code into your map. Use a script file (Ctrl+U) in your trigger editor, not a text-based trigger!                                                           |
|   2. Make sure that Debug Utils is the topmost script in your trigger editor (crucial for some features like local line numbering).                                       |
|   3. Create a GUI-trigger with "Map Initialization" event and add the Trigger Action "IngameConsole.createTriggers()" (via Custom Script).                                |
|   4. Adjust the settings in the settings-section to get to the debug environment that fits your needs.                                                                    |
|                                                                                                                                                                           |
| Deinstallation:                                                                                                                                                           |
|                                                                                                                                                                           |
|  - Debug Utils is meant to provide debugging utility and as such, shall be removed from the map closely before release.                                                   |
|  - Optimally delete the whole thing and all library function calls in your other scripts (to save map space).                                                             |
|  - If deleting isn't suitable for you, at least set all the Debug.settings to false and deactivate Ingame Console by disabling the trigger from Installation point 3).    |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
* 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, ...) -> ...
*        - Calls the specified function with the specified parameters in protected mode (i.e. code afterwards will continue to run even if the function fails to execute).
*        - If the call is successful, returns the specified function's original return values.
*        - 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 together with the next error 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 saved/printed.
*    Debug.throwError(...)
*        - Prints an error message including document, line number, stack trace, logged parameters and all specified parameters on screen. Parameters can have any type.
*    Debug.assert(condition:boolean, ...)
*        - Prints an error message including document, line number, stack trace, logged parameters and all specified parameters on screen, IF the specified condition resolves to false/nil. Parameters can have any type.
*    Debug.traceback() -> string
*        - Returns the stack trace at the position where this is called. You need to manually print it.
*    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 saved to Debug.sourceMap.
*        - Error Msg must be formatted like "<document>:<linenumber><Rest>".
*
* -----------------------------------
* | Warnings for undeclared globals |
* -----------------------------------
*        - DebugUtils will print warnings on screen, if you read an undeclared global variable.
*        - This is technically the case, when you misspelled on a function name, like calling CraeteUnit instead of CreateUnit.
*        - Keep in mind though that the same warning will pop up after reading a global that was intentionally nilled. If you don't like this, turn of this feature in the settings.
*
* -----------------
* | Print Caching |
* -----------------
*        - DebugUtils caches print()-calls occuring during loading screen and delays them to after game start.
*        - This also applies to loading screen error messages, so you can wrap erroneous parts of your Lua root in Debug.try-blocks and see the message after game start.
*
* -------------------------
* | Local File Stacktrace |
* -------------------------
*        - By default, error messages and stack traces printed by the error handling functionality of Debug Utils contain references to war3map.lua (a big file just appending all your local scripts).
*        - The Debug-library provides the two functions below to index your local scripts, activating local file names and line numbers (matching those in your IDE) instead of the war3map.lua ones.
*        - This allows you to inspect errors within your IDE (VSCode) instead of the World Editor.
*
*    Debug.beginFile(fileName)
*        - Begins a file and names it as specified to improve stack traces of error messages. Converts "war3map.lua"-lines referencing a line within the file to file-specific line numbers.
*        - 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".
*        - !!! Must be called in the Lua root in Line 1 of every document you wish to track. Line 1 means line 1, i.e. before any comment!
*    Debug.endFile()
*        - 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).
*
* ----------------
* | 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).
*        - 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(...).
*
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------]]

    ----------------
    --| Settings |--
    ----------------

    Debug = {
        --BEGIN OF SETTINGS--
        settings = {
                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".
            ,   NAME_CACHE_DEPTH = 4                            ---Set to an integer > 0 to also cache string names for subtables of _G (up to the specified depth) by nesting the names of parent tables. E.g. for "T = {{bla = {}}}", printing the innermost table would output "table: T[1].bla".
            ,   USE_TRY_ON_TRIGGERADDACTION = true              ---Set to true for automatic error handling on TriggerAddAction (applies Debug.try on every trigger action).
            ,   USE_TRY_ON_TIMERSTART = true                    ---Set to true for automatic error handling on TimerStart (applies Debug.try on every timer callback).
            ,   WARNING_FOR_UNDECLARED_GLOBALS = true           ---Set to true to print warnings upon accessing undeclared globals (i.e. globals with nil-value). This is technically the case after having misspelled on a function name (like CraeteUnit instead of CreateUnit).
            ,   USE_PRINT_CACHE = true                          ---Set to true to let print()-calls during loading screen be cached until the game starts.
            ,   PRINT_DURATION = nil                            ---Adjust the duration in seconds that values printed by print() last on screen. Set to nil to use default duration (which depends on string length).
        }
        --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)
            ,   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).
            ,   paramLog = {}                                   ---@type table<string,string> saves logged information per code location. to be filled by Debug.log(), to be printed by Debug.try()
            ,   sourceMap = {{firstLine= 1,file='DebugUtils'}}  ---@type table<integer,{firstLine:integer,file:string,lastLine?:integer}> saves lines and file names of all documents registered via Debug.beginFile().
            ,   printCache = {n=0}                              ---@type string[] contains the strings that were attempted to print during loading screen.
        }
    }

    --localization
    local paramLog, nameCache, nameDepths, sourceMap, printCache = Debug.data.paramLog, Debug.data.nameCache, Debug.data.nameDepths, Debug.data.sourceMap, Debug.data.printCache

    --Write DebugUtils first line number to sourceMap:
    ---@diagnostic disable-next-line: need-check-nil
    Debug.data.sourceMap[1].firstLine = tonumber(codeLoc:match(":\x25d+"):sub(2,-1)) - 1

    -------------------------------------------------
    --| File Indexing for local Error Msg Support |--
    -------------------------------------------------

    -- Functions for war3map.lua -> local file conversion for error messages.

    ---Begins a file and names it as specified to improve stack traces of error messages. Converts "war3map.lua"-references to file-specific references.
    ---
    ---Stacktrace will treat all war3map.lua-lines to be part of "fileName" that are located between the call of Debug.beginFile(fileName) and the next call of Debug.beginFile OR Debug.endFile.
    ---
    ---Must be called in the Lua root in Line 1 of every document you wish to track!!! Line 1 means before any comment!
    ---@param fileName string
    function Debug.beginFile(fileName)
        local _, location = pcall(error, "", 3) ---@diagnostic disable-next-line: need-check-nil
        local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000:..."
        line = line and tonumber(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)
        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}) --automatically sorted list, because calls of Debug.beginFile happen logically in the order of the map script.
        end
    end

    ---Optional, only use after Debug.beginFile().
    ---Let's Debug.try use global "war3map.lua" error messages for code after the Debug.endFile() call.
    function Debug.endFile()
        local _, location = pcall(error, "", 3) ---@diagnostic disable-next-line: need-check-nil
        local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000:..."
        sourceMap[#sourceMap].lastLine = line and tonumber(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

    ---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><Rest>".
    ---@return string convertedMsg a string of the form "<localDocument>:<localLinenumber><Rest>"
    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><Rest>".
            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 _, 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
                trace = trace .. separator .. ((tracePiece:match("^.-:") == lastFile) and tracePiece:match(":\x25d+"):sub(2,-1) or tracePiece:match("^.-:\x25d+"))
                lastFile, lastTracePiece, separator = tracePiece:match("^.-:"), tracePiece, " <- "
            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
    local function errorHandler(errorMsg)
        errorMsg = convertToLocalErrorMsg(errorMsg)
        --Print original error message and stack trace.
        print("|cffff5555ERROR at " .. errorMsg .. "|r")
        print("|cffff5555Traceback (most recent call first):|r")
        print("|cffff5555" .. getStackTrace(4,50) .. "|r")
        --Also print entries from param log, if there are any.
        for location, loggedParams in pairs(paramLog) do
            print("|cff888888Logged for " .. convertToLocalErrorMsg(location) .. loggedParams .. "|r")
            paramLog[location] = nil
        end
    end

    ---Tries to execute the specified function with the specified parameters and prints an error message (including stack trace) in case of an error.
    ---
    ---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 "try(CreateUnit, 0, 1, 2)", i.e. separate the function from the parameters.
    ---* Option 2: Change it to "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
    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.
    ---@param ... any objects/errormessages to be printed (doesn't have to be strings)
    function Debug.throwError(...)
        Debug.try(error, concat(...))
    end

    ---Prints an error, if the specified value/condition is false (or nil). The error message consists of all objects specified after the condition (4-space-separated).
    ---@param condition any actually a boolean, but you can use any object as a boolean.
    ---@param ... any
    function Debug.assert(condition, ...)
        if not condition then
            Debug.throwError(...)
        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,50)
    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] = concat(...)
    end

    ------------------------------------
    --| Name Caching (API-functions) |--
    ------------------------------------

    --Help-table. The registerName-functions below shall not work on call-by-value-types, i.e. booleans, strings and numbers (renaming a value of any primitive type doesn't make sense).
    local skipType = {boolean = true, string = true, number = true, ['nil'] = true}
    --Set weak values to nameCache and nameDepths to prevent garabage collection issues
    setmetatable(nameCache, {__mode = 'v'})
    setmetatable(nameDepths, getmetatable(nameCache))
    ---Registers a name for the specified object o, which tostring(o) will output afterwards.
    ---You can overwrite existing names by using this.
    ---@param whichObject any
    ---@param name string
    function Debug.registerName(whichObject, name)
        if not skipType[type(whichObject)] then
            nameCache[whichObject] = name
            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, don't add names for primitive types.
        if nameCache[object] or skipType[type(object)] then
            return
        end
        --apply dot-syntax for string keys without whitespace
        if type(key) == 'string' and not string.find(key, "\x25s") then
            if parentTableName == "" then
                nameCache[object] = key
                nameDepths[object] = 0
            else
                nameCache[object] =  parentTableName .. "." .. key
                nameDepths[object] = parentTableDepth + 1
            end
        --apply bracket-syntax for all other keys. This requires a parentTableName.
        elseif parentTableName ~= "" then
            key = type(key) == 'string' and ('"' .. key .. '"') or key
            nameCache[object] = parentTableName .. "[" .. tostring(key) .. "]"
            nameDepths[object] = parentTableDepth + 1
        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). Only specify integers >= 1.
    ---@overload fun(parentTable:table, depth:integer)
    function Debug.registerNamesFrom(parentTable, parentTableName, depth)
        --Support overloaded definition fun(parentTable:table, depth:integer)
        if type(parentTableName) == 'number' then
            depth = parentTableName
            parentTableName = nil
        end
        --Apply default values
        depth = depth or 1
        parentTableName = parentTableName or nameCache[parentTable] or ""
        --add name of T in case it hasn't already
        if not nameCache[parentTable] and parentTableName ~= "" then
            Debug.registerName(parentTable, parentTableName)
        end
        --Register all call-by-ref-objects in parentTable. To be preferred over simple recursive approach to ensure that top level names are preferred.
        registerAllObjectsInTable(parentTable, parentTableName)
        --if depth > 1 was specified, also register Names from subtables.
        if depth > 1 then
            for _, object in pairs(parentTable) do
                if type(object) == 'table' then
                    Debug.registerNamesFrom(object, nil, depth - 1)
                end
            end
        end
    end

    -------------------------------------------
    --| Name Caching (Loading Screen setup) |--
    -------------------------------------------

    ---Registers all existing object names from global scope and Lua incorporated libraries to be used by tostring() overwrite below.
    local function registerNamesFromGlobalScope()
        --Add all names from global scope to the name cache.
        Debug.registerNamesFrom(_G, "")
        --Add all names of Warcraft-enabled Lua libraries as well:
        --Could instead add a depth to the function call above, but we want to ensure that these libraries are added even if the user has chosen depth 0.
        for _, lib in ipairs({coroutine, math, os, string, table, utf8, Debug}) do
            Debug.registerNamesFrom(lib)
        end
        --Add further names that are not accessible from global scope:
        --Player(i)
        for i = 0, GetBJMaxPlayerSlots() - 1 do
            Debug.registerName(Player(i), "Player(" .. i .. ")")
        end
    end

    --Set empty metatable to _G. __index is added when game starts (for "attempt to read undeclared global"-errors), __newindex is added right below (for building the name cache).
    setmetatable(_G, {})

    -- Save old tostring into Debug Library before overwriting it.
    Debug.oldTostring = tostring
    if Debug.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.
            return nameCache[obj] and ((oldTostring(obj):match("^.-: ") or (oldTostring(obj) .. ": ")) .. nameCache[obj]) or 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()

        local nameRegisterNewIndex
        ---__newindex to be used for _G (and subtables up to a certain depth) to automatically register new names to the nameCache.
        ---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
            addNameToCache((t ~= _G and nameCache[t]) or "", k, v, parentDepth)
            --If v is a table, inherit __newindex to v's existing metatable (or create a new one)
            if type(v) == 'table' and parentDepth+1 < Debug.settings.NAME_CACHE_DEPTH then
                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
                local existingNewIndex = mt.__newindex
                --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.
                        return existingNewIndex(t,k,v)
                    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
            --Set t[k] = v.
            if not skipRawset then
                rawset(t,k,v)
            end
        end

        --Apply metamethod to _G.
        getmetatable(_G).__newindex = nameRegisterNewIndex
    end

    ------------------------------------------
    --| Cache prints during Loading Screen |--
    ------------------------------------------

    --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>

    --Overwrite TriggerAddAction and TimerStart natives to let them automatically apply Debug.try.
    if Debug.settings.USE_TRY_ON_TRIGGERADDACTION then
        local originalTriggerAddAction = TriggerAddAction
        TriggerAddAction = function(whichTrigger, actionFunc)
            if actionFunc then
                tryWrappers[actionFunc] = tryWrappers[actionFunc] or function() try(actionFunc) end
            end
            originalTriggerAddAction(whichTrigger, tryWrappers[actionFunc])
        end
    end
    if Debug.settings.USE_TRY_ON_TIMERSTART then
        local originalTimerStart = TimerStart
        TimerStart = function(whichTimer, timeout, periodic, handlerFunc)
            if handlerFunc then
                tryWrappers[handlerFunc] = tryWrappers[handlerFunc] or function() try(handlerFunc) end
            end
            originalTimerStart(whichTimer, timeout, periodic, tryWrappers[handlerFunc])
        end
    end

    -----------------------
    --| Print Overwrite |--
    -----------------------

    -- Apply the duration as specified in the settings.
    if Debug.settings.PRINT_DURATION then
        local display, localPlayer, dur = DisplayTimedTextToPlayer, GetLocalPlayer(), Debug.settings.PRINT_DURATION
        print = function(...)
            display(GetLocalPlayer(), 0, 0, dur, concat(...))
        end
    end
 
    -- Delay loading screen prints to after game start.
    if Debug.settings.USE_PRINT_CACHE then
        local oldPrint = print
        --loading screen print will write the values into the printCache
        print = function(...)
            oldPrint(...)
            if not bj_gameStarted then --during loading screen only: concatenate input arguments 4-space-separated, implicitely apply tostring on each, cache to table
                printCache.n = printCache.n + 1
                printCache[printCache.n] = concat(...)
            end
        end
    end

    -------------------------
    --| Modify Game Start |--
    -------------------------

    local originalMarkGameStarted = MarkGameStarted

    --Hook certain actions into the start of the game.
    MarkGameStarted = function()
        originalMarkGameStarted()
        if Debug.settings.WARNING_FOR_UNDECLARED_GLOBALS then
            getmetatable(_G).__index = function(_, n) if string.sub(tostring(n),1,3) ~= 'bj_' then print("Trying to read undeclared global : "..tostring(n)) end 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 Debug.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 Debug.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
            printCache = nil --frees reference for the garbage collector
        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

        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, maxDepth), table.print(T, pretty_yn) or table.print(T, maxDepth, pretty_yn).
        ---@param whichTable table
        ---@param maxDepth? integer default: unlimited
        ---@param pretty_yn? boolean default: false (concise)
        ---@overload fun(whichTable:table, pretty_yn?:boolean)
        function table.print(whichTable, maxDepth, pretty_yn)
            print(table.tostring(whichTable, maxDepth, pretty_yn))
        end
    end
end
Debug.endFile()

Edit

Just for completeness sake, the following changes are not yet part of the uploaded beta version, but included in my development version:
  • IngameConsole doesn't need a GUI-trigger anymore.
  • Added settings SHOW_TRACE_ON_ERROR, ALLOW_INGAME_CODE_EXECUTION and AUTO_REGISTER_NEW_NAMES.
  • Loop through tables on creation to add all elements to the name cache. This makes sure that new elements within table constructors (like T.x in T = {x = {}}) also get a name
 

Attachments

  • LuaDebugUtils_v1.4_beta.w3x
    84.8 KB · Views: 2
Last edited:
Level 19
Joined
Jan 3, 2022
Messages
320
Now calculates linebreaks based on Wc3 version 1.33 font size, IF used in Wc3 1.33 (i.e. old version will still apply old font size). This will hopefully fix the multiboard exceeding issue mentioned by @Luashine.
:wgrin: I'm not even using 1.33 I will try to make a test case with the new map, looks great!
PS: The map is saved in 1.33 :( I'm still using 1.32.10 for its stability. Would you resave it in 1.32.10 or no?
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
PS: The map is saved in 1.33 :( I'm still using 1.32.10 for its stability. Would you resave it in 1.32.10 or no?
True, just I don't have 1.32 installed :p Will definitely save it in 1.32 for the final release.

For now, I attach the three code files for you to copy-paste, if that's fine.
The three files need to be in the order DebugUtils above StringWidth above IngameConsole (on the very top of your map script).
Additionally, please create this GUI-trigger:
  • Trigger Init
    • Events
      • Map initialization
    • Conditions
    • Actions
      • Custom script: IngameConsole.createTriggers()
(Note: Trigger will be gone in the final version of 1.4)
 

Attachments

  • DebugUtils.lua
    47.3 KB · Views: 4
  • IngameConsole.lua
    35.4 KB · Views: 0
  • StringWidth.lua
    26.6 KB · Views: 1
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
The functionality of Debug.beginfile looks absolutely spectacular, and I'd like to implement it (optionally) to Total Initialization. To do so, I'd like to request that you allow an optional parameter to increase the stack depth offset by which you are retrieving the user script's line number.

This enables a user to call OnInit (and specify a library name), and I can then offset the depth of beginfile internally within the initialization system to capture the user's code location rather than the location within the Total Initialization file.

I'll also then promote that users embed their comments below the OnInit line to ensure that the line numbers sync up with those in the IDE.

This solves the following problems:
  1. Reduce the amount of typing a user needs in order to implement the functionality of both systems.
  2. Automate the debugging process so that a user of Total Initialization can automatically have IDE-relevant stack traces (rather than just war3map.lua stack traces) just by including the Debug library.
  3. Automatically associate the library name of an OnInit callback function with the "beginfile" file name, which Total Initialization can then tack a ".lua" extension if desired (as a configurable constant at the top of the Total Initialization script).
Does the line number offset currently skip the "Debug.dofile" call line, or does it include it? If it skips it, then I'd also request that you add a "line number offset" optional parameter that retains the position of the OnInit call and treats it accordingly as part of the IDE file.

EDIT:

@Eikonium here is an alternative to the beginfile/endfile stuff. If you use a modified "try" function that takes a name as declaration of what you're "trying" to do, you can tailor the error message specifically for that particular "try" method.

The API would look something like this:

Lua:
Debug.dofile(filename: string, callback: function [, depthOffset: integer, lineOffset: integer])

The callback would be:
Lua:
Debug.dofile("MyFile", function()
    error("This error should occur on MyFile, line 2.")
end)

I actually think that Total Initialization is going to have a problem with declaring the depth of the callback, unless it can pre-register the function to the Debug library. Therefore, I would recommend a function that does: Debug.logfile("FileName", function [, depth]), which registers "function" to that file name, and the callback depth is retrieved from the stack trace at the point where "logfile" is called.

Accordingly, once the function is called via "Debug.try", "try" checks if that function has been logged via Debug.logfile and, if so, execute the callback based on the pre-defined parameters of the Debug.logfile call.

It doesn't solve the need for an "endfile" to check for out-of-bounds stuff, but if Total Initialization incorporates this functionality behind-the-scenes, then there shouldn't be overlapping code (since any custom resource could be using OnInit to declare itself).

EDIT 2:

I can kind of see why you chose to do the traceback on one line in the end (because of the file-logging functionality), however it would definitely still look best if it kept each file on a separate line. Not sure if it's just me on that preference though.

Lastly, I think that the depth really does need to be set to 200 or higher (since if you stop at 50 recursive calls, it will not get you the function calls that led to the C stack overflow). Efficiency is 100% irrelevant with this tool, since it's only used during the debugging process and 200+ function calls is still very lightweight in terms of framerate.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
Does the line number offset currently skip the "Debug.dofile" call line, or does it include it? If it skips it, then I'd also request that you add a "line number offset" optional parameter that retains the position of the OnInit call and treats it accordingly as part of the IDE file.
The line executing Debug.beginFile counts as line 1 of the file, so the same would apply to OnInit() after increasing the depth. No line offset required.
I actually think that Total Initialization is going to have a problem with declaring the depth of the callback
Would't you just need to set the depth to 4?
Lua:
function OnInit.root(filename, func, ...)
    Debug.beginFile(filename, 4)
    executeLater(func, ...)
end

However, this would require the user to write alls scripts like this:
Lua:
OnInit.root(fileName, function() --first line of file, Oninit would need to be placed here to work

    --[[ Comment Block saying something about the file
    potentially very long
    ]]

    --Begin of code
    SomeClass = {}
    SomeClass.__index = SomeClass

    function SomeClass.create(...)
       ...
    end

    ...

end)
Debug.endFile()

The thing is, this is a very strong syntax to enforce and I personally don't (want to) write my code like that. I don't like to span one big anonymous function through the whole script and I don't use OnInit() on my root code, but rather define many small named functions and use OnInit() on them afterwards.
Also (as you said) Debug.endFile() would still need to be called manually by the user, which seems uninuitive to me. Not to mention that OnInit becomes less intuitive to use as well, because it starts to mix different functionalities and users probably don't want to begin a new file half of the times using it (or not at all).
That said, I find the OnInit method for registering file names too restrictive and would prefer to leave the two functionalities separated.

But I'm happy to discuss further ;)

unless it can pre-register the function to the Debug library. Therefore, I would recommend a function that does: Debug.logfile("FileName", function [, depth]), which registers "function" to that file name, and the callback depth is retrieved from the stack trace at the point where "logfile" is called.
I didn't understand this part. Not sure if it's still relevant after the above.

@Eikonium here is an alternative to the beginfile/endfile stuff. If you use a modified "try" function that takes a name as declaration of what you're "trying" to do, you can tailor the error message specifically for that particular "try" method.
I'm afraid I haven't yet understood this part either. What's the idea behind associating a function to execute to a filename? Aren't those just separate things or does this rely on packing all script code into one gigantic function again?

I can kind of see why you chose to do the traceback on one line in the end (because of the file-logging functionality), however it would definitely still look best if it kept each file on a separate line. Not sure if it's just me on that preference though.
I actually have a visual preference for the one-liner version :D
Despite that, I see screen space issues, because Wc3 has limited ability to display a lot of line breaks at the same time. The current error message needs 3+ lines already, so including one line breaks per file could easily make it go out of bounds (especially together with logged params).

Lastly, I think that the depth really does need to be set to 200 or higher (since if you stop at 50 recursive calls, it will not get you the function calls that led to the C stack overflow). Efficiency is 100% irrelevant with this tool, since it's only used during the debugging process and 200+ function calls is still very lightweight in terms of framerate.
Thx, will do!
 
Level 19
Joined
Jan 3, 2022
Messages
320
Lastly, I think that the depth really does need to be set to 200 or higher (since if you stop at 50 recursive calls, it will not get you the function calls that led to the C stack overflow).
If it's at all possible to capture the Stack overflow function by unwinding backwards then it's going to be the very last one... risky business.
A random thought: desync due to different thread stack sizes on different user machines when SO happens? :peasant-popcorn:
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
@Eikonium, maybe you can help me to understand your approach better, so we can sync up.

When would a library name not want to be the same as the file name? If I have a resource by the name of "My Awesome Library", odds are the trigger is also going to be named "My Awesome Library" and the file is saved in the IDE as "My Awesome Library".

Your example can simply use OnInit. Unless something actually needs to happen in root (like function hooks that need to apply to subsequent Lua systems, a la Lua-Infused GUI), then OnInit will suffice (which defaults to OnGlobalInit). The rarer cases where you need to initialize sooner or later can just add the .blah syntax after OnInit to identify when the initialization takes place.

Here's how I would envision it:

Lua:
OnInit("file name", function() --first line of file, Oninit would need to be placed here to work

    Require "Requirement 1" --url
    Require.optionally "Requirement 2" --url

    --[[ Comment Block saying something about the file
    potentially very long.
    Not the worst, as this is how vJass libraries were defined for years anyway.
    ]]

    --Begin of code
    SomeClass = {}
    SomeClass.__index = SomeClass

    function SomeClass.create(...)
       ...
    end

    ...

end)
--file has completed.
--next file begins
OnInit("next file name", function()
    ...
end)

Now comes a question - when "Debug.beginfile" is called for the "next file name", does it automatically declare "Debug.endfile" up to the line above that declaration?

If it doesn't, it definitely can (and should, in my opinion).

Scripts which do not use Total Initialization but want to be properly picked up by the script would just need to be declared like so:

Lua:
Debug.beginfile "raw file"
...
do
    ...
end

Then, to wrap everything up, you can use the model I listed here: [Lua] - Debug Utils (Ingame Console etc.) , but with the below modification to the "close root" trigger:

Lua:
end)
Debug.endfile() --done

This way, "Debug.endfile" only needs to be referenced once during the entire process, and all the sub-folders declare their "file terminators" accordingly via either an OnInit call or a Debug.beginfile call.

This potentially eliminates almost all need to open up the war3map.lua file (which can be annoying), by listing out only the triggers themselves.

@Luashine it's not risky, I've tested it plenty of times. As far as desyncs are concerned, this would be a non-issue for two reasons:
  1. Debugging would be done in advance of the map being published (ideally, unless the implementer forgot to disable the debug functionality beforehand).
  2. I think the string desync issue was specific to JASS, but I could be wrong.

EDIT:

The below change proposals will enable much more fluid script-line segmentation:

Lua:
---@class DebugFile
---@field firstLine integer
---@field lastLine integer
---@field file string

sourceMap = {} ---@type DebugFile[]

---@param fileName string
---@param depth? integer
---@return DebugFile
function Debug.beginFile(fileName, depth)
    local _, location = pcall(error, "", depth or 3) ---@diagnostic disable-next-line: need-check-nil
    local line = location:match(":\x25d+") ---@type DebugFile --extracts ":1000" from "war3map.lua:1000:..."
    if line then --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)
        line = tonumber(line:sub(2,-1))
        local n = #sourceMap
        sourceMap[n].lastLine = sourceMap[n].lastLine or line - 1 --automatically fill in the last line for users who didn't manually call "endfile".
        local file = {firstLine = line, file = fileName} ---@type DebugFile
        sourceMap[n+1] = file
        return file
    end
end

---@param depth? integer
---@return DebugFile
function Debug.endFile(depth)
    local file = sourceMap[#sourceMap]
    if not file.lastLine then --Allows Total Initialization to reconcile OnInit functions declared without a library name by calling "Debug.endfile" with each call to OnInit. Total Initialization will internally exclude nested OnInit calls or calls that occur outside of the Lua root.
        local _, location = pcall(error, "", depth or 3) ---@diagnostic disable-next-line: need-check-nil
        local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000:..."
        file.lastLine = line and tonumber(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)
        return file
    end
end
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
When would a library name not want to be the same as the file name? If I have a resource by the name of "My Awesome Library", odds are the trigger is also going to be named "My Awesome Library" and the file is saved in the IDE as "My Awesome Library".
I have the following cases in my code:

  1. Multiple libraries in one file, so neither library is named similar to the file.
  2. Libraries that don't use OnInit, because they have nothing to initialize.
  3. Several files without any library.
  4. I typically don't use OnInit in the first line of my code, but somewhere around the last (and I'd like to stick to that habit).
    It usually looks like this:
    Lua:
    --[[ Comment Block saying something about the file
    ]]
    do
        --Begin of code
        SomeClass = {}
        SomeClass.__index = SomeClass
    
        function SomeClass.create(...)
            ...
        end
    
        ...
    
        local function initStuff()
            ...
        end
    
        OnInit(initStuff) --simple call of OnInit, I hate nested code :P
    end
    This code has slightly worse error handling than yours (because Lua root errors are not catched), but it instead improves on readability, which is king in my personal book.

As we see by my own example, we shouldn't assume that other users are structuring their code similar to ours or are even willing to pack every single file into big OnInit calls. People always get creative with API usage and don't always read docs :p

You could now make file indexing via OnInit optional, but that has other disadvantages:

  • You need an optional fileName parameter in OnInit that some users will always leave nil. But not the last param, because the function is supposed to be the last. I don't like to call OnInit(nil, func).
  • Users must remember that OnInit is indexing the file, although the functionalities of initialization and good error messages are not logically connected.
  • Users will still need to use Debug.beginFile (as you said), which wouldn't be consistent.
  • Users will still need to use Debug.endFile, for instance in front of a GUI trigger.
  • I want the indexing to be optional, so users can actively decide to stay in the war3map.lua-world.
  • I'm not sure about implications on sumneko extension, if we start to define all code within OnInit calls.

Don't get me wrong - I absolutely like the idea of providing better service to the user and requiring less code to be written. I just think that the approach of combining OnInit with Debug.beginFile is not flexible enough.

Now comes a question - when "Debug.beginfile" is called for the "nextFileName", does it automatically declare "Debug.endfile" up to the line above that declaration?

If it doesn't, it definitely can (and should, in my opinion).
It does implicitly. Note that Debug.endFile() adds the .lastLine entry to sourceMap[#sourceMap]. But that entry is optional: If lastLine is missing, then the algorithm reading the sourceMap assumes it to be equal to the firstLine of the next file.

The below change proposals will enable much more fluid script-line segmentation:
Adding the depth parameter is fine, just I'm not yet convinced that we need it.
Making sure that sourceMap[i].lastLine exists is not necessary due to the way the algorithm reads it.
Have you added more that I'm missing?

Then, to wrap everything up, you can use the model I listed here: [Lua] - Debug Utils (Ingame Console etc.) , but with the below modification to the "close root" trigger:
Using your model increases the depth where Debug.beginFile is called. How would you find the right depth in a nested call of OnInit?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
You need an optional fileName parameter in OnInit that some users will always leave nil.

It has been optional all along actually. You don't need to include nil as the first parameter; the function will do fine.

Users must remember that OnInit is indexing the file, although the functionalities of initialization and good error messages are not logically connected.

OnInit is meant for encapsulation as well, but Global Initialization didn't start off that way, so maybe it is confusing if you're still thinking about it in the classic sense.

Users will still need to use Debug.beginFile (as you said), which wouldn't be consistent.

Only in raw code which lacks an OnInit wrapper yet is declared below an unclosed library AND there is an error which includes a stack trace to that part of the code. I had a thought about checking for any new declarations in _G via __newindex to automatically close off any open files, but THAT would be unpredictable to say the least. Easier to use OnInit with a function to contain the resource's contents.

I want the indexing to be optional, so users can actively decide to stay in the war3map.lua-world.

That's why putting it inside Global Initialization would be such a power move. The user doesn't have to think about tooling their map for debugging, because they would already be tooling it for initialization, encapsulation and requirements. Total Initialization would just have different behavior whether the Debug table exists or not (which is fine).

I just think that the approach of combining OnInit with Debug.beginFile is not flexible enough.

It's not a silver bullet. The next big release of vJass2Lua will definitely benefit from it though, and I'm sure the automatic conversion will have kinks that the debug library will help to identify.

Making sure that sourceMap.lastLine exists is not necessary due to the way the algorithm reads it.

Agreed for the beginfile line now that you've clarified it. But the endfile call should accommodate for it for "double-free" protection.

Using your model increases the depth where Debug.beginFile is called.

Only the total stack trace, but not the depth offset (e.g. 4 would still get the function line that called OnInit).

How would you find the right depth in a nested call of OnInit?

The system will auto-call endfile only for OnInit declared from the Lua root (or rather the sandwiched open/close root calls). The OnInit called within the Lua root that doesn't have a name will auto-call endfile but not start a new file of its own (allowing errors caught within itself to be treated as simple war3map.lua errors).

Users will still need to use Debug.endFile, for instance in front of a GUI trigger.

The GUI triggers are declared much lower in the script than ALL Lua-only scripts. So you'd actually just need to call "endfile" from the lowest trigger in the stack (or use that open/close root trick) and it will automatically cut off access to any and all GUI stuff.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
Eikonium said:
Users will still need to use Debug.beginFile (as you said), which wouldn't be consistent.
Only in raw code which lacks an OnInit wrapper yet is declared below an unclosed library AND there is an error which includes a stack trace to that part of the code.
I think this is the main point of disagreement. I do believe that lacking an OnInit wrapper in line 1 of every file is the standard case for most users, while they still might have OnInit calls including library names below line 1.
Those users don't want to begin a new file called "MyLibraryName" in line 250, but they just wanted to define a new library.
I've listed several reasons in my previous post, why library names and file names do not always match (and in fact rarely do in my own code).

Consider a user with this code:
Lua:
--line 1
...
local function initFunc() ... end
--line 249
OnInit("MyLib", initFunc)
...
--file ends

What is the user supposed to do now? Your change as intended would start a new file "MyLib" in line 250, although that doesn't match the IDE at all.

OnInit is meant for encapsulation as well, but Global Initialization didn't start off that way, so maybe it is confusing if you're still thinking about it in the classic sense.
I'm happy that people can use OnInit plus anonymous functions to encapsulate their code and execute it safely, even within Lua root.
Just don't want to confuse file names with library names. And don't want to be forced to start OnInit's in line 1.

I want the indexing to be optional, so users can actively decide to stay in the war3map.lua-world.
That's why putting it inside Global Initialization would be such a power move. The user doesn't have to think about tooling their map for debugging, because they would already be tooling it for initialization, encapsulation and requirements.
With optional, I was referring to users being able to use OnInit(libName, func) without starting a new file (so that errors referring to the file would stay in war3map.lua).
How would the user do that? I couldn't see that from your answer.

But the endfile call should accommodate for it for "double-free" protection.
[...]
The OnInit called within the Lua root that doesn't have a name will auto-call endfile
My current implementation prefers later calls of Debug.endFile over previous ones, i.e. it overwrites sourceMap[i].lastLine on every call. You want it the other way around to be able to auto-call Debug.endFile(), right?
If so, have you considered the case, where OnInit is called below or even within an unclosed file?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
Thank you - those points are indisputable. I have thought of the perfect solution in that case: users who want OnInit to be treated as a file can add ".lua" at the end of their library string. It will be stored internally without the .lua syntax, but if someone requires a ".lua"-suffixed library then I will teach the Require object to ignore that extension.

Basically:

Lua:
OnInit("my library.lua", function()
    error "traceback should include 'my library:2'"
end)

--and:

Require "my library" --works
Require "my library.lua" --also works

I've made the following changes to Total Initialization (my own unreleased copy of it) as such:

Added to the addUserFunc function:

1669452633518.png


Added to the processRequirement function:

1669452673111.png
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
I'm still on the keep-functionalities-separated side of thought, especially because I doubt that many people will adapt to OnInit-file-encapsulation. I honestly see more people getting confused and doing something wrong (like using OnInit below their documentation) than people benefitting from the combined functionality.

If you insist, I'd rather go for a separate API-function OnInit.dofile("Filename", func) than doing string-analysis with ".lua"-patterns, which is evil on its own.

Anyway, I still hope that I can drag you away from changing OnInit, so I just keep asking for technical implementation:
How do you plan to incorporate Debug.endFile()? I currently don't see a way to get the line where your OnInit-call ends.

Btw., you don't need rawget(_G, "Debug"). I assume that you wanted to prevent undeclared global warnings, but these warnings aren't active during loading screen in DebugUtils 1.4 anymore, so you can just ask if Debug.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
I've solved it!

OnInit can take a third optional parameter. The user can call Debug.endinit() from that third parameter at the bottom of the script, capturing the last line, then endinit() returns true, notifying OnInit to call Debug.begininit(), which then ties off the first line.

Full encapsulation with minimal, but deliberate, effort.

endinit basically is the inverse of beginfile, but doesn't know the name of that file. begininit knows the last line already, so will build off of it.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
Here are the proposed changes to the Debug script:

Lua:
    ---@param fileName string
    ---@param depth? integer
    ---@param lastLine? integer
    ---@return table|nil
    function Debug.beginFile(fileName, depth, lastLine)
        local _, location = pcall(error, "", depth or 3) ---@diagnostic disable-next-line: need-check-nil
        local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000:..."
        line = line and tonumber(line:sub(2)) --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)
        if line then --for safety reasons. we don't want to add a non-existing line to the sourceMap
            return 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

    ---Optional, only use after Debug.beginFile().
    ---Let's Debug.try use global "war3map.lua" error messages for code after the Debug.endFile() call.
    ---@param depth? integer
    ---@return integer|nil line
    function Debug.endFile(depth)
        local _, location = pcall(error, "", depth or 3) ---@diagnostic disable-next-line: need-check-nil
        local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000:..."
        if line then
            line = tonumber(line:sub(2)) --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)
            if not depth then
                sourceMap[#sourceMap].lastLine = line
            end
            return line
        end
    end

    ---@return integer|nil
    function Debug.endInit() return Debug.endFile(4) end

With those changes, OnInit would be able to capture the last line returned by Debug.endInit and pass it accordingly to Debug.beginFile.

A user would implement it as such:

Lua:
OnInit("my library", function()
    error("this should show 'my library:2'")
end, Debug.endInit())

Debug.try(error, "this should show 'war3map.lua:#'")
 
Level 20
Joined
Jul 10, 2009
Messages
478
Your version looks almost identical to the classic version:
Lua:
OnInit("my library", function()
    error("this should show 'my library:2'")
end)
Debug.endFile()

I see that your version passes the lastLine as a parameter to Debug.beginFile(), but the end-result is just the same and the syntax for the user is also almost the same. Am I missing something?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
Your version looks almost identical to the classic version:
Lua:
OnInit("my library", function()
    error("this should show 'my library:2'")
end)
Debug.endFile()

I see that your version passes the lastLine as a parameter to Debug.beginFile(), but the end-result is just the same and the syntax for the user is also almost the same. Am I missing something?
Yes, the difference is that doing it this way solves that problem you kept describing: how do you tell OnInit when it should declare itself via Debug.beginfile or not?

You convinced me that giving it a .lua extension was a bad idea, and also convinced me that I can't just include it by default. Therefore, allowing the functions to "talk to each other" through these additional return values and parameters, makes it so that there is no uncertainty about what the user wants to do.

On further thought, an even better approach is this:

Lua:
    ---@param atDepth? integer
    ---@return integer|nil
    function Debug.getLine(atDepth)
        local _, location = pcall(error, "", atDepth or 3) ---@diagnostic disable-next-line: need-check-nil
        local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000: "
        assert(line, "Line is nil - incorrect depth was specified for Debug.getLine!")
        return tonumber(line:sub(2))
    end

    ---Begins a file and names it as specified to improve stack traces of error messages. Converts "war3map.lua"-references to file-specific references.
    ---
    ---Stacktrace will treat all war3map.lua-lines to be part of "fileName" that are located between the call of Debug.beginFile(fileName) and the next call of Debug.beginFile OR Debug.endFile.
    ---
    ---Must be called in the Lua root in Line 1 of every document you wish to track!!! Line 1 means before any comment!
    ---@param fileName string
    ---@param depth? integer
    ---@param lastLine? integer
    function Debug.beginFile(fileName, depth, lastLine)
        table.insert(sourceMap, {firstLine = Debug.getLine(depth or 4), file = fileName, lastLine = lastLine}) --automatically sorted list, because calls of Debug.beginFile happen logically in the order of the map script.
    end

    ---Optional, only use after Debug.beginFile().
    ---Let's Debug.try use global "war3map.lua" error messages for code after the Debug.endFile() call.
    function Debug.endFile()
        sourceMap[#sourceMap].lastLine = Debug.getLine(4)
    end

The above change will allow me to use Debug.getLine from within Total Initialization, which would use an API such as "OnInit.file()".

This makes it so the user doesn't have to do annoying stuff like:

Lua:
if Debug and if Debug.beginFile then Debug.beginFile 'My File' end
function ShowError()
    error 'This should include "My File:3"'
end
if Debug and if Debug.endFile then Debug.endFile() end

--elsewhere in the code:
Debug.try(ShowError)

And instead can do cleaner stuff like:

Lua:
OnInit("My File", function()

    error 'This should include "My File:3"'

end, OnInit.file())

OnInit.file will revert to a "DoNothing" call if the Debug library does not exist, so it handles almost all of the complexity behind the scenes. The user will be able to have much more concise syntax this way.

Bear in mind, my main motivation here was that I was seeing lots of potentially-duplicated and too-verbose code, and wanted to give the best-of-all-worlds scenario for users who want to use both Total Initialization and your debug library. I think this latest iteration finally captures it.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
Yes, the difference is that doing it this way solves that problem you kept describing: how do you tell OnInit when it should declare itself via Debug.beginfile or not?

You convinced me that giving it a .lua extension was a bad idea, and also convinced me that I can't just include it by default. Therefore, allowing the functions to "talk to each other" through these additional return values and parameters, makes it so that there is no uncertainty about what the user wants to do.
Ah I see. Absolutely right, that is an improvement. I thought your third parameter was only there to capture the end-line.

Please take into account though that the user doesn't save much (if any) code with it.
You basically switch this:
Lua:
Debug.beginFile("blaFile")
OnInit("blaLibrary", function()
    ...
end)
--Debug.endFile() --optional
to this:
Lua:
OnInit("bla", function()
    ...
end, Debug.getLine())

That said, I'm still strongly opposed to the idea of connecting two unrelated systems just for the purpose of saving one line of code (per file).
Doing file indexing via OnInit is unintuitive, decreases code readability (because it's a hidden mechanism), forces users to span the function from exactly first line to last line (which I will never do), makes your API more confusing and the documentation more bloated. It even increases error potential (e.g. not opening OnInit in line 1) for users who don't carefully read your documentation (maybe most users?). It's not even always applicable (e.g. multiple libraries in one file), forcing you to go back to Debug.beginFile in those cases, mixing styles in the same project.


On further thought, an even better approach is this:

The changes you proposed (Debug.getLine etc.) look good despite me hating the idea of using those in OnInit. Thanks for the suggestion, I will implement them! And OnInit is your resource after all, so I can't hinder you from doing the changes you proposed (if you are going to do it, I'd still say creating a dedicated function OnInit.dofile is better, as it increases code reability and doesn't need the third parameter to ensure itself of the users intentions).


The above change will allow me to use Debug.getLine from within Total Initialization, which would use an API such as "OnInit.file()".

This makes it so the user doesn't have to do annoying stuff like:
The user knows, if he has added the Debug-library to his setup or not. He doesn't need annoying if Debug ... stuff.
He would simply code like this:
Lua:
Debug.beginFile('My File')
function ShowError()
    Debug.throwError( 'This should include "My File:3"' )
end
Debug.endFile() --optional


Bear in mind, my main motivation here was that I was seeing lots of potentially-duplicated and too-verbose code, and wanted to give the best-of-all-worlds scenario for users who want to use both Total Initialization and your debug library. I think this latest iteration finally captures it.
I know you are thinking this way and I salute you for opting for the user experience.
You can't be serious about "lots of potentially-duplicated and too-verbose code", though. We are still talking about one single Debug.beginFile() per file that you are trying to get rid of.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
The user knows, if he has added the Debug-library to his setup or not. He doesn't need annoying if Debug ... stuff.
He would simply code like this:
Lua:
Debug.beginFile('My File')
function ShowError()
    Debug.throwError( 'This should include "My File:3"' )
end
Debug.endFile() --optional

Here's my rationale: is the user going to add that stuff into every resource when they submit it? Definitely not. I saw your edits to [Lua] - Set/Group Datastructure, and it would set a scary precedent to try to follow. But I firmly believe that there is a huge potential for a very fluid, error-catching, informative debugging system to encourage people to switch to Lua and make their end-to-end problem solving stuff far less annoying to deal with.

Lua:
Debug.beginFile("blaFile")
OnInit("blaLibrary", function()
    ...
end)
--Debug.endFile() --optional

I hate to have to ask someone to declare (what I interpret to be) the name of their file in two separate locations to enable the functionality of both a named resource and a named file.

If we were to encourage (but not require) HiveWorkshop scripts to be easy to debug, diagnose and not invisibly crash for a mysterious reason, I could not bring myself to ever recommend the current syntax. It's far too robust.

However, in case I didn't say it clearly before, I think the discovery and idea to segment file lines is brilliant, and I'm just looking to make the most of it and try to get it into wide circulation as painlessly as possible.

For a map maker who is trying to get multiple systems to work together (which I envision happening as more popular resources get ported to Lua), the troubleshooting process should not be unnecessarily complicated for this user just because the resources are not debug-friendly. This work should be placed on the shoulders of the users who know what they are doing (the ones who are actually coding the systems). This way, the map maker might see "Total Initialization" and "Set/Group" show up as part of the stack trace, and then they check those triggers and see they are managed by Bribe and Eikonium, and can ask either of us what's going on (for example).

With just the war3map.lua data, the average user is not going to have any idea what to do with that information (nor would either of us, if all they did was give us a screenshot of the error message without also including a copy of their entire map's script). To see the war3map.lua file, they first need to save the map as a folder, and then open up that file from their IDE, then scroll for a very long time to that particular line, then try to understand which system the line belongs to. It's definitely doable, if they know how to do all those steps, but is less than ideal when you have discovered a much easier pill to swallow.

Regarding the OnInit syntax doubling down as file syntax, I agree it is not the best name to throw around for resources that want to try to have a reasonable name. I would prefer a name like "Library" (borrows from vJass) or "Module" (borrows from Lua) to encapsulate an entire thing. I was looking at @HerlySQR's Color resource yesterday, and I thought "this thing would be perfect as an unloaded module that only loads if a user requires it".

One such idea that might extend in a more readable direction would be allowing some of _G to get populated a bit further (for the sake of readability):

Lua:
File("My file name", function()
    print "this gets called on Lua root"

    Require "something" --works; File is run via coroutine immediately when declared in the Lua root.
    Require.optionally "another" --only works if the optional requirement was already declared in the Lua root, otherwise will be skipped if it's declared lower in the root.

    OnInit(function() print "this gets called after globals have initialized" end)
end, File'end')

The benefit of wrapping this one up in File() is that you get coroutines and "try" safety built-in, in addition to optionally supporting Debug.begin/endFile.

Lua:
Library("My library name", function()

    print "this gets called in sync with OnInit"

end, Library'end')

The above would be extremely similar to vJass, and also serves to capture the closing line of the library.

Lua:
Module("My module name", function()

    local myModule = { [69] = "nice" }

    print "this gets called only the first time this module is required"
    
    return myModule
end, Module'end')

The above would be the same as a library whose outermost function does not get called automatically, but rather only once it is Required. I can even program it to throw an error message once the game has loaded to inform "module 'x' wasn't loaded, maybe consider deleting it from your map to free up space".
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
Here's my rationale: is the user going to add that stuff into every resource when they submit it? Definitely not. I saw your edits to [Lua] - Set/Group Datastructure, and it would set a scary precedent to try to follow. But I firmly believe that there is a huge potential for a very fluid, error-catching, informative debugging system to encourage people to switch to Lua and make their end-to-end problem solving stuff far less annoying to deal with.
You are talking about submitted resources now, which is a completely different story.
  • Is the user going to add OnInit into every resource they submit? Definitely not. You see, we are not going far with that reasoning.
  • if Debug then as in the Set/Group data structure is not scary, but just the classic way of checking for dependencies. Why do you even think it's scary?
  • if Debug then is not even required, so there is no precedent to follow. Error messages outside of indexed files will stay in war3map.lua, which is just fine. Users (this time: downloading users) can add Debug-lines to all downloaded resources in their own setups, if they like.
  • Your own way requires submitted resources to use if OnInit instead. Why don't you find that scary? It looks much worse than if Debug then, because you are executing an anonymous function involving the whole file in case OnInit exists.

I hate to have to ask someone to declare (what I interpret to be) the name of their file in two separate locations to enable the functionality of both a named resource and a named file.
Again, Library Names are not File Names. Maybe you code that way, but I do not and many do not. You are basically annoyed about having to write one additional line per file and use that as a reason to add a feature to OnInit that only works with your own style of coding.
People should be encouraged to structure their project as they want to, not to follow unnecessary requirements like spanning OnInit-blocks across their files.
Take myself as an example: I have not declared a single library with OnInit in my code, although having tons of dependencies (which is why I have requested a Lite version of Global Init). How is that possible? I declare every system in Lua root and start using those systems later, where everything got already loaded.

If we were to encourage (but not require) HiveWorkshop scripts to be easy to debug, diagnose and not invisibly crash for a mysterious reason, I could not bring myself to ever recommend the current syntax. It's far too robust.
What do you mean "invisibly crash for a mysterious reason"? We've already got a working debug environment with error messages to war3map.lua and are only talking about improved references to files.
I can't believe you are saying you could not bring yourself to ever recommend the current syntax just because it requires one additional line of code, and instead recommend something that's only helpful for your own style of coding.
Especially because I've named so many disadvantages in my previous post that sould clearly outweight the "annoying" part.

With just the war3map.lua data, the average user is not going to have any idea what to do with that information. To see the war3map.lua file, they first need to save the map as a folder, and then open up that file from their IDE, then scroll for a very long time to that particular line, then try to understand which system the line belongs to. It's definitely doable, if they know how to do all those steps, but is less than ideal when you have discovered a much easier pill to swallow.
Saving map as a folder is absolutely not necessary. You can either export war3map.lua via File -> Export Script OR you can simply force a syntax error in your map (e.g. write "x" somewhere and save map) and scroll through war3map.lua within the error-popup.
Both DebugUtils and my Guide to mapping in Lua mention how to do it and the average user is definitely capable of doing so.
But anyway, we both agree on that local file references are better and just disagree on the best way to achieve them, right?

However, in case I didn't say it clearly before, I think the discovery and idea to segment file lines is brilliant, and I'm just looking to make the most of it and try to get it into wide circulation as painlessly as possible.
We're on the same page here :)
A good heaty argument sometimes helps to get clarity.



Anyway I think I will not convice you of that your path is too restrictive, no matter how hard I try. You can implement your idea, but it would be great, if you could see it as one option out of many afterwards, not referring to it as the standard :)
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
It's healthy for me to recognize that I have a blind spot towards what I percieve to be "ugly" or "too robust", and I definitely did not mean to discourage use of the Debug.beginFile/endFile syntax in the first place. What I meant is that it would be great for someone to submit a packaged resource (like what you did with Set/Group) that works with or without it.

I've therefore included this line in the unreleased Total Initialization version I'm tooling with:

Lua:
Debug = Debug or {beginFile = DoNothing, getLine = DoNothing, endFile = DoNothing} --allow users to not get errors for using Debug API when it isn't present in the map.

This way, the user does not have to make different versions of their script depending on whether Debug mode is enabled or not. 700 lines of a debug library don't need to be shipped in a release-ready map, but it would be fantastic for a user to just simply have all the debugging API ready for when they need to move their map back into testing mode (or vice-versa).

An option is that you release a "Debug - Lite" version of your script, which simply does the same thing, and "turns off" the syntax in a fluid way that doesn't require a user to manually delete all of those debug lines or add a bunch of "if" statements to each of them.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
This way, the user does not have to make different versions of their script depending on whether Debug mode is enabled or not. 700 lines of a debug library don't need to be shipped in a release-ready map, but it would be fantastic for a user to just simply have all the debugging API ready for when they need to move their map back into testing mode (or vice-versa).

An option is that you release a "Debug - Lite" version of your script, which simply does the same thing, and "turns off" the syntax in a fluid way that doesn't require a user to manually delete all of those debug lines or add a bunch of "if" statements to each of them.
I've included a "Deinstallation" section into Debug Utils 1.4 for exactly that reason. So far it basically says "turn all settings off or delete all Debug-stuff from the map". I will add Debug-Lite as an alternative.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
I've found that when I use my open/close Root trick to diagnose issues in the raw Lua root, it does indeed change the depth required by Debug.getLine (I am 99% sure that you mentioned this somewhere a few weeks ago).

The below amendment (to my earlier amendments to your code) addresses that problem:

Lua:
    local lineOffset = 0

    ---@param atDepth? integer
    ---@return number?
    function Debug.getLine(atDepth)
        local _, location = pcall(error, "", (atDepth or 3) + lineOffset) ---@diagnostic disable-next-line: need-check-nil
        local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000: "
        assert(line, "Line is nil - incorrect depth was specified for Debug.getLine!")
        return tonumber(line:sub(2))
    end

    function Debug.thread(func)
        lineOffset = lineOffset + 2
        Debug.try(func)
        lineOffset = lineOffset - 2
    end

Instead of opening with Debug.try, one would instead open with Debug.thread.


Edit: I was doing something else that was wrong. I've fixed it now. Once the Debug Library has the Debug.getLine change, I can update Total Initialization properly.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
Update to beta version 2 of Debug Utils 1.4
  • Implemented Debug.getLine as requested by @Bribe .
    Note: The function will return nil for nil-entries in the stack trace. The depth parameter starts at 1 (default) and should be safe for depth < 4.
  • DebugUtils doesn't need a GUI-trigger anymore to prepare Ingame Console.
  • DebugUtils now also applies Error Handling on Filter and Condition.
  • Added settings SHOW_TRACE_ON_ERROR, ALLOW_INGAME_CODE_EXECUTION, USE_TRY_ON_CONDITION and AUTO_REGISTER_NEW_NAMES.
  • Loop through tables on creation to add all elements to the name cache. This makes sure that new elements within table constructors (like T.x in T = {x = {}}) also get a name
  • Several bug fixes.

Next iteration:
- Debug Lite as the "deinstallation version" of Debug Utils, as requested by Bribe.
 

Attachments

  • LuaDebugUtils_v1.4_beta_2.w3x
    89.1 KB · Views: 1

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
For the official release, could you please hook coroutine.wrap and coroutine.create to wrap the callback function in "Debug.try"? I can then remove that logic from Total Initialization and from PreciseWait.

Lua:
    if settings.USE_TRY_ON_COROUTINES then
        local wrapper = function(original)
            return function(f)
                return original(function(...) return Debug.try(f, ...) end)
            end
        end
        coroutine.wrap   = wrapper(coroutine.wrap)
        coroutine.create = wrapper(coroutine.create)
    end
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
Major Update to Debug Utils 2.0

Including complete rewrite of the resource main page. I think rewriting the main page alone took like 20 hours (not kidding). Ooof.
This is the finished version of 1.4 beta that was presented in the comments.

General Changes
  • All API-functions are now part of the Debug-library (capital D, not to be mistaken with Lua's native debug library, which is disabled in Reforged). I.e., try becomes Debug.try and so on.
  • DebugUtils now needs to be the topmost script in your trigger editor (because local file support, print caching and Lua root error handling via Debug.try rely on that).
  • DebugUtils now includes a settings section.
  • Attached up-to-date definition files for Wc3 v1.33 common.lua, blizzard.lua and HiddenNatives.lua.
Improved Error Handling
  • Automatic error handling extended to Timer callbacks, Trigger Conditions and Coroutines (previously only Trigger Actions). I.e. Debug Utils now also overrides Condition, Filter, TimerStart, coroutine.create and coroutine.wrap.
  • Catched errors now print a stack trace in addition to their error message.
  • You can now convert war3map.lua-references into local file references via Debug.beginFile() and Debug.endFile()
  • New and changed library functions Debug.log (replaced LogStackTrace), Debug.assert, Debug.throwError, Debug.traceback (renamed from GetStackTrace) and Debug.wc3Type (renamed from Wc3Type).
Ingame Console and -exec
  • Now calculates linebreaks based on Wc3 version 1.33 font size, IF used in Wc3 1.33+ (i.e. old version will still apply old font size).
  • Improved both the code and its documentation.
  • Ingame Console can now properly display strings including linebreaks (by splitting them into different lines and reproducing color codes).
  • The "Invalid Lua-statement" error has been further improved, delivering details on what was actually wrong with the input-string.
  • Added a setting to turn on stack traces for its error messages.
  • Doesn't need a GUI-trigger anymore for first start up.
  • The -exec command now prints both executed statement and return values for better feedback. Error Messages now include stack trace.
Name Caching
  • Debug Utils will now build a name cache, which lets tostring show globals by their string name instead of their memory position. E.g. print(CreateUnit) will now print "function: CreateUnit" instead of "function: 0063A698".
  • Globals added during map runtime and entries within subtables of global scope are automatically added to the name cache.
  • Library functions Debug.registerName(object,name) and Debug.registerNamesFrom(table,[tableName], [depth]) for manually adding names.
Other Changes
  • table.print now has a pretty version.
  • Prints during loading screen are now delayed to after game start (so you can debug with prints during loading screen).
  • You can set the print duration in the configuration.
  • Warnings for undeclared globals now begin after game start to prevent them from triggering for library dependency checks (which typically happen during loading screen).
 
Last edited:
Level 8
Joined
Jan 23, 2015
Messages
121
Hello there. Spent an evening figuring out why DebugUtils wasn't working with Total Initialization in one project. Seems that getTryWrapper creates wrappers that don't pass parameters through, which in case of coroutines can lead to critical errors. Suggested change: DebugUtils, line 632 -> tryWrappers[func] = tryWrappers[func] or function(...) return try(func, ...) end.
 
Level 20
Joined
Jul 10, 2009
Messages
478
Hotfix

13.01.2023
, v2.0a:
  • Fixed a bug, where the coroutine returned by the overridden version of coroutine.create and coroutine.wrap would not pass parameters to its function.
  • Added document & line reference to undeclared global warnings
  • Added setting SHOW_TRACE_FOR_UNDECLARED_GLOBALS (default false) that also enables stack trace for undeclared global warnings.
  • Added third parameter lastLine to Debug.beginFile() for compatibility reasons with Total Initialization.
Thanks @Trokkin for debugging and reporting this!
That was a stupid bug of mine considering that the version @Bribe had previously suggested was working correctly.

Mainpost updated.
 
Last edited:
Level 8
Joined
Jan 23, 2015
Messages
121
goddamit you are fast
I just came up with an idea that it'd be great to provide stack trace for undeclared variables, so you'd know where to look after them
I mean, Suggested Change: line 710, print("Trying to read undeclared global : " .. tostring(k) .. " at " .. Debug.traceback()) or something, I am stupid to make it beautiful
 
Level 20
Joined
Jul 10, 2009
Messages
478
I just came up with an idea that it'd be great to provide stack trace for undeclared variables, so you'd know where to look after them
I mean, Suggested Change: line 710, print("Trying to read undeclared global : " .. tostring(k) .. " at " .. Debug.traceback()) or something, I am stupid to make it beautiful
Good idea.

1673570279945.png


I think including the main reference (ExampleFile:3) into the first line will actually be sufficient in most cases, because the stack trace will be printed anyway, if an error results from this afterwards.
I leave this to the user to configure, adding a constant SHOW_TRACE_FOR_UNDECLARED_GLOBALS with default false (main reference will always be included).

Edit: I've sneaked the change into version 2.0a. Mainpost and changelog updated.

Edit 2: I've also sneaked another small change into the same version for improved compatibility with Total Initialization (added lastLine parameter to Debug.beginFile()).
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
Hello, can you make the traceback doesn't include the call from the DebugUtils? Because that is redundant.
A configuration to optionally support this would be good, as people might want access to the full stack trace.
Good idea. I also prefer the optional config, as I personally like to see the source of error handling in the stack trace.

The setting is called INCLUDE_DEBUGUTILS_INTO_TRACE. Default is true.

I will include the change into the next version of Debug Utils as soon as enough changes have accumulated.
If you want it now, copy paste the new code from this spoiler:
Lua:
do; local _, codeLoc = pcall(error, "", 2) --get line number where DebugUtils begins.
--[[
 --------------------------
 -- | Debug Utils 2.0a | --
 --------------------------

 --> https://www.hiveworkshop.com/threads/debug-utils-ingame-console-etc.330758/

 - by Eikonium, with special thanks to:
    - @Bribe, for pretty table print, showing that xpcall's message handler executes before the stack unwinds and useful suggestions like name caching and stack trace improvements.
    - @Jampion, for useful suggestions like print caching and applying Debug.try to all code entry points
    - @Luashine, for useful feedback and building "WC3 Debug Console Paste Helper" (https://github.com/Luashine/wc3-debug-console-paste-helper#readme)
    - @HerlySQR, for showing a way to get a stack trace in Wc3 (https://www.hiveworkshop.com/threads/lua-getstacktrace.340841/)
    - @Macadamia, for showing a way to print warnings upon accessing undeclared globals, where this all started with (https://www.hiveworkshop.com/threads/lua-very-simply-trick-to-help-lua-users-track-syntax-errors.326266/)

-----------------------------------------------------------------------------------------------------------------------------
| Provides debugging utility for Wc3-maps using Lua.                                                                        |
|                                                                                                                           |
| Including:                                                                                                                |
|   1. Automatic ingame error messages upon running erroneous code from triggers or timers.                                 |
|   2. Ingame Console that allows you to execute code via Wc3 ingame chat.                                                  |
|   3. Automatic warnings upon reading undeclared globals (which also triggers after misspelling globals)                   |
|   4. Debug-Library functions for manual error handling.                                                                   |
|   5. Caching of loading screen print messages until game start (which simplifies error handling during loading screen)    |
|   6. Overwritten tostring/print-functions to show the actual string-name of an object instead of the memory position.     |
|   7. Conversion of war3map.lua-error messages to local file error messages.                                               |
|   8. Other useful debug utility (table.print and Debug.wc3Type)                                                           |
-----------------------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Installation:                                                                                                                                                             |
|                                                                                                                                                                           |
|   1. Copy the code (DebugUtils.lua, StringWidth.lua and IngameConsole.lua) into your map. Use script files (Ctrl+U) in your trigger editor, not text-based triggers!      |
|   2. Order the files: DebugUtils above StringWidth above IngameConsole. Make sure they are above ALL other scripts (crucial for local line number feature).               |
|   3. Adjust the settings in the settings-section further below to receive the debug environment that fits your needs.                                                     |
|                                                                                                                                                                           |
| Deinstallation:                                                                                                                                                           |
|                                                                                                                                                                           |
|  - Debug Utils is meant to provide debugging utility and as such, shall be removed or invalidated from the map closely before release.                                    |
|  - Optimally delete the whole Debug library. If that isn't suitable (because you have used library functions at too many places), you can instead replace Debug Utils     |
|    by the following line of code that will invalidate all Debug functionality (without breaking your code):                                                               |
|    Debug = setmetatable({try = function(...) return select(2,pcall(...)) end}, {__index = function(t,k) return DoNothing end}); try = Debug.try                           |
|  - If that is also not suitable for you (because your systems rely on the Debug functionality to some degree), at least set ALLOW_INGAME_CODE_EXECUTION to false.         |
|  - Be sure to test your map thoroughly after removing Debug Utils.                                                                                                        |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
* Documentation and API-Functions:
*
*       - All automatic functionality provided by Debug Utils can be deactivated using the settings directly below the documentation.
*
* -------------------------
* | Ingame Code Execution |
* -------------------------
*       - Debug Utils provides the ability to run code via chat command from within Wc3, if you have conducted step 3 from the installation section.
*       - You can either open the ingame console by typing "-console" into the chat, or directly execute code by typing "-exec <code>".
*       - See IngameConsole script for further documentation.
*
* ------------------
* | Error Handling |
* ------------------
*        - Debug Utils automatically applies error handling (i.e. Debug.try) to code executed by your triggers and timers (error handling means that error messages are printed on screen, if anything doesn't run properly).
*        - You can still use the below library functions for manual debugging.
*
*    Debug.try(funcToExecute, ...) / try(funcToExecute, ...) -> ...
*        - Calls the specified function with the specified parameters in protected mode (i.e. code after Debug.try will continue to run even if the function fails to execute).
*        - If the call is successful, returns the specified function's original return values (so p1 = Debug.try(Player, 0) will work fine).
*        - If the call is unsuccessful, prints an error message on screen (including stack trace and parameters you have potentially logged before the error occured)
*        - By default, the error message consists of a line-reference to war3map.lua (which you can look into by forcing a syntax error in WE or by exporting it from your map via File -> Export Script).
*          You can get more helpful references to local script files instead, see section about "Local script references".
*        - Example: Assume you have a code line like "func(param1,param2)", which doesn't work and you want to know why.
*           Option 1: Change it to "Debug.try(func, param1, param2)", i.e. separate the function from the parameters.
*           Option 2: Change it to "Debug.try(function() return func(param1, param2) end)", i.e. pack it into an anonymous function (optionally skip the return statement).
*    Debug.log(...)
*        - Logs the specified parameters to the Debug-log. The Debug-log will be printed upon the next error being catched by Debug.try, Debug.assert or Debug.throwError.
*        - The Debug-log will only hold one set of parameters per code-location. That means, if you call Debug.log() inside any function, only the params saved within the latest call of that function will be kept.
*    Debug.throwError(...)
*        - Prints an error message including document, line number, stack trace, previously logged parameters and all specified parameters on screen. Parameters can have any type.
*        - In contrast to Lua's native error function, this can be called outside of protected mode and doesn't halt code execution.
*    Debug.assert(condition:boolean, errorMsg:string, ...) -> ...
*        - Prints the specified error message including document, line number, stack trace and previously logged parameters on screen, IF the specified condition fails (i.e. resolves to false/nil).
*        - Returns ..., IF the specified condition holds.
*        - This works exactly like Lua's native assert, except that it also works outside of protected mode and does not halt code execution.
*    Debug.traceback() -> string
*        - Returns the stack trace at the position where this is called. You need to manually print it.
*    Debug.getLine([depth: integer]) -> integer?
*        - Returns the line in war3map.lua, where this function is executed.
*        - You can specify a depth d >= 1 to instead return the line, where the d-th function in the stack trace was called. I.e. depth = 2 will return the line of execution of the function that calls Debug.getLine.
*        - Due to Wc3's limited stack trace ability, this might sometimes return nil for depth >= 3, so better apply nil-checks on the result.
*    Debug.getLocalErrorMsg(errorMsg:string) -> string
*        - Takes an error message containing a file and a linenumber and converts war3map.lua-lines to local document lines as defined by uses of Debug.beginFile() and Debug.endFile().
*        - Error Msg must be formatted like "<document>:<linenumber><Rest>".
*
* -----------------------------------
* | Warnings for undeclared globals |
* -----------------------------------
*        - DebugUtils will print warnings on screen, if you read an undeclared global variable.
*        - This is technically the case, when you misspelled on a function name, like calling CraeteUnit instead of CreateUnit.
*        - Keep in mind though that the same warning will pop up after reading a global that was intentionally nilled. If you don't like this, turn of this feature in the settings.
*
* -----------------
* | Print Caching |
* -----------------
*        - DebugUtils caches print()-calls occuring during loading screen and delays them to after game start.
*        - This also applies to loading screen error messages, so you can wrap erroneous parts of your Lua root in Debug.try-blocks and see the message after game start.
*
* -------------------------
* | Local File Stacktrace |
* -------------------------
*        - By default, error messages and stack traces printed by the error handling functionality of Debug Utils contain references to war3map.lua (a big file just appending all your local scripts).
*        - The Debug-library provides the two functions below to index your local scripts, activating local file names and line numbers (matching those in your IDE) instead of the war3map.lua ones.
*        - This allows you to inspect errors within your IDE (VSCode) instead of the World Editor.
*
*    Debug.beginFile(fileName: string [, depth: integer])
*        - Tells the Debug library that the specified file begins exactly here (i.e. in the line, where this is called).
*        - Using this improves stack traces of error messages. "war3map.lua"-references between <here> and the next Debug.endFile() will be converted to file-specific references.
*        - All war3map.lua-lines located between the call of Debug.beginFile(fileName) and the next call of Debug.beginFile OR Debug.endFile are treated to be part of "fileName".
*        - !!! To be called in the Lua root in Line 1 of every document you wish to track. Line 1 means exactly line 1, before any comment! This way, the line shown in the trace will exactly match your IDE.
*        - Depth can be ignored, except if you want to use a custom wrapper around Debug.beginFile(), in which case you need to set the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.beginFile().
*    Debug.endFile([depth: integer])
*        - Ends the current file that was previously begun by using Debug.beginFile(). War3map.lua-lines after this will not be converted until the next instance of Debug.beginFile().
*        - The next call of Debug.beginFile() will also end the previous one, so using Debug.endFile() is optional. Mainly recommended to use, if you prefer to have war3map.lua-references in a certain part of your script (such as within GUI triggers).
*        - Depth can be ignored, except if you want to use a custom wrapper around Debug.endFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.endFile().
*
* ----------------
* | Name Caching |
* ----------------
*        - DebugUtils overwrites the tostring-function so that it prints the name of a non-primitive object (if available) instead of its memory position. The same applies to print().
*        - For instance, print(CreateUnit) will show "function: CreateUnit" on screen instead of "function: 0063A698".
*        - The table holding all those names is referred to as "Name Cache".
*        - All names of objects in global scope will automatically be added to the Name Cache both within Lua root and again at game start (to get names for overwritten natives and your own objects).
*        - New names entering global scope will also automatically be added, even after game start. The same applies to subtables of _G up to a depth of Debug.settings.NAME_CACHE_DEPTH.
*        - Objects within subtables will be named after their parent tables and keys. For instance, the name of the function within T = {{bla = function() end}} is "T[1].bla".
*        - The automatic adding doesn't work for objects saved into existing variables/keys after game start (because it's based on __newindex metamethod which simply doesn't trigger)
*        - You can manually add names to the name cache by using the following API-functions:
*
*    Debug.registerName(whichObject:any, name:string)
*        - Adds the specified object under the specified name to the name cache, letting tostring and print output "<type>: <name>" going foward.
*        - The object must be non-primitive, i.e. this won't work on strings, numbers and booleans.
*        - This will overwrite existing names for the specified object with the specified name.
*    Debug.registerNamesFrom(parentTable:table [, parentTableName:string] [, depth])
*        - Adds names for all values from within the specified parentTable to the name cache.
*        - Names for entries will be like "<parentTableName>.<key>" or "<parentTableName>[<key>]" (depending on the key type), using the existing name of the parentTable from the name cache.
*        - You can optionally specify a parentTableName to use that for the entry naming instead of the existing name. Doing so will also register that name for the parentTable, if it doesn't already has one.
*        - Specifying the empty string as parentTableName will suppress it in the naming and just register all values as "<key>". Note that only string keys will be considered this way.
*        - In contrast to Debug.registerName(), this function will NOT overwrite existing names, but just add names for new objects.
*    Debug.oldTostring(object:any) -> string
*        - The old tostring-function in case you still need outputs like "function: 0063A698".
*
* -----------------
* | Other Utility |
* -----------------
*
*    Debug.wc3Type(object:any) -> string
*        - Returns the Warcraft3-type of the input object. E.g. Debug.wc3Type(Player(0)) will return "player".
*        - Returns type(object), if used on Lua-objects.
*    table.tostring(whichTable [, depth:integer] [, pretty_yn:boolean])
*        - Creates a list of all (key,value)-pairs from the specified table. Also lists subtable entries up to the specified depth (unlimited, if not specified).
*        - E.g. for T = {"a", 5, {7}}, table.tostring(T) would output '{(1, "a"), (2, 5), (3, {(1, 7)})}' (if using concise style, i.e. pretty_yn being nil or false).
*        - Not specifying a depth can potentially lead to a stack overflow for self-referential tables (e.g X = {}; X[1] = X). Choose a sensible depth to prevent this (in doubt start with 1 and test upwards).
*        - Supports pretty style by setting pretty_yn to true. Pretty style is linebreak-separated, uses indentations and has other visual improvements. Use it on small tables only, because Wc3 can't show that many linebreaks at once.
*        - All of the following is valid syntax: table.tostring(T), table.tostring(T, depth), table.tostring(T, pretty_yn) or table.tostring(T, depth, pretty_yn).
*        - table.tostring is not multiplayer-synced.
*    table.print(whichTable [, depth:integer] [, pretty_yn:boolean])
*        - Prints table.tostring(...).
*
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------]]

    ----------------
    --| Settings |--
    ----------------

    Debug = {
        --BEGIN OF SETTINGS--
        settings = {
                SHOW_TRACE_ON_ERROR = true                      ---Set to true to show a stack trace on every error in addition to the regular message (msg sources: automatic error handling, Debug.try, Debug.throwError, ...)
            ,   INCLUDE_DEBUGUTILS_INTO_TRACE = true            ---Set to true to include lines from Debug Utils into the stack trace. Set to false to exclude those (they 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_COROUTINES = true                    ---Set to true for improved stack traces on errors within coroutines (applies Debug.try on coroutine.create and coroutine.wrap). This lets stack traces point to the erroneous function executed within the coroutine (instead of the function creating the coroutine).
            ,   ALLOW_INGAME_CODE_EXECUTION = true              ---Set to true to enable IngameConsole and -exec command.
            ,   WARNING_FOR_UNDECLARED_GLOBALS = true           ---Set to true to print warnings upon accessing undeclared globals (i.e. globals with nil-value). This is technically the case after having misspelled on a function name (like CraeteUnit instead of CreateUnit).
            ,   SHOW_TRACE_FOR_UNDECLARED_GLOBALS = false       ---Set to true to include a stack trace into undeclared global warnings. Only takes effect, if WARNING_FOR_UNDECLARED_GLOBALS is also true.
            ,   USE_PRINT_CACHE = true                          ---Set to true to let print()-calls during loading screen be cached until the game starts.
            ,   PRINT_DURATION = nil                            ---Adjust the duration in seconds that values printed by print() last on screen. Set to nil to use default duration (which depends on string length).
            ,   USE_NAME_CACHE = true                           ---Set to true to let tostring/print output the string-name of an object instead of its memory location (except for booleans/numbers/strings). E.g. print(CreateUnit) will output "function: CreateUnit" instead of "function: 0063A698".
            ,   AUTO_REGISTER_NEW_NAMES = true                  ---Automatically adds new names from global scope (and subtables of _G up to NAME_CACHE_DEPTH) to the name cache by adding metatables with the __newindex metamethod to ALL tables accessible from global scope.
            ,   NAME_CACHE_DEPTH = 0                            ---Set to 0 to only affect globals. Experimental feature: Set to an integer > 0 to also cache names for subtables of _G (up to the specified depth). Warning: This will alter the __newindex metamethod of subtables of _G (but not break existing functionality).
        }
        --END OF SETTINGS--
        --START OF CODE--
        ,   data = {
                nameCache = {}                                  ---@type table<any,string> contains the string names of any object in global scope (random for objects that have multiple names)
            ,   nameCacheMirror = {}                            ---@type table<string,any> contains the (name,object)-pairs of all objects in the name cache. Used to prevent name duplicates that might otherwise occur upon reassigning globals.
            ,   nameDepths = {}                                 ---@type table<any,integer> contains the depth of the name used by by any object in the name cache (i.e. the depth within the parentTable).
            ,   autoIndexedTables = {}                          ---@type table<table,boolean> contains (t,true), if DebugUtils already set a __newindex metamethod for name caching in t. Prevents double application.
            ,   paramLog = {}                                   ---@type table<string,string> saves logged information per code location. to be filled by Debug.log(), to be printed by Debug.try()
            ,   sourceMap = {{firstLine= 1,file='DebugUtils'}}  ---@type table<integer,{firstLine:integer,file:string,lastLine?:integer}> saves lines and file names of all documents registered via Debug.beginFile().
            ,   printCache = {n=0}                              ---@type string[] contains the strings that were attempted to print during loading screen.
        }
    }
    --localization
    local settings, paramLog, nameCache, nameDepths, autoIndexedTables, nameCacheMirror, sourceMap, printCache = Debug.settings, Debug.data.paramLog, Debug.data.nameCache, Debug.data.nameDepths, Debug.data.autoIndexedTables, Debug.data.nameCacheMirror, Debug.data.sourceMap, Debug.data.printCache

    --Write DebugUtils first line number to sourceMap:
    ---@diagnostic disable-next-line: need-check-nil
    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 > 0 to instead return the line, where the corresponding 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: need-check-nil
        local line = location:match(":\x25d+") --extracts ":1000" from "war3map.lua:1000:..."
        return tonumber(line and line:sub(2,-1)) --check if line is nil before applying string.sub to prevent errors (nil can result from string.match above, although it should never do so in our case)
    end

    ---Tells the Debug library that the specified file begins exactly here (i.e. in the line, where this is called).
    ---
    ---Using this improves stack traces of error messages. Stack trace will have "war3map.lua"-references between this and the next Debug.endFile() converted to file-specific references.
    ---
    ---To be called in the Lua root in Line 1 of every file you wish to track! Line 1 means exactly line 1, before any comment! This way, the line shown in the trace will exactly match your IDE.
    ---
    ---If you want to use a custom wrapper around Debug.beginFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.beginFile().
    ---@param fileName string
    ---@param depth? integer default: 0. Set to 1, if you call this from a wrapper (and use the wrapper in line 1 of every document).
    ---@param lastLine? integer Ignore this. For compatibility with Total Initialization.
    function Debug.beginFile(fileName, depth, lastLine)
        depth, fileName = depth or 0, fileName or '' --filename is not actually optional, we just default to '' to prevent crashes.
        local line = Debug.getLine(depth + 1)
        if line then --for safety reasons. we don't want to add a non-existing line to the sourceMap
            table.insert(sourceMap, {firstLine = line, file = fileName, lastLine = lastLine}) --automatically sorted list, because calls of Debug.beginFile happen logically in the order of the map script.
        end
    end

    ---Tells the Debug library that the file previously started with Debug.beginFile() ends here.
    ---This is in theory optional to use, as the next call of Debug.beginFile will also end the previous. Still good practice to always use this in the last line of every file.
    ---If you want to use a custom wrapper around Debug.endFile(), you need to increase the depth parameter to 1 to record the line of the wrapper instead of the line of Debug.endFile().
    ---@param depth? integer
    function Debug.endFile(depth)
        depth = depth or 0
        local line = Debug.getLine(depth + 1)
        sourceMap[#sourceMap].lastLine = line
    end

    ---Takes an error message containing a file and a linenumber and converts both to local file and line as saved to Debug.sourceMap.
    ---@param errorMsg string must be formatted like "<document>:<linenumber><RestOfMsg>".
    ---@return string convertedMsg a string of the form "<localDocument>:<localLinenumber><RestOfMsg>"
    function Debug.getLocalErrorMsg(errorMsg)
        local startPos, endPos = errorMsg:find(":\x25d*") --start and end position of line number. The part before that is the document, part after the error msg.
        if startPos and endPos then --can be nil, if input string was not of the desired form "<document>:<linenumber><RestOfMsg>".
            local document, line, rest = errorMsg:sub(1, startPos), tonumber(errorMsg:sub(startPos+1, endPos)), errorMsg:sub(endPos+1, -1) --get error line in war3map.lua
            if document == 'war3map.lua:' and line then --only convert war3map.lua-references to local position. Other files such as Blizzard.j.lua are not converted (obiously).
                for i = #sourceMap, 1, -1 do --find local file containing the war3map.lua error line.
                    if line >= sourceMap[i].firstLine then --war3map.lua line is part of sourceMap[i].file
                        if not sourceMap[i].lastLine or line <= sourceMap[i].lastLine then --if lastLine is given, we must also check for it
                            return sourceMap[i].file .. ":" .. (line - sourceMap[i].firstLine + 1) .. rest
                        else --if line is larger than firstLine and lastLine of sourceMap[i], it is not part of a tracked file -> return global war3map.lua position.
                            break --prevent return within next step of the loop ("line >= sourceMap[i].firstLine" would be true again, but wrong file)
                        end
                    end
                end
            end
        end
        return errorMsg
    end
    local convertToLocalErrorMsg = Debug.getLocalErrorMsg

    ----------------------
    --| Error Handling |--
    ----------------------

    local concat
    ---Applies tostring() on all input params and concatenates them 4-space-separated.
    ---@param firstParam any
    ---@param ... any
    ---@return string
    concat = function(firstParam, ...)
        if select('#', ...) == 0 then
            return tostring(firstParam)
        end
        return tostring(firstParam) .. '    ' .. concat(...)
    end

    ---Returns the stack trace between the specified startDepth and endDepth.
    ---The trace lists file names and line numbers. File name is only listed, if it has changed from the previous traced line.
    ---The previous file can also be specified as an input parameter to suppress the first file name in case it's identical.
    ---@param startDepth integer
    ---@param endDepth integer
    ---@return string trace
    local function getStackTrace(startDepth, endDepth)
        local trace, separator = "", ""
        local _, currentFile, lastFile, tracePiece, lastTracePiece
        for loopDepth = startDepth, endDepth do --get trace on different depth level
            _, tracePiece = pcall(error, "", loopDepth) ---@type boolean, string
            tracePiece = convertToLocalErrorMsg(tracePiece)
            if #tracePiece > 0 and lastTracePiece ~= tracePiece then --some trace pieces can be empty, but there can still be valid ones beyond that
                currentFile = tracePiece:match("^.-:")
                --Hide DebugUtils in the stack trace (except main reference), if settings.INCLUDE_DEBUGUTILS_INTO_TRACE is set to true.
                if settings.INCLUDE_DEBUGUTILS_INTO_TRACE or (loopDepth == startDepth) or currentFile ~= "DebugUtils:" then
                    trace = trace .. separator .. ((currentFile == lastFile) and tracePiece:match(":\x25d+"):sub(2,-1) or tracePiece:match("^.-:\x25d+"))
                    lastFile, lastTracePiece, separator = currentFile, tracePiece, " <- "
                end
            end
        end
        return trace
    end

    ---Message Handler to be used by the try-function below.
    ---Adds stack trace plus formatting to the message and prints it.
    ---@param errorMsg string
    ---@param startDepth? integer default: 4 for use in xpcall
    local function errorHandler(errorMsg, startDepth)
        startDepth = startDepth or 4 --xpcall doesn't specify this param, so it must default to 4 for this case
        errorMsg = convertToLocalErrorMsg(errorMsg)
        --Print original error message and stack trace.
        print("|cffff5555ERROR at " .. errorMsg .. "|r")
        if settings.SHOW_TRACE_ON_ERROR then
            print("|cffff5555Traceback (most recent call first):|r")
            print("|cffff5555" .. getStackTrace(startDepth,200) .. "|r")
        end
        --Also print entries from param log, if there are any.
        for location, loggedParams in pairs(paramLog) do
            print("|cff888888Logged at " .. convertToLocalErrorMsg(location) .. loggedParams .. "|r")
            paramLog[location] = nil
        end
    end

    ---Tries to execute the specified function with the specified parameters in protected mode and prints an error message (including stack trace), if unsuccessful.
    ---
    ---Example use: Assume you have a code line like "CreateUnit(0,1,2)", which doesn't work and you want to know why.
    ---* Option 1: Change it to "Debug.try(CreateUnit, 0, 1, 2)", i.e. separate the function from the parameters.
    ---* Option 2: Change it to "Debug.try(function() return CreateUnit(0,1,2) end)", i.e. pack it into an anonymous function. You can skip the "return", if you don't need the return values.
    ---When no error occured, the try-function will return all values returned by the input function.
    ---When an error occurs, try will print the resulting error and stack trace.
    ---@param funcToExecute function the function to call in protected mode
    ---@param ... any params for the input-function
    ---@return ... any
    function Debug.try(funcToExecute, ...)
        return select(2, xpcall(funcToExecute, errorHandler,...))
    end
    try = Debug.try

    ---Prints "ERROR:" and the specified error objects on the Screen. Also prints the stack trace leading to the error. You can specify as many arguments as you wish.
    ---
    ---In contrast to Lua's native error function, this can be called outside of protected mode and doesn't halt code execution.
    ---@param ... any objects/errormessages to be printed (doesn't have to be strings)
    function Debug.throwError(...)
        errorHandler(getStackTrace(4,4) .. ": " .. concat(...), 5)
    end

    ---Prints the specified error message, if the specified condition fails (i.e. if it resolves to false or nil).
    ---
    ---Returns all specified arguments after the errorMsg, if the condition holds.
    ---
    ---In contrast to Lua's native assert function, this can be called outside of protected mode and doesn't halt code execution (even in case of condition failure).
    ---@param condition any actually a boolean, but you can use any object as a boolean.
    ---@param errorMsg string the message to be printed, if the condition fails
    ---@param ... any will be returned, if the condition holds
    function Debug.assert(condition, errorMsg, ...)
        if condition then
            return ...
        else
            errorHandler(getStackTrace(4,4) .. ": " .. errorMsg, 5)
        end
    end

    ---Returns the stack trace at the code position where this function is called.
    ---The returned string includes war3map.lua/blizzard.j.lua code positions of all functions from the stack trace in the order of execution (most recent call last). It does NOT include function names.
    ---@return string
    function Debug.traceback()
        return getStackTrace(3,200)
    end

    ---Saves the specified parameters to the debug log at the location where this function is called. The Debug-log will be printed for all affected locations upon the try-function catching an error.
    ---The log is unique per code location: Parameters logged at code line x will overwrite the previous ones logged at x. Parameters logged at different locations will all persist and be printed.
    ---@param ... any save any information, for instance the parameters of the function call that you are logging.
    function Debug.log(...)
        local _, location = pcall(error, "", 3) ---@diagnostic disable-next-line: need-check-nil
        paramLog[location or ''] = concat(...)
    end

    ------------------------------------
    --| Name Caching (API-functions) |--
    ------------------------------------

    --Help-table. The registerName-functions below shall not work on call-by-value-types, i.e. booleans, strings and numbers (renaming a value of any primitive type doesn't make sense).
    local skipType = {boolean = true, string = true, number = true, ['nil'] = true}
    --Set weak keys to nameCache and nameDepths and weak values for nameCacheMirror to prevent garbage collection issues
    setmetatable(nameCache, {__mode = 'k'})
    setmetatable(nameDepths, getmetatable(nameCache))
    setmetatable(nameCacheMirror, {__mode = 'v'})

    ---Removes the name from the name cache, if already used for any object (freeing it for the new object). This makes sure that a name is always unique.
    ---This doesn't solve the 
    ---@param name string
    local function removeNameIfNecessary(name)
        if nameCacheMirror[name] then
            nameCache[nameCacheMirror[name]] = nil
            nameCacheMirror[name] = nil
        end
    end

    ---Registers a name for the specified object, which will be the future output for tostring(whichObject).
    ---You can overwrite existing names for whichObject by using this.
    ---@param whichObject any
    ---@param name string
    function Debug.registerName(whichObject, name)
        if not skipType[type(whichObject)] then
            removeNameIfNecessary(name)
            nameCache[whichObject] = name
            nameCacheMirror[name] = whichObject
            nameDepths[name] = 0
        end
    end

    ---Registers a new name to the nameCache as either just <key> (if parentTableName is the empty string), <table>.<key> (if parentTableName is given and string key doesn't contain whitespace) or <name>[<key>] notation (for other keys in existing tables).
    ---Only string keys without whitespace support <key>- and <table>.<key>-notation. All other keys require a parentTableName.
    ---@param parentTableName string | '""' empty string suppresses <table>-affix.
    ---@param key any
    ---@param object any only call-be-ref types allowed
    ---@param parentTableDepth? integer
    local function addNameToCache(parentTableName, key, object, parentTableDepth)
        parentTableDepth = parentTableDepth or -1
        --Don't overwrite existing names for the same object, don't add names for primitive types.
        if nameCache[object] or skipType[type(object)] then
            return
        end
        local name
        --apply dot-syntax for string keys without whitespace
        if type(key) == 'string' and not string.find(key, "\x25s") then
            if parentTableName == "" then
                name = key
                nameDepths[object] = 0
            else
                name =  parentTableName .. "." .. key
                nameDepths[object] = parentTableDepth + 1
            end
        --apply bracket-syntax for all other keys. This requires a parentTableName.
        elseif parentTableName ~= "" then
            name = type(key) == 'string' and ('"' .. key .. '"') or key
            name = parentTableName .. "[" .. tostring(name) .. "]"
            nameDepths[object] = parentTableDepth + 1
        end
        --Stop in cases without valid name (like parentTableName = "" and key = [1])
        if name then
            removeNameIfNecessary(name)
            nameCache[object] = name
            nameCacheMirror[name] = object
        end
    end

    ---Registers all call-by-reference objects in the given parentTable to the nameCache.
    ---Automatically filters out primitive objects and already registed Objects.
    ---@param parentTable table
    ---@param parentTableName? string
    local function registerAllObjectsInTable(parentTable, parentTableName)
        parentTableName = parentTableName or nameCache[parentTable] or ""
        --Register all call-by-ref-objects in parentTable
        for key, object in pairs(parentTable) do
            addNameToCache(parentTableName, key, object, nameDepths[parentTable])
        end
    end

    ---Adds names for all values of the specified parentTable to the name cache. Names will be "<parentTableName>.<key>" or "<parentTableName>[<key>]", depending on the key type.
    ---
    ---Example: Given a table T = {f = function() end, [1] = {}}, tostring(T.f) and tostring(T[1]) will output "function: T.f" and "table: T[1]" respectively after running Debug.registerNamesFrom(T).
    ---The name of T itself must either be specified as an input parameter OR have previously been registered. It can also be suppressed by inputting the empty string (so objects will just display by their own names).
    ---The names of objects in global scope are automatically registered during loading screen.
    ---@param parentTable table base table of which all entries shall be registered (in the Form parentTableName.objectName).
    ---@param parentTableName? string|'""' Nil: takes <parentTableName> as previously registered. Empty String: Skips <parentTableName> completely. String <s>: Objects will show up as "<s>.<objectName>".
    ---@param depth? integer objects within sub-tables up to the specified depth will also be added. Default: 1 (only elements of whichTable). Must be >= 1.
    ---@overload fun(parentTable:table, depth:integer)
    function Debug.registerNamesFrom(parentTable, parentTableName, depth)
        --Support overloaded definition fun(parentTable:table, depth:integer)
        if type(parentTableName) == 'number' then
            depth = parentTableName
            parentTableName = nil
        end
        --Apply default values
        depth = depth or 1
        parentTableName = parentTableName or nameCache[parentTable] or ""
        --add name of T in case it hasn't already
        if not nameCache[parentTable] and parentTableName ~= "" then
            Debug.registerName(parentTable, parentTableName)
        end
        --Register all call-by-ref-objects in parentTable. To be preferred over simple recursive approach to ensure that top level names are preferred.
        registerAllObjectsInTable(parentTable, parentTableName)
        --if depth > 1 was specified, also register Names from subtables.
        if depth > 1 then
            for _, object in pairs(parentTable) do
                if type(object) == 'table' then
                    Debug.registerNamesFrom(object, nil, depth - 1)
                end
            end
        end
    end

    -------------------------------------------
    --| Name Caching (Loading Screen setup) |--
    -------------------------------------------

    ---Registers all existing object names from global scope and Lua incorporated libraries to be used by tostring() overwrite below.
    local function registerNamesFromGlobalScope()
        --Add all names from global scope to the name cache.
        Debug.registerNamesFrom(_G, "")
        --Add all names of Warcraft-enabled Lua libraries as well:
        --Could instead add a depth to the function call above, but we want to ensure that these libraries are added even if the user has chosen depth 0.
        for _, lib in ipairs({coroutine, math, os, string, table, utf8, Debug}) do
            Debug.registerNamesFrom(lib)
        end
        --Add further names that are not accessible from global scope:
        --Player(i)
        for i = 0, GetBJMaxPlayerSlots() - 1 do
            Debug.registerName(Player(i), "Player(" .. i .. ")")
        end
    end

    --Set empty metatable to _G. __index is added when game starts (for "attempt to read undeclared global"-errors), __newindex is added right below (for building the name cache).
    setmetatable(_G, getmetatable(_G) or {}) --getmetatable(_G) should always return nil provided that DebugUtils is the topmost script file in the trigger editor, but we still include this for safety-

    -- Save old tostring into Debug Library before overwriting it.
    Debug.oldTostring = tostring

    if settings.USE_NAME_CACHE then
        local oldTostring = tostring
        tostring = function(obj) --new tostring(CreateUnit) prints "function: CreateUnit"
            --tostring of non-primitive object is NOT guaranteed to be like "<type>:<hex>", because it might have been changed by some __tostring-metamethod.
            if settings.USE_NAME_CACHE then --return names from name cache only if setting is enabled. This allows turning it off during runtime (via Ingame Console) to revert to old tostring.
                return nameCache[obj] and ((oldTostring(obj):match("^.-: ") or (oldTostring(obj) .. ": ")) .. nameCache[obj]) or oldTostring(obj)
            end
            return Debug.oldTostring(obj)
        end
        --Add names to Debug.data.objectNames within Lua root. Called below the other Debug-stuff to get the overwritten versions instead of the original ones.
        registerNamesFromGlobalScope()

        --Prepare __newindex-metamethod to automatically add new names to the name cache
        if settings.AUTO_REGISTER_NEW_NAMES then
            local nameRegisterNewIndex
            ---__newindex to be used for _G (and subtables up to a certain depth) to automatically register new names to the nameCache.
            ---Tables in global scope will use their own name. Subtables of them will use <parentName>.<childName> syntax.
            ---Global names don't support container[key]-notation (because "_G[...]" is probably not desired), so we only register string type keys instead of using prettyTostring.
            ---@param t table
            ---@param k any
            ---@param v any
            ---@param skipRawset? boolean set this to true when combined with another __newindex. Suppresses rawset(t,k,v) (because the other __newindex is responsible for that).
            nameRegisterNewIndex = function(t,k,v, skipRawset)
                local parentDepth = nameDepths[t] or 0
                --Make sure the parent table has an existing name before using it as part of the child name
                if t == _G or nameCache[t] then
                    local existingName = nameCache[v]
                    if not existingName then
                        addNameToCache((t == _G and "") or nameCache[t], k, v, parentDepth)
                    end
                    --If v is a table and the parent table has a valid name, inherit __newindex to v's existing metatable (or create a new one), if that wasn't already done.
                    if type(v) == 'table' and nameDepths[v] < settings.NAME_CACHE_DEPTH then
                        if not existingName then
                            --If v didn't have a name before, also add names for elements contained in v by construction (like v = {x = function() end} ).
                            Debug.registerNamesFrom(v, settings.NAME_CACHE_DEPTH - nameDepths[v])
                        end
                        --Apply __newindex to new tables.
                        if not autoIndexedTables[v] then
                            autoIndexedTables[v] = true
                            local mt = getmetatable(v)
                            if not mt then
                                mt = {}
                                setmetatable(v, mt) --only use setmetatable when we are sure there wasn't any before to prevent issues with "__metatable"-metamethod.
                            end
                            ---@diagnostic disable-next-line: assign-type-mismatch
                            local existingNewIndex = mt.__newindex
                            local isTable_yn = (type(existingNewIndex) == 'table')
                            --If mt has an existing __newindex, add the name-register effect to it (effectively create a new __newindex using the old)
                            if existingNewIndex then
                                mt.__newindex = function(t,k,v)
                                    nameRegisterNewIndex(t,k,v, true) --setting t[k] = v might not be desired in case of existing newindex. Skip it and let existingNewIndex make the decision.
                                    if isTable_yn then
                                        existingNewIndex[k] = v
                                    else
                                        return existingNewIndex(t,k,v)
                                    end
                                end
                            else
                            --If mt doesn't have an existing __newindex, add one that adds the object to the name cache.
                                mt.__newindex = nameRegisterNewIndex
                            end
                        end
                    end
                end
                --Set t[k] = v.
                if not skipRawset then
                    rawset(t,k,v)
                end
            end

            --Apply metamethod to _G.
            local existingNewIndex = getmetatable(_G).__newindex --should always be nil provided that DebugUtils is the topmost script in your trigger editor. Still included for safety.
            local isTable_yn = (type(existingNewIndex) == 'table')
            if existingNewIndex then
                getmetatable(_G).__newindex = function(t,k,v)
                    nameRegisterNewIndex(t,k,v, true)
                    if isTable_yn then
                        existingNewIndex[k] = v
                    else
                        existingNewIndex(t,k,v)
                    end
                end
            else
                getmetatable(_G).__newindex = nameRegisterNewIndex
            end
        end
    end

    ------------------------------------------------------
    --| Native Overwrite for Automatic Error Handling  |--
    ------------------------------------------------------

    --A table to store the try-wrapper for each function. This avoids endless re-creation of wrapper functions within the hooks below.
    --Weak keys ensure that garbage collection continues as normal.
    local tryWrappers = setmetatable({}, {__mode = 'k'}) ---@type table<function,function>
    local try = Debug.try

    ---Takes a function and returns a wrapper executing the same function within Debug.try.
    ---Wrappers are permanently stored (until the original function is garbage collected) to ensure that they don't have to be created twice for the same function.
    ---@param func? function
    ---@return function
    local function getTryWrapper(func)
        if func then
            tryWrappers[func] = tryWrappers[func] or function(...) return try(func, ...) end
        end
        return tryWrappers[func] --returns nil for func = nil (important for TimerStart overwrite below)
    end

    --Overwrite TriggerAddAction, TimerStart, Condition and Filter 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_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
                printCache.n = printCache.n + 1
                printCache[printCache.n] = concat(...)
            end
        end
    end

    -------------------------
    --| Modify Game Start |--
    -------------------------

    local originalMarkGameStarted = MarkGameStarted
    --Hook certain actions into the start of the game.
    MarkGameStarted = function()
        originalMarkGameStarted()
        if settings.WARNING_FOR_UNDECLARED_GLOBALS then
            local existingIndex = getmetatable(_G).__index
            local isTable_yn = (type(existingIndex) == 'table')
            getmetatable(_G).__index = function(t, k) --we made sure that _G has a metatable further above.
                --if string.sub(tostring(k),1,3) ~= 'bj_' then
                    print("Trying to read undeclared global at " .. getStackTrace(4,4) .. ": " .. tostring(k)
                        .. (settings.SHOW_TRACE_FOR_UNDECLARED_GLOBALS and "\nTraceback (most recent call first):\n" .. getStackTrace(4,200) or ""))
                --end
                if existingIndex then
                    if isTable_yn then
                        return existingIndex[k]
                    end
                    return existingIndex(t,k)
                end
                return rawget(t,k)
            end
        end

        --Add names to Debug.data.objectNames again to ensure that overwritten natives also make it to the name cache.
        --Overwritten natives have a new value, but the old key, so __newindex didn't trigger. But we can be sure that objectNames[v] doesn't yet exist, so adding again is safe.
        if settings.USE_NAME_CACHE then
            for _,v in pairs(_G) do
                nameCache[v] = nil
            end
            registerNamesFromGlobalScope()
        end

        --Print messages that have been cached during loading screen.
        if settings.USE_PRINT_CACHE then
            --Note that we don't restore the old print. The overwritten variant only applies caching behaviour to loading screen prints anyway and "unhooking" always adds other risks.
            for _, str in ipairs(printCache) do
                print(str)
            end ---@diagnostic disable-next-line: cast-local-type
            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()
 
Level 24
Joined
Feb 9, 2009
Messages
1,787
1674980977204.png

Mind if I bogart some lessons?
For context I just made a new LUA map and dragged over Bribe's Damage Engine for the first time.
(And apologies if it's plain as day on in the header but I'm struggling.)

What does this series of messages convey?
Normally when I see undeclared amount on a variable I assume it's a variable editor issue, but finding the mentioned variables has them working perfectly fine.
Lastly I assume the "XXXX" number refers to line?
 
Level 20
Joined
Jul 10, 2009
Messages
478
What does this series of messages convey?
The "Trying to read undeclared global"-messages indicate that your code (or any system you imported) has accessed a global variable that didn't hold a value (in other words, it contained Lua's representative for the missing value: nil).
Keep in mind that Lua allows you to read any name from global scope at all times, so it can't distinguish undeclared globals from intentionally nilled ones.

Normally when I see undeclared amount on a variable I assume it's a variable editor issue, but finding the mentioned variables has them working perfectly fine.
@Bribe can probably provide information about whether his Damage Engine reads variables that were intentionally nilled or not.
If yes, I suggest to get rid of the warnings by deactivating the undeclared-globals-messages (set WARNING_FOR_UNDECLARED_GLOBALS to false in the configuration section).

Regarding variable editor, you don't need it in Lua, because you can assign global variables anywhere in your code (or even better: save your variables into a table). The only reason to use the variable editor is when you need compatibility with GUI (which is exactly why Damage Engine is using it).

You can consult A comprehensive guide to Mapping in Lua for more info about proper Lua coding.

Lastly I assume the "XXXX" number refers to line?
Yes, it's referring to the line number within war3map.lua (big document containing all of your scripts).
You can read the Debug Utils main page to learn how to interpret an error message (Details -> Error Handling).
Feel free to let me know, if anything remains unclear, so I can improve the resource description.
 
Level 24
Joined
Feb 9, 2009
Messages
1,787
Thank you so much for the quick and heavily detailed response!
Regarding variable editor, you don't need it in Lua, because you can assign global variables anywhere in your code (or even better: save your variables into a table). The only reason to use the variable editor is when you need compatibility with GUI (which is exactly why Damage Engine is using it).

You can consult A comprehensive guide to Mapping in Lua for more info about proper Lua coding.
Yes! I'm very pleased to do away with the variable editor and reading Bribe's exploits and conquest into LUA got me really excited.
To which I stumbled on your guide which I've been following up until I ran off course because I stopped to create a LUA map to import the systems mentioned in the beginnings of the Getting_Started/Executing_Code/Executing_Lua-code_in_Wc3

I got distracted AGAIN because I wanted to play with an old beginner spell that I fiddled with in lua a couple years ago;
I then installed Bribe's Damage system to see if I was just dumb but then the error I posted above showed up and now it makes me want to cry.
My hope was to solve it on my own before bothering Bribe in case it was something caused by my own hand.

EDIT
Yes, it's referring to the line number within war3map.lua (big document containing all of your scripts).

Where can I find this to better locate the line flagged?

The spell is just a simple 5 second DOT that I gleamed from following a tutorial on youtube.
I've added (%X Missing HP) to "dmg" that is running into issues referencing the damage being dealt vs the damage that print was displaying.
Lua:
function IgniteSeteupTrigger()
    IgniteId = 1093677104
    local newtrig = CreateTrigger()
    TriggerRegisterAnyUnitEventBJ(newtrig, EVENT_PLAYER_UNIT_SPELL_EFFECT) -- A Unit Starts the effect of an ability
    TriggerAddAction(newtrig, function()
   
        if GetSpellAbilityId() == IgniteId then
            local u = GetTriggerUnit() --casting unit
            local t = GetSpellTargetUnit() --target unit of the ability being cast
            local timer = CreateTimer() --a timer that will periodically damage the target
            local dmg = 40+10*GetUnitAbilityLevel(u, IgniteId)+GetHeroInt(u, true)*5.5+( ( GetUnitStateSwap(UNIT_STATE_MAX_LIFE, t) - GetUnitStateSwap(UNIT_STATE_LIFE, t) ) * 0.16 ) --10/20/30 damage per second
            local ticks = 0 -- how many ticks have passed, increasees by 1 per second
            local maxticks = 10 --once ticks reaches this value the spell ends
            local dmgrate = dmg/maxticks
            local sfx = AddSpecialEffectTarget("Abilities\\Spells\\Other\\Incinerate\\IncinerateBuff.mdl", t, "chest")
            TimerStart(timer, 0.50, true, function()
                if ticks < maxticks then
                    ticks = ticks + 1
                    UnitDamageTarget(u, t, dmg/maxticks, nil, nil, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, nil) --Deals Spell Damage
                    print(dmgrate)
                else
                    DestroyEffect(sfx)
                    PauseTimer(timer)
                    DestroyTimer(timer)
                end        
            end)
        end
    end)
 end
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
Where can I find this to better locate the line flagged?
I recommended to read the Error Handling chapter in the Details tab, because I thought it would answer exactly this question. :p This is the passage in question:

Interpreting Error Messages

An error message will look like this:

ErrorWithTrace.png


You see that the message contains a file, a line number, a description and a stack trace.
The file will usually be war3map.lua, which is generated by the World Editor by appending all your trigger editor scripts to one big file, and thus contains all user-written code. This is a sad fact, because it requires us to look into the generated file to make sense of the given line number and get to the code causing the issue.

We can do so in two ways:
  1. Force a syntax error in your map (I usually put any single letter, say x, into any empty line of my code and save the map). A popup will appear, allowing you to scroll to the desired line. You typically want to use PageUp/PageDown buttons for that, because the mouse wheel takes ages.
    Note that the line number in the popup can be off by one from that in the error message.
  2. You can also export war3map.lua from your World Editor via File -> Export Script.
Both ways are applicable, while Warcraft is still up and running.
After finding the erroneous line, we can start investigating on the issue. As we will see in the chapter about Ingame Console, this doesn't even require closing Warcraft 3.

The subsequent paragraph tells you how to convert war3map.lua-references to local ones, so you don't always have to do the syntax error trick:

Replace war3map.lua references with local references

The war3map.lua-references are quite annoying, because they force us to waste time on opening and searching through the generated file on every new error. Wouldn't it be nicer to have the same file names and line numbers as in your development environment? Well, Debug Utils allows for that, but it requires a small bit of effort to get started:
You may add the line Debug.beginFile(<fileName>) (<fileName> must be a string) to line 1 of any script and Debug.endFile() to the very last line of the same script. Now, if an error occurs between the two, Debug Utils treats it as part of <fileName> and converts both the file name and the line number in its error message.
If you do this for every script you have, you get the same files and lines as in your IDE within every error message.
A little bit annoying to setup, but probably worth it.

Example (that is randomly assumed to start at war3map.lua:2230):
Lua:
Debug.beginFile("ExampleFile") --war3map.lua:2230, ExampleFile:1
--war3map.lua:2231, ExampleFile:2
---Adds one to the input parameter --war3map.lua:2232, ExampleFile:3
---@param x number --war3map.lua:2233, ExampleFile:4
---@return number --war3map.lua:2234, ExampleFile:5
function foo(x) --war3map.lua:2235, ExampleFile:6
return x+1 --war3map.lua:2236, ExampleFile:7
end --war3map.lua:2237, ExampleFile:8
--war3map.lua:2238, ExampleFile:9
function bar() --war3map.lua:2239, ExampleFile:10
print(foo(true)) --war3map.lua:2400, ExampleFile:11
end --war3map.lua:2401, ExampleFile:12
--war3map.lua:2402, ExampleFile:13
Debug.endFile() --war3map.lua:2403, ExampleFile:14
Again, Debug.beginFile(<fileName>) marks line 1 of the document, so you must call it in the first line (even before comments) to really match the lines in your IDE!

ErrorWithTraceConverted.png

Let me know if anything remains unclear!
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,467
I've been doing some really low-level testing of Event in ZeroBrane Studio with mocked War3 API, and because ZeroBrane's environment is not as restricted as WarCraft 3's, it allows you to get the local variable names from within the table you're printing. I couldn't find anything in a reasonable amount of time that did what I wanted, so after many iterations of working with GPT to find what's possible and tailor it as we went, we came up with this module:
Lua:
--- Finds the name of a given variable by searching in global and local scopes.
--- @param value any        - The value to find the variable name for.
--- @param level integer    - The stack level to start searching for local variables.
--- @return string|nil      - The name of the variable or nil if not found.
local function findVariableName(value, level)
    -- Check in global variables
    for k, v in pairs(_G) do
        if v == value then return k end
    end

    -- Check in local variables of the calling function
    local i = 1
    while true do
        local name, val = debug.getlocal(level + 1, i)
        if not name then break end
        if val == value then return name end
        i = i + 1
    end

    return nil
end

--- Generates a pretty-printed string representation of a table, including nested tables.
--- @param object any           - The object to pretty-print.
--- @param depth? integer       - The maximum depth to recurse into nested tables. Default is unlimited (-1).
--- @param indent? string       - The string used for indentation. Default is two spaces.
--- @param constTable? table    - A table used internally to keep track of references. Should generally be left nil.
--- @return string              - The pretty-printed string.
local function prettyTostring(object, depth, indent, constTable)
    depth = depth or -1
    constTable = constTable or {}
    indent = indent or '  '

    local objType = type(object)
    if objType == 'string' then
        return '\'' .. object .. '\''
    elseif objType == 'table' and depth ~= 0 then
        if not constTable[object] then
            local name = findVariableName(object, 3)  -- 3 to go up to the caller of prettyTostring
            constTable[object] = (name and (objType .. ': ' .. name)) or tostring(object)
            if next(object) == nil then
                return constTable[object] .. ': {}'
            else
                local mappedKV = {}
                for k, v in pairs(object) do
                    local debugKey = type(k) == 'string' and '\'' .. k .. '\'' or tostring(k)
                    local debugValue = type(v) == 'string' and '\'' .. v .. '\'' or prettyTostring(v, depth - 1, indent .. '  ', constTable)
                    table.insert(mappedKV, '\n  ' .. indent .. '[' .. debugKey .. '] = ' .. debugValue)
                end
                return constTable[object] .. ': {' .. table.concat(mappedKV, ',') .. '\n' .. indent .. '}'
            end
        else
            return constTable[object]
        end
    end
    return tostring(object)
end

--- Prints a pretty-printed string representation of an object.
--- @param obj any The object to pretty-print.
return function(obj)
    print(prettyTostring(obj))
end

I ran this test:
Lua:
local printTable = require 'print table'
local testTable = {
    a = 1,
    b = "hello",
    c = {
        d = 2,
        e = "world"
    }
}
testTable.d = testTable
printTable(testTable)
And got this output:
Lua:
table: testTable: {
    ['d'] = table: testTable,
    ['a'] = 1,
    ['b'] = 'hello',
    ['c'] = table: 0105c3d0: {
      ['d'] = 2,
      ['e'] = 'world'
    }
  }
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
478
VS Code found problems in these files... DebugUtils2.0a 5+, IngameConsole 9+
nvm, already fixed by restarting the app lol.
These issues might come back after opening one of those files, as sumneko extension prioritizes checks on currently opened docs. I had solved all sumneko warnings concerning Debug Utils in the past, but updates to the extension introduced more checks and thus created warnings that didn't exist before. These warnings do not impact the functionality of the resource, but they surely can be annoying at times.
I suggest to add the line ---@diagnostic disable at the top of all external resources to fix this issue (which disables checks in the respective file only). Be careful to not do this in line 1, though, but below any Debug.beginFile()-statement.

So installation-wise, you did everything right by just keeping the files in your workspace. :)
 
Level 19
Joined
Jan 3, 2022
Messages
320
@Eikonium I though about "why not on first line???" and now I get why: it expects its own line to be on Line 1. Why not add an optional parameter to beginFile that says on which line the function call is?

Well wait before you do this, how does it affect beginFile? I see it already gets its own line inside current file, what does it matter if it's on line 1 or not? The line offsets between beginFile and endFile are still going to be relative?

PS: You use so many clever tricks, it's worth its own article for description.
 
Level 20
Joined
Jul 10, 2009
Messages
478
I though about "why not on first line???" and now I get why: it expects its own line to be on Line 1. Why not add an optional parameter to beginFile that says on which line the function call is?
I had that line-parameter in an early dev version of Debug Utils 2.0, but eventually decided against it. Having a call Debug.beginFile("fileName", n) in line n of your file (could be line 100, because below documentation) forces you to change that number everytime you change the code or the documentation above it (which is a big maintenance task). The risk of forgetting this and producing wrong line numbers in error messages is very high and could lead to hours of debugging the wrong line of code. I think this would be most true for beginner level Lua developers, who need good debugging funtionality the most, but it's certainly true for myself. Yes it would be the user's decision to do this or not, but I didn't want to lay traps here, so that was my reasoning for simply forcing line 1. Feel free to challenge me on this decision, though ;)

Well wait before you do this, how does it affect beginFile? I see it already gets its own line inside current file, what does it matter if it's on line 1 or not? The line offsets between beginFile and endFile are still going to be relative?
The goal was to get the same line numbers in Wc3 ingame error messages as you have in your IDE, so it was crucial to know what line 1 in your IDE is. You can theoretically use Debug.beginFile() in line 2, but then errors occuring in line 10 of your IDE would be shown as line 9 in Wc3. Unless we introduce that extra parameter, which I didn't want for the reason mentioned above.

PS: You use so many clever tricks, it's worth its own article for description.
Haha :D A lot of thought went into it for sure and many people contributed (you amongst these nice ones).
 
Top