Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/azahar-emu/azahar/llms.txt

Use this file to discover all available pages before exploring further.

Azahar exposes a UDP RPC server that lets you interact with a running 3DS game from a Python script. You can read and write memory, enumerate running processes, and switch the active process — all without modifying the emulator itself. This makes the scripting API useful for automated testing, research, and tool development.
This API is intended for development and testing purposes only. Do not use it to gain unfair advantages in multiplayer games or to cheat in any online context.

Prerequisites

Before you can connect, you must enable the RPC server in Azahar’s settings. Once enabled, the server listens on UDP port 45987 by default. No package installation is required. Copy citra.py from the dist/scripting/ directory of the Azahar repository into your project and import it directly.

Connecting

Instantiate the Citra class to open a UDP socket. The constructor accepts an optional address and port.
from citra import Citra

c = Citra()                              # connects to 127.0.0.1:45987
c = Citra(address="127.0.0.1", port=45987)  # explicit defaults
address
string
default:"127.0.0.1"
IP address of the machine running Azahar.
port
number
default:"45987"
UDP port the RPC server is listening on. Defined as CITRA_PORT = 45987 in citra.py.

API reference

is_connected()

Returns True if the underlying UDP socket is open. Because UDP is connectionless, this only confirms that the socket was created — not that Azahar is running or that the RPC server is enabled.
if c.is_connected():
    print("Socket is open")
Returns: bool

process_list()

Returns a dictionary of all processes currently running in the emulator.
processes = c.process_list()
for pid, (title_id, name) in processes.items():
    print(f"PID {pid}: {name} (Title ID: {title_id:#018x})")
Returns: dict[int, tuple[int, str]] — maps each process_id to a tuple of (title_id, proc_name).
KeyTypeDescription
process_idintNumeric process identifier
title_idint64-bit Nintendo title ID
proc_namestrASCII process name, up to 8 characters

get_process()

Returns the process ID that is currently selected for memory operations.
current_pid = c.get_process()
print(f"Active process: {current_pid}")
Returns: int — the current process ID, or None if the request fails.

set_process(process_id)

Sets the active process for subsequent memory read and write operations.
c.set_process(0x00000028)
process_id
number
required
The process ID to select. Use process_list() to find valid IDs.
Returns: None

read_memory(read_address, read_size)

Reads bytes from the game’s memory at the given address. Internally splits the request into chunks of up to MAX_REQUEST_DATA_SIZE (1024) bytes.
data = c.read_memory(0x100000, 4)
if data is not None:
    print(data.hex())
read_address
number
required
The memory address to read from, as an integer (e.g. 0x100000).
read_size
number
required
Number of bytes to read. Reads larger than 1024 bytes are handled automatically across multiple requests.
Returns: bytes on success, or None if the request fails.

write_memory(write_address, write_contents)

Writes bytes into the game’s memory at the given address. Internally splits large writes into chunks.
success = c.write_memory(0x100000, b'\xff\xff\xff\xff')
if success:
    print("Write succeeded")
write_address
number
required
The memory address to write to, as an integer.
write_contents
bytes
required
The raw bytes to write. Each chunk is limited to MAX_REQUEST_DATA_SIZE - 8 (1016) bytes to leave room for the address and size fields in the request payload.
Returns: boolTrue if all chunks were written successfully, False if any chunk fails.

Working example

from citra import Citra

c = Citra()

# List all running processes
processes = c.process_list()
for pid, (title_id, name) in processes.items():
    print(f"PID {pid}: {name} (Title ID: {title_id:#018x})")

# Select the first process
first_pid = next(iter(processes))
c.set_process(first_pid)

# Read 4 bytes from address 0x100000
data = c.read_memory(0x100000, 4)
if data:
    print(data.hex())

# Write bytes to memory
success = c.write_memory(0x100000, b'\xff\xff\xff\xff')
print(f"Write succeeded: {success}")

Protocol details

The RPC server uses UDP for all communication. Each packet consists of a fixed 16-byte header followed by an optional data payload.
FieldTypeDescription
versionuint32Protocol version (currently 1)
request_iduint32Random 32-bit ID used to match replies to requests
request_typeuint32One of the RequestType enum values
data_sizeuint32Length of the data section in bytes
databytesRequest-specific payload

Request types

NameValueDescription
ReadMemory1Read bytes from a memory address
WriteMemory2Write bytes to a memory address
ProcessList3Enumerate running processes
SetGetProcess4Get or set the active process

Size limits

ConstantValueDescription
MAX_REQUEST_DATA_SIZE1024 bytesMaximum data payload per request
MAX_PACKET_SIZE1040 bytesMaximum total packet size (header + data)
The header is 16 bytes (4 × uint32), so MAX_PACKET_SIZE = 1024 + 0x10 = 1040.

Build docs developers (and LLMs) love