Skip to content

Helpers ​

Helpers are a set of auxiliary Lua functions for comfortable work in the project.

These helper functions significantly extend the standard Lua capabilities and simplify development.


String ​

Methods provided by Luanti:

string:lower() ​

Converts string to lowercase with Cyrillic support.

lua
print(('ПРИВЕТ МИР'):lower())  -- 'привет мир'

string:upper() ​

Converts string to uppercase with Cyrillic support.

lua
print(string.upper('привет мир'))  -- 'ПРИВЕТ МИР'

string:is_one_of(table) ​

Checks if the string is one of the values in the passed array.
(Semantic analogue of table.contains, which checks if the string is contained in the passed array.)

lua
local text = 'hello'
local options = { 'hello', 'world', 'lua' }
print(text:is_one_of(options))  -- true

string:first_to_upper() ​

Converts the first letter to uppercase.

lua
local text = 'hello world'
print(text:first_to_upper())  -- 'Hello world'

string :title()/:to_headline() ​

Converts the first letter of each word to title case.
Alternative name: string:to_headline()

lua
local text = 'hello world from lua'
print(text:title())           -- 'Hello World From Lua'
print(text:to_headline())     -- 'Hello World From Lua'

string:starts_with(prefix) ​

Checks if the string starts with the specified prefix.

lua
local filename = 'config.json'
print(filename:starts_with('config'))    -- true
print(filename:starts_with('settings'))  -- false

string:ends_with(suffix) ​

Checks if the string ends with the specified suffix.

lua
local filename = 'config.json'
print(filename:ends_with('.json'))    -- true
print(filename:ends_with('.conf'))    -- false

string:contains(sub_string) ​

Checks for the presence of a substring.

lua
local text = 'Hello, World !!!'
print(text:contains('World'))   -- true
print(text:contains('world'))   -- false (case sensitive)

string:replace(pattern, replacement, n?) ​

Replaces substring (analogue of gsub, but returns only the string without the number of replacements).
See documentation for string.gsub and Patterns

lua
local text = 'Hello, World, World!'
local result = text:replace('World', 'Lua')
print(result)  -- 'Hello, Lua, Lua!'

-- Replace only the first occurrence
local result2 = text:replace('World', 'Lua', 1)
print(result2)  -- 'Hello, Lua, World!'

string:remove(pattern, n?) ​

Removes substring.
See documentation for string.gsub and Patterns

lua
local text = 'Hello, World!'
print(text:remove(', '))        -- 'HelloWorld!'
print(text:remove('o', 1))      -- 'Hell, Wrld!' (remove only the first 'o')

string:reg_escape() ​

Escapes special characters for regular expressions.
See Patterns

lua
local pattern = 'file.txt'
print(pattern:reg_escape())      -- 'file%.txt'

local pattern2 = '^$()%.[]*+-?)'
print(pattern2:reg_escape())     -- '%^%$()%.%[%]%*%+\-%?)'

string:vxr_split(delimiter?, processor?) ​

Splits string by specified delimiter with the ability to process parts.

Luanti has its own string.split method, but it doesn't support processing parts.
This method adds the ability to process string parts.

lua
-- Without processing
('hello world'):vxr_split()             -- { 'hello', 'world' }
('apple,banana,cherry'):vxr_split(',')  -- { 'apple', 'banana', 'cherry' }

-- With processing
local numbers = '1,2,3,4,5'
local squared = numbers:vxr_split(',', function(x)
    return tonumber(x)^2 
end)
print(squared[1], squared[2], squared[3], squared[4], squared[5])
-- 1 4 9 16 25

string.or_nil(value) ​

Converts to string or returns nil.

lua
print(string.or_nil('hello'))     -- 'hello'
print(string.or_nil(42))          -- '42'
print(string.or_nil(nil))         -- nil
print(tostring(nil))              -- 'nil' (comparison)

Methods provided by Luanti: ​

Luanti string.split() ​

Also see: string.vxr_split(delimiter?, processor?)

Splits string into parts by specified separator. Returns array of strings. Arguments:

  • separator? separator, default: ","
  • include_empty? default: false
  • max_splits? if negative, splits are unlimited, default: -1
  • sep_is_pattern? whether separator is a regular string or a pattern (regex), default: false
lua
local text = 'apple,banana,cherry'
local parts = text.split(',')
print(parts[1], parts[2], parts[3])  -- 'apple', 'banana', 'cherry'

-- Split by spaces
local words = 'hello world lua'.split(' ')
print(words[1], words[2], words[3])  -- 'hello', 'world', 'lua'

Luanti string.trim() ​

Removes whitespace characters from the beginning and end of the string.

lua
print(("\n \t\tfoo bar\t "):trim())  -- 'foo bar'

Luanti string.pack() ​

Packs values into a binary string according to format.
Backport from Lua 5.4.

lua
-- Pack integers
local packed = string.pack('i4', 42)
print(#packed)  -- 4 (size in bytes)

-- Pack multiple values
local data = string.pack('i4f8', 100, 3.14)
print(#data)  -- 12 (4 + 8 bytes)

Luanti string.unpack() ​

Unpacks binary string into values according to format.
Backport from Lua 5.4.

lua
local packed = string.pack('i4f8', 100, 3.14)
local num, float, next_pos = string.unpack('i4f8', packed)
print(num, float)      -- 100, 3.14
print(next_pos)        -- 13 (position after unpacking)

-- Unpack from specified position
local value = string.unpack('i4', packed, 5)
print(value)            -- 3.14 (as float)

Luanti string.packsize() ​

Returns the size of the string that will be created by string.pack.
Backport from Lua 5.4.

lua
local size = string.packsize('i4f8')
print(size)  -- 12 (4 + 8 bytes)

-- For complex formats
local complex_size = string.packsize('i4c10f8')
print(complex_size)  -- 22 (4 + 10 + 8 bytes)

Table ​

Methods, provided by Luanti:

  • table.copy()
  • table.copy_with_metatables()
  • table.insert_all()
  • table.indexof()
  • table.keyof()
  • table.key_value_swap()
  • table.shuffle()

table.keys(table) ​

Returns an array of table keys.

lua
local data = { name = 'Alek', age = 25, city = 'Vladivostok' }

local keys = table.keys(data)
-- `keys` contains `{ 'name', 'age', 'city' }` (order may vary)

table.values(table) ​

Returns an array of table values.

lua
local data = { name = 'Alek', age = 25, city = 'Vladivostok' }

local values = table.values(data)
-- `values` contains { 'Alek', 25, 'Vladivostok' }

table.has_key(table, key) ​

Checks if a key exists in the table.

lua
local data = { name = 'Alek', age = 25 }
print(table.has_key(data, 'name'))   -- true
print(table.has_key(data, 'city'))   -- false

table .contains/has_value(table, value) ​

Checks if a value exists in the table.

lua
local fruits = { 'apple', 'banana', 'cherry' }
print(table.contains(fruits, 'banana'))  -- true
print(table.contains(fruits, 'orange'))  -- false

You can also use string:is_one_of():

lua
print('banana':is_one_of(fruits))  -- true
print('orange':is_one_of(fruits))  -- false

Alternative name: table.has_value

lua
local data = { x = 10, y = 20 }
print(table.has_value(data, 10))     -- true
print(table.has_value(data, 30))     -- false

table.keys_of(table, value) ​

Returns a table with keys from the specified table that have the specified value.

lua
local data = { a = 10, b = 20, c = 10, d = 30 }
local keys = table.keys_of(data, 10)
print(dump(keys))  -- { 'a', 'c' }

local keys2 = table.keys_of(data, 99)
print(keys2)  -- nil (no such keys)

table.has_any_key(table, find_keys) ​

Checks if the table keys contain at least one value from the specified array.

lua
local data = { name = 'test', age = 25, active = true }
local find_keys = { 'name', 'email', 'phone' }

print(table.has_any_key(data, find_keys))   -- true (key 'name' is in find_keys)

local find_keys2 = { 'email', 'phone', 'address' }
print(table.has_any_key(data, find_keys2))  -- false

table.equals(table1, table2) ​

Recursively compares two tables for complete equality.

lua
local table1 = { a = 1, b = { c = 2, d = 3 } }
local table2 = { a = 1, b = { c = 2, d = 3 } }
local table3 = { a = 1, b = { c = 2, d = 4 } }

print(table.equals(table1, table2))  -- true
print(table.equals(table1, table3))  -- false

table.is_empty(table) ​

Checks if the table is empty.

lua
print(table.is_empty({}))           -- true
print(table.is_empty({ a = 1 }))    -- false

table.is_position(table) ​

Checks if the table represents coordinates.

lua
print(table.is_position({ x = 10, y = 20, z = 30 }))  -- true
print(table.is_position({ x = 10, y = 20 }))          -- false
print(table.is_position({ a = 1, b = 2, c = 3 }))     -- false

table.each_value_is(table, value) ​

Checks that all elements of the table are equal to the specified value. By default checks for true.

lua
local data1 = { true, true, true }
print(table.each_value_is(data1))  -- true

local data2 = { 5, 5, 5, 5 }
print(table.each_value_is(data2, 5))  -- true

local data3 = { 1, 2, 3 }
print(table.each_value_is(data3, 1))  -- false

table.count(table) ​

Counts the number of elements in the table.

NOTE

For tables with integer keys, use #table.
Since the # operator doesn't work on tables with non-integer keys, use table.count for them.

lua
local data = { a = 1, b = 2, c = 3 }
print(table.count(data))  -- 3
print(#data)              -- 0, because the `#` operator doesn't work with non-integer keys.

local array = { 1, 2, 3, 4, 5 }
print(#array) -- 5

table.generate_sequence(max, start_from?, step?) ​

Generates a sequence of numbers.

lua
local seq1 = table.generate_sequence(5)
print(dump(seq1))  -- { 1, 2, 3, 4, 5 }

local seq2 = table.generate_sequence(10, 2, 2)
print(dump(seq2))  -- { 2, 4, 6, 8, 10 }

local seq3 = table.generate_sequence(1, 5, -1)
print(dump(seq3))  -- { 5, 4, 3, 2, 1 }

table.only(table, only) ​

Returns a new table containing data only with the specified keys.

lua
local data = { name = 'Alek', age = 25, city = 'Vladivostok', country = 'Russia' }
local filtered = table.only(data, { 'name', 'age' })
-- `filtered` contains `{ name = 'Alek', age = 25 }`
print(filtered.name, filtered.age)    -- 'Alek', 25
print(filtered.city)                  -- nil

table.except(table, keys) ​

Returns a new table containing data without the specified keys.
(copies the table, excluding the specified keys)

lua
local data = { name = 'Alek', age = 25, city = 'Vladivostok', country = 'Russia' }
local filtered = table.except(data, { 'age', 'country' })
-- `filtered` contains `{ name = 'Alek', city = 'Vladivostok' }`
print(filtered.name, filtered.city)      -- 'Alek', 'Vladivostok'
print(filtered.age,  filtered.country)   -- nil, nil

table.merge(table1, table2, overwrite?) ​

Recursively merges tables.

By default, the first table is not overwritten, a new one is created.

lua
local defaults = { theme = 'dark', font = { size = 12, family = 'Arial' } }
local user_config = { font = { size = 16 }, language = 'en' }

local merged = table.merge(defaults, user_config)
print(dump(merged))
-- {
--     theme = "dark",
--     font = {
--         size = 16,
--         family = "Arial",
--     },
--     language = "en",
-- }

Overwrite the first table:

lua
table.merge(defaults, user_config, true)

TIP

It's better to use table.overwrite() for better readability.

table.join(table1, table2, recursively?) ​

Adds missing keys from table2 to table1.
Values that are tables are copied using table.copy().

lua
local base     = { a = 1, b =  2        }
local addition = {        b = 10, c = 3 }

table.join(base, addition)
print(dump(base))  -- { a = 1, b = 2, c = 3 }

If recursively is true, the function will be applied recursively only for values that are tables in both table1 and table2.

lua
local base     = { a = 1, b = { c = 2          } }
local addition = {        b = { c = 10, d = 20 } }

table.join(base, addition, true)
print(dump(base))
-- {
--     a = 1,
--     b = {
--         c = 2,
--         d = 20,
--     },
-- }

table.overwrite(table1, table2) ​

Completely overwrites table1 with values from table2.

Semantic variant for table.merge(table1, table2, true).

table.merge_values(table1, table2) ​

Merges values from two tables into one, removing duplicates.
Order is preserved - first values from table1, then from table2 that weren't in table1.

lua
local table1 = { 'apple', 'banana', 'cherry' }
local table2 = { 'banana', 'date', 'apple', 'elderberry' }

local merged = table.merge_values(table1, table2)
print(dump(merged))  -- { 'apple', 'banana', 'cherry', 'date', 'elderberry' }

table.map(table, callback, overwrite?) ​

Applies function to each element of the table.
callback: fun(value: any, key: any): any - accepts value and key, returns new value.

By default returns a new table.

lua
local numbers = { 1, 2, 3, 4, 5 }
local squared = table.map(numbers, function(x) return x * x end)
print(dump(squared))
-- { 1, 4, 9, 16, 25 }

local data = { a = 10, b = 20 }
local doubled = table.map(data, function(value, key)
	print(key, value)
	return value * 2
end)
-- a       10
-- b       20
print(dump(doubled))
-- { a = 20, b = 40 }

If overwrite is true, then the function modifies the original table.

lua
local data = { a = 10, b = 20 }
table.map(data, function(value, key) return value * 2 end, true)
print(dump(data))  -- { a = 20, b = 40 }

table .walk/.each(table, callback) ​

Iterates over the table applying function (does not modify the table).
callback: fun(value: any, key: any): void - accepts value and key; returns nothing.

lua
local data = { a = 10, b = 20, c = 30 }
table.walk(data, function(value, key)
    print(key, value)
end)
-- a       10
-- c       30
-- b       20

Alternative name: table.each()

lua
local data = { a = 10, b = 20, c = 30 }
table.each(data, function(value, key)
    data[key] = value * 2
    -- Nothing prevents accessing upvalue `data`. Now `data` is overwritten.
end)
print(dump(data))  -- { a = 20, b = 40, c = 60 }

table.multiply_each_value(table, multiplier_table) ​

Multiplies each value of the table by the corresponding value from multiplier_table by key.

lua
local data = { a = 10, b = 20, c = 30 }
local multipliers = { a = 2, b = 0.5, c = 3 }

local result = table.multiply_each_value(data, multipliers)
print(dump(result))  -- { a = 20, b = 10, c = 90 }
-- Keys without a multiplier remain unchanged

table.add_values(table1, table2, empty_value?, overwrite?) ​

Adds values with the same keys from two tables.

lua
local table1 = { a = 10, b = 20 }
local table2 = { b = 5, c = 15 }

local result = table.add_values(table1, table2)
print(dump(result))  -- { a = 10, b = 25, c = 15 }

-- With empty value for missing keys
local result2 = table.add_values(table1, table2, 0)
print(dump(result2))  -- { a = 10, b = 25, c = 15 }

table.sub_values(table1, table2, empty_value?, overwrite?) ​

Subtracts table2 values from table1 for the same keys.

lua
local table1 = { a = 10, b = 20, c = 30 }
local table2 = { b = 5, c = 10, d = 5 }

local result = table.sub_values(table1, table2)
print(dump(result))  -- { a = 10, b = 15, c = 20, d = -5 }

table.mul_values(table1, table2, empty_value?, overwrite?) ​

Multiplies values with the same keys from two tables.

lua
local table1 = { a = 2, b = 3 }
local table2 = { b = 4, c = 5 }

local result = table.mul_values(table1, table2)
print(dump(result))  -- { a = 2, b = 12, c = 5 }

table.div_values(table1, table2, empty_value?, overwrite?) ​

Divides table1 values by table2 values for the same keys.

lua
local table1 = { a = 10, b = 20, c = 30 }
local table2 = { b = 5, c = 10, d = 2 }

local result = table.div_values(table1, table2)
print(dump(result))  -- { a = 10, b = 4, c = 3, d = 0.5 }

Math ​

math .limit/.clamp(value, min, max) ​

Limits value to the specified range.
Alternative name: math.clamp().

lua
local health = 150
local max_health = 100
local clamped_health = math.limit(health, 0, max_health)
print(clamped_health)  -- 100

math.is_within(value, min, max) ​

Checks that the value is strictly inside the range (min < value < max).

lua
print(math.is_within(5, 1, 10))   -- true
print(math.is_within(1, 1, 10))   -- false (boundaries not included)
print(math.is_within(10, 1, 10))  -- false

math.is_among(value, min, max) ​

Checks that the value is inside the range, including boundaries (min <= value <= max).

lua
print(math.is_among(5, 1, 10))   -- true
print(math.is_among(1, 1, 10))   -- true
print(math.is_among(10, 1, 10))  -- true

math.is_in_range(value, min, max) ​

Checks that the value is inside the half-open interval (min, max] (min < value <= max).

lua
print(math.is_in_range(5, 1, 10))   -- true
print(math.is_in_range(1, 1, 10))   -- false (equals minimum)
print(math.is_in_range(10, 1, 10))  -- true (equals maximum)

math.is_near(value, near, gap?) ​

Checks that the value is close to the specified one (|value - near| <= gap).

lua
print(math.is_near(10, 12))        -- false (gap = 1 by default)
print(math.is_near(11, 12))        -- true
print(math.is_near(10, 12, 3))     -- true (gap = 3)

math.quadratic_equation_roots(a, b, c) ​

Solves quadratic equation of the form y = a*x^2 + b*x + c.

lua
local root1, root2 = math.quadratic_equation_roots(1, -5, 6)
print(root1, root2)  -- 3, 2

-- No real roots
local r1, r2 = math.quadratic_equation_roots(1, 0, 1)
print(r1, r2)  -- nil, nil

math.point_on_circle(radius, angle) ​

Calculates a point on a circle.

lua
local x, z = math.point_on_circle(5, math.pi/4)
print(x, z)  -- coordinates of a point on a circle with radius 5 at angle 45°

Debug ​

__FILE__(depth?, full?) ​

Returns the name of the current file or the calling function's file depending on the stack depth.

  • depth? - call stack depth (default 0)
  • full? - show full path (default false)
lua
-- Get relative file path
print(__FILE__())  -- 'mods/Voxrame/helpers/src/lua_ext/debug.lua'

-- Get full file path
print(__FILE__(0, true))  -- '/home/alek13/projects/my-game/mods/Voxrame/helpers/src/lua_ext/debug.lua'

-- Get file 2 levels up the call stack
print(__FILE__(2))  -- file of the function that called the calling function

__LINE__(depth?) ​

Returns the current line number or the calling function's line number depending on the stack depth.

lua
print(__LINE__())  -- 57

-- Get calling function's line number
print(__LINE__(1))  -- line number in the calling file

__FILE_LINE__(depth?, full?) ​

Returns file and line in format "relative/path/to/file:line".

lua
print(__FILE_LINE__())  -- 'mods/Voxrame/helpers/src/lua_ext/debug.lua:65'

-- With full path
print(__FILE_LINE__(0, true))  -- '/home/alek13/projects/my-game/mods/Voxrame/helpers/src/lua_ext/debug.lua:65'

__DIR__(depth?) ​

Returns the directory of the current file or the calling function's file depending on the stack depth.

lua
print(__DIR__())  -- 'mods/Voxrame/helpers/src/lua_ext/'

__FUNC__(depth?) ​

Returns the name of the current function.

lua
function my_function()
    print(__FUNC__())  -- 'my_function'
end

Outputs the contents of all passed parameters ....
Before output, adds file name and line @ <file>:<line>.

If with_trace is true, additionally outputs stack trace.

NOTE

If your terminal supports links, each @ <file>:<line> will be linked to open IDE, see readme.md for setup.

TIP

Use pd() and pdt() for more concise syntax. See examples below.

pd(...) ​

Abbreviation for print_dump. Calls print_dump with with_trace == false.

lua
local player_name = 'test_player'
local position = { x = 10, y = 20, z = 30 }

pd(player_name, position)

output:

@ lord/mods/Voxrame/map/src/map/Corridor.lua:14
player_name: "test_player"
   position: {
    x = 10,
    y = 20,
    z = 30,
}

pdt(...) ​

Abbreviation for print_dump traced. Calls print_dump with with_trace == true.

lua
local player_name = 'test_player'
local position = { x = 10, y = 20, z = 30 }

pdt(player_name, position)

output:

debug.get_stack_frames(depth?) ​

Returns the call stack as an array of frames, from the innermost to the outermost.

Each frame is a table:

FieldTypeDescription
filestringsource file of the function (=[C] for functions written in C)
lineintegercurrent line in the file (-1 for functions written in C)
namestring?name of the function, if known
whatstringLua, C, main or tail

depth is how many levels to skip; by default 0 — the first frame is the function that called get_stack_frames().

lua
local function on_click()
    local frames = debug.get_stack_frames()
    pd(frames)
end

output (with debug enabled):

@ lord/mods/my_mod/src/on_click.lua:13
frames: {
    {
        file = "mods/my_mod/src/on_click.lua",
        line = 12,
        name = "on_click",
        what = "Lua",
    },
    {
        file = "mods/my_mod/init.lua",
        line = 30,
        name = "main",
        what = "main",
    },
    {
        file = "[C]",
        line = -1,
        what = "C",
    },
}

NOTE

file is an absolute path. Shortening it (or formatting it otherwise) is up to you, or you can leave it to debug.render_backtrace().

debug.render_backtrace(frames, plain?) ​

Renders frames from debug.get_stack_frames() (or from :catch()) into a multi-line string, one line per frame.

By default the string is styled for a terminal (colors + links). Pass plain = true to get it without styles and links, so it's safe to write to a log:

lua
local function on_click()
    local frames = debug.get_stack_frames()

    -- for a terminal:
    print(debug.render_backtrace(frames))

    -- for a log file (e.g. in production):
    core.log('error', debug.render_backtrace(frames, true))
end

output (print(debug.render_backtrace(frames))):

   1   @ lord/mods/my_mod/src/on_click.lua:12: in on_click
   2   @ lord/mods/my_mod/init.lua:30: in main
   3   @ [C]: in dofile

to a log (with plain = true):

   1   @ mods/my_mod/src/on_click.lua:12: in on_click
   2   @ mods/my_mod/init.lua:30: in main
   3   @ [C]: in dofile

NOTE

If your terminal supports links, each @ <file>:<line> will be linked to open IDE, see readme.md for setup.

debug.print_backtrace(frames) ​

Prints frames to the terminal (styled, with links). For a quick look while debugging.

Abbreviation for Voxrame.terminal.print(debug.render_backtrace(frames)).

TIP

Don't use it in production, write to a log with debug.render_backtrace(frames, true) instead.

debug.measure(name, callback, print_result?) ​

Measures function execution time.

lua
local function heavy_calculation()
    local sum = 0
    for i = 1, 2000000 do
        sum = sum + math.random(1, 100) + math.sin(i)
    end
end

-- Measure with result output
debug.measure('calculation', heavy_calculation, true)
debug.measure('calculation', heavy_calculation, true)
debug.measure('calculation', heavy_calculation, true)
debug.measure('calculation', heavy_calculation, true)
debug.measure('calculation', heavy_calculation, true)
-- output:
-- Measure of [calculation]:  Time:    42 ms ;  Average:    42 ms
-- Measure of [calculation]:  Time:    58 ms ;  Average:    50 ms
-- Measure of [calculation]:  Time:    58 ms ;  Average:    53 ms
-- Measure of [calculation]:  Time:    35 ms ;  Average:    48 ms
-- Measure of [calculation]:  Time:    33 ms ;  Average:    45 ms


-- Measure without output
local time, avg, count = debug.measure('calculation', heavy_calculation)
print(time, avg, count)
local time, avg, count = debug.measure('calculation', heavy_calculation)
print(time, avg, count)
-- output:
-- 33.075  43.170833333333 6
-- 33.135  41.737142857143 7

debug.measure_print(name) ​

Outputs statistics of previous measurements name obtained by calls to debug.measure(name, ....).

lua
debug.measure_print('calculation')
-- output:
-- Measure of [calculation]: Average time:    42 ms ; Last time:    33 ms ; Count of measures:     7

Exception ​

Convenient try/catch, just like you're used to in other languages.

lua
local try = Voxrame.exception.try

try(function()
    player:right_click(node)
end):catch(function(error, traceback)
    core.log('error', tostring(error) .. '\n' .. debug.render_backtrace(traceback, true))
end)

exception.try(callback) ​

Calls callback() immediately in the protected mode and returns an object with the :catch() method.

WARNING

Errors are not rethrown. Without :catch() an error will be silently swallowed.

The stack trace is collected only when an error occurs (at the moment of the error, before the stack is unwound), so a successful call costs almost nothing.

:catch(handler) ​

Calls handler(error, traceback) if the callback passed earlier to try() has failed. Does nothing otherwise.

  • error — what was thrown by error(). Usually it's a string, but error() can throw any value.
  • traceback — call stack at the moment of the error, an array of frames from debug.get_stack_frames(), innermost first.

Examples of handling/using traceback:

lua
try(function()
    error('Something went wrong')
end):catch(function(error, traceback)
    -- in development: to a terminal
    debug.print_backtrace(traceback)

    -- in production: to a log
    core.log('error', tostring(error) .. '\n' .. debug.render_backtrace(traceback, true))
end)

debug.print_backtrace(traceback) (with debug enabled):

debug.render_backtrace(traceback, true):

   1   @ [C]: in error
   2   @ mods/my_mod/init.lua:12: in function
   3   @ [C]: in xpcall
   4   @ mods/Voxrame/helpers/src/exception/FlowControl.lua:64: in try
   5   @ mods/Voxrame/helpers/src/exception.lua:19: in try
   6   @ mods/my_mod/init.lua:11: in main

NOTE

traceback contains all frames, including internal ones (xpcall, try) at the end.

TIP

:catch() returns the same object, so you can chain several :catch().

Global ​

errorf(message, ...) ​

Similar to error() in Lua, but message can be a template string with specified parameters, similar to string.format().

lua
local player_name = nil
errorf('Player `%s` not found', player_name)
-- Error: Player `nil` not found

output (when debug is enabled):

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ERROR:
  Player `nil` not found

Stack trace:
   1   @ [C]: in error
   2   @ lord/mods/Voxrame/helpers/src/lua_ext/global.lua:9: in errorf
   3   @ lord/mods/Voxrame/map/src/map/Corridor.lua:14: in main
   4   @ [C]: in dofile
   5   @ lord/mods/Voxrame/builtin_ext/src/mod/require.lua:29: in require
   6   @ lord/mods/Voxrame/map/src/map.lua:9: in main
   7   @ [C]: in dofile
   8   @ lord/mods/Voxrame/builtin_ext/src/mod/require.lua:29: in require
   9   @ lord/mods/Voxrame/map/init.lua:4: in mod_init_function
  10   @ lord/mods/Voxrame/builtin_ext/src/mod.lua:80: in mod
  11   @ lord/mods/Voxrame/map/init.lua:3: in main

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Error handling in Luanti ​

WARNING

The stack trace will contain extra lines, because Luanti doesn't pass the specified level to core.error_handler().

errorlf(message, level, ...) ​

Like in errorf() you can pass a format string and parameters, and also specify the trace level.

lua
errorlf('Error in data', 3, 'value: %d', 42)
-- Error with trace 3 levels up

see error handling in Luanti

assertf(condition, message, ...) ​

Similar to assert() in Lua, but as message you can specify a format string with parameters, similar to string.format().

Calls errorf() if the condition condition is not met.

lua
-- Continues execution if player exists, otherwise error
local player = nil
assertf(player, 'Player `%s` not found', player)

IO ​

io.file_exists(name) ​

Checks if a file exists.

lua
if io.file_exists('config.txt') then
    print('Configuration file found')
else
    print('Configuration file missing')
end

io.dirname(path) ​

Returns the directory from the path.

lua
print(io.dirname('/home/user/project/file.txt'))  -- '/home/user/project'
print(io.dirname('relative/path/file.txt'))       -- 'relative/path'
print(io.dirname('file.txt'))                     -- '.'

io.write_to_file(filepath, content, mode?) ​

Writes content to a file.

lua
local success, error_code, error_message = io.write_to_file('data.txt', 'Hello, World!')

if success then
    print('Data successfully written')
else
    print('Write error:', error_code, error_message)
end

-- Append to end of file
io.write_to_file('log.txt', 'New entry\n', 'a')

io.read_from_file(filepath, mode?) ​

Reads all content from a file.
Returns a string with file content on success or false, error_code, error_message on error.

lua
local content = io.read_from_file('config.json')
if content then
    print('File content:', content)
else
    print('File read error')
end

Error handling

lua
local success, error_code, error_message = io.read_from_file('nonexistent.txt')
if not success then
    print('Error:', error_code, error_message)
end
-- Error: 2       nonexistent.txt: No such file or directory

or:

lua
local success = io.read_from_file('nonexistent.txt')
if not success then
    local error_code, error_message = io.get_file_error()
    print('Error:', error_code, error_message)
end
-- Error: 2       nonexistent.txt: No such file or directory

Reading in binary mode

lua
local binary_content = io.read_from_file('image.png', 'rb')
if binary_content then
    print('File size:', #binary_content, 'bytes')
end

io.get_file_error() ​

Returns the code and message of the last error from io.read_from_file() or io.write_to_file() functions.

lua
local success = io.read_from_file('nonexistent.txt')
if not success then
    local error_code, error_message = io.get_file_error()
    print('Error:', error_code, error_message)
end
-- Output: Error: 2       nonexistent.txt: No such file or directory
lua
local success = io.write_to_file('/readonly/file.txt', 'data')
if not success then
    local error_code, error_message = io.get_file_error()
    print('Write error:', error_code, error_message)
end
-- Output: Write error: 13       /readonly/file.txt: Permission denied

OS ​

os.DIRECTORY_SEPARATOR ​

Directory separator for the current OS.

lua
local DS = os.DIRECTORY_SEPARATOR

print(DS)  -- '/' on Linux/Mac, '\' on Windows
print('folder' .. DS .. 'file.txt')
-- on Linux/Mac: 'folder/file.txt'
-- on Windows:   'folder\file.txt'

Made with ❤️ for the Luanti community