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.
function update(ui)
return {
hp = mem.u16(0x02001234),
party = { { name = "Red", level = 5 } },
}
enduiis the UI state: the shown page and any keys taps set. It's also available as the globalui.- 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
uientry in the returned table changes pages instead of becoming data. - If
update()throws, the snapshot gets_scriptErrorwith the message, and the error is logged underEmuThread.
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.
-- 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.
-- 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 = 4is 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.
-- 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:
adb logcat -s GBADuoPackprint 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):
"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.