mirror of
https://github.com/doukutsu-rs/doukutsu-rs
synced 2025-03-20 17:09:21 +00:00
ogg playback and persistent settings
This commit is contained in:
parent
c7522f9bf1
commit
1cc7ed8b2b
4
rustfmt.toml
Normal file
4
rustfmt.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
edition = "2018"
|
||||
max_width = 120
|
||||
use_small_heuristics = "Max"
|
||||
newline_style = "Unix"
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
|
29
src/scripting/doukutsu.d.ts
vendored
29
src/scripting/doukutsu.d.ts
vendored
|
@ -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>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
435
src/sound/mod.rs
435
src/sound/mod.rs
|
@ -2,28 +2,33 @@ use std::sync::mpsc;
|
|||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::time::Duration;
|
||||
|
||||
use cpal::Sample;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::Sample;
|
||||
use lewton::inside_ogg::OggStreamReader;
|
||||
use num_traits::clamp;
|
||||
|
||||
use crate::engine_constants::EngineConstants;
|
||||
use crate::framework::context::Context;
|
||||
use crate::framework::error::{GameResult};
|
||||
use crate::framework::error::GameError::{AudioError, InvalidValue};
|
||||
use crate::framework::error::{GameError, GameResult};
|
||||
use crate::framework::filesystem;
|
||||
use crate::framework::filesystem::File;
|
||||
use crate::sound::ogg_playback::{OggPlaybackEngine, SavedOggPlaybackState};
|
||||
use crate::sound::org_playback::{OrgPlaybackEngine, SavedOrganyaPlaybackState};
|
||||
use crate::sound::organya::Song;
|
||||
use crate::sound::pixtone::PixTonePlayback;
|
||||
use crate::sound::playback::{PlaybackEngine, SavedPlaybackState};
|
||||
use crate::sound::wave_bank::SoundBank;
|
||||
use crate::str;
|
||||
use crate::framework::error::GameError::{AudioError, ResourceLoadError, InvalidValue};
|
||||
use crate::settings::Settings;
|
||||
|
||||
mod wave_bank;
|
||||
mod ogg_playback;
|
||||
mod org_playback;
|
||||
mod organya;
|
||||
mod pixtone;
|
||||
mod pixtone_sfx;
|
||||
mod playback;
|
||||
mod stuff;
|
||||
mod wav;
|
||||
mod wave_bank;
|
||||
|
||||
pub struct SoundManager {
|
||||
tx: Sender<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
193
src/sound/ogg_playback.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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];
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue