---
name: mgba-scripting
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

```lua
-- emu: CoreAdapter instance (available when game loaded)
-- C: Exported constants
-- callbacks: CallbackManager instance
-- console: Console instance
-- util: Basic utility library
-- socket: TCP socket library
```

### Console Output

```lua
console.log("Info message")
console.warn("Warning message")
console.error("Error message")

-- Create text buffer
buffer = console.createBuffer("My Buffer")
buffer:print("Text in buffer")
buffer:clear()
```

### Utility Functions

```lua
-- Expand bitmask to list
bits = util.expandBitmask(0xFF)  -- {0,1,2,3,4,5,6,7}

-- Make bitmask from list
mask = util.makeBitmask({0, 3, 5})  -- 0x29
```

## Core API

### ROM Operations

```lua
-- Load ROM file
success = emu.loadFile("path/to/rom.gba")

-- Get game info
title = emu.getGameTitle()  -- "POKEMON FIRE"
code = emu.getGameCode()   -- "AGB-P-FE"
size = emu.romSize()       -- ROM size in bytes
platform = emu.platform()  -- 0=GBA, 1=GB
checksum = emu.checksum()  -- CRC32
```

### Save States

```lua
-- Save to file
emu.saveStateFile("path/to/state.state")

-- Load from file
emu.loadStateFile("path/to/state.state")

-- Save to slot (0-9)
emu.saveStateSlot(0)
emu.loadStateSlot(0)

-- Save/load buffer
buffer = emu.saveStateBuffer()
emu.loadStateBuffer(buffer)

-- Flags: SCREENSHOT=1, SAVEDATA=2, CHEATS=4, RTC=8, METADATA=16
-- ALL = 31
emu.saveStateSlot(0, 31)  -- Save everything
```

### Frame Control

```lua
-- Run one frame
emu.runFrame()

-- Run one instruction
emu.step()

-- Get current frame number
frame = emu.currentFrame()

-- Get cycle info
cycles = emu.frameCycles()    -- Cycles per frame
freq = emu.frequency()         -- Cycles per second
```

### Input Handling

```lua
-- Set all keys at once
emu.setKeys(0x0000)  -- No keys

-- Add keys (OR with existing)
emu.addKeys(C.GBA_KEY.A + C.GBA_KEY.R)

-- Clear keys
emu.clearKeys(C.GBA_KEY.B)

-- Check key state
if emu.getKey(C.GBA_KEY.UP) == 1 then
    print("UP is pressed")
end

-- Get all pressed keys
keys = emu.getKeys()
```

### GBA Key Constants

```lua
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

```lua
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

```lua
-- Read 8/16/32 bit values
value8 = emu.read8(address)
value16 = emu.read16(address)
value32 = emu.read32(address)

-- Read range
data = emu.readRange(address, length)

-- Read from memory domain
rom = emu.memory["cart0"]
value = rom:read8(offset)
data = rom:readRange(offset, length)
```

### Writing Memory

```lua
-- Write 8/16/32 bit values
emu.write8(address, value)
emu.write16(address, value)
emu.write32(address, value)
```

### Memory Domains (GBA)

```lua
-- Available memory domains
emu.memory["bios"]    -- BIOS (0x00000000)
emu.memory["wram"]     -- EWRAM (0x02000000)
emu.memory["iwram"]    -- IWRAM (0x03000000)
emu.memory["io"]       -- MMIO (0x04000000)
emu.memory["palette"]  -- Palette (0x05000000)
emu.memory["vram"]     -- VRAM (0x06000000)
emu.memory["oam"]      -- OAM (0x07000000)
emu.memory["cart0"]    -- ROM (0x08000000)
emu.memory["cart1"]    -- ROM WS1 (0x0a000000)
emu.memory["cart2"]    -- ROM WS2 (0x0c000000)
```

### Memory Domains (GB)

```lua
emu.memory["cart0"]  -- ROM Bank ($0000)
emu.memory["vram"]   -- VRAM ($8000)
emu.memory["sram"]   -- SRAM ($a000)
emu.memory["wram"]   -- WRAM ($c000)
emu.memory["oam"]    -- OAM ($fe00)
emu.memory["io"]     -- MMIO ($ff00)
emu.memory["hram"]   -- HRAM ($ff80)
```

## Registers

### GBA ARM Registers

