-
Notifications
You must be signed in to change notification settings - Fork 0
Examples
Recipes for things people actually want to do. Every flow here was run against a real console.
capture_screen() # 640x360 by default — enough to read a menu
tap(640, 360) # coordinates are ALWAYS native 1280x720
screen_changed() # wait for the UI to settle
screen_changed returns one image only if the screen actually changed, and
no image at all if nothing did. That second case is how you find out an input
did not register — much cheaper than screenshotting in a loop.
Navigating a menu:
send_input_sequence([
{"buttons": ["DDOWN"], "wait_ms": 150},
{"buttons": ["DDOWN"], "wait_ms": 150},
{"buttons": ["A"], "wait_ms": 400}
])
screen_changed(timeout_ms=5000)
Rather than describing the presses, do them once on the console and keep them:
record_input(duration_ms=8000) # press buttons on the console
# -> returns a `sequence` you can pass straight to send_input_sequence
launch_title("0100000011d90000")
wait_event(event="app_start", timeout_ms=20000)
app_start is level-triggered: it answers "is a game running?", returning
immediately if one already is. Launch first, then wait — the agent handles
one command at a time, so you cannot trigger something while a wait is blocking
the connection.
Homebrew cannot be launched this way.
launch_titlerefuses the hbloader ID0142b048fd620000, because doing it spawns a process that renders nothing and then blocks the real launch path. Use the Album applet.
The flagship workflow. A single scan is never enough — searching for 100
matches tens of thousands of addresses. You converge by telling it which way the
value moved:
live_meta() # title, heap extents, build ID
find_value(100) # -> 8,400 candidates
...take damage in the game...
narrow_search(op="decreased") # -> 213 candidates
...take damage again...
narrow_search(op="decreased") # -> 4 candidates
search_results() # the surviving addresses
live_read_mem("0x361c00a40", 4) # confirm
Operators eq, ne, gt, lt take a value. changed, unchanged,
increased, decreased compare against the previous scan — these are what make
it converge, because you rarely know the new number but always know the
direction it moved.
Candidates live on the console between calls, so each step costs one
round-trip. Only real mappings are scanned: the heap extent reported by
live_meta is a reserved ~8 GB range that is almost entirely unmapped.
All of this reads the game without pausing it. A debugger attach freezes the target, so a draining health bar or a running countdown is invisible that way.
diagnose() # sysinfo + running app + crash reports + screenshot
get_crash_reports() # list, then fetch one by name
fatal_reports() # different directory — written when the SYSTEM goes down
Turning an address into something useful:
debug_attach(pid=138)
debug_modules() # base, size, build ID per module
debug_backtrace() # walks the frame-pointer chain
debug_detach() # ALWAYS — this is what resumes the target
Subtract a module base from a backtrace address to get an RVA like
main+0x1a2f4, which is what Ghidra and IDA expect. The build ID identifies the
exact binary, so it is the key for matching a symbol map.
Register writes need a thread stopped at a debug event, not merely a paused attach — the kernel refuses otherwise. Reads work on any paused attach.
No title ID needed up front:
list_saves() # every save: game, system, BCAT, device, cache
list_saves(type="account") # just user game saves
mount_save(title_id="0100000011d90000", uid_hi="...", uid_lo="...")
fs_list("/", device="save")
fs_read_text("/SaveData.bin", device="save")
unmount_save()
The mount is read-only, and every write command refuses a non-SD device, so
nothing here can corrupt a save. Use backup_save / restore_save if you
genuinely need to write one back.
Some system saves will not mount: the owning service holds them open and a save cannot be opened twice. The error says so explicitly.
Both of these run on the console, so you are not pulling a directory tree over Wi-Fi to filter it locally:
fs_find(name_contains=".nro", files_only=true)
fs_grep("/atmosphere/config/system_settings.ini", "usb")
fs_free_space()
fs_find is depth- and scan-capped, and says explicitly when it truncated
rather than returning a partial answer that looks complete.
preflight() # battery, charger, free space, emuMMC vs sysMMC
reboot() # -> refused, returns a confirmation token
reboot(confirm="a1b2c3d4e5f6") # -> actually reboots
The token is bound to the exact effect, so one issued to delete
/harmless.txt cannot be replayed to delete something else. It expires after
five minutes. Set SWITCH_MCP_ALLOW_DESTRUCTIVE=1 to skip the dance entirely
for unattended use.
Preview without acting — any mutating command takes dry_run:
fs_delete("/switch/old.nro", dry_run=true) # says what it would do, does nothing
And afterwards:
journal() # on-device audit trail of every mutating command, with outcomes
watchdog() # boots with no client connecting — a rising count means unreachable
capabilities()
Reports the tier, whether this is emuMMC, and the exact list of commands permitted right now. Worth calling before planning anything invasive: a command missing from that list will be refused however you call it, and the fix is a config change on the SD card plus a restart — not a different tool call.