detect and extract vanilla exe resources

This commit is contained in:
Sallai József 2022-07-16 15:33:31 +03:00
parent 2177382b5a
commit 444539405a
5 changed files with 346 additions and 0 deletions

View File

@ -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"] }

127
src/exe_parser.rs Normal file
View File

@ -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<u8>,
pub name: String,
}
impl DataFile {
pub fn from(name: String, bytes: Vec<u8>) -> Self {
Self { name, bytes }
}
}
#[derive(Debug)]
pub struct ExeResourceDirectory {
pub name: String,
pub data_files: Vec<DataFile>,
}
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<u8>) -> GameResult<Self> {
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<ExeResourceDirectory> {
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<ExeResourceDirectory> {
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<Option<Range<u32>>> {
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);
}
}
}
}

View File

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

View File

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

207
src/vanilla.rs Normal file
View File

@ -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<u8>,
}
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<Self> {
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::<LE>(bitmap.bytes.len() as u32 + 0xE)?; // Size of BMP file
file.write_u32::<LE>(0)?; // unused null bytes
file.write_u32::<LE>(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(())
}
}