Getting started with Lua!

The whole language in one sitting: values, tables, functions, and the handful of Cheat Engine calls that make a table script tick.

Every Cheat Table I ship contains Lua. Teleportation, UI modifications, persistent States, all systems made in Lua. It is also, conveniently, one of the smallest real programming languages in existence: the whole thing fits in an afternoon. This post is that afternoon.

Assumption

No programming experience required. If you have written code in any other language, skim the first half and slow down at Tables, that is where Lua stops looking like everything else.

Running Lua#

You do not need to install anything. Cheat Engine ships with a complete Lua environment: open the Memory Viewer and pick Tools → Lua Engine. The window has an output pane at the top and an input box below. Type a line, press execute, and whatever you print lands in the output pane.

Lua Engine
print("hello from Cheat Engine") print(2 + 3) print(0x10) -- hex literals work: prints 16

Everything in this post runs in that window. Later, the same code moves into table scripts and hotkeys unchanged, the language is identical, only the place you store it differs.

Values and types#

Lua has exactly eight types, and you can ignore three of them for now. The five that matter:

  • nil, the absence of a value. Anything unset is nil.
  • boolean, true or false.
  • number, integers and floats in one type. 10, 3.5, 0xDEADBEEF.
  • string, text in single or double quotes. Immutable.
  • table, the one and only data structure. More on this below.

type(x) tells you what something is, and is worth using liberally while learning:

Lua Engine
print(type(42)) -- number print(type("42")) -- string print(type(nil)) -- nil print(type(print)) -- function (yes, functions are values)

Variables and scope#

A bare assignment creates a global variable. Adding local creates one that only exists in the current block. This distinction is the source of roughly half of all Lua bugs, so build the habit on day one: everything gets local unless you have a reason for it not to.

Lua Engine
local health = 100 -- lives in this script only score = 9999 -- global: visible to every script, forever local function heal(amount) health = health + amount -- finds the local above end

In Cheat Engine this matters more than usual: every table script and the Lua Engine share one global environment. A stray global called timer in one script will happily collide with a stray global called timer in another, and the resulting bug reports are not fun to read.

Strings and numbers#

Strings concatenate with .., not +. Numbers convert to strings automatically when concatenated, and string.format handles anything fancier:

Lua Engine
local name = "Player" print(name .. " has " .. 100 .. " HP") -- printf-style formatting; %X is uppercase hex print(string.format("address: %X, value: %d", 0x7FF6A0001234, 87)) -- string to number and back print(tonumber("1337")) -- 1337 print(tonumber("FF", 16)) -- 255 (base 16) print(tostring(3.14)) -- "3.14"

A few string functions you will actually use: string.sub(s, from, to), string.find(s, pattern), string.lower(s), and #s for the length. Every one of them is also callable method-style: s:lower() is the same as string.lower(s).

Control flow#

Lua Engine
local hp = 35 if hp <= 0 then print("dead") elseif hp < 50 then print("low") else print("fine") end -- numeric for: start, limit, optional step for i = 1, 5 do print("wave " .. i) end for i = 10, 2, -2 do print(i) -- 10, 8, 6, 4, 2 end -- while and repeat local tries = 0 while tries < 3 do tries = tries + 1 end repeat tries = tries - 1 until tries == 0

Comparison is ==, inequality is ~= (not !=). Logic is spelled out: and, or, not. There is break but no continue, you restructure with an if instead, and honestly, the code usually reads better for it.

Truthiness

Only nil and false count as false. Zero is true. An empty string is true. If you come from C, if damage then does not check for non-zero, it checks for non-nil.

Tables, the only data structure you get#

Arrays, dictionaries, objects, namespaces: in Lua they are all the same thing, a table. Once this clicks, the whole language clicks.

As an array#

Lua Engine
local weapons = { "Hammer", "Bow", "Gun Lance" } print(weapons[1]) -- "Hammer", Lua counts from 1, not 0 print(#weapons) -- 3 table.insert(weapons, "Insect Glaive") -- append table.remove(weapons, 2) -- remove "Bow", closes the gap

As a dictionary#

Lua Engine
local player = { name = "Hunter", hp = 100, ["max hp"] = 150, -- keys with spaces need brackets } print(player.name) -- dot access print(player["max hp"]) -- bracket access, same table player.stamina = 75 -- new keys appear on assignment player.hp = nil -- assigning nil deletes the key

Iterating#

Lua Engine
-- ipairs: the array part, in order, stops at the first hole for index, value in ipairs(weapons) do print(index, value) end -- pairs: every key, in no guaranteed order for key, value in pairs(player) do print(key, value) end
The # caveat

#t is only reliable on gap-free arrays. The moment you write t[5] = nil in the middle of one, the length operator may return anything. If you need to delete from an array, use table.remove, it closes the gap.

Functions#

Functions are plain values: you can store them in variables, in tables, and pass them around. Two features surprise newcomers, multiple return values and variadic arguments:

Lua Engine
local function clamp(value, low, high) if value < low then return low end if value > high then return high end return value end -- multiple returns local function minmax(t) local lo, hi = t[1], t[1] for _, v in ipairs(t) do lo = math.min(lo, v) hi = math.max(hi, v) end return lo, hi end local low, high = minmax({ 7, 2, 9, 4 }) print(low, high) -- 2 9 -- varargs local function sum(...) local total = 0 for _, n in ipairs({ ... }) do total = total + n end return total end print(sum(1, 2, 3, 4)) -- 10

When things go wrong#

An error in Lua stops the script and prints a message with a line number , read it, it is nearly always literally correct. The classic one:

Lua Engine
local player = nil print(player.hp) -- attempt to index a nil value (local 'player')

When failure is expected, a game that is not running, an address that does not resolve, wrap the risky call in pcall instead of letting the script die:

Lua Engine
local ok, result = pcall(function() return readInteger(getAddress("game.exe+1234")) end) if ok then print("value: " .. result) else print("could not read: " .. tostring(result)) end

Lua inside Cheat Engine#

Everything above is standard Lua. Cheat Engine adds a few hundred functions on top that talk to the attached process. These four cover a surprising amount of ground:

Lua Engine
-- resolve a symbol/pointer expression to an address local addr = getAddress("Tutorial-x86_64.exe+325B00") -- read and write memory (Integer, Float, Double, Bytes, String variants exist) local value = readInteger(addr) writeInteger(addr, value + 100) print(string.format("%X -> %d", addr, readInteger(addr)))

Your cheat table is scriptable too. Every entry in the address list is a memory record you can toggle from code, which is exactly how master hotkeys work:

Table Lua script
local list = getAddressList() local record = list.getMemoryRecordByDescription("Disable : Stamina Usage") createHotkey(function() record.Active = not record.Active print("Stamina usage: " .. (record.Active and "disabled" or "normal")) end, VK_F6)
The real reference

The complete list of Cheat Engine's Lua functions lives in celua.txt, right next to your Cheat Engine installation. It is a plain text file and it is the single most useful document for table scripting, search it before searching the web.

The classic beginner traps#

  • Indexing starts at 1. weapons[0] is not an error, it is quietly nil, which is worse.
  • = assigns, == compares. if x = 5 then is a syntax error; at least that one fails loudly.
  • Zero and empty strings are true. Only nil and false are false.
  • Forgotten local leaks globals. In Cheat Engine, all scripts share them.
  • .. concatenates, + adds. "10" + 5 is 15 because strings coerce to numbers, convenient right up until it hides a bug.
  • ~=, not !=. Muscle memory from other languages loses this one for a week.

Where to go from here#

You now know more Lua than most table scripts ever use. To go deeper: