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= TrueOn; with the ROM cache enabled, launch boots a local copy of the ROM.
attributedisplay_name= 'Azahar'Human-readable name.
attributeclears_stale_saves= TrueOn; activate empties every declared save subtree.
attributesave_root= USER_DIRThe 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_EXTENSIONSBootable formats, best first.
attributelog_path= AZAHAR_LOG_PATHThe 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) -> NoneInitialise the process handle and the session baseline.
paramselfReturns
Nonefuncclear_working_slot(self, excluded=()) -> NoneEmpty 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.
paramselfparamexcludedtuple[str, ...]= ()Subtrees carried by the whole-card routes. Azahar has no memory card, so this is always empty.
Returns
Nonefuncprepare_restore(self) -> NoneStop a running Azahar so the archive can be extracted under it.
paramselfReturns
Nonefuncimport_spec(self) -> imports.ImportSpecDeclare 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.
paramselfReturns
webstation_broker.imports.ImportSpecThe spec.
funcplace_import(self, member, spec, ctx) -> Union[imports.Placement, imports.ImportRefusal]Place one declared save file under the ids Azahar uses.
paramselfparammemberimports.ImportMemberThe member, already past the kind gate.
paramspecimports.ImportSpecThis emulator's spec.
paramctximports.ImportCtxThe launch context.
Returns
typing.UnionThe 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.
paramselfReturns
typing.OptionalA hex16 source that reads save_target, then title_id.
funcresolve_rom_file(self, path) -> Optional[Path]The file Azahar should boot for path.
paramselfparampathPathA ROM file, or a folder searched up to two levels deep.
Returns
typing.OptionalThe file itself, the best-ranked bootable file in the folder, or None.
funclaunch(self, rom_path, resume_slot) -> NonePatch 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.
paramselfparamrom_pathPathThe file to boot.
paramresume_slotOptional[int]Ignored with a log line; there are no reachable save states.
Returns
Nonefunc_modified_title_saves(self) -> list[Path]Title save dirs holding a file written while the session ran.
paramselfReturns
listThe <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.
paramselfparamslotOptional[int]Ignored, including None; there are no save states to write, and the save-data restamp is what an exit does either way.
Returns
dictstate_saved, state_slot and state_file, all None.