"""
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",
]