use std::io::{Cursor, Read}; use std::str::from_utf8; use byteorder::LE; use byteorder::ReadBytesExt; use log::info; use crate::encoding::read_cur_shift_jis; use crate::engine_constants::EngineConstants; use crate::framework::context::Context; use crate::framework::error::GameError::ResourceLoadError; use crate::framework::error::GameResult; use crate::framework::filesystem; use crate::map::{Map, NPCData}; use crate::text_script::TextScript; use crate::common::Color; #[derive(Debug, PartialEq, Eq, Hash)] pub struct NpcType { name: String, } impl Clone for NpcType { fn clone(&self) -> Self { Self { name: self.name.clone() } } } impl NpcType { pub fn new(name: &str) -> Self { Self { name: name.to_owned() } } pub fn filename(&self) -> String { ["Npc", &self.name].join("") } } #[derive(Debug, PartialEq, Eq, Hash)] pub struct Tileset { pub(crate) name: String, } impl Clone for Tileset { fn clone(&self) -> Self { Self { name: self.name.clone() } } } impl Tileset { pub fn new(name: &str) -> Self { Self { name: name.to_owned() } } pub fn filename(&self) -> String { ["Prt", &self.name].join("") } pub fn orig_width(&self) -> usize { // todo: move to json or something? if self.name == "Labo" { return 128; } 256 } } #[derive(Debug, PartialEq, Eq, Hash)] pub struct Background { name: String, } impl Clone for Background { fn clone(&self) -> Self { Self { name: self.name.clone() } } } impl Background { pub fn new(name: &str) -> Self { Self { name: name.to_owned() } } pub fn name(&self) -> &str { &self.name } pub fn filename(&self) -> String { self.name.to_owned() } } #[derive(Debug, EnumIter, PartialEq, Eq, Hash, Copy, Clone)] pub enum BackgroundType { TiledStatic, TiledParallax, Tiled, /// Used in Core room, renders water in front of tilemap which also affects player's physics. Water, Black, /// Used in Ironhead fight. Affects physics of XP/heart/missile drops. Scrolling, /// Same as Outside, except it affects physics of XP/heart/missile drops. OutsideWind, Outside, /// Present in CS+KAGE, Seems to be a clone of Outside, isn't used anywhere and has unknown purpose OutsideUnknown, /// Used by CS+KAGE in waterway, it's just TiledParallax with bkCircle2 drawn behind water Waterway, } impl From for BackgroundType { fn from(val: u8) -> Self { match val { 0 => Self::TiledStatic, 1 => Self::TiledParallax, 2 => Self::Tiled, 3 => Self::Water, 4 => Self::Black, 5 => Self::Scrolling, 6 => Self::OutsideWind, 7 => Self::Outside, 8 => Self::OutsideUnknown, 9 => Self::Waterway, _ => { // log::warn!("Unknown background type: {}", val); Self::Black } } } } #[derive(Debug, Copy, Clone, PartialEq)] pub enum PxPackScroll { Normal, ThreeQuarters, Half, Quarter, Eighth, Zero, HThreeQuarters, HHalf, HQuarter, V0Half, } impl From for PxPackScroll { fn from(val: u8) -> Self { match val { 0 => PxPackScroll::Normal, 1 => PxPackScroll::ThreeQuarters, 2 => PxPackScroll::Half, 3 => PxPackScroll::Quarter, 4 => PxPackScroll::Eighth, 5 => PxPackScroll::Zero, 6 => PxPackScroll::HThreeQuarters, 7 => PxPackScroll::HHalf, 8 => PxPackScroll::HQuarter, 9 => PxPackScroll::V0Half, _ => PxPackScroll::Normal, } } } impl PxPackScroll { pub fn transform_camera_pos(self, x: f32, y: f32) -> (f32, f32) { match self { PxPackScroll::Normal => (x, y), PxPackScroll::ThreeQuarters => (x * 0.75, y * 0.75), PxPackScroll::Half => (x * 0.5, y * 0.5), PxPackScroll::Quarter => (x * 0.25, y * 0.25), PxPackScroll::Eighth => (x * 0.125, y * 0.125), PxPackScroll::Zero => (0.0, 0.0), PxPackScroll::HThreeQuarters => (x * 0.75, y), PxPackScroll::HHalf => (x * 0.5, y), PxPackScroll::HQuarter => (x * 0.25, y), PxPackScroll::V0Half => (x, y) // ??? } } } #[derive(Debug, Clone)] pub struct PxPackStageData { pub tileset_fg: String, pub tileset_mg: String, pub tileset_bg: String, pub scroll_fg: PxPackScroll, pub scroll_mg: PxPackScroll, pub scroll_bg: PxPackScroll, pub size_fg: (u16, u16), pub size_mg: (u16, u16), pub size_bg: (u16, u16), pub offset_mg: u32, pub offset_bg: u32, } #[derive(Debug)] pub struct StageData { pub name: String, pub map: String, pub boss_no: u8, pub tileset: Tileset, pub pxpack_data: Option, pub background: Background, pub background_type: BackgroundType, pub background_color: Color, pub npc1: NpcType, pub npc2: NpcType, } impl Clone for StageData { fn clone(&self) -> Self { StageData { name: self.name.clone(), map: self.map.clone(), boss_no: self.boss_no, tileset: self.tileset.clone(), pxpack_data: self.pxpack_data.clone(), background: self.background.clone(), background_type: self.background_type, background_color: self.background_color, npc1: self.npc1.clone(), npc2: self.npc2.clone(), } } } const NXENGINE_BACKDROPS: [&str; 15] = [ "bk0", "bkBlue", "bkGreen", "bkBlack", "bkGard", "bkMaze", "bkGray", "bkRed", "bkWater", "bkMoon", "bkFog", "bkFall", "bkLight", "bkSunset", "bkHellish", ]; const NXENGINE_TILESETS: [&str; 22] = [ "0", "Pens", "Eggs", "EggX", "EggIn", "Store", "Weed", "Barr", "Maze", "Sand", "Mimi", "Cave", "River", "Gard", "Almond", "Oside", "Cent", "Jail", "White", "Fall", "Hell", "Labo", ]; const NXENGINE_NPCS: [&str; 34] = [ "Guest", "0", "Eggs1", "Ravil", "Weed", "Maze", "Sand", "Omg", "Cemet", "Bllg", "Plant", "Frog", "Curly", "Stream", "IronH", "Toro", "X", "Dark", "Almo1", "Eggs2", "TwinD", "Moon", "Cent", "Heri", "Red", "Miza", "Dr", "Almo2", "Kings", "Hell", "Press", "Priest", "Ballos", "Island", ]; fn zero_index(s: &[u8]) -> usize { s.iter().position(|&c| c == b'\0').unwrap_or(s.len()) } fn from_shift_jis(s: &[u8]) -> String { let mut cursor = Cursor::new(s); let mut chars = Vec::new(); let mut bytes = s.len() as u32; while bytes > 0 { let (consumed, chr) = read_cur_shift_jis(&mut cursor, bytes); chars.push(chr); bytes -= consumed; } chars.iter().collect() } impl StageData { pub fn load_stage_table(ctx: &mut Context, root: &str) -> GameResult> { let stage_tbl_path = [root, "stage.tbl"].join(""); let stage_sect_path = [root, "stage.sect"].join(""); let mrmap_bin_path = [root, "mrmap.bin"].join(""); let stage_dat_path = [root, "stage.dat"].join(""); if filesystem::exists(ctx, &stage_tbl_path) { // Cave Story+ stage table. let mut stages = Vec::new(); info!("Loading Cave Story+ stage table from {}", &stage_tbl_path); let mut data = Vec::new(); filesystem::open(ctx, stage_tbl_path)?.read_to_end(&mut data)?; let count = data.len() / 0xe5; let mut f = Cursor::new(data); for _ in 0..count { let mut ts_buf = vec![0u8; 0x20]; let mut map_buf = vec![0u8; 0x20]; let mut back_buf = vec![0u8; 0x20]; let mut npc1_buf = vec![0u8; 0x20]; let mut npc2_buf = vec![0u8; 0x20]; let mut name_jap_buf = vec![0u8; 0x20]; let mut name_buf = vec![0u8; 0x20]; f.read_exact(&mut ts_buf)?; f.read_exact(&mut map_buf)?; let bg_type = f.read_u32::()? as u8; f.read_exact(&mut back_buf)?; f.read_exact(&mut npc1_buf)?; f.read_exact(&mut npc2_buf)?; let boss_no = f.read_u8()?; f.read_exact(&mut name_jap_buf)?; f.read_exact(&mut name_buf)?; let tileset = from_shift_jis(&ts_buf[0..zero_index(&ts_buf)]); let map = from_shift_jis(&map_buf[0..zero_index(&map_buf)]); let background = from_shift_jis(&back_buf[0..zero_index(&back_buf)]); let npc1 = from_shift_jis(&npc1_buf[0..zero_index(&npc1_buf)]); let npc2 = from_shift_jis(&npc2_buf[0..zero_index(&npc2_buf)]); let name = from_shift_jis(&name_buf[0..zero_index(&name_buf)]); let stage = StageData { name: name.clone(), map: map.clone(), boss_no, tileset: Tileset::new(&tileset), pxpack_data: None, background: Background::new(&background), background_type: BackgroundType::from(bg_type), background_color: Color::from_rgb(0, 0, 32), npc1: NpcType::new(&npc1), npc2: NpcType::new(&npc2), }; stages.push(stage); } return Ok(stages); } else if filesystem::exists(ctx, &stage_sect_path) { // Cave Story freeware executable dump. let mut stages = Vec::new(); info!("Loading Cave Story freeware exe dump stage table from {}", &stage_sect_path); let mut data = Vec::new(); filesystem::open(ctx, stage_sect_path)?.read_to_end(&mut data)?; let count = data.len() / 0xc8; let mut f = Cursor::new(data); for _ in 0..count { let mut ts_buf = vec![0u8; 0x20]; let mut map_buf = vec![0u8; 0x20]; let mut back_buf = vec![0u8; 0x20]; let mut npc1_buf = vec![0u8; 0x20]; let mut npc2_buf = vec![0u8; 0x20]; let mut name_buf = vec![0u8; 0x20]; f.read_exact(&mut ts_buf)?; f.read_exact(&mut map_buf)?; let bg_type = f.read_u32::()? as u8; f.read_exact(&mut back_buf)?; f.read_exact(&mut npc1_buf)?; f.read_exact(&mut npc2_buf)?; let boss_no = f.read_u8()?; f.read_exact(&mut name_buf)?; // alignment { let mut lol = [0u8; 3]; let _ = f.read(&mut lol)?; } let tileset = from_shift_jis(&ts_buf[0..zero_index(&ts_buf)]); let map = from_shift_jis(&map_buf[0..zero_index(&map_buf)]); let background = from_shift_jis(&back_buf[0..zero_index(&back_buf)]); let npc1 = from_shift_jis(&npc1_buf[0..zero_index(&npc1_buf)]); let npc2 = from_shift_jis(&npc2_buf[0..zero_index(&npc2_buf)]); let name = from_shift_jis(&name_buf[0..zero_index(&name_buf)]); let stage = StageData { name: name.clone(), map: map.clone(), boss_no, tileset: Tileset::new(&tileset), pxpack_data: None, background: Background::new(&background), background_type: BackgroundType::from(bg_type), background_color: Color::from_rgb(0, 0, 32), npc1: NpcType::new(&npc1), npc2: NpcType::new(&npc2), }; stages.push(stage); } return Ok(stages); } else if filesystem::exists(ctx, &mrmap_bin_path) { // Moustache Rider stage table let mut stages = Vec::new(); info!("Loading Moustache Rider stage table from {}", &mrmap_bin_path); let mut data = Vec::new(); let mut fh = filesystem::open(ctx, &mrmap_bin_path)?; let count = fh.read_u32::()?; fh.read_to_end(&mut data)?; if data.len() < count as usize * 0x74 { return Err(ResourceLoadError( "Specified stage table size is bigger than actual number of entries.".to_string(), )); } let mut f = Cursor::new(data); for _ in 0..count { let mut ts_buf = vec![0u8; 0x10]; let mut map_buf = vec![0u8; 0x10]; let mut back_buf = vec![0u8; 0x10]; let mut npc1_buf = vec![0u8; 0x10]; let mut npc2_buf = vec![0u8; 0x10]; let mut name_buf = vec![0u8; 0x22]; f.read_exact(&mut ts_buf)?; f.read_exact(&mut map_buf)?; let bg_type = f.read_u8()?; f.read_exact(&mut back_buf)?; f.read_exact(&mut npc1_buf)?; f.read_exact(&mut npc2_buf)?; let boss_no = f.read_u8()?; f.read_exact(&mut name_buf)?; let tileset = from_shift_jis(&ts_buf[0..zero_index(&ts_buf)]); let map = from_shift_jis(&map_buf[0..zero_index(&map_buf)]); let background = from_shift_jis(&back_buf[0..zero_index(&back_buf)]); let npc1 = from_shift_jis(&npc1_buf[0..zero_index(&npc1_buf)]); let npc2 = from_shift_jis(&npc2_buf[0..zero_index(&npc2_buf)]); let name = from_shift_jis(&name_buf[0..zero_index(&name_buf)]); let stage = StageData { name: name.clone(), map: map.clone(), boss_no, tileset: Tileset::new(&tileset), pxpack_data: None, background: Background::new(&background), background_type: BackgroundType::from(bg_type), background_color: Color::from_rgb(0, 0, 32), npc1: NpcType::new(&npc1), npc2: NpcType::new(&npc2), }; stages.push(stage); } return Ok(stages); } else if filesystem::exists(ctx, &stage_dat_path) { let mut stages = Vec::new(); info!("Loading NXEngine stage table from {}", &stage_dat_path); let mut data = Vec::new(); let mut fh = filesystem::open(ctx, &stage_dat_path)?; let count = fh.read_u8()? as usize; fh.read_to_end(&mut data)?; if data.len() < count * 0x49 { return Err(ResourceLoadError( "Specified stage table size is bigger than actual number of entries.".to_string(), )); } let mut f = Cursor::new(data); for _ in 0..count { let mut map_buf = vec![0u8; 0x20]; let mut name_buf = vec![0u8; 0x23]; f.read_exact(&mut map_buf)?; f.read_exact(&mut name_buf)?; let tileset_id = f.read_u8()? as usize; let bg_id = f.read_u8()? as usize; let bg_type = f.read_u8()?; let boss_no = f.read_u8()?; let npc1 = f.read_u8()? as usize; let npc2 = f.read_u8()? as usize; let map = from_utf8(&map_buf) .map_err(|_| ResourceLoadError("UTF-8 error in map field".to_string()))? .trim_matches('\0') .to_owned(); let name = from_utf8(&name_buf) .map_err(|_| ResourceLoadError("UTF-8 error in name field".to_string()))? .trim_matches('\0') .to_owned(); let stage = StageData { name: name.clone(), map: map.clone(), boss_no, tileset: Tileset::new(NXENGINE_TILESETS.get(tileset_id).unwrap_or(&"0")), pxpack_data: None, background: Background::new(NXENGINE_BACKDROPS.get(bg_id).unwrap_or(&"0")), background_type: BackgroundType::from(bg_type), background_color: Color::from_rgb(0, 0, 32), npc1: NpcType::new(NXENGINE_NPCS.get(npc1).unwrap_or(&"0")), npc2: NpcType::new(NXENGINE_NPCS.get(npc2).unwrap_or(&"0")), }; stages.push(stage); } return Ok(stages); } Err(ResourceLoadError("No stage table found.".to_string())) } } pub struct Stage { pub map: Map, pub data: StageData, } impl Stage { pub fn load(root: &str, data: &StageData, ctx: &mut Context) -> GameResult { let mut data = data.clone(); if let Ok(pxpack_file) = filesystem::open(ctx, [root, "Stage/", &data.map, ".pxpack"].join("")) { let map = Map::load_pxpack(pxpack_file, root, &mut data, ctx)?; let stage = Self { map, data }; Ok(stage) } else { let map_file = filesystem::open(ctx, [root, "Stage/", &data.map, ".pxm"].join(""))?; let attrib_file = filesystem::open(ctx, [root, "Stage/", &data.tileset.name, ".pxa"].join(""))?; let map = Map::load_pxm(map_file, attrib_file)?; let stage = Self { map, data }; Ok(stage) } } pub fn load_text_script( &self, root: &str, constants: &EngineConstants, ctx: &mut Context, ) -> GameResult { let tsc_file = filesystem::open(ctx, [root, "Stage/", &self.data.map, ".tsc"].join(""))?; let text_script = TextScript::load_from(tsc_file, constants)?; Ok(text_script) } pub fn load_npcs(&self, root: &str, ctx: &mut Context) -> GameResult> { let pxe_file = filesystem::open(ctx, [root, "Stage/", &self.data.map, ".pxe"].join(""))?; let npc_data = NPCData::load_from(pxe_file)?; Ok(npc_data) } /// Returns map tile from foreground layer. pub fn tile_at(&self, x: usize, y: usize) -> u8 { if let Some(&tile) = self.map.tiles.get(y.wrapping_mul(self.map.width as usize).wrapping_add(x)) { tile } else { 0 } } /// Changes map tile on foreground layer. Returns true if smoke should be emitted pub fn change_tile(&mut self, x: usize, y: usize, tile_type: u8) -> bool { if let Some(ptr) = self.map.tiles.get_mut(y.wrapping_mul(self.map.width as usize).wrapping_add(x)) { if *ptr != tile_type { *ptr = tile_type; return true; } } false } }