From 1cc7ed8b2bf9ee9bbaeff46f96d6a2b7698b8883 Mon Sep 17 00:00:00 2001 From: Alula <6276139+alula@users.noreply.github.com> Date: Fri, 12 Feb 2021 11:05:28 +0100 Subject: [PATCH] ogg playback and persistent settings --- rustfmt.toml | 4 + src/bmfont.rs | 1 - src/engine_constants/mod.rs | 621 +++++++++++++++++++-- src/framework/error.rs | 7 + src/framework/filesystem.rs | 98 ++-- src/framework/vfs.rs | 22 +- src/lib.rs | 9 +- src/profile.rs | 2 +- src/scene/title_scene.rs | 189 ++++--- src/scripting/doukutsu.d.ts | 29 +- src/scripting/doukutsu.rs | 9 +- src/settings.rs | 29 +- src/sound/mod.rs | 435 +++++++++++---- src/sound/ogg_playback.rs | 193 +++++++ src/sound/{playback.rs => org_playback.rs} | 185 +++--- src/text_script.rs | 443 +++++++++++---- 16 files changed, 1725 insertions(+), 551 deletions(-) create mode 100644 rustfmt.toml create mode 100644 src/sound/ogg_playback.rs rename src/sound/{playback.rs => org_playback.rs} (77%) diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f645dcf --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +edition = "2018" +max_width = 120 +use_small_heuristics = "Max" +newline_style = "Unix" diff --git a/src/bmfont.rs b/src/bmfont.rs index 59d8a5f..06fc096 100644 --- a/src/bmfont.rs +++ b/src/bmfont.rs @@ -3,7 +3,6 @@ use std::io; use byteorder::{LE, ReadBytesExt}; -use crate::framework::context::Context; use crate::framework::error::GameError::ResourceLoadError; use crate::framework::error::GameResult; use crate::str; diff --git a/src/engine_constants/mod.rs b/src/engine_constants/mod.rs index ad5f6fc..91c0502 100644 --- a/src/engine_constants/mod.rs +++ b/src/engine_constants/mod.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use case_insensitive_hashmap::CaseInsensitiveHashMap; use log::info; @@ -92,7 +94,6 @@ impl Clone for CaretConsts { } } - #[derive(Debug, Copy, Clone)] pub struct BulletData { pub damage: u8, @@ -171,7 +172,6 @@ pub struct TextScriptConsts { pub cursor: [Rect; 2], } - #[derive(Debug)] pub struct TitleConsts { pub intro_text: String, @@ -222,6 +222,8 @@ pub struct EngineConstants { pub font_path: String, pub font_scale: f32, pub font_space_offset: f32, + pub soundtracks: HashMap, + pub music_table: Vec, pub organya_paths: Vec, } @@ -243,6 +245,8 @@ impl Clone for EngineConstants { font_path: self.font_path.clone(), font_scale: self.font_scale, font_space_offset: self.font_space_offset, + soundtracks: self.soundtracks.clone(), + music_table: self.music_table.clone(), organya_paths: self.organya_paths.clone(), } } @@ -421,78 +425,536 @@ impl EngineConstants { question_left_rect: Rect { left: 0, top: 80, right: 16, bottom: 96 }, question_right_rect: Rect { left: 48, top: 64, right: 64, bottom: 80 }, }, - world: WorldConsts { - snack_rect: Rect { left: 256, top: 48, right: 272, bottom: 64 }, - }, + world: WorldConsts { snack_rect: Rect { left: 256, top: 48, right: 272, bottom: 64 } }, npc: serde_yaml::from_str("dummy: \"lol\"").unwrap(), weapon: WeaponConsts { bullet_table: vec![ // Null - BulletData { damage: 0, life: 0, lifetime: 0, flags: BulletFlag(0), enemy_hit_width: 0, enemy_hit_height: 0, block_hit_width: 0, block_hit_height: 0, display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 } }, + BulletData { + damage: 0, + life: 0, + lifetime: 0, + flags: BulletFlag(0), + enemy_hit_width: 0, + enemy_hit_height: 0, + block_hit_width: 0, + block_hit_height: 0, + display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 }, + }, // Snake - BulletData { damage: 4, life: 1, lifetime: 20, flags: BulletFlag(36), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 6, life: 1, lifetime: 23, flags: BulletFlag(36), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 8, life: 1, lifetime: 30, flags: BulletFlag(36), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, + BulletData { + damage: 4, + life: 1, + lifetime: 20, + flags: BulletFlag(36), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 6, + life: 1, + lifetime: 23, + flags: BulletFlag(36), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 8, + life: 1, + lifetime: 30, + flags: BulletFlag(36), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, // Polar Star - BulletData { damage: 1, life: 1, lifetime: 8, flags: BulletFlag(32), enemy_hit_width: 6, enemy_hit_height: 6, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 2, life: 1, lifetime: 12, flags: BulletFlag(32), enemy_hit_width: 6, enemy_hit_height: 6, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 4, life: 1, lifetime: 16, flags: BulletFlag(32), enemy_hit_width: 6, enemy_hit_height: 6, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, + BulletData { + damage: 1, + life: 1, + lifetime: 8, + flags: BulletFlag(32), + enemy_hit_width: 6, + enemy_hit_height: 6, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 2, + life: 1, + lifetime: 12, + flags: BulletFlag(32), + enemy_hit_width: 6, + enemy_hit_height: 6, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 4, + life: 1, + lifetime: 16, + flags: BulletFlag(32), + enemy_hit_width: 6, + enemy_hit_height: 6, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, // Fireball - BulletData { damage: 2, life: 2, lifetime: 100, flags: BulletFlag(8), enemy_hit_width: 8, enemy_hit_height: 16, block_hit_width: 4, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 3, life: 2, lifetime: 100, flags: BulletFlag(8), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 4, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 3, life: 2, lifetime: 100, flags: BulletFlag(8), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 4, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, + BulletData { + damage: 2, + life: 2, + lifetime: 100, + flags: BulletFlag(8), + enemy_hit_width: 8, + enemy_hit_height: 16, + block_hit_width: 4, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 3, + life: 2, + lifetime: 100, + flags: BulletFlag(8), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 4, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 3, + life: 2, + lifetime: 100, + flags: BulletFlag(8), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 4, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, // Machine Gun - BulletData { damage: 2, life: 1, lifetime: 20, flags: BulletFlag(32), enemy_hit_width: 2, enemy_hit_height: 2, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 4, life: 1, lifetime: 20, flags: BulletFlag(32), enemy_hit_width: 2, enemy_hit_height: 2, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 6, life: 1, lifetime: 20, flags: BulletFlag(32), enemy_hit_width: 2, enemy_hit_height: 2, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, + BulletData { + damage: 2, + life: 1, + lifetime: 20, + flags: BulletFlag(32), + enemy_hit_width: 2, + enemy_hit_height: 2, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 4, + life: 1, + lifetime: 20, + flags: BulletFlag(32), + enemy_hit_width: 2, + enemy_hit_height: 2, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 6, + life: 1, + lifetime: 20, + flags: BulletFlag(32), + enemy_hit_width: 2, + enemy_hit_height: 2, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, // Missile Launcher - BulletData { damage: 0, life: 10, lifetime: 50, flags: BulletFlag(40), enemy_hit_width: 2, enemy_hit_height: 2, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 0, life: 10, lifetime: 70, flags: BulletFlag(40), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 4, block_hit_height: 4, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 0, life: 10, lifetime: 90, flags: BulletFlag(40), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 0, block_hit_height: 0, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, + BulletData { + damage: 0, + life: 10, + lifetime: 50, + flags: BulletFlag(40), + enemy_hit_width: 2, + enemy_hit_height: 2, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 0, + life: 10, + lifetime: 70, + flags: BulletFlag(40), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 4, + block_hit_height: 4, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 0, + life: 10, + lifetime: 90, + flags: BulletFlag(40), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 0, + block_hit_height: 0, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, // Missile Launcher explosion - BulletData { damage: 1, life: 100, lifetime: 100, flags: BulletFlag(20), enemy_hit_width: 16, enemy_hit_height: 16, block_hit_width: 0, block_hit_height: 0, display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 } }, - BulletData { damage: 1, life: 100, lifetime: 100, flags: BulletFlag(20), enemy_hit_width: 16, enemy_hit_height: 16, block_hit_width: 0, block_hit_height: 0, display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 } }, - BulletData { damage: 1, life: 100, lifetime: 100, flags: BulletFlag(20), enemy_hit_width: 16, enemy_hit_height: 16, block_hit_width: 0, block_hit_height: 0, display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 } }, + BulletData { + damage: 1, + life: 100, + lifetime: 100, + flags: BulletFlag(20), + enemy_hit_width: 16, + enemy_hit_height: 16, + block_hit_width: 0, + block_hit_height: 0, + display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 }, + }, + BulletData { + damage: 1, + life: 100, + lifetime: 100, + flags: BulletFlag(20), + enemy_hit_width: 16, + enemy_hit_height: 16, + block_hit_width: 0, + block_hit_height: 0, + display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 }, + }, + BulletData { + damage: 1, + life: 100, + lifetime: 100, + flags: BulletFlag(20), + enemy_hit_width: 16, + enemy_hit_height: 16, + block_hit_width: 0, + block_hit_height: 0, + display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 }, + }, // Bubbler - BulletData { damage: 1, life: 1, lifetime: 20, flags: BulletFlag(8), enemy_hit_width: 2, enemy_hit_height: 2, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 } }, - BulletData { damage: 2, life: 1, lifetime: 20, flags: BulletFlag(8), enemy_hit_width: 2, enemy_hit_height: 2, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 } }, - BulletData { damage: 2, life: 1, lifetime: 20, flags: BulletFlag(8), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 4, block_hit_height: 4, display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 } }, + BulletData { + damage: 1, + life: 1, + lifetime: 20, + flags: BulletFlag(8), + enemy_hit_width: 2, + enemy_hit_height: 2, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 }, + }, + BulletData { + damage: 2, + life: 1, + lifetime: 20, + flags: BulletFlag(8), + enemy_hit_width: 2, + enemy_hit_height: 2, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 }, + }, + BulletData { + damage: 2, + life: 1, + lifetime: 20, + flags: BulletFlag(8), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 4, + block_hit_height: 4, + display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 }, + }, // Bubbler level 3 thorns - BulletData { damage: 3, life: 1, lifetime: 32, flags: BulletFlag(32), enemy_hit_width: 2, enemy_hit_height: 2, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 } }, + BulletData { + damage: 3, + life: 1, + lifetime: 32, + flags: BulletFlag(32), + enemy_hit_width: 2, + enemy_hit_height: 2, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 }, + }, // Blade slashes - BulletData { damage: 0, life: 100, lifetime: 0, flags: BulletFlag(36), enemy_hit_width: 8, enemy_hit_height: 8, block_hit_width: 8, block_hit_height: 8, display_bounds: Rect { left: 12, top: 12, right: 12, bottom: 12 } }, + BulletData { + damage: 0, + life: 100, + lifetime: 0, + flags: BulletFlag(36), + enemy_hit_width: 8, + enemy_hit_height: 8, + block_hit_width: 8, + block_hit_height: 8, + display_bounds: Rect { left: 12, top: 12, right: 12, bottom: 12 }, + }, // Falling spike - BulletData { damage: 127, life: 1, lifetime: 2, flags: BulletFlag(4), enemy_hit_width: 8, enemy_hit_height: 4, block_hit_width: 8, block_hit_height: 4, display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 } }, + BulletData { + damage: 127, + life: 1, + lifetime: 2, + flags: BulletFlag(4), + enemy_hit_width: 8, + enemy_hit_height: 4, + block_hit_width: 8, + block_hit_height: 4, + display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 }, + }, // Blade - BulletData { damage: 15, life: 1, lifetime: 30, flags: BulletFlag(36), enemy_hit_width: 8, enemy_hit_height: 8, block_hit_width: 4, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 6, life: 3, lifetime: 18, flags: BulletFlag(36), enemy_hit_width: 10, enemy_hit_height: 10, block_hit_width: 4, block_hit_height: 2, display_bounds: Rect { left: 12, top: 12, right: 12, bottom: 12 } }, - BulletData { damage: 1, life: 100, lifetime: 30, flags: BulletFlag(36), enemy_hit_width: 6, enemy_hit_height: 6, block_hit_width: 4, block_hit_height: 4, display_bounds: Rect { left: 12, top: 12, right: 12, bottom: 12 } }, + BulletData { + damage: 15, + life: 1, + lifetime: 30, + flags: BulletFlag(36), + enemy_hit_width: 8, + enemy_hit_height: 8, + block_hit_width: 4, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 6, + life: 3, + lifetime: 18, + flags: BulletFlag(36), + enemy_hit_width: 10, + enemy_hit_height: 10, + block_hit_width: 4, + block_hit_height: 2, + display_bounds: Rect { left: 12, top: 12, right: 12, bottom: 12 }, + }, + BulletData { + damage: 1, + life: 100, + lifetime: 30, + flags: BulletFlag(36), + enemy_hit_width: 6, + enemy_hit_height: 6, + block_hit_width: 4, + block_hit_height: 4, + display_bounds: Rect { left: 12, top: 12, right: 12, bottom: 12 }, + }, // Super Missile Launcher - BulletData { damage: 0, life: 10, lifetime: 30, flags: BulletFlag(40), enemy_hit_width: 2, enemy_hit_height: 2, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 0, life: 10, lifetime: 40, flags: BulletFlag(40), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 4, block_hit_height: 4, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 0, life: 10, lifetime: 40, flags: BulletFlag(40), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 0, block_hit_height: 0, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, + BulletData { + damage: 0, + life: 10, + lifetime: 30, + flags: BulletFlag(40), + enemy_hit_width: 2, + enemy_hit_height: 2, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 0, + life: 10, + lifetime: 40, + flags: BulletFlag(40), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 4, + block_hit_height: 4, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 0, + life: 10, + lifetime: 40, + flags: BulletFlag(40), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 0, + block_hit_height: 0, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, // Super Missile Launcher explosion - BulletData { damage: 2, life: 100, lifetime: 100, flags: BulletFlag(20), enemy_hit_width: 12, enemy_hit_height: 12, block_hit_width: 0, block_hit_height: 0, display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 } }, - BulletData { damage: 2, life: 100, lifetime: 100, flags: BulletFlag(20), enemy_hit_width: 12, enemy_hit_height: 12, block_hit_width: 0, block_hit_height: 0, display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 } }, - BulletData { damage: 2, life: 100, lifetime: 100, flags: BulletFlag(20), enemy_hit_width: 12, enemy_hit_height: 12, block_hit_width: 0, block_hit_height: 0, display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 } }, + BulletData { + damage: 2, + life: 100, + lifetime: 100, + flags: BulletFlag(20), + enemy_hit_width: 12, + enemy_hit_height: 12, + block_hit_width: 0, + block_hit_height: 0, + display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 }, + }, + BulletData { + damage: 2, + life: 100, + lifetime: 100, + flags: BulletFlag(20), + enemy_hit_width: 12, + enemy_hit_height: 12, + block_hit_width: 0, + block_hit_height: 0, + display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 }, + }, + BulletData { + damage: 2, + life: 100, + lifetime: 100, + flags: BulletFlag(20), + enemy_hit_width: 12, + enemy_hit_height: 12, + block_hit_width: 0, + block_hit_height: 0, + display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 }, + }, // Nemesis - BulletData { damage: 4, life: 4, lifetime: 20, flags: BulletFlag(32), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 3, block_hit_height: 3, display_bounds: Rect { left: 8, top: 8, right: 24, bottom: 8 } }, - BulletData { damage: 4, life: 2, lifetime: 20, flags: BulletFlag(32), enemy_hit_width: 2, enemy_hit_height: 2, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 24, bottom: 8 } }, - BulletData { damage: 1, life: 1, lifetime: 20, flags: BulletFlag(32), enemy_hit_width: 2, enemy_hit_height: 2, block_hit_width: 2, block_hit_height: 2, display_bounds: Rect { left: 8, top: 8, right: 24, bottom: 8 } }, + BulletData { + damage: 4, + life: 4, + lifetime: 20, + flags: BulletFlag(32), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 3, + block_hit_height: 3, + display_bounds: Rect { left: 8, top: 8, right: 24, bottom: 8 }, + }, + BulletData { + damage: 4, + life: 2, + lifetime: 20, + flags: BulletFlag(32), + enemy_hit_width: 2, + enemy_hit_height: 2, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 24, bottom: 8 }, + }, + BulletData { + damage: 1, + life: 1, + lifetime: 20, + flags: BulletFlag(32), + enemy_hit_width: 2, + enemy_hit_height: 2, + block_hit_width: 2, + block_hit_height: 2, + display_bounds: Rect { left: 8, top: 8, right: 24, bottom: 8 }, + }, // Spur - BulletData { damage: 4, life: 4, lifetime: 30, flags: BulletFlag(64), enemy_hit_width: 6, enemy_hit_height: 6, block_hit_width: 3, block_hit_height: 3, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 8, life: 8, lifetime: 30, flags: BulletFlag(64), enemy_hit_width: 6, enemy_hit_height: 6, block_hit_width: 3, block_hit_height: 3, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, - BulletData { damage: 12, life: 12, lifetime: 30, flags: BulletFlag(64), enemy_hit_width: 6, enemy_hit_height: 6, block_hit_width: 3, block_hit_height: 3, display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 } }, + BulletData { + damage: 4, + life: 4, + lifetime: 30, + flags: BulletFlag(64), + enemy_hit_width: 6, + enemy_hit_height: 6, + block_hit_width: 3, + block_hit_height: 3, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 8, + life: 8, + lifetime: 30, + flags: BulletFlag(64), + enemy_hit_width: 6, + enemy_hit_height: 6, + block_hit_width: 3, + block_hit_height: 3, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, + BulletData { + damage: 12, + life: 12, + lifetime: 30, + flags: BulletFlag(64), + enemy_hit_width: 6, + enemy_hit_height: 6, + block_hit_width: 3, + block_hit_height: 3, + display_bounds: Rect { left: 8, top: 8, right: 8, bottom: 8 }, + }, // Spur trail - BulletData { damage: 3, life: 100, lifetime: 30, flags: BulletFlag(32), enemy_hit_width: 6, enemy_hit_height: 6, block_hit_width: 3, block_hit_height: 3, display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 } }, - BulletData { damage: 6, life: 100, lifetime: 30, flags: BulletFlag(32), enemy_hit_width: 6, enemy_hit_height: 6, block_hit_width: 3, block_hit_height: 3, display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 } }, - BulletData { damage: 11, life: 100, lifetime: 30, flags: BulletFlag(32), enemy_hit_width: 6, enemy_hit_height: 6, block_hit_width: 3, block_hit_height: 3, display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 } }, + BulletData { + damage: 3, + life: 100, + lifetime: 30, + flags: BulletFlag(32), + enemy_hit_width: 6, + enemy_hit_height: 6, + block_hit_width: 3, + block_hit_height: 3, + display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 }, + }, + BulletData { + damage: 6, + life: 100, + lifetime: 30, + flags: BulletFlag(32), + enemy_hit_width: 6, + enemy_hit_height: 6, + block_hit_width: 3, + block_hit_height: 3, + display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 }, + }, + BulletData { + damage: 11, + life: 100, + lifetime: 30, + flags: BulletFlag(32), + enemy_hit_width: 6, + enemy_hit_height: 6, + block_hit_width: 3, + block_hit_height: 3, + display_bounds: Rect { left: 4, top: 4, right: 4, bottom: 4 }, + }, // Curly's Nemesis - BulletData { damage: 4, life: 4, lifetime: 20, flags: BulletFlag(32), enemy_hit_width: 4, enemy_hit_height: 4, block_hit_width: 3, block_hit_height: 3, display_bounds: Rect { left: 8, top: 8, right: 24, bottom: 8 } }, + BulletData { + damage: 4, + life: 4, + lifetime: 20, + flags: BulletFlag(32), + enemy_hit_width: 4, + enemy_hit_height: 4, + block_hit_width: 3, + block_hit_height: 3, + display_bounds: Rect { left: 8, top: 8, right: 24, bottom: 8 }, + }, // EnemyClear? - BulletData { damage: 0, life: 4, lifetime: 4, flags: BulletFlag(4), enemy_hit_width: 0, enemy_hit_height: 0, block_hit_width: 0, block_hit_height: 0, display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 } }, + BulletData { + damage: 0, + life: 4, + lifetime: 4, + flags: BulletFlag(4), + enemy_hit_width: 0, + enemy_hit_height: 0, + block_hit_width: 0, + block_hit_height: 0, + display_bounds: Rect { left: 0, top: 0, right: 0, bottom: 0 }, + }, // Whimsical Star - BulletData { damage: 1, life: 1, lifetime: 1, flags: BulletFlag(36), enemy_hit_width: 1, enemy_hit_height: 1, block_hit_width: 1, block_hit_height: 1, display_bounds: Rect { left: 1, top: 1, right: 1, bottom: 1 } }, + BulletData { + damage: 1, + life: 1, + lifetime: 1, + flags: BulletFlag(36), + enemy_hit_width: 1, + enemy_hit_height: 1, + block_hit_width: 1, + block_hit_height: 1, + display_bounds: Rect { left: 1, top: 1, right: 1, bottom: 1 }, + }, ], bullet_rects: BulletRects { b001_snake_l1: [ @@ -822,10 +1284,56 @@ impl EngineConstants { font_path: "builtin/builtin_font.fnt".to_string(), font_scale: 1.0, font_space_offset: 0.0, + soundtracks: HashMap::new(), + music_table: vec![ + "xxxx".to_string(), + "wanpaku".to_string(), + "anzen".to_string(), + "gameover".to_string(), + "gravity".to_string(), + "weed".to_string(), + "mdown2".to_string(), + "fireeye".to_string(), + "vivi".to_string(), + "mura".to_string(), + "fanfale1".to_string(), + "ginsuke".to_string(), + "cemetery".to_string(), + "plant".to_string(), + "kodou".to_string(), + "fanfale3".to_string(), + "fanfale2".to_string(), + "dr".to_string(), + "escape".to_string(), + "jenka".to_string(), + "maze".to_string(), + "access".to_string(), + "ironh".to_string(), + "grand".to_string(), + "curly".to_string(), + "oside".to_string(), + "requiem".to_string(), + "wanpak2".to_string(), + "quiet".to_string(), + "lastcave".to_string(), + "balcony".to_string(), + "lastbtl".to_string(), + "lastbt3".to_string(), + "ending".to_string(), + "zonbie".to_string(), + "bdown".to_string(), + "hell".to_string(), + "jenka2".to_string(), + "marine".to_string(), + "ballos".to_string(), + "toroko".to_string(), + "white".to_string(), + "kaze".to_string(), + ], organya_paths: vec![ - str!("/org/"), // NXEngine - str!("/base/Org/"), // CS+ - str!("/Resource/ORG/"), // CSE2E + "/org/".to_string(), // NXEngine + "/base/Org/".to_string(), // CS+ + "/Resource/ORG/".to_string(), // CSE2E ], } } @@ -842,9 +1350,10 @@ impl EngineConstants { self.font_path = str!("csfont.fnt"); self.font_scale = 0.5; self.font_space_offset = 2.0; + self.soundtracks.insert("Remastered".to_string(), "/base/Ogg11/".to_string()); + self.soundtracks.insert("New".to_string(), "/base/Ogg/".to_string()); } - pub fn apply_csplus_nx_patches(&mut self) { info!("Applying Switch-specific Cave Story+ constants patches..."); @@ -856,5 +1365,7 @@ impl EngineConstants { self.textscript.encoding = TextScriptEncoding::UTF8; self.textscript.encrypted = false; self.textscript.animated_face_pics = true; + self.soundtracks.insert("Famitracks".to_string(), "/base/ogg17/".to_string()); + self.soundtracks.insert("Ridiculon".to_string(), "/base/ogg_ridic/".to_string()); } } diff --git a/src/framework/error.rs b/src/framework/error.rs index 52b19d5..dc25a4e 100644 --- a/src/framework/error.rs +++ b/src/framework/error.rs @@ -93,6 +93,13 @@ impl From for GameError { } } +impl From for GameError { + fn from(e: serde_yaml::Error) -> Self { + let errstr = format!("Yaml error: {:?}", e); + GameError::ParseError(errstr) + } +} + #[cfg(target_os = "android")] impl From for GameError { fn from(e: jni::errors::Error) -> GameError { diff --git a/src/framework/filesystem.rs b/src/framework/filesystem.rs index 09d80b7..f2cc450 100644 --- a/src/framework/filesystem.rs +++ b/src/framework/filesystem.rs @@ -6,10 +6,11 @@ use std::path; use std::path::PathBuf; use directories::ProjectDirs; -use crate::framework::vfs; -use crate::framework::vfs::{VFS, OpenOptions}; -use crate::framework::error::{GameResult, GameError}; + use crate::framework::context::Context; +use crate::framework::error::{GameError, GameResult}; +use crate::framework::vfs; +use crate::framework::vfs::{OpenOptions, VFS}; /// A structure that contains the filesystem state and cache. #[derive(Debug)] @@ -25,6 +26,8 @@ pub enum File { VfsFile(Box), } +unsafe impl Send for File {} + impl fmt::Debug for File { // Make this more useful? // But we can't seem to get a filename out of a file, @@ -81,13 +84,13 @@ impl Filesystem { /// Opens the given `path` and returns the resulting `File` /// in read-only mode. - pub(crate) fn open>(&mut self, path: P) -> GameResult { + pub(crate) fn open>(&self, path: P) -> GameResult { self.vfs.open(path.as_ref()).map(|f| File::VfsFile(f)) } /// Opens the given `path` from user directory and returns the resulting `File` /// in read-only mode. - pub(crate) fn user_open>(&mut self, path: P) -> GameResult { + pub(crate) fn user_open>(&self, path: P) -> GameResult { self.user_vfs.open(path.as_ref()).map(|f| File::VfsFile(f)) } @@ -95,44 +98,36 @@ impl Filesystem { /// [`filesystem::OpenOptions`](struct.OpenOptions.html). /// Note that even if you open a file read-write, it can only /// write to files in the "user" directory. - pub(crate) fn open_options>( - &mut self, - path: P, - options: OpenOptions, - ) -> GameResult { + pub(crate) fn open_options>(&self, path: P, options: OpenOptions) -> GameResult { self.user_vfs .open_options(path.as_ref(), options) .map(|f| File::VfsFile(f)) .map_err(|e| { - GameError::ResourceLoadError(format!( - "Tried to open {:?} but got error: {:?}", - path.as_ref(), - e - )) + GameError::ResourceLoadError(format!("Tried to open {:?} but got error: {:?}", path.as_ref(), e)) }) } /// Creates a new file in the user directory and opens it /// to be written to, truncating it if it already exists. - pub(crate) fn user_create>(&mut self, path: P) -> GameResult { + pub(crate) fn user_create>(&self, path: P) -> GameResult { self.user_vfs.create(path.as_ref()).map(|f| File::VfsFile(f)) } /// Create an empty directory in the user dir /// with the given name. Any parents to that directory /// that do not exist will be created. - pub(crate) fn user_create_dir>(&mut self, path: P) -> GameResult<()> { + pub(crate) fn user_create_dir>(&self, path: P) -> GameResult<()> { self.user_vfs.mkdir(path.as_ref()) } /// Deletes the specified file in the user dir. - pub(crate) fn user_delete>(&mut self, path: P) -> GameResult<()> { + pub(crate) fn user_delete>(&self, path: P) -> GameResult<()> { self.user_vfs.rm(path.as_ref()) } /// Deletes the specified directory in the user dir, /// and all its contents! - pub(crate) fn user_delete_dir>(&mut self, path: P) -> GameResult<()> { + pub(crate) fn user_delete_dir>(&self, path: P) -> GameResult<()> { self.user_vfs.rmrf(path.as_ref()) } @@ -156,10 +151,7 @@ impl Filesystem { /// Check whether a path points at a file. pub(crate) fn is_file>(&self, path: P) -> bool { - self.vfs - .metadata(path.as_ref()) - .map(|m| m.is_file()) - .unwrap_or(false) + self.vfs.metadata(path.as_ref()).map(|m| m.is_file()).unwrap_or(false) } /// Check whether a path points at a directory. @@ -172,10 +164,7 @@ impl Filesystem { /// Check whether a path points at a directory. pub(crate) fn is_dir>(&self, path: P) -> bool { - self.vfs - .metadata(path.as_ref()) - .map(|m| m.is_dir()) - .unwrap_or(false) + self.vfs.metadata(path.as_ref()).map(|m| m.is_dir()).unwrap_or(false) } /// Returns a list of all files and directories in the user directory, @@ -183,12 +172,13 @@ impl Filesystem { /// /// Lists the base directory if an empty path is given. pub(crate) fn user_read_dir>( - &mut self, + &self, path: P, - ) -> GameResult>> { - let itr = self.user_vfs.read_dir(path.as_ref())?.map(|fname| { - fname.expect("Could not read file in read_dir()? Should never happen, I hope!") - }); + ) -> GameResult>> { + let itr = self + .user_vfs + .read_dir(path.as_ref())? + .map(|fname| fname.expect("Could not read file in read_dir()? Should never happen, I hope!")); Ok(Box::new(itr)) } @@ -197,16 +187,17 @@ impl Filesystem { /// /// Lists the base directory if an empty path is given. pub(crate) fn read_dir>( - &mut self, + &self, path: P, - ) -> GameResult>> { - let itr = self.vfs.read_dir(path.as_ref())?.map(|fname| { - fname.expect("Could not read file in read_dir()? Should never happen, I hope!") - }); + ) -> GameResult>> { + let itr = self + .vfs + .read_dir(path.as_ref())? + .map(|fname| fname.expect("Could not read file in read_dir()? Should never happen, I hope!")); Ok(Box::new(itr)) } - fn write_to_string(&mut self) -> String { + fn write_to_string(&self) -> String { use std::fmt::Write; let mut s = String::new(); for vfs in self.vfs.roots() { @@ -214,8 +205,7 @@ impl Filesystem { match vfs.read_dir(path::Path::new("/")) { Ok(files) => { for itm in files { - write!(s, " {:?}", itm) - .expect("Could not write to string; should never happen?"); + write!(s, " {:?}", itm).expect("Could not write to string; should never happen?"); } } Err(e) => write!(s, " Could not read source: {:?}", e) @@ -242,7 +232,6 @@ impl Filesystem { self.vfs.push_back(vfs); } - pub fn mount_user_vfs(&mut self, vfs: Box) { self.user_vfs.push_back(vfs); } @@ -250,46 +239,42 @@ impl Filesystem { /// Opens the given path and returns the resulting `File` /// in read-only mode. -pub fn open>(ctx: &mut Context, path: P) -> GameResult { +pub fn open>(ctx: &Context, path: P) -> GameResult { ctx.filesystem.open(path) } /// Opens the given path in the user directory and returns the resulting `File` /// in read-only mode. -pub fn user_open>(ctx: &mut Context, path: P) -> GameResult { +pub fn user_open>(ctx: &Context, path: P) -> GameResult { ctx.filesystem.user_open(path) } /// Opens a file in the user directory with the given `filesystem::OpenOptions`. -pub fn open_options>( - ctx: &mut Context, - path: P, - options: OpenOptions, -) -> GameResult { +pub fn open_options>(ctx: &Context, path: P, options: OpenOptions) -> GameResult { ctx.filesystem.open_options(path, options) } /// Creates a new file in the user directory and opens it /// to be written to, truncating it if it already exists. -pub fn user_create>(ctx: &mut Context, path: P) -> GameResult { +pub fn user_create>(ctx: &Context, path: P) -> GameResult { ctx.filesystem.user_create(path) } /// Create an empty directory in the user dir /// with the given name. Any parents to that directory /// that do not exist will be created. -pub fn user_create_dir>(ctx: &mut Context, path: P) -> GameResult { +pub fn user_create_dir>(ctx: &Context, path: P) -> GameResult { ctx.filesystem.user_create_dir(path.as_ref()) } /// Deletes the specified file in the user dir. -pub fn user_delete>(ctx: &mut Context, path: P) -> GameResult { +pub fn user_delete>(ctx: &Context, path: P) -> GameResult { ctx.filesystem.user_delete(path.as_ref()) } /// Deletes the specified directory in the user dir, /// and all its contents! -pub fn user_delete_dir>(ctx: &mut Context, path: P) -> GameResult { +pub fn user_delete_dir>(ctx: &Context, path: P) -> GameResult { ctx.filesystem.user_delete_dir(path.as_ref()) } @@ -313,9 +298,9 @@ pub fn user_is_dir>(ctx: &Context, path: P) -> bool { /// /// Lists the base directory if an empty path is given. pub fn user_read_dir>( - ctx: &mut Context, + ctx: &Context, path: P, -) -> GameResult>> { +) -> GameResult>> { ctx.filesystem.user_read_dir(path) } @@ -338,10 +323,7 @@ pub fn is_dir>(ctx: &Context, path: P) -> bool { /// in no particular order. /// /// Lists the base directory if an empty path is given. -pub fn read_dir>( - ctx: &mut Context, - path: P, -) -> GameResult>> { +pub fn read_dir>(ctx: &Context, path: P) -> GameResult>> { ctx.filesystem.read_dir(path) } diff --git a/src/framework/vfs.rs b/src/framework/vfs.rs index 459e78a..0c73324 100644 --- a/src/framework/vfs.rs +++ b/src/framework/vfs.rs @@ -9,13 +9,13 @@ //! as a trait object, and its path abstraction is not the most //! convenient. - use std::collections::VecDeque; use std::fmt::{self, Debug}; use std::fs; -use std::io::{Read, Seek, Write, BufRead}; +use std::io::{BufRead, Read, Seek, Write}; use std::path::{self, Path, PathBuf}; -use crate::framework::error::{GameResult, GameError}; + +use crate::framework::error::{GameError, GameResult}; fn convenient_path_to_str(path: &path::Path) -> GameResult<&str> { path.to_str().ok_or_else(|| { @@ -25,9 +25,9 @@ fn convenient_path_to_str(path: &path::Path) -> GameResult<&str> { } /// Virtual file -pub trait VFile: Read + Write + Seek + Debug {} +pub trait VFile: Read + Write + Seek + Debug + Send + Sync {} -impl VFile for T where T: Read + Write + Seek + Debug {} +impl VFile for T where T: Read + Write + Seek + Debug + Send + Sync {} /// Options for opening files /// @@ -132,7 +132,7 @@ pub trait VFS: Debug { fn metadata(&self, path: &Path) -> GameResult>; /// Retrieve all file and directory entries in the given directory. - fn read_dir(&self, path: &Path) -> GameResult>>>; + fn read_dir(&self, path: &Path) -> GameResult>>>; /// Retrieve the actual location of the VFS root, if available. fn to_path_buf(&self) -> Option; @@ -268,9 +268,9 @@ impl VFS for PhysicalFS { fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult> { if self.readonly && (open_options.write - || open_options.create - || open_options.append - || open_options.truncate) + || open_options.create + || open_options.append + || open_options.truncate) { let msg = format!( "Cannot alter file {:?} in root {:?}, filesystem read-only", @@ -359,7 +359,7 @@ impl VFS for PhysicalFS { } /// Retrieve the path entries in this path - fn read_dir(&self, path: &Path) -> GameResult>>> { + fn read_dir(&self, path: &Path) -> GameResult>>> { self.create_root()?; let p = self.to_absolute(path)?; // This is inconvenient because path() returns the full absolute @@ -513,7 +513,7 @@ impl VFS for OverlayFS { } /// Retrieve the path entries in this path - fn read_dir(&self, path: &Path) -> GameResult>>> { + fn read_dir(&self, path: &Path) -> GameResult>>> { // This is tricky 'cause we have to actually merge iterators together... // Doing it the simple and stupid way works though. let mut v = Vec::new(); diff --git a/src/lib.rs b/src/lib.rs index ed6bab5..4bb03f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,6 @@ extern crate strum; #[macro_use] extern crate strum_macros; -use core::mem; use std::cell::UnsafeCell; use std::env; use std::path::PathBuf; @@ -337,11 +336,9 @@ pub fn init() -> GameResult { let state_ref = unsafe { &mut *game.state.get() }; #[cfg(feature = "scripting")] { - unsafe { - state_ref - .lua - .update_refs(game.state.get(), &mut context as *mut Context); - } + state_ref + .lua + .update_refs(game.state.get(), &mut context as *mut Context); } state_ref.next_scene = Some(Box::new(LoadingScene::new())); diff --git a/src/profile.rs b/src/profile.rs index 0f70c13..6d86ba2 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -53,7 +53,7 @@ impl GameProfile { state.control_flags.set_tick_world(true); state.control_flags.set_control_enabled(true); - let _ = state.sound_manager.play_song(self.current_song as usize, &state.constants, ctx); + let _ = state.sound_manager.play_song(self.current_song as usize, &state.constants, &state.settings, ctx); game_scene.inventory_player1.current_weapon = self.current_weapon as u16; game_scene.inventory_player1.current_item = self.current_item as u16; diff --git a/src/scene/title_scene.rs b/src/scene/title_scene.rs index 41709fb..b5459c0 100644 --- a/src/scene/title_scene.rs +++ b/src/scene/title_scene.rs @@ -1,4 +1,4 @@ -use crate::common::{Rect, VERSION_BANNER, Color}; +use crate::common::{Color, Rect, VERSION_BANNER}; use crate::framework::context::Context; use crate::framework::error::GameResult; use crate::framework::graphics; @@ -44,27 +44,22 @@ impl TitleScene { let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "bkMoon")?; let offset = (self.tick % 640) as isize; - batch.add_rect(((state.canvas_size.0 - 320.0) / 2.0).floor(), 0.0, - &Rect::new_size(0, 0, 320, 88)); + batch.add_rect(((state.canvas_size.0 - 320.0) / 2.0).floor(), 0.0, &Rect::new_size(0, 0, 320, 88)); for x in ((-offset / 2)..(state.canvas_size.0 as isize)).step_by(320) { - batch.add_rect(x as f32, 88.0, - &Rect::new_size(0, 88, 320, 35)); + batch.add_rect(x as f32, 88.0, &Rect::new_size(0, 88, 320, 35)); } for x in ((-offset % 320)..(state.canvas_size.0 as isize)).step_by(320) { - batch.add_rect(x as f32, 123.0, - &Rect::new_size(0, 123, 320, 23)); + batch.add_rect(x as f32, 123.0, &Rect::new_size(0, 123, 320, 23)); } for x in ((-offset * 2)..(state.canvas_size.0 as isize)).step_by(320) { - batch.add_rect(x as f32, 146.0, - &Rect::new_size(0, 146, 320, 30)); + batch.add_rect(x as f32, 146.0, &Rect::new_size(0, 146, 320, 30)); } for x in ((-offset * 4)..(state.canvas_size.0 as isize)).step_by(320) { - batch.add_rect(x as f32, 176.0, - &Rect::new_size(0, 176, 320, 64)); + batch.add_rect(x as f32, 176.0, &Rect::new_size(0, 176, 320, 64)); } batch.draw(ctx)?; @@ -74,7 +69,14 @@ impl TitleScene { fn draw_text_centered(&self, text: &str, y: f32, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { let width = state.font.text_width(text.chars(), &state.constants); - state.font.draw_text(text.chars(), ((state.canvas_size.0 - width) / 2.0).floor(), y, &state.constants, &mut state.texture_set, ctx)?; + state.font.draw_text( + text.chars(), + ((state.canvas_size.0 - width) / 2.0).floor(), + y, + &state.constants, + &mut state.texture_set, + ctx, + )?; Ok(()) } @@ -93,7 +95,7 @@ impl Scene for TitleScene { self.controller.add(state.settings.create_player1_controller()); self.controller.add(state.settings.create_player2_controller()); - state.sound_manager.play_song(24, &state.constants, ctx)?; + state.sound_manager.play_song(24, &state.constants, &state.settings, ctx)?; self.main_menu.push_entry(MenuEntry::Active("New game".to_string())); self.main_menu.push_entry(MenuEntry::Active("Load game".to_string())); self.main_menu.push_entry(MenuEntry::Active("Options".to_string())); @@ -104,16 +106,21 @@ impl Scene for TitleScene { } self.main_menu.push_entry(MenuEntry::Active("Quit".to_string())); - self.option_menu.push_entry(MenuEntry::Toggle("Original timing (50TPS)".to_string(), state.timing_mode == TimingMode::_50Hz)); + self.option_menu.push_entry(MenuEntry::Toggle( + "Original timing (50TPS)".to_string(), + state.timing_mode == TimingMode::_50Hz, + )); self.option_menu.push_entry(MenuEntry::Toggle("Lighting effects".to_string(), state.settings.shader_effects)); if state.constants.supports_og_textures { - self.option_menu.push_entry(MenuEntry::Toggle("Original textures".to_string(), state.settings.original_textures)); + self.option_menu + .push_entry(MenuEntry::Toggle("Original textures".to_string(), state.settings.original_textures)); } else { self.option_menu.push_entry(MenuEntry::Disabled("Original textures".to_string())); } if state.constants.is_cs_plus { - self.option_menu.push_entry(MenuEntry::Toggle("Seasonal textures".to_string(), state.settings.seasonal_textures)); + self.option_menu + .push_entry(MenuEntry::Toggle("Seasonal textures".to_string(), state.settings.seasonal_textures)); } else { self.option_menu.push_entry(MenuEntry::Disabled("Seasonal textures".to_string())); } @@ -147,75 +154,75 @@ impl Scene for TitleScene { self.option_menu.y = ((state.canvas_size.1 + 70.0 - self.option_menu.height as f32) / 2.0).floor() as isize; match self.current_menu { - CurrentMenu::MainMenu => { - match self.main_menu.tick(&mut self.controller, state) { - MenuSelectionResult::Selected(0, _) => { - state.reset(); - state.sound_manager.play_song(0, &state.constants, ctx)?; - self.tick = 1; - self.current_menu = CurrentMenu::StartGame; - } - MenuSelectionResult::Selected(1, _) => { - state.sound_manager.play_song(0, &state.constants, ctx)?; - self.tick = 1; - self.current_menu = CurrentMenu::LoadGame; - } - MenuSelectionResult::Selected(2, _) => { - self.current_menu = CurrentMenu::OptionMenu; - } - MenuSelectionResult::Selected(4, _) => { - state.shutdown(); - } - _ => {} + CurrentMenu::MainMenu => match self.main_menu.tick(&mut self.controller, state) { + MenuSelectionResult::Selected(0, _) => { + state.reset(); + state.sound_manager.play_song(0, &state.constants, &state.settings, ctx)?; + self.tick = 1; + self.current_menu = CurrentMenu::StartGame; } - } - CurrentMenu::OptionMenu => { - match self.option_menu.tick(&mut self.controller, state) { - MenuSelectionResult::Selected(0, toggle) => { - if let MenuEntry::Toggle(_, value) = toggle { - match state.timing_mode { - TimingMode::_50Hz => { state.timing_mode = TimingMode::_60Hz } - TimingMode::_60Hz => { state.timing_mode = TimingMode::_50Hz } - _ => {} - } - - *value = state.timing_mode == TimingMode::_50Hz; - } - } - MenuSelectionResult::Selected(1, toggle) => { - if let MenuEntry::Toggle(_, value) = toggle { - state.settings.shader_effects = !state.settings.shader_effects; - - *value = state.settings.shader_effects; - } - } - MenuSelectionResult::Selected(2, toggle) => { - if let MenuEntry::Toggle(_, value) = toggle { - state.settings.original_textures = !state.settings.original_textures; - state.reload_textures(); - - *value = state.settings.original_textures; - } - } - MenuSelectionResult::Selected(3, toggle) => { - if let MenuEntry::Toggle(_, value) = toggle { - state.settings.seasonal_textures = !state.settings.seasonal_textures; - state.reload_textures(); - - *value = state.settings.seasonal_textures; - } - } - MenuSelectionResult::Selected(4, _) => { - if let Err(e) = webbrowser::open(DISCORD_LINK) { - log::warn!("Error opening web browser: {}", e); - } - } - MenuSelectionResult::Selected(6, _) | MenuSelectionResult::Canceled => { - self.current_menu = CurrentMenu::MainMenu; - } - _ => {} + MenuSelectionResult::Selected(1, _) => { + state.sound_manager.play_song(0, &state.constants, &state.settings, ctx)?; + self.tick = 1; + self.current_menu = CurrentMenu::LoadGame; } - } + MenuSelectionResult::Selected(2, _) => { + self.current_menu = CurrentMenu::OptionMenu; + } + MenuSelectionResult::Selected(4, _) => { + state.shutdown(); + } + _ => {} + }, + CurrentMenu::OptionMenu => match self.option_menu.tick(&mut self.controller, state) { + MenuSelectionResult::Selected(0, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + match state.timing_mode { + TimingMode::_50Hz => state.timing_mode = TimingMode::_60Hz, + TimingMode::_60Hz => state.timing_mode = TimingMode::_50Hz, + _ => {} + } + state.settings.save(ctx); + + *value = state.timing_mode == TimingMode::_50Hz; + } + } + MenuSelectionResult::Selected(1, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + state.settings.shader_effects = !state.settings.shader_effects; + state.settings.save(ctx); + + *value = state.settings.shader_effects; + } + } + MenuSelectionResult::Selected(2, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + state.settings.original_textures = !state.settings.original_textures; + state.reload_textures(); + state.settings.save(ctx); + + *value = state.settings.original_textures; + } + } + MenuSelectionResult::Selected(3, toggle) => { + if let MenuEntry::Toggle(_, value) = toggle { + state.settings.seasonal_textures = !state.settings.seasonal_textures; + state.reload_textures(); + state.settings.save(ctx); + + *value = state.settings.seasonal_textures; + } + } + MenuSelectionResult::Selected(4, _) => { + if let Err(e) = webbrowser::open(DISCORD_LINK) { + log::warn!("Error opening web browser: {}", e); + } + } + MenuSelectionResult::Selected(6, _) | MenuSelectionResult::Canceled => { + self.current_menu = CurrentMenu::MainMenu; + } + _ => {} + }, CurrentMenu::StartGame => { if self.tick == 10 { state.start_new_game(ctx)?; @@ -244,9 +251,11 @@ impl Scene for TitleScene { { let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Title")?; - batch.add_rect(((state.canvas_size.0 - state.constants.title.logo_rect.width() as f32) / 2.0).floor(), - 40.0, - &state.constants.title.logo_rect); + batch.add_rect( + ((state.canvas_size.0 - state.constants.title.logo_rect.width() as f32) / 2.0).floor(), + 40.0, + &state.constants.title.logo_rect, + ); batch.draw(ctx)?; } @@ -262,8 +271,12 @@ impl Scene for TitleScene { } match self.current_menu { - CurrentMenu::MainMenu => { self.main_menu.draw(state, ctx)?; } - CurrentMenu::OptionMenu => { self.option_menu.draw(state, ctx)?; } + CurrentMenu::MainMenu => { + self.main_menu.draw(state, ctx)?; + } + CurrentMenu::OptionMenu => { + self.option_menu.draw(state, ctx)?; + } _ => {} } diff --git a/src/scripting/doukutsu.d.ts b/src/scripting/doukutsu.d.ts index 6ea4d4b..002860f 100644 --- a/src/scripting/doukutsu.d.ts +++ b/src/scripting/doukutsu.d.ts @@ -1,11 +1,34 @@ declare type EventHandler = (this: void, param: T) => void; +/** + * Represents a + */ declare interface DoukutsuPlayer { + /** + * The ID of player. + */ + id(): number; + + /** + * Current position of player in X axis (as floating point, not internal fixed point representation). + */ x(): number; + + /** + * Current position of player in Y axis (as floating point, not internal fixed point representation). + */ y(): number; + + /** + * Current velocity of player in X axis (as floating point, not internal fixed point representation). + */ velX(): number; + + /** + * Current velocity of player in Y axis (as floating point, not internal fixed point representation). + */ velY(): number; -}; +} declare interface DoukutsuScene { /** @@ -32,7 +55,7 @@ declare interface DoukutsuScene { * Returns player with specified id. */ player(id: number): DoukutsuPlayer | null; -}; +} declare namespace doukutsu { /** @@ -49,4 +72,4 @@ declare namespace doukutsu { function on(event: "tick", handler: EventHandler): EventHandler; function on(event: string, handler: EventHandler): EventHandler; -}; +} diff --git a/src/scripting/doukutsu.rs b/src/scripting/doukutsu.rs index f7845d5..b757a11 100644 --- a/src/scripting/doukutsu.rs +++ b/src/scripting/doukutsu.rs @@ -1,5 +1,5 @@ use lua_ffi::ffi::luaL_Reg; -use lua_ffi::{LuaObject, State, c_int}; +use lua_ffi::{c_int, LuaObject, State}; use crate::scripting::LuaScriptingState; @@ -10,9 +10,7 @@ pub struct Doukutsu { #[allow(unused)] impl Doukutsu { pub fn new(ptr: *mut LuaScriptingState) -> Doukutsu { - Doukutsu { - ptr, - } + Doukutsu { ptr } } unsafe fn lua_play_sfx(&self, state: &mut State) -> c_int { @@ -30,7 +28,8 @@ impl Doukutsu { let game_state = &mut (*(*self.ptr).state_ptr); let ctx = &mut (*(*self.ptr).ctx_ptr); - let _ = game_state.sound_manager.play_song(index as usize, &game_state.constants, ctx); + let _ = + game_state.sound_manager.play_song(index as usize, &game_state.constants, &game_state.settings, ctx); } 0 diff --git a/src/settings.rs b/src/settings.rs index 629456e..3c0e5f8 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,7 +1,9 @@ use serde::{Deserialize, Serialize}; +use serde_yaml::Error; use crate::framework::context::Context; use crate::framework::error::GameResult; +use crate::framework::filesystem::{user_create, user_open}; use crate::framework::keyboard::ScanCode; use crate::input::keyboard_player_controller::KeyboardController; use crate::input::player_controller::PlayerController; @@ -16,9 +18,12 @@ pub struct Settings { pub subpixel_coords: bool, pub motion_interpolation: bool, pub touch_controls: bool, + pub soundtrack: String, + #[serde(default = "p1_default_keymap")] pub player1_key_map: PlayerKeyMap, + #[serde(default = "p2_default_keymap")] pub player2_key_map: PlayerKeyMap, - #[serde(skip)] + #[serde(skip, default = "default_speed")] pub speed: f64, #[serde(skip)] pub god_mode: bool, @@ -28,11 +33,28 @@ pub struct Settings { pub debug_outlines: bool, } +fn default_speed() -> f64 { + 1.0 +} + impl Settings { - pub fn load(_ctx: &mut Context) -> GameResult { + pub fn load(ctx: &Context) -> GameResult { + if let Ok(file) = user_open(ctx, "/settings.yml") { + match serde_yaml::from_reader::<_, Settings>(file) { + Ok(settings) => return Ok(settings), + Err(err) => log::warn!("Failed to deserialize settings: {}", err), + } + } + Ok(Settings::default()) } + pub fn save(&self, ctx: &Context) -> GameResult { + let file = user_create(ctx, "/settings.yml")?; + serde_yaml::to_writer(file, self)?; + Ok(()) + } + pub fn create_player1_controller(&self) -> Box { if self.touch_controls { return Box::new(TouchPlayerController::new()); @@ -55,6 +77,7 @@ impl Default for Settings { subpixel_coords: true, motion_interpolation: true, touch_controls: cfg!(target_os = "android"), + soundtrack: "".to_string(), player1_key_map: p1_default_keymap(), player2_key_map: p2_default_keymap(), speed: 1.0, @@ -90,7 +113,7 @@ fn p1_default_keymap() -> PlayerKeyMap { next_weapon: ScanCode::S, jump: ScanCode::Z, shoot: ScanCode::X, - skip: ScanCode::LControl, + skip: ScanCode::E, inventory: ScanCode::Q, map: ScanCode::W, } diff --git a/src/sound/mod.rs b/src/sound/mod.rs index 6b1f170..0a35d52 100644 --- a/src/sound/mod.rs +++ b/src/sound/mod.rs @@ -2,28 +2,33 @@ use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; use std::time::Duration; -use cpal::Sample; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::Sample; +use lewton::inside_ogg::OggStreamReader; use num_traits::clamp; use crate::engine_constants::EngineConstants; use crate::framework::context::Context; -use crate::framework::error::{GameResult}; +use crate::framework::error::GameError::{AudioError, InvalidValue}; +use crate::framework::error::{GameError, GameResult}; use crate::framework::filesystem; +use crate::framework::filesystem::File; +use crate::sound::ogg_playback::{OggPlaybackEngine, SavedOggPlaybackState}; +use crate::sound::org_playback::{OrgPlaybackEngine, SavedOrganyaPlaybackState}; use crate::sound::organya::Song; use crate::sound::pixtone::PixTonePlayback; -use crate::sound::playback::{PlaybackEngine, SavedPlaybackState}; use crate::sound::wave_bank::SoundBank; use crate::str; -use crate::framework::error::GameError::{AudioError, ResourceLoadError, InvalidValue}; +use crate::settings::Settings; -mod wave_bank; +mod ogg_playback; +mod org_playback; mod organya; mod pixtone; mod pixtone_sfx; -mod playback; mod stuff; mod wav; +mod wave_bank; pub struct SoundManager { tx: Sender, @@ -31,61 +36,26 @@ pub struct SoundManager { current_song_id: usize, } -static SONGS: [&str; 43] = [ - "xxxx", - "wanpaku", - "anzen", - "gameover", - "gravity", - "weed", - "mdown2", - "fireeye", - "vivi", - "mura", - "fanfale1", - "ginsuke", - "cemetery", - "plant", - "kodou", - "fanfale3", - "fanfale2", - "dr", - "escape", - "jenka", - "maze", - "access", - "ironh", - "grand", - "curly", - "oside", - "requiem", - "wanpak2", - "quiet", - "lastcave", - "balcony", - "lastbtl", - "lastbt3", - "ending", - "zonbie", - "bdown", - "hell", - "jenka2", - "marine", - "ballos", - "toroko", - "white", - "kaze" -]; +enum SongFormat { + Organya, + OggSinglePart, + OggMultiPart, +} impl SoundManager { pub fn new(ctx: &mut Context) -> GameResult { let (tx, rx): (Sender, Receiver) = mpsc::channel(); let host = cpal::default_host(); - let device = host.default_output_device().ok_or_else(|| AudioError(str!("Error initializing audio device.")))?; + let device = host + .default_output_device() + .ok_or_else(|| AudioError(str!("Error initializing audio device.")))?; let config = device.default_output_config()?; - let bnk = wave_bank::SoundBank::load_from(filesystem::open(ctx, "/builtin/organya-wavetable-doukutsu.bin")?)?; + let bnk = wave_bank::SoundBank::load_from(filesystem::open( + ctx, + "/builtin/organya-wavetable-doukutsu.bin", + )?)?; std::thread::spawn(move || { if let Err(err) = match config.sample_format() { @@ -108,7 +78,13 @@ impl SoundManager { let _ = self.tx.send(PlaybackMessage::PlaySample(id)); } - pub fn play_song(&mut self, song_id: usize, constants: &EngineConstants, ctx: &mut Context) -> GameResult { + pub fn play_song( + &mut self, + song_id: usize, + constants: &EngineConstants, + settings: &Settings, + ctx: &mut Context, + ) -> GameResult { if self.current_song_id == song_id { return Ok(()); } @@ -121,27 +97,139 @@ impl SoundManager { self.tx.send(PlaybackMessage::SaveState)?; self.tx.send(PlaybackMessage::Stop)?; - } else if let Some(song_name) = SONGS.get(song_id) { - let path = constants.organya_paths - .iter() - .map(|prefix| [prefix, &song_name.to_lowercase(), ".org"].join("")) - .find(|path| filesystem::exists(ctx, path)) - .ok_or_else(|| ResourceLoadError(format!("BGM {:?} does not exist.", song_name)))?; + } else if let Some(song_name) = constants.music_table.get(song_id) { + let mut paths = constants.organya_paths.clone(); - match filesystem::open(ctx, path).map(|f| organya::Song::load_from(f)) { - Ok(Ok(org)) => { - log::info!("Playing BGM: {} {}", song_id, song_name); + if let Some(soundtrack) = constants.soundtracks.get(&settings.soundtrack) { + paths.insert(0, soundtrack.clone()); + } - self.prev_song_id = self.current_song_id; - self.current_song_id = song_id; - self.tx.send(PlaybackMessage::SaveState)?; - self.tx.send(PlaybackMessage::PlaySong(Box::new(org)))?; - } - Ok(Err(err)) | Err(err) => { - log::warn!("Failed to load BGM {}: {}", song_id, err); + let songs_paths = paths.iter().map(|prefix| { + [ + ( + SongFormat::OggMultiPart, + vec![ + format!("{}{}_intro.ogg", prefix, song_name), + format!("{}{}_loop.ogg", prefix, song_name), + ], + ), + ( + SongFormat::OggSinglePart, + vec![format!("{}{}.ogg", prefix, song_name)], + ), + ( + SongFormat::Organya, + vec![format!("{}{}.org", prefix, song_name)], + ), + ] + }); + + for songs in songs_paths { + for (format, paths) in songs + .iter() + .filter(|(_, paths)| paths.iter().all(|path| filesystem::exists(ctx, path))) + { + match format { + SongFormat::Organya => { + // we're sure that there's one element + let path = unsafe { paths.get_unchecked(0) }; + + match filesystem::open(ctx, path).map(|f| organya::Song::load_from(f)) { + Ok(Ok(org)) => { + log::info!("Playing Organya BGM: {} {}", song_id, path); + + self.prev_song_id = self.current_song_id; + self.current_song_id = song_id; + self.tx.send(PlaybackMessage::SaveState)?; + self.tx + .send(PlaybackMessage::PlayOrganyaSong(Box::new(org)))?; + + return Ok(()); + } + Ok(Err(err)) | Err(err) => { + log::warn!("Failed to load Organya BGM {}: {}", song_id, err); + } + } + } + SongFormat::OggSinglePart => { + // we're sure that there's one element + let path = unsafe { paths.get_unchecked(0) }; + + match filesystem::open(ctx, path).map(|f| { + OggStreamReader::new(f) + .map_err(|e| GameError::ResourceLoadError(e.to_string())) + }) { + Ok(Ok(song)) => { + log::info!("Playing single part Ogg BGM: {} {}", song_id, path); + + self.prev_song_id = self.current_song_id; + self.current_song_id = song_id; + self.tx.send(PlaybackMessage::SaveState)?; + self.tx.send(PlaybackMessage::PlayOggSongSinglePart( + Box::new(song), + ))?; + + return Ok(()); + } + Ok(Err(err)) | Err(err) => { + log::warn!( + "Failed to load single part Ogg BGM {}: {}", + song_id, + err + ); + } + } + } + SongFormat::OggMultiPart => { + // we're sure that there are two elements + let path_intro = unsafe { paths.get_unchecked(0) }; + let path_loop = unsafe { paths.get_unchecked(1) }; + + match ( + filesystem::open(ctx, path_intro).map(|f| { + OggStreamReader::new(f) + .map_err(|e| GameError::ResourceLoadError(e.to_string())) + }), + filesystem::open(ctx, path_loop).map(|f| { + OggStreamReader::new(f) + .map_err(|e| GameError::ResourceLoadError(e.to_string())) + }), + ) { + (Ok(Ok(song_intro)), Ok(Ok(song_loop))) => { + log::info!( + "Playing multi part Ogg BGM: {} {} + {}", + song_id, + path_intro, + path_loop + ); + + self.prev_song_id = self.current_song_id; + self.current_song_id = song_id; + self.tx.send(PlaybackMessage::SaveState)?; + self.tx.send(PlaybackMessage::PlayOggSongMultiPart( + Box::new(song_intro), + Box::new(song_loop), + ))?; + + return Ok(()); + } + (Ok(Err(err)), _) + | (Err(err), _) + | (_, Ok(Err(err))) + | (_, Err(err)) => { + log::warn!( + "Failed to load multi part Ogg BGM {}: {}", + song_id, + err + ); + } + } + } + } } } } + Ok(()) } @@ -175,7 +263,9 @@ impl SoundManager { enum PlaybackMessage { Stop, - PlaySong(Box), + PlayOrganyaSong(Box), + PlayOggSongSinglePart(Box>), + PlayOggSongMultiPart(Box>, Box>), PlaySample(u8), SetSpeed(f32), SaveState, @@ -185,32 +275,46 @@ enum PlaybackMessage { #[derive(PartialEq, Eq)] enum PlaybackState { Stopped, - Playing, + PlayingOrg, + PlayingOgg, } -fn run(rx: Receiver, bank: SoundBank, - device: &cpal::Device, config: &cpal::StreamConfig) -> GameResult where +enum PlaybackStateType { + None, + Organya(SavedOrganyaPlaybackState), + Ogg(SavedOggPlaybackState), +} + +fn run( + rx: Receiver, + bank: SoundBank, + device: &cpal::Device, + config: &cpal::StreamConfig, +) -> GameResult +where T: cpal::Sample, { let sample_rate = config.sample_rate.0 as f32; let channels = config.channels as usize; let mut state = PlaybackState::Stopped; - let mut saved_state: Option = None; + let mut saved_state: PlaybackStateType = PlaybackStateType::None; let mut speed = 1.0; - let mut org_engine = PlaybackEngine::new(Song::empty(), &bank); + let mut org_engine = OrgPlaybackEngine::new(&bank); + let mut ogg_engine = OggPlaybackEngine::new(); let mut pixtone = PixTonePlayback::new(); pixtone.create_samples(); log::info!("Audio format: {} {}", sample_rate, channels); org_engine.set_sample_rate(sample_rate as usize); org_engine.loops = usize::MAX; + ogg_engine.set_sample_rate(sample_rate as usize); let buf_size = sample_rate as usize * 10 / 1000; - let mut bgm_buf = vec![0x8080; buf_size]; + let mut bgm_buf = vec![0x8080; buf_size * 2]; let mut pxt_buf = vec![0x8000; buf_size]; let mut bgm_index = 0; let mut pxt_index = 0; - let mut frames = org_engine.render_to(&mut bgm_buf); + let mut samples = 0; pixtone.mix(&mut pxt_buf, sample_rate); let err_fn = |err| eprintln!("an error occurred on stream: {}", err); @@ -220,25 +324,57 @@ fn run(rx: Receiver, bank: SoundBank, move |data: &mut [T], _: &cpal::OutputCallbackInfo| { loop { match rx.try_recv() { - Ok(PlaybackMessage::PlaySong(song)) => { + Ok(PlaybackMessage::PlayOrganyaSong(song)) => { if state == PlaybackState::Stopped { - saved_state = None; + saved_state = PlaybackStateType::None; } org_engine.start_song(*song, &bank); - for i in &mut bgm_buf[0..frames] { *i = 0x8080 }; - frames = org_engine.render_to(&mut bgm_buf); + for i in &mut bgm_buf[0..samples] { + *i = 0x8080 + } + samples = org_engine.render_to(&mut bgm_buf); bgm_index = 0; - state = PlaybackState::Playing; + state = PlaybackState::PlayingOrg; + } + Ok(PlaybackMessage::PlayOggSongSinglePart(data)) => { + if state == PlaybackState::Stopped { + saved_state = PlaybackStateType::None; + } + + ogg_engine.start_single(data); + + for i in &mut bgm_buf[0..samples] { + *i = 0x8000 + } + samples = ogg_engine.render_to(&mut bgm_buf); + bgm_index = 0; + + state = PlaybackState::PlayingOgg; + } + Ok(PlaybackMessage::PlayOggSongMultiPart(data_intro, data_loop)) => { + if state == PlaybackState::Stopped { + saved_state = PlaybackStateType::None; + } + + ogg_engine.start_multi(data_intro, data_loop); + + for i in &mut bgm_buf[0..samples] { + *i = 0x8000 + } + samples = ogg_engine.render_to(&mut bgm_buf); + bgm_index = 0; + + state = PlaybackState::PlayingOgg; } Ok(PlaybackMessage::PlaySample(id)) => { pixtone.play_sfx(id); } Ok(PlaybackMessage::Stop) => { if state == PlaybackState::Stopped { - saved_state = None; + saved_state = PlaybackStateType::None; } state = PlaybackState::Stopped; @@ -246,74 +382,143 @@ fn run(rx: Receiver, bank: SoundBank, Ok(PlaybackMessage::SetSpeed(new_speed)) => { assert!(new_speed > 0.0); speed = new_speed; + ogg_engine.set_sample_rate((sample_rate / new_speed) as usize); org_engine.set_sample_rate((sample_rate / new_speed) as usize); } Ok(PlaybackMessage::SaveState) => { - saved_state = Some(org_engine.get_state()); + saved_state = match state { + PlaybackState::Stopped => PlaybackStateType::None, + PlaybackState::PlayingOrg => { + PlaybackStateType::Organya(org_engine.get_state()) + } + PlaybackState::PlayingOgg => { + PlaybackStateType::Ogg(ogg_engine.get_state()) + } + }; } Ok(PlaybackMessage::RestoreState) => { - if saved_state.is_some() { - org_engine.set_state(saved_state.clone().unwrap(), &bank); - saved_state = None; + let mut saved_state_loc = PlaybackStateType::None; + std::mem::swap(&mut saved_state_loc, &mut saved_state); - if state == PlaybackState::Stopped { - org_engine.set_position(0); + match saved_state_loc { + PlaybackStateType::None => {} + PlaybackStateType::Organya(playback_state) => { + org_engine.set_state(playback_state, &bank); + + if state == PlaybackState::Stopped { + org_engine.rewind(); + } + + for i in &mut bgm_buf[0..samples] { + *i = 0x8080 + } + samples = org_engine.render_to(&mut bgm_buf); + bgm_index = 0; + + state = PlaybackState::PlayingOrg; } + PlaybackStateType::Ogg(playback_state) => { + ogg_engine.set_state(playback_state); - for i in &mut bgm_buf[0..frames] { *i = 0x8080 }; - frames = org_engine.render_to(&mut bgm_buf); - bgm_index = 0; + if state == PlaybackState::Stopped { + ogg_engine.rewind(); + } - state = PlaybackState::Playing; + for i in &mut bgm_buf[0..samples] { + *i = 0x8000 + } + samples = ogg_engine.render_to(&mut bgm_buf); + bgm_index = 0; + + state = PlaybackState::PlayingOgg; + } } } - Err(_) => { break; } + Err(_) => { + break; + } } } for frame in data.chunks_mut(channels) { - let (org_sample_l, org_sample_r): (u16, u16) = { + let (bgm_sample_l, bgm_sample_r): (u16, u16) = { if state == PlaybackState::Stopped { (0x8000, 0x8000) - } else if bgm_index < frames { - let sample = bgm_buf[bgm_index]; - bgm_index += 1; - ((sample & 0xff) << 8, sample & 0xff00) + } else if bgm_index < samples { + match state { + PlaybackState::PlayingOrg => { + let sample = bgm_buf[bgm_index]; + bgm_index += 1; + ((sample & 0xff) << 8, sample & 0xff00) + } + PlaybackState::PlayingOgg => { + let samples = (bgm_buf[bgm_index], bgm_buf[bgm_index + 1]); + bgm_index += 2; + samples + } + _ => unreachable!(), + } } else { - for i in &mut bgm_buf[0..frames] { *i = 0x8080 }; - frames = org_engine.render_to(&mut bgm_buf); - bgm_index = 0; - let sample = bgm_buf[0]; - ((sample & 0xff) << 8, sample & 0xff00) + for i in &mut bgm_buf[0..samples] { + *i = 0x8080 + } + + match state { + PlaybackState::PlayingOrg => { + samples = org_engine.render_to(&mut bgm_buf); + bgm_index = 1; + let sample = bgm_buf[0]; + ((sample & 0xff) << 8, sample & 0xff00) + } + PlaybackState::PlayingOgg => { + samples = ogg_engine.render_to(&mut bgm_buf); + bgm_index = 2; + (bgm_buf[0], bgm_buf[1]) + } + _ => unreachable!(), + } } }; + let pxt_sample: u16 = pxt_buf[pxt_index]; if pxt_index < (pxt_buf.len() - 1) { pxt_index += 1; } else { pxt_index = 0; - for i in pxt_buf.iter_mut() { *i = 0x8000 }; + for i in pxt_buf.iter_mut() { + *i = 0x8000 + } pixtone.mix(&mut pxt_buf, sample_rate / speed); } if frame.len() >= 2 { let sample_l = clamp( - (((org_sample_l ^ 0x8000) as i16) as isize) - + (((pxt_sample ^ 0x8000) as i16) as isize) - , -0x7fff, 0x7fff) as u16 ^ 0x8000; + (((bgm_sample_l ^ 0x8000) as i16) as isize) + + (((pxt_sample ^ 0x8000) as i16) as isize), + -0x7fff, + 0x7fff, + ) as u16 + ^ 0x8000; let sample_r = clamp( - (((org_sample_r ^ 0x8000) as i16) as isize) - + (((pxt_sample ^ 0x8000) as i16) as isize) - , -0x7fff, 0x7fff) as u16 ^ 0x8000; + (((bgm_sample_r ^ 0x8000) as i16) as isize) + + (((pxt_sample ^ 0x8000) as i16) as isize), + -0x7fff, + 0x7fff, + ) as u16 + ^ 0x8000; frame[0] = Sample::from::(&sample_l); frame[1] = Sample::from::(&sample_r); } else { let sample = clamp( - (((org_sample_l ^ 0x8000) as i16) as isize) - + (((pxt_sample ^ 0x8000) as i16) as isize) - , -0x7fff, 0x7fff) as u16 ^ 0x8000; + ((((bgm_sample_l ^ 0x8000) as i16) + ((bgm_sample_r ^ 0x8000) as i16)) / 2) + as isize + + (((pxt_sample ^ 0x8000) as i16) as isize), + -0x7fff, + 0x7fff, + ) as u16 + ^ 0x8000; frame[0] = Sample::from::(&sample); } diff --git a/src/sound/ogg_playback.rs b/src/sound/ogg_playback.rs new file mode 100644 index 0000000..6f391dc --- /dev/null +++ b/src/sound/ogg_playback.rs @@ -0,0 +1,193 @@ +use std::sync::{Arc, RwLock}; + +use lewton::inside_ogg::OggStreamReader; +use num_traits::clamp; + +use crate::framework::filesystem::File; +use crate::sound::stuff::cubic_interp; +use crate::sound::wav::WavFormat; + +pub(crate) struct OggPlaybackEngine { + intro_music: Option>>>>, + loop_music: Option>>>>, + output_format: WavFormat, + playing_intro: bool, + position: u64, + buffer: Vec, +} + +pub struct SavedOggPlaybackState { + intro_music: Option>>>>, + loop_music: Option>>>>, + playing_intro: bool, + position: u64, +} + +impl OggPlaybackEngine { + pub fn new() -> OggPlaybackEngine { + OggPlaybackEngine { + intro_music: None, + loop_music: None, + output_format: WavFormat { + channels: 2, + sample_rate: 44100, + bit_depth: 16, + }, + playing_intro: false, + position: 0, + buffer: Vec::with_capacity(4096), + } + } + + pub fn set_sample_rate(&mut self, sample_rate: usize) {} + + pub fn get_state(&self) -> SavedOggPlaybackState { + SavedOggPlaybackState { + intro_music: self.intro_music.clone(), + loop_music: self.loop_music.clone(), + playing_intro: self.playing_intro, + position: self.position, + } + } + + pub fn set_state(&mut self, state: SavedOggPlaybackState) { + self.intro_music = state.intro_music; + self.loop_music = state.loop_music; + self.playing_intro = state.playing_intro; + self.position = state.position; + } + + pub fn start_single(&mut self, loop_music: Box>) { + self.intro_music = None; + self.loop_music = Some(Arc::new(RwLock::new(loop_music))); + self.playing_intro = false; + self.position = 0; + } + + pub fn start_multi( + &mut self, + intro_music: Box>, + loop_music: Box>, + ) { + self.intro_music = Some(Arc::new(RwLock::new(intro_music))); + self.loop_music = Some(Arc::new(RwLock::new(loop_music))); + self.playing_intro = true; + self.position = 0; + } + + pub fn rewind(&mut self) { + if let Some(music) = self.intro_music.as_ref() { + let _ = music.write().unwrap().seek_absgp_pg(0); + self.position = 0; + self.playing_intro = true; + } else { + if let Some(music) = self.loop_music.as_ref() { + let _ = music.write().unwrap().seek_absgp_pg(0); + } + + self.position = 0; + self.playing_intro = false; + } + } + + fn decode(&mut self) { + if self.playing_intro { + if let Some(music) = self.intro_music.as_ref() { + let mut music = music.write().unwrap(); + + let mut buf = match music.read_dec_packet_itl() { + Ok(Some(buf)) => buf, + Ok(None) | Err(_) => { + self.playing_intro = false; + return; + } + }; + + self.position = music.get_last_absgp().unwrap_or(0); + buf = self.resample_buffer( + buf, + music.ident_hdr.audio_sample_rate, + music.ident_hdr.audio_channels, + ); + self.buffer.append(&mut buf); + } else { + self.playing_intro = false; + } + } else { + if let Some(music) = self.loop_music.as_ref() { + let mut music = music.write().unwrap(); + + let mut buf = match music.read_dec_packet_itl() { + Ok(Some(buf)) => buf, + Ok(None) => { + if let Err(e) = music.seek_absgp_pg(0) { + vec![0, 1000] + } else { + return; + } + } + Err(e) => { + vec![0, 1000] + } + }; + + self.position = music.get_last_absgp().unwrap_or(0); + buf = self.resample_buffer( + buf, + music.ident_hdr.audio_sample_rate, + music.ident_hdr.audio_channels, + ); + self.buffer.append(&mut buf); + } else { + let mut buf = vec![0; 1000]; + self.buffer.append(&mut buf); + } + } + } + + fn resample_buffer(&self, mut data: Vec, sample_rate: u32, channels: u8) -> Vec { + if sample_rate != self.output_format.sample_rate { + let mut tmp_data = Vec::new(); + let mut pos = 0.0; + let mut phase = sample_rate as f32 / self.output_format.sample_rate as f32; + + loop { + if pos >= data.len() as f32 { + data = tmp_data; + break; + } + + let s = unsafe { + let upos = pos as usize; + let s1 = (*data.get_unchecked(upos) as f32) / 32768.0; + let s2 = + (*data.get_unchecked(clamp(upos + 1, 0, data.len() - 1)) as f32) / 32768.0; + let s3 = + (*data.get_unchecked(clamp(upos + 2, 0, data.len() - 1)) as f32) / 32768.0; + let s4 = (*data.get_unchecked(upos.saturating_sub(1)) as f32) / 32768.0; + + (cubic_interp(s1, s2, s4, s3, pos.fract()) * 32768.0) as i16 + }; + tmp_data.push(s); + + pos += phase; + } + } + + data + } + + pub fn render_to(&mut self, buf: &mut [u16]) -> usize { + while self.buffer.len() < buf.len() { + self.decode(); + } + + self.buffer + .drain(0..buf.len()) + .map(|n| n as u16 ^ 0x8000) + .zip(buf.iter_mut()) + .for_each(|(n, tgt)| *tgt = n); + + buf.len() + } +} diff --git a/src/sound/playback.rs b/src/sound/org_playback.rs similarity index 77% rename from src/sound/playback.rs rename to src/sound/org_playback.rs index 3f51974..def61ce 100644 --- a/src/sound/playback.rs +++ b/src/sound/org_playback.rs @@ -5,11 +5,19 @@ use crate::sound::stuff::*; use crate::sound::wav::*; use crate::sound::wave_bank::SoundBank; -pub struct PlaybackEngine { +pub(crate) struct OrgPlaybackEngine { song: Organya, lengths: [u8; 8], swaps: [usize; 8], keys: [u8; 8], + /// Octave 0 Track 0 Swap 0 + /// Octave 0 Track 1 Swap 0 + /// ... + /// Octave 1 Track 0 Swap 0 + /// ... + /// Octave 0 Track 0 Swap 1 + /// octave * 8 + track + swap + /// 128..136: Drum Tracks track_buffers: [RenderBuffer; 136], output_format: WavFormat, play_pos: i32, @@ -18,74 +26,32 @@ pub struct PlaybackEngine { pub loops: usize, } -pub struct SavedPlaybackState { +pub struct SavedOrganyaPlaybackState { song: Organya, play_pos: i32, } -impl Clone for SavedPlaybackState { - fn clone(&self) -> SavedPlaybackState { - SavedPlaybackState { +impl Clone for SavedOrganyaPlaybackState { + fn clone(&self) -> SavedOrganyaPlaybackState { + SavedOrganyaPlaybackState { song: self.song.clone(), play_pos: self.play_pos, } } } -impl PlaybackEngine { - pub fn new(song: Organya, samples: &SoundBank) -> Self { +impl OrgPlaybackEngine { + pub fn new(samples: &SoundBank) -> Self { + let mut buffers: [MaybeUninit; 136] = unsafe { MaybeUninit::uninit().assume_init() }; - // Octave 0 Track 0 Swap 0 - // Octave 0 Track 1 Swap 0 - // ... - // Octave 1 Track 0 Swap 0 - // ... - // Octave 0 Track 0 Swap 1 - // octave * 8 + track + swap - // 128..136: Drum Tracks - let mut buffers: [MaybeUninit; 136] = unsafe { - MaybeUninit::uninit().assume_init() - }; - - // track - for i in 0..8 { - let sound_index = song.tracks[i].inst.inst as usize; - - // WAVE100 uses 8-bit signed audio, but wav audio wants 8-bit unsigned. - // On 2s complement system, we can simply flip the top bit - // No need to cast to u8 here because the sound bank data is one big &[u8]. - let sound = samples.get_wave(sound_index) - .iter() - .map(|&x| x ^ 128) - .collect(); - - let format = WavFormat { channels: 1, sample_rate: 22050, bit_depth: 8 }; - - let rbuf = RenderBuffer::new_organya(WavSample { format, data: sound }); - - // octave - for j in 0..8 { - // swap - for &k in &[0, 64] { - buffers[i + (j * 8) + k] = MaybeUninit::new(rbuf.clone()); - } - } - } - - for (idx, (_track, buf)) in song.tracks[8..].iter().zip(buffers[128..].iter_mut()).enumerate() { - *buf = - MaybeUninit::new( - RenderBuffer::new( - // FIXME: *frustrated screaming* - //samples.samples[track.inst.inst as usize].clone() - samples.samples[idx].clone() - ) - ); + for buffer in buffers.iter_mut() { + *buffer = MaybeUninit::new(RenderBuffer::empty()); } + let song = Organya::empty(); let frames_per_tick = (44100 / 1000) * song.time.wait as usize; - PlaybackEngine { + OrgPlaybackEngine { song, lengths: [0; 8], swaps: [0; 8], @@ -104,7 +70,8 @@ impl PlaybackEngine { } pub fn set_sample_rate(&mut self, sample_rate: usize) { - self.frames_this_tick = (self.frames_this_tick as f32 * (self.output_format.sample_rate as f32 / sample_rate as f32)) as usize; + self.frames_this_tick = + (self.frames_this_tick as f32 * (self.output_format.sample_rate as f32 / sample_rate as f32)) as usize; self.output_format.sample_rate = sample_rate as u32; self.frames_per_tick = (sample_rate / 1000) * self.song.time.wait as usize; @@ -113,14 +80,14 @@ impl PlaybackEngine { } } - pub fn get_state(&self) -> SavedPlaybackState { - SavedPlaybackState { + pub fn get_state(&self) -> SavedOrganyaPlaybackState { + SavedOrganyaPlaybackState { song: self.song.clone(), play_pos: self.play_pos, } } - pub fn set_state(&mut self, state: SavedPlaybackState, samples: &SoundBank) { + pub fn set_state(&mut self, state: SavedOrganyaPlaybackState, samples: &SoundBank) { self.start_song(state.song, samples); self.play_pos = state.play_pos; } @@ -128,12 +95,13 @@ impl PlaybackEngine { pub fn start_song(&mut self, song: Organya, samples: &SoundBank) { for i in 0..8 { let sound_index = song.tracks[i].inst.inst as usize; - let sound = samples.get_wave(sound_index) - .iter() - .map(|&x| x ^ 128) - .collect(); + let sound = samples.get_wave(sound_index).iter().map(|&x| x ^ 128).collect(); - let format = WavFormat { channels: 1, sample_rate: 22050, bit_depth: 8 }; + let format = WavFormat { + channels: 1, + sample_rate: 22050, + bit_depth: 8, + }; let rbuf = RenderBuffer::new_organya(WavSample { format, data: sound }); @@ -144,7 +112,11 @@ impl PlaybackEngine { } } - for (idx, (track, buf)) in song.tracks[8..].iter().zip(self.track_buffers[128..].iter_mut()).enumerate() { + for (idx, (track, buf)) in song.tracks[8..] + .iter() + .zip(self.track_buffers[128..].iter_mut()) + .enumerate() + { if self.song.version == Version::Extended { *buf = RenderBuffer::new(samples.samples[track.inst.inst as usize].clone()); } else { @@ -156,16 +128,25 @@ impl PlaybackEngine { self.play_pos = 0; self.frames_per_tick = (self.output_format.sample_rate as usize / 1000) * self.song.time.wait as usize; self.frames_this_tick = 0; - for i in self.lengths.iter_mut() { *i = 0 }; - for i in self.swaps.iter_mut() { *i = 0 }; - for i in self.keys.iter_mut() { *i = 255 }; + for i in self.lengths.iter_mut() { + *i = 0 + } + for i in self.swaps.iter_mut() { + *i = 0 + } + for i in self.keys.iter_mut() { + *i = 255 + } } - #[allow(unused)] pub fn set_position(&mut self, position: i32) { self.play_pos = position; } + pub fn rewind(&mut self) { + self.set_position(0); + } + #[allow(unused)] pub fn get_total_samples(&self) -> u32 { let ticks_intro = self.song.time.loop_range.start; @@ -177,12 +158,12 @@ impl PlaybackEngine { fn update_play_state(&mut self) { for track in 0..8 { - if let Some(note) = - self.song.tracks[track].notes.iter().find(|x| x.pos == self.play_pos) { + if let Some(note) = self.song.tracks[track].notes.iter().find(|x| x.pos == self.play_pos) { // New note //eprintln!("{:?}", &self.keys); if note.key != 255 { - if self.keys[track] == 255 { // New + if self.keys[track] == 255 { + // New let octave = (note.key / 12) * 8; let j = octave as usize + track + self.swaps[track]; for k in 0..16 { @@ -194,13 +175,15 @@ impl PlaybackEngine { let l = p_oct as usize * 8 + track + swap; self.track_buffers[l].set_frequency(freq as u32); - self.track_buffers[l].organya_select_octave(p_oct as usize, self.song.tracks[track].inst.pipi != 0); + self.track_buffers[l] + .organya_select_octave(p_oct as usize, self.song.tracks[track].inst.pipi != 0); } self.track_buffers[j].looping = true; self.track_buffers[j].playing = true; // last playing key self.keys[track] = note.key; - } else if self.keys[track] == note.key { // Same + } else if self.keys[track] == note.key { + // Same //assert!(self.lengths[track] == 0); let octave = (self.keys[track] / 12) * 8; let j = octave as usize + track + self.swaps[track]; @@ -210,10 +193,12 @@ impl PlaybackEngine { self.swaps[track] += 64; self.swaps[track] %= 128; let j = octave as usize + track + self.swaps[track]; - self.track_buffers[j].organya_select_octave(note.key as usize / 12, self.song.tracks[track].inst.pipi != 0); + self.track_buffers[j] + .organya_select_octave(note.key as usize / 12, self.song.tracks[track].inst.pipi != 0); self.track_buffers[j].looping = true; self.track_buffers[j].playing = true; - } else { // change + } else { + // change let octave = (self.keys[track] / 12) * 8; let j = octave as usize + track + self.swaps[track]; if self.song.tracks[track].inst.pipi == 0 { @@ -231,7 +216,8 @@ impl PlaybackEngine { let freq = org_key_to_freq(key + p_oct * 12, self.song.tracks[track].inst.freq as i16); let l = p_oct as usize * 8 + track + swap; self.track_buffers[l].set_frequency(freq as u32); - self.track_buffers[l].organya_select_octave(p_oct as usize, self.song.tracks[track].inst.pipi != 0); + self.track_buffers[l] + .organya_select_octave(p_oct as usize, self.song.tracks[track].inst.pipi != 0); } self.track_buffers[j].looping = true; self.track_buffers[j].playing = true; @@ -278,9 +264,7 @@ impl PlaybackEngine { // start a new note // note (hah) that drums are unaffected by length and pi values. This is the only case we have to handle. - if let Some(note) = - notes.iter().find(|x| x.pos == self.play_pos) { - + if let Some(note) = notes.iter().find(|x| x.pos == self.play_pos) { // FIXME: Add constants for dummy values if note.key != 255 { let freq = org_key_to_drum_freq(note.key); @@ -344,13 +328,12 @@ pub fn mix(dst: &mut [u16], dst_fmt: WavFormat, srcs: &mut [RenderBuffer]) { let vol = centibel_to_scale(buf.volume); - let (pan_l, pan_r) = - match buf.pan.signum() { - 0 => (1.0, 1.0), - 1 => (centibel_to_scale(-buf.pan), 1.0), - -1 => (1.0, centibel_to_scale(buf.pan)), - _ => unsafe { std::hint::unreachable_unchecked() } - }; + let (pan_l, pan_r) = match buf.pan.signum() { + 0 => (1.0, 1.0), + 1 => (centibel_to_scale(-buf.pan), 1.0), + -1 => (1.0, centibel_to_scale(buf.pan)), + _ => unsafe { std::hint::unreachable_unchecked() }, + }; fn clamp(v: T, limit: T) -> T { if v > limit { @@ -361,7 +344,6 @@ pub fn mix(dst: &mut [u16], dst_fmt: WavFormat, srcs: &mut [RenderBuffer]) { } #[allow(unused_variables)] - for frame in dst.iter_mut() { let pos = buf.position as usize + buf.base_pos; // -1..1 @@ -379,7 +361,7 @@ pub fn mix(dst: &mut [u16], dst_fmt: WavFormat, srcs: &mut [RenderBuffer]) { //let s = s1 + (s2 - s1) * r1; // Linear interp //let s = s1 * (1.0 - r2) + s2 * r2; // Cosine interp let s = cubic_interp(s1, s2, s4, s3, r1); // Cubic interp - // Ideally we want sinc/lanczos interpolation, since that's what DirectSound appears to use. + // Ideally we want sinc/lanczos interpolation, since that's what DirectSound appears to use. // -128..128 let sl = s * pan_l * vol * 128.0; @@ -450,6 +432,28 @@ impl RenderBuffer { } } + pub fn empty() -> RenderBuffer { + RenderBuffer { + position: 0.0, + frequency: 22050, + volume: 0, + pan: 0, + len: 0, + sample: WavSample { + format: WavFormat { + channels: 2, + sample_rate: 22050, + bit_depth: 16, + }, + data: vec![], + }, + playing: false, + looping: false, + base_pos: 0, + nloops: -1, + } + } + pub fn new_organya(mut sample: WavSample) -> RenderBuffer { let wave = sample.data.clone(); sample.data.clear(); @@ -473,10 +477,7 @@ impl RenderBuffer { #[inline] pub fn organya_select_octave(&mut self, octave: usize, pipi: bool) { - const OFFS: &[usize] = &[0x000, 0x100, - 0x200, 0x280, - 0x300, 0x340, - 0x360, 0x370]; + const OFFS: &[usize] = &[0x000, 0x100, 0x200, 0x280, 0x300, 0x340, 0x360, 0x370]; const LENS: &[usize] = &[256_usize, 256, 128, 128, 64, 32, 16, 8]; self.base_pos = OFFS[octave]; self.len = LENS[octave]; diff --git a/src/text_script.rs b/src/text_script.rs index cea0f7e..52b918c 100644 --- a/src/text_script.rs +++ b/src/text_script.rs @@ -289,7 +289,6 @@ pub enum OpCode { KE2, /// GameResult { loop { - if state.textscript_vm.suspend { break; } + if state.textscript_vm.suspend { + break; + } match state.textscript_vm.state { TextScriptExecutionState::Ended => { @@ -569,12 +574,14 @@ impl TextScriptVM { if remaining > 1 { let ticks = if state.textscript_vm.flags.fast() || game_scene.player1.controller.skip() - || game_scene.player2.controller.skip() { + || game_scene.player2.controller.skip() + { 0 } else if game_scene.player1.controller.jump() || game_scene.player1.controller.shoot() || game_scene.player2.controller.jump() - || game_scene.player2.controller.shoot() { + || game_scene.player2.controller.shoot() + { 1 } else { 4 @@ -584,9 +591,11 @@ impl TextScriptVM { state.sound_manager.play_sfx(2); } - state.textscript_vm.state = TextScriptExecutionState::Msg(event, cursor.position() as u32, remaining - 1, ticks); + state.textscript_vm.state = + TextScriptExecutionState::Msg(event, cursor.position() as u32, remaining - 1, ticks); } else { - state.textscript_vm.state = TextScriptExecutionState::Running(event, cursor.position() as u32); + state.textscript_vm.state = + TextScriptExecutionState::Running(event, cursor.position() as u32); } } else { state.textscript_vm.reset(); @@ -602,16 +611,19 @@ impl TextScriptVM { } TextScriptExecutionState::WaitConfirmation(event, ip, no_event, wait, selection) => { if wait > 0 { - state.textscript_vm.state = TextScriptExecutionState::WaitConfirmation(event, ip, no_event, wait - 1, selection); + state.textscript_vm.state = + TextScriptExecutionState::WaitConfirmation(event, ip, no_event, wait - 1, selection); break; } if game_scene.player1.controller.trigger_left() || game_scene.player1.controller.trigger_right() || game_scene.player2.controller.trigger_left() - || game_scene.player2.controller.trigger_right() { + || game_scene.player2.controller.trigger_right() + { state.sound_manager.play_sfx(1); - state.textscript_vm.state = TextScriptExecutionState::WaitConfirmation(event, ip, no_event, 0, !selection); + state.textscript_vm.state = + TextScriptExecutionState::WaitConfirmation(event, ip, no_event, 0, !selection); break; } @@ -641,7 +653,8 @@ impl TextScriptVM { || game_scene.player1.controller.skip() || game_scene.player2.controller.trigger_jump() || game_scene.player2.controller.trigger_shoot() - || game_scene.player2.controller.skip() { + || game_scene.player2.controller.skip() + { state.textscript_vm.state = TextScriptExecutionState::Running(event, ip); } break; @@ -672,7 +685,13 @@ impl TextScriptVM { Ok(()) } - pub fn execute(event: u16, ip: u32, state: &mut SharedGameState, game_scene: &mut GameScene, ctx: &mut Context) -> GameResult { + pub fn execute( + event: u16, + ip: u32, + state: &mut SharedGameState, + game_scene: &mut GameScene, + ctx: &mut Context, + ) -> GameResult { let mut exec_state = state.textscript_vm.state; let state_ref = state as *mut SharedGameState; @@ -682,8 +701,8 @@ impl TextScriptVM { let mut cursor = Cursor::new(bytecode); cursor.seek(SeekFrom::Start(ip as u64))?; - let op_maybe: Option = FromPrimitive::from_i32(read_cur_varint(&mut cursor) - .unwrap_or_else(|_| OpCode::END as i32)); + let op_maybe: Option = + FromPrimitive::from_i32(read_cur_varint(&mut cursor).unwrap_or_else(|_| OpCode::END as i32)); if let Some(op) = op_maybe { println!("opcode: {:?}", op); @@ -724,7 +743,9 @@ impl TextScriptVM { OpCode::SLP => { state.textscript_vm.set_mode(ScriptMode::StageSelect); - let event_num = if let Some(slot) = state.teleporter_slots.get(game_scene.stage_select.current_teleport_slot as usize) { + let event_num = if let Some(slot) = + state.teleporter_slots.get(game_scene.stage_select.current_teleport_slot as usize) + { 1000 + slot.0 } else { 1000 @@ -1026,7 +1047,13 @@ impl TextScriptVM { state.sound_manager.play_sfx(5); - exec_state = TextScriptExecutionState::WaitConfirmation(event, cursor.position() as u32, event_no, 16, ConfirmSelection::Yes); + exec_state = TextScriptExecutionState::WaitConfirmation( + event, + cursor.position() as u32, + event_no, + 16, + ConfirmSelection::Yes, + ); } OpCode::NUM => { let index = read_cur_varint(&mut cursor)? as usize; @@ -1161,12 +1188,12 @@ impl TextScriptVM { } OpCode::CMU => { let song_id = read_cur_varint(&mut cursor)? as usize; - state.sound_manager.play_song(song_id, &state.constants, ctx)?; + state.sound_manager.play_song(song_id, &state.constants, &state.settings, ctx)?; exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } OpCode::FMU => { - state.sound_manager.play_song(0, &state.constants, ctx)?; + state.sound_manager.play_song(0, &state.constants, &state.settings, ctx)?; exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } @@ -1261,11 +1288,8 @@ impl TextScriptVM { npc.tsc_direction = tsc_direction as u16; if direction == Direction::FacingPlayer { - npc.direction = if game_scene.player1.x < npc.x { - Direction::Right - } else { - Direction::Left - }; + npc.direction = + if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left }; } else { npc.direction = direction; } @@ -1317,16 +1341,21 @@ impl TextScriptVM { npc.tsc_direction = tsc_direction as u16; if direction == Direction::FacingPlayer { - npc.direction = if game_scene.player1.x < npc.x { - Direction::Right - } else { - Direction::Left - }; + npc.direction = + if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left }; } else { npc.direction = direction; } - npc.tick(state, ([&mut game_scene.player1, &mut game_scene.player2], &game_scene.npc_list, &mut game_scene.stage, &game_scene.bullet_manager))?; + npc.tick( + state, + ( + [&mut game_scene.player1, &mut game_scene.player2], + &game_scene.npc_list, + &mut game_scene.stage, + &game_scene.bullet_manager, + ), + )?; } } @@ -1346,11 +1375,8 @@ impl TextScriptVM { npc.tsc_direction = tsc_direction as u16; if direction == Direction::FacingPlayer { - npc.direction = if game_scene.player1.x < npc.x { - Direction::Right - } else { - Direction::Left - }; + npc.direction = + if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left }; } else { npc.direction = direction; } @@ -1375,11 +1401,8 @@ impl TextScriptVM { npc.tsc_direction = tsc_direction as u16; if direction == Direction::FacingPlayer { - npc.direction = if game_scene.player1.x < npc.x { - Direction::Right - } else { - Direction::Left - }; + npc.direction = + if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left }; } else { npc.direction = direction; } @@ -1519,18 +1542,31 @@ impl TextScriptVM { } // unimplemented opcodes // Zero operands - OpCode::CIL | OpCode::CPS | OpCode::KE2 | - OpCode::CRE | OpCode::CSS | OpCode::FLA | OpCode::MLP | - OpCode::SPS | OpCode::FR2 | - OpCode::STC | OpCode::HM2 => { + OpCode::CIL + | OpCode::CPS + | OpCode::KE2 + | OpCode::CRE + | OpCode::CSS + | OpCode::FLA + | OpCode::MLP + | OpCode::SPS + | OpCode::FR2 + | OpCode::STC + | OpCode::HM2 => { log::warn!("unimplemented opcode: {:?}", op); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } // One operand codes - OpCode::MPp | OpCode::SKm | OpCode::SKp | - OpCode::UNJ | OpCode::MPJ | OpCode::XX1 | OpCode::SIL | - OpCode::SSS | OpCode::ACH => { + OpCode::MPp + | OpCode::SKm + | OpCode::SKp + | OpCode::UNJ + | OpCode::MPJ + | OpCode::XX1 + | OpCode::SIL + | OpCode::SSS + | OpCode::ACH => { let par_a = read_cur_varint(&mut cursor)?; log::warn!("unimplemented opcode: {:?} {}", op, par_a); @@ -1564,9 +1600,7 @@ pub struct TextScript { impl Clone for TextScript { fn clone(&self) -> Self { - Self { - event_map: self.event_map.clone(), - } + Self { event_map: self.event_map.clone() } } } @@ -1578,9 +1612,7 @@ impl Default for TextScript { impl TextScript { pub fn new() -> TextScript { - Self { - event_map: HashMap::new(), - } + Self { event_map: HashMap::new() } } /// Loads, decrypts and compiles a text script from specified stream. @@ -1590,11 +1622,7 @@ impl TextScript { if constants.textscript.encrypted { let half = buf.len() / 2; - let key = if let Some(0) = buf.get(half) { - 0xf9 - } else { - (-(*buf.get(half).unwrap() as isize)) as u8 - }; + let key = if let Some(0) = buf.get(half) { 0xf9 } else { (-(*buf.get(half).unwrap() as isize)) as u8 }; log::info!("Decrypting TSC using key {:#x}", key); for (idx, byte) in buf.iter_mut().enumerate() { @@ -1637,8 +1665,12 @@ impl TextScript { } match TextScript::skip_until(b'#', &mut iter).ok() { - Some(_) => { continue; } - None => { break; } + Some(_) => { + continue; + } + None => { + break; + } } } @@ -1661,12 +1693,14 @@ impl TextScript { } } - Ok(TextScript { - event_map - }) + Ok(TextScript { event_map }) } - fn compile_event>(iter: &mut Peekable, strict: bool, encoding: TextScriptEncoding) -> GameResult> { + fn compile_event>( + iter: &mut Peekable, + strict: bool, + encoding: TextScriptEncoding, + ) -> GameResult> { let mut bytecode = Vec::new(); let mut char_buf = Vec::with_capacity(16); @@ -1687,7 +1721,8 @@ impl TextScript { } iter.next(); - let n = iter.next_tuple::<(u8, u8, u8)>() + let n = iter + .next_tuple::<(u8, u8, u8)>() .map(|t| [t.0, t.1, t.2]) .ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; @@ -1747,56 +1782,144 @@ impl TextScript { out.push(n); - if x == 0 { break; } + if x == 0 { + break; + } } } #[allow(unused)] - fn read_varint>(iter: &mut I) -> GameResult { + fn read_varint>(iter: &mut I) -> GameResult { let mut result = 0u32; for o in 0..5 { let n = iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; result |= (n as u32 & 0x7f) << (o * 7); - if n & 0x80 == 0 { break; } + if n & 0x80 == 0 { + break; + } } Ok(((result << 31) ^ (result >> 1)) as i32) } - fn compile_code>(code: &str, strict: bool, iter: &mut Peekable, out: &mut Vec) -> GameResult { + fn compile_code>( + code: &str, + strict: bool, + iter: &mut Peekable, + out: &mut Vec, + ) -> GameResult { let instr = OpCode::from_str(code).map_err(|_| ParseError(format!("Unknown opcode: {}", code)))?; match instr { // Zero operand codes - OpCode::AEp | OpCode::CAT | OpCode::CIL | OpCode::CLO | OpCode::CLR | OpCode::CPS | - OpCode::CRE | OpCode::CSS | OpCode::END | OpCode::ESC | OpCode::FLA | OpCode::FMU | - OpCode::FRE | OpCode::HMC | OpCode::INI | OpCode::KEY | OpCode::LDP | OpCode::MLP | - OpCode::MM0 | OpCode::MNA | OpCode::MS2 | OpCode::MS3 | OpCode::MSG | OpCode::NOD | - OpCode::PRI | OpCode::RMU | OpCode::SAT | OpCode::SLP | OpCode::SMC | OpCode::SPS | - OpCode::STC | OpCode::SVP | OpCode::TUR | OpCode::WAS | OpCode::ZAM | OpCode::HM2 | - OpCode::POP | OpCode::KE2 | OpCode::FR2 => { + OpCode::AEp + | OpCode::CAT + | OpCode::CIL + | OpCode::CLO + | OpCode::CLR + | OpCode::CPS + | OpCode::CRE + | OpCode::CSS + | OpCode::END + | OpCode::ESC + | OpCode::FLA + | OpCode::FMU + | OpCode::FRE + | OpCode::HMC + | OpCode::INI + | OpCode::KEY + | OpCode::LDP + | OpCode::MLP + | OpCode::MM0 + | OpCode::MNA + | OpCode::MS2 + | OpCode::MS3 + | OpCode::MSG + | OpCode::NOD + | OpCode::PRI + | OpCode::RMU + | OpCode::SAT + | OpCode::SLP + | OpCode::SMC + | OpCode::SPS + | OpCode::STC + | OpCode::SVP + | OpCode::TUR + | OpCode::WAS + | OpCode::ZAM + | OpCode::HM2 + | OpCode::POP + | OpCode::KE2 + | OpCode::FR2 => { TextScript::put_varint(instr as i32, out); } // One operand codes - OpCode::BOA | OpCode::BSL | OpCode::FOB | OpCode::FOM | OpCode::QUA | OpCode::UNI | - OpCode::MYB | OpCode::MYD | OpCode::FAI | OpCode::FAO | OpCode::WAI | OpCode::FAC | - OpCode::GIT | OpCode::NUM | OpCode::DNA | OpCode::DNP | OpCode::FLm | OpCode::FLp | - OpCode::MPp | OpCode::SKm | OpCode::SKp | OpCode::EQp | OpCode::EQm | OpCode::MLp | - OpCode::ITp | OpCode::ITm | OpCode::AMm | OpCode::UNJ | OpCode::MPJ | OpCode::YNJ | - OpCode::EVE | OpCode::XX1 | OpCode::SIL | OpCode::LIp | OpCode::SOU | OpCode::CMU | - OpCode::SSS | OpCode::ACH | OpCode::S2MV | OpCode::PSH => { + OpCode::BOA + | OpCode::BSL + | OpCode::FOB + | OpCode::FOM + | OpCode::QUA + | OpCode::UNI + | OpCode::MYB + | OpCode::MYD + | OpCode::FAI + | OpCode::FAO + | OpCode::WAI + | OpCode::FAC + | OpCode::GIT + | OpCode::NUM + | OpCode::DNA + | OpCode::DNP + | OpCode::FLm + | OpCode::FLp + | OpCode::MPp + | OpCode::SKm + | OpCode::SKp + | OpCode::EQp + | OpCode::EQm + | OpCode::MLp + | OpCode::ITp + | OpCode::ITm + | OpCode::AMm + | OpCode::UNJ + | OpCode::MPJ + | OpCode::YNJ + | OpCode::EVE + | OpCode::XX1 + | OpCode::SIL + | OpCode::LIp + | OpCode::SOU + | OpCode::CMU + | OpCode::SSS + | OpCode::ACH + | OpCode::S2MV + | OpCode::PSH => { let operand = TextScript::read_number(iter)?; TextScript::put_varint(instr as i32, out); TextScript::put_varint(operand as i32, out); } // Two operand codes - OpCode::FON | OpCode::MOV | OpCode::AMp | OpCode::NCJ | OpCode::ECJ | OpCode::FLJ | - OpCode::ITJ | OpCode::SKJ | OpCode::AMJ | OpCode::SMP | OpCode::PSp | OpCode::IpN | - OpCode::FFm => { + OpCode::FON + | OpCode::MOV + | OpCode::AMp + | OpCode::NCJ + | OpCode::ECJ + | OpCode::FLJ + | OpCode::ITJ + | OpCode::SKJ + | OpCode::AMJ + | OpCode::SMP + | OpCode::PSp + | OpCode::IpN + | OpCode::FFm => { let operand_a = TextScript::read_number(iter)?; - if strict { TextScript::expect_char(b':', iter)?; } else { iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; } + if strict { + TextScript::expect_char(b':', iter)?; + } else { + iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; + } let operand_b = TextScript::read_number(iter)?; TextScript::put_varint(instr as i32, out); @@ -1806,9 +1929,17 @@ impl TextScript { // Three operand codes OpCode::ANP | OpCode::CNP | OpCode::INP | OpCode::TAM | OpCode::CMP | OpCode::INJ => { let operand_a = TextScript::read_number(iter)?; - if strict { TextScript::expect_char(b':', iter)?; } else { iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; } + if strict { + TextScript::expect_char(b':', iter)?; + } else { + iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; + } let operand_b = TextScript::read_number(iter)?; - if strict { TextScript::expect_char(b':', iter)?; } else { iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; } + if strict { + TextScript::expect_char(b':', iter)?; + } else { + iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; + } let operand_c = TextScript::read_number(iter)?; TextScript::put_varint(instr as i32, out); @@ -1819,11 +1950,23 @@ impl TextScript { // Four operand codes OpCode::TRA | OpCode::MNP | OpCode::SNP => { let operand_a = TextScript::read_number(iter)?; - if strict { TextScript::expect_char(b':', iter)?; } else { iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; } + if strict { + TextScript::expect_char(b':', iter)?; + } else { + iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; + } let operand_b = TextScript::read_number(iter)?; - if strict { TextScript::expect_char(b':', iter)?; } else { iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; } + if strict { + TextScript::expect_char(b':', iter)?; + } else { + iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; + } let operand_c = TextScript::read_number(iter)?; - if strict { TextScript::expect_char(b':', iter)?; } else { iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; } + if strict { + TextScript::expect_char(b':', iter)?; + } else { + iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; + } let operand_d = TextScript::read_number(iter)?; TextScript::put_varint(instr as i32, out); @@ -1851,31 +1994,106 @@ impl TextScript { if let Some(op) = op_maybe { match op { // Zero operand codes - OpCode::AEp | OpCode::CAT | OpCode::CIL | OpCode::CLO | OpCode::CLR | OpCode::CPS | - OpCode::CRE | OpCode::CSS | OpCode::END | OpCode::ESC | OpCode::FLA | OpCode::FMU | - OpCode::FRE | OpCode::HMC | OpCode::INI | OpCode::KEY | OpCode::LDP | OpCode::MLP | - OpCode::MM0 | OpCode::MNA | OpCode::MS2 | OpCode::MS3 | OpCode::MSG | OpCode::NOD | - OpCode::PRI | OpCode::RMU | OpCode::SAT | OpCode::SLP | OpCode::SMC | OpCode::SPS | - OpCode::STC | OpCode::SVP | OpCode::TUR | OpCode::WAS | OpCode::ZAM | OpCode::HM2 | - OpCode::POP | OpCode::KE2 | OpCode::FR2 => { + OpCode::AEp + | OpCode::CAT + | OpCode::CIL + | OpCode::CLO + | OpCode::CLR + | OpCode::CPS + | OpCode::CRE + | OpCode::CSS + | OpCode::END + | OpCode::ESC + | OpCode::FLA + | OpCode::FMU + | OpCode::FRE + | OpCode::HMC + | OpCode::INI + | OpCode::KEY + | OpCode::LDP + | OpCode::MLP + | OpCode::MM0 + | OpCode::MNA + | OpCode::MS2 + | OpCode::MS3 + | OpCode::MSG + | OpCode::NOD + | OpCode::PRI + | OpCode::RMU + | OpCode::SAT + | OpCode::SLP + | OpCode::SMC + | OpCode::SPS + | OpCode::STC + | OpCode::SVP + | OpCode::TUR + | OpCode::WAS + | OpCode::ZAM + | OpCode::HM2 + | OpCode::POP + | OpCode::KE2 + | OpCode::FR2 => { result.push_str(format!("{:?}()\n", op).as_str()); } // One operand codes - OpCode::BOA | OpCode::BSL | OpCode::FOB | OpCode::FOM | OpCode::QUA | OpCode::UNI | - OpCode::MYB | OpCode::MYD | OpCode::FAI | OpCode::FAO | OpCode::WAI | OpCode::FAC | - OpCode::GIT | OpCode::NUM | OpCode::DNA | OpCode::DNP | OpCode::FLm | OpCode::FLp | - OpCode::MPp | OpCode::SKm | OpCode::SKp | OpCode::EQp | OpCode::EQm | OpCode::MLp | - OpCode::ITp | OpCode::ITm | OpCode::AMm | OpCode::UNJ | OpCode::MPJ | OpCode::YNJ | - OpCode::EVE | OpCode::XX1 | OpCode::SIL | OpCode::LIp | OpCode::SOU | OpCode::CMU | - OpCode::SSS | OpCode::ACH | OpCode::S2MV | OpCode::PSH => { + OpCode::BOA + | OpCode::BSL + | OpCode::FOB + | OpCode::FOM + | OpCode::QUA + | OpCode::UNI + | OpCode::MYB + | OpCode::MYD + | OpCode::FAI + | OpCode::FAO + | OpCode::WAI + | OpCode::FAC + | OpCode::GIT + | OpCode::NUM + | OpCode::DNA + | OpCode::DNP + | OpCode::FLm + | OpCode::FLp + | OpCode::MPp + | OpCode::SKm + | OpCode::SKp + | OpCode::EQp + | OpCode::EQm + | OpCode::MLp + | OpCode::ITp + | OpCode::ITm + | OpCode::AMm + | OpCode::UNJ + | OpCode::MPJ + | OpCode::YNJ + | OpCode::EVE + | OpCode::XX1 + | OpCode::SIL + | OpCode::LIp + | OpCode::SOU + | OpCode::CMU + | OpCode::SSS + | OpCode::ACH + | OpCode::S2MV + | OpCode::PSH => { let par_a = read_cur_varint(&mut cursor)?; result.push_str(format!("{:?}({})\n", op, par_a).as_str()); } // Two operand codes - OpCode::FON | OpCode::MOV | OpCode::AMp | OpCode::NCJ | OpCode::ECJ | OpCode::FLJ | - OpCode::ITJ | OpCode::SKJ | OpCode::AMJ | OpCode::SMP | OpCode::PSp | OpCode::IpN | - OpCode::FFm => { + OpCode::FON + | OpCode::MOV + | OpCode::AMp + | OpCode::NCJ + | OpCode::ECJ + | OpCode::FLJ + | OpCode::ITJ + | OpCode::SKJ + | OpCode::AMJ + | OpCode::SMP + | OpCode::PSp + | OpCode::IpN + | OpCode::FFm => { let par_a = read_cur_varint(&mut cursor)?; let par_b = read_cur_varint(&mut cursor)?; @@ -1939,7 +2157,7 @@ impl TextScript { } } - fn expect_char>(expect: u8, iter: &mut I) -> GameResult { + fn expect_char>(expect: u8, iter: &mut I) -> GameResult { let res = iter.next(); match res { @@ -1949,7 +2167,7 @@ impl TextScript { } } - fn skip_until>(expect: u8, iter: &mut Peekable) -> GameResult { + fn skip_until>(expect: u8, iter: &mut Peekable) -> GameResult { while let Some(&chr) = iter.peek() { if chr == expect { return Ok(()); @@ -1963,7 +2181,7 @@ impl TextScript { /// Reads a 4 digit TSC formatted number from iterator. /// Intentionally does no '0'..'9' range checking, since it was often exploited by modders. - fn read_number>(iter: &mut Peekable) -> GameResult { + fn read_number>(iter: &mut Peekable) -> GameResult { Some(0) .and_then(|result| iter.next().map(|v| result + 1000 * v.wrapping_sub(b'0') as i32)) .and_then(|result| iter.next().map(|v| result + 100 * v.wrapping_sub(b'0') as i32)) @@ -1972,7 +2190,6 @@ impl TextScript { .ok_or_else(|| ParseError(str!("Script unexpectedly ended."))) } - pub fn has_event(&self, id: u16) -> bool { self.event_map.contains_key(&id) }