• 🏆 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.)

Level 20
Joined
Jul 10, 2009
Messages
477
Debug Utils 2.0a

Overview

Quick Reference

Details

Installation

Changelog


A debug library for Lua mapmaking that will save you tons of time.

Feature Overview:
  • Automatic ingame error messages.
  • Ingame Console (execute code via Wc3 ingame chat).
  • Debug-Library functions for error handling and investigation.
  • Automatic warnings upon reading undeclared globals.
  • Print Caching: Prints during loading screen will be executed after game start.
  • Name Caching: tostring and print will show the actual string-name of an object.
Refer to the tab Quick Reference for a feature summary or to Details for more in-depth explanations.

This resource was a lot of work. Be gentle upon providing feedback and leave a like, if you found it helpful.

Motivation

Without the help of a resource like this, Wc3 mapping in Lua is rather prone to bugs. One main reason is Lua's missing type-safety: the language doesn't allow to denote the types of function parameters and return values, so the Lua syntax check can't inform you, if you passed the wrong type. The same holds, if you pass the wrong number of arguments to a function, accidently swap two arguments or misspell the name of anything (like CraeteUnit): Lua syntax check won't tell you. It can't in fact, because playing with argument types and argument counts is legit to do on purpose.
Even worse, Warcraft by default won't even inform you about breaking code ingame. If a trigger fails to execute, it will fail silently. You will simply see that nothing happens, when it should.

If you are coding without proper safeguard in place, a simple mistake like any of the above will silently break your code, costing you hours to debug an issue that Jass Syntax check would have spotted before even starting the map.

Lua theoretically offers its own debug library, but Blizzard disabled it in Warcraft Reforged for an unknown reason.
Clustering thousands of prints into your code is clearly not a good alternative, so what shall we do?

This resource attempts to solve all of these issues. It enables automatic ingame error messages, provides useful tools for bug investigation, gives advice on how to setup a proper developing environment to compensate for Lua's bad syntax check and even allows for ingame code execution.

Avoiding Bugs

Debug Utils provides error handling functionality, but it doesn't prevent you from causing them in the first place. Clearly, being supported in avoiding bugs is just as important as being supported in solving them, so let's begin with some good advice before proceeding with the resource.

Use a proper development environment
Switching to a proper development environment was one of the most effective things I’ve ever done to make my coding more efficient and joyful. That fact should go without saying, but I know from personal experience that working in a text editor (like Notepad++) or even in the Wc3 editor doesn’t feel bad enough as long as you haven’t tested the alternatives. Trust me, once you tried a proper development environment (like VS Code), you will never want to go back.

I personally use Visual Studio Code with sumneko’s lua extension “sumneko.lua”. It offers syntax highlighting, auto-complete, function parameter preview and many other intellisense features.

VSCode_AutoComplete.png


Attached to this post, you also find the definition files common.lua, blizzard.lua and HiddenNatives.lua that you can use with the mentioned sumneko extension. They enable auto-completion and parameter preview for all Wc3 natives, just as shown in the screenshot above.

A comprehensive guide to mapping in Lua (Getting Started -> IDE Setup) contains all the instructions you need to setup Visual Studio Code and import the definition files.

Annotate your code
The sumneko extension in VS Code also enables the option to annotate your code.
An annotation is a special type of comment that provides additional standardized documentation, which your development environment might depend on to provide certain intellisense features like type checking and parameter preview.

EmmyAnnotation.png

Annotations play a crucial role in Lua programming, because they are the only option for denoting function input and output parameter types, compensating for Lua's missing type safety by letting your development environment conduct the syntax check that WE isn't capable of.

Adding several lines of comments to every single function can feel annoying at first, but trust me, it's worth it. Being lazy on annotation will lead to bugs and cost you time, so better don't be lazy.
Consult this guide about Emmy annotation, if you want to learn more.

Credits

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


Quick Reference

Debug Utils enables the features and library functions listed below.
The ⚙️-boxes contain configuration settings related to the feature.
You can change the configuration in the downloaded code of Debug Utils (scroll down to directly below the documentation).


FeatureExplanation
Automatic Error Handling
Debug Utils will automatically print error messages for code executed within triggers or timers.
The error message will contain the relevant document, line number, description and a stack trace.

ErrorWithTrace.png

⤴️Please refer to the Details tab (chapter: Error Handling) to learn how to interpret an error message like this.

⚙️
Lua:
--Activate Automatic Error Handling:
--TRY refers to the library function Debug.try.
USE_TRY_ON_TRIGGERADDACTION = true -- (default)
USE_TRY_ON_CONDITION = true -- (default)
USE_TRY_ON_TIMERSTART = true -- (default)
USE_TRY_ON_COROUTINES = true -- (default)
--Include Stack Traces:
SHOW_TRACE_ON_ERROR = true -- (default)
Convert war3map.lua-referencesUse Debug.beginFile(<fileName>) in the very first line (even above comments!!) and Debug.endFile() in the very last line of any script file. This will convert references for errors happening in that file from war3map.lua to the specified file name (and adjust the line number accordingly).

ErrorWithTraceConverted.png
Ingame Code ExecutionType -exec <code> into the ingame chat to execute <code> and print return values on screen.

Type -console to open an Ingame Console that will execute any further chat input as code, showing inputs, outputs, timestamps, prints and errors in different colors.
Type exit to close the console.

IngameConsole_small.png


⚙️Requires ALLOW_INGAME_CODE_EXECUTION = true in the settings.

You can also download @Luashine's Console Paste Helper. This little tool helps with pasting code from outside Wc3 to the Ingame Console to effectively bypass the restrictions of the Wc3 ingame chat.
Warnings for undeclared globalsDebug Utils prints ingame warnings, if you try to access undeclared globals (i.e. a global variables that are nil). This is technically the case, when you have misspelled any function name, like CraeteUnit instead of CreateUnit.
Takes effect after game start to prevent warnings for library dependency checks during loading screen.

undeclaredGlobals.png

⚙️
Lua:
--Activate undeclared global warnings
WARNING_FOR_UNDECLARED_GLOBALS = true
--Include stack trace into undeclared global warnings
SHOW_TRACE_FOR_UNDECLARED_GLOBALS = true

Bear in mind though that Lua can't distinguish undeclared globals from those intentionally nilled. If you encounter too many undesired warnings, better disable the feature.
Manual Error HandlingThe below library functions can catch/throw errors, which looks just like the screenshot presented in the Automatic Error Handling section.

⚙️Stack trace is included, if SHOW_TRACE_ON_ERROR is set to true.
  • Debug.try(func, ...)
    will execute func(...) in protected mode, returning all return values from func(...) if successful, and displaying an error message otherwise.

  • assert(condition, errorMsg, ...)
    will return ..., if the condition holds, and otherwise throws errorMsg and halts code execution.
    errorMsg is only visible in protected mode, which Debug Utils' automatically applies to all code from triggers and timers. Hence, you can freely use assert in such code.

  • Debug.assert(condition, errorMsg, ...)
    works exactly like assert, except that it can be used outside of protected mode (such as in Lua root) and doesn't halt code execution.

  • error(errorMsg)
    halts code execution and throws an error consisting of the specified message, if in protected mode.
    Again, Debug Utils' automatic error handling makes sure you can use this within code executed by triggers and timers.

  • Debug.throwError(...)
    prints an error message consisting of all specified arguments on screen.
    Similar to error, but can be used outside of protected mode and doesn't halt code execution.
Error InvestigationThese library functions help investigating on errors.
  • Debug.log(...)
    writes all specified parameters to the Debug-log, which is printed upon the next error being catched (by any of the library functions above). This only saves the last execution at any particular code location, so you don't need to worry about the Debug-log getting flooded with information.
    Debug.log.png


  • Debug.wc3Type(object)
    returns the Warcraft 3 type, if object is a Wc3-object. Returns the Lua-type otherwise.
    Lua:
    Debug.wc3Type(Player(0)) --> "player"
    Debug.wc3Type({}) --> "table"

  • table.print(T:table, [depth:integer], [pretty_yn:boolean])
    alternatively table.print(T, pretty_yn), prints all (key,value)-pairs inside T and subtables on screen.
    You can quickly take a look into your data structures this way.
    Concise version: (table.print(T))
    table.print.png

    Pretty version: (table.print(T,true))
    table.print_pretty.png


  • Debug.traceback()
    returns the stack trace at the point where this is called.
    Errors print a stack trace anyway, but this function can output traces without a given error.

  • Debug.getLine()
    returns the line in war3map.lua, where this is called (not converted to local file).
Print Override
⚙️
Lua:
--Delay loading screen prints to after game start
USE_PRINT_CACHE = true
--Set Print Duration (leave nil for default behaviour)
PRINT_DURATION = nil ---@type number
Name CachingDebug Utils will cache the string name for objects in global scope and override print and tostring to show these names.
  • Printing global objects will show their name instead of their memory position.
    Lua:
    print(CreateUnit)
    --> function: 0000016F29E16700  (if feature is turned off)
    --> function: CreateUnit  (if feature is turned on)
    The same applies to tostring.
  • New Globals will automatically be added to the name cache.
    Lua:
    T = {} --declared after game start
    print(T) --> table: T
  • Even objects within subtables of global scope will automatically get a name up to the depth specified in the settings.
    Lua:
    T = {loc = Location(0,0)}
    print(T.loc) --> locaton: T.loc (assuming you have set a depth of 1 or higher)
⚙️
Lua:
--Activate Name Cache for existing globals
USE_NAME_CACHE = true -- (default)
--Automatically add new globals to the name cache (adds __newindex to _G)
AUTO_REGISTER_NEW_NAMES = true -- (default)
--Auto-name new objects within subtables of _G, up to the specified depth
--Adds __newindex to every(!) such subtable
NAME_CACHE_DEPTH = 4 --deactivate by setting to 0.

Library functions
(require USE_NAME_CACHE = true)
  • Debug.registerName(object:any, name:string)
    adds the specified name for the specified object to the name cache. Use this for any object not affected by the automatic features listed above, for example locals.
  • Debug.registerNamesFrom(parentTable:table, [parentTableName:string], [maxDepth:integer])
    will add names for all elements within the specified parentTable to the name cache, using the best applicable notation out of <parentTableName>[<childName>] and <parentTableName>.<childName>.
  • Debug.oldTostring(object)
    returns the old name of any object (i.e. without using the name cache).

In-Depth Feature Discussion

Ingame Console and -exec command

A usual thing upon encountering a bug in our map during testing is that we want to immediately investigate on it and retrieve as much information from the running game state as possible. Unfortunately, adding prints to our code requires restarting Wc3 and trying to reproduce the error, which is time-consuming and annoying.

DebugUtils helps out with this issue by enabling Ingame Code Execution via either an Ingame Console or using the -exec command.

⚙️You can disable the functionality for ingame code execution by setting ALLOW_INGAME_CODE_EXECUTION to false.

-exec command

Typing -exec <code> ingame will directly execute <code>.

For instance, if we have a data structure x that we consider related to the bug, we might just type -exec table.print(x) and hopefully spot the issue in a blink of an eye (table.print will be discussed later). We can also use Wc3 natives, e.g. -exec CreateUnit(Player(0), FourCC('hfoo'), 0,0,0) would create a Footman at location (0,0).
We may even define further code, like first -exec function a(x) return x end and then -exec print(a('Test')).

The -exec command prints all return values given by the executed statement. As such, you an also use it for term evaluation, i.e. type -exec 2+5 to see 7 being printed, or -exec v to see the value stored inside the variable v.

ExecCommand.png


The -exec command is a niche utility, though. You typically use the Ingame Console instead, which is presented below.

Ingame Console

With Debug Utils in your map, you can open up an Ingame Console by typing -console into the Ingame Chat. It will execute any further chat input as code, except input beginning with a hyphen (to still allow for trigger-based chat commands).
The console supports function execution, variable declaration (consequently function definitions), term statements (like just writing y to see whats inside) and multiline-input.
It shows all inputs and outputs, including prints and error messages, in different colors.

2_4_Console.jpg


The Wc3 ingame chat obviously misses a lot of features that other console inputs typically have. It's limited to a low amount of characters. It misses the ability to use up and down-arrow to navigate through all previous inputs. You can't even use left and right arrow to move the text cursor, because this will instead move Warcraft's ingame camera (or can you actually move the text cursor? If you know how, leave a comment!).
You can however use Home and End buttons on your keyboard to jump to the very beginning or end of the input. This often comes in handy.

Multiline-Input

You can prevent a chat input from being immediately executed by preceeding it with the '>' character. All lines entered this way are halted, until the first line without '>' is entered, which will execute all halted lines (and itself) in one chunk.

Examplary Chat Input:
Lua:
>function foo(x,y)
>return x+y
end
The preceeding '>' are not part of the code, but they just halt execution. The end line is not preceeded, so the whole chunk is executed at that point.

Multiplayer

The console is fully multiplayer-compatible, allowing both separate consoles for each player as well as a single shared one. In a multiplayer testgame, every player typing -console will get their own reserved console, which they can share by typing share afterwards (which is a reserved keyword). Sharing a console will close other player's active consoles.
No matter if separate or shared, all players will always code in the same environment. That means, if any player enters a = 5, all players can do print(a) and will see “5” being printed.

Reserved Keywords

Lua:
 --[[---------------
 |Reserved Keywords|
 -------------------
 The following keywords have a reserved functionality, i.e. are direct commands for the console and will not be interpreted as code:
 - 'help'          - will show a list of all reserved keywords and short explanations.
 - 'exit'          - will shut down the console
 - 'share'         - will share the players console with every other player, allowing others to read and write into it. Will force-close other players consoles, if they have one active.
 - 'clear'         - will clear all text from the console, except the word 'clear'
 - 'lasttrace'     - will show the stack trace of the latest error that occured within IngameConsole
 - 'show'          - will show the console, after it was accidently hidden (you can accidently hide it by showing another multiboard, while the console functionality is still up and running).
 - 'printtochat'   - will let the print function return to normal behaviour (i.e. print to the chat instead of the console).
 - 'printtoconsole'- will let the print function print to the console (which is default behaviour).
 - 'autosize on'   - will enable automatic console resize depending on the longest string in the display. This is turned on by default.
 - 'autosize off'  - will disable automatic console resize and instead linebreak long strings into multiple lines.
 - 'textlang eng'  - lets the console use english Wc3 text language font size to compute linebreaks (look in your Blizzard launcher settings to find out)
 - 'textlang ger'  - lets the console use german Wc3 text language font size to compute linebreaks (look in your Blizzard launcher settings to find out)--]]

Restrictions

The console is based off a Wc3 multiboard, so opening it will hide other active multiboards. If you need other multiboards to stay open, better rely on the -exec command.
As such, the console also needs to compute linebreaks manually, so those will not be perfect. A big bummer during development was that the font size is actually depending on the Wc3 installation text language (which can be changed in the Blizzard launcher). E.g. the letter y consumes less pixels in English text language than it does in German text language. The console is currently only optimized for English and German (default: English), so using other languages might in worst case lead to end of lines exceeding the multiboard. Also the linebreaks were computed using 1920 pixel width resolution, so using other resolution might yield suboptimal results as well.

Configuration

The Ingame Console code contains a settings section directly below the documentation. Feel free to adjust the values as desired. Some of the values are just the start up configuration and will change during playtime (such as currentWidth).
The most important setting to change is textLanguage, which you should set to either 'eng' or 'ger', depending on the text language configured in your blizzard launcher.

Lua:
IngameConsole = {
    --Settings
    numRows = 20                        ---@type integer Number of Rows of the console (multiboard), excluding the title row. So putting 20 here will show 21 rows, first being the title row.
    ,   autosize = true                 ---@type boolean Defines, whether the width of the main Column automatically adjusts with the longest string in the display.
    ,   currentWidth = 0.5              ---@type number Current and starting Screen Share of the console main column.
    ,   mainColMinWidth = 0.3           ---@type number Minimum Screen share of the console main column.
    ,   mainColMaxWidth = 0.8           ---@type number Maximum Scren share of the console main column.
    ,   tsColumnWidth = 0.06            ---@type number Screen Share of the Timestamp Column
    ,   linebreakBuffer = 0.008         ---@type number Screen Share that is added to longest string in display to calculate the screen share for the console main column. Compensates for the small inaccuracy of the String Width function.
    ,   maxLinebreaks = 8               ---@type integer Defines the maximum amount of linebreaks, before the remaining output string will be cut and not further displayed.
    ,   printToConsole = true           ---@type boolean defines, if the print function should print to the console or to the chat
    ,   sharedConsole = false           ---@type boolean defines, if the console is displayed to each player at the same time (accepting all players input) or if all players much start their own console.
    ,   showTraceOnError = false        ---@type boolean defines, if the console shows a trace upon printing errors. Usually not too useful within console, because you have just initiated the erroneous call.
    ,   textLanguage = 'eng'            ---@type string text language of your Wc3 installation, which influences font size (look in the settings of your Blizzard launcher). Currently only supports 'eng' and 'ger'.
    ,   colors = {
        timestamp = "bbbbbb"            ---@type string Timestamp Color
        ,   singleLineInput = "ffffaa"  ---@type string Color to be applied to single line console inputs
        ,   multiLineInput = "ffcc55"   ---@type string Color to be applied to multi line console inputs
        ,   returnValue = "00ffff"      ---@type string Color applied to return values
        ,   error = "ff5555"            ---@type string Color to be applied to errors resulting of function calls
        ,   keywordInput = "ff00ff"     ---@type string Color to be applied to reserved keyword inputs (console reserved keywords)
        ,   info = "bbbbbb"             ---@type string Color to be applied to info messages from the console itself (for instance after creation or after printrestore)
    }
...
}

Luashine's Paste Helper

The Ingame Console is restricted by the Wc3 Ingame Chat, which only supports a limited amount of characters for any input.
It does support multiline-input for that reason, but that as well is tedious to use and you can easily screw it up by forgetting any preceeding '>' character.
For that reason, @Luashine has created a tool that simplifies pasting multiple lines of code from outside Wc3 into the IngameConsole.
This is particularly useful, when you want to execute a large chunk of testcode containing several linebreaks.
Go here.

Glint's Console

There has been another post at hive offering an Ingame Console, which I also want to mention. It was made by @Glint and can be found here. His implementation is using custom UI and a proper textbox, which has its own advantages and disadvantages. His implementation was using it's own Lua environment, so you couldn't access Warcraft natives or even your own map code. As such, it's not really suitable for debugging.

Error Handling
Error Handling, in the context of Debug Utils, refers to getting a visible error message ingame on screen, after running erroneous code.

Automatic Error Handling

Debug Utils automatically handles errors within code executed by triggers (both conditions and actions), timers and coroutines.
It does so by overriding the natives TriggerAddAction, Condition, Filter, TimerStart, coroutine.create and coroutine.wrap, letting them wrap their callbacks into Debug.try (based of xpcall), effectively executing them in protected mode.

⚙️You can disable this behaviour by setting these constants to false in the configuration section:
Lua:
USE_TRY_ON_TRIGGERADDACTION
USE_TRY_ON_TIMERSTART
USE_TRY_ON_CONDITION
USE_TRY_ON_COROUTINES

Other sources of code execution (everything except triggers, timers and coroutines) will not automatically be error handled, so you have to do it manually (you will learn how to further below).
These include the Lua root, but also code executed by hooking into loading screen natives (like MarkGameStarted). To safely execute functions during loading screen, I suggest using Total Initialization, which offers several code entry points at different points during loading screen, all of them being properly error handled.

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.

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


Stack Traces

Error Messages show a stack trace by default, listing the chain of function executions that let to the error in question. Such stack traces are a little bit imprecise in Warcraft 3 in the sense that they contain holes. Not every function in the execution chain is guaranteed to be part of the printed stack trace, but the existing information should still be helpful.

Looking at the stack trace from the "ExampleFile" example above, you see that file names are not repeated. In this case ExampleFile:7 <- 11, line 7 and 11 were part of the same file, so it was only mentioned once.

⚙️If you don't need stack traces, you can turn them off by setting SHOW_TRACE_ON_ERROR to false.

All references inside the stack trace are also affected by the war3map.lua-to-local-file conversion.

Credits to @HerlySQR for the original resource.

Error Handling (Library functions)
Lua theoretically offers the pcall and xpcall functions for manual error handling. Both try to execute another function and return a boolean indicating the success of the call plus either that functions return values (if successful) or an error message (if unsuccessful). These two error handlers are not that quick to use, though, because they require several lines of code to make them work with both the successful and unsuccessful case (printing the error message), making them tedious to use as a routine.

Debug.try(func, ...) -> func(...)

Debug Utils offers Debug.try(func, ...) (equivalently try(func, ...)) for the same purpose (where ... are the parameters passed to func). It does all the work for you and is really quick to use. It calls a potentially erroneous function in protected mode and displays an error message in case of an error, while still retaining all return values and letting your code continue as normal in the successful case.
Plus, it incorporates a stack trace into error messages, uses war3map.lua-reference-conversion (as stated previously) and interacts with other library functions such as Debug.log(...) (which we will discuss in the chapter about Error Investigation).

Manually applying Debug.try makes sense, if you can't rely on the automatic error handling. In particular, that is the case for error handling within the Lua root.

Example Use:
Lua:
---Consider this simple erroneous function
---@param unitId string
function CreateUnitForPlayerRed(unitId)
    return CreateUnit(0, FourCC(unitId),0,0,0)
end
u = CreateUnitForPlayerRed('hfoo') --doesn’t work and you want to know why.

Investigation with Debug.try:
Lua:
u = Debug.try(CreateUnitForPlayerRed, 'hfoo')
--or equivalently:
u = Debug.try(function() return CreateUnitForPlayerRed('hfoo') end)
which will display an error message like war3map.lua:633: bad argument #1 to CreateUnit ('player' expected).

assert(condition, errorMsg, ...) -> ...

assert(condition, errorMsg, ...) is a native Lua function. It throws an error consisting of the specified message, IF the specified condition fails (i.e. resolves to false or nil), and returns ... otherwise.
It only shows a visible error message if called in protected mode, i.e. inside pcall and xpcall and as such, also in Debug.try. That means, given that your settings USE_TRY_ON_TRIGGERADDACTION, USE_TRY_ON_CONDITION and USE_TRY_ON_TIMERSTART are set to true, you can freely use this inside any function that you execute within triggers and timers due to Debug Utils' automatic error handling functionality.

assert halts code execution, if its condition fails (i.e. code after it will not be executed anymore).

Example:
Lua:
Debug.beginFile("ExampleFile")

---Adds one to the input parameter
---@param x number
---@return number
function foo(x)
    assert(type(x)=='number', "wrong type of param #1 for foo: " .. tostring(x) .. " is not a number")
    return x+1
end

function bar()
    print(foo(true)) --will throw an error due to attempting to evaluate true+1
end

Debug.endFile()

The specified condition type(x)=='number' fails for the input true, so assert will print the following error:

assert.png


As assert returns its ... arguments in case the condition holds, you can even rewrite foo as follows:

Lua:
function foo(x)
    return assert(type(x)=='number', "wrong type of param #1 for foo: " .. tostring(x) .. " is not a number", x) + 1
end

Do not write assert(type(x)=='number', "wrong type of param #1 for foo: " .. tostring(x) .. " is not a number", x+1), though, because it would throw an error inside the brackets, before assert is even executed.

Debug.assert(condition, errorMsg, ...) -> ...

Debug.assert(condition, errorMsg, ...) works exactly like assert(condition, errorMsg, ...), except that it also works outside of protected mode and it does not halt code execution. As such, you can use it in every function, no matter if that is called within Debug.try or not (for example within Lua root).
If used in protected mode, the fact that it doesn't halt code execution can obviously lead to another error being thrown directly afterwards.
Lua:
--Assuming the value true for x, this might print two errors (the second one only if in protected mode)
function foo(x)
    --Debug.assert prints an error, but doesn't halt code execution
    Debug.assert(type(x)=='number', "wrong type of param #1 for foo: " .. tostring(x) .. " is not a number")
    return x+1 --true+1 prints an error in protected mode
end

error(errorMsg)

error(errorMsg) is a native Lua function. It halts code execution and throws an error if in protected mode that can be picked up by the error handling functions pcall, xpcall and Debug.try.
As Debug Utils wraps all functions executed within triggers and timers in Debug.try, you can freely use error in such code (and it will contain the specified error message, document, line number, stack trace and Debug-log).

Debug.throwError(...)

Prints an error message on screen consisting of all arguments specified (plus document, line number, stack trace and Debug-log).
Works similar to error, except that it doesn't halt code execution and works outside protected mode.

Error Investigation
This chapter discusses the options Debug Utils gives you to investigate errors.

The most important tool has already been presented in the previous chapter: Using the Ingame Console to look at your data, while the buggy gamestate is still available.

Debug.log(...)

Debug.log(...) writes all arguments into the Debug-log, which is going to be printed upon every error catched by Debug Utils. This helps tracking valuable information such as function parameters that led to the actual error. But yes, this requires altering your code, restarting Wc3 and reproducing the error.

You can log any type of object. The Debug-log saves only one set of arguments per code-location, so you don't need to worry about that any periodically executed function including Debug.log(...) could spam your screen.

Example:
Lua:
Debug.beginFile("ExampleFile")

---Adds one to the input parameter
---@param x number
---@return number
function foo(x)
    Debug.log("Input param for foo:", x)
    Debug.log("Some more useful information")
    return x+1
end

function bar()
    print(foo(true)) --will throw an error due to attempting to evaluate true+1
end

Debug.endFile()

Resulting Error:

Debug.log.png


Note that Debug.log(...) adds its arguments to any subsequent error message, no matter if they are related to the issue or not.

table.tostring and table.print

Often, when we encounter bugs in our map, we want to take a look at those parts of our data structure that are potentially bug-related. We usually choose to print a bunch of values on screen, until we find something that looks suspicious.

Sometimes it can be a lot quicker to print a whole part of your data structure at once, i.e. a whole table including all keys and values.

We can do so by using table.tostring(T) or table.print(T) (which just prints table.tostring(T)).

Example:
Lua:
T = {function() end, x = "Hello", y = {true,false}}
table.print(T)

table.print.png


Both functions also have two optional parameters: table.print(T, [depth:integer], [pretty_yn:boolean]).
Not specifying a depth will unpack all subtables of T, as you can see above. Consequently and unsurprisingly, table.print(T) on self-referential/recursive tables will lead to a stack overflow.
Better limit the depth up to which subtables will be unpacked by providing a sensible integer for the depth parameter. This will also improve readability for big tables.

Example:
Lua:
T = {function() end, x = "Hello", y = {true,false}}
table.print(T, 1)

table.print_depth.png


The one-liner output of table.print fits well on screen, but can be hard to read. You can instead use its pretty version, which includes more linebreaks and indentation. Wc3 has limited ability to display linebreaks though, so preferably do this for small tables only. You can use both table.print(T, depth, pretty_yn) and table.print(T, pretty_yn) syntax. In short, just append true to make it pretty.
Credits to @Bribe for contributing this version.

Example:
Lua:
T = {function() end, x = "Hello", y = {true,false}}
table.print(T, true)

table.print_pretty.png


Debug.wc3Type(object) -> string

Debug.wc3Type(object) or short Wc3Type(object) returns the Warcraft-3-type of Warcraft objects and the Lua-type of Lua-objects. Lua is not type-safe, so extracting the type actually stored at a certain place is often useful. Lua's native type function returns "userdata" for Warcraft objects (which is exactly their Lua type), hence the need for this Wc3-version.

Lua:
Debug.wc3Type( Player(0) ) --> "player"
Debug.wc3Type( GetPlayableMapRect() ) --> "rect"
Debug.wc3Type( {} ) --> "table"
Debug.wc3Type( 1.5 ) --> "number"

Debug.traceback() -> string

Returns the stack trace at the code location, where it is executed. Stack traces are printed on every error anyway, if the setting SHOW_TRACE_ON_ERROR is set to true. This library function however can be used at any point in code, not just in case of errors. This just returns a string, so you must print(Debug.traceback()) to see it on screen.
Note that the stack trace provided by Wc3 is sometimes incomplete. It can miss out function calls that were definitely part of the chain.
The trace only contains documents and line numbers. It does not currently include function names or even arguments passed. If you know a method to receive this kind of information, please write me a PM or leave a comment under this post.

Debug.getLine([depth:integer]) -> integer?

Debug.getLine() returns the line in war3map.lua, where this function is executed (NOT converted to local file).
This is mainly utilized by Debug Utils to feed the other library functions, but can have useful applications for your own debugging as well.
You can specify a depth d >= 0 to instead return the line number of the d-th function in the stack trace leading to this execution.

Lua:
--Consider this simple example:
function Third()                        --war3map.lua:1000
    print(Debug.getLine(depth))         --war3map.lua:1001
end                                     --war3map.lua:1002
                                        --war3map.lua:1003
function Second()                       --war3map.lua:1004
    Third()                             --war3map.lua:1005
end                                     --war3map.lua:1006
                                        --war3map.lua:1007
function First()                        --war3map.lua:1008
    Second()                            --war3map.lua:1009
end                                     --war3map.lua:1010

Given that your code executes First, the call of Debug.getLine(depth) would output 1001 for depth = 0, 1005 for depth = 1 and 1009 for depth = 2.

Debug.getLine(depth) can return nil for depth >= 4 (not sure about the exact threshold). This is, because the Wc3 stack trace contains nil-entries, which Debug.traceback() simply skips, but they are there. Be sure to either not use high choices for depth or include a nil-check, if you rely on the result being an integer.

Debug.getLocalErrorMsg(errorMsg) -> string

