webstation-broker
DeveloperPython referenceemulatorsemulators.azahar

emulators.azahar.Azahar

Nintendo 3DS via Azahar, driven by command line flags and config file patching.

Nintendo 3DS via Azahar, driven by command line flags and config file patching.

Azahar has no control API reachable from outside the process, so the broker pins qt-config.ini before every launch (no close confirmation, no update check, no Discord presence) and boots windowed with -w. A patch that fails aborts the launch: those keys are the only thing keeping a modal nobody can click out of the session. Fullscreen Azahar stops rendering when the display resizes under it, which happens whenever the player resizes their browser. Azahar installs no SIGTERM handler, so the stop is a hard kill and the dump takes whatever the game had already committed to disk.

There are no reachable save states: persistence is the game's own save data under the emulated SD card and NAND, and the archive is scoped to those save trees. At exit every file in the title save dirs written during the session gets its mtime refreshed so the delta dump ships those saves whole while other titles' saves stay filtered out. A resume slot is logged and ignored.

Attributes

attributename
= 'azahar'

Provider key, azahar.

attributerom_cacheable
= True

On; with the ROM cache enabled, launch boots a local copy of the ROM.

attributedisplay_name
= 'Azahar'

Human-readable name.

attributeclears_stale_saves
= True

On; activate empties every declared save subtree.

attributesave_root
= USER_DIR

The data root the save subtrees hang off.

attributesave_subtrees
= (f'sdmc/Nintendo 3DS/{SYSTEM_ID}/{SDCARD_ID}/title', f'sdmc/Nintendo 3DS/{SYSTEM_ID}/{SDCARD_ID}/extdata', f'nand/data/{SYSTEM_ID}/extdata', f'nand/data/{SYSTEM_ID}/sysdata')

The SD title and extdata trees plus the NAND extdata and sysdata trees.

attributerom_extensions
= ROM_EXTENSIONS

Bootable formats, best first.

attributelog_path
= AZAHAR_LOG_PATH

The emulator log file.

attributeterm_timeout
= float(os.environ.get('AZAHAR_STOP_WAIT', '5'))

SIGTERM grace before SIGKILL (env AZAHAR_STOP_WAIT, default 5).

attribute_session_start
= float('inf')

Unix time launch started the emulator at; infinity until it does.

Infinity rather than zero so an instance whose launch never completed matches no file at all. A baseline of zero is newer than nothing, so every title in the container would read as touched this session and save_and_exit would restamp and ship all of them.

Functions

func__init__(self) -> None

Initialise the process handle and the session baseline.

paramself

Returns

None
funcclear_working_slot(self, excluded=()) -> None

Empty the SD and NAND save trees before the archive restore.

Azahar files a save under the title id alone, with nothing in the path naming the player, so the previous session's saves sit exactly where this one's belong. The restore only writes the members the incoming archive carries: a title the last player saved and this one's archive does not name would stay readable, and the exit restamp ships a title whole once anything under it is written, so it would leave again in this player's dump.

paramself
paramexcludedtuple[str, ...]
= ()

Subtrees carried by the whole-card routes. Azahar has no memory card, so this is always empty.

Returns

None
funcprepare_restore(self) -> None

Stop a running Azahar so the archive can be extracted under it.

paramself

Returns

None
funcimport_spec(self) -> imports.ImportSpec

Declare what Azahar takes: SD and NAND save trees, and nothing else.

Azahar has no states and no cards, so the save kind is the only one.

paramself

Returns

webstation_broker.imports.ImportSpec

The spec.

funcplace_import(self, member, spec, ctx) -> Union[imports.Placement, imports.ImportRefusal]

Place one declared save file under the ids Azahar uses.

paramself
parammemberimports.ImportMember

The member, already past the kind gate.

paramspecimports.ImportSpec

This emulator's spec.

paramctximports.ImportCtx

The launch context.

Returns

typing.Union

The placement, or a refusal.

funcidentity_source(self) -> Optional[imports.IdentitySource]

Take the session's game from RomM's save target, or its title id when there is none.

A 3DS save_target is the title's high/low pair and title_id is that same pair written as one sixteen-digit id, so the two normalise alike and either names the folder a title save belongs in. Azahar boots a file, not a path that names an id, so there is no rom reader.

paramself

Returns

typing.Optional

A hex16 source that reads save_target, then title_id.

funcresolve_rom_file(self, path) -> Optional[Path]

The file Azahar should boot for path.

paramself
parampathPath

A ROM file, or a folder searched up to two levels deep.

Returns

typing.Optional

The file itself, the best-ranked bootable file in the folder, or None.

funclaunch(self, rom_path, resume_slot) -> None

Patch qt-config.ini and boot the game windowed.

Raises

  • OSError: When qt-config.ini could not be patched, so nothing is spawned; see _patch_config.
  • configparser.Error: Likewise, when the patched values cannot be serialized.
  • UnicodeError: Likewise, when the merged contents cannot be encoded.
paramself
paramrom_pathPath

The file to boot.

paramresume_slotOptional[int]

Ignored with a log line; there are no reachable save states.

Returns

None
func_modified_title_saves(self) -> list[Path]

Title save dirs holding a file written while the session ran.

paramself

Returns

list

The <titleHigh>/<titleLow> directories under every save group

funcsave_and_exit(self, slot) -> dict[str, Any]

Stop Azahar and mark this session's title saves for the dump.

paramself
paramslotOptional[int]

Ignored, including None; there are no save states to write, and the save-data restamp is what an exit does either way.

Returns

dict

state_saved, state_slot and state_file, all None.

On this page