agentsclimarketplace

Fceux lua

Skill Yuki001/nes-skills/skills/fceux-lua

Write and run FCEUX Lua scripts for NES ROM automation — screenshots, PPU memory dumps, automated input, code execution tracing, and API discovery. Use when the user wants to automate FCEUX, capture ROM data, dump CHR/nametable/palette, trace code execution, or script NES emulation tasks. Triggers on mentions of FCEUX, Lua scripting for NES, ROM analysis automation, ppu.readbyte, memory.registerexec, gui.savescreenshotas, joypad.set.From its SKILL.md

Install
npx -y skills add Yuki001/nes-skills --skill fceux-lua

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.6 KB, ~2.7k tokens by cl100k_base, as published. Nobody here has run it

FCEUX Lua Scripting

Write Lua scripts for FCEUX (fceux64.exe -lua script.lua rom.nes) to automate NES ROM analysis tasks.

FCEUX binary

The skill bundles FCEUX 2.6.6 for Windows at bin/fceux-2.6.6-win64.zip.

If the project already has FCEUX installed (e.g. tools/fceux/fceux64.exe), use that. If not, extract the skill's bundled zip:

# Linux/WSL
unzip .claude/skills/fceux-lua/bin/fceux-2.6.6-win64.zip -d tools/fceux/

# Windows (PowerShell)
Expand-Archive .claude/skills/fceux-lua/bin/fceux-2.6.6-win64.zip -DestinationPath tools/fceux/

The zip contains fceux64.exe and required DLLs (lua5.1.dll, 7z_64.dll, etc.).

Critical Lua syntax for FCEUX

-- FCEUX uses LuaJIT. Key gotchas:
-- 1. Callbacks take FUNCTION REFERENCES, not strings:
memory.registerexec(0x8000, 0x80FF, my_callback)        -- CORRECT
memory.registerexec(0x8000, 0x80FF, "my_callback")       -- WRONG: bad argument #3, need function

-- 2. Print goes to console, often invisible. Write to files instead:
local log = io.open("output.txt", "w")
log:write("message\n"); log:flush()

-- 3. Non-ASCII text may crash on Windows GBK terminal. Use ASCII labels.

-- 4. Callback functions must be global, not local.

Script template

local log = io.open("script_log.txt", "w")
local function p(msg) log:write(msg .. "\n"); log:flush() end

-- ... script body ...

p("Done. Total frames: " .. emu.framecount())
log:close()
emu.exit()  -- always exit cleanly

Run: ./fceux64.exe -lua script.lua "../../path/to/rom.nes"

Common Tasks

1. Screenshots at specific frames

-- Single screenshot after 2 seconds
local target = 120  -- 60fps * 2
while emu.framecount() < target do emu.frameadvance() end
gui.savescreenshotas("screenshot.png")
emu.exit()

-- Multiple screenshots during animation
local shots = {120, 240, 360, 480, 600, 720}
for _, frame in ipairs(shots) do
    while emu.framecount() < frame do emu.frameadvance() end
    gui.savescreenshotas("shot_" .. math.floor(frame/60) .. "s.png")
end

Adjust the frame count based on the ROM's boot time. For ROMs with long intro animations, capture up to 900+ frames (15 seconds).

2. Dump PPU memory (CHR-RAM, nametables, palettes)

-- Wait for ROM to reach stable state (adjust frame count as needed)
for i = 1, 600 do emu.frameadvance() end

-- Helper: dump byte range to file
local function dump(ppu_addr, len, filename)
    local f = io.open(filename, "wb")
    for a = ppu_addr, ppu_addr + len - 1 do
        f:write(string.char(ppu.readbyte(a)))
    end
    f:close()
end

dump(0x0000, 8192, "chr_ram.bin")    -- CHR-RAM 8KB (pattern tables)
dump(0x2000, 1024, "nt0.bin")         -- Nametable 0
dump(0x2400, 1024, "nt1.bin")         -- Nametable 1 (horizontal mirroring)
dump(0x3F00, 32,   "palette.bin")     -- Palette 32B

emu.exit()

Note: ppu.readbyte() may not exist on older FCEUX builds. Use memory.readbyte() for CPU-addressable ranges as fallback.

For CHR-ROM carts (non-CHR-RAM), read from the ROM directly with rom.readbyterange().

3. Automated input simulation

-- Wait for ROM to reach interactive state
for i = 1, 480 do emu.frameadvance() end

-- Press sequence with hold/release timing
local keys = {"D","D","D","D","D","U","U","U","R","D","D","L","U"}
for _, key in ipairs(keys) do
    joypad.set(1, {[key]=true})
    for i = 1, 4 do emu.frameadvance() end    -- hold 4 frames
    joypad.set(1, {[key]=false})
    for i = 1, 3 do emu.frameadvance() end    -- release 3 frames
end
emu.exit()

Valid key strings: "A", "B", "Select", "Start", "U" (Up), "D" (Down), "L" (Left), "R" (Right).

Use joypad.set(2, ...) for player 2 controller. Use joypad.getdown(1) to read which buttons were just pressed (useful for verifying input took effect).

4. API Discovery

To find what Lua APIs are available in the current FCEUX build:

local log = io.open("api_log.txt", "w")
local function p(msg) log:write(msg .. "\n"); log:flush() end

for i = 1, 300 do emu.frameadvance() end

local namespaces = {
    "emu", "gui", "joypad", "memory", "ppu", "sound",
    "debugger", "rom", "cdl", "movie", "zapper", "input"
}
for _, ns in ipairs(namespaces) do
    local ok, tbl = pcall(function() return _G[ns] end)
    if ok and tbl and type(tbl) == "table" then
        p("=== " .. ns .. " ===")
        for k, v in pairs(tbl) do
            local vt = type(v)
            if vt == "function" then
                p("  " .. k .. "()")
            elseif vt == "number" or vt == "string" or vt == "boolean" then
                p("  " .. k .. " = " .. tostring(v))
            end
        end
    end
end
log:close()
emu.exit()

Run this whenever you need to check API availability on an unfamiliar FCEUX version. See references/api-reference.md for a snapshot of FCEUX 2.6.4 Windows x64.

5. Code Execution Tracing with memory.registerexec

Track which CPU addresses are executed. Use a sparse bitmap in the callback — any I/O or string formatting inside the callback will make it unusably slow:

local exec = {}  -- exec[cpu_addr] = true

-- Callback must be a GLOBAL function
function on_code(addr)
    exec[addr] = true
end

-- Register per 256-byte page
for page = 0x80, 0xFF do  -- $8000-$FFFF
    memory.registerexec(page * 256, page * 256 + 255, on_code)
end

-- Run (use fewer frames than usual — callbacks are expensive)
for i = 1, 120 do emu.frameadvance() end

-- Write results (do this after emulation, not inside callback)
local f = io.open("executed.txt", "w")
for addr, _ in pairs(exec) do
    f:write(string.format("$%04X\n", addr))
end
f:close()
emu.exit()

Performance: Per-instruction callbacks are extremely slow. Expect 30-120 second runtimes even for 120-600 frames. Keep the callback body minimal — no function calls, no string ops, no I/O. Write results only after emulation stops.

Mapper compatibility: Address-based bank switching mappers (where writes to PRG-ROM space trigger bank changes) may prevent registerexec from firing on the $8000-$BFFF range. If $8000 range gets zero hits but $C000 range works, this is the likely cause. For these mappers, trace only $C000-$FFFF or use CDL (Task 6) instead.

6. CDL (Code/Data Logger) for Perfect Code/Data Separation

CDL marks every PRG-ROM byte as CODE, DATA, PCM audio, or unaccessed. Cannot be programmatically enabled from Lua — requires manual GUI interaction:

  1. Open FCEUX → Debug → Code/Data Logger (opens the CDL window)
  2. Then load ROM or press Hard Reset (logging must be active before first instruction executes)
  3. Exercise code paths (navigate menus, trigger gameplay, etc.)
  4. Close ROM — CDL auto-saves to romname.cdl (requires autosaveCDL 1 in fceux.cfg)

CDL file format:

  • 1 byte per PRG-ROM byte (no header — file size = PRG-ROM size)
  • Bit 0 ($01): CODE (byte was fetched as instruction)
  • Bit 1 ($02): DATA (byte was read as data)
  • Bit 2 ($04): PCM audio
  • All zeros = unaccessed during session

Check CDL quality:

python3 -c "
with open('rom.cdl', 'rb') as f:
    cdl = f.read()
print(f'Size: {len(cdl)} bytes')
print(f'Code bytes: {sum(1 for b in cdl if b & 1)}')
print(f'Data bytes: {sum(1 for b in cdl if b & 2)}')
print(f'Total non-zero: {sum(1 for b in cdl if b)}')
"

Use CDL with retrodisasm for perfect code/data separation:

retrodisasm -s nes -cdl rom.cdl -o output.asm rom.nes

Scoping tip: If you only care about specific PRG banks (e.g. the fixed bank and one switchable bank), verify those banks have non-zero CDL entries. Banks that are all zero were never mapped during the session. You may need multiple CDL sessions to cover everything.

Mapper limitation: CDL maps to physical ROM bytes. With bank switching mappers, verify the CDL covers all banks you need. Some mappers may cause phantom CDL entries (single-byte DATA markers at regular intervals across every bank) — these are mapper noise, not real accesses. Real code/data shows concentrated blocks of entries.

Common Pitfalls

IssueCauseFix
registerexec callback never firesFunction passed as string "name"Pass function reference directly: on_exec
$8000-$BFFF range gets zero hitsAddress-based mapper writes interfere with exec detectionTrace only $C000-$FFFF or use CDL
CDL all zeros after sessionCDL Logger window not opened before ROM executionOpen GUI window first, then Hard Reset
CDL has only phantom entries (single bytes every ~251 bytes)CDL logging was not active; phantom is noiseEnsure CDL Logger was open DURING code execution
print() produces no outputFCEUX suppresses Lua stdoutWrite to file: io.open("log.txt","w")
Lua syntax error at for x, y, z in ...Lua uses ipairs(), not Python-style tuple iterationfor _, r in ipairs(t) do local x,y,z = r[1],r[2],r[3]
Screenshot is black screenNot enough frames for PPU warmup + ROM initWait 300+ frames minimum; complex ROMs may need 600+
Non-ASCII text garbled on WindowsTerminal uses GBK encodingUse ASCII labels in Lua; Python: sys.stdout.reconfigure(encoding='utf-8')
FCEUX hangs with registerexecToo many callbacks per frameReduce registered range, or reduce frame count

What ships with it: 2 files

4638.2 KB alongside SKILL.md

references/

Keep looking

Skills are one crate of 326,852. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.