Mercurial > touhou
view python/src/main.rs @ 795:2d60a14f4816 default tip
python: Rewrite the main entrypoint in Rust
This lets us progressively replace Python modules with Rust ones.
Currently missing features include:
- Saving replays
- Networking code for cooperative mode
- Reading a configuration file for options
- Maybe more.
But the base game is working, so yay!
| author | Link Mauve <linkmauve@linkmauve.fr> |
|---|---|
| date | Tue, 02 Jun 2026 19:06:16 +0200 |
| parents | |
| children |
line wrap: on
line source
//! Main entrypoint for PyTouhou, now in Rust. // Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com> // Copyright (C) 2026 Link Mauve <linkmauve@linkmauve.fr> // // 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. use clap::{Parser, ValueEnum}; use pyo3::{prelude::*, types::PyDict}; use std::{fs::File, path::PathBuf}; use touhou_formats::th06::t6rp::{T6rp, Level}; #[derive(Debug, Clone, ValueEnum)] enum Verbosity { Debug, Info, Warning, Error, Critical, } #[derive(Debug, Clone, ValueEnum)] enum Game { EoSD, Sample, } #[derive(Debug, Clone, ValueEnum)] enum Frontend { Sdl, Glfw, } impl Frontend { const fn to_str(&self) -> &'static str { match self { Frontend::Sdl => "sdl", Frontend::Glfw => "glfw", } } } #[derive(Debug, Clone, ValueEnum)] enum Backend { OpenGL, Glide, Sdl, } impl Backend { const fn to_str(&self) -> &'static str { match self { Backend::OpenGL => "opengl", Backend::Glide => "glide", Backend::Sdl => "sdl", } } } #[derive(Debug, Clone, ValueEnum)] enum GlFlavor { Core, Es, Compatibility, Legacy, } impl GlFlavor { const fn to_str(&self) -> &'static str { match self { GlFlavor::Core => "core", GlFlavor::Es => "es", GlFlavor::Compatibility => "compatibility", GlFlavor::Legacy => "legacy", } } } /// Libre reimplementation of the Touhou 6 engine. #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Options { /// Game’s data files. #[arg(short, long/*, default_value_t = vec!["CM.DAT", "ST.DAT", "IN.DAT", "MD.DAT", "102h.exe"]*/)] data: Vec<PathBuf>, /// Game directory path. #[arg(short, long/*, default_value_t = PathBuf::from(".")*/)] path: Option<PathBuf>, /// Set unlimited continues, and perhaps other debug features. #[arg(long)] debug: bool, /// Select the wanted logging level. #[arg(long, value_enum, default_value_t = Verbosity::Warning)] verbosity: Verbosity, /// Stage, 1 to 7 (Extra), nothing means story mode. #[arg(short, long)] stage: Option<u8>, /// Rank, from 0 (Easy, default) to 3 (Lunatic). #[arg(short, long, default_value_t = 0)] rank: u8, /// Select the character to use, from 0 (ReimuA, default) to 3 (MarisaB). #[arg(short, long, default_value_t = 0)] character: u8, /// Fight only bosses. #[arg(short, long)] boss_rush: bool, /// Select the game engine to use. #[arg(long, value_enum, default_value_t = Game::EoSD)] game: Game, /// Select the interface to use. #[arg(long, value_enum, default_value_t = Game::EoSD)] interface: Game, /// Hints file, to display text while playing. #[arg(long)] hints: Option<String>, /// Select a file to replay. #[arg(long)] replay: Option<String>, /// Save the upcoming game into a replay file. #[arg(long)] save_replay: Option<String>, /// Skip the replay and start to play when it’s finished. #[arg(long)] skip_replay: bool, /// Local port to use. #[arg(long, default_value_t = 0)] port: u16, /// Remote address. #[arg(long)] remote: Option<String>, /// Allow friendly-fire during netplay. #[arg(long)] friendly_fire: bool, /// Which windowing library to use (glfw or sdl). #[arg(long, value_enum, default_value_t = Frontend::Glfw)] frontend: Frontend, /// Which backend to use (opengl, glide or sdl). #[arg(long, value_enum, default_value_t = Backend::OpenGL)] backend: Backend, /// Set fps limit. A value of 0 disables fps limiting, while a negative value limits to 60 fps if and only if vsync doesn’t work. #[arg(long, default_value_t = -1)] fps_limit: i16, /// Set the frameskip, as 1/FRAMESKIP, or disabled if 0. #[arg(long, default_value_t = 1)] frameskip: i8, /// Disable background display (huge performance boost on slow systems). #[arg(long)] no_background: bool, /// Disable particles handling (huge performance boost on slow systems). #[arg(long)] no_particles: bool, /// Disable music and sound effects. #[arg(long)] no_sound: bool, /// OpenGL profile to use. #[arg(long, value_enum, default_value_t = GlFlavor::Compatibility)] gl_flavor: GlFlavor, /// OpenGL version to use. #[arg(long, default_value_t = 2.1)] gl_version: f32, /// Enable double buffering. #[arg(long)] double_buffer: Option<bool>, } fn main() -> PyResult<()> { let mut args = Options::parse(); if args.data.is_empty() { args.data = vec!["CM.DAT".into(), "ST.DAT".into(), "IN.DAT".into(), "MD.DAT".into(), "102h.exe".into()]; } if args.path.is_none() { args.path = Some(".".into()); } Python::initialize(); Python::attach(|py| { let game = PyModule::import(py, "pytouhou.games.eosd.game").unwrap(); let Common = game.getattr("Common").unwrap(); let Game = game.getattr("Game").unwrap(); let interface = PyModule::import(py, "pytouhou.games.eosd.interface").unwrap(); let Interface = interface.getattr("Interface").unwrap(); let width: usize = Interface.getattr("width").unwrap().extract().unwrap(); let height: usize = Interface.getattr("height").unwrap().extract().unwrap(); let backend = PyModule::import(py, "pytouhou.ui.opengl.backend").unwrap(); let backend_init = backend.getattr("init").unwrap(); let options = PyDict::new(py); options.set_item("flavor", args.gl_flavor.to_str()).unwrap(); options.set_item("version", args.gl_version.to_string()).unwrap(); options.set_item("double-buffer", args.double_buffer).unwrap(); options.set_item("frontend", args.frontend.to_str()).unwrap(); backend_init.call1((options,)).unwrap(); let GameRenderer = backend.getattr("GameRenderer").unwrap(); let window = PyModule::import(py, "pytouhou.ui.window").unwrap(); let Window = window.getattr("Window").unwrap(); let window = Window.call1((backend, width, height, args.fps_limit, args.frameskip)).unwrap(); let gamerunner = PyModule::import(py, "pytouhou.ui.gamerunner").unwrap(); let GameRunner = gamerunner.getattr("GameRunner").unwrap(); let loader = PyModule::import(py, "pytouhou.resource.loader").unwrap(); let Loader = loader.getattr("Loader").unwrap(); let resource_loader = Loader.call1((args.path,)).unwrap(); let scan_archives = resource_loader.getattr("scan_archives").unwrap(); let data: Vec<_> = args.data.into_iter().map(|x| x.display().to_string()).collect(); if let Err(err) = scan_archives.call1((data,)) { eprintln!("Some data files were not found, did you forget the -p option?"); eprintln!("{err}"); std::process::exit(1); } let libtouhou = PyModule::import(py, "libtouhou").unwrap(); let Prng = libtouhou.getattr("Prng").unwrap(); let mut stage_num; let story; let mut continues; if let Some(stage) = args.stage { stage_num = stage as usize; story = false; continues = 0; } else { stage_num = 1; story = true; continues = 3; } if args.debug { continues = -1; // Infinite lives } let mut replay = None; let rank; let character; if let Some(replay_path) = args.replay { let data = std::fs::read(replay_path).unwrap(); let (encrypted, header) = T6rp::parse_header(&data).unwrap(); let decrypted = T6rp::decrypt(header.key, encrypted); assert!(T6rp::verify(header.key, header.checksum, &decrypted)); let (_, replay2) = T6rp::from_slice(&decrypted).unwrap(); replay = Some(replay2); rank = header.rank; character = header.character; } else { rank = args.rank; character = args.character; } let mut save_keystates: Option<Vec<u8>> = None; /* if let Some(save_filename) = args.save_replay { let save_replay = T6RP(); save_replay.rank = rank; save_replay.character = character; } */ let mut difficulty = 16; let selected_player; let characters; let con; /* let addr; let prng; if args.port != 0 { if let Some(remote) = args.remote { let (remote_addr, remote_port) = remote.split_once(':').unwrap(); addr = Some((remote_addr, u16::from_str_radix(remote_port, 10))); selected_player = 0; } else { addr = None; selected_player = 1; } prng = Prng::new(0); con = Network(args.port, addr, selected_player); characters = [1, 3]; } else { */ con = None::<u32>; selected_player = 0; characters = [character]; /* } */ if let Some(hints_filename) = args.hints { let data = std::fs::read(hints_filename).unwrap(); //let hints = Hint.read(data); } let game_class = Game/*if args.boss_rush { GameBossRush } else { Game }*/; let common = Common.call1((resource_loader.clone(), characters, continues)).unwrap(); let first_player = common.getattr("players").unwrap().get_item(0).unwrap(); let interface = Interface.call1((resource_loader.clone(), first_player.clone())).unwrap(); common.setattr("interface", interface).unwrap(); let renderer = if !GameRenderer.is_none() { Some(GameRenderer.call1((resource_loader.clone(), window.clone())).unwrap()) } else { None }; let runner = GameRunner.call1((window.clone(), renderer, common.clone(), resource_loader.clone(), args.skip_replay, con, args.no_sound)).unwrap(); let window_set_runner = window.getattr("set_runner").unwrap(); window_set_runner.call1((runner.clone(),)).unwrap(); loop { let prng; if let Some(replay) = &replay { let Some(level) = &replay.levels[stage_num - 1] else { break; }; prng = Prng.call1((level.random_seed,)).unwrap(); // TODO: apply the replay to the other players. // TODO: see if the stored score is used or if it’s the one from the previous stage. if stage_num != 1 && replay.levels[stage_num - 2].is_some() { if let Some(previous_level) = &replay.levels[stage_num - 1] { first_player.setattr("score", previous_level.score).unwrap(); first_player.setattr("effective_score", previous_level.score).unwrap(); } } first_player.setattr("points", level.point_items).unwrap(); first_player.setattr("power", level.power).unwrap(); first_player.setattr("lives", level.lives).unwrap(); first_player.setattr("bombs", level.bombs).unwrap(); difficulty = level.difficulty; } else if args.port == 0 { prng = Prng.call0().unwrap(); } else { panic!("TODO: Remove that branch!"); } if args.save_replay.is_some() { if replay.is_none() { let keys = Vec::new(); let level = Level { score: first_player.getattr("score").unwrap().extract().unwrap(), random_seed: prng.getattr("seed").unwrap().extract().unwrap(), point_items: first_player.getattr("points").unwrap().extract().unwrap(), power: first_player.getattr("power").unwrap().extract().unwrap(), lives: first_player.getattr("lives").unwrap().extract().unwrap(), bombs: first_player.getattr("bombs").unwrap().extract().unwrap(), difficulty, unknown: 0, keys, }; //save_replay.levels[stage_num - 1] = level; } save_keystates = Some(Vec::new()); } let hints_stage = if false/*hints*/ { Some(true) // hints.stages[stage_num - 1] } else { None }; let game = game_class.call1((resource_loader.clone(), stage_num, args.rank, difficulty, common.clone(), prng.clone(), hints_stage, args.friendly_fire)).unwrap(); if args.no_particles { let new_particle = pyo3::types::PyCFunction::new_closure(py, Some(c"new_particle"), None, |_, _| ()).unwrap(); game.setattr("new_particle", new_particle).unwrap(); } let background = if args.no_background { None } else { Some(game.getattr("background").unwrap()) }; let runner_load_game = runner.getattr("load_game").unwrap(); let game_std = game.getattr("std").unwrap(); let game_std_bgms = game_std.getattr("bgms").unwrap(); runner_load_game.call1((game, background, game_std_bgms, None::<u32>/*replay*/, save_keystates.clone())).unwrap(); // Main loop let window_run = window.getattr("run").unwrap(); match window_run.call0() { Ok(_) => break, ref orig_err @ Err(ref err) => { match err.get_type(py).to_string().as_str() { "<class 'pytouhou.game.NextStage'>" => { if !story || stage_num == (if false/*boss_rush*/ { 7 } else if rank > 0 { 6 } else { 5 }) { break; } stage_num += 1; } "<class 'pytouhou.game.GameOver'>" => { println!("Game over!"); break; } err => { eprintln!("Got unexpected exception {err:?}!"); orig_err.as_ref().unwrap(); } } } /* finally: if save_filename: last_key = -1 for time, key in enumerate(save_keystates): if key != last_key: level.keys.append((time, key, 0)) last_key = key */ } } window_set_runner.call1((None::<u32>,)).unwrap(); if let Some(save_filename) = args.save_replay { let file = File::create(save_filename).unwrap(); //save_replay.write(file); } }); Ok(()) }
