Mercurial > touhou
changeset 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 | 1ada4036ab88 |
| files | formats/src/th06/std.rs python/src/lib.rs pytouhou/formats/std.py pytouhou/resource/loader.py |
| diffstat | 4 files changed, 82 insertions(+), 212 deletions(-) [+] |
line wrap: on
line diff
--- a/formats/src/th06/std.rs +++ b/formats/src/th06/std.rs @@ -218,7 +218,6 @@ // TODO: replace this assert with a custom error. assert_eq!(size, 12); let (i, instr) = parse_instruction_args(i, opcode)?; - println!("{} {:?}", time, instr); let call = Call { time, instr }; Ok((i, call)) }
--- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,7 +1,8 @@ -use pyo3::exceptions::PyIOError; +use pyo3::exceptions::{PyIOError, PyKeyError}; use pyo3::prelude::*; -use pyo3::types::PyBytes; +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; @@ -11,6 +12,69 @@ 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>>, @@ -111,13 +175,24 @@ /// 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(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()) + let bytes = archive.get_file_internal(&name); + let (_, inner) = stage::Stage::from_slice(&bytes).unwrap(); + Ok(Py::new(py, PyStage { inner })?) } }
deleted file mode 100644 --- a/pytouhou/formats/std.py +++ /dev/null @@ -1,198 +0,0 @@ -# -*- encoding: utf-8 -*- -## -## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com> -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation; version 3 only. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## - -"""Stage Definition (STD) files handling. - -This module provides classes for handling the Stage Definition file format. -The STD file format is a format used in Touhou 6: EoSD to describe non-gameplay -aspects of a stage: its name, its music, 3D models composing its background, -and various scripted events such as camera movement. -""" - - -from struct import pack, unpack, calcsize -from pytouhou.utils.helpers import read_string, get_logger - -logger = get_logger(__name__) - - -class Model: - def __init__(self, unknown=0, bounding_box=None, quads=None): - self.unknown = 0 - self.bounding_box = bounding_box or (0., 0., 0., - 0., 0., 0.) - self.quads = quads or [] - - - -class Stage: - """Handle Touhou 6 Stage Definition files. - - Stage Definition files are structured files describing non-gameplay aspects - aspects of a stage. They are split in a header an 3 additional sections. - - The header contains the name of the stage, the background musics (BGM) used, - as well as the number of quads and objects composing the background. - The first section describes the models composing the background, whereas - the second section dictates how they are used. - The last section describes the changes to the camera, fog, and other things. - - Instance variables: - name -- name of the stage - bgms -- list of (name, path) describing the different background musics used - models -- list of Model objects - object_instances -- list of instances of the aforementioned models - script -- stage script (camera, fog, etc.) - """ - - _instructions = {0: ('fff', 'set_viewpos'), - 1: ('BBBxff', 'set_fog'), - 2: ('fff', 'set_viewpos2'), - 3: ('Ixxxxxxxx', 'start_interpolating_viewpos2'), - 4: ('Ixxxxxxxx', 'start_interpolating_fog')} - - def __init__(self): - self.name = '' - self.bgms = (('', ''), ('', ''), ('', ''), ('', '')) - self.models = [] - self.object_instances = [] - self.script = [] - - - @classmethod - def read(cls, file): - """Read a Stage Definition file. - - Raise an exception if the file is invalid. - Return a STD instance otherwise. - """ - - stage = Stage() - - nb_models, nb_faces = unpack('<HH', file.read(4)) - object_instances_offset, script_offset, zero = unpack('<III', file.read(12)) - assert zero == 0 - - stage.name = read_string(file, 128, 'shift_jis') - - bgm_a = read_string(file, 128, 'shift_jis') - bgm_b = read_string(file, 128, 'shift_jis') - bgm_c = read_string(file, 128, 'shift_jis') - bgm_d = read_string(file, 128, 'shift_jis') - - bgm_a_path = read_string(file, 128, 'ascii') - bgm_b_path = read_string(file, 128, 'ascii') - bgm_c_path = read_string(file, 128, 'ascii') - bgm_d_path = read_string(file, 128, 'ascii') - - stage.bgms = [None if bgm[0] == u' ' else bgm - for bgm in ((bgm_a, bgm_a_path), (bgm_b, bgm_b_path), (bgm_c, bgm_c_path), (bgm_d, bgm_d_path))] - - # Read model definitions - offsets = unpack('<%s' % ('I' * nb_models), file.read(4 * nb_models)) - for offset in offsets: - model = Model() - file.seek(offset) - - # Read model header - id_, unknown, x, y, z, width, height, depth = unpack('<HHffffff', file.read(28)) - model.unknown = unknown - model.bounding_box = x, y, z, width, height, depth #TODO: check - - # Read model quads - while True: - unknown, size = unpack('<HH', file.read(4)) - if unknown == 0xffff: - break - assert size == 0x1c - script_index, x, y, z, width, height = unpack('<Hxxfffff', file.read(24)) - model.quads.append((script_index, x, y, z, width, height)) - stage.models.append(model) - - - # Read object usages - file.seek(object_instances_offset) - while True: - obj_id, unknown, x, y, z = unpack('<HHfff', file.read(16)) - if (obj_id, unknown) == (0xffff, 0xffff): - break - assert unknown == 256 #TODO: really? - stage.object_instances.append((obj_id, x, y, z)) - - - # Read the script - file.seek(script_offset) - while True: - frame, opcode, size = unpack('<IHH', file.read(8)) - if (frame, opcode, size) == (0xffffffff, 0xffff, 0xffff): - break - assert size == 0x0c - data = file.read(size) - if opcode in cls._instructions: - args = unpack('<%s' % cls._instructions[opcode][0], data) - else: - args = (data,) - logger.warning('unknown opcode %d', opcode) - stage.script.append((frame, opcode, args)) - - return stage - - - def write(self, file): - """Write to a Stage Definition file.""" - model_offsets = [] - second_section_offset = 0 - third_section_offset = 0 - - nb_faces = sum(len(model.quads) for model in self.models) - - # Write header (offsets, number of quads, name and background musics) - file.write(pack('<HH', len(self.models), nb_faces)) - file.write(pack('<II', 0, 0)) - file.write(pack('<I', 0)) - file.write(pack('<128s', self.name.encode('shift_jis'))) - for bgm_name, bgm_path in self.bgms: - file.write(pack('<128s', bgm_name.encode('shift_jis'))) - for bgm_name, bgm_path in self.bgms: - file.write(pack('<128s', bgm_path.encode('ascii'))) - file.write(b'\x00\x00\x00\x00' * len(self.models)) - - # Write first section (models) - for i, model in enumerate(self.models): - model_offsets.append(file.tell()) - file.write(pack('<HHffffff', i, model.unknown, *model.bounding_box)) - for quad in model.quads: - file.write(pack('<HH', 0x00, 0x1c)) - file.write(pack('<Hxxfffff', *quad)) - file.write(pack('<HH', 0xffff, 4)) - - # Write second section (object instances) - second_section_offset = file.tell() - for obj_id, x, y, z in self.object_instances: - file.write(pack('<HHfff', obj_id, 256, x, y, z)) - file.write(b'\xff' * 16) - - # Write third section (script) - third_section_offset = file.tell() - for frame, opcode, args in self.script: - size = calcsize(self._instructions[opcode][0]) - file.write(pack('<IHH%s' % self._instructions[opcode][0], frame, opcode, size, *args)) - file.write(b'\xff' * 20) - - # Fix offsets - file.seek(4) - file.write(pack('<II', second_section_offset, third_section_offset)) - file.seek(16+128+128*2*4) - file.write(pack('<%sI' % len(self.models), *model_offsets)) -
--- a/pytouhou/resource/loader.py +++ b/pytouhou/resource/loader.py @@ -13,7 +13,6 @@ ## from libtouhou import Loader as RustLoader -from pytouhou.formats.std import Stage from pytouhou.formats.ecl import ECL from pytouhou.formats.anm0 import ANM0 from pytouhou.formats.msg import MSG @@ -44,11 +43,6 @@ return anm - def get_stage(self, name): - file = self.get_file(name) - return Stage.read(file) #TODO: modular - - def get_ecl(self, name): file = self.get_file(name) return ECL.read(file) #TODO: modular
