Skip to content

Latest commit

 

History

History
985 lines (773 loc) · 20.2 KB

File metadata and controls

985 lines (773 loc) · 20.2 KB
name mgba-scripting
description Lua scripting for mGBA emulator - game automation, memory hacking, cheats, callbacks, and ROM manipulation
metadata
author version tags
mte90
1.0.0
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

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

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

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

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

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

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

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

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

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

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

Memory Domains (GBA)

-- 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)

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

-- 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)

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

Callbacks

Adding Callbacks

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

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

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

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

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

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

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

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

Auto-Farmer

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

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

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

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

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

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

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

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

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

4. Reset State on Script Load

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

Common Issues

Address Not Found

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

Keys Not Working

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

Socket Connection Timeout

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

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

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

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

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

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

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

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

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

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

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

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