view python/src/lib.rs @ 781:5b43c42fa680

Stop exposing PBG3 to Python
author Link Mauve <linkmauve@linkmauve.fr>
date Sun, 09 Nov 2025 19:50:35 +0100
parents ee09657d3789
children a30ce01b9154
line wrap: on
line source

use pyo3::exceptions::{PyIOError, PyKeyError};
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyTuple};
use touhou_formats::th06::pbg3;
use touhou_formats::th06::std as stage;
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

#[cfg(feature = "glide")]
mod glide;

#[pyclass(module = "libtouhou")]
struct PyModel {
    inner: stage::Model,
}

#[pymethods]
impl PyModel {
    #[getter]
    fn quads(&self) -> Vec<(u16, f32, f32, f32, f32, f32)> {
        self.inner.quads.iter().map(|quad| (quad.anm_script, quad.pos.x, quad.pos.y, quad.pos.z, quad.size_override.width, quad.size_override.height)).collect()
    }

    #[getter]
    fn bounding_box(&self) -> [f32; 6] {
        self.inner.bounding_box
    }
}

#[pyclass(module = "libtouhou")]
struct PyStage {
    inner: stage::Stage,
}

#[pymethods]
impl PyStage {
    #[getter]
    fn models(&self) -> Vec<PyModel> {
        self.inner.models.clone().into_iter().map(|inner| PyModel { inner }).collect()
    }

    #[getter]
    fn object_instances(&self) -> Vec<(u16, f32, f32, f32)> {
        self.inner.instances.iter().map(|instance| (instance.id, instance.pos.x, instance.pos.y, instance.pos.z)).collect()
    }

    #[getter]
    fn name(&self) -> &str {
        &self.inner.name
    }

    #[getter]
    fn bgms(&self) -> Vec<Option<(String, String)>> {
        self.inner.musics.clone()
    }

    #[getter]
    fn script(&self, py: Python) -> Vec<(u32, u16, Py<PyTuple>)> {
        fn call_to_python(py: Python, call: &stage::Call) -> PyResult<(u32, u16, Py<PyTuple>)> {
            let (opcode, args) = match call.instr {
                stage::Instruction::SetViewpos(x, y, z) => (0, (x, y, z).into_pyobject(py)?.unbind()),
                stage::Instruction::SetFog(r, g, b, _a, near, far) => (1, (r, g, b, near, far).into_pyobject(py)?.unbind()),
                stage::Instruction::SetViewpos2(x, y, z) => (2, (x, y, z).into_pyobject(py)?.unbind()),
                stage::Instruction::StartInterpolatingViewpos2(frame, _unused1, _unused2) => (3, (frame,).into_pyobject(py)?.unbind()),
                stage::Instruction::StartInterpolatingFog(frame, _unused1, _unused2) => (4, (frame,).into_pyobject(py)?.unbind()),
                stage::Instruction::Unknown(unused1, unused2, unused3) => (5, (unused1, unused2, unused3).into_pyobject(py)?.unbind()),
            };
            Ok((call.time, opcode, args))
        }

        self.inner.script.iter().map(|call| call_to_python(py, call).unwrap()).collect()
    }
}

/// A loader for Touhou files.
#[pyclass(module = "libtouhou", subclass)]
#[derive(Default)]
struct Loader {
    /// The file names to the possible executable.
    #[pyo3(get)]
    exe_files: Vec<PathBuf>,

    /// The path to the game directory.
    #[pyo3(get)]
    game_dir: Option<PathBuf>,

    /// A map from inner filenames to the archive containing them.
    known_files: HashMap<String, Arc<Mutex<pbg3::PBG3<BufReader<File>>>>>,
}

#[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, 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_path_buffered(path)?;
                let filenames: Vec<_> = pbg3.list_files().cloned().collect();
                let pbg3 = Arc::new(Mutex::new(pbg3));
                for name in filenames {
                    self.known_files.insert(name.clone(), Arc::clone(&pbg3));
                }
            }
        }
        Ok(())
    }

    /// Return the given file as a Vec<u8>.
    fn get_file_internal(&self, name: &str) -> PyResult<Vec<u8>> {
        if let Some(archive) = self.known_files.get(name) {
            let mut archive = archive.lock().unwrap();
            let bytes = archive.get_file(name, true)?;
            Ok(bytes)
        } else {
            Err(PyKeyError::new_err(format!("Unknown file {name:?}")))
        }
    }

    /// Return the given file as an io.BytesIO object.
    fn get_file(&self, py: Python, name: String) -> PyResult<Py<PyAny>> {
        let vec = self.get_file_internal(&name)?;
        let bytes = PyBytes::new(py, &vec);
        let io = py.import("io")?;
        let bytesio_class = io.dict().get_item("BytesIO")?.unwrap();
        let file = bytesio_class.call1((bytes,))?;
        Ok(file.unbind())
    }

    fn get_stage(&self, py: Python, name: String) -> PyResult<Py<PyStage>> {
        let vec = self.get_file_internal(&name)?;
        let (_, inner) = stage::Stage::from_slice(&vec).unwrap();
        Ok(Py::new(py, PyStage { inner })?)
    }
}

#[pymodule]
mod libtouhou {
    #[pymodule_export]
    use super::Loader;

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