Mercurial > touhou
changeset 778:816e1f01d650
Partially replace the Loader with a Rust one
| author | Link Mauve <linkmauve@linkmauve.fr> |
|---|---|
| date | Sat, 08 Nov 2025 18:26:01 +0100 |
| parents | 11249e4b4e03 |
| children | ee09657d3789 |
| files | python/Cargo.toml python/src/lib.rs pytouhou/resource/loader.py |
| diffstat | 3 files changed, 95 insertions(+), 92 deletions(-) [+] |
line wrap: on
line diff
--- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "touhou-python" version = "0.1.0" -edition = "2021" +edition = "2024" authors = [ "Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>", ] @@ -14,8 +14,9 @@ [dependencies] touhou-formats = "*" -pyo3 = "0.26" +pyo3 = "0.27" image = { version = "0.25", default-features = false, features = ["png"], optional = true } +glob = "0.3.3" [features] default = []
--- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,27 +1,42 @@ +use pyo3::exceptions::PyIOError; use pyo3::prelude::*; use pyo3::types::PyBytes; use touhou_formats::th06::pbg3; +use std::collections::HashMap; use std::fs::File; use std::io::BufReader; +use std::path::PathBuf; #[cfg(feature = "glide")] mod glide; -#[pyclass] +#[pyclass(module = "libtouhou")] struct PBG3 { + filename: PathBuf, inner: pbg3::PBG3<BufReader<File>>, } +impl PBG3 { + fn get_file_internal(&mut self, name: &str) -> Vec<u8> { + self.inner.get_file(name, true).unwrap() + } +} + #[pymethods] impl PBG3 { #[staticmethod] - fn from_filename(filename: &str) -> PyResult<PBG3> { - let inner = pbg3::from_path_buffered(filename)?; + fn from_filename(filename: PathBuf) -> PyResult<PBG3> { + let inner = pbg3::from_path_buffered(&filename)?; Ok(PBG3 { + filename, inner }) } + fn __repr__(&self) -> String { + format!("PBG3({})", self.filename.to_str().unwrap()) + } + #[getter] fn file_list(&self) -> Vec<String> { self.inner.list_files().cloned().collect() @@ -31,16 +46,85 @@ self.inner.list_files().cloned().collect() } - fn get_file(&mut self, py: Python, name: &str) -> Py<PyAny> { - let data = self.inner.get_file(name, true).unwrap(); + fn get_file(&mut self, py: Python, name: &str) -> Py<PyBytes> { + let data = self.get_file_internal(name); PyBytes::new(py, &data).into() } } +/// A loader for Touhou files. +#[pyclass(module = "libtouhou", get_all, subclass)] +#[derive(Default)] +struct Loader { + /// The file names to the possible executable. + exe_files: Vec<PathBuf>, + + /// The path to the game directory. + game_dir: Option<PathBuf>, + + /// A map from inner filenames to the archive containing them. + known_files: HashMap<String, Py<PBG3>>, +} + +#[pymethods] +impl Loader { + /// Create a new Loader for the given game_dir. + #[new] + fn new(game_dir: Option<PathBuf>) -> Loader { + Loader { + exe_files: Vec::new(), + game_dir, + known_files: HashMap::new(), + } + } + + /// Scan the game_dir for archives. + /// + /// paths_lists is a list of ':'-separated glob patterns, the first matching file will be used + /// and the other ones ignored. + fn scan_archives(&mut self, py: Python, paths_lists: Vec<String>) -> PyResult<()> { + for paths in paths_lists.iter() { + let found_paths: Vec<_> = paths.split(':').map(|path| { + glob::glob(if let Some(game_dir) = self.game_dir.as_ref() { + game_dir.join(path) + } else { + PathBuf::from(path) + }.to_str().unwrap()).unwrap() + }).flatten().map(Result::unwrap).map(PathBuf::from).collect(); + if found_paths.is_empty() { + return Err(PyIOError::new_err(format!("No path found for {paths:?}"))); + } + let path = &found_paths[0]; + if let Some(extension) = path.extension() && extension == "exe" { + self.exe_files.extend(found_paths); + } else { + let pbg3 = PBG3::from_filename(path.to_owned())?; + let filenames = pbg3.list_files(); + let pbg3 = Py::new(py, pbg3)?; + for name in filenames { + self.known_files.insert(name.clone(), Py::clone_ref(&pbg3, py)); + } + } + } + Ok(()) + } + + /// Return the given file as an io.BytesIO object. + fn get_file(&self, py: Python, name: String) -> PyResult<Py<PyAny>> { + let archive = self.known_files.get(&name).unwrap(); + let mut archive = archive.borrow_mut(py); + let bytes = archive.get_file(py, &name); + let io = py.import("io")?; + let bytesio_class = io.dict().get_item("BytesIO")?.unwrap(); + let file = bytesio_class.call1((bytes,))?; + Ok(file.unbind()) + } +} + #[pymodule] mod libtouhou { #[pymodule_export] - use super::PBG3; + use super::Loader; #[cfg(feature = "glide")] #[pymodule_export]
--- a/pytouhou/resource/loader.py +++ b/pytouhou/resource/loader.py @@ -12,13 +12,7 @@ ## GNU General Public License for more details. ## -import os -from glob import glob -from itertools import chain -from io import BytesIO - -from pytouhou.formats import WrongFormatError -from libtouhou import PBG3 +from libtouhou import Loader as RustLoader from pytouhou.formats.std import Stage from pytouhou.formats.ecl import ECL from pytouhou.formats.anm0 import ANM0 @@ -34,88 +28,12 @@ -class Directory: - def __init__(self, path): - self.path = path - - - def __enter__(self): - return self - - - def __exit__(self, type, value, traceback): - return False - - - @property - def file_list(self): - return self.list_files() - - def list_files(self): - file_list = [] - for path in os.listdir(self.path): - if os.path.isfile(os.path.join(self.path, path)): - file_list.append(path) - return file_list - - - def get_file(self, name): - return open(os.path.join(self.path, str(name)), 'rb') - - - -class ArchiveDescription: - _formats = {b'PBG3': PBG3} - - @classmethod - def get_from_path(cls, path): - if os.path.isdir(path): - instance = Directory(path) - file_list = instance.list_files() - return instance - with open(path, 'rb') as file: - magic = file.read(4) - format_class = cls._formats[magic] - return format_class.from_filename(path) - - - -class Loader: +class Loader(RustLoader): def __init__(self, game_dir=None): - self.exe_files = [] - self.game_dir = game_dir - self.known_files = {} self.instanced_anms = {} # Cache for the textures. self.loaded_anms = [] # For the double loading warnings. - def scan_archives(self, paths_lists): - for paths in paths_lists: - def _expand_paths(): - for path in paths.split(os.path.pathsep): - if self.game_dir and not os.path.isabs(path): - path = os.path.join(self.game_dir, path) - yield glob(path) - paths = list(chain(*_expand_paths())) - if not paths: - raise IOError - path = paths[0] - if os.path.splitext(path)[1] == '.exe': - self.exe_files.extend(paths) - else: - archive_description = ArchiveDescription.get_from_path(path) - for name in archive_description.file_list: - self.known_files[name] = archive_description - - - def get_file(self, name): - archive = self.known_files[name] - file = archive.get_file(name) - if isinstance(file, bytes): - return BytesIO(file) - return file - - def get_anm(self, name): if name in self.loaded_anms: logger.warning('ANM0 %s already loaded', name)