```lua
-- Read register
value = emu.readRegister("r0")
emu.readRegister("pc")
emu.readRegister("sp")
emu.readRegister("lr")

-- Write register
emu.writeRegister("r0", 0x12345678)
emu.writeRegister("pc", 0x08000000)
```

### Register Names (GBA)

```lua
-- General purpose: r0-r12
-- Special: sp (r13), lr (r14), pc (r15), cpsr
```

## Callbacks

### Adding Callbacks

```lua
-- Add callback (returns callback ID)
id = callbacks.add("frame", function()
    -- Called every frame
end)

id = callbacks.add("start", function()
    -- Called when emulation starts
end)

id = callbacks.add("reset", function()
    -- Called when emulation resets
end)

id = callbacks.add("shutdown", function()
    -- Called when emulation stops
end)

-- Remove callback
callbacks.remove(id)
```

### Available Callbacks

```lua
-- alarm     - In-game alarm went off
-- crashed   - Emulation crashed
-- frame     - Frame finished
-- keysRead  - About to read key input
-- reset     - Emulation reset
-- savedataUpdated - Save data modified
-- sleep     - Entered low-power mode
-- shutdown  - Powered off
-- start     - Started
-- stop      - Voluntarily shut down
```

### Frame Callback Example

```lua
-- Auto-press A every 10 frames
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

```lua
-- Create socket
sock = socket.tcp()

-- Connect (blocking!)
err = sock:connect("192.168.1.100", 1234)
if err then
    print("Error: " .. socket.ERRORS[err])
end

-- Bind for server
server = socket.tcp()
server:bind(nil, 8080)
server:listen(1)
client = server:accept()

-- Send data
sock:send("Hello, world!")

-- Receive data
data = sock:receive(1024)

-- Check if data available
if sock:hasdata() then
    data = sock:receive(256)
end

-- Close
sock:close()
```

### Socket Events

```lua
-- Add event callback
id = sock:add("received", function(data)
    print("Received: " .. data)
end)

id = sock:add("error", function(err)
    print("Error: " .. err)
end)

-- Poll manually
sock:poll()

-- Remove callback
sock:remove(id)
```

## Constants

### Platform

```lua
C.PLATFORM.NONE  = -1
C.PLATFORM.GBA   = 0
C.PLATFORM.GB    = 1
```

### Save State Flags

```lua
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

```lua
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

```lua
-- Infinite health (example address)
local healthAddr = 0x02001234

callbacks.add("frame", function()
    -- Always write max health
    emu.write16(healthAddr, 999)
end)
```

### Auto-Farmer

```lua
-- Press A every 60 frames
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

```lua
-- Watch specific RAM addresses
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

```lua
-- Auto-save every 5 seconds (300 frames at 60fps)
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

```lua
-- Mash A button as fast as possible
callbacks.add("frame", function()
    emu.addKeys(C.GBA_KEY.A)
    emu.clearKeys(C.GBA_KEY.A)
end)
```

### RNG Manipulation

```lua
-- Advance RNG for shiny hunting
-- Example: GBA games often use LCG at specific address
local rngAddr = 0x02001234  -- Replace with actual address

callbacks.add("frame", function()
    local rng = emu.read32(rngAddr)
    -- Simple LCG: new = (old * 0x41C64E6D + 0x6073) & 0xFFFFFFFF
    local newRng = (rng * 0x41C64E6D + 0x6073) & 0xFFFFFFFF
    emu.write32(rngAddr, newRng)
end)
```

### Screenshot Capture

```lua
-- Capture screenshot every 1000 frames
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

```lua
-- Bad: Keys stay pressed
callbacks.add("frame", function()
    emu.addKeys(C.GBA_KEY.A)
end)

-- Good: Clear after use
callbacks.add("frame", function()
    emu.addKeys(C.GBA_KEY.A)
    emu.clearKeys(C.GBA_KEY.A)
end)
```

### 2. Use Frame Callback for Input

```lua
-- Input should be handled in frame callback
callbacks.add("frame", function()
    if btnp(6) then  -- UP pressed this frame
        -- Handle input
    end
end)
```

### 3. Watch for Crashes

```lua
callbacks.add("crashed", function()
    console.error("Emulation crashed!")
    -- Save state before exit
    emu.saveStateFile("crash.state")
end)
```

### 4. Reset State on Script Load

```lua
-- Clear any previous state when loading
emu.clearKeys(0xFFFF)
callbacks.remove(cbid)  -- Remove old callbacks
```

## Common Issues

### Address Not Found

```lua
-- Some games use different RAM locations
-- Use mGBA's cheat search or memory viewer to find correct addresses
```

### Keys Not Working

```lua
-- Some games poll keys differently
-- Try using addKeys instead of setKeys
emu.addKeys(C.GBA_KEY.A)  -- OR with existing
```

### Socket Connection Timeout

```lua
-- Socket connect is blocking!
-- Use connect with timeout or run in separate thread
-- For async, use callbacks and poll()
```

## MemoryDomain Class

Memory domains provide direct access to specific memory regions (ROM, RAM, etc.).

### Methods

```lua
-- Get domain info
local rom = emu.memory["cart0"]
local baseAddr = rom:base()      -- Base address
local boundAddr = rom:bound()    -- End address (exclusive)
local size = rom:size()          -- Size in bytes
local name = rom:name()          -- Human-readable name

-- Read from domain
value8 = rom:read8(offset)
value16 = rom:read16(offset)
value32 = rom:read32(offset)
data = rom:readRange(offset, length)

-- Write to domain (RAM only, not ROM)
local wram = emu.memory["wram"]
wram:write8(offset, 0xFF)
wram:write16(offset, 0xFFFF)
wram:write32(offset, 0xFFFFFFFF)
```

### Example: ROM Analysis

```lua
-- Read ROM header
local rom = emu.memory["cart0"]

-- Nintendo logo starts at 0x04
local logo = rom:readRange(0x04, 156)

-- Game title at 0xA0 (12 bytes)
local title = rom:readRange(0xA0, 12)
console.log("Game: " .. title)

-- Game code at 0xAC (4 bytes)
local code = rom:readRange(0xAC, 4)
console.log("Code: " .. code)

-- Maker code at 0xB0 (2 bytes)
local maker = rom:readRange(0xB0, 2)
console.log("Maker: " .. maker)
```

## TextBuffer Class

Create custom text buffers for displaying information.

### Methods

```lua
-- Create buffer
local buf = console.createBuffer("My Buffer")

-- Set size
buf:setSize(80, 25)  -- 80 columns, 25 rows

-- Get dimensions
local cols = buf:cols()
local rows = buf:rows()

-- Print text
buf:print("Hello, World!")

-- Move cursor
buf:moveCursor(10, 5)  -- x=10, y=5

-- Get cursor position
local x = buf:getX()
local y = buf:getY()

-- Advance cursor
buf:advance(5)  -- Move 5 columns right

-- Clear buffer
buf:clear()

-- Set visible name
buf:setName("Stats Display")
```

### Example: Stats Display

```lua
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

```lua
-- 8-bit registers
emu.readRegister("a")   -- Accumulator
emu.readRegister("f")   -- Flags
emu.readRegister("b")
emu.readRegister("c")
emu.readRegister("d")
emu.readRegister("e")
emu.readRegister("h")
emu.readRegister("l")

-- 16-bit register pairs
emu.readRegister("bc")
emu.readRegister("de")
emu.readRegister("hl")
emu.readRegister("af")
emu.readRegister("pc")  -- Program counter
emu.readRegister("sp")  -- Stack pointer
```

### Example: GB Register Watch

```lua
callbacks.add("frame", function()
    -- Watch GB registers
    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

```lua
-- Find all occurrences of a value in RAM
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

-- Usage
local addresses = searchMemory(100, 2)  -- Find 100 as 16-bit
for _, addr in ipairs(addresses) do
    console.log(string.format("Found at 0x%08X", addr))
end
```

### Cheat Engine

```lua
-- Simple cheat engine with multiple cheats
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)

-- Toggle cheat
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

```lua
-- Frame counter and IGT (In-Game Time) tracker
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

```lua
-- Record and playback inputs
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

```lua
-- Sync game state over network
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(),
        -- Add more state as needed
    }
    
    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

- **Official Documentation**: https://mgba.io/docs/scripting.html
- **mGBA GitHub**: https://github.com/mgba-emu/mgba
- **Forums**: https://forums.mgba.io/
- **Discord**: https://discord.gg/em2M2sG
- **Scripting API Reference**: https://mgba.io/docs/scripting.html