diff --git a/Cargo.toml b/Cargo.toml index 273618c..1c06b37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ lua-ffi = { git = "https://github.com/doukutsu-rs/lua-ffi.git", rev = "e0b2ff596 num-derive = "0.3.2" num-traits = "0.2.12" paste = "1.0.0" +pelite = "0.9.1" sdl2 = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "95bcf63768abf422527f86da41da910649b9fcc9", optional = true, features = ["unsafe_textures", "bundled", "static-link"] } sdl2-sys = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "95bcf63768abf422527f86da41da910649b9fcc9", optional = true, features = ["bundled", "static-link"] } serde = { version = "1", features = ["derive"] } diff --git a/src/exe_parser.rs b/src/exe_parser.rs new file mode 100644 index 0000000..0ac2668 --- /dev/null +++ b/src/exe_parser.rs @@ -0,0 +1,127 @@ +use std::ops::Range; + +use pelite::{ + image::RT_BITMAP, + pe32::{headers::SectionHeaders, Pe, PeFile}, + resources::{Directory, Entry, Name, Resources}, +}; + +use crate::framework::error::{GameError::ParseError, GameResult}; + +#[derive(Debug)] +pub struct DataFile { + pub bytes: Vec, + pub name: String, +} + +impl DataFile { + pub fn from(name: String, bytes: Vec) -> Self { + Self { name, bytes } + } +} + +#[derive(Debug)] +pub struct ExeResourceDirectory { + pub name: String, + pub data_files: Vec, +} + +impl ExeResourceDirectory { + pub fn new(name: String) -> Self { + Self { name, data_files: Vec::new() } + } +} + +pub struct ExeParser<'a> { + pub resources: Resources<'a>, + pub section_headers: Box<&'a SectionHeaders>, +} + +impl<'a> ExeParser<'a> { + pub fn from(file: &'a Vec) -> GameResult { + let pe = PeFile::from_bytes(file); + + return match pe { + Ok(pe) => { + let resources = pe.resources(); + + if resources.is_err() { + return Err(ParseError("Failed to parse resources.".to_string())); + } + + let section_headers = pe.section_headers(); + + Ok(Self { resources: resources.unwrap(), section_headers: Box::new(section_headers) }) + } + Err(_) => Err(ParseError("Failed to parse PE file".to_string())), + }; + } + + pub fn get_resource_dir(&self, name: String) -> GameResult { + let mut dir_data = ExeResourceDirectory::new(name.to_owned()); + + let path = format!("/{}", name.to_owned()); + let dir = self.resources.find_dir(&path); + + return match dir { + Ok(dir) => { + self.read_dir(dir, &mut dir_data, "unknown".to_string()); + Ok(dir_data) + } + Err(_) => return Err(ParseError("Failed to find resource directory.".to_string())), + }; + } + + pub fn get_bitmap_dir(&self) -> GameResult { + let mut dir_data = ExeResourceDirectory::new("Bitmap".to_string()); + + let root = self.resources.root().unwrap(); + let dir = root.get_dir(Name::Id(RT_BITMAP.into())); + + return match dir { + Ok(dir) => { + self.read_dir(dir, &mut dir_data, "unknown".to_string()); + Ok(dir_data) + } + Err(_) => return Err(ParseError("Failed to open bitmap directory.".to_string())), + }; + } + + pub fn get_named_section_byte_range(&self, name: String) -> GameResult>> { + let section_header = self.section_headers.by_name(name.as_bytes()); + return match section_header { + Some(section_header) => Ok(Some(section_header.file_range())), + None => Ok(None), + }; + } + + fn read_dir(&self, directory: Directory, dir_data: &mut ExeResourceDirectory, last_dir_name: String) { + for dir in directory.entries() { + let raw_entry = dir.entry(); + + if raw_entry.is_err() { + continue; + } + + if let Entry::Directory(entry) = raw_entry.unwrap() { + let dir_name = dir.name(); + let name = match dir_name { + Ok(name) => name.to_string(), + Err(_) => last_dir_name.to_owned(), + }; + self.read_dir(entry, dir_data, name); + } + + if let Entry::DataEntry(entry) = raw_entry.unwrap() { + let entry_bytes = entry.bytes(); + if entry_bytes.is_err() { + continue; + } + + let bytes = entry_bytes.unwrap(); + let data_file = DataFile::from(last_dir_name.to_owned(), bytes.to_vec()); + dir_data.data_files.push(data_file); + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 299af27..9451f32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ mod editor; mod encoding; mod engine_constants; mod entity; +mod exe_parser; mod frame; mod framework; #[cfg(feature = "hooks")] @@ -65,6 +66,7 @@ mod shared_game_state; mod sound; mod stage; mod texture_set; +mod vanilla; mod weapon; pub struct LaunchOptions { diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 7a076ee..d0dc4dc 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -36,6 +36,7 @@ use crate::settings::Settings; use crate::sound::SoundManager; use crate::stage::StageData; use crate::texture_set::TextureSet; +use crate::vanilla::VanillaExtractor; #[derive(PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)] pub enum TimingMode { @@ -331,6 +332,14 @@ impl SharedGameState { let settings = Settings::load(ctx)?; let mod_requirements = ModRequirements::load(ctx)?; + let vanilla_extractor = VanillaExtractor::from(ctx, "Doukutsu.exe".to_string()); + if vanilla_extractor.is_some() { + let result = vanilla_extractor.unwrap().extract_data(); + if result.is_err() { + error!("Failed to extract vanilla data: {}", result.unwrap_err()); + } + } + if filesystem::exists(ctx, "/base/lighting.tbl") { info!("Cave Story+ (Switch) data files detected."); ctx.size_hint = (854, 480); diff --git a/src/vanilla.rs b/src/vanilla.rs new file mode 100644 index 0000000..a26e7e5 --- /dev/null +++ b/src/vanilla.rs @@ -0,0 +1,207 @@ +use std::{ + env, + io::{Read, Write}, + ops::Range, + path::PathBuf, +}; + +use byteorder::{WriteBytesExt, LE}; + +use crate::{ + exe_parser::ExeParser, + framework::{ + context::Context, + error::{GameError::ParseError, GameResult}, + filesystem, + }, +}; + +pub struct VanillaExtractor { + exe_buffer: Vec, +} + +const VANILLA_STAGE_COUNT: u32 = 95; +const VANILLA_STAGE_ENTRY_SIZE: u32 = 0xC8; +const VANILLA_STAGE_OFFSET: u32 = 0x937B0; +const VANILLA_STAGE_TABLE_SIZE: u32 = VANILLA_STAGE_COUNT * VANILLA_STAGE_ENTRY_SIZE; + +impl VanillaExtractor { + pub fn from(ctx: &mut Context, exe_name: String) -> Option { + let mut vanilla_exe_path = env::current_exe().unwrap(); + vanilla_exe_path.pop(); + vanilla_exe_path.push(exe_name); + + if !vanilla_exe_path.is_file() { + return None; + } + + log::info!("Found vanilla game executable, attempting to extract resources."); + + if filesystem::exists(ctx, "/data/stage.sect") { + log::info!("Vanilla resources are already extracted, not proceeding."); + return None; + } + + let file = std::fs::File::open(vanilla_exe_path); + if file.is_err() { + log::error!("Failed to open vanilla game executable: {}", file.unwrap_err()); + return None; + } + + let mut exe_buffer = Vec::new(); + let result = file.unwrap().read_to_end(&mut exe_buffer); + if result.is_err() { + log::error!("Failed to read vanilla game executable: {}", result.unwrap_err()); + return None; + } + + Some(Self { exe_buffer }) + } + + pub fn extract_data(&self) -> GameResult { + let parser = ExeParser::from(&self.exe_buffer); + if parser.is_err() { + return Err(ParseError("Failed to create vanilla parser.".to_string())); + } + + let parser = parser.unwrap(); + + self.extract_organya(&parser)?; + self.extract_bitmaps(&parser)?; + self.extract_stage_table(&parser)?; + + Ok(()) + } + + fn deep_create_dir_if_not_exists(&self, path: PathBuf) -> GameResult { + if path.is_dir() { + return Ok(()); + } + + let result = std::fs::create_dir_all(path); + if result.is_err() { + return Err(ParseError(format!("Failed to create directory structure: {}", result.unwrap_err()))); + } + + Ok(()) + } + + fn extract_organya(&self, parser: &ExeParser) -> GameResult { + let orgs = parser.get_resource_dir("ORG".to_string()); + + if orgs.is_err() { + return Err(ParseError("Failed to retrieve Organya resource directory.".to_string())); + } + + for org in orgs.unwrap().data_files { + let mut org_path = env::current_exe().unwrap(); + org_path.pop(); + org_path.push("data/Org/"); + + if self.deep_create_dir_if_not_exists(org_path.clone()).is_err() { + return Err(ParseError("Failed to create directory structure.".to_string())); + } + + org_path.push(format!("{}.org", org.name)); + + let mut org_file = match std::fs::File::create(org_path) { + Ok(file) => file, + Err(_) => { + return Err(ParseError("Failed to create organya file.".to_string())); + } + }; + + let result = org_file.write_all(&org.bytes); + if result.is_err() { + return Err(ParseError("Failed to write organya file.".to_string())); + } + + log::info!("Extracted organya file: {}", org.name); + } + + Ok(()) + } + + fn extract_bitmaps(&self, parser: &ExeParser) -> GameResult { + let bitmaps = parser.get_bitmap_dir(); + + if bitmaps.is_err() { + return Err(ParseError("Failed to retrieve bitmap directory.".to_string())); + } + + for bitmap in bitmaps.unwrap().data_files { + let mut data_path = env::current_exe().unwrap(); + data_path.pop(); + data_path.push("data/"); + + if self.deep_create_dir_if_not_exists(data_path.clone()).is_err() { + return Err(ParseError("Failed to create data directory structure.".to_string())); + } + + data_path.push(format!("{}.pbm", bitmap.name)); + + let file = std::fs::File::create(data_path); + if file.is_err() { + return Err(ParseError("Failed to create bitmap file.".to_string())); + } + + let mut file = file.unwrap(); + + file.write_u8(0x42)?; // B + file.write_u8(0x4D)?; // M + file.write_u32::(bitmap.bytes.len() as u32 + 0xE)?; // Size of BMP file + file.write_u32::(0)?; // unused null bytes + file.write_u32::(0x76)?; // Bitmap data offset (hardcoded for now, might wanna get the actual offset) + + let result = file.write_all(&bitmap.bytes); + if result.is_err() { + return Err(ParseError("Failed to write bitmap file.".to_string())); + } + + log::info!("Extracted bitmap file: {}", bitmap.name); + } + + Ok(()) + } + + fn extract_stage_table(&self, parser: &ExeParser) -> GameResult { + let range = parser.get_named_section_byte_range(".csmap".to_string()); + if range.is_err() { + return Err(ParseError("Failed to retrieve stage table from executable.".to_string())); + } + + let range = match range.unwrap() { + Some(range) => range, + None => Range { start: VANILLA_STAGE_OFFSET, end: VANILLA_STAGE_OFFSET + VANILLA_STAGE_TABLE_SIZE }, + }; + + let start = range.start as usize; + let end = range.end as usize; + + let byte_slice = &self.exe_buffer[start..end]; + + let mut stage_tbl_path = env::current_exe().unwrap(); + stage_tbl_path.pop(); + stage_tbl_path.push("data/"); + + if self.deep_create_dir_if_not_exists(stage_tbl_path.clone()).is_err() { + return Err(ParseError("Failed to create data directory structure.".to_string())); + } + + stage_tbl_path.push("stage.sect"); + + let mut stage_tbl_file = match std::fs::File::create(stage_tbl_path) { + Ok(file) => file, + Err(_) => { + return Err(ParseError("Failed to create stage table file.".to_string())); + } + }; + + let result = stage_tbl_file.write_all(byte_slice); + if result.is_err() { + return Err(ParseError("Failed to write to stage table file.".to_string())); + } + + Ok(()) + } +}