Docs

Lua API

Everything a companion script can call, including memory reads, sprite decoding, modules and limits.

A pack's script is Lua 5.4, run by GBA Duo in a small sandbox. It reads the game's memory and returns the values the layout shows.

update(ui)

GBA Duo calls the global update function tickHz times a second (from the manifest, 4 by default, up to 30). The table it returns becomes the snapshot the layout binds to.

lua
function update(ui)
  return {
    hp = mem.u16(0x02001234),
    party = { { name = "Red", level = 5 } },
  }
end
  • ui is the UI state: the shown page and any keys taps set. It's also available as the global ui.
  • Return strings, numbers, booleans, and tables of them, nested up to 32 levels. A table with keys 1..n becomes an array (for list); other tables become objects.
  • A ui entry in the returned table changes pages instead of becoming data.
  • If update() throws, the snapshot gets _scriptError with the message, and the error is logged under EmuThread.

Script-level code (outside functions) runs once when the pack loads. Use it for constants and lookup tables; locals there keep their values between ticks.

Reading memory

All reads are little-endian, from the GBA's address space: EWRAM (0x02000000), IWRAM (0x03000000), I/O, and the cartridge ROM (0x08000000).

Function Returns
mem.u8(addr), mem.s8(addr) Unsigned / signed 8-bit value
mem.u16(addr), mem.s16(addr) Unsigned / signed 16-bit value
mem.u32(addr), mem.s32(addr) Unsigned / signed 32-bit value
mem.read(addr, n) n raw bytes as a Lua string (at most 65,536)

Reads return 0 when no game is loaded.

lua
-- A pointer, then a field behind it
local sb1 = mem.u32(0x03005008)
local money = mem.u32(sb1 + 0x290)

-- A block of bytes, decoded with string.unpack
local raw = mem.read(sb1 + 0x310, 4)
local itemId, qty = string.unpack("<I2I2", raw)

lz77(addr)

Decompresses GBA BIOS-style LZ77 data (type 0x10) starting at addr, usually in ROM, and returns it as a string. Output is limited to 1 MB; invalid data raises an error.

gfx.decode

Decodes 4bpp GBA tile graphics into an image the layout can show. Returns a key like "gfx:1a2b3c4d" to use as an image src.

lua
-- Example addresses: find the real ones for your game
local icon = gfx.decode({
  tiles = 0x083D4294,   -- address of the tile data
  palette = 0x083D3740, -- address of a 16-color BGR555 palette
  width = 32,           -- multiples of 8, at most 256
  height = 32,
  lz77 = false,         -- true if the tile data is LZ77-compressed
})

return { icon = icon }  -- layout: { "type": "image", "src": "$icon" }
  • Only bpp = 4 is supported. Palette index 0 is transparent.
  • Decoded images are cached, so calling it each tick with the same arguments is cheap.

pack.require(name)

Loads <name>.lua from the pack's folder, once, and returns what it returns. Use it to split a pack into modules or ship data tables.

lua
-- item_names.lua
return { [1] = "Master Ball", [2] = "Ultra Ball" }

-- main.lua
local ITEM_NAMES = pack.require("item_names")

Names may use letters, digits, _, - and ., with no path separators or .., so a pack can only load its own files.

log(message)

Writes to the Android log under the GBADuoPack tag:

sh
adb logcat -s GBADuoPack

print is not available; use log.

actions

Functions in the global actions table can be called from a tap with { "call": "name" }. See Script actions.

The sandbox

Available Removed
Base functions, string, table, math, utf8 io, os, require, load, loadfile, dofile, print, collectgarbage, string.dump

Limits that keep a buggy script from stalling the game:

  • Memory: 16 MB for the script.
  • Time: about 5 million Lua instructions per tick. A script that runs over is stopped for that tick, and the error goes to _scriptError.

A script can't touch files, the network or anything outside the game. That's why packs are safe to share.

Without a script

Very simple packs can skip Lua and read fixed addresses straight from the manifest (the addresses below are examples):

json
"values": {
  "money": { "addr": "0x02025A0C", "type": "u32" },
  "badges": { "addr": "0x02025B2C", "type": "u8", "mask": "0x0F" },
  "hp": { "ptr": "0x03005008", "offset": 36, "type": "u16" }
}
Field
addr Address to read (hex string), or
ptr + offset read a 32-bit pointer at ptr, then read at pointer + offset
type u8, s8, u16, s16, u32 or s32
mask, shift Optional: AND with mask (hex), then shift right by shift

When a pack has both, values from update() win on the same key.