add gci_quest_extract tool
This commit is contained in:
parent
bb82496b65
commit
53c0a7f537
|
@ -2,7 +2,8 @@
|
|||
|
||||
members = [
|
||||
"psoutils",
|
||||
"psogc_quest_tool"
|
||||
"psogc_quest_tool",
|
||||
"gci_quest_extract"
|
||||
]
|
||||
|
||||
|
||||
|
|
14
gci_quest_extract/Cargo.toml
Normal file
14
gci_quest_extract/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "gci_quest_extract"
|
||||
version = "0.1.0"
|
||||
authors = ["gered <gered@blarg.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.40"
|
||||
byteorder = "1.4.3"
|
||||
|
||||
[dependencies.psoutils]
|
||||
path = "../psoutils"
|
121
gci_quest_extract/src/gci.rs
Normal file
121
gci_quest_extract/src/gci.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
use psoutils::bytes::ReadFixedLengthByteArray;
|
||||
use psoutils::quest::bin::QuestBin;
|
||||
use psoutils::quest::dat::QuestDat;
|
||||
use psoutils::quest::Quest;
|
||||
|
||||
// see https://github.com/suloku/gcmm/blob/master/source/gci.h for detailed GCI file format header
|
||||
// we will not be re-defining that struct here, since we're only interested in a handful of fields
|
||||
|
||||
const GCI_HEADER_SIZE: usize = 64;
|
||||
const CARD_FILE_HEADER: usize = 0x2040;
|
||||
const DATA_START_OFFSET: usize = GCI_HEADER_SIZE + CARD_FILE_HEADER;
|
||||
|
||||
fn extract_quest_data(path: &Path) -> Result<Box<[u8]>> {
|
||||
let mut file = File::open(path)?;
|
||||
|
||||
let gamecode: [u8; 4] = file.read_bytes()?;
|
||||
if &gamecode != b"GPOJ" && &gamecode != b"GPOE" && &gamecode != b"GPOP" {
|
||||
return Err(anyhow!(
|
||||
"GCI header 'gamecode' field does not match any expected string: {:02x?}",
|
||||
gamecode
|
||||
));
|
||||
}
|
||||
|
||||
let company: [u8; 2] = file.read_bytes()?;
|
||||
if &company != b"8P" {
|
||||
return Err(anyhow!(
|
||||
"GCI header 'company' field is not the expected value: {:02x?}",
|
||||
company
|
||||
));
|
||||
}
|
||||
|
||||
// move past the majority of GCI header and the actual Gamecube memory card header
|
||||
file.seek(SeekFrom::Start(DATA_START_OFFSET as u64))?;
|
||||
|
||||
// this "size" value actually accounts for an extra dword value that we do not care about
|
||||
let data_size = file.read_u32::<BigEndian>()? - 4;
|
||||
|
||||
// move past the remaining bits of the header to the actual start of the quest data
|
||||
file.seek(SeekFrom::Current(20))?;
|
||||
|
||||
// there will be remaining junk after the data which we probably don't want, so only read
|
||||
// the exact amount of bytes indicated in the header
|
||||
let mut buffer = vec![0u8; data_size as usize];
|
||||
file.read_exact(&mut buffer)?;
|
||||
|
||||
Ok(buffer.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn load_quest_from_gci_files(gci1: &Path, gci2: &Path) -> Result<Quest> {
|
||||
let gci1_bytes = extract_quest_data(gci1).context(format!(
|
||||
"Failed to extract quest data from: {}",
|
||||
gci1.to_string_lossy()
|
||||
))?;
|
||||
let gci2_bytes = extract_quest_data(gci2).context(format!(
|
||||
"Failed to extract quest data from: {}",
|
||||
gci2.to_string_lossy()
|
||||
))?;
|
||||
|
||||
// now try to figure out which is the .bin and which is the .dat
|
||||
let bin: QuestBin;
|
||||
let dat: QuestDat;
|
||||
if let Ok(loaded) = QuestBin::from_compressed_bytes(gci1_bytes.as_ref()) {
|
||||
bin = loaded;
|
||||
dat = QuestDat::from_compressed_bytes(gci2_bytes.as_ref())
|
||||
.context("Failed to load second GCI file as quest .dat")?;
|
||||
} else if let Ok(loaded) = QuestDat::from_compressed_bytes(gci1_bytes.as_ref()) {
|
||||
dat = loaded;
|
||||
bin = QuestBin::from_compressed_bytes(gci2_bytes.as_ref())
|
||||
.context("Failed to load second GCI file as quest .bin")?;
|
||||
} else {
|
||||
return Err(anyhow!("Unable to load first GCI file as either a quest .bin or .dat file. It might not contain quest data, or it might not be pre-decrypted, or it might be corrupted."));
|
||||
}
|
||||
|
||||
Ok(Quest { bin, dat })
|
||||
}
|
||||
|
||||
pub fn extract_to_bindat(
|
||||
gci1: &Path,
|
||||
gci2: &Path,
|
||||
output_bin: &Path,
|
||||
output_dat: &Path,
|
||||
) -> Result<()> {
|
||||
println!(
|
||||
"Reading quest data from GCI files:\n - {}\n - {}",
|
||||
gci1.to_string_lossy(),
|
||||
gci2.to_string_lossy()
|
||||
);
|
||||
|
||||
let mut quest = load_quest_from_gci_files(gci1, gci2)?;
|
||||
|
||||
println!("Loaded quest .bin and .dat data successfully.\n");
|
||||
println!(
|
||||
"{}\n{}\n",
|
||||
quest.display_bin_info(),
|
||||
quest.display_dat_info()
|
||||
);
|
||||
|
||||
if quest.is_download() {
|
||||
println!("Turning 'download' flag off before saving.");
|
||||
quest.set_is_download(false);
|
||||
}
|
||||
|
||||
println!(
|
||||
"Saving quest as PRS-compressed bin/dat files:\n .bin file: {}\n .dat file: {}",
|
||||
output_bin.to_string_lossy(),
|
||||
output_dat.to_string_lossy()
|
||||
);
|
||||
|
||||
quest
|
||||
.to_compressed_bindat_files(output_bin, output_dat)
|
||||
.context("Failed to save quest to bin/dat files")?;
|
||||
|
||||
Ok(())
|
||||
}
|
1
gci_quest_extract/src/lib.rs
Normal file
1
gci_quest_extract/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod gci;
|
35
gci_quest_extract/src/main.rs
Normal file
35
gci_quest_extract/src/main.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use gci_quest_extract::gci::extract_to_bindat;
|
||||
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn display_banner() {
|
||||
println!("gci_quest_extract v{}", VERSION);
|
||||
}
|
||||
|
||||
fn display_help() {
|
||||
println!("Tool for extracting PSO Gamecube quests out of pre-decrypted .gci files.\n");
|
||||
println!("USAGE: gci_quest_extract <quest_1.gci> <quest_2.gci> <output.bin> <output.dat>");
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 5 {
|
||||
display_help();
|
||||
} else {
|
||||
let gci1_path = Path::new(&args[1]);
|
||||
let gci2_path = Path::new(&args[2]);
|
||||
let output_bin_path = Path::new(&args[3]);
|
||||
let output_dat_path = Path::new(&args[4]);
|
||||
extract_to_bindat(gci1_path, gci2_path, output_bin_path, output_dat_path)
|
||||
.context("Failed to extract quest from GCI files")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in a new issue