"""
This module contains the Python interface for DeSmuME.
:class:`DeSmuME` is the main entrypoint to load and interact with the emulator.
"""
# Copyright 2020-2024 Marco Köpcke (Capypara)
#
# This file is part of py-desmume.
#
# py-desmume is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# py-desmume is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with py-desmume. If not, see <https://www.gnu.org/licenses/>.
import os
import platform
from ctypes import cdll, create_string_buffer, cast, c_char_p, POINTER, c_int, c_char, c_uint16, c_uint8, Structure, \
c_uint, CFUNCTYPE, c_int8, c_int16, c_uint32, c_int32
from enum import Enum
from typing import Union, Callable, List, Optional
try:
from PIL import Image
except ImportError:
from pil import Image
from desmume.controls import add_key, rm_key
SCREEN_WIDTH = 256
SCREEN_HEIGHT = 192
SCREEN_HEIGHT_BOTH = SCREEN_HEIGHT * 2
SCREEN_PIXEL_SIZE = SCREEN_WIDTH * SCREEN_HEIGHT
SCREEN_PIXEL_SIZE_BOTH = SCREEN_WIDTH * SCREEN_HEIGHT_BOTH
NB_STATES = 10
MEMORY_CB_FN = CFUNCTYPE(None, c_uint, c_int)
MemoryCbFn = Callable[[int, int], None]
"""
A callback function for the memory hooks. The first parameter of the callback function will receive the watched address,
the second the size of the event that triggered it, in bytes (eg. how many bytes were written / read).
"""
def strbytes(s):
return s.encode('utf-8')
[docs]
class Language(Enum):
"""Language codes."""
JAPANESE = 0
ENGLISH = 1
FRENCH = 2
GERMAN = 3
ITALIAN = 4
SPANISH = 5
[docs]
class StartFrom(Enum):
"""Start codes for movie recording."""
START_BLANK = 0
START_SRAM = 1
START_SAVESTATE = 2
[docs]
class DeSmuME_SDL_Window:
"""
A window that displays the emulator and processes touchscreen and keyboard inputs (default keyboard
configuration only).
This is meant to be a simple way to use and test the library, for intergration in custom UIs you
probably want to process input and display manually.
Should not be instantiated manually!
"""
def __init__(self, emu: 'DeSmuME', auto_pause=True, use_opengl_if_possible=True):
self.lib = emu.lib
self.lib.desmume_draw_window_init(bool(auto_pause), bool(use_opengl_if_possible))
def __del__(self):
self.destroy()
[docs]
def destroy(self):
"""Destroy the window."""
self.lib.desmume_draw_window_free()
[docs]
def draw(self):
"""Draw the current framebuffer to the window."""
self.lib.desmume_draw_window_frame()
[docs]
def has_quit(self) -> bool:
"""Returns true, when the window was closed by the user."""
return bool(self.lib.desmume_draw_window_has_quit())
[docs]
class DeSmuME_Savestate:
"""
Load and save savestates. Either slots can be used (maximum number of slots is in the constant ``NB_STATES``),
or savestates can be directly loaded from / saved to files.
Should not be instantiated manually!
"""
def __init__(self, emu: 'DeSmuME'):
self.emu = emu
[docs]
def scan(self):
"""Scan all savestate slots for if they exist or not. Required to be called before calling ``exists``."""
self.emu.lib.desmume_savestate_scan()
[docs]
def exists(self, slot_id: int) -> bool:
"""Returns whether or not a savestate in the specified slot exists."""
return bool(self.emu.lib.desmume_savestate_slot_exists(slot_id))
[docs]
def load(self, slot_id: int):
"""Load the savestate in the specified slot. It needs to exist."""
return self.emu.lib.desmume_savestate_slot_load(slot_id)
[docs]
def save(self, slot_id: int):
"""Save the current game state to the savestate in the specified slot."""
return self.emu.lib.desmume_savestate_slot_save(slot_id)
[docs]
def load_file(self, file_name: str):
"""
Load a savestate from file.
:raise: RuntimeError If the savestate could not be loaded.
"""
if not self.emu.lib.desmume_savestate_load(c_char_p(strbytes(file_name))):
raise RuntimeError("Unable to load savesate.")
[docs]
def save_file(self, file_name: str):
"""
Save a savestate to file.
:raise: RuntimeError If the savestate could not be saved.
"""
if not self.emu.lib.desmume_savestate_save(c_char_p(strbytes(file_name))):
raise RuntimeError("Unable to save savesate.")
[docs]
def date(self, slot_id: int) -> str:
"""Return the date a savestate was saved at as a string."""
self.emu.lib.desmume_savestate_slot_date.restype = c_char_p
return str(self.emu.lib.desmume_savestate_slot_date(slot_id), 'utf-8')
[docs]
class DeSmuME_Date(Structure):
"""A date C struct, to be used with setting a date for movie recording."""
_fields_ = [
("year", c_int),
("month", c_int),
("day", c_int),
("hour", c_int),
("minute", c_int),
("second", c_int),
("millisecond", c_int),
]
[docs]
class DeSmuME_Movie:
"""Record and play movies. Should not be instantiated manually!"""
def __init__(self, emu: 'DeSmuME'):
self.emu = emu
[docs]
def play(self, file_name: str):
"""
Load a movie file from a file and play it back.
:raise: RuntimeError If playback failed.
"""
self.emu.lib.desmume_movie_play.restype = c_char_p
err = self.emu.lib.desmume_movie_play(c_char_p(strbytes(file_name)))
if err is not None and err != "":
raise RuntimeError(str(err, 'utf-8'))
[docs]
def record(
self, file_name: str, author_name: str,
start_from: StartFrom = StartFrom.START_BLANK, sram_save: str = "", rtc_date: DeSmuME_Date = None
):
"""
Record a movie.
:param file_name: The name of the file to save to.
:param author_name: Name of the author of the movie.
:param start_from: Where to start the recording from.
:param sram_save: Filename of the SRAM save to use, optional.
:param rtc_date: Date to set the real-time-clock to, optional, defaults to now.
:return:
"""
if not rtc_date:
self.emu.lib.desmume_movie_record(
c_char_p(strbytes(file_name)), c_char_p(strbytes(author_name)), start_from.value, c_char_p(strbytes(sram_save))
)
else:
self.emu.lib.desmume_movie_record_from_date(
c_char_p(strbytes(file_name)), c_char_p(strbytes(author_name)), start_from.value, c_char_p(strbytes(sram_save)), rtc_date
)
[docs]
def stop(self):
"""Stops the current movie playback."""
self.emu.lib.desmume_movie_stop()
[docs]
def is_active(self):
return bool(self.emu.lib.desmume_movie_is_active())
[docs]
def is_recording(self):
return bool(self.emu.lib.desmume_movie_is_recording())
[docs]
def is_playing(self):
return bool(self.emu.lib.desmume_movie_is_playing())
[docs]
def is_finished(self):
return bool(self.emu.lib.desmume_movie_is_finished())
[docs]
def get_length(self):
if self.is_active():
return self.emu.lib.desmume_movie_get_length()
raise ValueError("No movie is active.")
[docs]
def get_name(self):
if self.is_active():
self.emu.lib.desmume_movie_get_name.restype = c_char_p
return self.emu.lib.desmume_movie_get_name()
raise ValueError("No movie is active.")
[docs]
def get_rerecord_count(self):
if self.is_active():
return self.emu.lib.desmume_movie_get_rerecord_count()
raise ValueError("No movie is active.")
[docs]
def set_rerecord_count(self, count: int):
if self.is_active():
return self.emu.lib.desmume_movie_set_rerecord_count(count)
raise ValueError("No movie is active.")
[docs]
def get_readonly(self):
if self.is_active():
return bool(self.emu.lib.desmume_movie_get_readonly())
raise ValueError("No movie is active.")
[docs]
def set_readonly(self, state: bool):
if self.is_active():
return self.emu.lib.desmume_movie_set_readonly(state)
raise ValueError("No movie is active.")
[docs]
class MemoryAccessor:
"""
Pythonic accessor class for manipulating the memory.
Access a single unsigned byte of memory at offset 0x100:
>>> emu_memory = DeSmuME().memory
>>> emu_memory.unsigned[0x100]
<<< 3
Access a subset of the memory as bytes:
>>> emu_memory = DeSmuME().memory
>>> emu_memory.unsigned[0x100:0x200]
<<< bytes(...)
Access a subset of the memory as signed integers:
>>> emu_memory = DeSmuME().memory
>>> emu_memory.signed[0x100:0x200]
<<< [-3, 2, ...]
Access a subset of memory as 4-byte unsigned integers:
>>> emu_memory = DeSmuME().memory
>>> emu_memory.unsigned[0x100:0x200:4]
<<< [-236, 1002, ...]
Writing to memory works the same way. It doesn't matter if you use the signed or unsigned accessor for this, both
work the same:
>>> emu_memory = DeSmuME().memory
>>> emu_memory.unsigned[0x100] = 2
You can also use the ``read_*`` methods instead for a more verbose way to read the memory as integers.
For writing those methods can be found in the ``DeSmuME_Memory`` parent object.
Should not be instantiated manually!
"""
def __init__(self, signed, mem: 'DeSmuME_Memory'):
self.signed = signed
self.mem = mem
def __getitem__(self, key: Union[int, slice]) -> Union[int, bytes, List[int]]:
if isinstance(key, int):
return self.mem.read(key, key, 1, self.signed)
return self.mem.read(key.start, key.stop, key.step, self.signed)
def __setitem__(self, key: Union[int, slice], value: Union[int, bytes, List[int]]):
if isinstance(key, int):
return self.mem.write(key, key, 1, bytes([value]))
return self.mem.write(key.start, key.stop, key.step, value)
[docs]
def read_byte(self, addr: int) -> int:
"""Read a 1-byte size integer at the specified address."""
return self[addr]
[docs]
def read_short(self, addr: int) -> int:
"""Read a 2-byte size integer at the specified address."""
return self[addr:addr:2]
[docs]
def read_long(self, addr: int) -> int:
"""Read a 2-byte size integer at the specified address."""
return self[addr:addr:4]
[docs]
class RegisterAccessor:
"""
Access the registers of the emulator. The properties are the register names.
``rX`` are the numbered register names, some registers are available via aliases.
Returned are integers.
You can also get a register value by accessing this object:
>>> DeSmuME().memory.register_arm9[0] == DeSmuME().memory.register_arm9.r0
<<< True
Should not be instantiated manually!
"""
def __init__(self, prefix, mem: 'DeSmuME_Memory'):
self.prefix = prefix
self.lib = mem.emu.lib
# <editor-fold desc="register accessors" defaultstate="collapsed">
def __getitem__(self, item):
if item == 0:
return self.r0
if item == 1:
return self.r1
if item == 2:
return self.r2
if item == 3:
return self.r3
if item == 4:
return self.r4
if item == 5:
return self.r5
if item == 6:
return self.r6
if item == 7:
return self.r7
if item == 8:
return self.r8
if item == 9:
return self.r9
if item == 10:
return self.r10
if item == 11:
return self.r11
if item == 12:
return self.r12
if item == 13:
return self.r13
if item == 14:
return self.r14
if item == 15:
return self.r15
raise ValueError("Invalid register")
def __setitem__(self, item, value):
if item == 0:
self.r0 = value
if item == 1:
self.r1 = value
if item == 2:
self.r2 = value
if item == 3:
self.r3 = value
if item == 4:
self.r4 = value
if item == 5:
self.r5 = value
if item == 6:
self.r6 = value
if item == 7:
self.r7 = value
if item == 8:
self.r8 = value
if item == 9:
self.r9 = value
if item == 10:
self.r10 = value
if item == 11:
self.r11 = value
if item == 12:
self.r12 = value
if item == 13:
self.r13 = value
if item == 14:
self.r14 = value
if item == 15:
self.r15 = value
raise ValueError("Invalid register")
@property
def r0(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r0")))
@r0.setter
def r0(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r0")), value)
@property
def r1(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r1")))
@r1.setter
def r1(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r1")), value)
@property
def r2(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r2")))
@r2.setter
def r2(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r2")), value)
@property
def r3(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r3")))
@r3.setter
def r3(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r3")), value)
@property
def r4(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r4")))
@r4.setter
def r4(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r4")), value)
@property
def r5(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r5")))
@r5.setter
def r5(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r5")), value)
@property
def r6(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r6")))
@r6.setter
def r6(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r6")), value)
@property
def r7(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r7")))
@r7.setter
def r7(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r7")), value)
@property
def r8(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r8")))
@r8.setter
def r8(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r8")), value)
@property
def r9(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r9")))
@r9.setter
def r9(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r9")), value)
@property
def r10(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r10")))
@r10.setter
def r10(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r10")), value)
@property
def r11(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r11")))
@r11.setter
def r11(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r11")), value)
@property
def r12(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r12")))
@r12.setter
def r12(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r12")), value)
@property
def r13(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r13")))
@r13.setter
def r13(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r13")), value)
@property
def r14(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r14")))
@r14.setter
def r14(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r14")), value)
@property
def r15(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "r15")))
@r15.setter
def r15(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "r15")), value)
@property
def cpsr(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "cpsr")))
@cpsr.setter
def cpsr(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "cpsr")), value)
@property
def spsr(self):
return self.lib.desmume_memory_read_register(c_char_p(strbytes(self.prefix + "spsr")))
@spsr.setter
def spsr(self, value):
self.lib.desmume_memory_write_register(c_char_p(strbytes(self.prefix + "spsr")), value)
# Aliases
@property
def sp(self):
"""Alias for r13."""
return self.r13
@sp.setter
def sp(self, value):
self.r13 = value
@property
def lr(self):
"""Alias for r14."""
return self.r14
@lr.setter
def lr(self, value):
self.r14 = value
@property
def pc(self):
"""Alias for r15."""
return self.r15
@pc.setter
def pc(self, value):
self.r15 = value
# </editor-fold>
RegisterAccesor = RegisterAccessor # Typo alias for backwards-comaptibility.
[docs]
class DeSmuME_Memory:
"""Access and manipulate the memory of the emulator. Should not be instantiated manually!"""
def __init__(self, emu: 'DeSmuME'):
self.emu = emu
self._unsigned: MemoryAccessor = MemoryAccessor(False, self)
self._signed: MemoryAccessor = MemoryAccessor(True, self)
self._register_arm9: RegisterAccessor = RegisterAccessor("arm9.", self)
self._register_arm7: RegisterAccessor = RegisterAccessor("arm7.", self)
self.emu.lib.desmume_memory_read_byte.restype = c_uint8
self.emu.lib.desmume_memory_read_byte_signed.restype = c_int8
self.emu.lib.desmume_memory_read_short.restype = c_uint16
self.emu.lib.desmume_memory_read_short_signed.restype = c_int16
self.emu.lib.desmume_memory_read_long.restype = c_uint32
self.emu.lib.desmume_memory_read_long_signed.restype = c_int32
self.emu.lib.desmume_memory_write_byte.argtypes = [c_int, c_uint8]
self.emu.lib.desmume_memory_write_short.argtypes = [c_int, c_uint16]
self.emu.lib.desmume_memory_write_long.argtypes = [c_int, c_uint32]
self.emu.lib.desmume_memory_read_register.argtypes = [c_char_p]
self.emu.lib.desmume_memory_write_register.argtypes = [c_char_p, c_int]
# Used to make sure that callbacks aren't GCed.
# TODO: Some way to clean this up again.
self._registered_cbs = []
@property
def unsigned(self) -> MemoryAccessor:
"""
The accessor for accessing the memory values as raw unsigned bytes/ints.
:type: MemoryAccessor
"""
return self._unsigned
@property
def signed(self) -> MemoryAccessor:
"""
The accessor for accessing the memory values as signed ints.
:type: MemoryAccessor
"""
return self._signed
@property
def register_arm9(self) -> RegisterAccessor:
"""
ARM9 Registers.
:type: RegisterAccessor
"""
return self._register_arm9
@property
def register_arm7(self) -> RegisterAccessor:
"""
ARM7 Registers.
:type: RegisterAccessor
"""
return self._register_arm7
[docs]
def read(self, start: int, end: int, size: int, signed: bool) -> Union[int, bytes, List[int]]:
"""
Read part of NDS memory. You probably don't want to use this. Use the ``unsigned`` and ``signed``
properties instead.
Allowed sizes: 1 = byte, 2 = short, 4 = long
If start and end are equal, returns an integer based on the size and the signed flag.
If not, but size is 1 and signed is False, returns a bytes object with all bytes between start and end.
Otherwise returns a list of ints based on size and signed.
"""
if size is None:
size = 1
if start == end:
# Read a single value, what kind depends on size
if signed:
if size == 1:
return self.emu.lib.desmume_memory_read_byte_signed(start)
elif size == 2:
return self.emu.lib.desmume_memory_read_short_signed(start)
elif size == 4:
return self.emu.lib.desmume_memory_read_long_signed(start)
else:
if size == 1:
return self.emu.lib.desmume_memory_read_byte(start)
elif size == 2:
return self.emu.lib.desmume_memory_read_short(start)
elif size == 4:
return self.emu.lib.desmume_memory_read_long(start)
raise ValueError("Invalid size.")
# Read a range.
if signed:
if size == 1:
b = []
for i, addr in enumerate(range(start, end, size)):
b.append(self.emu.lib.desmume_memory_read_byte_signed(addr))
elif size == 2:
b = []
for i, addr in enumerate(range(start, end, size)):
b.append(self.emu.lib.desmume_memory_read_short_signed(addr))
elif size == 4:
b = []
for i, addr in enumerate(range(start, end, size)):
b.append(self.emu.lib.desmume_memory_read_long_signed(addr))
else:
raise ValueError("Invalid size.")
else:
if size == 1:
b = bytearray(int(end - start))
for i, addr in enumerate(range(start, end, size)):
b[i] = self.emu.lib.desmume_memory_read_byte(addr)
b = bytes(b)
elif size == 2:
b = []
for i, addr in enumerate(range(start, end, size)):
b.append(self.emu.lib.desmume_memory_read_short(addr))
elif size == 4:
b = []
for i, addr in enumerate(range(start, end, size)):
b.append(self.emu.lib.desmume_memory_read_long(addr))
else:
raise ValueError("Invalid size.")
return b
[docs]
def write(self, start: int, end: int, size: int, value: Union[bytes, List[int]]):
"""
Write part of NDS memory. You probably don't want to use this. Use the ``unsigned`` and ``signed``
properties instead, or the ``write_*`` methods.
"""
if start == end:
end += 1 # Write at least one.
if size == 1:
for i, addr in enumerate(range(start, end, size)):
self.emu.lib.desmume_memory_write_byte(addr, c_uint8(value[i]))
elif size == 2:
for i, addr in enumerate(range(start, end, size)):
# value must be List[int]
self.emu.lib.desmume_memory_write_short(addr, c_uint16(value[i]))
elif size == 4:
for i, addr in enumerate(range(start, end, size)):
# value must be List[int]
self.emu.lib.desmume_memory_write_long(addr, c_uint32(value[i]))
else:
raise ValueError("Invalid size.")
[docs]
def read_string(self, address: int, codec='windows-1255'):
"""Read a null-terminated string, beginning at address."""
max_len = 50
string_buff = bytearray(max_len)
cur_byte = self.unsigned.read_byte(address)
i = 0
while cur_byte != 0:
if i >= max_len:
max_len += 50
string_buff += bytearray(50)
string_buff[i] = cur_byte
i += 1
cur_byte = self.unsigned.read_byte(address + i)
return str(memoryview(string_buff)[:i], codec, 'ignore')
[docs]
def write_byte(self, addr: int, value: int):
"""Write a 1-byte integer to the memory at the specified address."""
self.write(addr, addr, 1, [value])
[docs]
def write_short(self, addr: int, value: int):
"""Write a 2-byte integer to the memory at the specified address."""
self.write(addr, addr, 2, [value])
[docs]
def write_long(self, addr: int, value: int):
"""Write a 4-byte integer to the memory at the specified address."""
self.write(addr, addr, 4, [value])
[docs]
def get_next_instruction(self) -> int:
"""Returns the next instruction to be executed by the ARM9 processor."""
return self.emu.lib.desmume_memory_get_next_instruction()
[docs]
def set_next_instruction(self, address: int):
"""
Sets the next instruction to be executed by the ARM9 processor.
You should probably also consider updating the PC.
"""
self.emu.lib.desmume_memory_set_next_instruction(address)
[docs]
def register_write(self, address: int, callback: Optional[MemoryCbFn], size=1):
"""
Add a memory callback for when the memory at the specified address was changed.
Setting a callback will override the previously registered one for this address.
Set callback to None, to remove the callback for this address.
A usage example can be found for ``register_exec``.
:param address: The address to monitor.
:param callback: This callback will be called when the memory was written to. See ``MemoryCbFn``.
:param size: The maximum size that will be watched. If you set this to 4 for example,
a range of (address, address + 3) will be monitored.
"""
casted_cbfn = callback
if callback is not None:
casted_cbfn = MEMORY_CB_FN(callback)
self._registered_cbs.append(casted_cbfn)
self.emu.lib.desmume_memory_register_write(address, size, casted_cbfn)
[docs]
def register_read(self, address: int, callback: Optional[MemoryCbFn], size=1):
"""
Add a memory callback for when the memory at the specified address was read.
Setting a callback will override the previously registered one for this address.
Set callback to None, to remove the callback for this address.
A usage example can be found for ``register_exec``.
:param address: The address to monitor.
:param callback: This callback will be called when the memory was read. See ``MemoryCbFn``.
:param size: The maximum size that will be watched. If you set this to 4 for example,
a range of (address, address + 3) will be monitored.
"""
casted_cbfn = callback
if callback is not None:
casted_cbfn = MEMORY_CB_FN(callback)
self._registered_cbs.append(casted_cbfn)
self.emu.lib.desmume_memory_register_read(address, size, casted_cbfn)
[docs]
def register_exec(self, address: int, callback: Optional[MemoryCbFn], size=2):
"""
Add a memory callback for when the PC processed the operation at the specified address.
Setting a callback will override the previously registered one for this address.
Set callback to None, to remove the callback for this address.
Example (will print 'Hello World', whenever the PC executes the code at 0x022f8818):
>>> def my_callback(address, size):
>>> print("Hello World!")
>>>
>>> DeSmuME().memory.register_exec(0x022f8818, my_callback)
:param address: The address to monitor.
:param callback: This callback will be called when the operation was executed. See ``MemoryCbFn``.
:param size: Leave this at 2.
"""
casted_cbfn = callback
if callback is not None:
casted_cbfn = MEMORY_CB_FN(callback)
self._registered_cbs.append(casted_cbfn)
self.emu.lib.desmume_memory_register_exec(address, size, casted_cbfn)
[docs]
class DeSmuME:
"""DeSmuME, the Nintendo DS emulator."""
def __init__(self, dl_name: str = None):
"""
Initializes a new emulator instance.
A path to the shard library for DeSmuME can be passed, if not it will be auto-detected.
"""
self.lib = None
# Load the correct library
if dl_name is None:
# Try autodetect / CWD
try:
if platform.system().lower().startswith('windows'):
dl_name = "libdesmume.dll"
os.add_dll_directory(os.getcwd())
elif platform.system().lower().startswith('linux'):
dl_name = "libdesmume.so"
elif platform.system().lower().startswith('darwin'):
dl_name = "libdesmume.dylib"
else:
RuntimeError(f"Unknown platform {platform.system()}, can't autodetect DLL to load.")
self.lib = cdll.LoadLibrary(dl_name)
except OSError:
# Okay now try the package directory
dl_name = os.path.dirname(os.path.realpath(__file__))
if platform.system().lower().startswith('windows'):
os.add_dll_directory(dl_name)
dl_name = os.path.join(dl_name, "libdesmume.dll")
elif platform.system().lower().startswith('linux'):
dl_name = os.path.join(dl_name, "libdesmume.so")
elif platform.system().lower().startswith('darwin'):
dl_name = os.path.join(dl_name, "libdesmume.dylib")
self.lib = cdll.LoadLibrary(dl_name)
else:
if platform.system().lower().startswith('windows'):
os.add_dll_directory(os.path.dirname(dl_name))
dl_name = os.path.basename(dl_name)
self.lib = cdll.LoadLibrary(dl_name)
self.lib.desmume_set_savetype(0)
if self.lib.desmume_init() < 0:
raise RuntimeError("Failed to init DeSmuME")
self._input = DeSmuME_Input(self)
self._savestate = DeSmuME_Savestate(self)
self._movie = DeSmuME_Movie(self)
self._memory = DeSmuME_Memory(self)
self._sdl_window = None
self._raw_buffer_rgbx = None
def __del__(self):
if self.lib is not None:
self.lib.desmume_free()
self._input.__del__()
if self._sdl_window:
self._sdl_window.__del__()
self.lib = None
[docs]
def destroy(self):
"""Destroy the emulator and free memory. Calling any method after this WILL result in a crash."""
self.__del__()
@property
def input(self):
"""
Keyboard and joystick configuration.
:type: DeSmuME_Input
"""
return self._input
@property
def savestate(self):
"""
Loading and saving savestates.
:type: DeSmuME_Savestate
"""
return self._savestate
@property
def movie(self):
"""
Recording and playing back movies.
:type: DeSmuME_Movie
"""
return self._movie
@property
def memory(self):
"""
Accessing and manipulating the memory.
:type: DeSmuME_Memory
"""
return self._memory
[docs]
def set_language(self, lang: Language):
"""Set the current firmware language."""
self.lib.desmume_set_language(lang.value)
[docs]
def open(self, file_name: str, auto_resume=True):
"""
Open a ROM by file name.
If ``auto_resume`` is True, the emulator will automatically begin emulating the game.
Otherwise the emulator is paused and you may call ``resume`` to unpause it.
"""
if self.lib.desmume_open(c_char_p(strbytes(file_name))) < 0:
raise RuntimeError("Unable to open ROM file.")
if auto_resume:
self.resume()
[docs]
def close(self):
"""
Close a previously opened ROM, freeing up memory.
You don't need to call this before opening a new ROM (it is done automatically).
"""
self.lib.desmume_close()
[docs]
def set_savetype(self, value: int):
"""
Set the type of the SRAM. Please see the DeSmuME documentation for possible values.
0 is auto-detect and set by default.
"""
# TODO: Enum?
self.lib.desmume_set_savetype(value)
[docs]
def pause(self):
"""Pause the emulator."""
self.lib.desmume_pause()
[docs]
def resume(self, keep_keypad=False):
"""
Resume / unpause the emulator. This will reset the keypad (release all keys),
except if ``keep_keypad`` is provided.
"""
if keep_keypad:
self.input.keypad_update(0)
self.lib.desmume_resume()
[docs]
def reset(self):
"""Resets the emulator / restarts the current game."""
self.lib.desmume_reset()
[docs]
def is_running(self) -> bool:
"""Returns ``True``, if a game is loaded and the emulator is running (not paused)."""
return bool(self.lib.desmume_running())
[docs]
def skip_next_frame(self):
"""Tell the emulator to skip the next frame."""
self.lib.desmume_skip_next_frame()
[docs]
def cycle(self, with_joystick=True):
"""
Cycle one game cycle / frame. Set ``with_joystick`` to
``False``, if joystick processing was not initialized.
"""
self.lib.desmume_cycle(with_joystick)
[docs]
def has_opengl(self) -> bool:
"""Returns ``True``, if OpenGL is available for rendering."""
return bool(self.lib.desmume_has_opengl())
[docs]
def create_sdl_window(self, auto_pause=False, use_opengl_if_possible=True) -> DeSmuME_SDL_Window:
"""
Create an SDL window for drawing the
emulator in and processing inputs.
:param auto_pause: Whether or not "tabbing out" of the window pauses the game.
:param use_opengl_if_possible: Whether or not to use OpenGL for rendering, if available.
"""
if not self._sdl_window:
self._sdl_window = DeSmuME_SDL_Window(self, auto_pause, use_opengl_if_possible)
return self._sdl_window
[docs]
def display_buffer(self):
"""Return the display buffer in the internal format. You probably want to use display_buffer_as_rgbx instead."""
self.lib.desmume_draw_raw.restype = POINTER(c_uint16)
return self.lib.desmume_draw_raw()
[docs]
def display_buffer_as_rgbx(self, reuse_buffer=True) -> memoryview:
"""
Return the display buffer as RGBX color values,
see the screen size constants for how many pixels make up lines.
"""
if reuse_buffer:
if not self._raw_buffer_rgbx:
self._raw_buffer_rgbx = create_string_buffer(SCREEN_WIDTH * SCREEN_HEIGHT_BOTH * 4)
buff = self._raw_buffer_rgbx
else:
buff = create_string_buffer(SCREEN_WIDTH * SCREEN_HEIGHT_BOTH * 4)
self.lib.desmume_draw_raw_as_rgbx(cast(buff, c_char_p))
# XXX: Yes this is stupid, but cairo NEEDS a bytearray....
return memoryview(bytearray(buff.raw))
[docs]
def screenshot(self) -> Image.Image:
"""Convert the current display buffer into a PIL image."""
buff = create_string_buffer(SCREEN_WIDTH * SCREEN_HEIGHT_BOTH * 3)
self.lib.desmume_screenshot(cast(buff, c_char_p))
return Image.frombuffer('RGB', (SCREEN_WIDTH, SCREEN_HEIGHT_BOTH), buff.raw, 'raw', 'RGB', 0, 1)
[docs]
def get_ticks(self) -> int:
"""Get the current SDL tick number."""
return self.lib.desmume_sdl_get_ticks()
[docs]
def volume_get(self) -> int:
"""Get the current value (between 0 - 100)."""
return self.lib.desmume_volume_get()
[docs]
def volume_set(self, volume: int):
"""Set the current value (to a value between 0 - 100)."""
return self.lib.desmume_volume_set(volume)
[docs]
def gpu_get_layer_main_enable_state(self, layer_index: int):
"""Get the current display status of the specified layer on the main GPU."""
return bool(self.lib.desmume_gpu_get_layer_main_enable_state(layer_index))
[docs]
def gpu_get_layer_sub_enable_state(self, layer_index: int):
"""Get the current display status of the specified layer on the sub GPU."""
return bool(self.lib.desmume_gpu_get_layer_sub_enable_state(layer_index))
[docs]
def gpu_set_layer_main_enable_state(self, layer_index: int, state: bool):
"""Set the current display status of the specified layer on the main GPU."""
self.lib.desmume_gpu_set_layer_main_enable_state(layer_index, state)
[docs]
def gpu_set_layer_sub_enable_state(self, layer_index: int, state: bool):
"""Set the current display status of the specified layer on the sub GPU."""
self.lib.desmume_gpu_set_layer_sub_enable_state(layer_index, state)