Skip to content

The standard library

Lesson 8.4

Lua comes with a small set of built-in libraries. This lesson is a tour of what is there, and of what is different inside a Celestial mod.

The libraries at a glance

Library What it’s for Covered in
basic functions print, type, tostring, pairs, pcall… throughout
string text Strings, String patterns
table lists Lists, Working with tables
math numbers Numbers and maths
utf8 Unicode text below
coroutine coroutines Coroutines
os time and dates (and, outside mods, the system) below
io files and the console below
debug inspecting running code below
package how require finds modules Modules

Basic functions you haven’t met yet

select with a negative number counts from the end: select(-1, ...) is the last argument.

next(t, key) returns the key after key in t. pairs uses it internally. next(t) == nil means “the table is empty”.

rawequal, rawget, rawset, rawlen do the plain operation and skip metamethods.

load(text) turns a string of Lua code into a function. It returns nil and a message if the code has a syntax error:

local f = load("return 2 + 3")
print(f())                --> 5

local ok, err = load("return +")
print(ok)                 --> nil

You can give the loaded code its own environment, so it only sees the names you allow:

local env = { x = 5 }
local g = load("return x * 2", "calc", "t", env)
print(g())  --> 10

The "t" means text only. Never run code that a player typed without a limited environment like this. Usually you don’t need load at all.

collectgarbage("count") gives the memory Lua is using, in kilobytes. Lua frees memory by itself, so you rarely call the collector by hand.

utf8

Lua strings are bytes. The utf8 library works with characters:

print(utf8.len("Café"))           --> 4
print(utf8.char(72, 228, 8364))   --> Hä€
print(utf8.codepoint("ä"))        --> 228

utf8.codes loops over the characters, giving each one’s byte position and code:

for pos, code in utf8.codes("hé") do
  print(pos, code)
end
1	104
2	233

utf8.len returns nil (and the position of the bad byte) when a string isn’t valid UTF-8. utf8.offset(s, n) gives the byte position of the nth character, which you can pass to string.sub to cut by characters:

local name = "Zoë the Great"
print(name:sub(1, utf8.offset(name, 4) - 1))  --> Zoë

os: time and dates

local now = os.time()                  -- seconds since 1 January 1970
print(type(now))                       --> number

local t = os.time({ year = 2026, month = 10, day = 17, hour = 12 })
print(os.date("%Y-%m-%d %H:%M", t))    --> 2026-10-17 12:00

local d = os.date("*t", t)             -- a table with the parts
print(d.year, d.month, d.day)          --> 2026	10	17

print(os.difftime(t + 90, t))          --> 90.0
  • os.time() is the current time in seconds. With a table, it converts a date into seconds.
  • os.date(format, time) formats a time: %Y year, %m month, %d day, %H:%M:%S time. The format "*t" returns a table instead. Without a time it uses now.
  • os.clock() is the CPU time your program has used, in seconds, as a float. Measure how long something takes with two calls:
local start = os.clock()
local sum = 0
for i = 1, 1000000 do sum = sum + i end
print(string.format("took %.3f s", os.clock() - start))

Outside a mod, os also has os.getenv, os.remove, os.rename, os.execute and os.exit, which touch the computer itself.

io: files and the console

In a normal Lua install, io reads and writes files:

-- plain Lua on your computer; this doesn't work in a mod
local file = assert(io.open("scores.txt", "w"))
file:write("Alex 12\n")
file:close()

for line in io.lines("scores.txt") do
  print(line)
end

io.write(...) prints without a line break or tabs, and io.read() reads a line typed in the terminal.

debug

debug.traceback() returns the current call stack as text, which is very useful in error handlers (see Error handling). The rest of debug lets code inspect and change local variables and hooks. It is meant for debuggers, not for normal programs.

Inside a Celestial mod

Mods run in a sandbox so they can’t harm the player’s computer. Some of the library is therefore missing or replaced:

Plain Lua In a Celestial mod
print works, and writes to the game log
string, table, math, utf8, coroutine all there (no string.dump)
os.time, os.clock, os.date, os.difftime there
os.execute, os.remove, os.getenv, os.exit not available
io (files, console) not available. Use cl.fs: it reads your mod’s own folder and writes only in mods/<your mod>/data/ (Files and the web)
settings files mod.config holds your mod’s config, and cl.config writes it
require only finds files in your mod’s scripts/ folder
load text only, no precompiled chunks; dofile and loadfile are gone
debug only debug.traceback
math.random works as usual
logging prefer cl.log("text"), cl.log.warn(...) and cl.log.error(...), which add the level and your mod’s name

Everything you have learned about the language works the same. Only the parts that reach outside the game are swapped for the cl API, which part 2 covers, starting with Setting up.

Try it

Write format_duration(seconds) that turns a number of seconds into text like 1h 02m 05s, leaving out the hours when there are none (3m 20s). Then print how many days are left from 17 October 2026 until 25 December 2026, using os.time and os.difftime.

Show a solution
local function format_duration(seconds)
  local h = seconds // 3600
  local m = seconds % 3600 // 60
  local s = seconds % 60
  if h > 0 then
    return string.format("%dh %02dm %02ds", h, m, s)
  end
  return string.format("%dm %02ds", m, s)
end

print(format_duration(3725))  --> 1h 02m 05s
print(format_duration(200))   --> 3m 20s

local from = os.time({ year = 2026, month = 10, day = 17, hour = 12 })
local to = os.time({ year = 2026, month = 12, day = 25, hour = 12 })
print(os.difftime(to, from) // 86400)  --> 69.0

Using noon (hour = 12) for both dates avoids surprises from daylight-saving changes, which can make a day 23 or 25 hours long.

Recap: the standard library covers text, tables, maths, UTF-8, coroutines and time; in a Celestial mod, files and the system are reached through the cl API instead of io and os.