view python/src/lib.rs @ 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 28d8b892fd06
children ee09657d3789
line wrap: on
line source

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(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: 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()
    }

    fn list_files(&self) -> Vec<String> {
        self.inner.list_files().cloned().collect()
    }

    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::Loader;

    #[cfg(feature = "glide")]
    #[pymodule_export]
    use super::glide::module;
}