GameMaker: __FILE__/__LINE__ macros

Occasionally you may want to get current script's name/current line for debug purposes.

This post is about a small trick to have just that.
And a bit on debugging ds_grid out-of-bounds warnings.

The idea

While GameMaker lacks C-style __FILE__ / __LINE__ macros, it has a function for getting the callstack. While, obviously, this forms a new array on call, and thus should be used with care, you usually wouldn't have too much logging code in performance-critical areas anyway.

Still, we can do a bit of caching not to redo string operations.

The code

So we'll have a few scripts,

debug_get_callstack_pos

/// @param callstack_array
/// @param index
gml_pragma("global", "global.g_debug_get_callstack_pos = ds_map_create();");
var _item = argument0[argument1];
var _pos = global.g_debug_get_callstack_pos[?_item];
if (is_undefined(_pos)) {
    _pos = _item;
    if (string_copy(_pos, 1, 4) == "gml_")
    switch (string_ord_at(_pos, 5)) {
        case ord("S"): case ord("O"): // gml_Script_, gml_Object_
            _pos = string_delete(_pos, 1, 11);
            break;
    }
    global.g_debug_get_callstack_pos[?_item] = _pos;
}
return _pos;

This one's pretty straightforward. We get an item from the callstack, we trim off gml_Script or gml_Object from the beginning, and we store it for later reuse (so that we don't have to trim again on the next call).

debug_get_callstack_file

/// @param callstack_array
/// @param index
gml_pragma("global", "global.g_debug_get_callstack_file = ds_map_create();");
var _item = argument0[argument1];
var _file = global.g_debug_get_callstack_file[?_item];
if (is_undefined(_file)) {
    _file = _item;
    var _pos = string_pos(":", _file);
    if (_pos) _file = string_copy(_file, 1, _pos - 1);
    //
    if (string_copy(_file, 1, 4) == "gml_")
    switch (string_ord_at(_file, 5)) {
        case ord("S"): case ord("O"): // gml_Script_, gml_Object_
            _file = string_delete(_file, 1, 11);
            break;
    }
    //
    global.g_debug_get_callstack_file[?_item] = _file;
}
return _file;

Akin to above, but we also trim :line from the end if there's one.

debug_get_callstack_line

/// @param callstack_array
/// @param index
gml_pragma("global", "global.g_debug_get_callstack_line = ds_map_create();");
var _item = argument0[argument1];
var _line = global.g_debug_get_callstack_line[?_item];
if (is_undefined(_line)) {
    var _pos = string_pos(":", _item);
    if (_pos) {
        _line = real(string_delete(_item, 1, _pos));
    } else _line = -1;
    global.g_debug_get_callstack_line[?_item] = _line;
}
return _line;

The opposite of _file - we get just the line, or return -1 if there's none (which is the case in non-YYC release builds).

Macros

Onwards to the interesting things - macros.

In GMS2 you can just add the following wherever:

#macro __FILE__ debug_get_callstack_file(debug_get_callstack(), 0)
#macro __LINE__ debug_get_callstack_line(debug_get_callstack(), 0)
#macro __POS__   debug_get_callstack_pos(debug_get_callstack(), 0)

In GMS1 you'd have to use the macro editor instead.

Using

With the above done, doing

show_debug_message(__POS__);
show_debug_message(__FILE__);
show_debug_message(__LINE__);

in a script called scr_test would output

scr_test:1
scr_test
3

Other uses

Using the same scripts, you can have scripts retrieve information about where they are called from.

For example, let's suppose that you want to find out where is it in your game that you are reading outside a grid, flooding your output log with "attempting to read outside the grid" warnings. You could make a script called ds_grid_get_debug:

/// @param grid
/// @param x
/// @param y
var g = argument0;
var i = argument1;
var k = argument2;
var w = ds_grid_width(g);
var h = ds_grid_height(g);
if (i >= 0 && k >= 0 && i < w && k < h) {
    return ds_grid_get(g, i, k);
} else {
    show_debug_message("ds_grid_get_debug["
        + debug_get_callstack_pos(debug_get_callstack(), 1)
        + "] attempting to read outside the grid, x="
        + string(i) + ", y=" + string(k) + ", w=" + string(w) + ", h=" + string(h));
    return undefined;
}

and thus get more helpful warnings like

ds_grid_get_debug[scr_test:2] attempting to read outside the grid, x=-1, y=1, w=4, h=4

(and you could put a breakpoint on show_debug_message line too)

Then, once everything is figured out, you'd find-replace ds_grid_get_debug back to regular ds_grid_get.


Have fun!

Related posts:

2 thoughts on “GameMaker: __FILE__/__LINE__ macros

  1. This is awesome.

    For “Other uses”, instead of find-replacing all ds_grid_get with ds_grid_get_debug and vice-versa, you could change it all in one place using a macro. At least, this works in GMS 2:

    “`
    #macro grid_get ds_grid_get
    #macro grid_set ds_grid_set
    var grid = ds_grid_create(5, 5);
    grid_set(grid, 0, 0, “hello”);
    grid_set(grid, 999, 999, “hello”);
    show_debug_message(grid_get(grid, 0, 0));
    show_debug_message(grid_get(grid, 999, 999));
    “`

    Output:
    “`
    Grid 6, index out of bounds writing [999,999] – size is [5,5]
    hello
    Grid 6, index out of bounds writing [999,999] – size is [5,5]
    undefined
    “`

    If you change the macros as follows:
    “`
    #macro grid_get ds_grid_get_debug
    #macro grid_set ds_grid_set_debug // I created this using ds_grid_get_debug as a template
    “`

    …then output is:
    “`
    debug_ds_grid_set[GlobalInitializer_Create_0:51] attempting to write outside the grid, x=999, y=999, w=5, h=5
    hello
    debug_ds_grid_get[GlobalInitializer_Create_0:53] attempting to read outside the grid, x=999, y=999, w=5, h=5
    undefined
    “`

    Is there a way to set a macro based on another macro? Then, the appropriate one could be chosen based off of whether a DEBUG_MODE macro is true or false.

    • There is not a way to set a macro based on other macro, but there is a way to set a macro based on current configuration –

      #macro grid_get ds_grid_get
      #macro grid_get:Debug ds_grid_get_debug

Leave a Reply to Vadim Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.