Source code for libretro.api.content

"""
Types that define the content that can be (or has been) loaded by a :class:`.Core`.

.. seealso::

    :class:`.ContentDriver`
        The :class:`.Protocol` that uses these types to load content in libretro.py.

    :mod:`libretro.drivers.content`
        libretro.py's included :class:`.ContentDriver` implementations.
"""

from __future__ import annotations

import os
from collections.abc import Buffer, Generator, Iterator, Mapping, Sequence
from contextlib import contextmanager
from ctypes import (
    POINTER,
    Structure,
    addressof,
    c_bool,
    c_char_p,
    c_size_t,
    c_ubyte,
    c_uint,
)
from dataclasses import dataclass
from os import PathLike
from types import MappingProxyType
from typing import overload, override
from zipfile import Path as ZipPath

from libretro.api._utils import (
    MemoDict,
    as_bytes,
    deepcopy_array,
    deepcopy_buffer,
    mmap_file,
)
from libretro.ctypes import TypedPointer, c_void_ptr


[docs] @dataclass(init=False, slots=True) class retro_system_info(Structure): """ Describes the :class:`.Core`'s basic properties such as its name, version, and supported file extensions. Corresponds to :c:type:`retro_system_info` in ``libretro.h``. .. seealso:: :meth:`.Core.get_system_info` The method used to provide this information to libretro.py. """ library_name: bytes | None """Human-readable name for the core.""" library_version: bytes | None """Human-readable version string for the core.""" valid_extensions: bytes | None """Pipe-delimited string of file extensions the core accepts, without leading dots.""" need_fullpath: bool """ If :obj:`True`, the :class:`.ContentDriver` should not open content files itself, instead letting the :class:`.Core` handle it. """ block_extract: bool """ If :obj:`True`, the :class:`.ContentDriver` should not extract content from compressed archives. .. tip:: This is useful for cores that treat the archive itself as content, like most cores for arcade machines. """ _fields_ = ( ("library_name", c_char_p), ("library_version", c_char_p), ("valid_extensions", c_char_p), ("need_fullpath", c_bool), ("block_extract", c_bool), )
[docs] def __deepcopy__(self, _): """ Return a deep copy of this system info, including copies of all strings. Intended for use with :func:`copy.deepcopy`. >>> import copy >>> from libretro.api import retro_system_info >>> info = retro_system_info(library_name=b'TestCore', library_version=b'1.0') >>> info2 = copy.deepcopy(info) >>> info == info2 True >>> info is info2 False >>> info.library_name == info2.library_name True >>> info.library_name is info2.library_name False """ return retro_system_info( library_name=self.library_name, library_version=self.library_version, valid_extensions=self.valid_extensions, need_fullpath=self.need_fullpath, block_extract=self.block_extract, )
@property def extensions(self) -> Iterator[bytes]: """ Yields each extension named in :attr:`valid_extensions` as :class:`bytes`, or nothing if :attr:`valid_extensions` is :obj:`None`. >>> from libretro.api import retro_system_info >>> info = retro_system_info(valid_extensions=b'bin|rom|iso') >>> list(info.extensions) [b'bin', b'rom', b'iso'] """ if self.valid_extensions: yield from self.valid_extensions.split(b"|")
[docs] @dataclass(init=False, slots=True) class retro_game_info(Structure): """ Describes a game that's been loaded and exposed to a :class:`.Core`. Corresponds to :c:type:`retro_game_info` in ``libretro.h``. .. seealso:: :meth:`.Core.load_game`, :meth:`.Core.load_game_special` The methods used to load a game into a core. """ path: bytes | None """ Path from which the content was loaded, or :obj:`None` if loaded from memory. """ data: c_void_ptr | None """ Pointer to the content data in memory, or :obj:`None` if :attr:`~retro_system_info.need_fullpath` was set. """ size: int """ Size of the content data in bytes. Assigned values are bitwise-masked to fit into a ``size_t``. """ meta: bytes | None """ Optional metadata string. Usually :obj:`None`, but can contain any additional text. """ _fields_ = ( ("path", c_char_p), ("data", c_void_ptr), ("size", c_size_t), ("meta", c_char_p), ) @property def ext(self) -> bytes | None: """ Returns the file extension of :attr:`path` without a leading dot, :obj:`None` if :attr:`path` is :obj:`None`, or an empty string if :attr:`path` has no extension. >>> from libretro.api import retro_game_info >>> retro_game_info(path=b'/game.bin').ext b'bin' >>> retro_game_info(path=None).ext is None True >>> retro_game_info(path=b'/game').ext b'' """ path = self.path if path is None: return None _, ext = os.path.splitext(path) return ext.removeprefix(b".")
[docs] def __deepcopy__(self, _): """ Return a deep copy of this object, including copies of all strings and content data. Intended for use with :func:`copy.deepcopy`. >>> import copy >>> from libretro.api import retro_game_info >>> info = retro_game_info(path=b'/game.bin') >>> info2 = copy.deepcopy(info) >>> info == info2 True >>> info is info2 False >>> info.data == info2.data False """ return retro_game_info( path=self.path, data=deepcopy_buffer(self.data, self.size), size=self.size, meta=self.meta, )
type ContentPath = str | PathLike[str] | PathLike[bytes] | ZipPath """A path to content, supporting filesystem paths and ZIP archive paths.""" type ContentData = bytes | bytearray | memoryview[int] | Buffer """Raw content data in memory.""" type Content = ContentPath | ContentData | retro_game_info """Any of the supported ways to provide content to a core."""
[docs] @dataclass(frozen=True) class SubsystemContent: """ Content for a subsystem, pairing a game type with a sequence of content items. >>> from libretro.api.content import SubsystemContent >>> sc = SubsystemContent(game_type=0, info=[b'/game.bin']) >>> sc.game_type 0 """ game_type: int | str | bytes info: Sequence[Content]
[docs] @dataclass(init=False, slots=True) class retro_subsystem_memory_info(Structure): """ Describes a memory type associated with a subsystem ROM. Usually refers to save data, but not always. Corresponds to :c:type:`retro_subsystem_memory_info` in ``libretro.h``. """ extension: bytes | None """File extension used when saving this memory type to disk, e.g. ``b"srm"``.""" type: int """ Memory type identifier. Should be at least 0x100 to avoid conflict with standard memory types. Assigned values are bitwise-masked to fit into an :c:expr:`unsigned int`. .. seealso:: :obj:`.RETRO_MEMORY_SAVE_RAM` """ _fields_ = ( ("extension", c_char_p), ("type", c_uint), )
[docs] def __deepcopy__(self, _): """ Return a deep copy of this object, including copies of strings. Intended for use with :func:`copy.deepcopy`. """ return retro_subsystem_memory_info(self.extension, self.type)
[docs] @dataclass(init=False, slots=True) class retro_subsystem_rom_info(Structure): """ Describes a type of ROM (or other data) that can be used with a subsystem. Can be indexed like a :class:`~collections.abc.Sequence` to access this ROM type's associated memory info. Corresponds to :c:type:`retro_subsystem_rom_info` in ``libretro.h``. """ desc: bytes | None """Human-readable description of the content type.""" valid_extensions: bytes | None """Pipe-delimited string of accepted file extensions.""" need_fullpath: bool """ If :obj:`True`, the :class:`.ContentDriver` should populate :attr:`retro_game_info.path` without loading its content into memory. """ block_extract: bool """ If :obj:`True`, the :class:`.ContentDriver` should not extract content from compressed archives. """ required: bool """ If :obj:`True`, the :class:`.ContentDriver` should reject attempts to use the associated subsystem without this ROM. """ memory: TypedPointer[retro_subsystem_memory_info] | None """ Pointer to an array of memory descriptors associated with this subsystem ROM. May be :obj:`None` if there are no associated memory types (i.e. :attr:`num_memory` is ``0``). .. seealso:: :meth:`__getitem__` for a more Pythonic way of accessing this. """ num_memory: int """ Number of entries in the array pointed to by :attr:`memory`. Assigned values are bitwise-masked to fit into an :c:expr:`unsigned int`. .. seealso:: :meth:`__len__` for a more Pythonic way of accessing this. """ _fields_ = ( ("desc", c_char_p), ("valid_extensions", c_char_p), ("need_fullpath", c_bool), ("block_extract", c_bool), ("required", c_bool), ("memory", POINTER(retro_subsystem_memory_info)), ("num_memory", c_uint), )
[docs] def __len__(self): """ Return :attr:`num_memory`. >>> from libretro.api import retro_subsystem_rom_info >>> rom = retro_subsystem_rom_info(num_memory=0) >>> len(rom) 0 """ return int(self.num_memory)
@overload def __getitem__(self, index: int) -> retro_subsystem_memory_info: ... @overload def __getitem__( self, index: slice[retro_subsystem_memory_info] ) -> list[retro_subsystem_memory_info]: ...
[docs] def __getitem__(self, index: int | slice[retro_subsystem_memory_info]): """ Return a :class:`retro_subsystem_memory_info` at the given index, or a list of them if a slice is provided. :raises ValueError: If :attr:`memory` is :obj:`None`. :raises IndexError: If ``index`` is out of the range given by :attr:`num_memory`. """ if not self.memory: raise ValueError("No subsystem ROM memory types available") match index: case int(i): if not (0 <= i < self.num_memory): raise IndexError(f"Expected 0 <= index < {len(self)}, got {i}") return self.memory[i] case slice() as s: return self.memory[s] case _: raise TypeError(f"Expected an int or slice index, got {type(index).__name__}")
[docs] def __deepcopy__(self, memo: MemoDict = None): """ Return a deep copy of this object, including copies of all strings and subobjects. Intended for use with :func:`copy.deepcopy`. """ return retro_subsystem_rom_info( desc=self.desc, valid_extensions=self.valid_extensions, need_fullpath=self.need_fullpath, block_extract=self.block_extract, required=self.required, memory=(deepcopy_array(self.memory, self.num_memory, memo) if self.memory else None), num_memory=self.num_memory, )
@property def extensions(self) -> Iterator[bytes]: """ Yields each extension named in :attr:`valid_extensions`, or nothing if :attr:`valid_extensions` is :obj:`None`. >>> from libretro.api import retro_subsystem_rom_info >>> rom = retro_subsystem_rom_info(valid_extensions=b'bin|rom') >>> list(rom.extensions) [b'bin', b'rom'] """ if self.valid_extensions: yield from self.valid_extensions.split(b"|")
[docs] @dataclass(init=False, slots=True) class retro_subsystem_info(Structure): """ Describes a subsystem that supports loading zero or more content files. Corresponds to :c:type:`retro_subsystem_info` in ``libretro.h``. """ desc: bytes | None """Human-readable description of the subsystem.""" ident: bytes | None """Short identifier for the subsystem, used for lookups.""" roms: TypedPointer[retro_subsystem_rom_info] | None """ Pointer to an array of ROM info structures. .. seealso:: :meth:`__getitem__` for a more Pythonic way of accessing this. """ num_roms: int """ Number of entries in the array pointed to by :attr:`roms`. Assigned values are bitwise-masked to fit into an :c:expr:`unsigned int`. .. seealso:: :meth:`__len__` for a more Pythonic way of accessing this. """ id: int """ Unique identifier for this subsystem. Assigned values are bitwise-masked to fit into an :c:expr:`unsigned int`. """ _fields_ = ( ("desc", c_char_p), ("ident", c_char_p), ("roms", POINTER(retro_subsystem_rom_info)), ("num_roms", c_uint), ("id", c_uint), )
[docs] def __len__(self): """Return :attr:`num_roms`.""" return int(self.num_roms)
@overload def __getitem__(self, index: int) -> retro_subsystem_rom_info: ... @overload def __getitem__( self, index: slice[retro_subsystem_rom_info] ) -> list[retro_subsystem_rom_info]: ...
[docs] def __getitem__( self, index: int | slice[retro_subsystem_rom_info] ) -> retro_subsystem_rom_info | list[retro_subsystem_rom_info]: """ Return a :class:`retro_subsystem_rom_info` at the given index, or a list of them if a slice is provided. :raises ValueError: If :attr:`roms` is :obj:`None`. :raises IndexError: If ``index`` is out of range. """ if not self.roms: raise ValueError("No subsystem ROM types available") match index: case int(i): if not (0 <= i < self.num_roms): raise IndexError(f"Expected 0 <= index < {len(self)}, got {i}") return self.roms[i] case slice() as s: return self.roms[s] case _: raise TypeError(f"Expected an int or slice index, got {type(index).__name__}")
[docs] def __deepcopy__(self, memo: MemoDict = None): """ Return a deep copy of this subsystem info, including all strings and subobjects. Intended for use with :func:`copy.deepcopy`. >>> import copy >>> from libretro.api import retro_subsystem_info >>> sub = retro_subsystem_info(desc=b'SGB', ident=b'sgb', id=1) >>> copy.deepcopy(sub).ident b'sgb' """ return retro_subsystem_info( desc=self.desc, ident=self.ident, roms=deepcopy_array(self.roms, self.num_roms, memo) if self.roms else None, num_roms=self.num_roms, id=self.id, )
@property def extensions(self) -> Iterator[bytes]: """Yields all valid extensions across all ROM slots.""" for rom in self: yield from rom.extensions @property def by_extensions(self) -> Iterator[tuple[bytes, retro_subsystem_rom_info]]: """ Yields ``(extension, rom_info)`` pairs for all ROM slots. >>> from libretro.api import retro_subsystem_info >>> sub = retro_subsystem_info(num_roms=0) >>> list(sub.by_extensions) [] """ for info in self: for ext in info.extensions: yield ext, info
[docs] def by_extension(self, ext: str | bytes) -> retro_subsystem_rom_info: """ Return the ROM info for the given file extension. :param ext: The file extension (with or without leading dot). :raises KeyError: If no ROM slot supports the extension. >>> from libretro.api import retro_subsystem_info >>> sub = retro_subsystem_info(num_roms=0) >>> try: ... sub.by_extension('bin') ... except KeyError: ... print('Not found') Not found """ ext = as_bytes(ext).removeprefix(b".") for info in self: if ext in info.extensions: return info raise KeyError(f"Subsystem ROM with extension {ext!r} not found")
[docs] class Subsystems(Sequence[retro_subsystem_info]): """ An indexed and identifier-keyed collection of :class:`retro_subsystem_info`. Supports lookup by integer index, string identifier, or bytes identifier. >>> from libretro.api.content import Subsystems >>> from libretro.api import retro_subsystem_info >>> subs = Subsystems([retro_subsystem_info(desc=b'SGB', ident=b'sgb', id=1)]) >>> len(subs) 1 >>> subs[b'sgb'].id 1 """
[docs] def __init__(self, subsystems: Sequence[retro_subsystem_info]): """ Initialize from a sequence of :class:`retro_subsystem_info`. :param subsystems: The subsystem info entries. :raises TypeError: If *subsystems* is not a sequence or contains non-:class:`retro_subsystem_info` elements. >>> from libretro.api.content import Subsystems >>> subs = Subsystems([]) >>> len(subs) 0 """ if not isinstance(subsystems, Sequence): raise TypeError( f"Expected a sequence of retro_subsystem_info objects, got {type(subsystems).__name__}" ) if not all(isinstance(subsystem, retro_subsystem_info) for subsystem in subsystems): raise TypeError("All elements in the sequence must be retro_subsystem_info objects") self._subsystems = tuple(subsystems) self._subsystems_by_ident = {bytes(s.ident): s for s in subsystems if s.ident}
@overload def __getitem__(self, item: int | str | bytes) -> retro_subsystem_info: ... @overload def __getitem__(self, item: slice) -> Sequence[retro_subsystem_info]: ...
[docs] @override def __getitem__( self, item: int | str | bytes | slice ) -> retro_subsystem_info | Sequence[retro_subsystem_info]: """ Return a subsystem info by index or identifier. :param item: An integer index, string/bytes identifier, or slice. :raises IndexError: If an integer index is out of range. :raises KeyError: If a string/bytes identifier is not found. >>> from libretro.api.content import Subsystems >>> from libretro.api import retro_subsystem_info >>> subs = Subsystems([retro_subsystem_info(ident=b'sgb', id=1)]) >>> subs[0].id 1 """ length = len(self._subsystems) match item: case int() if -length <= item < length: return self._subsystems[item] case int(): raise IndexError(f"Expected {-length} <= index < {length}, got {item}") case str() | bytes(): ident = as_bytes(item) if ident in self._subsystems_by_ident: return self._subsystems_by_ident[ident] raise KeyError(f"Subsystem with identifier {item!r} not found") case _: raise TypeError(f"Expected an int, str, or bytes; got {type(item).__name__}")
[docs] @override def __contains__(self, item: str | bytes | retro_subsystem_info | object): """ Test for membership by identifier or value. >>> from libretro.api.content import Subsystems >>> from libretro.api import retro_subsystem_info >>> subs = Subsystems([retro_subsystem_info(ident=b'sgb')]) >>> b'sgb' in subs True """ match item: case str(ident) | bytes(ident): return as_bytes(ident) in self._subsystems_by_ident case retro_subsystem_info(): return item in self._subsystems case _: raise TypeError( f"Expected a str, bytes, or retro_subsystem_info object; got {type(item).__name__}" )
[docs] @override def __iter__(self) -> Iterator[retro_subsystem_info]: """ Iterate over the subsystem info entries. >>> from libretro.api.content import Subsystems >>> list(Subsystems([])) [] """ return iter(self._subsystems)
[docs] @override def __len__(self) -> int: """ Return the number of subsystem info entries. >>> from libretro.api.content import Subsystems >>> len(Subsystems([])) 0 """ return len(self._subsystems)
[docs] @dataclass(init=False, slots=True) class retro_system_content_info_override(Structure): """ Descriptors for core content that needs to be handled differently than described by :meth:`.Core.get_system_info`. Corresponds to :c:type:`retro_system_content_info_override` in ``libretro.h``. """ extensions: bytes | None need_fullpath: bool persistent_data: bool _fields_ = ( ("extensions", c_char_p), ("need_fullpath", c_bool), ("persistent_data", c_bool), )
[docs] def __deepcopy__(self, _): """ Return a deep copy. >>> import copy >>> from libretro.api import retro_system_content_info_override >>> ov = retro_system_content_info_override(extensions=b'bin') >>> copy.deepcopy(ov).extensions b'bin' """ return retro_system_content_info_override( extensions=self.extensions, need_fullpath=self.need_fullpath, persistent_data=self.persistent_data, )
[docs] def get_extensions(self) -> Iterator[bytes]: """ Yield each extension in the pipe-delimited :attr:`extensions` field. >>> from libretro.api import retro_system_content_info_override >>> ov = retro_system_content_info_override(extensions=b'a|b') >>> list(ov.get_extensions()) [b'a', b'b'] """ if self.extensions: yield from self.extensions.split(b"|")
[docs] class ContentInfoOverrides(Sequence[retro_system_content_info_override]): """ An indexed and extension-keyed collection of :class:`retro_system_content_info_override`. Supports lookup by integer index or by file extension. >>> from libretro.api.content import ContentInfoOverrides >>> from libretro.api import retro_system_content_info_override >>> overrides = ContentInfoOverrides([retro_system_content_info_override(extensions=b'bin')]) >>> len(overrides) 1 """
[docs] def __init__(self, overrides: Sequence[retro_system_content_info_override]): """ Initialize from a sequence of :class:`retro_system_content_info_override`. :param overrides: The override entries. >>> from libretro.api.content import ContentInfoOverrides >>> len(ContentInfoOverrides([])) 0 """ if not isinstance(overrides, Sequence): raise TypeError( f"Expected a sequence of retro_system_content_info_override objects, " f"got {type(overrides).__name__}" ) if not all( isinstance(override, retro_system_content_info_override) for override in overrides ): raise TypeError( "All elements in the sequence must be retro_system_content_info_override objects" ) self._overrides = tuple(overrides) overrides_by_ext: dict[bytes, retro_system_content_info_override] = {} for o in self._overrides: for e in o.get_extensions(): if e not in overrides_by_ext: # If this isn't a duplicate override... overrides_by_ext[e] = o # If an extension is listed more than once in a RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE call, # only the first occurrence is used. self._overrides_by_ext = MappingProxyType(overrides_by_ext)
@overload def __getitem__(self, item: int | str | bytes) -> retro_system_content_info_override: ... @overload def __getitem__(self, item: slice) -> Sequence[retro_system_content_info_override]: ...
[docs] @override def __getitem__( self, item: int | slice | str | bytes ) -> retro_system_content_info_override | Sequence[retro_system_content_info_override]: """ Return an override by index, extension, or slice. :param item: An integer index, extension string/bytes, or slice. :raises IndexError: If an integer index is out of range. :raises KeyError: If an extension is not found. >>> from libretro.api.content import ContentInfoOverrides >>> from libretro.api import retro_system_content_info_override >>> overrides = ContentInfoOverrides([retro_system_content_info_override(extensions=b'bin')]) >>> overrides[0].extensions b'bin' >>> overrides[b'bin'].extensions b'bin' """ match item: case int() if -len(self) <= item < len(self): return self._overrides[item] case int(): raise IndexError(f"Expected {-len(self)} <= index < {len(self)}, got {item}") case slice() as s: return self._overrides[s] case str() | bytes(): ext = as_bytes(item).removeprefix(b".") if ext in self._overrides_by_ext: return self._overrides_by_ext[ext] raise KeyError(f"Override for extension {item!r} not found") case _: raise TypeError(f"Expected an int, str, or bytes; got {type(item).__name__}")
[docs] @override def __contains__( self, item: str | bytes | retro_system_content_info_override | object ) -> bool: """ Test if the given item is in the overrides list. :param item: If a :class:`retro_system_content_info_override`, checks for an exact match. If :class:`str` or :class:`bytes`, checks for a matching extension. >>> from libretro.api.content import ContentInfoOverrides >>> from libretro.api import retro_system_content_info_override >>> overrides = ContentInfoOverrides([retro_system_content_info_override(extensions=b'bin')]) >>> b'bin' in overrides True >>> b'rom' in overrides False """ match item: case str(ext) | bytes(ext): return as_bytes(ext).removeprefix(b".") in self._overrides_by_ext case retro_system_content_info_override(): return item in self._overrides case _: raise TypeError( f"Expected a str, bytes, or retro_system_content_info_override object; got {type(item).__name__}" )
[docs] @override def __iter__(self) -> Iterator[retro_system_content_info_override]: """ Iterate over the override entries. >>> from libretro.api.content import ContentInfoOverrides >>> list(ContentInfoOverrides([])) [] """ return iter(self._overrides)
[docs] @override def __len__(self) -> int: """ Return the number of override entries. >>> from libretro.api.content import ContentInfoOverrides >>> len(ContentInfoOverrides([])) 0 """ return len(self._overrides)
@property def by_extension(self) -> Mapping[bytes, retro_system_content_info_override]: """ A read-only mapping from extension to override. >>> from libretro.api.content import ContentInfoOverrides >>> from libretro.api import retro_system_content_info_override >>> overrides = ContentInfoOverrides([retro_system_content_info_override(extensions=b'bin')]) >>> b'bin' in overrides.by_extension True """ return self._overrides_by_ext
[docs] @dataclass(init=False, slots=True) class retro_game_info_ext(Structure): """ An extended version of :class:`retro_game_info` with additional metadata about the content file, including archive and directory information. Corresponds to :c:type:`retro_game_info_ext` in ``libretro.h``. >>> from libretro.api import retro_game_info_ext >>> info = retro_game_info_ext(full_path=b'/games/test.bin', ext=b'bin') >>> info.ext b'bin' """ full_path: bytes | None """ Full path to the content file. Can be :obj:`None` if :attr:`file_in_archive` is :obj:`True`. """ archive_path: bytes | None """ Path to the archive containing the content file, if applicable. Can be :obj:`None` if :attr:`file_in_archive` is :obj:`False`. """ archive_file: bytes | None """ Path to the content file within the archive, if applicable. Can be :obj:`None` if :attr:`file_in_archive` is :obj:`False`. """ dir: bytes | None """ Path of the directory containing the content file if :attr:`file_in_archive` is :obj:`False`, or to the directory containing the archive itself if :obj:`True`. """ name: bytes | None """ A 'canonical' name for the content file without an extension, intended for loading complementary files. If :attr:`file_in_archive` is :obj:`False`, this is the basename of :attr:`full_path` without :attr:`ext`. Otherwise, it can be the basename of :attr:`archive_path` or :attr:`archive_file` (also without :attr:`ext`). """ ext: bytes | None """ Contains the file extension of the content file. If :attr:`file_in_archive` is :obj:`False`, this is the extension of :attr:`full_path`. Otherwise, it can be the extension of :attr:`archive_path`. """ meta: bytes | None """ Optional metadata string, similar to :attr:`retro_game_info.meta`. """ data: c_void_ptr | None """ Pointer to the content data in memory. Can be :obj:`None` if :attr:`~retro_system_info.need_fullpath` or an overriding :attr:`retro_system_content_info_override.need_fullpath` is :obj:`True`. """ size: int """ Size of the content data in bytes. Assigned values are bitwise-masked to fit into a ``size_t``. """ file_in_archive: bool """ :obj:`True` if the content is inside an archive file. """ persistent_data: bool """ If :obj:`True`, the :class:`.ContentDriver` must keep :attr:`data` in memory until :meth:`.Core.deinit` completes. Otherwise, the :class:`.ContentDriver` may unload it after :meth:`.Core.load_game` completes. """ _fields_ = ( ("full_path", c_char_p), ("archive_path", c_char_p), ("archive_file", c_char_p), ("dir", c_char_p), ("name", c_char_p), ("ext", c_char_p), ("meta", c_char_p), ("data", c_void_ptr), ("size", c_size_t), ("file_in_archive", c_bool), ("persistent_data", c_bool), )
[docs] def __deepcopy__(self, _): """ Return a deep copy of this object, including copies of all strings and content data. Intended for use with :func:`copy.deepcopy`. >>> import copy >>> from libretro.api import retro_game_info_ext >>> info = retro_game_info_ext(full_path=b'/test.bin') >>> copy.deepcopy(info).full_path b'/test.bin' """ return retro_game_info_ext( full_path=self.full_path, archive_path=self.archive_path, archive_file=self.archive_file, dir=self.dir, name=self.name, ext=self.ext, meta=self.meta, data=deepcopy_buffer(self.data, self.size), size=self.size, file_in_archive=self.file_in_archive, persistent_data=self.persistent_data, )
@overload def map_content(content: None) -> Iterator[None]: ... @overload def map_content(content: Content) -> Iterator[retro_game_info]: ...
[docs] @contextmanager def map_content( content: Content | None, ) -> Generator[retro_game_info | None]: """ Context manager for mapping a content file into memory. The content is mapped on entering, and unmapped on exiting. """ match content: case None: yield None case retro_game_info() as info: yield info case retro_game_info_ext() as info_ext: yield retro_game_info( path=info_ext.full_path, data=info_ext.data, size=info_ext.size, meta=info_ext.meta, ) case str() | PathLike() as path: with mmap_file(path) as contentview: # You can't directly get an address from a memoryview, # so you need to resort to C-like casting array_type = c_ubyte * len(contentview) buffer_array = array_type.from_buffer(contentview) info = retro_game_info( os.fsencode(path), addressof(buffer_array), len(contentview), None ) yield info del info del buffer_array del array_type # Need to clear all outstanding pointers, or else mmap will raise a BufferError case memoryview(): data = content.cast("B") array_type = c_ubyte * len(data) buffer_array = array_type.from_buffer(data) yield retro_game_info(data=addressof(buffer_array), size=len(data)) case bytes() | bytearray() as data: array_type = c_ubyte * len(data) buffer_array = array_type.from_buffer(data) yield retro_game_info(data=addressof(buffer_array), size=len(data)) case _: raise TypeError( f"Expected a content path, data, or retro_game_info object, got {type(content).__name__}" )
[docs] def get_extension(content: Content | retro_game_info_ext) -> bytes | None: """Return the extension of the content file or path, without a leading dot.""" match content: case ZipPath() as zippath: return zippath.suffix.encode().removeprefix(b".") case str() | PathLike() as path: _, e = os.path.splitext(os.fsencode(path)) return e.removeprefix(b".") case bytes() | bytearray() | memoryview() | retro_game_info(path=None): return None case retro_game_info(path=path) if path is not None: _, ext = os.path.splitext(path) return ext.removeprefix(b".") case retro_game_info_ext(): return content.ext case _: raise TypeError( f"Expected a str, path-like, buffer, retro_game_info, or retro_game_info_ext object; got {type(content).__name__}" )
__all__ = [ "retro_system_info", "Content", "ContentData", "ContentPath", "SubsystemContent", "retro_game_info", "retro_subsystem_memory_info", "retro_subsystem_rom_info", "retro_subsystem_info", "retro_system_content_info_override", "retro_game_info_ext", "map_content", "get_extension", "Subsystems", "ContentInfoOverrides", ]