Introduction to Lua
The parts of Lua 5.5 a proxy plugin author needs, with examples that run on Basalt Proxy.
Lua is a small scripting language: a few types, one data structure and functions that are values. Basalt Proxy embeds the official Lua 5.5, so what you learn here is plain Lua and applies to any other Lua book. This page covers what a proxy plugin uses. If you already write Java plugins, Coming from Java? (Velocity) and Coming from Java? (BungeeCord) map that way of working to this one.
Run something#
Plugins are off until the operator enables them, see Plugins. A plugin is a folder
with a plugin.toml and a src/main.lua, zipped into a .bppkg. Every example here can go in main.lua. After a change,
rebuild the package and type reload in the proxy console.
log.info("Hello from Lua")log.info writes a line to the proxy log under the plugin's name. print does the same.
Values and variables#
local name = "Steve" -- string
local lives = 3 -- integer
local speed = 0.2 -- float
local alive = true -- boolean
local nothing = nil -- the absence of a value- Always write
local. A name assigned without it becomes a global, visible to every function of the plugin. nilis what you get from a missing key or a failed lookup, such asplayers.by_name("nobody"). Onlynilandfalseare false in a condition.0and""are true.type(x)returns"nil","boolean","number","string","table"or"function".
Numbers and strings#
log.info(7 / 2) -- 3.5, `/` always gives a float
log.info(7 // 2) -- 3, integer division
log.info(7 % 2) -- 1
local greeting = "Hello, " .. name .. "!" -- `..` joins strings
log.info(#greeting) -- 14, the length in bytes
log.info(string.format("%s has %d lives", name, lives))Text sent to players is MiniMessage, for example "<green>Hello", and takes {name}
arguments from a table, so you rarely build it with ...
Comparison and logic#
if lives ~= 0 and alive then -- `~=` is "not equal"; `and`, `or` and `not` are words
log.info("still playing")
end
local lobby = config.lobby or "lobby" -- `or` gives a default when the left side is niland and or return one of their operands, not a boolean.
Tables#
A table is the only data structure. It is a list, a map, an object and a module, depending on how you use it.
local staff = { "Alex", "Sam" } -- a list, from index 1
log.info(staff[1]) -- Alex, not staff[0]
table.insert(staff, "Robin")
local limits = { lobby = 100, survival = 50 } -- a map
limits.lobby = limits.lobby + 10
limits["creative"] = 20
limits.survival = nil -- setting nil removes the keyfor i, who in ipairs(staff) do
log.info(i .. ": " .. who)
end
for backend, max in pairs(limits) do
log.info(backend .. " " .. max)
endThings that catch people out:
- Lists start at 1.
pairshas no guaranteed order. Keep a list when order matters.- A
nilin the middle of a list breaks#andipairs. Usetable.remove. - Tables are references.
local b = amakes two names for one table. #does not count the keys of a map.
Functions#
Functions are values: store them, pass them, return them.
local function visits_message(name, count)
return string.format("%s has joined %d times", name, count)
end
local function min_max(list)
local lo, hi = list[1], list[1]
for _, v in ipairs(list) do
lo = math.min(lo, v)
hi = math.max(hi, v)
end
return lo, hi -- more than one result
endA function keeps the locals around it alive, which is how a handler remembers things between calls:
local joined = 0
on("player.join", function(e)
joined = joined + 1
log.info(joined .. " joins since the plugin loaded")
end)That counter is lost on reload and when the proxy stops. For anything that must survive, use
Storage.
Methods and the colon#
a:b(x) is a.b(a, x). Player tables use it:
e.player:send("<green>Hello") -- the same as e.player.send(e.player, "<green>Hello")
local server = e.player.server -- a field is read with a dotForgetting the colon is the most common mistake when starting. The error usually says a string or nil was found where a
player was expected.
Control flow#
for i = 1, 5 do ... end -- 1, 2, 3, 4, 5
for i = 10, 1, -2 do ... end -- 10, 8, 6, 4, 2
while lives > 0 do lives = lives - 1 end
for _, p in ipairs(players.online()) do
if p.server ~= "lobby" then goto continue end -- Lua has no `continue`
p:send("You are in the lobby")
::continue::
endbreak leaves a loop, goto jumps to a label in the same function, and return ends a function and must be the last
statement of its block.
Errors#
error("message") raises an error and pcall catches one:
local ok, err = pcall(function()
players.by_name("nobody"):send("hi") -- the lookup is nil, so this fails
end)
if not ok then
log.warn("could not send: " .. err)
endYou seldom need it. An error in a handler is logged with the plugin and the line, the proxy carries on, and five failures
in a row switch the plugin off, see The sandbox. Check for nil where you expect it
instead of catching it:
local target = players.by_name("Steve")
if not target then
return
endSplitting code into files#
require loads another file of src/ once and returns what it returns:
-- src/visits.lua
local M = {}
function M.bump(player)
player.data.visits = (player.data.visits or 0) + 1
return player.data.visits
end
return M-- src/main.lua
local visits = require("visits")
on("player.join", function(e)
e.player:send("<green>Visit number {n}", { n = visits.bump(e.player) })
end)require("shop.prices") loads src/shop/prices.lua. A plugin can also export modules to the plugins that depend on it,
see Plugins. The worked example splits a plugin this way,
Worked example.
What the proxy adds#
The functions above, on, cmd, later, players, servers, config, store and the rest, are globals the proxy
provides next to the standard library. They are all in Plugin API.
on("player.join", function(e)
broadcast("<yellow>{name} joined", { name = e.player_name })
end)
cmd("lobby", function(src)
if not src.is_console then
src:connect("lobby")
end
end)
every(20 * 60, function()
log.info(players.count() .. " players online")
end)Three differences from a standalone Lua program:
- The standard library is trimmed.
io,debug,dofile,loadfileandcollectgarbagedo not exist, andoshas noexecute,exit,getenv,remove,rename,tmpnameorsetlocale. A plugin cannot reach files or processes of the machine. - Every entry into your code has an instruction budget, so a loop that never ends is stopped and disables the plugin instead of freezing the proxy.
- All plugins share one thread. A handler never runs at the same time as another, and it must be short. To pause
without blocking everyone else, call
wait, see Plugin API.
Where to go next#
| You want | Read |
|---|---|
| To package, configure and load a plugin | Plugins |
| To see every function a plugin can call | Plugin API |
| To react to players and pings | Plugin events |
| A finished plugin, step by step | Worked example |
| To compare with a Java Velocity plugin | Coming from Java? (Velocity) |
| To compare with a Java BungeeCord plugin | Coming from Java? (BungeeCord) |
| The full Lua language | The Lua 5.5 reference manual |