Debug.getLocalErrorMsg(errorMsg) takes a string in the format <document>:<lineNumber><restOfMsg> (referred to as error message, but it's honestly just any string in the mentioned format) and returns the same string with war3map.lua-references replaced by local ones, as defined by your usage of Debug.beginFile() and Debug.endFile(). That means, if <document> is "war3map.lua", then both <document> and <lineNumber> are replaced by their local counterparts. Otherwise, this just returns the input string.

Warnings for undeclared Globals
DebugUtils will print warnings on screen, if you attempt to read an undeclared global variable.
This is technically the case, when you misspelled a function name, like calling CraeteUnit instead of CreateUnit. Remember that Lua syntax will not inform you about spelling errors, so this is an ingame replacement that should give you an early indication on that something is wrong.

undeclaredGlobals.png


Sure thing, Visual Studio Code and sumneko extension already do 90% of the spelling error prevention for you, but this still sometimes comes in handy.

Keep in mind though that the same warning will pop up after reading a global that was intentionally nilled. Lua can't distinguish the two, because the concept of global declaration doesn't actually exist (you can just use any global name).
⚙️If you don't like this behaviour, set WARNING_FOR_UNDECLARED_GLOBALS to false.

Undeclared global warnings start to take effect after loading screen, because otherwise requirement checking would be affected by this. I.e. any line if someLibrary then would print a warning, if someLibrary didn't exist. Requirement checking during game runtime is still affected, so if you do so in your map, that may be a reason to turn this off.

Print Caching and Print Duration
We often like to cluster print statements into our code during debugging. That is typically not applicable for code running during loading screen though, because loading screen prints will not show up in Wc3.

Debug Utils solves this issue by delaying all early print statements to after game start, so you can see them.

⚙️You can disable this behaviour by setting USE_PRINT_CACHE to false.

Warcraft's implementation of print automatically calculates the on-screen duration based on the length of the printed string. This can be annoying during debugging, so Debug Utils allows you to set a fixed duration instead. This can even be a very high number to keep them "forever".

⚙️Simply set PRINT_DURATION = <desiredDurationInSeconds> or set it to nil (default) to keep original behaviour.

Name Caching
Debug Utils will cache the string name for objects in global scope and override tostring to show these names instead of their memory position (as such, it also applies to print).
Objects refers to call-by-reference data, i.e. everything except strings, numbers, booleans and nil.

Lua:
print(CreateUnit) --printing a native
--> function: 0000016F29E16700  (if feature is turned off)
--> function: CreateUnit  (if feature is turned on)

⚙️Set USE_NAME_CACHE to false to deactivate this feature.

Global objects (including Wc3 objects)
Globals are added to the name cache during loading screen and at game start.
This basic feature includes all native globals and those you have created during loading screen.

Automatically add new globals to name cache

Debug Utils automatically adds new globals (i.e. created after game start) to the name cache.

⚙️Set AUTO_REGISTER_NEW_NAMES to false to deactivate this feature.

Lua:
T = {} --executed after game start
print(T) --> table: T

Debug Utils enables this feature by setting a __newindex metamethod to _G. As such, it only works for new names (because that's how __newindex operates).
Objects can only have one name each, so the first name will be preferred over later names (in case you are assigning the same object to a different variable).
As a consequence of these two facts, the name cache for objects added during map runtime can theoretically get outdated, as you can see in this example:

Lua:
U = {}
print(U) --> prints "U"
V = U
print(V) --> prints "U" (already had a name, first was preferred)
U = {} --> U now holds a new table, but this didn't trigger __newindex...
print(V) --> still prints "U", although U now doesn't hold the same table anymore
print(U) --> prints "table: 000001F34E982"

Despite this possibility to screw up object names, the automatic name feature should in general be very reliable. In doubt, you can always get the "old" name (table: 000001F34E982 or similar) by using Debug.oldTostring(object).

Also note that Debug Utils prevents any two different objects from getting the same name. If an object receives a name that was already assigned to another object, the name is transferred and the previous object gets nameless, showing up with the default <type>: <memPos> again (although this process couldn't take place in the example above, because we coded in a way that __newindex didn't even trigger).
As such, you are safe to compare two different objects by name, which I consider a basic requirement for any implementation of tostring. If the names are the same, then the objects are the same. If the names are different, the objects are different.

Automatic Name Caching for subtables of global scope

Debug Utils can even apply the automatic Name Caching for objects within subtables of global scope up to a specified maximum depth, if you enable that in the settings. Be sure about the implications listed below, before you do so.

Lua:
T = {loc = Location(0,0), sub = {loc2 = Location(0,0)} }
--Assume you have set the maximum depth to 1
print(T.loc) --> prints "locaton: T.loc" (this object is on depth 1)
print(T.sub) --> prints "table: sub" (depth 1)
print(T.sub.loc2) --> prints "location: 000001A29C8231" (depth 2, not added to name cache)

⚙️Set NAME_CACHE_DEPTH to an integer > 0 to let Debug Utils automatically register Names up to the specified depth (=distance from global scope).
Set to 0 (default) to deactivate name caching within subtables.

Caution: This feature is invasive and as such deactivated by default. It requires Debug Utils to set a __newindex metamethod to every subtable of _G up to the maximum depth to scan for new objects during runtime. This obviously also requires to create a metatable in case the subtable didn't already have one. Existing __newindex metamethods will be altered (i.e. name caching is added, while the old behaviour is being kept).
This behaviour should be safe in terms of functionality, but you can theoretically come up with very rare edge cases, where it can cause bugs. One such example would be checking the __newindex metamethod for equality to another function (might have returned true before the altering and false afterwards). I personally consider this type of bug so rare that it shouldn't happen for you whatsoever. Still, if you are unsure, leave this feature deactivated.

The creation of metatables obviously also has performance implications, because you effectively now create two tables instead of one when in depth-range of global scope. This shouldn't matter too much, though, because scenarios creating a lot of (temporary) tables nearly always happen in local scope, which is not affected by this feature.

Library functions

  • Debug.registerName(object:any, name:string)
    adds the specified name for the specified object to the name cache. Use this for any object not affected by the automatic features listed above, for example locals.
    Will overwrite the existing name for the object, if there is any.
    Will do nothing for primitive objects (booleans, numbers, strings, nil).
  • Debug.registerNamesFrom(parentTable:table, [parentTableName:string], [maxDepth:integer])
    will add names for all non-primitive values within the specified parentTable to the name cache, using the best applicable notation out of <parentTableName>.<stringKey> and <parentTableName>[<nonStingKey>].
    If you specify maxDepth, Debug.registerNamesFrom will also give names to values inside subtables of the parent table up to the specified depth (default is 1, i.e. only give names to direct elements of the parent table).
    You can also specify parentTableName, if you don't want to use the original one (you might find it too long already). If the parent table doesn't yet have a name, you obviously must specify this and the parent table will receive that name before applying it to its elements.
    In contrast to Debug.registerName, this does not replace existing names.
  • Debug.oldTostring(object)
    returns the old name of any object (i.e. <type>:<memPos> as if you weren't using the name cache).


Installation

Please conduct the following steps to use Debug Utils in your map:
  • Either open the attached map file LuaDebugUtils_v2.0a.w3x, copy/paste the "Debug Utils" folder from the trigger editor into your own map (and you are good to go)
    OR create three new script files in your Trigger Editor ("DebugUtils" above "StringWidth" above "IngameConsole") and manually paste the code from the below spoilers into it.
  • In any case, make sure that DebugUtils is the topmost script in your trigger editor at all times. Some features rely on that fact.
  • If you are using Visual Studio Code (which you should), be sure to also copy the three script files (DebugUtils, StringWidth and IngameConsole) into your workspace.
  • Also import the attached common.1.33.v2.lua, blizzard.1.33.v2.lua and HiddenNatives.1.33.v2.lua into your VS Code workspace, if you haven't already. This is not directly related to the debug functionality, but will help a ton and avoid bugs in the first place.
After the installation, please take a look at the Configuration section of Debug Utils and Ingame Console (in both cases in their script files directly below their documentation). Some features might be desired, others might not, and you better set it up properly before getting surprised.

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, ...)
            ,   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 _, 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
    ---@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 defaults to 4 in 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
                            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(...)
            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
            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()

Lua:
------------------------
----| String Width |----
------------------------

--[[
    offers functions to measure the width of a string (i.e. the space it takes on screen, not the number of chars). Wc3 font is not monospace, so the system below has protocolled every char width and simply sums up all chars in a string.
    output measures are:
    1. Multiboard-width (i.e. 1-based screen share used in Multiboards column functions)
    2. Line-width for screen prints
    every unknown char will be treated as having default width (see constants below)
--]]

do
    ----------------------------
    ----| String Width API |----
    ----------------------------

    local multiboardCharTable = {}                        ---@type table  -- saves the width in screen percent (on 1920 pixel width resolutions) that each char takes up, when displayed in a multiboard.
    local DEFAULT_MULTIBOARD_CHAR_WIDTH = 1. / 128.        ---@type number    -- used for unknown chars (where we didn't define a width in the char table)
    local MULTIBOARD_TO_PRINT_FACTOR = 1. / 36.            ---@type number    -- 36 is actually the lower border (longest width of a non-breaking string only consisting of the letter "i")

    ---Returns the width of a char in a multiboard, when inputting a char (string of length 1) and 0 otherwise.
    ---also returns 0 for non-recorded chars (like ` and ´ and ß and § and €)
    ---@param char string | integer integer bytecode representations of chars are also allowed, i.e. the results of string.byte().
    ---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
    ---@return number
    function string.charMultiboardWidth(char, textlanguage)
        return multiboardCharTable[textlanguage or 'eng'][char] or DEFAULT_MULTIBOARD_CHAR_WIDTH
    end

    ---returns the width of a string in a multiboard (i.e. output is in screen percent)
    ---unknown chars will be measured with default width (see constants above)
    ---@param multichar string
    ---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
    ---@return number
    function string.multiboardWidth(multichar, textlanguage)
        local chartable = table.pack(multichar:byte(1,-1)) --packs all bytecode char representations into a table
        local charWidth = 0.
        for i = 1, chartable.n do
            charWidth = charWidth + string.charMultiboardWidth(chartable[i], textlanguage)
        end
        return charWidth
    end

    ---The function should match the following criteria: If the value returned by this function is smaller than 1.0, than the string fits into a single line on screen.
    ---The opposite is not necessarily true (but should be true in the majority of cases): If the function returns bigger than 1.0, the string doesn't necessarily break.
    ---@param char string | integer integer bytecode representations of chars are also allowed, i.e. the results of string.byte().
    ---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
    ---@return number
    function string.charPrintWidth(char, textlanguage)
        return string.charMultiboardWidth(char, textlanguage) * MULTIBOARD_TO_PRINT_FACTOR
    end

    ---The function should match the following criteria: If the value returned by this function is smaller than 1.0, than the string fits into a single line on screen.
    ---The opposite is not necessarily true (but should be true in the majority of cases): If the function returns bigger than 1.0, the string doesn't necessarily break.
    ---@param multichar string
    ---@param textlanguage? '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
    ---@return number
    function string.printWidth(multichar, textlanguage)
        return string.multiboardWidth(multichar, textlanguage) * MULTIBOARD_TO_PRINT_FACTOR
    end

    ----------------------------------
    ----| String Width Internals |----
    ----------------------------------

    ---@param charset '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
    ---@param char string|integer either the char or its bytecode
    ---@param lengthInScreenWidth number
    local function setMultiboardCharWidth(charset, char, lengthInScreenWidth)
        multiboardCharTable[charset] = multiboardCharTable[charset] or {}
        multiboardCharTable[charset][char] = lengthInScreenWidth
    end

    ---numberPlacements says how often the char can be placed in a multiboard column, before reaching into the right bound.
    ---@param charset '"ger"'| '"eng"' (default: 'eng'), depending on the text language in the Warcraft 3 installation settings.
    ---@param char string|integer either the char or its bytecode
    ---@param numberPlacements integer
    local function setMultiboardCharWidthBase80(charset, char, numberPlacements)
        setMultiboardCharWidth(charset, char, 0.8 / numberPlacements) --1-based measure. 80./numberPlacements would result in Screen Percent.
        setMultiboardCharWidth(charset, char:byte(1,-1), 0.8 / numberPlacements)
    end

    -- Set Char Width for all printable ascii chars in screen width (1920 pixels). Measured on a 80percent screen width multiboard column by counting the number of chars that fit into it.
    -- Font size differs by text install language and patch (1.32- vs. 1.33+)
    if BlzGetUnitOrderCount then --identifies patch 1.33+
        --German font size for patch 1.33+
        setMultiboardCharWidthBase80('ger', "a", 144)
        setMultiboardCharWidthBase80('ger', "b", 131)
        setMultiboardCharWidthBase80('ger', "c", 144)
        setMultiboardCharWidthBase80('ger', "d", 120)
        setMultiboardCharWidthBase80('ger', "e", 131)
        setMultiboardCharWidthBase80('ger', "f", 240)
        setMultiboardCharWidthBase80('ger', "g", 120)
        setMultiboardCharWidthBase80('ger', "h", 131)
        setMultiboardCharWidthBase80('ger', "i", 288)
        setMultiboardCharWidthBase80('ger', "j", 288)
        setMultiboardCharWidthBase80('ger', "k", 144)
        setMultiboardCharWidthBase80('ger', "l", 288)
        setMultiboardCharWidthBase80('ger', "m", 85)
        setMultiboardCharWidthBase80('ger', "n", 131)
        setMultiboardCharWidthBase80('ger', "o", 120)
        setMultiboardCharWidthBase80('ger', "p", 120)
        setMultiboardCharWidthBase80('ger', "q", 120)
        setMultiboardCharWidthBase80('ger', "r", 206)
        setMultiboardCharWidthBase80('ger', "s", 160)
        setMultiboardCharWidthBase80('ger', "t", 206)
        setMultiboardCharWidthBase80('ger', "u", 131)
        setMultiboardCharWidthBase80('ger', "v", 131)
        setMultiboardCharWidthBase80('ger', "w", 96)
        setMultiboardCharWidthBase80('ger', "x", 144)
        setMultiboardCharWidthBase80('ger', "y", 131)
        setMultiboardCharWidthBase80('ger', "z", 144)
        setMultiboardCharWidthBase80('ger', "A", 103)
        setMultiboardCharWidthBase80('ger', "B", 120)
        setMultiboardCharWidthBase80('ger', "C", 111)
        setMultiboardCharWidthBase80('ger', "D", 103)
        setMultiboardCharWidthBase80('ger', "E", 144)
        setMultiboardCharWidthBase80('ger', "F", 160)
        setMultiboardCharWidthBase80('ger', "G", 96)
        setMultiboardCharWidthBase80('ger', "H", 96)
        setMultiboardCharWidthBase80('ger', "I", 240)
        setMultiboardCharWidthBase80('ger', "J", 240)
        setMultiboardCharWidthBase80('ger', "K", 120)
        setMultiboardCharWidthBase80('ger', "L", 144)
        setMultiboardCharWidthBase80('ger', "M", 76)
        setMultiboardCharWidthBase80('ger', "N", 96)
        setMultiboardCharWidthBase80('ger', "O", 90)
        setMultiboardCharWidthBase80('ger', "P", 131)
        setMultiboardCharWidthBase80('ger', "Q", 90)
        setMultiboardCharWidthBase80('ger', "R", 120)
        setMultiboardCharWidthBase80('ger', "S", 131)
        setMultiboardCharWidthBase80('ger', "T", 144)
        setMultiboardCharWidthBase80('ger', "U", 103)
        setMultiboardCharWidthBase80('ger', "V", 120)
        setMultiboardCharWidthBase80('ger', "W", 76)
        setMultiboardCharWidthBase80('ger', "X", 111)
        setMultiboardCharWidthBase80('ger', "Y", 120)
        setMultiboardCharWidthBase80('ger', "Z", 120)
        setMultiboardCharWidthBase80('ger', "1", 144)
        setMultiboardCharWidthBase80('ger', "2", 120)
        setMultiboardCharWidthBase80('ger', "3", 120)
        setMultiboardCharWidthBase80('ger', "4", 120)
        setMultiboardCharWidthBase80('ger', "5", 120)
        setMultiboardCharWidthBase80('ger', "6", 120)
        setMultiboardCharWidthBase80('ger', "7", 131)
        setMultiboardCharWidthBase80('ger', "8", 120)
        setMultiboardCharWidthBase80('ger', "9", 120)
        setMultiboardCharWidthBase80('ger', "0", 120)
        setMultiboardCharWidthBase80('ger', ":", 288)
        setMultiboardCharWidthBase80('ger', ";", 288)
        setMultiboardCharWidthBase80('ger', ".", 288)
        setMultiboardCharWidthBase80('ger', "#", 120)
        setMultiboardCharWidthBase80('ger', ",", 288)
        setMultiboardCharWidthBase80('ger', " ", 286) --space
        setMultiboardCharWidthBase80('ger', "'", 180)
        setMultiboardCharWidthBase80('ger', "!", 180)
        setMultiboardCharWidthBase80('ger', "$", 131)
        setMultiboardCharWidthBase80('ger', "&", 90)
        setMultiboardCharWidthBase80('ger', "/", 180)
        setMultiboardCharWidthBase80('ger', "(", 240)
        setMultiboardCharWidthBase80('ger', ")", 240)
        setMultiboardCharWidthBase80('ger', "=", 120)
        setMultiboardCharWidthBase80('ger', "?", 144)
        setMultiboardCharWidthBase80('ger', "^", 144)
        setMultiboardCharWidthBase80('ger', "<", 144)
        setMultiboardCharWidthBase80('ger', ">", 144)
        setMultiboardCharWidthBase80('ger', "-", 180)
        setMultiboardCharWidthBase80('ger', "+", 120)
        setMultiboardCharWidthBase80('ger', "*", 180)
        setMultiboardCharWidthBase80('ger', "|", 287) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
        setMultiboardCharWidthBase80('ger', "~", 111)
        setMultiboardCharWidthBase80('ger', "{", 240)
        setMultiboardCharWidthBase80('ger', "}", 240)
        setMultiboardCharWidthBase80('ger', "[", 240)
        setMultiboardCharWidthBase80('ger', "]", 240)
        setMultiboardCharWidthBase80('ger', "_", 144)
        setMultiboardCharWidthBase80('ger', "\x25", 103) --percent
        setMultiboardCharWidthBase80('ger', "\x5C", 205) --backslash
        setMultiboardCharWidthBase80('ger', "\x22", 120) --double quotation mark
        setMultiboardCharWidthBase80('ger', "\x40", 90) --at sign
        setMultiboardCharWidthBase80('ger', "\x60", 144) --Gravis (Accent)

        --English font size for patch 1.33+
        setMultiboardCharWidthBase80('eng', "a", 144)
        setMultiboardCharWidthBase80('eng', "b", 120)
        setMultiboardCharWidthBase80('eng', "c", 131)
        setMultiboardCharWidthBase80('eng', "d", 120)
        setMultiboardCharWidthBase80('eng', "e", 120)
        setMultiboardCharWidthBase80('eng', "f", 240)
        setMultiboardCharWidthBase80('eng', "g", 120)
        setMultiboardCharWidthBase80('eng', "h", 120)
        setMultiboardCharWidthBase80('eng', "i", 288)
        setMultiboardCharWidthBase80('eng', "j", 288)
        setMultiboardCharWidthBase80('eng', "k", 144)
        setMultiboardCharWidthBase80('eng', "l", 288)
        setMultiboardCharWidthBase80('eng', "m", 80)
        setMultiboardCharWidthBase80('eng', "n", 120)
        setMultiboardCharWidthBase80('eng', "o", 111)
        setMultiboardCharWidthBase80('eng', "p", 111)
        setMultiboardCharWidthBase80('eng', "q", 111)
        setMultiboardCharWidthBase80('eng', "r", 206)
        setMultiboardCharWidthBase80('eng', "s", 160)
        setMultiboardCharWidthBase80('eng', "t", 206)
        setMultiboardCharWidthBase80('eng', "u", 120)
        setMultiboardCharWidthBase80('eng', "v", 144)
        setMultiboardCharWidthBase80('eng', "w", 90)
        setMultiboardCharWidthBase80('eng', "x", 131)
        setMultiboardCharWidthBase80('eng', "y", 144)
        setMultiboardCharWidthBase80('eng', "z", 144)
        setMultiboardCharWidthBase80('eng', "A", 103)
        setMultiboardCharWidthBase80('eng', "B", 120)
        setMultiboardCharWidthBase80('eng', "C", 103)
        setMultiboardCharWidthBase80('eng', "D", 96)
        setMultiboardCharWidthBase80('eng', "E", 131)
        setMultiboardCharWidthBase80('eng', "F", 160)
        setMultiboardCharWidthBase80('eng', "G", 96)
        setMultiboardCharWidthBase80('eng', "H", 90)
        setMultiboardCharWidthBase80('eng', "I", 240)
        setMultiboardCharWidthBase80('eng', "J", 240)
        setMultiboardCharWidthBase80('eng', "K", 120)
        setMultiboardCharWidthBase80('eng', "L", 131)
        setMultiboardCharWidthBase80('eng', "M", 76)
        setMultiboardCharWidthBase80('eng', "N", 90)
        setMultiboardCharWidthBase80('eng', "O", 85)
        setMultiboardCharWidthBase80('eng', "P", 120)
        setMultiboardCharWidthBase80('eng', "Q", 85)
        setMultiboardCharWidthBase80('eng', "R", 120)
        setMultiboardCharWidthBase80('eng', "S", 131)
        setMultiboardCharWidthBase80('eng', "T", 144)
        setMultiboardCharWidthBase80('eng', "U", 96)
        setMultiboardCharWidthBase80('eng', "V", 120)
        setMultiboardCharWidthBase80('eng', "W", 76)
        setMultiboardCharWidthBase80('eng', "X", 111)
        setMultiboardCharWidthBase80('eng', "Y", 120)
        setMultiboardCharWidthBase80('eng', "Z", 111)
        setMultiboardCharWidthBase80('eng', "1", 103)
        setMultiboardCharWidthBase80('eng', "2", 111)
        setMultiboardCharWidthBase80('eng', "3", 111)
        setMultiboardCharWidthBase80('eng', "4", 111)
        setMultiboardCharWidthBase80('eng', "5", 111)
        setMultiboardCharWidthBase80('eng', "6", 111)
        setMultiboardCharWidthBase80('eng', "7", 111)
        setMultiboardCharWidthBase80('eng', "8", 111)
        setMultiboardCharWidthBase80('eng', "9", 111)
        setMultiboardCharWidthBase80('eng', "0", 111)
        setMultiboardCharWidthBase80('eng', ":", 288)
        setMultiboardCharWidthBase80('eng', ";", 288)
        setMultiboardCharWidthBase80('eng', ".", 288)
        setMultiboardCharWidthBase80('eng', "#", 103)
        setMultiboardCharWidthBase80('eng', ",", 288)
        setMultiboardCharWidthBase80('eng', " ", 286) --space
        setMultiboardCharWidthBase80('eng', "'", 360)
        setMultiboardCharWidthBase80('eng', "!", 288)
        setMultiboardCharWidthBase80('eng', "$", 131)
        setMultiboardCharWidthBase80('eng', "&", 120)
        setMultiboardCharWidthBase80('eng', "/", 180)
        setMultiboardCharWidthBase80('eng', "(", 206)
        setMultiboardCharWidthBase80('eng', ")", 206)
        setMultiboardCharWidthBase80('eng', "=", 111)
        setMultiboardCharWidthBase80('eng', "?", 180)
        setMultiboardCharWidthBase80('eng', "^", 144)
        setMultiboardCharWidthBase80('eng', "<", 111)
        setMultiboardCharWidthBase80('eng', ">", 111)
        setMultiboardCharWidthBase80('eng', "-", 160)
        setMultiboardCharWidthBase80('eng', "+", 111)
        setMultiboardCharWidthBase80('eng', "*", 144)
        setMultiboardCharWidthBase80('eng', "|", 479) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
        setMultiboardCharWidthBase80('eng', "~", 144)
        setMultiboardCharWidthBase80('eng', "{", 160)
        setMultiboardCharWidthBase80('eng', "}", 160)
        setMultiboardCharWidthBase80('eng', "[", 206)
        setMultiboardCharWidthBase80('eng', "]", 206)
        setMultiboardCharWidthBase80('eng', "_", 120)
        setMultiboardCharWidthBase80('eng', "\x25", 103) --percent
        setMultiboardCharWidthBase80('eng', "\x5C", 180) --backslash
        setMultiboardCharWidthBase80('eng', "\x22", 180) --double quotation mark
        setMultiboardCharWidthBase80('eng', "\x40", 85) --at sign
        setMultiboardCharWidthBase80('eng', "\x60", 206) --Gravis (Accent)
    else
        --German font size up to patch 1.32
        setMultiboardCharWidthBase80('ger', "a", 144)
        setMultiboardCharWidthBase80('ger', "b", 144)
        setMultiboardCharWidthBase80('ger', "c", 144)
        setMultiboardCharWidthBase80('ger', "d", 131)
        setMultiboardCharWidthBase80('ger', "e", 144)
        setMultiboardCharWidthBase80('ger', "f", 240)
        setMultiboardCharWidthBase80('ger', "g", 120)
        setMultiboardCharWidthBase80('ger', "h", 144)
        setMultiboardCharWidthBase80('ger', "i", 360)
        setMultiboardCharWidthBase80('ger', "j", 288)
        setMultiboardCharWidthBase80('ger', "k", 144)
        setMultiboardCharWidthBase80('ger', "l", 360)
        setMultiboardCharWidthBase80('ger', "m", 90)
        setMultiboardCharWidthBase80('ger', "n", 144)
        setMultiboardCharWidthBase80('ger', "o", 131)
        setMultiboardCharWidthBase80('ger', "p", 131)
        setMultiboardCharWidthBase80('ger', "q", 131)
        setMultiboardCharWidthBase80('ger', "r", 206)
        setMultiboardCharWidthBase80('ger', "s", 180)
        setMultiboardCharWidthBase80('ger', "t", 206)
        setMultiboardCharWidthBase80('ger', "u", 144)
        setMultiboardCharWidthBase80('ger', "v", 131)
        setMultiboardCharWidthBase80('ger', "w", 96)
        setMultiboardCharWidthBase80('ger', "x", 144)
        setMultiboardCharWidthBase80('ger', "y", 131)
        setMultiboardCharWidthBase80('ger', "z", 144)
        setMultiboardCharWidthBase80('ger', "A", 103)
        setMultiboardCharWidthBase80('ger', "B", 131)
        setMultiboardCharWidthBase80('ger', "C", 120)
        setMultiboardCharWidthBase80('ger', "D", 111)
        setMultiboardCharWidthBase80('ger', "E", 144)
        setMultiboardCharWidthBase80('ger', "F", 180)
        setMultiboardCharWidthBase80('ger', "G", 103)
        setMultiboardCharWidthBase80('ger', "H", 103)
        setMultiboardCharWidthBase80('ger', "I", 288)
        setMultiboardCharWidthBase80('ger', "J", 240)
        setMultiboardCharWidthBase80('ger', "K", 120)
        setMultiboardCharWidthBase80('ger', "L", 144)
        setMultiboardCharWidthBase80('ger', "M", 80)
        setMultiboardCharWidthBase80('ger', "N", 103)
        setMultiboardCharWidthBase80('ger', "O", 96)
        setMultiboardCharWidthBase80('ger', "P", 144)
        setMultiboardCharWidthBase80('ger', "Q", 90)
        setMultiboardCharWidthBase80('ger', "R", 120)
        setMultiboardCharWidthBase80('ger', "S", 144)
        setMultiboardCharWidthBase80('ger', "T", 144)
        setMultiboardCharWidthBase80('ger', "U", 111)
        setMultiboardCharWidthBase80('ger', "V", 120)
        setMultiboardCharWidthBase80('ger', "W", 76)
        setMultiboardCharWidthBase80('ger', "X", 111)
        setMultiboardCharWidthBase80('ger', "Y", 120)
        setMultiboardCharWidthBase80('ger', "Z", 120)
        setMultiboardCharWidthBase80('ger', "1", 288)
        setMultiboardCharWidthBase80('ger', "2", 131)
        setMultiboardCharWidthBase80('ger', "3", 144)
        setMultiboardCharWidthBase80('ger', "4", 120)
        setMultiboardCharWidthBase80('ger', "5", 144)
        setMultiboardCharWidthBase80('ger', "6", 131)
        setMultiboardCharWidthBase80('ger', "7", 144)
        setMultiboardCharWidthBase80('ger', "8", 131)
        setMultiboardCharWidthBase80('ger', "9", 131)
        setMultiboardCharWidthBase80('ger', "0", 131)
        setMultiboardCharWidthBase80('ger', ":", 480)
        setMultiboardCharWidthBase80('ger', ";", 360)
        setMultiboardCharWidthBase80('ger', ".", 480)
        setMultiboardCharWidthBase80('ger', "#", 120)
        setMultiboardCharWidthBase80('ger', ",", 360)
        setMultiboardCharWidthBase80('ger', " ", 288) --space
        setMultiboardCharWidthBase80('ger', "'", 480)
        setMultiboardCharWidthBase80('ger', "!", 360)
        setMultiboardCharWidthBase80('ger', "$", 160)
        setMultiboardCharWidthBase80('ger', "&", 96)
        setMultiboardCharWidthBase80('ger', "/", 180)
        setMultiboardCharWidthBase80('ger', "(", 288)
        setMultiboardCharWidthBase80('ger', ")", 288)
        setMultiboardCharWidthBase80('ger', "=", 160)
        setMultiboardCharWidthBase80('ger', "?", 180)
        setMultiboardCharWidthBase80('ger', "^", 144)
        setMultiboardCharWidthBase80('ger', "<", 160)
        setMultiboardCharWidthBase80('ger', ">", 160)
        setMultiboardCharWidthBase80('ger', "-", 144)
        setMultiboardCharWidthBase80('ger', "+", 160)
        setMultiboardCharWidthBase80('ger', "*", 206)
        setMultiboardCharWidthBase80('ger', "|", 480) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
        setMultiboardCharWidthBase80('ger', "~", 144)
        setMultiboardCharWidthBase80('ger', "{", 240)
        setMultiboardCharWidthBase80('ger', "}", 240)
        setMultiboardCharWidthBase80('ger', "[", 240)
        setMultiboardCharWidthBase80('ger', "]", 288)
        setMultiboardCharWidthBase80('ger', "_", 144)
        setMultiboardCharWidthBase80('ger', "\x25", 111) --percent
        setMultiboardCharWidthBase80('ger', "\x5C", 206) --backslash
        setMultiboardCharWidthBase80('ger', "\x22", 240) --double quotation mark
        setMultiboardCharWidthBase80('ger', "\x40", 103) --at sign
        setMultiboardCharWidthBase80('ger', "\x60", 240) --Gravis (Accent)

        --English Font size up to patch 1.32
        setMultiboardCharWidthBase80('eng', "a", 144)
        setMultiboardCharWidthBase80('eng', "b", 120)
        setMultiboardCharWidthBase80('eng', "c", 131)
        setMultiboardCharWidthBase80('eng', "d", 120)
        setMultiboardCharWidthBase80('eng', "e", 131)
        setMultiboardCharWidthBase80('eng', "f", 240)
        setMultiboardCharWidthBase80('eng', "g", 120)
        setMultiboardCharWidthBase80('eng', "h", 131)
        setMultiboardCharWidthBase80('eng', "i", 360)
        setMultiboardCharWidthBase80('eng', "j", 288)
        setMultiboardCharWidthBase80('eng', "k", 144)
        setMultiboardCharWidthBase80('eng', "l", 360)
        setMultiboardCharWidthBase80('eng', "m", 80)
        setMultiboardCharWidthBase80('eng', "n", 131)
        setMultiboardCharWidthBase80('eng', "o", 120)
        setMultiboardCharWidthBase80('eng', "p", 120)
        setMultiboardCharWidthBase80('eng', "q", 120)
        setMultiboardCharWidthBase80('eng', "r", 206)
        setMultiboardCharWidthBase80('eng', "s", 160)
        setMultiboardCharWidthBase80('eng', "t", 206)
        setMultiboardCharWidthBase80('eng', "u", 131)
        setMultiboardCharWidthBase80('eng', "v", 144)
        setMultiboardCharWidthBase80('eng', "w", 90)
        setMultiboardCharWidthBase80('eng', "x", 131)
        setMultiboardCharWidthBase80('eng', "y", 144)
        setMultiboardCharWidthBase80('eng', "z", 144)
        setMultiboardCharWidthBase80('eng', "A", 103)
        setMultiboardCharWidthBase80('eng', "B", 120)
        setMultiboardCharWidthBase80('eng', "C", 103)
        setMultiboardCharWidthBase80('eng', "D", 103)
        setMultiboardCharWidthBase80('eng', "E", 131)
        setMultiboardCharWidthBase80('eng', "F", 160)
        setMultiboardCharWidthBase80('eng', "G", 103)
        setMultiboardCharWidthBase80('eng', "H", 96)
        setMultiboardCharWidthBase80('eng', "I", 288)
        setMultiboardCharWidthBase80('eng', "J", 240)
        setMultiboardCharWidthBase80('eng', "K", 120)
        setMultiboardCharWidthBase80('eng', "L", 131)
        setMultiboardCharWidthBase80('eng', "M", 76)
        setMultiboardCharWidthBase80('eng', "N", 96)
        setMultiboardCharWidthBase80('eng', "O", 85)
        setMultiboardCharWidthBase80('eng', "P", 131)
        setMultiboardCharWidthBase80('eng', "Q", 85)
        setMultiboardCharWidthBase80('eng', "R", 120)
        setMultiboardCharWidthBase80('eng', "S", 131)
        setMultiboardCharWidthBase80('eng', "T", 144)
        setMultiboardCharWidthBase80('eng', "U", 103)
        setMultiboardCharWidthBase80('eng', "V", 120)
        setMultiboardCharWidthBase80('eng', "W", 76)
        setMultiboardCharWidthBase80('eng', "X", 111)
        setMultiboardCharWidthBase80('eng', "Y", 120)
        setMultiboardCharWidthBase80('eng', "Z", 111)
        setMultiboardCharWidthBase80('eng', "1", 206)
        setMultiboardCharWidthBase80('eng', "2", 131)
        setMultiboardCharWidthBase80('eng', "3", 131)
        setMultiboardCharWidthBase80('eng', "4", 111)
        setMultiboardCharWidthBase80('eng', "5", 131)
        setMultiboardCharWidthBase80('eng', "6", 120)
        setMultiboardCharWidthBase80('eng', "7", 131)
        setMultiboardCharWidthBase80('eng', "8", 111)
        setMultiboardCharWidthBase80('eng', "9", 120)
        setMultiboardCharWidthBase80('eng', "0", 111)
        setMultiboardCharWidthBase80('eng', ":", 360)
        setMultiboardCharWidthBase80('eng', ";", 360)
        setMultiboardCharWidthBase80('eng', ".", 360)
        setMultiboardCharWidthBase80('eng', "#", 103)
        setMultiboardCharWidthBase80('eng', ",", 360)
        setMultiboardCharWidthBase80('eng', " ", 288) --space
        setMultiboardCharWidthBase80('eng', "'", 480)
        setMultiboardCharWidthBase80('eng', "!", 360)
        setMultiboardCharWidthBase80('eng', "$", 131)
        setMultiboardCharWidthBase80('eng', "&", 120)
        setMultiboardCharWidthBase80('eng', "/", 180)
        setMultiboardCharWidthBase80('eng', "(", 240)
        setMultiboardCharWidthBase80('eng', ")", 240)
        setMultiboardCharWidthBase80('eng', "=", 111)
        setMultiboardCharWidthBase80('eng', "?", 180)
        setMultiboardCharWidthBase80('eng', "^", 144)
        setMultiboardCharWidthBase80('eng', "<", 131)
        setMultiboardCharWidthBase80('eng', ">", 131)
        setMultiboardCharWidthBase80('eng', "-", 180)
        setMultiboardCharWidthBase80('eng', "+", 111)
        setMultiboardCharWidthBase80('eng', "*", 180)
        setMultiboardCharWidthBase80('eng', "|", 480) --2 vertical bars in a row escape to one. So you could print 960 ones in a line, 480 would display. Maybe need to adapt to this before calculating string width.
        setMultiboardCharWidthBase80('eng', "~", 144)
        setMultiboardCharWidthBase80('eng', "{", 240)
        setMultiboardCharWidthBase80('eng', "}", 240)
        setMultiboardCharWidthBase80('eng', "[", 240)
        setMultiboardCharWidthBase80('eng', "]", 240)
        setMultiboardCharWidthBase80('eng', "_", 120)
        setMultiboardCharWidthBase80('eng', "\x25", 103) --percent
        setMultiboardCharWidthBase80('eng', "\x5C", 180) --backslash
        setMultiboardCharWidthBase80('eng', "\x22", 206) --double quotation mark
        setMultiboardCharWidthBase80('eng', "\x40", 96) --at sign
        setMultiboardCharWidthBase80('eng', "\x60", 206) --Gravis (Accent)
    end
end

Lua:
Debug.beginFile("IngameConsole")
--[[

--------------------------
----| Ingame Console |----
--------------------------

/**********************************************
* Allows you to use the following ingame commands:
* "-exec <code>" to execute any code ingame.
* "-console" to start an ingame console interpreting any further chat input as code and showing both return values of function calls and error messages. Furthermore, the print function will print
*    directly to the console after it got started. You can still look up all print messages in the F12-log.
***********************
* -------------------
* |Using the console|
* -------------------
* Any (well, most) chat input by any player after starting the console is interpreted as code and directly executed. You can enter terms (like 4+5 or just any variable name), function calls (like print("bla"))
* and set-statements (like y = 5). If the code has any return values, all of them are printed to the console. Erroneous code will print an error message.
* Chat input starting with a hyphen is being ignored by the console, i.e. neither executed as code nor printed to the console. This allows you to still use other chat commands like "-exec" without prompting errors.
***********************
* ------------------
* |Multiline-Inputs|
* ------------------
* You can prevent a chat input from being immediately executed by preceeding it with the '>' character. All lines entered this way are halted, until any line not starting with '>' is being entered.
* The first input without '>' will execute all halted lines (and itself) in one chunk.
* Example of a chat input (the console will add an additional '>' to every line):
* >function a(x)
* >return x
* end
***********************
* Note that multiline inputs don't accept pure term evaluations, e.g. the following input is not supported and will prompt an error, while the same lines would have worked as two single-line inputs:
* >x = 5
* x
***********************
* -------------------
* |Reserved Keywords|
* -------------------
* The following keywords have a reserved functionality, i.e. are direct commands for the console and will not be interpreted as code:
* - 'help'          - will show a list of all reserved keywords along very short explanations.
* - 'exit'          - will shut down the console
* - 'share'         - will share the players console with every other player, allowing others to read and write into it. Will force-close other players consoles, if they have one active.
* - 'clear'         - will clear all text from the console, except the word 'clear'
* - 'lasttrace'     - will show the stack trace of the latest error that occured within IngameConsole
* - 'show'          - will show the console, after it was accidently hidden (you can accidently hide it by showing another multiboard, while the console functionality is still up and running).
* - 'printtochat'   - will let the print function return to normal behaviour (i.e. print to the chat instead of the console).
* - 'printtoconsole'- will let the print function print to the console (which is default behaviour).
* - 'autosize on'   - will enable automatic console resize depending on the longest string in the display. This is turned on by default.
* - 'autosize off'  - will disable automatic console resize and instead linebreak long strings into multiple lines.
* - 'textlang eng'  - lets the console use english Wc3 text language font size to compute linebreaks (look in your Blizzard launcher settings to find out)
* - 'textlang ger'  - lets the console use german Wc3 text language font size to compute linebreaks (look in your Blizzard launcher settings to find out)
***********************
* --------------
* |Paste Helper|
* --------------
* @Luashine has created a tool that simplifies pasting multiple lines of code from outside Wc3 into the IngameConsole.
* This is particularly useful, when you want to execute a large chunk of testcode containing several linebreaks.
* Goto: https://github.com/Luashine/wc3-debug-console-paste-helper#readme
*
*************************************************/
--]]

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

---@class IngameConsole
IngameConsole = {
    --Settings
    numRows = 20                        ---@type integer Number of Rows of the console (multiboard), excluding the title row. So putting 20 here will show 21 rows, first being the title row.
    ,   autosize = true                 ---@type boolean Defines, whether the width of the main Column automatically adjusts with the longest string in the display.
    ,   currentWidth = 0.5              ---@type number Current and starting Screen Share of the console main column.
    ,   mainColMinWidth = 0.3           ---@type number Minimum Screen share of the console main column.
    ,   mainColMaxWidth = 0.8           ---@type number Maximum Scren share of the console main column.
    ,   tsColumnWidth = 0.06            ---@type number Screen Share of the Timestamp Column
    ,   linebreakBuffer = 0.008         ---@type number Screen Share that is added to longest string in display to calculate the screen share for the console main column. Compensates for the small inaccuracy of the String Width function.
    ,   maxLinebreaks = 8               ---@type integer Defines the maximum amount of linebreaks, before the remaining output string will be cut and not further displayed.
    ,   printToConsole = true           ---@type boolean defines, if the print function should print to the console or to the chat
    ,   sharedConsole = false           ---@type boolean defines, if the console is displayed to each player at the same time (accepting all players input) or if all players much start their own console.
    ,   showTraceOnError = false        ---@type boolean defines, if the console shows a trace upon printing errors. Usually not too useful within console, because you have just initiated the erroneous call.
    ,   textLanguage = 'eng'            ---@type string text language of your Wc3 installation, which influences font size (look in the settings of your Blizzard launcher). Currently only supports 'eng' and 'ger'.
    ,   colors = {
        timestamp = "bbbbbb"            ---@type string Timestamp Color
        ,   singleLineInput = "ffffaa"  ---@type string Color to be applied to single line console inputs
        ,   multiLineInput = "ffcc55"   ---@type string Color to be applied to multi line console inputs
        ,   returnValue = "00ffff"      ---@type string Color applied to return values
        ,   error = "ff5555"            ---@type string Color to be applied to errors resulting of function calls
        ,   keywordInput = "ff00ff"     ---@type string Color to be applied to reserved keyword inputs (console reserved keywords)
        ,   info = "bbbbbb"             ---@type string Color to be applied to info messages from the console itself (for instance after creation or after printrestore)
    }
    --Privates
    ,   numCols = 2                     ---@type integer Number of Columns of the console (multiboard). Adjusting this requires further changes on code base.
    ,   player = nil                    ---@type player player for whom the console is being created
    ,   currentLine = 0                 ---@type integer Current Output Line of the console.
    ,   inputload = ''                  ---@type string Input Holder for multi-line-inputs
    ,   output = {}                     ---@type string[] Array of all output strings
    ,   outputTimestamps = {}           ---@type string[] Array of all output string timestamps
    ,   outputWidths = {}               ---@type number[] remembers all string widths to allow for multiboard resize
    ,   trigger = nil                   ---@type trigger trigger processing all inputs during console lifetime
    ,   multiboard = nil                ---@type multiboard
    ,   timer = nil                     ---@type timer gets started upon console creation to measure timestamps
    ,   errorHandler = nil              ---@type fun(errorMsg:string):string error handler to be used within xpcall. We create one per console to make it compatible with console-specific settings.
    ,   lastTrace = ''                  ---@type string trace of last error occured within console. To be printed via reserved keyword "lasttrace"
    --Statics
    ,   keywords = {}                   ---@type table<string,function> saves functions to be executed for all reserved keywords
    ,   playerConsoles = {}             ---@type table<player,IngameConsole> Consoles currently being active. up to one per player.
    ,   originalPrint = print           ---@type function original print function to restore, after the console gets closed.
}
IngameConsole.__index = IngameConsole
IngameConsole.__name = 'IngameConsole'

------------------------
--| Console Creation |--
------------------------

---Creates and opens up a new console.
---@param consolePlayer player player for whom the console is being created
---@return IngameConsole
function IngameConsole.create(consolePlayer)
    local new = {} ---@type IngameConsole
    setmetatable(new, IngameConsole)
    ---setup Object data
    new.player = consolePlayer
    new.output = {}
    new.outputTimestamps = {}
    new.outputWidths = {}
    --Timer
    new.timer = CreateTimer()
    TimerStart(new.timer, 3600., true, nil) --just to get TimeElapsed for printing Timestamps.
    --Trigger to be created after short delay, because otherwise it would fire on "-console" input immediately and lead to stack overflow.
    new:setupTrigger()
    --Multiboard
    new:setupMultiboard()
    --Create own error handler per console to be compatible with console-specific settings
    new:setupErrorHandler()
    --Share, if settings say so
    if IngameConsole.sharedConsole then
        new:makeShared() --we don't have to exit other players consoles, because we look for the setting directly in the class and there just logically can't be other active consoles.
    end
    --Welcome Message
    new:out('info', 0, false, "Console started. Any further chat input will be executed as code, except when beginning with \x22-\x22.")
    return new
end

---Creates the multiboard used for console display.
function IngameConsole:setupMultiboard()
    self.multiboard = CreateMultiboard()
    MultiboardSetRowCount(self.multiboard, self.numRows + 1) --title row adds 1
    MultiboardSetColumnCount(self.multiboard, self.numCols)
    MultiboardSetTitleText(self.multiboard, "Console")
    local mbitem
    for col = 1, self.numCols do
        for row = 1, self.numRows + 1 do --Title row adds 1
            mbitem = MultiboardGetItem(self.multiboard, row -1, col -1)
            MultiboardSetItemStyle(mbitem, true, false)
            MultiboardSetItemValueColor(mbitem, 255, 255, 255, 255)    -- Colors get applied via text color code
            MultiboardSetItemWidth(mbitem, (col == 1 and self.tsColumnWidth) or self.currentWidth )
            MultiboardReleaseItem(mbitem)
        end
    end
    mbitem = MultiboardGetItem(self.multiboard, 0, 0)
    MultiboardSetItemValue(mbitem, "|cffffcc00Timestamp|r")
    MultiboardReleaseItem(mbitem)
    mbitem = MultiboardGetItem(self.multiboard, 0, 1)
    MultiboardSetItemValue(mbitem, "|cffffcc00Line|r")
    MultiboardReleaseItem(mbitem)
    self:showToOwners()
end

---Creates the trigger that responds to chat events.
function IngameConsole:setupTrigger()
    self.trigger = CreateTrigger()
    TriggerRegisterPlayerChatEvent(self.trigger, self.player, "", false) --triggers on any input of self.player
    TriggerAddCondition(self.trigger, Condition(function() return string.sub(GetEventPlayerChatString(),1,1) ~= '-' end)) --console will not react to entered stuff starting with '-'. This still allows to use other chat orders like "-exec".
    TriggerAddAction(self.trigger, function() self:processInput(GetEventPlayerChatString()) end)
end

---Creates an Error Handler to be used by xpcall below.
---Adds stack trace plus formatting to the message.
function IngameConsole:setupErrorHandler()
    self.errorHandler = function(errorMsg)
        errorMsg = Debug.getLocalErrorMsg(errorMsg)
        local _, tracePiece, lastFile = nil, "", errorMsg:match("^.-:") or "<unknown>" -- errors on objects created within Ingame Console don't have a file and linenumber. Consider "x = {}; x[nil] = 5".
        local fullMsg = errorMsg .. "\nTraceback (most recent call first):\n" .. (errorMsg:match("^.-:\x25d+") or "<unknown>")
        --Get Stack Trace. Starting at depth 5 ensures that "error", "messageHandler", "xpcall" and the input error message are not included.
        for loopDepth = 5, 50 do --get trace on depth levels up to 50
            ---@diagnostic disable-next-line: cast-local-type, assign-type-mismatch
            _, tracePiece = pcall(error, "", loopDepth) ---@type boolean, string
            tracePiece = Debug.getLocalErrorMsg(tracePiece)
            if #tracePiece > 0 then --some trace pieces can be empty, but there can still be valid ones beyond that
                fullMsg = fullMsg .. " <- " .. ((tracePiece:match("^.-:") == lastFile) and tracePiece:match(":\x25d+"):sub(2,-1) or tracePiece:match("^.-:\x25d+"))
                lastFile = tracePiece:match("^.-:")
            end
        end
        self.lastTrace = fullMsg
        return "ERROR: " .. (self.showTraceOnError and fullMsg or errorMsg)
    end
end

---Shares this console with all players.
function IngameConsole:makeShared()
    local player
    for i = 0, GetBJMaxPlayers() -1 do
        player = Player(i)
        if (GetPlayerSlotState(player) == PLAYER_SLOT_STATE_PLAYING) and (IngameConsole.playerConsoles[player] ~= self) then --second condition ensures that the player chat event is not added twice for the same player.
            IngameConsole.playerConsoles[player] = self
            TriggerRegisterPlayerChatEvent(self.trigger, player, "", false) --triggers on any input
        end
    end
    self.sharedConsole = true
end

---------------------
--|      In       |--
---------------------

---Processes a chat string. Each input will be printed. Incomplete multiline-inputs will be halted until completion. Completed inputs will be converted to a function and executed. If they have an output, it will be printed.
---@param inputString string
function IngameConsole:processInput(inputString)
    --if the input is a reserved keyword, conduct respective actions and skip remaining actions.
    if IngameConsole.keywords[inputString] then --if the input string is a reserved keyword
        self:out('keywordInput', 1, false, inputString)
        IngameConsole.keywords[inputString](self) --then call the method with the same name. IngameConsole.keywords["exit"](self) is just self.keywords:exit().
        return
    end
    --if the input is a multi-line-input, queue it into the string buffer (inputLoad), but don't yet execute anything
    if string.sub(inputString, 1, 1) == '>' then --multiLineInput
        inputString = string.sub(inputString, 2, -1)
        self:out('multiLineInput',2, false, inputString)
        self.inputload = self.inputload .. inputString .. '\r' --carriage return
    else --if the input is either singleLineInput OR the last line of multiLineInput, execute the whole thing.
        self:out(self.inputload == '' and 'singleLineInput' or 'multiLineInput', 1, false, inputString)
        self.inputload = self.inputload .. inputString
        local loadedFunc, errorMsg = load("return " .. self.inputload) --adds return statements, if possible (works for term statements)
        if loadedFunc == nil then
            loadedFunc, errorMsg = load(self.inputload)
        end
        self.inputload = '' --empty inputload before execution of pcall. pcall can break (rare case, can for example be provoked with metatable.__tostring = {}), which would corrupt future console inputs.
        --manually catch case, where the input did not define a proper Lua statement (i.e. loadfunc is nil)
        local results = loadedFunc and table.pack(xpcall(loadedFunc, self.errorHandler)) or {false, "Input is not a valid Lua-statement: " .. errorMsg}
        --output error message (unsuccessful case) or return values (successful case)
        if not results[1] then --results[1] is the error status that pcall always returns. False stands for: error occured.
            self:out('error', 0, true, results[2]) -- second result of pcall is the error message in case an error occured
        elseif results.n > 1 then --Check, if there was at least one valid output argument. We check results.n instead of results[2], because we also get nil as a proper return value this way.
            self:out('returnValue', 0, true, table.unpack(results, 2, results.n))
        end
    end
end

----------------------
--|      Out       |--
----------------------

-- split color codes, split linebreaks, print lines separately, print load-errors, update string width, update text, error handling with stack trace.

---Duplicates Color coding around linebreaks to make each line printable separately.
---Operates incorrectly on lookalike color codes invalidated by preceeding escaped vertical bar (like "||cffffcc00bla|r").
---Also operates incorrectly on multiple color codes, where the first is missing the end sequence (like "|cffffcc00Hello |cff0000ffWorld|r")
---@param inputString string
---@return string, integer
function IngameConsole.spreadColorCodes(inputString)
    local replacementTable = {} --remembers all substrings to be replaced and their replacements.
    for foundInstance, color in inputString:gmatch("((|c\x25x\x25x\x25x\x25x\x25x\x25x\x25x\x25x).-|r)") do
        replacementTable[foundInstance] = foundInstance:gsub("(\r?\n)", "|r\x251" .. color)
    end
    return inputString:gsub("((|c\x25x\x25x\x25x\x25x\x25x\x25x\x25x\x25x).-|r)", replacementTable)
end

---Concatenates all inputs to one string, spreads color codes around line breaks and prints each line to the console separately.
---@param colorTheme? '"timestamp"'| '"singleLineInput"' | '"multiLineInput"' | '"result"' | '"keywordInput"' | '"info"' | '"error"' | '"returnValue"' Decides about the color to be applied. Currently accepted: 'timestamp', 'singleLineInput', 'multiLineInput', 'result', nil. (nil equals no colorTheme, i.e. white color)
---@param numIndentations integer Number of '>' chars that shall preceed the output
---@param hideTimestamp boolean Set to false to hide the timestamp column and instead show a "->" symbol.
---@param ... any the things to be printed in the console.
function IngameConsole:out(colorTheme, numIndentations, hideTimestamp, ...)
    local inputs = table.pack(...)
    for i = 1, inputs.n do
        inputs[i] = tostring(inputs[i]) --apply tostring on every input param in preparation for table.concat
    end
    --Concatenate all inputs (4-space-separated)
    local printOutput = table.concat(inputs, '    ', 1, inputs.n)
    printOutput = printOutput:find("(\r?\n)") and IngameConsole.spreadColorCodes(printOutput) or printOutput
    local substrStart, substrEnd = 1, 1
    local numLinebreaks, completePrint = 0, true
    repeat
        substrEnd = (printOutput:find("(\r?\n)", substrStart) or 0) - 1
        numLinebreaks, completePrint = self:lineOut(colorTheme, numIndentations, hideTimestamp, numLinebreaks, printOutput:sub(substrStart, substrEnd))
        hideTimestamp = true
        substrStart = substrEnd + 2
    until substrEnd == -1 or numLinebreaks > self.maxLinebreaks
    if substrEnd ~= -1 or not completePrint then
        self:lineOut('info', 0, false, 0, "Previous value not entirely printed after exceeding maximum number of linebreaks. Consider adjusting 'IngameConsole.maxLinebreaks'.")
    end
    self:updateMultiboard()
end

---Prints the given string to the console with the specified colorTheme and the specified number of indentations.
---Only supports one-liners (no \n) due to how multiboards work. Will add linebreaks though, if the one-liner doesn't fit into the given multiboard space.
---@param colorTheme? '"timestamp"'| '"singleLineInput"' | '"multiLineInput"' | '"result"' | '"keywordInput"' | '"info"' | '"error"' | '"returnValue"' Decides about the color to be applied. Currently accepted: 'timestamp', 'singleLineInput', 'multiLineInput', 'result', nil. (nil equals no colorTheme, i.e. white color)
---@param numIndentations integer Number of greater '>' chars that shall preceed the output
---@param hideTimestamp boolean Set to false to hide the timestamp column and instead show a "->" symbol.
---@param numLinebreaks integer
---@param printOutput string the line to be printed in the console.
---@return integer numLinebreaks, boolean hasPrintedEverything returns true, if everything could be printed. Returns false otherwise (can happen for very long strings).
function IngameConsole:lineOut(colorTheme, numIndentations, hideTimestamp, numLinebreaks, printOutput)
    --add preceeding greater chars
    printOutput = ('>'):rep(numIndentations) .. printOutput
    --Print a space instead of the empty string. This allows the console to identify, if the string has already been fully printed (see while-loop below).
    if printOutput == '' then
        printOutput = ' '
    end
    --Compute Linebreaks.
    local linebreakWidth = ((self.autosize and self.mainColMaxWidth) or self.currentWidth )
    local partialOutput = nil
    local maxPrintableCharPosition
    local printWidth
    while string.len(printOutput) > 0  and numLinebreaks <= self.maxLinebreaks do --break, if the input string has reached length 0 OR when the maximum number of linebreaks would be surpassed.
        --compute max printable substring (in one multiboard line)
        maxPrintableCharPosition, printWidth = IngameConsole.getLinebreakData(printOutput, linebreakWidth - self.linebreakBuffer, self.textLanguage)
        --adds timestamp to the first line of any output
        if numLinebreaks == 0 then
            partialOutput = printOutput:sub(1, numIndentations) .. ((IngameConsole.colors[colorTheme] and "|cff" .. IngameConsole.colors[colorTheme] .. printOutput:sub(numIndentations + 1, maxPrintableCharPosition) .. "|r") or printOutput:sub(numIndentations + 1, maxPrintableCharPosition)) --Colorize the output string, if a color theme was specified. IngameConsole.colors[colorTheme] can be nil.
            table.insert(self.outputTimestamps, "|cff" .. IngameConsole.colors['timestamp'] .. ((hideTimestamp and '            ->') or IngameConsole.formatTimerElapsed(TimerGetElapsed(self.timer))) .. "|r")
        else
            partialOutput = (IngameConsole.colors[colorTheme] and "|cff" .. IngameConsole.colors[colorTheme] .. printOutput:sub(1, maxPrintableCharPosition) .. "|r") or printOutput:sub(1, maxPrintableCharPosition) --Colorize the output string, if a color theme was specified. IngameConsole.colors[colorTheme] can be nil.
            table.insert(self.outputTimestamps, '            ..') --need a dummy entry in the timestamp list to make it line-progress with the normal output.
        end
        numLinebreaks = numLinebreaks + 1
        --writes output string and width to the console tables.
        table.insert(self.output, partialOutput)
        table.insert(self.outputWidths, printWidth + self.linebreakBuffer) --remember the Width of this printed string to adjust the multiboard size in case. 0.5 percent is added to avoid the case, where the multiboard width is too small by a tiny bit, thus not showing some string without spaces.
        --compute remaining string to print
        printOutput = string.sub(printOutput, maxPrintableCharPosition + 1, -1) --remaining string until the end. Returns empty string, if there is nothing left
    end
    self.currentLine = #self.output
    return numLinebreaks, string.len(printOutput) == 0 --printOutput is the empty string, if and only if everything has been printed
end

---Lets the multiboard show the recently printed lines.
function IngameConsole:updateMultiboard()
    local startIndex = math.max(self.currentLine - self.numRows, 0) --to be added to loop counter to get to the index of output table to print
    local outputIndex = 0
    local maxWidth = 0.
    local mbitem
    for i = 1, self.numRows do --doesn't include title row (index 0)
        outputIndex = i + startIndex
        mbitem = MultiboardGetItem(self.multiboard, i, 0)
        MultiboardSetItemValue(mbitem, self.outputTimestamps[outputIndex] or '')
        MultiboardReleaseItem(mbitem)
        mbitem = MultiboardGetItem(self.multiboard, i, 1)
        MultiboardSetItemValue(mbitem, self.output[outputIndex] or '')
        MultiboardReleaseItem(mbitem)
        maxWidth = math.max(maxWidth, self.outputWidths[outputIndex] or 0.) --looping through non-defined widths, so need to coalesce with 0
    end
    --Adjust Multiboard Width, if necessary.
    maxWidth = math.min(math.max(maxWidth, self.mainColMinWidth), self.mainColMaxWidth)
    if self.autosize and self.currentWidth ~= maxWidth then
        self.currentWidth = maxWidth
        for i = 1, self.numRows +1 do
            mbitem = MultiboardGetItem(self.multiboard, i-1, 1)
            MultiboardSetItemWidth(mbitem, maxWidth)
            MultiboardReleaseItem(mbitem)
        end
        self:showToOwners() --reshow multiboard to update item widths on the frontend
    end
end

---Shows the multiboard to all owners (one or all players)
function IngameConsole:showToOwners()
    if self.sharedConsole or GetLocalPlayer() == self.player then
        MultiboardDisplay(self.multiboard, true)
        MultiboardMinimize(self.multiboard, false)
    end
end

---Formats the elapsed time as "mm: ss. hh" (h being a hundreds of a sec)
function IngameConsole.formatTimerElapsed(elapsedInSeconds)
    return string.format("\x2502d: \x2502.f. \x2502.f", elapsedInSeconds // 60, math.fmod(elapsedInSeconds, 60.) // 1, math.fmod(elapsedInSeconds, 1) * 100)
end

---Computes the max printable substring for a given string and a given linebreakWidth (regarding a single line of console).
---Returns both the substrings last char position and its total width in the multiboard.
---@param stringToPrint string the string supposed to be printed in the multiboard console.
---@param linebreakWidth number the maximum allowed width in one line of the console, before a string must linebreak
---@param textLanguage string 'ger' or 'eng'
---@return integer maxPrintableCharPosition, number printWidth
function IngameConsole.getLinebreakData(stringToPrint, linebreakWidth, textLanguage)
    local loopWidth = 0.
    local bytecodes = table.pack(string.byte(stringToPrint, 1, -1))
    for i = 1, bytecodes.n do
        loopWidth = loopWidth + string.charMultiboardWidth(bytecodes[i], textLanguage)
        if loopWidth > linebreakWidth then
            return i-1, loopWidth - string.charMultiboardWidth(bytecodes[i], textLanguage)
        end
    end
    return bytecodes.n, loopWidth
end

-------------------------
--| Reserved Keywords |--
-------------------------

---Exits the Console
---@param self IngameConsole
function IngameConsole.keywords.exit(self)
    DestroyMultiboard(self.multiboard)
    DestroyTrigger(self.trigger)
    DestroyTimer(self.timer)
    IngameConsole.playerConsoles[self.player] = nil
    if next(IngameConsole.playerConsoles) == nil then --set print function back to original, when no one has an active console left.
        print = IngameConsole.originalPrint
    end
end

---Lets the console print to chat
---@param self IngameConsole
function IngameConsole.keywords.printtochat(self)
    self.printToConsole = false
    self:out('info', 0, false, "The print function will print to the normal chat.")
end

---Lets the console print to itself (default)
---@param self IngameConsole
function IngameConsole.keywords.printtoconsole(self)
    self.printToConsole = true
    self:out('info', 0, false, "The print function will print to the console.")
end

---Shows the console in case it was hidden by another multiboard before
---@param self IngameConsole
function IngameConsole.keywords.show(self)
    self:showToOwners() --might be necessary to do, if another multiboard has shown up and thereby hidden the console.
    self:out('info', 0, false, "Console is showing.")
end

---Prints all available reserved keywords plus explanations.
---@param self IngameConsole
function IngameConsole.keywords.help(self)
    self:out('info', 0, false, "The Console currently reserves the following keywords:")
    self:out('info', 0, false, "'help' shows the text you are currently reading.")
    self:out('info', 0, false, "'exit' closes the console.")
    self:out('info', 0, false, "'lasttrace' shows the stack trace of the latest error that occured within IngameConsole.")
    self:out('info', 0, false, "'share' allows other players to read and write into your console, but also force-closes their own consoles.")
    self:out('info', 0, false, "'clear' clears all text from the console.")
    self:out('info', 0, false, "'show' shows the console. Sensible to use, when displaced by another multiboard.")
    self:out('info', 0, false, "'printtochat' lets Wc3 print text to normal chat again.")
    self:out('info', 0, false, "'printtoconsole' lets Wc3 print text to the console (default).")
    self:out('info', 0, false, "'autosize on' enables automatic console resize depending on the longest line in the display.")
    self:out('info', 0, false, "'autosize off' retains the current console size.")
    self:out('info', 0, false, "'textlang eng' will use english text installation font size to compute linebreaks (default).")
    self:out('info', 0, false, "'textlang ger' will use german text installation font size to compute linebreaks.")
    self:out('info', 0, false, "Preceeding a line with '>' prevents immediate execution, until a line not starting with '>' has been entered.")
end

---Clears the display of the console.
---@param self IngameConsole
function IngameConsole.keywords.clear(self)
    self.output = {}
    self.outputTimestamps = {}
    self.outputWidths = {}
    self.currentLine = 0
    self:out('keywordInput', 1, false, 'clear') --we print 'clear' again. The keyword was already printed by self:processInput, but cleared immediately after.
end

---Shares the console with other players in the same game.
---@param self IngameConsole
function IngameConsole.keywords.share(self)
    for _, console in pairs(IngameConsole.playerConsoles) do
        if console ~= self then
            IngameConsole.keywords['exit'](console) --share was triggered during console runtime, so there potentially are active consoles of others players that need to exit.
        end
    end
    self:makeShared()
    self:showToOwners() --showing it to the other players.
    self:out('info', 0,false, "The console of player " .. GetConvertedPlayerId(self.player) .. " is now shared with all players.")
end

---Enables auto-sizing of console (will grow and shrink together with text size)
---@param self IngameConsole
IngameConsole.keywords["autosize on"] = function(self)
    self.autosize = true
    self:out('info', 0,false, "The console will now change size depending on its content.")
end

---Disables auto-sizing of console
---@param self IngameConsole
IngameConsole.keywords["autosize off"] = function(self)
    self.autosize = false
    self:out('info', 0,false, "The console will retain the width that it currently has.")
end

---Lets linebreaks be computed by german font size
---@param self IngameConsole
IngameConsole.keywords["textlang ger"] = function(self)
    self.textLanguage = 'ger'
    self:out('info', 0,false, "Linebreaks will now compute with respect to german text installation font size.")
end

---Lets linebreaks be computed by english font size
---@param self IngameConsole
IngameConsole.keywords["textlang eng"] = function(self)
    self.textLanguage = 'eng'
    self:out('info', 0,false, "Linebreaks will now compute with respect to english text installation font size.")
end

---Prints the stack trace of the latest error that occured within IngameConsole.
---@param self IngameConsole
IngameConsole.keywords["lasttrace"] = function(self)
    self:out('error', 0,false, self.lastTrace)
end

--------------------
--| Main Trigger |--
--------------------

do
    --Actions to be executed upon typing -exec
    local function execCommand_Actions()
        local input = string.sub(GetEventPlayerChatString(),7,-1)
        print("Executing input: |cffffff44" .. input .. "|r")
        --try preceeding the input by a return statement (preparation for printing below)
        local loadedFunc, errorMsg = load("return ".. input)
        if not loadedFunc then --if that doesn't produce valid code, try without return statement
            loadedFunc, errorMsg = load(input)
        end
        --execute loaded function in case the string defined a valid function. Otherwise print error.
        if errorMsg then
            print("|cffff5555Invalid Lua-statement: " .. Debug.getLocalErrorMsg(errorMsg) .. "|r")
        else
            ---@diagnostic disable-next-line: param-type-mismatch
            local results = table.pack(Debug.try(loadedFunc))
            if results[1] ~= nil or results.n > 1 then
                for i = 1, results.n do
                    results[i] = tostring(results[i])
                end
                --concatenate all function return values to one colorized string
                print("|cff00ffff" .. table.concat(results, '    ', 1, results.n) .. "|r")
            end
        end
    end

    local function execCommand_Condition()
        return string.sub(GetEventPlayerChatString(), 1, 6) == "-exec "
    end

    local function startIngameConsole()
        --if the triggering player already has a console, show that console and stop executing further actions
        if IngameConsole.playerConsoles[GetTriggerPlayer()] then
            IngameConsole.playerConsoles[GetTriggerPlayer()]:showToOwners()
            return
        end
        --create Ingame Console object
        IngameConsole.playerConsoles[GetTriggerPlayer()] = IngameConsole.create(GetTriggerPlayer())
        --overwrite print function
        print = function(...)
            IngameConsole.originalPrint(...) --the new print function will also print "normally", but clear the text immediately after. This is to add the message to the F12-log.
            if IngameConsole.playerConsoles[GetLocalPlayer()] and IngameConsole.playerConsoles[GetLocalPlayer()].printToConsole then
                ClearTextMessages() --clear text messages for all players having an active console
            end
            for player, console in pairs(IngameConsole.playerConsoles) do
                if console.printToConsole and (player == console.player) then --player == console.player ensures that the console only prints once, even if the console was shared among all players
                    console:out(nil, 0, false, ...)
                end
            end
        end
    end

    ---Creates the triggers listening to "-console" and "-exec" chat input.
    ---Being executed within DebugUtils (MarkGameStart overwrite).
    function IngameConsole.createTriggers()
        --Exec
        local execTrigger = CreateTrigger()
        TriggerAddCondition(execTrigger, Condition(execCommand_Condition))
        TriggerAddAction(execTrigger, execCommand_Actions)
        --Real Console
        local consoleTrigger = CreateTrigger()
        TriggerAddAction(consoleTrigger, startIngameConsole)
        --Events
        for i = 0, GetBJMaxPlayers() -1 do
            TriggerRegisterPlayerChatEvent(execTrigger, Player(i), "-exec ", false)
            TriggerRegisterPlayerChatEvent(consoleTrigger, Player(i), "-console", true)
        end
    end
end
Debug.endFile()

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 remove all Debug functionality by replacing the Debug Utils with this short code:
Lua:
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 in the configuration and delete the files for StringWidth and IngameConsole.

21.02.2021, v1.0 (Initial release)

  • Ingame Console and -exec command
  • Automatic warnings upon accessing undeclared globals
  • Definition files for common.lua and blizzard.lua to use with VS Code and sumneko extension.
  • Library functions try() for error handling, table.print() for taking quick looks into data structures and Wc3Type() to receive the Warcraft type of userdata objects.

12.09.2021, v1.1:
  • Console can now deal with pcall breaks (yes, even pcall can break, if you know how to :D)
  • Fixed a rare bug only occuring for long multiline inputs, where parts of the input were not displayed properly (but still executed properly)
  • The output only consisting of the empty string will now properly display (as... well, an empty line)
  • Improved annotations
03.10.2021, v1.1a:
  • The try-function will now properly display an error message on screen upon using it with erroneous function calls. This fixes a bug invented with version 1.1.
28.01.2022, v1.2:
  • Added a variant of section 1.2 (warnings for undeclared globals) that excludes bj-variables. Also added some explanation about variables that were intentionally nilled.
  • Added automatic error handling: the code snippet in section 2.1 will replace TriggerAddAction by a version using try to monitor code entry points. Thanks @Jampion for the suggestion.
  • Added an installation section to the main post, including a copy-paste option for all parts of this resource in one.
24.02.2022, v1.2a:
  • Ingame Console now manually recognizes erroneous Lua statements and will throw an "Invalid Lua-statement"-error instead of the non-sensible "Attempt to call a nil value".
  • Fixed an IngameConsole error, where inputting function calls separated by semicolons was not properly executed.
  • Fixed an IngameConsole error, where inputting statements including raw strings (such as load('a=5')) was sometimes not properly executed.
08.08.2022, v1.3:
  • Added Chapter 2.6. about stack traces and Chapter 2.7 about Logging additional information.
  • Included GetStackTrace to be able to print traces of erroneous function calls.
    Credits to @HerlySQR for the original resource and permission to use it in Debug Utils.
  • Included LogStackTrace for logging the function trace provided by GetStackTrace plus further information that the try-function could decide to print upon encountering errors.
07.01.2023, v2.0:

Major update including complete rewrite of both code and resource main page.

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

Attachments

  • LuaDebugUtils_v1.1a.w3x
    46 KB · Views: 71
  • LuaDebugUtils_v1.2a.w3x
    45.8 KB · Views: 38
  • LuaDebugUtils_v1.3.w3x
    48.1 KB · Views: 40
  • Blizzard.1.33.v2.lua
    452.4 KB · Views: 20
  • HiddenNatives.1.33.v2.lua
    5.4 KB · Views: 26
  • common.1.33.v2.lua
    369.6 KB · Views: 23
  • LuaDebugUtils_v2.0a.w3x
    93.2 KB · Views: 87
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
477
@AGD Sumnekos extension writes its settings into the standard VSCode "settings.json", which is usually located at "C:\Users\<username>\AppData\Roaming\Code\User\settings.json".

You can also access it directly from VSCode:
1. In VSCode, go File -> Preferences -> Settings
2. Type "settings" into the search box (or any other search keyword, lol). There will be results showing a "Edit in settings.json", which you need to click.

1617122176574.png


3. VSCode will open a new tab, where you can change and save your settings. Mine are:
Lua:
{
    "editor.minimap.enabled": false,
    "json.maxItemsComputed": 30000,
    "editor.snippetSuggestions": "bottom",
    "Lua.runtime.version": "Lua 5.3",
    "Lua.workspace.preloadFileSize": 2000,
    "Lua.workspace.maxPreload": 1000,
    "Lua.intelliSense.searchDepth": 10,
    "[json]": {
        "editor.quickSuggestions": {
            "strings": true
        },
        "editor.suggest.insertMode": "replace",
        "gitlens.codeLens.scopes": [
            "document"
        ]
    },
    "Lua.diagnostics.disable": [
        "lowercase-global"
    ],
    "editor.tabCompletion": "on",
    "workbench.editor.enablePreview": false,
    "editor.semanticHighlighting.enabled": true,
    "editor.fontLigatures": null
}
The settings starting with "Lua" are the one's sumneko's extension is working with. I'm not sure, if there are any additional options apart from the mentioned ones, though. Just like the supported keywords from Emmy annotation, it's rather hard to find proper information on this extension in the internet, even though it seems to be widely used. For example, sumnekos extension doesn't support the @public annotation for public class attributes, while other extensions do. This doesn't prevent you from using @public, of course. It just means, there won't be any syntax highlighting or intellisense feature using it.

Regarding loading times, you might tweak the lines "Lua.workspace.preloadFileSize", "Lua.workspace.maxPreload" and "Lua.intelliSense.searchDepth" a bit. Hovering your mouse over these in the settings.json shows their descriptions, but I'm not familiar enough with the extensions internals to really understand their purpose. At least, with my settings, I don't need to click on blizzard.lua to load it upon starting a session. It's still sometimes reloading stuff very slowly, when I change filenames, delete or add files.

Hope that works for you. I'd be happy to see your settings, when you find better suitable ones :)
 

AGD

AGD

Level 16
Joined
Mar 29, 2016
Messages
688
The settings starting with "Lua" are the one's sumneko's extension is working with. I'm not sure, if there are any additional options apart from the mentioned ones, though. Just like the supported keywords from Emmy annotation, it's rather hard to find proper information on this extension in the internet, even though it seems to be widely used. For example, sumnekos extension doesn't support the @public annotation for public class attributes, while other extensions do. This doesn't prevent you from using @public, of course. It just means, there won't be any syntax highlighting or intellisense feature using it.
I'm specifically curious if it has something similar to a snippet.json used by keyring.Lua located at "%userprofile%\.vscode\extensions\keyring.lua-0.0.9\snippets\snippet.json", where I can add all common.j and blizzard.j functions and globals for intellisense.
But yup extension documentation seems lacking instructions especially with regards to tweaking.

Regarding loading times, you might tweak the lines "Lua.workspace.preloadFileSize", "Lua.workspace.maxPreload" and "Lua.intelliSense.searchDepth" a bit. Hovering your mouse over these in the settings.json shows their descriptions, but I'm not familiar enough with the extensions internals to really understand their purpose. At least, with my settings, I don't need to click on blizzard.lua to load it upon starting a session. It's still sometimes reloading stuff very slowly, when I change filenames, delete or add files.

Hope that works for you. I'd be happy to see your settings, when you find better suitable ones :)
I just realized the reason they don't automatically load for me is because I put them in a separate folder in the workspace and is not being tracked. At least it's fine now =).
 
Level 20
Joined
Jul 10, 2009
Messages
477
I'm not aware of any extension-specific snippets file for sumneko.
Using the lua snippets file "%userprofile%\AppData\Roaming\Code\User\snippets\lua.json" or creating a workspace specific snippets file "<workspaceLocation>\.vscode\wc3snippets.code-snippets" should work, though (I'm not really using snippets, sorry).
You can press F1 and go to Preferences: Configure User Snippets to see your options from within VSCode.

Btw, I found some documentation in "%userprofile%\.vscode\extensions\sumneko.lua-1.20.1\README.md". Have not yet read it, but it links to a fair amount of documented git repos, so might be worth a read.
 
Level 20
Joined
Jul 10, 2009
Messages
477
Update v1.1:
  • Console can now deal with pcall breaks (yes, even pcall can break, if you know how to :D)
  • Fixed a rare bug only occuring for long multiline inputs, where parts of the input were not displayed properly (but still executed properly)
  • The output only consisting of the empty string will now properly display (as... well, an empty line)
  • Improved annotations
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
Hey Eikonium, this debug script is great and all, but I've just realised that the mention of "WC3 type" function won't be useable since you claim in this post that tostring(object), used in WC3type, causes desyncs:
  • Avoid Lua functionality that can desync.
    E.g., iteration order within pairs-loops is not guaranteed to be the same for all players, so changing game state within such a loop can lead to a desync. SyncedTables exist as a solution.
    Wc3 hashtables can (afaik) also produce desyncs in combination with the Lua garbage collector, especially after using FlushChildHashtable, so people should just use Lua tables.
    GetHandleId(object) and tostring(object) are also not synced in Lua (and there is no reason to use GetHandleId in Lua anyway).
  • Don't use Wc3 natives in the Lua root (i.e. code not part of any function).
    @Tasyen conducted some tests about this. Conclusion is, just don't do it. Earliest point to use Wc3 natives is during the initialization process, typically the GUI Map Init event.
Does that mean there's no way around it?
 
Level 20
Joined
Jul 10, 2009
Messages
477
Hey Eikonium, this debug script is great and all, but I've just realised that the mention of "WC3 type" function won't be useable since you claim in this post that tostring(object), used in WC3type, causes desyncs:

Does that mean there's no way around it?
Hey @Wrda,

tostring() is indeed not synchronous between players (except for primitive data types, i.e. booleans, numbers, strings), but the Wc3Type function actually is synced.
The reason is that tostring(nonPrimitiveObject) outputs a string of the form <type>: <hexCode>. The hexCode is the non-synced part. Wc3Type only uses <type>, so it's safe to use in multiplayer.

Please bear in mind though that the whole stuff in my main post is intended for debug utility, so you would probably remove it from your map before releasing it to the public anyway.

Granted, Wc3Type() has use cases outside debugging (and I'm even using it in Set/Group datastructure myself :p).
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
477
Ahhh I see.
Now I'm intrigued by your Set/Group datastructure, seems epic :D
Lua is really beyond my scope of comprehension.
Thanks man, I have worked hard on that submission :)
Using it in my own projects, it saves a lot of time.

If you want to get into Lua, I highly recommend reading a bit through Programming with Lua 4th Edition. If you bring some programming knowledge (even on a beginner level), you will have no problems understanding the basics (more or less the first half of the book) and it will help so much during map development.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
That's a lot of useful information. I'm not sure if it fits as a code submission, as it's more of a tutorial. If you can combine it into one piece of code that's easy to copy, I think it will be fine to have it here. Otherwise I'd suggest to upload it to spells or as a tutorial.

From the submission rules:
Where to Submit
Spells or systems which require a demo map or would make the most sense to have a demo map should be submitted to the Spells section. If the code is simple or straightforward enough to be more or less copy & paste, you should submit it here.
[...]


1.1
I fully agree. I've been using the same lua extension.

1.2
As far as I know there is no way for lua to distinguish between an undefined variable (e.g. when misspelling) and a variable that was intentionally nulled.
This is also used inside some of blizzard's functions, so running the default new map already shows a few of these errors:
Output
Trying to read undeclared global: bj_dayAmbientSound
Trying to read undeclared global: bj_nightAmbientSound
Trying to read undeclared global: bj_meleeNearestMine
Maybe excluding all of the instances caused by common.j would be smart. I would definitely make it clear to the reader, that nulled variables will trigger this warning if accessed. Even if you just check if they are nil.

2.1-2.2 and 2.5
Not much to say here. Useful functions for debugging.

2.3-2.4
This is big and really shows the power of lua compared to JASS.
I few things I noticed while skimming through the code:
Lua:
...
    self:out('info', 0, false, "'textlang eng' will use english text installation font size to compute linebreaks (default).")
    self:out('info', 0, false, "'textlang ger' will use german text installation font size to compute linebreaks (default).")
...
Both text languages are described as default.

Lua:
for i = 0,23 do
Even if it likely won't ever change, it's still good practice to use the available function/constants instead of hard coding the number of players.



I've spent some time to set up a good debugging environment for myself and this tutorial/resource was very helpful for that.
Here's some of the things I did on top of what's described.

Wrapping code entry points with a try function:
Lua:
local oldTriggerAddAction = TriggerAddAction
    TriggerAddAction = function(whichTrigger, actionFunc)
        oldTriggerAddAction(whichTrigger, Try(actionFunc))
    end
If you do this for every code entry point, you basically get automatic error messages without having to change your code in any way. As a result you can also easily disable it if you want to release your map.

Cache print messages at map initialization. Print messages at map initialization are not added to the ingame log. You can override all print functions to instead store the message and then print them once the map really started (0 second timer). After that you can revert the changes to the print functions.

You can also use Preloading to effectively get a file based error log. So you can change the try function to instead (or additionally) write the errors to a file. The ingame log has a limited length and you can't copy from it. It might also be more convenient to have the log file and the map script open next to each other to easily see the lines causing errors.

The last thing is a bit special, because it's done automatically by my build script, but it is possible to translate the global map script line number to a local line number. If you have a script file like this:
Lua:
do
     ...
end

You can get the global line number simply by causing an error:
Lua:
do
     xpcall(function() local tmp = "" .. math.sqrt end, function(e) errorString = tostring(e) end)
     ...
end
Then you can extract the global line number from errorString and use it calculate the local line number in your script file.
 
Level 20
Joined
Jul 10, 2009
Messages
477
28.01.2022, v1.2:
  • Added a variant of section 1.2 (warnings for undeclared globals) that excludes bj-variables. Also added some explanation about variables that were intentionally nilled.
  • Added a code snippet to replace TriggerAddAction by a version using try to monitor code entry points. Thanks @Jampion for the suggestion.
  • Replaced hardcoded player counts (23) by GetBJMaxPlayers()
  • Added an installation section to the main post, also including a copy-paste option for all parts of this resource in one.
  • German fontsize is no longer marked as Default.


Thank you for reviewing this resource, @Jampion!

The additional measures you've described sound great. If they proof helpful to you, feel free to share implementation details, so I might add it to the main post (or reference your post from there). I've already done so with your TriggerAction-using-try idea. :)

Wrapping code entry points with a try function:
Lua:
local oldTriggerAddAction = TriggerAddAction
TriggerAddAction = function(whichTrigger, actionFunc)
    oldTriggerAddAction(whichTrigger, Try(actionFunc))
end
Don't forget the anonymous function wrap :) Directly passing Try(actionFunc) to oldTriggerAddAction would immediately execute actionFunc.
Lua:
--This way:
local oldTriggerAddAction = TriggerAddAction
TriggerAddAction = function(whichTrigger, actionFunc)
    oldTriggerAddAction(whichTrigger, function() Try(actionFunc) end)
end

Regarding the logging to file idea, one could think of using the FileIO library by @TriggerHappy that @Luashine has just recently converted to Lua. Although that sounds a bit overkill and I don't like dependencies. I'm not really familar with FileIO anyway, but feel free to push me into the right direction.
 
Level 19
Joined
Jan 3, 2022
Messages
320
The basic idea behind using Preload files is the following:
Lua:
out = {} -- text lines
PreloadGenClear() -- clear data
PreloadGenStart() -- start
-- Preload has a string limit of 259 chars... Windows' MAX_PATH?
for i = 1, #out do
    -- write a line
    Preload(out[i])
    -- call Preload( "<passed string>" )
end
PreloadGenEnd("filename.txt")
First and foremost: these are not empty files, in other words you do not have full control of the contents and all you can do is insert more lines beginning with "call Preload". Secondly: the 259 char limit. Because Preload is supposed to only instruct the game to load a file ahead of time (my theory), MAX_PATH limitation makes sense. Lastly: apparently everything is held in memory until the file is written out and flushed. Awful for actual logs of unknown size.
Now since there's apparently no other way to write out contents to file system, you've gotta work around these limitations to create a real Logging library. To not have "call Preload" rubbish in the output, you must have a script for post-processing of these files? A little cumbersome.

If you're doing Lua, not Jass, you're free to do whatever inside Preload files with //! beginusercode directive.
 
Level 19
Joined
Jan 3, 2022
Messages
320
Bug report: I try to use this as the regular Lua console, but this code: print(1);print(2) throws "attempt to call a nil value" error.
Although Lua's interpreter in console works differently from regular Lua:
In interactive mode, Lua repeatedly prompts and waits for a line. After reading a line, Lua first try to interpret the line as an expression. If it succeeds, it prints its value. Otherwise, it interprets the line as a statement. If you write an incomplete statement, the interpreter waits for its completion by issuing a different prompt.
I don't know if this can be fixed. If someone needs this, here's an obvious workaround:
Lua:
local f=function() print(1);print(2) end; f()
-- EDIT: This is not obvious:
(function() print(1);print(2) end)()
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
477
Bug report: I try to use this as the regular Lua console, but this code: print(1);print(2) throws "attempt to call a nil value" error.
Thanks for the report, much appreciated!
I found that the erroneous line of code causing the bug you've described was also causing the console to sometimes not print return statements of certain inputs containing raw strings, such as load('a=5').
These issues should be fixed now, i.e. raw strings and function calls next to semicolons should work fine now.
The newest version is attached to the main post.

Lua 5.4 manual said:
If you write an incomplete statement, the interpreter waits for its completion by issuing a different prompt.
Admitted, Ingame Console unfortunately doesn't behave like that. It will not automatically hold execution and wait, until you have produced a valid Lua statement. The only way currently to span inputs across multiple lines is to manually preceed them by the '>'-sign.
However, I will definitely take this feature into consideration for potential future releases of the resource.
For now, I have instead included a check for whether the input statement you have entered is valid or not (and if not, throws "Input is not a valid Lua-statement"). This check also prevents the previous "Attempt to call a nil value" from popping up, which was more confusing than helpful in these cases.
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
477
24.02.2022, v1.2a:
  • Ingame Console now manually recognizes erroneous Lua statements and will throw an "Invalid Lua-statement"-error instead of the non-sensible "Attempt to call a nil value".
  • Fixed an IngameConsole error, where inputting function calls separated by semicolons was not properly executed.
  • Fixed an IngameConsole error, where inputting statements including raw strings (such as load('a=5')) was sometimes not properly executed or would not print their return values back to the console.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I had literally just started with VS Code yesterday, and it instantly changed the game for me. I wish I had seen this resource sooner — I have no idea why this isn’t already stickied and marked as “essential”. Imo you could cross-post this to the Spells section and it would get even more views/a Director’s Cut nomination. It’s a big enough resource that makes enough of a splash to be there.

Edit: Holy shit, your custom Lua Blizzard and Common files look CLEAN AF. I am attaching mine for reference, so you can see how ugly I did it.
 

Attachments

  • common.lua
    260.8 KB · Views: 21
  • Blizzard.lua
    383.9 KB · Views: 22
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Reasons for approval:
  1. This brings critical ability to users by enabling them with far more powerful error messages than what are provided within WarCraft 3.
  2. This is universally-applicable and can be useful for any map which needs its code debugged.
  3. This is an excellent teaching opportunity for users, in conjunction with your tutorial on how to set up a proper scripting environment in VS Code.
  4. This can save people a lot of development time and headache.
I would like to highlight this as a staple for the kind of quality one can expect from HiveWorkshop Lua resources. Thank you for the time and effort that you have put into this!
 
Level 20
Joined
Jul 10, 2009
Messages
477
08.08.2022, v1.3:
  • Added Chapter 2.6 about stack traces and Chapter 2.7 about logging additional information.
  • Included GetStackTrace to be able to print traces of erroneous function calls.
    Credits to @HerlySQR for the original resource and permission to use it in Debug Utils.
  • Included LogStackTrace for logging the function trace provided by GetStackTrace plus further information that the try-function could decide to print upon encountering errors.
This is quite a noticable update from my point of view. I'd be happy to receive feedback, if these changes were helpful to you by any means.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I'd been struggling for hours trying to find the source of a problem in a map that I've been testing, where I was getting "attempt to index a nil value" without a line number as part of the error message. I needed the line number, because otherwise I can't know the source of the issue.

I tried to weave in GetStackTrace, but I learned during this time that it does not get the error stack, but the external stack. So, I dove deeper and modified "try" to use xpcall instead of pcall, and internally call GetStackTrace. Unfortunately, this also did not get me any extra information. However, by slashing apart @HerlySQR's original version, I was able to produce this, which finally gave me the missing line number that I needed:

Lua:
---Engulf a function in a try-block to catch and print errors.
---Example use: Assume you have a code line like "CreateUnit(0)", which doesn't work and you want to know why.
---* Option 1: Change it to "try(CreateUnit, 0)", i.e. separating the function from the parameters.
---* Option 2: Change it to "try(function() return CreateUnit(0) end)", i.e. pack it into an anonymous function. You can leave out the "return", if you don't need to forward the return value to the try function.
---* Option 3: Change it to "try('return CreateUnit(0)')", i.e. engulf the function call by string-marks and pass it to the try function as a string. Again, you can skip the 'return' keyword in case you don't need the return values. Pay attention that input variables are taken from global scope, if you do it this way.
---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.
---@param input function | string
function try(input, ...)
    return select(2, xpcall((type(input) == 'function' and input) or load(input),
        function(msg)
            xpcall(error, print, "|cffff5555"..msg.."|r", 4)
        end,...))
end

@Eikonium , I might recommend updating your "try" function to include something like this for future users.
 
Last edited:
Level 19
Joined
Jan 3, 2022
Messages
320
First off, never mind, I thought the problem is with accessing _G[nil]. His solution is about another problem: catching errors properly. The initial problem was due to the error being caught but providing no extra info. No line number and no key name.

Accessing nil in random tables (not his issue, but what I thought)
Lua:
-- erroring code
hello="HELLO!"
print(hello, world)

1. You can create all of your tables with the catch-all __index metatable, like Eikonium did on _G (slightly improved version):
Lua:
local dump_table_contents = falsesetmetatable(_G,{
    __index=function(tbl, key)
        local keyName = tostring(key)
        if string.sub(keyName,1,3) ~= 'bj_' then
            print(string.format(
                "Trying to read undeclared global in table: '\037s', key: '\037s'",
                tostring(tbl), keyName
            ))
            if dump_table_contents and "optionally print table contents" then
                local visited = {} -- prevent infinite cycles
                for k, v in pairs(tbl) do
                    -- pairs is safe because we only do local print
                    if not visited[k] then
                        print(tostring(k), tostring(v))
                        visited[k] = true
                    end
                end
            end
        end
    end,
})

hello="HELLO!"

print(hello, world)
That's what I thought wasn't happening on _G. The above code only adds the tostring(table) to tell you which table is being tried, in case it isn't _G. It has an option to print all table contents too, so you can guess which table it was.

To make it work for other tables other than GLOBAL, you need to apply this __index function to ALL tables you create (as a metatable).

2. Actually tracking the errors like Bribe suggested.
What happens in his case is an error that's not caught & printed properly? I thought Eikonium's code did. Anyway...
You can test his try(illegal) or my safeCall(illegal) with this:

Lua:
--[[ safely call functions from other code and catch any errors, outputting them to all chat
Note: By default Reforged doesn't output any errors, they can only be found in memory dumps ]]
function safeCall(...) --[[ for GC: either current vararg approach or table alloc ]]
    return safeCallHandle(pcall(...))
end
--[[ This is needed due to how varargs work in Lua ]]
function safeCallHandle(...)
    local ok, err = ...
    if ok == true then
        return select(2, ...)
    else
        print("|cffee2222Error occurred: ")
        for line in err:gmatch("[^\n]+") do
            --[[ I think errors don't have stack traces here, so only single line :( ]]
            print("|cffee2222".. line)
        end
        --[[ abort execution and point to above function ]]
        error(err, 3)
    end
end

Lua:
-- erroring code
hello="HELLO!"

function illegal()
print(hello, world[lol])
end

--try(illegal)
--safeCall(illegal)
Short interjection: my safeCall function is better because it creates no tables during calls. The arguments and returns are handled on the stack (so I assume, anyhow it solved my GC issues when I hooked every function). You should adopt the same approach going forward.

Before @HerlySQR 's stack tracer, I didn't know you could unwind the stack like that. I never needed to because regular Lua prints the entire stack after calling error() and shows multiple lines.
Lua:
|cffee2222Error occurred:
|cffee2222input:26: attempt to index a nil value (global 'world')
input:33: input:26: attempt to index a nil value (global 'world')
You can see the last line contains input:33 and input:26 that's basically your stack trace in a single line. WC3 only shows you the last value (input:26) which isn't 100% helpful.
I can add it to WC3 though, recreating the error stack trace using his trick: (damn that's a lot of code suddenly)
Lua:
--[[ safely call functions from other code and catch any errors, outputting them to all chat
Note: By default Reforged doesn't output any errors, they can only be found in memory dumps ]]
function safeCall(...) --[[ for GC: either current vararg approach or table alloc ]]
    return safeCallHandle(pcall(...))
end
--[[ This is needed due to how varargs work in Lua ]]
function safeCallHandle(...)

    -- here's the caught error message. Only the offending line
    local ok, err = ...
    if ok == true then
        -- All good, return values
        return select(2, ...)
    else
        -- CREATE STACK TRACE AND CALL error()
        local safeCallLevel = 3 -- point to the initial safeCall(userFunc)
      
        local stacktrace = { [0] = err } -- goes backwards from 0 to negative
        local stIndex = 0
        local emptyMsgCount = 0
        local emptyMsgLimit = 2
        --[[ collect information about the current stack ]]
      
        local level = safeCallLevel + 1
        while emptyMsgCount <= emptyMsgLimit do -- allow up to N empty error msg lines
            local _, errMsg = pcall(error, "", level)
            if #errMsg == 0 then
                emptyMsgCount = emptyMsgCount + 1
            else
                emptyMsgCount = 0
            end
            stIndex = safeCallLevel - level
            stacktrace[stIndex] = errMsg
            level = level + 1
        end
      
        -- skip initial empty messages
        stIndex = stIndex + emptyMsgLimit + 1
      
        -- PRINT THE STACK TRACE
      
        -- either safeCallLevel here or 0 to suppress Lua's own msg
        --error(table.concat(stacktrace, "\n>> ", stIndex, 0), safeCallLevel)
      
        -- OR pretty-print:
        local manualErrorMsg = {"Stacktrace for error at:"}
        for i = stIndex, 0 do
            local msg = stacktrace[i]
            if #msg == 0 then
                msg = "<unknown>"
            end
          
            -- repeat count starts at 1 due to initial msg in table
            msg = (">"):rep(#manualErrorMsg) .. " " .. msg
          
            table.insert(manualErrorMsg, msg)
        end
        error(table.concat(manualErrorMsg, "\n"), 0)
    end
end

hello="HELLO!"
function illegal()
    print(hello, world[lol])
end
function main()
    safeCall(illegal)
end
main()
Cool thing about this, you can actually see the internals of Lua Demo: there's one empty line in between (output)
Code:
safeCall(illegal)
---
Stacktrace for error at:
> demo.lua:60:
>> <unknown>
>>> input:69:
>>>> input:64: attempt to index a nil value (global 'world')
Although it looks intimidating (even to myself), this code has great advantages:
  1. safeCall is already effective, no anonymous functions or tables
  2. Pretty print all of the stack: tables are only created on errors (unavoidable here)
  3. The unwinding of the stack isn't recursive, so you shouldn't run into stack overflow problems at all (almost impossible on x64 anyway)

TLDR:​

Although Eikonium added GetStackTrace(), he didn't add this functionality to try(). Instead of improving his try(), I improved my function that serves the same purpose.
Q: Should I post it as a separate resource?
 

Attachments

  • safeCall-v2.lua
    1.9 KB · Views: 7
Level 20
Joined
Jul 10, 2009
Messages
477
Good discussion here!

Although Eikonium added GetStackTrace(), he didn't add this functionality to try().
At least not directly, because I thought it's impossible.
GetStackTrace() as presented by @HerlySQR outputs the stack trace at the point the function is called, i.e.
executing it within try gets you the stack trace leading to the execution of try, not the stack trace to the actual error.

Lua:
--This doesn't output the stack trace we want
function try(...)
    ...
    print(GetStackTrace())
end

Your safeCall seems to suffer from exactly the same issue (wrong stack trace).

This is why my main post suggested first looking at the line number of the first error message and manually adding GetStackTrace() at that point of your code.
I even added LogStackTrace as an alternative to make sure that the stack trace would only be printed, if an error was thrown afterwards.

Anyway, @Bribe found the solution. Using xpcall and retreiving the stack trace within the message handler seems to get it at the point where the error is thrown. This is huge. May seem obvious in retrospect, but I honestly wasn't aware of it.

P.S.:
Your safeCall also executes error(table.concat(manualErrorMsg, "\n"), 0) directly, but error can only be called within safe calls (pcall/xpcall), so your code breaks at that point. Maybe you have tested your function in a map, where everything was safe-called? Like try on all trigger actions or execution within Ingame Console.

P.S. 2:
I will try to incorporate several of your suggestions, though. I like the pretty print and the idea to jump over blank entries to some degree (although the latter needs discussion).

Short interjection: my safeCall function is better because it creates no tables during calls. The arguments and returns are handled on the stack (so I assume, anyhow it solved my GC issues when I hooked every function). You should adopt the same approach going forward.
try is supposed to run upon testing your map, not after release.
Creating one table per use in test-mode shouldn't matter at all.
Still, the way you (and Bribe) presented it has no downsides, so I will adopt it. :)

Q: Should I post it as a separate resource?
I'd prefer to improve Debug Utils based on your and Bribe's feedback, after we've finished our discussion.
You can ofc post a separate resource, if you are unhappy with the result.

I'd been struggling for hours trying to find the source of a problem in a map that I've been testing, where I was getting "attempt to index a nil value" without a line number as part of the error message.
Maybe an error thrown within an anonymous function call? At least it makes sense not to have a line number here.

I tried to weave in GetStackTrace, but I learned during this time that it does not get the error stack, but the external stack. So, I dove deeper and modified "try" to use xpcall instead of pcall, and internally call GetStackTrace. Unfortunately, this also did not get me any extra information.
It gets you the stack at the point where you call it. For this reason, my main post suggests to look at line number of the first error message, insert print(GetStackTrace()) at that point of your code and reproduce error. I'm totally aware that this procedure wasn't possible for you, because you didn't have a line number to start with.

GetStackTrace stops at the first blank trace (or rather at the first line not referring to war3map.lua/Blizzard.j.lua).
Including it into xpcall changes the depth, where the stack is produced and thus requires adjusting the start level of the trace. If you haven't done so, the first trace might have been referred to your anonymous function again, which was blank and terminating the process.

Anyway, fortunately your investigation was very fruitful. Calling the stack trace within the message handler is exactly what I needed. Very good finding!



Here is my updated try. It prints a stack trace for every error (thanks to @Bribe 's finding on calling GetStackTrace within the message handler of xpcall) and applies @Luashine 's pretty print.
I also dared to deprecate the support for string inputs (like try("CreateUnit(...)")), because it takes all params from global scope, which can lead to more rather than less confusion.

Lua:
do
    ---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 messageHandler(errorMsg)
        local startLevel = 3 --Starting depth for stack trace. Starting at 3 ensures that "error" and "pcall" are not included.
        local _, traces, loopLevel = nil, {[-startLevel] = errorMsg}, startLevel --save actual error message to key -3
        --Get Stack Trace
        repeat
            loopLevel = loopLevel+1
            _, traces[-loopLevel] = pcall(error, "", loopLevel) --save further traces into keys -4, -5, ... to enable table.concat loop in reverse order below.
        until #traces[-loopLevel] == 0 or loopLevel == startLevel+11 --stop after 12 lines
        traces[-loopLevel] = "ERROR. Traceback (most recent call last):" --add title
        --Pretty Print: Add preceeding ">", remove colon and whitespace at end of lines
        for i = -loopLevel+1, -startLevel do
            traces[i] = (">"):rep(i + loopLevel) .. " " .. traces[i]:gsub(":\x25s*\r?$","")
        end
        print("|cffff5555" .. table.concat(traces, "\n", -loopLevel, -startLevel) .. "|r")
    end

    ---@param input function the function to call in protected mode
    ---@param ... any params for the input-function
    function try(input, ...)
        return select(2, xpcall(input, messageHandler,...))
    end
end

Questions

Questions for everyone!

  1. The current stack trace is pretty, but can get huge:
    Code:
    Traceback (most recent call last):
     > war3map.lua:131
     >> war3map.lua:127
     >>> war3map.lua:110
     >>>> war3map.lua:100: Attempt to call a nil value (global 'x')
    Imagine a trace with 20 entries or more...
    Alternative:
    Code:
    war3map.lua:100: Attempt to call a nil value (global 'x')
     Traceback (most recent call last): war3map.lua:131; war3map.lua:127; war3map.lua:110; war3map.lua:100
    The second presentation focusses more on the main error message (which I prefer).
    Which one would you prefer?

  2. Should we limit the trace to the last x calls? A very long trace will not be as helpful, when it spams your whole screen. The current try above does so and limits the trace to 12 entries. Limit can be higher, if we choose the second display option above (the not-so-pretty one).

  3. Should we jump over blank entries within the trace to see, if there are valid ones beyond that? @Luashine does so in her safeCall, when I'm not mistaken. I want to display helpful entries only. Maybe only include those pointing to war3map.lua and Blizzard.j.lua?
    Jumping over blank entries might be absolutely necessary to get beyond anonymous functions involved in the trace. Absolutely not sure about this, though - anonymous function might as well be disregarded by the trace alltogether.

  4. @Bribe could you maybe test the try presented above on the issue you've had? I'd like to know, if it gets a valid trace now, where the starting depth is right for being used within xpcall.
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I would prefer if the stack traceback printed the most recent call first (that's how I'm used to seeing it in other error handlers):

Lua:
do
    ---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 messageHandler(errorMsg)
        local startLevel = 3 --Starting depth for stack trace. Starting at 3 ensures that "error" and "pcall" are not included.
        local _, traces, loopLevel = nil, {[-startLevel] = errorMsg}, startLevel --save actual error message to key -3
        --Get Stack Trace
        repeat
            loopLevel = loopLevel+1
            _, traces[-loopLevel] = pcall(error, "", loopLevel) --save further traces into keys -4, -5, ... to enable table.concat loop in reverse order below.
        until #traces[-loopLevel] == 0 or loopLevel == startLevel+11 --stop after 12 lines
        --Pretty Print: Add preceeding ">", remove colon and whitespace at end of lines
        local revTable = {"lua stack traceback:"} --add title
        local pos = 0
        for i = -startLevel,-loopLevel+1,-1 do
            print(i, startLevel, loopLevel)
            pos = pos + 1
            revTable[pos + 1] = (">"):rep(pos) .. " " .. traces[i]:gsub(":\x25s*\r?$","")
        end
        print("|cffff5555" .. table.concat(revTable, "\n") .. "|r")
    end

    ---@param input function the function to call in protected mode
    ---@param ... any params for the input-function
    function try(input, ...)
        return select(2, xpcall(input, messageHandler,...))
    end
end

Otherwise, it seems to catch the "attempt to index a nil value (local 'i')" appropriately.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I've been working on modifications to the error function, and I've got (what I believe to be) the best solution:

Lua:
do
    local _,skipThisLine

    local function catchError(errorMsg)
        local level, traceback, msg = 3, {}
        repeat
            _,msg = pcall(error, "", level) --traceback skips function calls used by the "try" function.
            if msg ~= skipThisLine and msg ~= "" then
                traceback[#traceback+1] = msg
            end
            level = level + 1
        until level == 16
        --"print" does not work in the Lua root. If Global Initialization is in the map, delay the print message until the game has loaded.
        (rawget(_G, "OnGameStart") or pcall)(function()
            print("|cffff5555ERROR: " .. errorMsg .. "\n> Traceback: " .. table.concat(traceback):gsub("war3map.lua:(\x25d+):", "\x251;"):sub(1,-3) .. "|r")
        end)
    end

    ---@param userFunc function the function to call in protected mode
    ---@param ... any params for the user's function
    function try(userFunc, ...)
        if not skipThisLine then _,skipThisLine = pcall(error, "", 2) end return select(2, xpcall(userFunc, catchError, ...))
    end
end
--[[Example output for a caught error:

    ERROR: war3map.lua:31: attempt to index a nil value (local 'i')
    > Traceback: war3map.lua:31: war3map.lua:34:
--]]

The above solution addresses the following:
  1. Focuses more on the main error message.
  2. Jump over blank entries within the trace to see if there are valid ones beyond that.
  3. It gets a valid trace now, where the starting depth is right for being used within xpcall.
  4. The most recent call is shown first.
  5. Hides unnecessary traceback to try's own caller.
  6. Will postpone displaying the error message if OnGameStart is found, to ensure the "print" call is not invisible.
For a clean view, we can also remove the redundant war3map.lua lines. Since we don't get any useful data outside of line numbers, or in the rare occasion that we get a function call involving the blizzard.j stack or such,

1665949079914.png
 
Last edited:
Level 19
Joined
Jan 3, 2022
Messages
320
Idea: only replace war3map.lua when there're no other files/sources in the compressed stack message. Example:
ERROR: war3map.lua:234: big problem
Traceback: L473 then L184 then L284
Here the order is root--> (descending) ->error line. Else "then" should be called "via" like "errorline via function line via root". The current way isn't incomprehensible, but a little human touch won't hurt :)

If the error happened in blizzard.lua, then there'll be two files in the trace log and I suggest the line shouldn't get compressed this much (leave both file names in tact, it won't hurt).
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Idea: only replace war3map.lua when there're no other files/sources in the compressed stack message. Example:
ERROR: war3map.lua:234: big problem
Traceback: L473 then L184 then L284
Here the order is root--> (descending) ->error line. Else "then" should be called "via" like "errorline via function line via root". The current way isn't incomprehensible, but a little human touch won't hurt :)

If the error happened in blizzard.lua, then there'll be two files in the trace log and I suggest the line shouldn't get compressed this much (leave both file names in tact, it won't hurt).
Could you please share with me an example of where a stack error could include an Blizzard or common file? I'd like to be able to reproduce an error like that.
 
Last edited:
Level 19
Joined
Jan 3, 2022
Messages
320
@Bribe very easy :D
Lua:
--[[ safely call functions from other code and catch any errors, outputting them to all chat
Note: By default Reforged doesn't output any errors, they can only be found in memory dumps ]]
function safeCall2(...) --[[ for GC: either current vararg approach or table alloc ]]
    return safeCall2Handle(pcall(...))
end
--[[ This is needed due to how varargs work in Lua ]]
function safeCall2Handle(...)

    -- here's the caught error message. Only the offending line
    local ok, err = ...
    if ok == true then
        -- All good, return values
        return select(2, ...)
    else
        -- CREATE STACK TRACE AND CALL error()
        local safeCallLevel = 2 -- point to the initial safeCall(userFunc)
      
        local stacktrace = { [0] = err } -- goes backwards from 0 to negative
        local stIndex = 0
        local emptyMsgCount = 0
        local emptyMsgLimit = 2
        --[[ collect information about the current stack ]]
      
        local level = safeCallLevel + 1
        while emptyMsgCount <= emptyMsgLimit do -- allow up to N empty error msg lines
            local _, errMsg = pcall(error, "", level)
            if #errMsg == 0 then
                emptyMsgCount = emptyMsgCount + 1
            else
                emptyMsgCount = 0
            end
            stIndex = safeCallLevel - level
            stacktrace[stIndex] = errMsg
            level = level + 1
        end
      
        -- skip initial empty messages
        stIndex = stIndex + emptyMsgLimit + 1
      
        -- PRINT THE STACK TRACE
      
        -- either safeCallLevel here or 0 to suppress Lua's own msg
        --error(table.concat(stacktrace, "\n>> ", stIndex, 0), safeCallLevel)
      
        -- OR pretty-print:
        local manualErrorMsg = {"Stacktrace for error at:"}
        for i = stIndex, 0 do
            local msg = stacktrace[i]
            if #msg == 0 then
                msg = "<unknown>"
            end
          
            -- repeat count starts at 1 due to initial msg in table
            msg = (">"):rep(#manualErrorMsg) .. " " .. msg
          
            table.insert(manualErrorMsg, msg)
        end
      
        for i = 1, #manualErrorMsg do
            print("|cffff5555" .. manualErrorMsg[i]) -- in-game output
        end
        error(table.concat(manualErrorMsg, "\n"), 0)
    end
end
Lua:
do

    local function reallyHiddenFailingFunction()
        -- this will throw an error in 1.32.10 Lua
        -- however if you call ("str", nil) then it'll fail silently
        QuestSetEnabledBJ("ignored", "causesErr")
    end

    function failingFunction()
        reallyFailingFunction()
    end

    function reallyFailingFunction()
        reallyHiddenFailingFunction()
    end

end
Lua:
function Trig_safeCallv2Error_Actions()
    DisplayTextToForce(GetPlayersAll(), "TRIGSTR_007")
        safeCall2(failingFunction)
end

war3map-line.png


blizj-line.png
safecallv2-fix1-error.png


It took me an in-depth look to understand why it doesn't show my matröshka of failingFunction calls. That is because the pcall(error, "", level) stack tracing part happens ABOVE the failing functions, not at the very point where the error happened. I don't know if xpcall is better in this regard, at which point is the message handler called?

PS: Map is v1.32.10
 

Attachments

  • lua-error-in-blizzardj-v1.w3m
    18.8 KB · Views: 4
Level 20
Joined
Jul 10, 2009
Messages
477
It took me an in-depth look to understand why it doesn't show my matröshka of failingFunction calls. That is because the pcall(error, "", level) stack tracing part happens ABOVE the failing functions, not at the very point where the error happened. I don't know if xpcall is better in this regard, at which point is the message handler called?
Yes, producing the stack trace within the message handler of xpcall does it at the point where the error was thrown. This is exactly the game-changing finding @Bribe had. I also didn't know, which is the reason why I didn't incorporate the stack trace into try before.

I tried to tell you in this post :p (Spoiler for Luashine on top)
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Alright, this latest version should provide the best of all worlds:

Lua:
do
    local _,skipThisLine,caller
    local map = {
        w = "war3map",
        b = "blizzard.j",
        c = "common.j",
        --j = "jdoodle"
    }

    local function catchError(errorMsg)
        local level, n, traceback, msg = 3, 0, {}, nil
        repeat
            _,msg = pcall(error, "", level) --traceback skips function calls used by the "try" function.
            if msg ~= skipThisLine and msg ~= "" then
                n = n + 1
                traceback[n] = msg
            end
            level = level + 1
        until level == 16
        local i, results, prev = 0, {}, nil
        repeat
            msg = traceback[n - i]
            local current = msg:sub(1,1)
            i = i + 1
            if current ~= prev then
                prev = current
                results[i] = "\n-> " .. map[current] .. ".lua: " .. msg
            else
                results[i] = "-> " .. msg
            end
        until i == n
        caller(function() print("|cffff5555ERROR: " .. errorMsg .. "\nStack Traceback: " .. table.concat(results):gsub("[^\x25s]+.lua:(\x25d+):", "\x251") .. "<- error" .. "|r") end )
    end

    ---@param userFunc function the function to call in protected mode
    ---@param ... any params for the user's function
    function try(userFunc, ...)
        caller = rawget(_G, "OnGameStart") or pcall --"print" does not work in the Lua root. If Global Initialization is in the map, delay the print message until the game has loaded.
        if not skipThisLine then _,skipThisLine = pcall(error, "", 2) end return select(2, xpcall(userFunc, catchError, ...))
    end
end
--[[Example output for a caught error:

ERROR: war3map.lua:55: attempt to index a nil value (local 'i')
Stack Traceback:
-> war3map.lua: 50 -> 52 -> 53 -> 55 <- error
--]]
1666024672329.png

1666023827457.png


This variation adds a ThrowError(msg) function, so the user can define their own error message handler inclusive of this formatted stack trace. Additionally, adds _DEBUG as "true" to the _G table, which if set to false will disable the error message printing offered by the system (it transforms 'try' into 'pcall' and 'ThrowError' into 'DoNothing'). _DEBUG is named (but not declared) by Blizzard's JASS2Lua transpiler, so this is keeping with the naming convention. The vJass equivalent is DEBUG_MODE.

The perk of "ThrowError" over "assert" or "error" is that it works outside of a pcall. Otherwise, if you know you're inside of a pcall, you should definitely use one of those two as they will crash the thread.

Lua:
_DEBUG = true

do
    local _,skipThisLine1,skipThisLine2,caller
    local map = {
        w = "war3map",
        b = "blizzard.j",
        --c = "common.j",
        --j = "jdoodle"
    }

    local function checkCaller()
        caller = rawget(_G, "OnGameStart") or pcall --"print" does not work in the Lua root. If Global Initialization is in the map, delay the print message until the game has loaded.
    end
        

    local function traceError(errorMsg)
        local level, n, traceback, msg = 3, 0, {}, nil
        repeat
            _,msg = pcall(error, "", level) --traceback skips function calls used by the "try" function.
            if msg ~= "" and msg ~= skipThisLine1 and msg ~= skipThisLine2 then
                n = n + 1
                traceback[n] = msg
            end
            level = level + 1
        until level == 16
        local i, results, prev = 0, {}, nil
        repeat
            msg = traceback[n - i]
            local current = msg:sub(1,1)
            i = i + 1
            if current ~= prev then
                prev = current
                results[i] = "\n-> " .. map[current] .. ".lua: " .. msg
            else
                results[i] = "-> " .. msg
            end
        until i == n
        caller(function() print("|cffff5555ERROR: " .. errorMsg .. "\nStack Traceback: " .. table.concat(results):gsub("[^\x25s]+.lua:(\x25d+):", "\x251") .. "<- error" .. "|r") end )
    end

    ---@param userFunc function the function to call in protected mode
    ---@param ... any params for the user's function
    function try(userFunc, ...)
        checkCaller()
        if not skipThisLine1 then _,skipThisLine1 = pcall(error, "", 2) end return select(2, xpcall(userFunc, traceError, ...))
    end
    
    function ThrowError(msg)
        checkCaller()
        local _, errorAt = pcall(error, "", 3)
        if not skipThisLine2 then _,skipThisLine2 = pcall(error, "", 2) end traceError(errorAt .. msg)
    end

    if not _DEBUG then
        try        = pcall
        ThrowError = DoNothing
    end

end
--[[Example output for a caught error:

ERROR: war3map.lua:55: attempt to index a nil value (local 'i')
Stack Traceback:
-> war3map.lua: 50 -> 52 -> 53 -> 55 <- error
--]]

--[[Example output for a user-created error:

ERROR: war3map.lua:70: This is a custom error message
Stack Traceback: 
-> war3map.lua: 72 -> 70 <- error
--]]

The current table.tostring function is problematic in case of circular tables, especially with depth < 0.

This fixes it, and gives the table's contents - and any nested tables - breathing room via indentation and newlines. Also, tables are named table1,2,3...etc, which is useful for detecting multiple references to the same table.

Lua:
do
    local innerLoop
    innerLoop = function(object, constTable, depth)
        local objType = type(object)
        if objType == "string" then
            return '"'..object..'"' --wrap the string in quotes.
        elseif objType == 'table' then
            if not constTable[object] then
                constTable.count = constTable.count + 1
                constTable[object] = "table"..constTable.count
                if next(object)==nil then
                    return constTable[object]..": {}"
                elseif not constTable.maxDepth or depth <= constTable.maxDepth then
                    local mappedKV = {}
                    local function deepParse(val)
                        return innerLoop(val, constTable, depth + 1)
                    end
                    local indent = ("  "):rep(depth - 1)
                    for k,v in pairs(object) do
                        table.insert(mappedKV, '\n  '..indent ..'[' .. deepParse(k) .. '] = ' .. deepParse(v))
                    end
                    return constTable[object]..': {'.. table.concat(mappedKV, ',') .. '\n'..indent..'}'
                end
            end
        end
        return constTable[object] or tostring(object)
    end
    function table.tostring(whichTable, maxDepth)
        return innerLoop(whichTable, {maxDepth=maxDepth, count=0}, 1)
    end
    function table.print(whichTable, maxDepth)
        print(table.tostring(whichTable, maxDepth))
    end
end

Test code:

Lua:
local selfPointingTable = {}
selfPointingTable[selfPointingTable]=selfPointingTable
table.print{{{someKey='someVal', otherKey="otherVal"}},{{{}}},{selfPointingTable}}

Output:

Lua:
table1: {
  [1] = table2: {
    [1] = table3: {
      ["someKey"] = "someVal",
      ["otherKey"] = "otherVal"
    }
  },
  [2] = table4: {
    [1] = table5: {
      [1] = table6: {}
    }
  },
  [3] = table7: {
    [1] = table8: {
      [table8] = table8
    }
  }
}
 
Last edited:
Level 20
Joined
Jul 10, 2009
Messages
477
The current table.tostring function is problematic in case of circular tables, especially with depth < 0.

That's intended and stated in the description. Not really problematic from my point of view.
Basically, if you try to print a table with recursive references, you need to pass a depth (as you should anyway to get what you search for). If you forget, Lua will throw a stack overflow error, so you notice immediately.


Regarding your pretty print, I really like it! The format is well-designed, engulfing strings by quotation marks is nice, adding enumeration to tables is useful and not unpacking tables on second and further encounters is a neat idea for smaller tables and self-referential ones.

It also has a big disadvantage though, because Warcraft has only limited ability to display a lot of linebreaks (same for Ingame Console). Your version spams the screen pretty quickly and is not able to fully display tables with more than 14 or so entries, which is very little. Yes, you can scroll through the F12-log after printing, but that's not really convenient. This was even the main reason why I chose a one-liner in my version to begin with.

That said, we should probably have both versions available for the user. Your version as an optically simplified approach for smaller tables, my version as the "safe" approach.
The syntax would be table.tostring(T:table, maxDepth?:integer, pretty_yn?:boolean), both maxDepth and pretty_yn being optional.
I normally hate overloading, but in this case I will also make table.tostring(T, pretty_yn) work.

Btw, your version can maybe be improved for tables used as keys. Try to use a table with depth 2 or more as a key inside another table and you see what I mean. I decided to not unpack tables used as keys in my version for that reason. Sure thing, unpacking keys has its own advantages. I don't have a fixed opinion on this.


The new table.tostring with optional pretty-print looks as follows:

Lua:
do
    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
        local result
        if type(object) == 'string' then
            result = '"' .. object .. '"'
        elseif depth ~= 0 and type(object) == 'table' then
            local elementArray = {}
            for k,v in pairs(object) do
                table.insert(elementArray, '(' .. tostring(k) .. ', ' .. conciseTostring(v, depth -1) .. ')')
            end
            result = '{' .. table.concat(elementArray, ', ') .. '}'
        end
        return result or tostring(object)
    end

    ---Translates a table into a prettified list of its (key,value)-pairs. Also translates subtables 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

    ---Translates a table into a list of its (key,value)-pairs. Also translates subtables 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, maxDepth), table.tostring(T, pretty_yn) or table.tostring(T, maxDepth, pretty_yn).
    ---table.tostring is not multiplayer-synced.
    ---@param whichTable table
    ---@param maxDepth? integer default: unlimited
    ---@param pretty_yn? boolean default: false (concise)
    ---@return string
    ---@overload fun(whichTable:table, pretty_yn?:boolean):string
    function table.tostring(whichTable, maxDepth, pretty_yn)
        --reassign input params, if function was called as table.tostring(whichTable, pretty_yn)
        if type(maxDepth) == 'boolean' then
            pretty_yn = maxDepth
            maxDepth = -1
        end
        return pretty_yn and prettyTostring(whichTable, maxDepth, {}, "") or conciseTostring(whichTable, maxDepth)
    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

I also made a few changes to your version:
  • Replaced the manual table enumeration by existing memory positions (you know, the hex code you get from tostring). This will make the table comparable to different data structures or variables.
  • Removed local deepParse. That seemed a bit overkill, especially because you define it locally for every subtable and don't really need it.
  • Passed indentation as a parameter. This way, you can extend it within the existing recursion instead of building it new.

I will update Debug Utils with this once I finished my other ToDo's (improving stack trace and improve Ingame Console's ability to deal with line breaks to properly display your pretty print).



P.S.: As you hold a moderator position, you could maybe lead with good example and use the Edit-button instead of multi-posting? :pwink: Not a big deal though, your contribution is still much appreciated!
Edit: I got advised by Deepstrasz that you have in fact not multiposted in case of posting a substantial update (which is up to you to decide). I appreciate that you have merged your posts, though :)
 
Last edited:

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
During some heavy troubleshooting on my resources, I've coded solutions to some of the problems I was facing.

Lua:
    --Allows tostring to provide the variable name in the _G table. Useful for getting the original native name when using hooked names.
    local varNames = {}
    for key,val in pairs(_G) do
        local typeOf = type(val)
        if typeOf~="number" and typeOf~="boolean" and typeOf~="string" then
            varNames[val] = key
        end
    end
    local oldTS = tostring
    tostring = function(obj)
        return varNames[obj] and (varNames[obj]..": "..oldTS(obj)) or oldTS(obj)
    end
    
    --Allows print to wait until the game has started. Useful to ensure printed messages show up in the F12 log (or show up at all, since "print" does nothing in the Lua root).
    local printQueue = {}
    local oldPrint = print
    local newPrint = function(...)
        if printQueue then
            table.insert(printQueue, table.pack(...))
        else
            print(...)
        end
    end
    print = newPrint
    local oldStart = MarkGameStarted
    MarkGameStarted = function()
        oldStart()
        for _,msg in ipairs(printQueue) do
            print(table.unpack(msg, 1, msg.n)) --now that the game has started, call the queued error messages.
        end
        if print == newPrint then
            print = oldPrint
        end
        printQueue = nil
    end

    --Fixes an issue with the "C Stack Overflow" error message to avoid spamming the same line number,
    --and ensures that the loop continues long enough to actually show the traceback before the infinite loop.
    local function traceError(errorMsg)
        local level, n, traceback, msg, last = 3, 0, {}, nil, ""
        repeat
            _,msg = pcall(error, "", level) --traceback skips function calls used by the "try" function.
            if msg ~= "" and msg ~= skipThisLine1 and msg ~= skipThisLine2 and msg ~= last then
                n = n + 1
                traceback[n] = msg
                last = msg
            end
            level = level + 1
        until level == 200
        local i, results, prev = 0, {}, nil
        repeat
            msg = traceback[n - i]
            local current = msg:sub(1,1)
            i = i + 1
            if current ~= prev then
                prev = current
                results[i] = "\n-> " .. map[current] .. ".lua: " .. msg
            else
                results[i] = "-> " .. msg
            end
        until i == n
        print("|cffff5555ERROR: " .. errorMsg .. "\nStack Traceback: " .. table.concat(results):gsub("[^\x25s]+.lua:(\x25d+):", "\x251") .. "<- error" .. "|r")
    end

    ---@param userFunc function the function to call in protected mode
    ---@param ... any params for the user's function
    function try(userFunc, ...)
        if not skipThisLine1 then _,skipThisLine1 = pcall(error, "", 2) end return select(2, xpcall(userFunc, traceError, ...))
    end
 
Level 20
Joined
Jul 10, 2009
Messages
477
Thank you for your continuous contributions!
Some of your suggestions are already part of the Next version to come (which is 90% finished and will be a big one), such as caching prints during loading screen, stack trace in try, pretty table.print.

The varnames table is also a neat idea that I am happy to incorporate, although it has minor flaws (there might be multiple names for the same variable and the name caching kind of interferes with resources that overwrite natives). Still very useful!

Stay tuned!

Can you elaborate more on the C stack overflow error?
How do you provoke one?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
The varnames table is also a neat idea that I am happy to incorporate, although it has minor flaws (there might be multiple names for the same variable and the name caching kind of interferes with resources that overwrite natives). Still very useful!
Main reason for this is that I was defining a function, and I was getting an error when calling that function "bad argument #1 to GlobalRemap (table expected, got string)". This was utter B.S. and took a ton of time to figure out what was going on, because the function I defined defininitely benefits from having a string as its first parameter.

Once I got the reverse-_G table set up, I discovered that somehow GlobalRemap had been defined by some metatable tom-foolery as "rawget". Still haven't gotten to the bottom of that one, but I am probably close to a solution.

Can you elaborate more on the C stack overflow error?
How do you provoke one?
This is easily caused by an __index or __newindex metatable method that recursively calls itself. Here is an easy way to make it happen:

Lua:
setmetatable(_G, {__index = function(...) return _G[...] end})

bob()
output:
Lua:
lua: jdoodle.lua:1: C stack overflow
stack traceback:
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    ...    (skipping 177 levels)
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:1: in metamethod 'index'
    jdoodle.lua:3: in main chunk
    [C]: in ?
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,887
Main reason for this is that I was defining a function, and I was getting an error when calling that function "bad argument #1 to GlobalRemap (table expected, got string)". This was utter B.S. and took a ton of time to figure out what was going on, because the function I defined defininitely benefits from having a string as its first parameter.
I got you covered.
Best part is when you get an error from a comment (screenshots of what happened just now). That made my day ;)
Using LogStackTrace made it go to the next line (error part 3, 4). Better, but when I look at my code I don't see what could cause it. Will be hard to debug.


I also suffered from other errors some days ago, that made no sense, such as it claiming error in blizzard.lua, some random line number expecting "
SetTextTagVisibility", got something else.
I don't remember exactly what I had, but it had something to do with ShowTextTagForceBJ and I was using wrong parameters in the wrong order, thinking it was SetTextTagVisibility. Yet it didn't match the line closely of ShowTextTagForceBJ in blizzard.lua.
 

Attachments

  • error part 3.png
    error part 3.png
    77.7 KB · Views: 19
  • error part 4.png
    error part 4.png
    22.4 KB · Views: 15
  • error 1 part 2.png
    error 1 part 2.png
    18.9 KB · Views: 15
  • error 1.png
    error 1.png
    1.5 MB · Views: 22
Level 19
Joined
Jan 3, 2022
Messages
320
@Eikonium I probably found the root of the issue where the console shrinked although it contained wider text. That's because the Multiboard width is controlled either by the title or by the total width of the first row:
CSS:
/**
Sets the new text width for the cell.

Default width is `0.03` (3%), this is enough to fit 1-3 characters
(depending on font and character).

@bug **NOTE!** Multiboard's total width is calculated based on ONLY the first row's
widths.

*Example:* Your first row is very short, but second row is twice is long.

*Result:* The second row will not fit inside the table and overflow to the right,
beyond the visible area.

**Summary:** To set the multiboard width, set the width of columns in the first row.

@bug Although the column width is set immediately and items in the same row are
moved left/right, the multiboard is not redrawn to accomodate the new width.

To update the entire multiboard's width, you must manually minimize/maximize
the multiboard or call `MultiboardDisplay(udg_myMultiboard, true)`
or `MultiboardMinimize(udg_myMultiboard, false).

For example, if you only change the width of cell at (x=0, y=0) to
be 0.2x of screen width, then the cell (x=1, y=0) will be moved right beyond the
visible screen.

@param mbi Target cell handle
@param width New cell width expressed as screen width. `1.0` = 100% of screen width,
`0.05` = 5% of screen width.

The multiboard is right-aligned (begins at the right) at the window border.
See Tasyen's
[The Big UI-Frame Tutorial](https://www.hiveworkshop.com/pastebin/e23909d8468ff4942ccea268fbbcafd1.20598#PosFrames)
for the explanation of screen width.

@note See: `MultiboardSetItemsWidth`
*/
native MultiboardSetItemWidth           takes multiboarditem mbi, real width returns nothing

And I wrote a function to debug/compare/print custom type variables against their defined constants like GAME_TYPE_XXX: wc3-test-maps/global_constants_to_string.lua because it was tiresome to compare each one by hand...
 
Level 20
Joined
Jul 10, 2009
Messages
477
@Eikonium I probably found the root of the issue where the console shrinked although it contained wider text. That's because the Multiboard width is controlled either by the title or by the total width of the first row:
IngameConsole resizes all rows and minimizes/maximizes after each update, so it should not be affected by the bug you have quoted.

The issue from my point of view is that Warcraft changed font size with version 1.33 and lngameConsole still computes line width with respect to 1.32 font.
I have updated font size in the new version of Debug Utils, which will hopefully release this week, maybe next.
This should solve the issue.

Just to be on the safe side, what string exactly didn't fit for you? I will test it in the new version before release.

And I wrote a function to debug/compare/print custom type variables against their defined constants like GAME_TYPE_XXX: wc3-test-maps/global_constants_to_string.lua because it was tiresome to compare each one by hand...
This can definitely come in handy. The next version of Debug Utils includes a similar tool (although not too similar): Implementing @Bribe's suggestion on building up a nameCache for global variables at the start of the game, we can let tostring() output names from global scope instead of the usual hexStuff.
For instance, print(GetGameTypeSelected()) would visually print gametype: GAME_TYPE_MELEE, which makes it very easy to compare with Wc3 global variables.
I reckon, this solution would be just as beneficial for you?
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
I would like to add that there is a nice format to encompass the user's scripts in the Lua root into the "try" function via adding "try(function()" below the "try" script, then in the lowest trigger adding "end)". It can be neatly organized by including a "body" category where all the custom script triggers will go into:

1668277305510.png


1668277296000.png


1668277283927.png


1668277325974.png
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
That's a great tool! I'm not familiar with that language, but it looks similar to Visual Basic.

Is it stripping spaces and Lua comments?

One constructive recommendation from my part would be to concatenate short lines (where their sum is <127 chars) via ";" to save time when pasting in larger scripts.
 
Top