| name | mgba |
| description | Lua scripting for mGBA emulator - game automation, memory hacking, cheats, callbacks, and ROM manipulation |
| metadata | {"author":"mte90","version":"1.0.0","tags":["lua","emulator","gba","gameboy-advance","scripting","memory-hacking"]} |
mGBA Scripting
Lua scripting for mGBA emulator.
Overview
Starting with version 0.10, mGBA has built-in scripting capabilities. To use scripting, click "Scripting..." from the Tools menu. Currently, only Lua scripting is supported.
Key Features:
- Full memory access (ROM, RAM, MMIO)
- Input manipulation (button presses)
- Save state management
- Callbacks for frame/events
- TCP socket networking
- Console output
- Screenshot capture
Opening Scripting Console
Tools → Scripting...
This opens a console where you can load and run Lua scripts.
Top-Level Objects
Available Objects
Console Output
console.log("Info message")
console.warn("Warning message")
console.error("Error message")
buffer = console.createBuffer("My Buffer")
buffer:print("Text in buffer")
buffer:clear()
Utility Functions
bits = util.expandBitmask(0xFF)
mask = util.makeBitmask({0, 3, 5})
Core API
ROM Operations
success = emu.loadFile("path/to/rom.gba")
title = emu.getGameTitle()
code = emu.getGameCode()
size = emu.romSize()
platform = emu.platform()
checksum = emu.checksum()
Save States
emu.saveStateFile("path/to/state.state")
emu.loadStateFile("path/to/state.state")
emu.saveStateSlot(0)
emu.loadStateSlot(0)
buffer = emu.saveStateBuffer()
emu.loadStateBuffer(buffer)
emu.saveStateSlot(0, 31)
Frame Control
emu.runFrame()
emu.step()
frame = emu.currentFrame()
cycles = emu.frameCycles()
freq = emu.frequency()
Input Handling
emu.setKeys(0x0000)
emu.addKeys(C.GBA_KEY.A + C.GBA_KEY.R)
emu.clearKeys(C.GBA_KEY.B)
if emu.getKey(C.GBA_KEY.UP) == 1 then
print("UP is pressed")
end
keys = emu.getKeys()
GBA Key Constants
C.GBA_KEY.A = 0
C.GBA_KEY.B = 1
C.GBA_KEY.SELECT = 2
C.GBA_KEY.START = 3
C.GBA_KEY.RIGHT = 4
C.GBA_KEY.LEFT = 5
C.GBA_KEY.UP = 6
C.GBA_KEY.DOWN = 7
C.GBA_KEY.R = 8
C.GBA_KEY.L = 9
Game Boy Key Constants
C.GB_KEY.A = 0
C.GB_KEY.B = 1
C.GB_KEY.SELECT = 2
C.GB_KEY.START = 3
C.GB_KEY.RIGHT = 4
C.GB_KEY.LEFT = 5
C.GB_KEY.UP = 6
C.GB_KEY.DOWN = 7
Memory Access
Reading Memory
value8 = emu.read8(address)
value16 = emu.read16(address)
value32 = emu.read32(address)
data = emu.readRange(address, length)
rom = emu.memory["cart0"]
value = rom:read8(offset)
data = rom:readRange(offset, length)
Writing Memory
emu.write8(address, value)
emu.write16(address, value)
emu.write32(address, value)
Memory Domains (GBA)
emu.memory["bios"]
emu.memory["wram"]
emu.memory["iwram"]
emu.memory["io"]
emu.memory["palette"]
emu.memory["vram"]
emu.memory["oam"]
emu.memory["cart0"]
emu.memory["cart1"]
emu.memory["cart2"]
Memory Domains (GB)
emu.memory["cart0"]
emu.memory["vram"]
emu.memory["sram"]
emu.memory["wram"]
emu.memory["oam"]
emu.memory["io"]
emu.memory["hram"]
Registers
GBA ARM Registers
value = emu.readRegister("r0")
emu.readRegister("pc")
emu.readRegister("sp")
emu.readRegister("lr")
emu.writeRegister("r0", 0x12345678)
emu.writeRegister("pc", 0x08000000)
Register Names (GBA)
Callbacks
Adding Callbacks
id = callbacks.add("frame", function()
end)
id = callbacks.add("start", function()
end)
id = callbacks.add("reset", function()
end)
id = callbacks.add("shutdown", function()
end)
callbacks.remove(id)
Available Callbacks
Frame Callback Example
local counter = 0
callbacks.add("frame", function()
counter = counter + 1
if counter >= 10 then
emu.addKeys(C.GBA_KEY.A)
counter = 0
else
emu.clearKeys(C.GBA_KEY.A)
end
end)
Socket Networking
TCP Socket Basics
sock = socket.tcp()
err = sock:connect("192.168.1.100", 1234)
if err then
print("Error: " .. socket.ERRORS[err])
end
server = socket.tcp()
server:bind(nil, 8080)
server:listen(1)
client = server:accept()
sock:send("Hello, world!")
data = sock:receive(1024)
if sock:hasdata() then
data = sock:receive(256)
end
sock:close()
Socket Events
id = sock:add("received", function(data)
print("Received: " .. data)
end)
id = sock:add("error", function(err)
print("Error: " .. err)
end)
sock:poll()
sock:remove(id)
Constants
Platform
C.PLATFORM.NONE = -1
C.PLATFORM.GBA = 0
C.PLATFORM.GB = 1
Save State Flags
C.SAVESTATE.SCREENSHOT = 1
C.SAVESTATE.SAVEDATA = 2
C.SAVESTATE.CHEATS = 4
C.SAVESTATE.RTC = 8
C.SAVESTATE.METADATA = 16
C.SAVESTATE.ALL = 31
Socket Errors
C.SOCKERR.OK = 0
C.SOCKERR.AGAIN = 1
C.SOCKERR.ADDR_IN_USE = 2
C.SOCKERR.CONN_REFUSED = 3
C.SOCKERR.DENIED = 4
C.SOCKERR.FAILED = 5
C.SOCKERR.NETWORK_UNREACHABLE = 6
C.SOCKERR.NOT_FOUND = 7
C.SOCKERR.NO_DATA = 8
C.SOCKERR.OUT_OF_MEMORY = 9
C.SOCKERR.TIMEOUT = 10
C.SOCKERR.UNSUPPORTED = 11
Examples
Simple Memory Cheat
local healthAddr = 0x02001234
callbacks.add("frame", function()
emu.write16(healthAddr, 999)
end)
Auto-Farmer
callbacks.add("frame", function()
local frame = emu.currentFrame()
if frame % 60 == 0 then
emu.addKeys(C.GBA_KEY.A)
else
emu.clearKeys(C.GBA_KEY.A)
end
end)
RAM Watch
callbacks.add("frame", function()
local hp = emu.read16(0x02001234)
local mp = emu.read16(0x02001236)
console.log(string.format("HP: %d, MP: %d", hp, mp))
end)
Save State Timer
local frameCount = 0
local saveSlot = 0
callbacks.add("frame", function()
frameCount = frameCount + 1
if frameCount >= 300 then
emu.saveStateSlot(saveSlot)
console.log("Auto-saved to slot " .. saveSlot)
frameCount = 0
end
end)
Button Masher
callbacks.add("frame", function()
emu.addKeys(C.GBA_KEY.A)
emu.clearKeys(C.GBA_KEY.A)
end)
RNG Manipulation
local rngAddr = 0x02001234
callbacks.add("frame", function()
local rng = emu.read32(rngAddr)
local newRng = (rng * 0x41C64E6D + 0x6073) & 0xFFFFFFFF
emu.write32(rngAddr, newRng)
end)
Screenshot Capture
callbacks.add("frame", function()
if emu.currentFrame() % 1000 == 0 then
local filename = string.format("screenshot_%04d.png", emu.currentFrame())
emu.screenshot(filename)
end
end)
Best Practices
1. Always Clear Keys
callbacks.add("frame", function()
emu.addKeys(C.GBA_KEY.A)
end)
callbacks.add("frame", function()
emu.addKeys(C.GBA_KEY.A)
emu.clearKeys(C.GBA_KEY.A)
end)
2. Use Frame Callback for Input
callbacks.add("frame", function()
if btnp(6) then
end
end)
3. Watch for Crashes
callbacks.add("crashed", function()
console.error("Emulation crashed!")
emu.saveStateFile("crash.state")
end)
4. Reset State on Script Load
emu.clearKeys(0xFFFF)
callbacks.remove(cbid)
Common Issues
Address Not Found
Keys Not Working
emu.addKeys(C.GBA_KEY.A)
Socket Connection Timeout
MemoryDomain Class
Memory domains provide direct access to specific memory regions (ROM, RAM, etc.).
Methods
local rom = emu.memory["cart0"]
local baseAddr = rom:base()
local boundAddr = rom:bound()
local size = rom:size()
local name = rom:name()
value8 = rom:read8(offset)
value16 = rom:read16(offset)
value32 = rom:read32(offset)
data = rom:readRange(offset, length)
local wram = emu.memory["wram"]
wram:write8(offset, 0xFF)
wram:write16(offset, 0xFFFF)
wram:write32(offset, 0xFFFFFFFF)
Example: ROM Analysis
local rom = emu.memory["cart0"]
local logo = rom:readRange(0x04, 156)
local title = rom:readRange(0xA0, 12)
console.log("Game: " .. title)
local code = rom:readRange(0xAC, 4)
console.log("Code: " .. code)
local maker = rom:readRange(0xB0, 2)
console.log("Maker: " .. maker)
TextBuffer Class
Create custom text buffers for displaying information.
Methods
local buf = console.createBuffer("My Buffer")
buf:setSize(80, 25)
local cols = buf:cols()
local rows = buf:rows()
buf:print("Hello, World!")
buf:moveCursor(10, 5)
local x = buf:getX()
local y = buf:getY()
buf:advance(5)
buf:clear()
buf:setName("Stats Display")
Example: Stats Display
local statsBuf = console.createBuffer("Game Stats")
statsBuf:setSize(40, 10)
statsBuf:setName("Live Stats")
callbacks.add("frame", function()
statsBuf:clear()
local hp = emu.read16(0x02001234)
local mp = emu.read16(0x02001236)
local gold = emu.read32(0x02001238)
statsBuf:moveCursor(0, 0)
statsBuf:print("=== GAME STATS ===\n")
statsBuf:print(string.format("HP: %5d\n", hp))
statsBuf:print(string.format("MP: %5d\n", mp))
statsBuf:print(string.format("Gold: %5d\n", gold))
statsBuf:print("==================")
end)
Game Boy Registers
For Game Boy (DMG/CGB) emulation.
Register Names
emu.readRegister("a")
emu.readRegister("f")
emu.readRegister("b")
emu.readRegister("c")
emu.readRegister("d")
emu.readRegister("e")
emu.readRegister("h")
emu.readRegister("l")
emu.readRegister("bc")
emu.readRegister("de")
emu.readRegister("hl")
emu.readRegister("af")
emu.readRegister("pc")
emu.readRegister("sp")
Example: GB Register Watch
callbacks.add("frame", function()
local a = emu.readRegister("a")
local hl = emu.readRegister("hl")
local pc = emu.readRegister("pc")
console.log(string.format("A=%02X HL=%04X PC=%04X", a, hl, pc))
end)
Advanced Examples
Memory Search
function searchMemory(value, size)
local wram = emu.memory["wram"]
local results = {}
for i = 0, wram:size() - size, size do
local v
if size == 1 then
v = wram:read8(i)
elseif size == 2 then
v = wram:read16(i)
else
v = wram:read32(i)
end
if v == value then
table.insert(results, i)
end
end
return results
end
local addresses = searchMemory(100, 2)
for _, addr in ipairs(addresses) do
console.log(string.format("Found at 0x%08X", addr))
end
Cheat Engine
local cheats = {
{ name = "Infinite HP", addr = 0x02001234, value = 999, size = 2, enabled = true },
{ name = "Max Gold", addr = 0x02001238, value = 999999, size = 4, enabled = true },
{ name = "All Items", addr = 0x02002000, value = 0xFF, size = 1, enabled = false },
}
callbacks.add("frame", function()
for _, cheat in ipairs(cheats) do
if cheat.enabled then
if cheat.size == 1 then
emu.write8(cheat.addr, cheat.value)
elseif cheat.size == 2 then
emu.write16(cheat.addr, cheat.value)
else
emu.write32(cheat.addr, cheat.value)
end
end
end
end)
function toggleCheat(name)
for _, cheat in ipairs(cheats) do
if cheat.name == name then
cheat.enabled = not cheat.enabled
console.log(name .. ": " .. (cheat.enabled and "ON" or "OFF"))
end
end
end
Speedrunner Tools
local startFrame = nil
local lastSplit = nil
local splits = {}
callbacks.add("start", function()
startFrame = emu.currentFrame()
splits = {}
end)
function split(name)
local currentFrame = emu.currentFrame()
local frameTime = currentFrame - (lastSplit or startFrame)
lastSplit = currentFrame
table.insert(splits, {
name = name,
frame = frameTime,
total = currentFrame - startFrame
})
local seconds = frameTime / 60
local totalSeconds = (currentFrame - startFrame) / 60
console.log(string.format("%s: %.2fs (Total: %.2fs)", name, seconds, totalSeconds))
end
function printSplits()
console.log("=== SPLITS ===")
for _, s in ipairs(splits) do
console.log(string.format("%s: %.2fs", s.name, s.frame / 60))
end
console.log("==============")
end
TAS Helper
local recording = {}
local isRecording = false
local isPlaying = false
local playbackFrame = 0
function startRecording()
recording = {}
isRecording = true
console.log("Recording started...")
end
function stopRecording()
isRecording = false
console.log("Recording stopped. " .. #recording .. " frames recorded.")
end
function startPlayback()
isPlaying = true
playbackFrame = 0
console.log("Playback started...")
end
function stopPlayback()
isPlaying = false
console.log("Playback stopped.")
end
callbacks.add("frame", function()
if isRecording then
table.insert(recording, emu.getKeys())
elseif isPlaying then
playbackFrame = playbackFrame + 1
if playbackFrame <= #recording then
emu.setKeys(recording[playbackFrame])
else
isPlaying = false
console.log("Playback complete.")
end
end
end)
Networked Multi-Script
local server = nil
local clients = {}
function startSyncServer(port)
server = socket.tcp()
server:bind(nil, port or 8080)
server:listen(5)
server:add("received", function()
local client = server:accept()
table.insert(clients, client)
console.log("Client connected!")
end)
console.log("Sync server started on port " .. (port or 8080))
end
function broadcastState()
if not server then return end
local state = {
frame = emu.currentFrame(),
keys = emu.getKeys(),
}
local data = string.format("%d,%d\n", state.frame, state.keys)
for i, client in ipairs(clients) do
local err = client:send(data)
if err then
table.remove(clients, i)
end
end
end
callbacks.add("frame", broadcastState)
Complete API Reference
Core Methods Summary
| Method | Description |
|---|
loadFile(path) | Load ROM file |
getGameTitle() | Get ROM title |
getGameCode() | Get ROM code |
romSize() | Get ROM size |
platform() | Get platform (GBA=0, GB=1) |
checksum(type) | Get ROM checksum |
reset() | Reset emulation |
runFrame() | Run one frame |
step() | Run one instruction |
currentFrame() | Get frame number |
frameCycles() | Cycles per frame |
frequency() | Cycles per second |
screenshot(filename) | Save screenshot |
Memory Methods Summary
| Method | Description |
|---|
read8(addr) | Read 8-bit value |
read16(addr) | Read 16-bit value |
read32(addr) | Read 32-bit value |
readRange(addr, len) | Read byte range |
write8(addr, val) | Write 8-bit value |
write16(addr, val) | Write 16-bit value |
write32(addr, val) | Write 32-bit value |
readRegister(name) | Read CPU register |
writeRegister(name, val) | Write CPU register |
Input Methods Summary
| Method | Description |
|---|
setKeys(mask) | Set key bitmask |
addKeys(mask) | Add keys to current |
clearKeys(mask) | Remove keys from current |
addKey(key) | Add single key |
clearKey(key) | Clear single key |
getKey(key) | Get key state |
getKeys() | Get all keys as mask |
Save State Methods Summary
| Method | Description |
|---|
saveStateFile(path, flags) | Save to file |
loadStateFile(path, flags) | Load from file |
saveStateSlot(slot, flags) | Save to slot |
loadStateSlot(slot, flags) | Load from slot |
saveStateBuffer(flags) | Save to buffer |
loadStateBuffer(buf, flags) | Load from buffer |
autoloadSave() | Load associated save |
References