Mercurial > touhou
view python/src/lib.rs @ 779:ee09657d3789
Replace the stage parser with the Rust one
| author | Link Mauve <linkmauve@linkmauve.fr> |
|---|---|
| date | Sat, 08 Nov 2025 19:29:33 +0100 |
| parents | 816e1f01d650 |
| children | 5b43c42fa680 |
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; #[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() } } #[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>> { if let Some(archive) = self.known_files.get(&name) { 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()) } else { Err(PyKeyError::new_err(format!("Unknown file {name:?}"))) } } fn get_stage(&self, py: Python, name: String) -> PyResult<Py<PyStage>> { let archive = self.known_files.get(&name).unwrap(); let mut archive = archive.borrow_mut(py); let bytes = archive.get_file_internal(&name); let (_, inner) = stage::Stage::from_slice(&bytes).unwrap(); Ok(Py::new(py, PyStage { inner })?) } } #[pymodule] mod libtouhou { #[pymodule_export] use super::Loader; #[cfg(feature = "glide")] #[pymodule_export] use super::glide::module; }
