mirror of
https://github.com/doukutsu-rs/doukutsu-rs
synced 2025-04-16 18:54:28 +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 byteorder::{LE, ReadBytesExt};
|
||||||
|
|
||||||
use crate::framework::context::Context;
|
|
||||||
use crate::framework::error::GameError::ResourceLoadError;
|
use crate::framework::error::GameError::ResourceLoadError;
|
||||||
use crate::framework::error::GameResult;
|
use crate::framework::error::GameResult;
|
||||||
use crate::str;
|
use crate::str;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use case_insensitive_hashmap::CaseInsensitiveHashMap;
|
use case_insensitive_hashmap::CaseInsensitiveHashMap;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
|
@ -92,7 +94,6 @@ impl Clone for CaretConsts {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct BulletData {
|
pub struct BulletData {
|
||||||
pub damage: u8,
|
pub damage: u8,
|
||||||
|
@ -171,7 +172,6 @@ pub struct TextScriptConsts {
|
||||||
pub cursor: [Rect<u16>; 2],
|
pub cursor: [Rect<u16>; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TitleConsts {
|
pub struct TitleConsts {
|
||||||
pub intro_text: String,
|
pub intro_text: String,
|
||||||
|
@ -222,6 +222,8 @@ pub struct EngineConstants {
|
||||||
pub font_path: String,
|
pub font_path: String,
|
||||||
pub font_scale: f32,
|
pub font_scale: f32,
|
||||||
pub font_space_offset: f32,
|
pub font_space_offset: f32,
|
||||||
|
pub soundtracks: HashMap<String, String>,
|
||||||
|
pub music_table: Vec<String>,
|
||||||
pub organya_paths: Vec<String>,
|
pub organya_paths: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,6 +245,8 @@ impl Clone for EngineConstants {
|
||||||
font_path: self.font_path.clone(),
|
font_path: self.font_path.clone(),
|
||||||
font_scale: self.font_scale,
|
font_scale: self.font_scale,
|
||||||
font_space_offset: self.font_space_offset,
|
font_space_offset: self.font_space_offset,
|
||||||
|
soundtracks: self.soundtracks.clone(),
|
||||||
|
music_table: self.music_table.clone(),
|
||||||
organya_paths: self.organya_paths.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_left_rect: Rect { left: 0, top: 80, right: 16, bottom: 96 },
|
||||||
question_right_rect: Rect { left: 48, top: 64, right: 64, bottom: 80 },
|
question_right_rect: Rect { left: 48, top: 64, right: 64, bottom: 80 },
|
||||||
},
|
},
|
||||||
world: WorldConsts {
|
world: WorldConsts { snack_rect: Rect { left: 256, top: 48, right: 272, bottom: 64 } },
|
||||||
snack_rect: Rect { left: 256, top: 48, right: 272, bottom: 64 },
|
|
||||||
},
|
|
||||||
npc: serde_yaml::from_str("dummy: \"lol\"").unwrap(),
|
npc: serde_yaml::from_str("dummy: \"lol\"").unwrap(),
|
||||||
weapon: WeaponConsts {
|
weapon: WeaponConsts {
|
||||||
bullet_table: vec![
|
bullet_table: vec![
|
||||||
// Null
|
// 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
|
// 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 {
|
||||||
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 } },
|
damage: 4,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 1,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 2,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 2,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 0,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 1,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 1,
|
||||||
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 } },
|
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
|
// 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
|
// 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
|
// 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
|
// 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 {
|
||||||
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 } },
|
damage: 15,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 0,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 2,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 4,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 4,
|
||||||
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 } },
|
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
|
// 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 {
|
||||||
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 } },
|
damage: 3,
|
||||||
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 } },
|
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
|
// 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?
|
// 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
|
// 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 {
|
bullet_rects: BulletRects {
|
||||||
b001_snake_l1: [
|
b001_snake_l1: [
|
||||||
|
@ -822,10 +1284,56 @@ impl EngineConstants {
|
||||||
font_path: "builtin/builtin_font.fnt".to_string(),
|
font_path: "builtin/builtin_font.fnt".to_string(),
|
||||||
font_scale: 1.0,
|
font_scale: 1.0,
|
||||||
font_space_offset: 0.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![
|
organya_paths: vec![
|
||||||
str!("/org/"), // NXEngine
|
"/org/".to_string(), // NXEngine
|
||||||
str!("/base/Org/"), // CS+
|
"/base/Org/".to_string(), // CS+
|
||||||
str!("/Resource/ORG/"), // CSE2E
|
"/Resource/ORG/".to_string(), // CSE2E
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -842,9 +1350,10 @@ impl EngineConstants {
|
||||||
self.font_path = str!("csfont.fnt");
|
self.font_path = str!("csfont.fnt");
|
||||||
self.font_scale = 0.5;
|
self.font_scale = 0.5;
|
||||||
self.font_space_offset = 2.0;
|
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) {
|
pub fn apply_csplus_nx_patches(&mut self) {
|
||||||
info!("Applying Switch-specific Cave Story+ constants patches...");
|
info!("Applying Switch-specific Cave Story+ constants patches...");
|
||||||
|
|
||||||
|
@ -856,5 +1365,7 @@ impl EngineConstants {
|
||||||
self.textscript.encoding = TextScriptEncoding::UTF8;
|
self.textscript.encoding = TextScriptEncoding::UTF8;
|
||||||
self.textscript.encrypted = false;
|
self.textscript.encrypted = false;
|
||||||
self.textscript.animated_face_pics = true;
|
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")]
|
#[cfg(target_os = "android")]
|
||||||
impl From<jni::errors::Error> for GameError {
|
impl From<jni::errors::Error> for GameError {
|
||||||
fn from(e: jni::errors::Error) -> GameError {
|
fn from(e: jni::errors::Error) -> GameError {
|
||||||
|
|
|
@ -6,10 +6,11 @@ use std::path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use directories::ProjectDirs;
|
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::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.
|
/// A structure that contains the filesystem state and cache.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -25,6 +26,8 @@ pub enum File {
|
||||||
VfsFile(Box<dyn vfs::VFile>),
|
VfsFile(Box<dyn vfs::VFile>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for File {}
|
||||||
|
|
||||||
impl fmt::Debug for File {
|
impl fmt::Debug for File {
|
||||||
// Make this more useful?
|
// Make this more useful?
|
||||||
// But we can't seem to get a filename out of a file,
|
// 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`
|
/// Opens the given `path` and returns the resulting `File`
|
||||||
/// in read-only mode.
|
/// 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))
|
self.vfs.open(path.as_ref()).map(|f| File::VfsFile(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the given `path` from user directory and returns the resulting `File`
|
/// Opens the given `path` from user directory and returns the resulting `File`
|
||||||
/// in read-only mode.
|
/// 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))
|
self.user_vfs.open(path.as_ref()).map(|f| File::VfsFile(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,44 +98,36 @@ impl Filesystem {
|
||||||
/// [`filesystem::OpenOptions`](struct.OpenOptions.html).
|
/// [`filesystem::OpenOptions`](struct.OpenOptions.html).
|
||||||
/// Note that even if you open a file read-write, it can only
|
/// Note that even if you open a file read-write, it can only
|
||||||
/// write to files in the "user" directory.
|
/// write to files in the "user" directory.
|
||||||
pub(crate) fn open_options<P: AsRef<path::Path>>(
|
pub(crate) fn open_options<P: AsRef<path::Path>>(&self, path: P, options: OpenOptions) -> GameResult<File> {
|
||||||
&mut self,
|
|
||||||
path: P,
|
|
||||||
options: OpenOptions,
|
|
||||||
) -> GameResult<File> {
|
|
||||||
self.user_vfs
|
self.user_vfs
|
||||||
.open_options(path.as_ref(), options)
|
.open_options(path.as_ref(), options)
|
||||||
.map(|f| File::VfsFile(f))
|
.map(|f| File::VfsFile(f))
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
GameError::ResourceLoadError(format!(
|
GameError::ResourceLoadError(format!("Tried to open {:?} but got error: {:?}", path.as_ref(), e))
|
||||||
"Tried to open {:?} but got error: {:?}",
|
|
||||||
path.as_ref(),
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new file in the user directory and opens it
|
/// Creates a new file in the user directory and opens it
|
||||||
/// to be written to, truncating it if it already exists.
|
/// 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))
|
self.user_vfs.create(path.as_ref()).map(|f| File::VfsFile(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an empty directory in the user dir
|
/// Create an empty directory in the user dir
|
||||||
/// with the given name. Any parents to that directory
|
/// with the given name. Any parents to that directory
|
||||||
/// that do not exist will be created.
|
/// 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())
|
self.user_vfs.mkdir(path.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the specified file in the user dir.
|
/// 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())
|
self.user_vfs.rm(path.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the specified directory in the user dir,
|
/// Deletes the specified directory in the user dir,
|
||||||
/// and all its contents!
|
/// 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())
|
self.user_vfs.rmrf(path.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,10 +151,7 @@ impl Filesystem {
|
||||||
|
|
||||||
/// Check whether a path points at a file.
|
/// Check whether a path points at a file.
|
||||||
pub(crate) fn is_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
pub(crate) fn is_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
||||||
self.vfs
|
self.vfs.metadata(path.as_ref()).map(|m| m.is_file()).unwrap_or(false)
|
||||||
.metadata(path.as_ref())
|
|
||||||
.map(|m| m.is_file())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether a path points at a directory.
|
/// Check whether a path points at a directory.
|
||||||
|
@ -172,10 +164,7 @@ impl Filesystem {
|
||||||
|
|
||||||
/// Check whether a path points at a directory.
|
/// Check whether a path points at a directory.
|
||||||
pub(crate) fn is_dir<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
pub(crate) fn is_dir<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
||||||
self.vfs
|
self.vfs.metadata(path.as_ref()).map(|m| m.is_dir()).unwrap_or(false)
|
||||||
.metadata(path.as_ref())
|
|
||||||
.map(|m| m.is_dir())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a list of all files and directories in the user directory,
|
/// 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.
|
/// Lists the base directory if an empty path is given.
|
||||||
pub(crate) fn user_read_dir<P: AsRef<path::Path>>(
|
pub(crate) fn user_read_dir<P: AsRef<path::Path>>(
|
||||||
&mut self,
|
&self,
|
||||||
path: P,
|
path: P,
|
||||||
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
|
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
||||||
let itr = self.user_vfs.read_dir(path.as_ref())?.map(|fname| {
|
let itr = self
|
||||||
fname.expect("Could not read file in read_dir()? Should never happen, I hope!")
|
.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))
|
Ok(Box::new(itr))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,16 +187,17 @@ impl Filesystem {
|
||||||
///
|
///
|
||||||
/// Lists the base directory if an empty path is given.
|
/// Lists the base directory if an empty path is given.
|
||||||
pub(crate) fn read_dir<P: AsRef<path::Path>>(
|
pub(crate) fn read_dir<P: AsRef<path::Path>>(
|
||||||
&mut self,
|
&self,
|
||||||
path: P,
|
path: P,
|
||||||
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
|
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
||||||
let itr = self.vfs.read_dir(path.as_ref())?.map(|fname| {
|
let itr = self
|
||||||
fname.expect("Could not read file in read_dir()? Should never happen, I hope!")
|
.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))
|
Ok(Box::new(itr))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_to_string(&mut self) -> String {
|
fn write_to_string(&self) -> String {
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
for vfs in self.vfs.roots() {
|
for vfs in self.vfs.roots() {
|
||||||
|
@ -214,8 +205,7 @@ impl Filesystem {
|
||||||
match vfs.read_dir(path::Path::new("/")) {
|
match vfs.read_dir(path::Path::new("/")) {
|
||||||
Ok(files) => {
|
Ok(files) => {
|
||||||
for itm in files {
|
for itm in files {
|
||||||
write!(s, " {:?}", itm)
|
write!(s, " {:?}", itm).expect("Could not write to string; should never happen?");
|
||||||
.expect("Could not write to string; should never happen?");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => write!(s, " Could not read source: {:?}", e)
|
Err(e) => write!(s, " Could not read source: {:?}", e)
|
||||||
|
@ -242,7 +232,6 @@ impl Filesystem {
|
||||||
self.vfs.push_back(vfs);
|
self.vfs.push_back(vfs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn mount_user_vfs(&mut self, vfs: Box<dyn vfs::VFS>) {
|
pub fn mount_user_vfs(&mut self, vfs: Box<dyn vfs::VFS>) {
|
||||||
self.user_vfs.push_back(vfs);
|
self.user_vfs.push_back(vfs);
|
||||||
}
|
}
|
||||||
|
@ -250,46 +239,42 @@ impl Filesystem {
|
||||||
|
|
||||||
/// Opens the given path and returns the resulting `File`
|
/// Opens the given path and returns the resulting `File`
|
||||||
/// in read-only mode.
|
/// 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)
|
ctx.filesystem.open(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the given path in the user directory and returns the resulting `File`
|
/// Opens the given path in the user directory and returns the resulting `File`
|
||||||
/// in read-only mode.
|
/// 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)
|
ctx.filesystem.user_open(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a file in the user directory with the given `filesystem::OpenOptions`.
|
/// Opens a file in the user directory with the given `filesystem::OpenOptions`.
|
||||||
pub fn open_options<P: AsRef<path::Path>>(
|
pub fn open_options<P: AsRef<path::Path>>(ctx: &Context, path: P, options: OpenOptions) -> GameResult<File> {
|
||||||
ctx: &mut Context,
|
|
||||||
path: P,
|
|
||||||
options: OpenOptions,
|
|
||||||
) -> GameResult<File> {
|
|
||||||
ctx.filesystem.open_options(path, options)
|
ctx.filesystem.open_options(path, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new file in the user directory and opens it
|
/// Creates a new file in the user directory and opens it
|
||||||
/// to be written to, truncating it if it already exists.
|
/// 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)
|
ctx.filesystem.user_create(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an empty directory in the user dir
|
/// Create an empty directory in the user dir
|
||||||
/// with the given name. Any parents to that directory
|
/// with the given name. Any parents to that directory
|
||||||
/// that do not exist will be created.
|
/// 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())
|
ctx.filesystem.user_create_dir(path.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the specified file in the user dir.
|
/// 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())
|
ctx.filesystem.user_delete(path.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the specified directory in the user dir,
|
/// Deletes the specified directory in the user dir,
|
||||||
/// and all its contents!
|
/// 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())
|
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.
|
/// Lists the base directory if an empty path is given.
|
||||||
pub fn user_read_dir<P: AsRef<path::Path>>(
|
pub fn user_read_dir<P: AsRef<path::Path>>(
|
||||||
ctx: &mut Context,
|
ctx: &Context,
|
||||||
path: P,
|
path: P,
|
||||||
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
|
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
||||||
ctx.filesystem.user_read_dir(path)
|
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.
|
/// in no particular order.
|
||||||
///
|
///
|
||||||
/// Lists the base directory if an empty path is given.
|
/// Lists the base directory if an empty path is given.
|
||||||
pub fn read_dir<P: AsRef<path::Path>>(
|
pub fn read_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
||||||
ctx: &mut Context,
|
|
||||||
path: P,
|
|
||||||
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
|
|
||||||
ctx.filesystem.read_dir(path)
|
ctx.filesystem.read_dir(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,13 @@
|
||||||
//! as a trait object, and its path abstraction is not the most
|
//! as a trait object, and its path abstraction is not the most
|
||||||
//! convenient.
|
//! convenient.
|
||||||
|
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::fmt::{self, Debug};
|
use std::fmt::{self, Debug};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{Read, Seek, Write, BufRead};
|
use std::io::{BufRead, Read, Seek, Write};
|
||||||
use std::path::{self, Path, PathBuf};
|
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> {
|
fn convenient_path_to_str(path: &path::Path) -> GameResult<&str> {
|
||||||
path.to_str().ok_or_else(|| {
|
path.to_str().ok_or_else(|| {
|
||||||
|
@ -25,9 +25,9 @@ fn convenient_path_to_str(path: &path::Path) -> GameResult<&str> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Virtual file
|
/// 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
|
/// Options for opening files
|
||||||
///
|
///
|
||||||
|
@ -132,7 +132,7 @@ pub trait VFS: Debug {
|
||||||
fn metadata(&self, path: &Path) -> GameResult<Box<dyn VMetadata>>;
|
fn metadata(&self, path: &Path) -> GameResult<Box<dyn VMetadata>>;
|
||||||
|
|
||||||
/// Retrieve all file and directory entries in the given directory.
|
/// 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.
|
/// Retrieve the actual location of the VFS root, if available.
|
||||||
fn to_path_buf(&self) -> Option<PathBuf>;
|
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>> {
|
fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult<Box<dyn VFile>> {
|
||||||
if self.readonly
|
if self.readonly
|
||||||
&& (open_options.write
|
&& (open_options.write
|
||||||
|| open_options.create
|
|| open_options.create
|
||||||
|| open_options.append
|
|| open_options.append
|
||||||
|| open_options.truncate)
|
|| open_options.truncate)
|
||||||
{
|
{
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"Cannot alter file {:?} in root {:?}, filesystem read-only",
|
"Cannot alter file {:?} in root {:?}, filesystem read-only",
|
||||||
|
@ -359,7 +359,7 @@ impl VFS for PhysicalFS {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the path entries in this path
|
/// 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()?;
|
self.create_root()?;
|
||||||
let p = self.to_absolute(path)?;
|
let p = self.to_absolute(path)?;
|
||||||
// This is inconvenient because path() returns the full absolute
|
// This is inconvenient because path() returns the full absolute
|
||||||
|
@ -513,7 +513,7 @@ impl VFS for OverlayFS {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the path entries in this path
|
/// 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...
|
// This is tricky 'cause we have to actually merge iterators together...
|
||||||
// Doing it the simple and stupid way works though.
|
// Doing it the simple and stupid way works though.
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
|
|
|
@ -7,7 +7,6 @@ extern crate strum;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate strum_macros;
|
extern crate strum_macros;
|
||||||
|
|
||||||
use core::mem;
|
|
||||||
use std::cell::UnsafeCell;
|
use std::cell::UnsafeCell;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -337,11 +336,9 @@ pub fn init() -> GameResult {
|
||||||
let state_ref = unsafe { &mut *game.state.get() };
|
let state_ref = unsafe { &mut *game.state.get() };
|
||||||
#[cfg(feature = "scripting")]
|
#[cfg(feature = "scripting")]
|
||||||
{
|
{
|
||||||
unsafe {
|
state_ref
|
||||||
state_ref
|
.lua
|
||||||
.lua
|
.update_refs(game.state.get(), &mut context as *mut Context);
|
||||||
.update_refs(game.state.get(), &mut context as *mut Context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state_ref.next_scene = Some(Box::new(LoadingScene::new()));
|
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_tick_world(true);
|
||||||
state.control_flags.set_control_enabled(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_weapon = self.current_weapon as u16;
|
||||||
game_scene.inventory_player1.current_item = self.current_item 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::context::Context;
|
||||||
use crate::framework::error::GameResult;
|
use crate::framework::error::GameResult;
|
||||||
use crate::framework::graphics;
|
use crate::framework::graphics;
|
||||||
|
@ -44,27 +44,22 @@ impl TitleScene {
|
||||||
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "bkMoon")?;
|
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "bkMoon")?;
|
||||||
let offset = (self.tick % 640) as isize;
|
let offset = (self.tick % 640) as isize;
|
||||||
|
|
||||||
batch.add_rect(((state.canvas_size.0 - 320.0) / 2.0).floor(), 0.0,
|
batch.add_rect(((state.canvas_size.0 - 320.0) / 2.0).floor(), 0.0, &Rect::new_size(0, 0, 320, 88));
|
||||||
&Rect::new_size(0, 0, 320, 88));
|
|
||||||
|
|
||||||
for x in ((-offset / 2)..(state.canvas_size.0 as isize)).step_by(320) {
|
for x in ((-offset / 2)..(state.canvas_size.0 as isize)).step_by(320) {
|
||||||
batch.add_rect(x as f32, 88.0,
|
batch.add_rect(x as f32, 88.0, &Rect::new_size(0, 88, 320, 35));
|
||||||
&Rect::new_size(0, 88, 320, 35));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for x in ((-offset % 320)..(state.canvas_size.0 as isize)).step_by(320) {
|
for x in ((-offset % 320)..(state.canvas_size.0 as isize)).step_by(320) {
|
||||||
batch.add_rect(x as f32, 123.0,
|
batch.add_rect(x as f32, 123.0, &Rect::new_size(0, 123, 320, 23));
|
||||||
&Rect::new_size(0, 123, 320, 23));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for x in ((-offset * 2)..(state.canvas_size.0 as isize)).step_by(320) {
|
for x in ((-offset * 2)..(state.canvas_size.0 as isize)).step_by(320) {
|
||||||
batch.add_rect(x as f32, 146.0,
|
batch.add_rect(x as f32, 146.0, &Rect::new_size(0, 146, 320, 30));
|
||||||
&Rect::new_size(0, 146, 320, 30));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for x in ((-offset * 4)..(state.canvas_size.0 as isize)).step_by(320) {
|
for x in ((-offset * 4)..(state.canvas_size.0 as isize)).step_by(320) {
|
||||||
batch.add_rect(x as f32, 176.0,
|
batch.add_rect(x as f32, 176.0, &Rect::new_size(0, 176, 320, 64));
|
||||||
&Rect::new_size(0, 176, 320, 64));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
batch.draw(ctx)?;
|
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 {
|
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);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -93,7 +95,7 @@ impl Scene for TitleScene {
|
||||||
self.controller.add(state.settings.create_player1_controller());
|
self.controller.add(state.settings.create_player1_controller());
|
||||||
self.controller.add(state.settings.create_player2_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("New game".to_string()));
|
||||||
self.main_menu.push_entry(MenuEntry::Active("Load game".to_string()));
|
self.main_menu.push_entry(MenuEntry::Active("Load game".to_string()));
|
||||||
self.main_menu.push_entry(MenuEntry::Active("Options".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.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));
|
self.option_menu.push_entry(MenuEntry::Toggle("Lighting effects".to_string(), state.settings.shader_effects));
|
||||||
if state.constants.supports_og_textures {
|
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 {
|
} else {
|
||||||
self.option_menu.push_entry(MenuEntry::Disabled("Original textures".to_string()));
|
self.option_menu.push_entry(MenuEntry::Disabled("Original textures".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.constants.is_cs_plus {
|
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 {
|
} else {
|
||||||
self.option_menu.push_entry(MenuEntry::Disabled("Seasonal textures".to_string()));
|
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;
|
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 {
|
match self.current_menu {
|
||||||
CurrentMenu::MainMenu => {
|
CurrentMenu::MainMenu => match self.main_menu.tick(&mut self.controller, state) {
|
||||||
match self.main_menu.tick(&mut self.controller, state) {
|
MenuSelectionResult::Selected(0, _) => {
|
||||||
MenuSelectionResult::Selected(0, _) => {
|
state.reset();
|
||||||
state.reset();
|
state.sound_manager.play_song(0, &state.constants, &state.settings, ctx)?;
|
||||||
state.sound_manager.play_song(0, &state.constants, ctx)?;
|
self.tick = 1;
|
||||||
self.tick = 1;
|
self.current_menu = CurrentMenu::StartGame;
|
||||||
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();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
MenuSelectionResult::Selected(1, _) => {
|
||||||
CurrentMenu::OptionMenu => {
|
state.sound_manager.play_song(0, &state.constants, &state.settings, ctx)?;
|
||||||
match self.option_menu.tick(&mut self.controller, state) {
|
self.tick = 1;
|
||||||
MenuSelectionResult::Selected(0, toggle) => {
|
self.current_menu = CurrentMenu::LoadGame;
|
||||||
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(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 => {
|
CurrentMenu::StartGame => {
|
||||||
if self.tick == 10 {
|
if self.tick == 10 {
|
||||||
state.start_new_game(ctx)?;
|
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")?;
|
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(),
|
batch.add_rect(
|
||||||
40.0,
|
((state.canvas_size.0 - state.constants.title.logo_rect.width() as f32) / 2.0).floor(),
|
||||||
&state.constants.title.logo_rect);
|
40.0,
|
||||||
|
&state.constants.title.logo_rect,
|
||||||
|
);
|
||||||
|
|
||||||
batch.draw(ctx)?;
|
batch.draw(ctx)?;
|
||||||
}
|
}
|
||||||
|
@ -262,8 +271,12 @@ impl Scene for TitleScene {
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.current_menu {
|
match self.current_menu {
|
||||||
CurrentMenu::MainMenu => { self.main_menu.draw(state, ctx)?; }
|
CurrentMenu::MainMenu => {
|
||||||
CurrentMenu::OptionMenu => { self.option_menu.draw(state, ctx)?; }
|
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;
|
declare type EventHandler<T> = (this: void, param: T) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a
|
||||||
|
*/
|
||||||
declare interface DoukutsuPlayer {
|
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;
|
x(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current position of player in Y axis (as floating point, not internal fixed point representation).
|
||||||
|
*/
|
||||||
y(): number;
|
y(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current velocity of player in X axis (as floating point, not internal fixed point representation).
|
||||||
|
*/
|
||||||
velX(): number;
|
velX(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current velocity of player in Y axis (as floating point, not internal fixed point representation).
|
||||||
|
*/
|
||||||
velY(): number;
|
velY(): number;
|
||||||
};
|
}
|
||||||
|
|
||||||
declare interface DoukutsuScene {
|
declare interface DoukutsuScene {
|
||||||
/**
|
/**
|
||||||
|
@ -32,7 +55,7 @@ declare interface DoukutsuScene {
|
||||||
* Returns player with specified id.
|
* Returns player with specified id.
|
||||||
*/
|
*/
|
||||||
player(id: number): DoukutsuPlayer | null;
|
player(id: number): DoukutsuPlayer | null;
|
||||||
};
|
}
|
||||||
|
|
||||||
declare namespace doukutsu {
|
declare namespace doukutsu {
|
||||||
/**
|
/**
|
||||||
|
@ -49,4 +72,4 @@ declare namespace doukutsu {
|
||||||
function on(event: "tick", handler: EventHandler<DoukutsuScene>): EventHandler<DoukutsuScene>;
|
function on(event: "tick", handler: EventHandler<DoukutsuScene>): EventHandler<DoukutsuScene>;
|
||||||
|
|
||||||
function on<T>(event: string, handler: EventHandler<T>): EventHandler<T>;
|
function on<T>(event: string, handler: EventHandler<T>): EventHandler<T>;
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use lua_ffi::ffi::luaL_Reg;
|
use lua_ffi::ffi::luaL_Reg;
|
||||||
use lua_ffi::{LuaObject, State, c_int};
|
use lua_ffi::{c_int, LuaObject, State};
|
||||||
|
|
||||||
use crate::scripting::LuaScriptingState;
|
use crate::scripting::LuaScriptingState;
|
||||||
|
|
||||||
|
@ -10,9 +10,7 @@ pub struct Doukutsu {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
impl Doukutsu {
|
impl Doukutsu {
|
||||||
pub fn new(ptr: *mut LuaScriptingState) -> Doukutsu {
|
pub fn new(ptr: *mut LuaScriptingState) -> Doukutsu {
|
||||||
Doukutsu {
|
Doukutsu { ptr }
|
||||||
ptr,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn lua_play_sfx(&self, state: &mut State) -> c_int {
|
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 game_state = &mut (*(*self.ptr).state_ptr);
|
||||||
let ctx = &mut (*(*self.ptr).ctx_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
|
0
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml::Error;
|
||||||
|
|
||||||
use crate::framework::context::Context;
|
use crate::framework::context::Context;
|
||||||
use crate::framework::error::GameResult;
|
use crate::framework::error::GameResult;
|
||||||
|
use crate::framework::filesystem::{user_create, user_open};
|
||||||
use crate::framework::keyboard::ScanCode;
|
use crate::framework::keyboard::ScanCode;
|
||||||
use crate::input::keyboard_player_controller::KeyboardController;
|
use crate::input::keyboard_player_controller::KeyboardController;
|
||||||
use crate::input::player_controller::PlayerController;
|
use crate::input::player_controller::PlayerController;
|
||||||
|
@ -16,9 +18,12 @@ pub struct Settings {
|
||||||
pub subpixel_coords: bool,
|
pub subpixel_coords: bool,
|
||||||
pub motion_interpolation: bool,
|
pub motion_interpolation: bool,
|
||||||
pub touch_controls: bool,
|
pub touch_controls: bool,
|
||||||
|
pub soundtrack: String,
|
||||||
|
#[serde(default = "p1_default_keymap")]
|
||||||
pub player1_key_map: PlayerKeyMap,
|
pub player1_key_map: PlayerKeyMap,
|
||||||
|
#[serde(default = "p2_default_keymap")]
|
||||||
pub player2_key_map: PlayerKeyMap,
|
pub player2_key_map: PlayerKeyMap,
|
||||||
#[serde(skip)]
|
#[serde(skip, default = "default_speed")]
|
||||||
pub speed: f64,
|
pub speed: f64,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub god_mode: bool,
|
pub god_mode: bool,
|
||||||
|
@ -28,11 +33,28 @@ pub struct Settings {
|
||||||
pub debug_outlines: bool,
|
pub debug_outlines: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_speed() -> f64 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
impl Settings {
|
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())
|
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> {
|
pub fn create_player1_controller(&self) -> Box<dyn PlayerController> {
|
||||||
if self.touch_controls {
|
if self.touch_controls {
|
||||||
return Box::new(TouchPlayerController::new());
|
return Box::new(TouchPlayerController::new());
|
||||||
|
@ -55,6 +77,7 @@ impl Default for Settings {
|
||||||
subpixel_coords: true,
|
subpixel_coords: true,
|
||||||
motion_interpolation: true,
|
motion_interpolation: true,
|
||||||
touch_controls: cfg!(target_os = "android"),
|
touch_controls: cfg!(target_os = "android"),
|
||||||
|
soundtrack: "".to_string(),
|
||||||
player1_key_map: p1_default_keymap(),
|
player1_key_map: p1_default_keymap(),
|
||||||
player2_key_map: p2_default_keymap(),
|
player2_key_map: p2_default_keymap(),
|
||||||
speed: 1.0,
|
speed: 1.0,
|
||||||
|
@ -90,7 +113,7 @@ fn p1_default_keymap() -> PlayerKeyMap {
|
||||||
next_weapon: ScanCode::S,
|
next_weapon: ScanCode::S,
|
||||||
jump: ScanCode::Z,
|
jump: ScanCode::Z,
|
||||||
shoot: ScanCode::X,
|
shoot: ScanCode::X,
|
||||||
skip: ScanCode::LControl,
|
skip: ScanCode::E,
|
||||||
inventory: ScanCode::Q,
|
inventory: ScanCode::Q,
|
||||||
map: ScanCode::W,
|
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::sync::mpsc::{Receiver, Sender};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use cpal::Sample;
|
|
||||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
|
use cpal::Sample;
|
||||||
|
use lewton::inside_ogg::OggStreamReader;
|
||||||
use num_traits::clamp;
|
use num_traits::clamp;
|
||||||
|
|
||||||
use crate::engine_constants::EngineConstants;
|
use crate::engine_constants::EngineConstants;
|
||||||
use crate::framework::context::Context;
|
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;
|
||||||
|
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::organya::Song;
|
||||||
use crate::sound::pixtone::PixTonePlayback;
|
use crate::sound::pixtone::PixTonePlayback;
|
||||||
use crate::sound::playback::{PlaybackEngine, SavedPlaybackState};
|
|
||||||
use crate::sound::wave_bank::SoundBank;
|
use crate::sound::wave_bank::SoundBank;
|
||||||
use crate::str;
|
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 organya;
|
||||||
mod pixtone;
|
mod pixtone;
|
||||||
mod pixtone_sfx;
|
mod pixtone_sfx;
|
||||||
mod playback;
|
|
||||||
mod stuff;
|
mod stuff;
|
||||||
mod wav;
|
mod wav;
|
||||||
|
mod wave_bank;
|
||||||
|
|
||||||
pub struct SoundManager {
|
pub struct SoundManager {
|
||||||
tx: Sender<PlaybackMessage>,
|
tx: Sender<PlaybackMessage>,
|
||||||
|
@ -31,61 +36,26 @@ pub struct SoundManager {
|
||||||
current_song_id: usize,
|
current_song_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
static SONGS: [&str; 43] = [
|
enum SongFormat {
|
||||||
"xxxx",
|
Organya,
|
||||||
"wanpaku",
|
OggSinglePart,
|
||||||
"anzen",
|
OggMultiPart,
|
||||||
"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"
|
|
||||||
];
|
|
||||||
|
|
||||||
impl SoundManager {
|
impl SoundManager {
|
||||||
pub fn new(ctx: &mut Context) -> GameResult<SoundManager> {
|
pub fn new(ctx: &mut Context) -> GameResult<SoundManager> {
|
||||||
let (tx, rx): (Sender<PlaybackMessage>, Receiver<PlaybackMessage>) = mpsc::channel();
|
let (tx, rx): (Sender<PlaybackMessage>, Receiver<PlaybackMessage>) = mpsc::channel();
|
||||||
|
|
||||||
let host = cpal::default_host();
|
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 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 || {
|
std::thread::spawn(move || {
|
||||||
if let Err(err) = match config.sample_format() {
|
if let Err(err) = match config.sample_format() {
|
||||||
|
@ -108,7 +78,13 @@ impl SoundManager {
|
||||||
let _ = self.tx.send(PlaybackMessage::PlaySample(id));
|
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 {
|
if self.current_song_id == song_id {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
@ -121,27 +97,139 @@ impl SoundManager {
|
||||||
|
|
||||||
self.tx.send(PlaybackMessage::SaveState)?;
|
self.tx.send(PlaybackMessage::SaveState)?;
|
||||||
self.tx.send(PlaybackMessage::Stop)?;
|
self.tx.send(PlaybackMessage::Stop)?;
|
||||||
} else if let Some(song_name) = SONGS.get(song_id) {
|
} else if let Some(song_name) = constants.music_table.get(song_id) {
|
||||||
let path = constants.organya_paths
|
let mut paths = constants.organya_paths.clone();
|
||||||
.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)))?;
|
|
||||||
|
|
||||||
match filesystem::open(ctx, path).map(|f| organya::Song::load_from(f)) {
|
if let Some(soundtrack) = constants.soundtracks.get(&settings.soundtrack) {
|
||||||
Ok(Ok(org)) => {
|
paths.insert(0, soundtrack.clone());
|
||||||
log::info!("Playing BGM: {} {}", song_id, song_name);
|
}
|
||||||
|
|
||||||
self.prev_song_id = self.current_song_id;
|
let songs_paths = paths.iter().map(|prefix| {
|
||||||
self.current_song_id = song_id;
|
[
|
||||||
self.tx.send(PlaybackMessage::SaveState)?;
|
(
|
||||||
self.tx.send(PlaybackMessage::PlaySong(Box::new(org)))?;
|
SongFormat::OggMultiPart,
|
||||||
}
|
vec![
|
||||||
Ok(Err(err)) | Err(err) => {
|
format!("{}{}_intro.ogg", prefix, song_name),
|
||||||
log::warn!("Failed to load BGM {}: {}", song_id, err);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +263,9 @@ impl SoundManager {
|
||||||
|
|
||||||
enum PlaybackMessage {
|
enum PlaybackMessage {
|
||||||
Stop,
|
Stop,
|
||||||
PlaySong(Box<Song>),
|
PlayOrganyaSong(Box<Song>),
|
||||||
|
PlayOggSongSinglePart(Box<OggStreamReader<File>>),
|
||||||
|
PlayOggSongMultiPart(Box<OggStreamReader<File>>, Box<OggStreamReader<File>>),
|
||||||
PlaySample(u8),
|
PlaySample(u8),
|
||||||
SetSpeed(f32),
|
SetSpeed(f32),
|
||||||
SaveState,
|
SaveState,
|
||||||
|
@ -185,32 +275,46 @@ enum PlaybackMessage {
|
||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
enum PlaybackState {
|
enum PlaybackState {
|
||||||
Stopped,
|
Stopped,
|
||||||
Playing,
|
PlayingOrg,
|
||||||
|
PlayingOgg,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run<T>(rx: Receiver<PlaybackMessage>, bank: SoundBank,
|
enum PlaybackStateType {
|
||||||
device: &cpal::Device, config: &cpal::StreamConfig) -> GameResult where
|
None,
|
||||||
|
Organya(SavedOrganyaPlaybackState),
|
||||||
|
Ogg(SavedOggPlaybackState),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run<T>(
|
||||||
|
rx: Receiver<PlaybackMessage>,
|
||||||
|
bank: SoundBank,
|
||||||
|
device: &cpal::Device,
|
||||||
|
config: &cpal::StreamConfig,
|
||||||
|
) -> GameResult
|
||||||
|
where
|
||||||
T: cpal::Sample,
|
T: cpal::Sample,
|
||||||
{
|
{
|
||||||
let sample_rate = config.sample_rate.0 as f32;
|
let sample_rate = config.sample_rate.0 as f32;
|
||||||
let channels = config.channels as usize;
|
let channels = config.channels as usize;
|
||||||
let mut state = PlaybackState::Stopped;
|
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 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();
|
let mut pixtone = PixTonePlayback::new();
|
||||||
pixtone.create_samples();
|
pixtone.create_samples();
|
||||||
|
|
||||||
log::info!("Audio format: {} {}", sample_rate, channels);
|
log::info!("Audio format: {} {}", sample_rate, channels);
|
||||||
org_engine.set_sample_rate(sample_rate as usize);
|
org_engine.set_sample_rate(sample_rate as usize);
|
||||||
org_engine.loops = usize::MAX;
|
org_engine.loops = usize::MAX;
|
||||||
|
ogg_engine.set_sample_rate(sample_rate as usize);
|
||||||
|
|
||||||
let buf_size = sample_rate as usize * 10 / 1000;
|
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 pxt_buf = vec![0x8000; buf_size];
|
||||||
let mut bgm_index = 0;
|
let mut bgm_index = 0;
|
||||||
let mut pxt_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);
|
pixtone.mix(&mut pxt_buf, sample_rate);
|
||||||
|
|
||||||
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
|
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| {
|
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||||
loop {
|
loop {
|
||||||
match rx.try_recv() {
|
match rx.try_recv() {
|
||||||
Ok(PlaybackMessage::PlaySong(song)) => {
|
Ok(PlaybackMessage::PlayOrganyaSong(song)) => {
|
||||||
if state == PlaybackState::Stopped {
|
if state == PlaybackState::Stopped {
|
||||||
saved_state = None;
|
saved_state = PlaybackStateType::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
org_engine.start_song(*song, &bank);
|
org_engine.start_song(*song, &bank);
|
||||||
|
|
||||||
for i in &mut bgm_buf[0..frames] { *i = 0x8080 };
|
for i in &mut bgm_buf[0..samples] {
|
||||||
frames = org_engine.render_to(&mut bgm_buf);
|
*i = 0x8080
|
||||||
|
}
|
||||||
|
samples = org_engine.render_to(&mut bgm_buf);
|
||||||
bgm_index = 0;
|
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)) => {
|
Ok(PlaybackMessage::PlaySample(id)) => {
|
||||||
pixtone.play_sfx(id);
|
pixtone.play_sfx(id);
|
||||||
}
|
}
|
||||||
Ok(PlaybackMessage::Stop) => {
|
Ok(PlaybackMessage::Stop) => {
|
||||||
if state == PlaybackState::Stopped {
|
if state == PlaybackState::Stopped {
|
||||||
saved_state = None;
|
saved_state = PlaybackStateType::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
state = PlaybackState::Stopped;
|
state = PlaybackState::Stopped;
|
||||||
|
@ -246,74 +382,143 @@ fn run<T>(rx: Receiver<PlaybackMessage>, bank: SoundBank,
|
||||||
Ok(PlaybackMessage::SetSpeed(new_speed)) => {
|
Ok(PlaybackMessage::SetSpeed(new_speed)) => {
|
||||||
assert!(new_speed > 0.0);
|
assert!(new_speed > 0.0);
|
||||||
speed = new_speed;
|
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);
|
org_engine.set_sample_rate((sample_rate / new_speed) as usize);
|
||||||
}
|
}
|
||||||
Ok(PlaybackMessage::SaveState) => {
|
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) => {
|
Ok(PlaybackMessage::RestoreState) => {
|
||||||
if saved_state.is_some() {
|
let mut saved_state_loc = PlaybackStateType::None;
|
||||||
org_engine.set_state(saved_state.clone().unwrap(), &bank);
|
std::mem::swap(&mut saved_state_loc, &mut saved_state);
|
||||||
saved_state = None;
|
|
||||||
|
|
||||||
if state == PlaybackState::Stopped {
|
match saved_state_loc {
|
||||||
org_engine.set_position(0);
|
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 };
|
if state == PlaybackState::Stopped {
|
||||||
frames = org_engine.render_to(&mut bgm_buf);
|
ogg_engine.rewind();
|
||||||
bgm_index = 0;
|
}
|
||||||
|
|
||||||
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) {
|
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 {
|
if state == PlaybackState::Stopped {
|
||||||
(0x8000, 0x8000)
|
(0x8000, 0x8000)
|
||||||
} else if bgm_index < frames {
|
} else if bgm_index < samples {
|
||||||
let sample = bgm_buf[bgm_index];
|
match state {
|
||||||
bgm_index += 1;
|
PlaybackState::PlayingOrg => {
|
||||||
((sample & 0xff) << 8, sample & 0xff00)
|
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 {
|
} else {
|
||||||
for i in &mut bgm_buf[0..frames] { *i = 0x8080 };
|
for i in &mut bgm_buf[0..samples] {
|
||||||
frames = org_engine.render_to(&mut bgm_buf);
|
*i = 0x8080
|
||||||
bgm_index = 0;
|
}
|
||||||
let sample = bgm_buf[0];
|
|
||||||
((sample & 0xff) << 8, sample & 0xff00)
|
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];
|
let pxt_sample: u16 = pxt_buf[pxt_index];
|
||||||
|
|
||||||
if pxt_index < (pxt_buf.len() - 1) {
|
if pxt_index < (pxt_buf.len() - 1) {
|
||||||
pxt_index += 1;
|
pxt_index += 1;
|
||||||
} else {
|
} else {
|
||||||
pxt_index = 0;
|
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);
|
pixtone.mix(&mut pxt_buf, sample_rate / speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if frame.len() >= 2 {
|
if frame.len() >= 2 {
|
||||||
let sample_l = clamp(
|
let sample_l = clamp(
|
||||||
(((org_sample_l ^ 0x8000) as i16) as isize)
|
(((bgm_sample_l ^ 0x8000) as i16) as isize)
|
||||||
+ (((pxt_sample ^ 0x8000) as i16) as isize)
|
+ (((pxt_sample ^ 0x8000) as i16) as isize),
|
||||||
, -0x7fff, 0x7fff) as u16 ^ 0x8000;
|
-0x7fff,
|
||||||
|
0x7fff,
|
||||||
|
) as u16
|
||||||
|
^ 0x8000;
|
||||||
let sample_r = clamp(
|
let sample_r = clamp(
|
||||||
(((org_sample_r ^ 0x8000) as i16) as isize)
|
(((bgm_sample_r ^ 0x8000) as i16) as isize)
|
||||||
+ (((pxt_sample ^ 0x8000) as i16) as isize)
|
+ (((pxt_sample ^ 0x8000) as i16) as isize),
|
||||||
, -0x7fff, 0x7fff) as u16 ^ 0x8000;
|
-0x7fff,
|
||||||
|
0x7fff,
|
||||||
|
) as u16
|
||||||
|
^ 0x8000;
|
||||||
|
|
||||||
frame[0] = Sample::from::<u16>(&sample_l);
|
frame[0] = Sample::from::<u16>(&sample_l);
|
||||||
frame[1] = Sample::from::<u16>(&sample_r);
|
frame[1] = Sample::from::<u16>(&sample_r);
|
||||||
} else {
|
} else {
|
||||||
let sample = clamp(
|
let sample = clamp(
|
||||||
(((org_sample_l ^ 0x8000) as i16) as isize)
|
((((bgm_sample_l ^ 0x8000) as i16) + ((bgm_sample_r ^ 0x8000) as i16)) / 2)
|
||||||
+ (((pxt_sample ^ 0x8000) as i16) as isize)
|
as isize
|
||||||
, -0x7fff, 0x7fff) as u16 ^ 0x8000;
|
+ (((pxt_sample ^ 0x8000) as i16) as isize),
|
||||||
|
-0x7fff,
|
||||||
|
0x7fff,
|
||||||
|
) as u16
|
||||||
|
^ 0x8000;
|
||||||
|
|
||||||
frame[0] = Sample::from::<u16>(&sample);
|
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::wav::*;
|
||||||
use crate::sound::wave_bank::SoundBank;
|
use crate::sound::wave_bank::SoundBank;
|
||||||
|
|
||||||
pub struct PlaybackEngine {
|
pub(crate) struct OrgPlaybackEngine {
|
||||||
song: Organya,
|
song: Organya,
|
||||||
lengths: [u8; 8],
|
lengths: [u8; 8],
|
||||||
swaps: [usize; 8],
|
swaps: [usize; 8],
|
||||||
keys: [u8; 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],
|
track_buffers: [RenderBuffer; 136],
|
||||||
output_format: WavFormat,
|
output_format: WavFormat,
|
||||||
play_pos: i32,
|
play_pos: i32,
|
||||||
|
@ -18,74 +26,32 @@ pub struct PlaybackEngine {
|
||||||
pub loops: usize,
|
pub loops: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SavedPlaybackState {
|
pub struct SavedOrganyaPlaybackState {
|
||||||
song: Organya,
|
song: Organya,
|
||||||
play_pos: i32,
|
play_pos: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for SavedPlaybackState {
|
impl Clone for SavedOrganyaPlaybackState {
|
||||||
fn clone(&self) -> SavedPlaybackState {
|
fn clone(&self) -> SavedOrganyaPlaybackState {
|
||||||
SavedPlaybackState {
|
SavedOrganyaPlaybackState {
|
||||||
song: self.song.clone(),
|
song: self.song.clone(),
|
||||||
play_pos: self.play_pos,
|
play_pos: self.play_pos,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlaybackEngine {
|
impl OrgPlaybackEngine {
|
||||||
pub fn new(song: Organya, samples: &SoundBank) -> Self {
|
pub fn new(samples: &SoundBank) -> Self {
|
||||||
|
let mut buffers: [MaybeUninit<RenderBuffer>; 136] = unsafe { MaybeUninit::uninit().assume_init() };
|
||||||
|
|
||||||
// Octave 0 Track 0 Swap 0
|
for buffer in buffers.iter_mut() {
|
||||||
// Octave 0 Track 1 Swap 0
|
*buffer = MaybeUninit::new(RenderBuffer::empty());
|
||||||
// ...
|
|
||||||
// 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()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let song = Organya::empty();
|
||||||
let frames_per_tick = (44100 / 1000) * song.time.wait as usize;
|
let frames_per_tick = (44100 / 1000) * song.time.wait as usize;
|
||||||
|
|
||||||
PlaybackEngine {
|
OrgPlaybackEngine {
|
||||||
song,
|
song,
|
||||||
lengths: [0; 8],
|
lengths: [0; 8],
|
||||||
swaps: [0; 8],
|
swaps: [0; 8],
|
||||||
|
@ -104,7 +70,8 @@ impl PlaybackEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_sample_rate(&mut self, sample_rate: usize) {
|
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.output_format.sample_rate = sample_rate as u32;
|
||||||
self.frames_per_tick = (sample_rate / 1000) * self.song.time.wait as usize;
|
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 {
|
pub fn get_state(&self) -> SavedOrganyaPlaybackState {
|
||||||
SavedPlaybackState {
|
SavedOrganyaPlaybackState {
|
||||||
song: self.song.clone(),
|
song: self.song.clone(),
|
||||||
play_pos: self.play_pos,
|
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.start_song(state.song, samples);
|
||||||
self.play_pos = state.play_pos;
|
self.play_pos = state.play_pos;
|
||||||
}
|
}
|
||||||
|
@ -128,12 +95,13 @@ impl PlaybackEngine {
|
||||||
pub fn start_song(&mut self, song: Organya, samples: &SoundBank) {
|
pub fn start_song(&mut self, song: Organya, samples: &SoundBank) {
|
||||||
for i in 0..8 {
|
for i in 0..8 {
|
||||||
let sound_index = song.tracks[i].inst.inst as usize;
|
let sound_index = song.tracks[i].inst.inst as usize;
|
||||||
let sound = samples.get_wave(sound_index)
|
let sound = samples.get_wave(sound_index).iter().map(|&x| x ^ 128).collect();
|
||||||
.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 });
|
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 {
|
if self.song.version == Version::Extended {
|
||||||
*buf = RenderBuffer::new(samples.samples[track.inst.inst as usize].clone());
|
*buf = RenderBuffer::new(samples.samples[track.inst.inst as usize].clone());
|
||||||
} else {
|
} else {
|
||||||
|
@ -156,16 +128,25 @@ impl PlaybackEngine {
|
||||||
self.play_pos = 0;
|
self.play_pos = 0;
|
||||||
self.frames_per_tick = (self.output_format.sample_rate as usize / 1000) * self.song.time.wait as usize;
|
self.frames_per_tick = (self.output_format.sample_rate as usize / 1000) * self.song.time.wait as usize;
|
||||||
self.frames_this_tick = 0;
|
self.frames_this_tick = 0;
|
||||||
for i in self.lengths.iter_mut() { *i = 0 };
|
for i in self.lengths.iter_mut() {
|
||||||
for i in self.swaps.iter_mut() { *i = 0 };
|
*i = 0
|
||||||
for i in self.keys.iter_mut() { *i = 255 };
|
}
|
||||||
|
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) {
|
pub fn set_position(&mut self, position: i32) {
|
||||||
self.play_pos = position;
|
self.play_pos = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn rewind(&mut self) {
|
||||||
|
self.set_position(0);
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn get_total_samples(&self) -> u32 {
|
pub fn get_total_samples(&self) -> u32 {
|
||||||
let ticks_intro = self.song.time.loop_range.start;
|
let ticks_intro = self.song.time.loop_range.start;
|
||||||
|
@ -177,12 +158,12 @@ impl PlaybackEngine {
|
||||||
|
|
||||||
fn update_play_state(&mut self) {
|
fn update_play_state(&mut self) {
|
||||||
for track in 0..8 {
|
for track in 0..8 {
|
||||||
if let Some(note) =
|
if let Some(note) = self.song.tracks[track].notes.iter().find(|x| x.pos == self.play_pos) {
|
||||||
self.song.tracks[track].notes.iter().find(|x| x.pos == self.play_pos) {
|
|
||||||
// New note
|
// New note
|
||||||
//eprintln!("{:?}", &self.keys);
|
//eprintln!("{:?}", &self.keys);
|
||||||
if note.key != 255 {
|
if note.key != 255 {
|
||||||
if self.keys[track] == 255 { // New
|
if self.keys[track] == 255 {
|
||||||
|
// New
|
||||||
let octave = (note.key / 12) * 8;
|
let octave = (note.key / 12) * 8;
|
||||||
let j = octave as usize + track + self.swaps[track];
|
let j = octave as usize + track + self.swaps[track];
|
||||||
for k in 0..16 {
|
for k in 0..16 {
|
||||||
|
@ -194,13 +175,15 @@ impl PlaybackEngine {
|
||||||
|
|
||||||
let l = p_oct as usize * 8 + track + swap;
|
let l = p_oct as usize * 8 + track + swap;
|
||||||
self.track_buffers[l].set_frequency(freq as u32);
|
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].looping = true;
|
||||||
self.track_buffers[j].playing = true;
|
self.track_buffers[j].playing = true;
|
||||||
// last playing key
|
// last playing key
|
||||||
self.keys[track] = note.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);
|
//assert!(self.lengths[track] == 0);
|
||||||
let octave = (self.keys[track] / 12) * 8;
|
let octave = (self.keys[track] / 12) * 8;
|
||||||
let j = octave as usize + track + self.swaps[track];
|
let j = octave as usize + track + self.swaps[track];
|
||||||
|
@ -210,10 +193,12 @@ impl PlaybackEngine {
|
||||||
self.swaps[track] += 64;
|
self.swaps[track] += 64;
|
||||||
self.swaps[track] %= 128;
|
self.swaps[track] %= 128;
|
||||||
let j = octave as usize + track + self.swaps[track];
|
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].looping = true;
|
||||||
self.track_buffers[j].playing = true;
|
self.track_buffers[j].playing = true;
|
||||||
} else { // change
|
} else {
|
||||||
|
// change
|
||||||
let octave = (self.keys[track] / 12) * 8;
|
let octave = (self.keys[track] / 12) * 8;
|
||||||
let j = octave as usize + track + self.swaps[track];
|
let j = octave as usize + track + self.swaps[track];
|
||||||
if self.song.tracks[track].inst.pipi == 0 {
|
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 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;
|
let l = p_oct as usize * 8 + track + swap;
|
||||||
self.track_buffers[l].set_frequency(freq as u32);
|
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].looping = true;
|
||||||
self.track_buffers[j].playing = true;
|
self.track_buffers[j].playing = true;
|
||||||
|
@ -278,9 +264,7 @@ impl PlaybackEngine {
|
||||||
|
|
||||||
// start a new note
|
// start a new note
|
||||||
// note (hah) that drums are unaffected by length and pi values. This is the only case we have to handle.
|
// note (hah) that drums are unaffected by length and pi values. This is the only case we have to handle.
|
||||||
if let Some(note) =
|
if let Some(note) = notes.iter().find(|x| x.pos == self.play_pos) {
|
||||||
notes.iter().find(|x| x.pos == self.play_pos) {
|
|
||||||
|
|
||||||
// FIXME: Add constants for dummy values
|
// FIXME: Add constants for dummy values
|
||||||
if note.key != 255 {
|
if note.key != 255 {
|
||||||
let freq = org_key_to_drum_freq(note.key);
|
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 vol = centibel_to_scale(buf.volume);
|
||||||
|
|
||||||
let (pan_l, pan_r) =
|
let (pan_l, pan_r) = match buf.pan.signum() {
|
||||||
match buf.pan.signum() {
|
0 => (1.0, 1.0),
|
||||||
0 => (1.0, 1.0),
|
1 => (centibel_to_scale(-buf.pan), 1.0),
|
||||||
1 => (centibel_to_scale(-buf.pan), 1.0),
|
-1 => (1.0, centibel_to_scale(buf.pan)),
|
||||||
-1 => (1.0, centibel_to_scale(buf.pan)),
|
_ => unsafe { std::hint::unreachable_unchecked() },
|
||||||
_ => unsafe { std::hint::unreachable_unchecked() }
|
};
|
||||||
};
|
|
||||||
|
|
||||||
fn clamp<T: Ord>(v: T, limit: T) -> T {
|
fn clamp<T: Ord>(v: T, limit: T) -> T {
|
||||||
if v > limit {
|
if v > limit {
|
||||||
|
@ -361,7 +344,6 @@ pub fn mix(dst: &mut [u16], dst_fmt: WavFormat, srcs: &mut [RenderBuffer]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
|
|
||||||
for frame in dst.iter_mut() {
|
for frame in dst.iter_mut() {
|
||||||
let pos = buf.position as usize + buf.base_pos;
|
let pos = buf.position as usize + buf.base_pos;
|
||||||
// -1..1
|
// -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 + (s2 - s1) * r1; // Linear interp
|
||||||
//let s = s1 * (1.0 - r2) + s2 * r2; // Cosine interp
|
//let s = s1 * (1.0 - r2) + s2 * r2; // Cosine interp
|
||||||
let s = cubic_interp(s1, s2, s4, s3, r1); // Cubic 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
|
// -128..128
|
||||||
let sl = s * pan_l * vol * 128.0;
|
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 {
|
pub fn new_organya(mut sample: WavSample) -> RenderBuffer {
|
||||||
let wave = sample.data.clone();
|
let wave = sample.data.clone();
|
||||||
sample.data.clear();
|
sample.data.clear();
|
||||||
|
@ -473,10 +477,7 @@ impl RenderBuffer {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn organya_select_octave(&mut self, octave: usize, pipi: bool) {
|
pub fn organya_select_octave(&mut self, octave: usize, pipi: bool) {
|
||||||
const OFFS: &[usize] = &[0x000, 0x100,
|
const OFFS: &[usize] = &[0x000, 0x100, 0x200, 0x280, 0x300, 0x340, 0x360, 0x370];
|
||||||
0x200, 0x280,
|
|
||||||
0x300, 0x340,
|
|
||||||
0x360, 0x370];
|
|
||||||
const LENS: &[usize] = &[256_usize, 256, 128, 128, 64, 32, 16, 8];
|
const LENS: &[usize] = &[256_usize, 256, 128, 128, 64, 32, 16, 8];
|
||||||
self.base_pos = OFFS[octave];
|
self.base_pos = OFFS[octave];
|
||||||
self.len = LENS[octave];
|
self.len = LENS[octave];
|
|
@ -289,7 +289,6 @@ pub enum OpCode {
|
||||||
KE2,
|
KE2,
|
||||||
/// <FRE related to player 2?
|
/// <FRE related to player 2?
|
||||||
FR2,
|
FR2,
|
||||||
|
|
||||||
// ---- Custom opcodes, for use by modders ----
|
// ---- Custom opcodes, for use by modders ----
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,12 +468,16 @@ impl TextScriptVM {
|
||||||
|
|
||||||
pub fn set_global_script(&mut self, script: TextScript) {
|
pub fn set_global_script(&mut self, script: TextScript) {
|
||||||
self.scripts.global_script = script;
|
self.scripts.global_script = script;
|
||||||
if !self.suspend { self.reset(); }
|
if !self.suspend {
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_scene_script(&mut self, script: TextScript) {
|
pub fn set_scene_script(&mut self, script: TextScript) {
|
||||||
self.scripts.scene_script = script;
|
self.scripts.scene_script = script;
|
||||||
if !self.suspend { self.reset(); }
|
if !self.suspend {
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_inventory_script(&mut self, script: TextScript) {
|
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 {
|
pub fn run(state: &mut SharedGameState, game_scene: &mut GameScene, ctx: &mut Context) -> GameResult {
|
||||||
loop {
|
loop {
|
||||||
if state.textscript_vm.suspend { break; }
|
if state.textscript_vm.suspend {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
match state.textscript_vm.state {
|
match state.textscript_vm.state {
|
||||||
TextScriptExecutionState::Ended => {
|
TextScriptExecutionState::Ended => {
|
||||||
|
@ -569,12 +574,14 @@ impl TextScriptVM {
|
||||||
if remaining > 1 {
|
if remaining > 1 {
|
||||||
let ticks = if state.textscript_vm.flags.fast()
|
let ticks = if state.textscript_vm.flags.fast()
|
||||||
|| game_scene.player1.controller.skip()
|
|| game_scene.player1.controller.skip()
|
||||||
|| game_scene.player2.controller.skip() {
|
|| game_scene.player2.controller.skip()
|
||||||
|
{
|
||||||
0
|
0
|
||||||
} else if game_scene.player1.controller.jump()
|
} else if game_scene.player1.controller.jump()
|
||||||
|| game_scene.player1.controller.shoot()
|
|| game_scene.player1.controller.shoot()
|
||||||
|| game_scene.player2.controller.jump()
|
|| game_scene.player2.controller.jump()
|
||||||
|| game_scene.player2.controller.shoot() {
|
|| game_scene.player2.controller.shoot()
|
||||||
|
{
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
4
|
4
|
||||||
|
@ -584,9 +591,11 @@ impl TextScriptVM {
|
||||||
state.sound_manager.play_sfx(2);
|
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 {
|
} else {
|
||||||
state.textscript_vm.state = TextScriptExecutionState::Running(event, cursor.position() as u32);
|
state.textscript_vm.state =
|
||||||
|
TextScriptExecutionState::Running(event, cursor.position() as u32);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.textscript_vm.reset();
|
state.textscript_vm.reset();
|
||||||
|
@ -602,16 +611,19 @@ impl TextScriptVM {
|
||||||
}
|
}
|
||||||
TextScriptExecutionState::WaitConfirmation(event, ip, no_event, wait, selection) => {
|
TextScriptExecutionState::WaitConfirmation(event, ip, no_event, wait, selection) => {
|
||||||
if wait > 0 {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if game_scene.player1.controller.trigger_left()
|
if game_scene.player1.controller.trigger_left()
|
||||||
|| game_scene.player1.controller.trigger_right()
|
|| game_scene.player1.controller.trigger_right()
|
||||||
|| game_scene.player2.controller.trigger_left()
|
|| 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.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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -641,7 +653,8 @@ impl TextScriptVM {
|
||||||
|| game_scene.player1.controller.skip()
|
|| game_scene.player1.controller.skip()
|
||||||
|| game_scene.player2.controller.trigger_jump()
|
|| game_scene.player2.controller.trigger_jump()
|
||||||
|| game_scene.player2.controller.trigger_shoot()
|
|| game_scene.player2.controller.trigger_shoot()
|
||||||
|| game_scene.player2.controller.skip() {
|
|| game_scene.player2.controller.skip()
|
||||||
|
{
|
||||||
state.textscript_vm.state = TextScriptExecutionState::Running(event, ip);
|
state.textscript_vm.state = TextScriptExecutionState::Running(event, ip);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -672,7 +685,13 @@ impl TextScriptVM {
|
||||||
Ok(())
|
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 mut exec_state = state.textscript_vm.state;
|
||||||
|
|
||||||
let state_ref = state as *mut SharedGameState;
|
let state_ref = state as *mut SharedGameState;
|
||||||
|
@ -682,8 +701,8 @@ impl TextScriptVM {
|
||||||
let mut cursor = Cursor::new(bytecode);
|
let mut cursor = Cursor::new(bytecode);
|
||||||
cursor.seek(SeekFrom::Start(ip as u64))?;
|
cursor.seek(SeekFrom::Start(ip as u64))?;
|
||||||
|
|
||||||
let op_maybe: Option<OpCode> = FromPrimitive::from_i32(read_cur_varint(&mut cursor)
|
let op_maybe: Option<OpCode> =
|
||||||
.unwrap_or_else(|_| OpCode::END as i32));
|
FromPrimitive::from_i32(read_cur_varint(&mut cursor).unwrap_or_else(|_| OpCode::END as i32));
|
||||||
|
|
||||||
if let Some(op) = op_maybe {
|
if let Some(op) = op_maybe {
|
||||||
println!("opcode: {:?}", op);
|
println!("opcode: {:?}", op);
|
||||||
|
@ -724,7 +743,9 @@ impl TextScriptVM {
|
||||||
OpCode::SLP => {
|
OpCode::SLP => {
|
||||||
state.textscript_vm.set_mode(ScriptMode::StageSelect);
|
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
|
1000 + slot.0
|
||||||
} else {
|
} else {
|
||||||
1000
|
1000
|
||||||
|
@ -1026,7 +1047,13 @@ impl TextScriptVM {
|
||||||
|
|
||||||
state.sound_manager.play_sfx(5);
|
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 => {
|
OpCode::NUM => {
|
||||||
let index = read_cur_varint(&mut cursor)? as usize;
|
let index = read_cur_varint(&mut cursor)? as usize;
|
||||||
|
@ -1161,12 +1188,12 @@ impl TextScriptVM {
|
||||||
}
|
}
|
||||||
OpCode::CMU => {
|
OpCode::CMU => {
|
||||||
let song_id = read_cur_varint(&mut cursor)? as usize;
|
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);
|
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
|
||||||
}
|
}
|
||||||
OpCode::FMU => {
|
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);
|
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
|
||||||
}
|
}
|
||||||
|
@ -1261,11 +1288,8 @@ impl TextScriptVM {
|
||||||
npc.tsc_direction = tsc_direction as u16;
|
npc.tsc_direction = tsc_direction as u16;
|
||||||
|
|
||||||
if direction == Direction::FacingPlayer {
|
if direction == Direction::FacingPlayer {
|
||||||
npc.direction = if game_scene.player1.x < npc.x {
|
npc.direction =
|
||||||
Direction::Right
|
if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left };
|
||||||
} else {
|
|
||||||
Direction::Left
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
npc.direction = direction;
|
npc.direction = direction;
|
||||||
}
|
}
|
||||||
|
@ -1317,16 +1341,21 @@ impl TextScriptVM {
|
||||||
npc.tsc_direction = tsc_direction as u16;
|
npc.tsc_direction = tsc_direction as u16;
|
||||||
|
|
||||||
if direction == Direction::FacingPlayer {
|
if direction == Direction::FacingPlayer {
|
||||||
npc.direction = if game_scene.player1.x < npc.x {
|
npc.direction =
|
||||||
Direction::Right
|
if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left };
|
||||||
} else {
|
|
||||||
Direction::Left
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
npc.direction = direction;
|
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;
|
npc.tsc_direction = tsc_direction as u16;
|
||||||
|
|
||||||
if direction == Direction::FacingPlayer {
|
if direction == Direction::FacingPlayer {
|
||||||
npc.direction = if game_scene.player1.x < npc.x {
|
npc.direction =
|
||||||
Direction::Right
|
if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left };
|
||||||
} else {
|
|
||||||
Direction::Left
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
npc.direction = direction;
|
npc.direction = direction;
|
||||||
}
|
}
|
||||||
|
@ -1375,11 +1401,8 @@ impl TextScriptVM {
|
||||||
npc.tsc_direction = tsc_direction as u16;
|
npc.tsc_direction = tsc_direction as u16;
|
||||||
|
|
||||||
if direction == Direction::FacingPlayer {
|
if direction == Direction::FacingPlayer {
|
||||||
npc.direction = if game_scene.player1.x < npc.x {
|
npc.direction =
|
||||||
Direction::Right
|
if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left };
|
||||||
} else {
|
|
||||||
Direction::Left
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
npc.direction = direction;
|
npc.direction = direction;
|
||||||
}
|
}
|
||||||
|
@ -1519,18 +1542,31 @@ impl TextScriptVM {
|
||||||
}
|
}
|
||||||
// unimplemented opcodes
|
// unimplemented opcodes
|
||||||
// Zero operands
|
// Zero operands
|
||||||
OpCode::CIL | OpCode::CPS | OpCode::KE2 |
|
OpCode::CIL
|
||||||
OpCode::CRE | OpCode::CSS | OpCode::FLA | OpCode::MLP |
|
| OpCode::CPS
|
||||||
OpCode::SPS | OpCode::FR2 |
|
| OpCode::KE2
|
||||||
OpCode::STC | OpCode::HM2 => {
|
| OpCode::CRE
|
||||||
|
| OpCode::CSS
|
||||||
|
| OpCode::FLA
|
||||||
|
| OpCode::MLP
|
||||||
|
| OpCode::SPS
|
||||||
|
| OpCode::FR2
|
||||||
|
| OpCode::STC
|
||||||
|
| OpCode::HM2 => {
|
||||||
log::warn!("unimplemented opcode: {:?}", op);
|
log::warn!("unimplemented opcode: {:?}", op);
|
||||||
|
|
||||||
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
|
exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32);
|
||||||
}
|
}
|
||||||
// One operand codes
|
// One operand codes
|
||||||
OpCode::MPp | OpCode::SKm | OpCode::SKp |
|
OpCode::MPp
|
||||||
OpCode::UNJ | OpCode::MPJ | OpCode::XX1 | OpCode::SIL |
|
| OpCode::SKm
|
||||||
OpCode::SSS | OpCode::ACH => {
|
| OpCode::SKp
|
||||||
|
| OpCode::UNJ
|
||||||
|
| OpCode::MPJ
|
||||||
|
| OpCode::XX1
|
||||||
|
| OpCode::SIL
|
||||||
|
| OpCode::SSS
|
||||||
|
| OpCode::ACH => {
|
||||||
let par_a = read_cur_varint(&mut cursor)?;
|
let par_a = read_cur_varint(&mut cursor)?;
|
||||||
|
|
||||||
log::warn!("unimplemented opcode: {:?} {}", op, par_a);
|
log::warn!("unimplemented opcode: {:?} {}", op, par_a);
|
||||||
|
@ -1564,9 +1600,7 @@ pub struct TextScript {
|
||||||
|
|
||||||
impl Clone for TextScript {
|
impl Clone for TextScript {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self { event_map: self.event_map.clone() }
|
||||||
event_map: self.event_map.clone(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1578,9 +1612,7 @@ impl Default for TextScript {
|
||||||
|
|
||||||
impl TextScript {
|
impl TextScript {
|
||||||
pub fn new() -> TextScript {
|
pub fn new() -> TextScript {
|
||||||
Self {
|
Self { event_map: HashMap::new() }
|
||||||
event_map: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads, decrypts and compiles a text script from specified stream.
|
/// Loads, decrypts and compiles a text script from specified stream.
|
||||||
|
@ -1590,11 +1622,7 @@ impl TextScript {
|
||||||
|
|
||||||
if constants.textscript.encrypted {
|
if constants.textscript.encrypted {
|
||||||
let half = buf.len() / 2;
|
let half = buf.len() / 2;
|
||||||
let key = if let Some(0) = buf.get(half) {
|
let key = if let Some(0) = buf.get(half) { 0xf9 } else { (-(*buf.get(half).unwrap() as isize)) as u8 };
|
||||||
0xf9
|
|
||||||
} else {
|
|
||||||
(-(*buf.get(half).unwrap() as isize)) as u8
|
|
||||||
};
|
|
||||||
log::info!("Decrypting TSC using key {:#x}", key);
|
log::info!("Decrypting TSC using key {:#x}", key);
|
||||||
|
|
||||||
for (idx, byte) in buf.iter_mut().enumerate() {
|
for (idx, byte) in buf.iter_mut().enumerate() {
|
||||||
|
@ -1637,8 +1665,12 @@ impl TextScript {
|
||||||
}
|
}
|
||||||
|
|
||||||
match TextScript::skip_until(b'#', &mut iter).ok() {
|
match TextScript::skip_until(b'#', &mut iter).ok() {
|
||||||
Some(_) => { continue; }
|
Some(_) => {
|
||||||
None => { break; }
|
continue;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1661,12 +1693,14 @@ impl TextScript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(TextScript {
|
Ok(TextScript { event_map })
|
||||||
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 bytecode = Vec::new();
|
||||||
let mut char_buf = Vec::with_capacity(16);
|
let mut char_buf = Vec::with_capacity(16);
|
||||||
|
|
||||||
|
@ -1687,7 +1721,8 @@ impl TextScript {
|
||||||
}
|
}
|
||||||
|
|
||||||
iter.next();
|
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])
|
.map(|t| [t.0, t.1, t.2])
|
||||||
.ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?;
|
.ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?;
|
||||||
|
|
||||||
|
@ -1747,56 +1782,144 @@ impl TextScript {
|
||||||
|
|
||||||
out.push(n);
|
out.push(n);
|
||||||
|
|
||||||
if x == 0 { break; }
|
if x == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[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;
|
let mut result = 0u32;
|
||||||
|
|
||||||
for o in 0..5 {
|
for o in 0..5 {
|
||||||
let n = iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?;
|
let n = iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?;
|
||||||
result |= (n as u32 & 0x7f) << (o * 7);
|
result |= (n as u32 & 0x7f) << (o * 7);
|
||||||
|
|
||||||
if n & 0x80 == 0 { break; }
|
if n & 0x80 == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(((result << 31) ^ (result >> 1)) as i32)
|
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)))?;
|
let instr = OpCode::from_str(code).map_err(|_| ParseError(format!("Unknown opcode: {}", code)))?;
|
||||||
|
|
||||||
match instr {
|
match instr {
|
||||||
// Zero operand codes
|
// Zero operand codes
|
||||||
OpCode::AEp | OpCode::CAT | OpCode::CIL | OpCode::CLO | OpCode::CLR | OpCode::CPS |
|
OpCode::AEp
|
||||||
OpCode::CRE | OpCode::CSS | OpCode::END | OpCode::ESC | OpCode::FLA | OpCode::FMU |
|
| OpCode::CAT
|
||||||
OpCode::FRE | OpCode::HMC | OpCode::INI | OpCode::KEY | OpCode::LDP | OpCode::MLP |
|
| OpCode::CIL
|
||||||
OpCode::MM0 | OpCode::MNA | OpCode::MS2 | OpCode::MS3 | OpCode::MSG | OpCode::NOD |
|
| OpCode::CLO
|
||||||
OpCode::PRI | OpCode::RMU | OpCode::SAT | OpCode::SLP | OpCode::SMC | OpCode::SPS |
|
| OpCode::CLR
|
||||||
OpCode::STC | OpCode::SVP | OpCode::TUR | OpCode::WAS | OpCode::ZAM | OpCode::HM2 |
|
| OpCode::CPS
|
||||||
OpCode::POP | OpCode::KE2 | OpCode::FR2 => {
|
| 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);
|
TextScript::put_varint(instr as i32, out);
|
||||||
}
|
}
|
||||||
// One operand codes
|
// One operand codes
|
||||||
OpCode::BOA | OpCode::BSL | OpCode::FOB | OpCode::FOM | OpCode::QUA | OpCode::UNI |
|
OpCode::BOA
|
||||||
OpCode::MYB | OpCode::MYD | OpCode::FAI | OpCode::FAO | OpCode::WAI | OpCode::FAC |
|
| OpCode::BSL
|
||||||
OpCode::GIT | OpCode::NUM | OpCode::DNA | OpCode::DNP | OpCode::FLm | OpCode::FLp |
|
| OpCode::FOB
|
||||||
OpCode::MPp | OpCode::SKm | OpCode::SKp | OpCode::EQp | OpCode::EQm | OpCode::MLp |
|
| OpCode::FOM
|
||||||
OpCode::ITp | OpCode::ITm | OpCode::AMm | OpCode::UNJ | OpCode::MPJ | OpCode::YNJ |
|
| OpCode::QUA
|
||||||
OpCode::EVE | OpCode::XX1 | OpCode::SIL | OpCode::LIp | OpCode::SOU | OpCode::CMU |
|
| OpCode::UNI
|
||||||
OpCode::SSS | OpCode::ACH | OpCode::S2MV | OpCode::PSH => {
|
| 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)?;
|
let operand = TextScript::read_number(iter)?;
|
||||||
TextScript::put_varint(instr as i32, out);
|
TextScript::put_varint(instr as i32, out);
|
||||||
TextScript::put_varint(operand as i32, out);
|
TextScript::put_varint(operand as i32, out);
|
||||||
}
|
}
|
||||||
// Two operand codes
|
// Two operand codes
|
||||||
OpCode::FON | OpCode::MOV | OpCode::AMp | OpCode::NCJ | OpCode::ECJ | OpCode::FLJ |
|
OpCode::FON
|
||||||
OpCode::ITJ | OpCode::SKJ | OpCode::AMJ | OpCode::SMP | OpCode::PSp | OpCode::IpN |
|
| OpCode::MOV
|
||||||
OpCode::FFm => {
|
| 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)?;
|
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)?;
|
let operand_b = TextScript::read_number(iter)?;
|
||||||
|
|
||||||
TextScript::put_varint(instr as i32, out);
|
TextScript::put_varint(instr as i32, out);
|
||||||
|
@ -1806,9 +1929,17 @@ impl TextScript {
|
||||||
// Three operand codes
|
// Three operand codes
|
||||||
OpCode::ANP | OpCode::CNP | OpCode::INP | OpCode::TAM | OpCode::CMP | OpCode::INJ => {
|
OpCode::ANP | OpCode::CNP | OpCode::INP | OpCode::TAM | OpCode::CMP | OpCode::INJ => {
|
||||||
let operand_a = TextScript::read_number(iter)?;
|
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)?;
|
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)?;
|
let operand_c = TextScript::read_number(iter)?;
|
||||||
|
|
||||||
TextScript::put_varint(instr as i32, out);
|
TextScript::put_varint(instr as i32, out);
|
||||||
|
@ -1819,11 +1950,23 @@ impl TextScript {
|
||||||
// Four operand codes
|
// Four operand codes
|
||||||
OpCode::TRA | OpCode::MNP | OpCode::SNP => {
|
OpCode::TRA | OpCode::MNP | OpCode::SNP => {
|
||||||
let operand_a = TextScript::read_number(iter)?;
|
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)?;
|
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)?;
|
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)?;
|
let operand_d = TextScript::read_number(iter)?;
|
||||||
|
|
||||||
TextScript::put_varint(instr as i32, out);
|
TextScript::put_varint(instr as i32, out);
|
||||||
|
@ -1851,31 +1994,106 @@ impl TextScript {
|
||||||
if let Some(op) = op_maybe {
|
if let Some(op) = op_maybe {
|
||||||
match op {
|
match op {
|
||||||
// Zero operand codes
|
// Zero operand codes
|
||||||
OpCode::AEp | OpCode::CAT | OpCode::CIL | OpCode::CLO | OpCode::CLR | OpCode::CPS |
|
OpCode::AEp
|
||||||
OpCode::CRE | OpCode::CSS | OpCode::END | OpCode::ESC | OpCode::FLA | OpCode::FMU |
|
| OpCode::CAT
|
||||||
OpCode::FRE | OpCode::HMC | OpCode::INI | OpCode::KEY | OpCode::LDP | OpCode::MLP |
|
| OpCode::CIL
|
||||||
OpCode::MM0 | OpCode::MNA | OpCode::MS2 | OpCode::MS3 | OpCode::MSG | OpCode::NOD |
|
| OpCode::CLO
|
||||||
OpCode::PRI | OpCode::RMU | OpCode::SAT | OpCode::SLP | OpCode::SMC | OpCode::SPS |
|
| OpCode::CLR
|
||||||
OpCode::STC | OpCode::SVP | OpCode::TUR | OpCode::WAS | OpCode::ZAM | OpCode::HM2 |
|
| OpCode::CPS
|
||||||
OpCode::POP | OpCode::KE2 | OpCode::FR2 => {
|
| 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());
|
result.push_str(format!("{:?}()\n", op).as_str());
|
||||||
}
|
}
|
||||||
// One operand codes
|
// One operand codes
|
||||||
OpCode::BOA | OpCode::BSL | OpCode::FOB | OpCode::FOM | OpCode::QUA | OpCode::UNI |
|
OpCode::BOA
|
||||||
OpCode::MYB | OpCode::MYD | OpCode::FAI | OpCode::FAO | OpCode::WAI | OpCode::FAC |
|
| OpCode::BSL
|
||||||
OpCode::GIT | OpCode::NUM | OpCode::DNA | OpCode::DNP | OpCode::FLm | OpCode::FLp |
|
| OpCode::FOB
|
||||||
OpCode::MPp | OpCode::SKm | OpCode::SKp | OpCode::EQp | OpCode::EQm | OpCode::MLp |
|
| OpCode::FOM
|
||||||
OpCode::ITp | OpCode::ITm | OpCode::AMm | OpCode::UNJ | OpCode::MPJ | OpCode::YNJ |
|
| OpCode::QUA
|
||||||
OpCode::EVE | OpCode::XX1 | OpCode::SIL | OpCode::LIp | OpCode::SOU | OpCode::CMU |
|
| OpCode::UNI
|
||||||
OpCode::SSS | OpCode::ACH | OpCode::S2MV | OpCode::PSH => {
|
| 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)?;
|
let par_a = read_cur_varint(&mut cursor)?;
|
||||||
|
|
||||||
result.push_str(format!("{:?}({})\n", op, par_a).as_str());
|
result.push_str(format!("{:?}({})\n", op, par_a).as_str());
|
||||||
}
|
}
|
||||||
// Two operand codes
|
// Two operand codes
|
||||||
OpCode::FON | OpCode::MOV | OpCode::AMp | OpCode::NCJ | OpCode::ECJ | OpCode::FLJ |
|
OpCode::FON
|
||||||
OpCode::ITJ | OpCode::SKJ | OpCode::AMJ | OpCode::SMP | OpCode::PSp | OpCode::IpN |
|
| OpCode::MOV
|
||||||
OpCode::FFm => {
|
| 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_a = read_cur_varint(&mut cursor)?;
|
||||||
let par_b = 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();
|
let res = iter.next();
|
||||||
|
|
||||||
match res {
|
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() {
|
while let Some(&chr) = iter.peek() {
|
||||||
if chr == expect {
|
if chr == expect {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -1963,7 +2181,7 @@ impl TextScript {
|
||||||
|
|
||||||
/// Reads a 4 digit TSC formatted number from iterator.
|
/// Reads a 4 digit TSC formatted number from iterator.
|
||||||
/// Intentionally does no '0'..'9' range checking, since it was often exploited by modders.
|
/// 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)
|
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 + 1000 * v.wrapping_sub(b'0') as i32))
|
||||||
.and_then(|result| iter.next().map(|v| result + 100 * 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.")))
|
.ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn has_event(&self, id: u16) -> bool {
|
pub fn has_event(&self, id: u16) -> bool {
|
||||||
self.event_map.contains_key(&id)
|
self.event_map.contains_key(&id)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue