ogg playback and persistent settings

This commit is contained in:
Alula 2021-02-12 11:05:28 +01:00
parent c7522f9bf1
commit 1cc7ed8b2b
No known key found for this signature in database
GPG Key ID: 3E00485503A1D8BA
16 changed files with 1725 additions and 551 deletions

4
rustfmt.toml Normal file
View File

@ -0,0 +1,4 @@
edition = "2018"
max_width = 120
use_small_heuristics = "Max"
newline_style = "Unix"

View File

@ -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;

View File

@ -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<u16>; 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<String, String>,
pub music_table: Vec<String>,
pub organya_paths: Vec<String>,
}
@ -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());
}
}

View File

@ -93,6 +93,13 @@ impl From<std::string::FromUtf8Error> for GameError {
}
}
impl From<serde_yaml::Error> for GameError {
fn from(e: serde_yaml::Error) -> Self {
let errstr = format!("Yaml error: {:?}", e);
GameError::ParseError(errstr)
}
}
#[cfg(target_os = "android")]
impl From<jni::errors::Error> for GameError {
fn from(e: jni::errors::Error) -> GameError {

View File

@ -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<dyn vfs::VFile>),
}
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<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
pub(crate) fn open<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
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<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
pub(crate) fn user_open<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
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<P: AsRef<path::Path>>(
&mut self,
path: P,
options: OpenOptions,
) -> GameResult<File> {
pub(crate) fn open_options<P: AsRef<path::Path>>(&self, path: P, options: OpenOptions) -> GameResult<File> {
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<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
pub(crate) fn user_create<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
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<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
pub(crate) fn user_create_dir<P: AsRef<path::Path>>(&self, path: P) -> GameResult<()> {
self.user_vfs.mkdir(path.as_ref())
}
/// Deletes the specified file in the user dir.
pub(crate) fn user_delete<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
pub(crate) fn user_delete<P: AsRef<path::Path>>(&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<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
pub(crate) fn user_delete_dir<P: AsRef<path::Path>>(&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<P: AsRef<path::Path>>(&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<P: AsRef<path::Path>>(&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<P: AsRef<path::Path>>(
&mut self,
&self,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
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<Box<dyn Iterator<Item = path::PathBuf>>> {
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<P: AsRef<path::Path>>(
&mut self,
&self,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
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<Box<dyn Iterator<Item = path::PathBuf>>> {
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<dyn vfs::VFS>) {
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<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
pub fn open<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<File> {
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<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
pub fn user_open<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<File> {
ctx.filesystem.user_open(path)
}
/// Opens a file in the user directory with the given `filesystem::OpenOptions`.
pub fn open_options<P: AsRef<path::Path>>(
ctx: &mut Context,
path: P,
options: OpenOptions,
) -> GameResult<File> {
pub fn open_options<P: AsRef<path::Path>>(ctx: &Context, path: P, options: OpenOptions) -> GameResult<File> {
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<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
pub fn user_create<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<File> {
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<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
pub fn user_create_dir<P: AsRef<path::Path>>(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<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
pub fn user_delete<P: AsRef<path::Path>>(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<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
pub fn user_delete_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult {
ctx.filesystem.user_delete_dir(path.as_ref())
}
@ -313,9 +298,9 @@ pub fn user_is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
///
/// Lists the base directory if an empty path is given.
pub fn user_read_dir<P: AsRef<path::Path>>(
ctx: &mut Context,
ctx: &Context,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
ctx.filesystem.user_read_dir(path)
}
@ -338,10 +323,7 @@ pub fn is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
/// in no particular order.
///
/// Lists the base directory if an empty path is given.
pub fn read_dir<P: AsRef<path::Path>>(
ctx: &mut Context,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
pub fn read_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
ctx.filesystem.read_dir(path)
}

View File

@ -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<T> VFile for T where T: Read + Write + Seek + Debug {}
impl<T> 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<Box<dyn VMetadata>>;
/// Retrieve all file and directory entries in the given directory.
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item=GameResult<PathBuf>>>>;
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item = GameResult<PathBuf>>>>;
/// Retrieve the actual location of the VFS root, if available.
fn to_path_buf(&self) -> Option<PathBuf>;
@ -268,9 +268,9 @@ impl VFS for PhysicalFS {
fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult<Box<dyn VFile>> {
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<Box<dyn Iterator<Item=GameResult<PathBuf>>>> {
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item = GameResult<PathBuf>>>> {
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<Box<dyn Iterator<Item=GameResult<PathBuf>>>> {
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item = GameResult<PathBuf>>>> {
// 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();

View File

@ -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()));

View File

@ -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;

View File

@ -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)?;
}
_ => {}
}

View File

@ -1,11 +1,34 @@
declare type EventHandler<T> = (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<DoukutsuScene>): EventHandler<DoukutsuScene>;
function on<T>(event: string, handler: EventHandler<T>): EventHandler<T>;
};
}

View File

@ -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

View File

@ -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<Settings> {
pub fn load(ctx: &Context) -> GameResult<Settings> {
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<dyn PlayerController> {
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,
}

View File

@ -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<PlaybackMessage>,
@ -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<SoundManager> {
let (tx, rx): (Sender<PlaybackMessage>, Receiver<PlaybackMessage>) = 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<Song>),
PlayOrganyaSong(Box<Song>),
PlayOggSongSinglePart(Box<OggStreamReader<File>>),
PlayOggSongMultiPart(Box<OggStreamReader<File>>, Box<OggStreamReader<File>>),
PlaySample(u8),
SetSpeed(f32),
SaveState,
@ -185,32 +275,46 @@ enum PlaybackMessage {
#[derive(PartialEq, Eq)]
enum PlaybackState {
Stopped,
Playing,
PlayingOrg,
PlayingOgg,
}
fn run<T>(rx: Receiver<PlaybackMessage>, bank: SoundBank,
device: &cpal::Device, config: &cpal::StreamConfig) -> GameResult where
enum PlaybackStateType {
None,
Organya(SavedOrganyaPlaybackState),
Ogg(SavedOggPlaybackState),
}
fn run<T>(
rx: Receiver<PlaybackMessage>,
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<SavedPlaybackState> = 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<T>(rx: Receiver<PlaybackMessage>, 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<T>(rx: Receiver<PlaybackMessage>, 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::<u16>(&sample_l);
frame[1] = Sample::from::<u16>(&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::<u16>(&sample);
}

193
src/sound/ogg_playback.rs Normal file
View File

@ -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<Arc<RwLock<Box<OggStreamReader<File>>>>>,
loop_music: Option<Arc<RwLock<Box<OggStreamReader<File>>>>>,
output_format: WavFormat,
playing_intro: bool,
position: u64,
buffer: Vec<i16>,
}
pub struct SavedOggPlaybackState {
intro_music: Option<Arc<RwLock<Box<OggStreamReader<File>>>>>,
loop_music: Option<Arc<RwLock<Box<OggStreamReader<File>>>>>,
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<OggStreamReader<File>>) {
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<OggStreamReader<File>>,
loop_music: Box<OggStreamReader<File>>,
) {
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<i16>, sample_rate: u32, channels: u8) -> Vec<i16> {
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()
}
}

View File

@ -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<RenderBuffer>; 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<RenderBuffer>; 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<T: Ord>(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];

View File

@ -289,7 +289,6 @@ pub enum OpCode {
KE2,
/// <FRE related to player 2?
FR2,
// ---- Custom opcodes, for use by modders ----
}
@ -469,12 +468,16 @@ impl TextScriptVM {
pub fn set_global_script(&mut self, script: TextScript) {
self.scripts.global_script = script;
if !self.suspend { self.reset(); }
if !self.suspend {
self.reset();
}
}
pub fn set_scene_script(&mut self, script: TextScript) {
self.scripts.scene_script = script;
if !self.suspend { self.reset(); }
if !self.suspend {
self.reset();
}
}
pub fn set_inventory_script(&mut self, script: TextScript) {
@ -514,7 +517,9 @@ impl TextScriptVM {
pub fn run(state: &mut SharedGameState, game_scene: &mut GameScene, ctx: &mut Context) -> 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<TextScriptExecutionState> {
pub fn execute(
event: u16,
ip: u32,
state: &mut SharedGameState,
game_scene: &mut GameScene,
ctx: &mut Context,
) -> GameResult<TextScriptExecutionState> {
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<OpCode> = FromPrimitive::from_i32(read_cur_varint(&mut cursor)
.unwrap_or_else(|_| OpCode::END as i32));
let op_maybe: Option<OpCode> =
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<I: Iterator<Item=u8>>(iter: &mut Peekable<I>, strict: bool, encoding: TextScriptEncoding) -> GameResult<Vec<u8>> {
fn compile_event<I: Iterator<Item = u8>>(
iter: &mut Peekable<I>,
strict: bool,
encoding: TextScriptEncoding,
) -> GameResult<Vec<u8>> {
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<I: Iterator<Item=u8>>(iter: &mut I) -> GameResult<i32> {
fn read_varint<I: Iterator<Item = u8>>(iter: &mut I) -> GameResult<i32> {
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<I: Iterator<Item=u8>>(code: &str, strict: bool, iter: &mut Peekable<I>, out: &mut Vec<u8>) -> GameResult {
fn compile_code<I: Iterator<Item = u8>>(
code: &str,
strict: bool,
iter: &mut Peekable<I>,
out: &mut Vec<u8>,
) -> 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<I: Iterator<Item=u8>>(expect: u8, iter: &mut I) -> GameResult {
fn expect_char<I: Iterator<Item = u8>>(expect: u8, iter: &mut I) -> GameResult {
let res = iter.next();
match res {
@ -1949,7 +2167,7 @@ impl TextScript {
}
}
fn skip_until<I: Iterator<Item=u8>>(expect: u8, iter: &mut Peekable<I>) -> GameResult {
fn skip_until<I: Iterator<Item = u8>>(expect: u8, iter: &mut Peekable<I>) -> 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<I: Iterator<Item=u8>>(iter: &mut Peekable<I>) -> GameResult<i32> {
fn read_number<I: Iterator<Item = u8>>(iter: &mut Peekable<I>) -> GameResult<i32> {
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)
}