detect and extract vanilla exe resources
This commit is contained in:
parent
2177382b5a
commit
444539405a
|
@ -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"] }
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue