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