Skip to content

Instantly share code, notes, and snippets.

@0xa
Created May 9, 2018 18:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 0xa/0b1bac9f2aaad6c9ac3d545111fba131 to your computer and use it in GitHub Desktop.
Save 0xa/0b1bac9f2aaad6c9ac3d545111fba131 to your computer and use it in GitHub Desktop.
/*
Ok(
JsavFile {
version: 115,
tokens: [
"uv",
"landmark",
"state",
"globalentity_t",
"mapName",
"levelName",
"originMapName",
"m_list",
"GameHeader",
"elems",
"GLOBAL",
"counter",
"name",
"comment"
],
map_name: "d1_town_04",
save_name: "#HL2_Chapter6_Title 052:37",
content: [
JsavValvEntry { name: "d1_town_02a.hl1", data: [429164 bytes] },
JsavValvEntry { name: "d1_town_02a.hl2", data: [122170 bytes] },
JsavValvEntry { name: "d1_town_02a.hl3", data: [56 bytes] },
JsavValvEntry { name: "d1_town_04.hl1", data: [347028 bytes] },
JsavValvEntry { name: "d1_town_04.hl2", data: [83718 bytes] },
JsavValvEntry { name: "d1_town_04.hl3", data: [4 bytes] }
]
}
)
*/
#[macro_use] extern crate structopt;
#[macro_use] extern crate quick_error;
extern crate byteorder;
use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt};
use std::path::PathBuf;
use structopt::StructOpt;
extern crate pretty_env_logger;
#[macro_use] extern crate log;
extern crate itertools;
use itertools::Itertools;
use std::io::Cursor;
use std::fs::File;
use std::io::prelude::*;
use std::fmt;
quick_error! {
#[derive(Debug)]
pub enum DecodeError {
Io(err: std::io::Error) {
cause(err)
description(err.description())
from()
}
Incomplete(len: usize, minimum: usize) {
display("Incomplete data: {} bytes, expected at least {}", len, minimum)
}
InvalidFileType(found_header: Vec<u8>, expected: Vec<u8>) {
display("Found header {:?}, expected {:?}", found_header, expected)
}
Other(err: Box<std::error::Error>) {
cause(&**err)
description(err.description())
}
}
}
fn check_input_size(input: &[u8], required: usize) -> Result<(), DecodeError> {
if input.len() < required {
return Err(DecodeError::Incomplete(input.len(), required));
}
Ok(())
}
fn get_bytes(cursor: &mut Cursor<&[u8]>, l: usize) -> Result<Vec<u8>, DecodeError> {
let mut r = vec![0; l];
cursor.read_exact(&mut r)?;
Ok(r)
}
fn get_string(cursor: &mut Cursor<&[u8]>, l: usize) -> Result<String, DecodeError> {
let bytes = get_bytes(cursor, l)?;
let end = bytes.iter().take_while(|&&x| x != 0).count();
let s = String::from_utf8_lossy(&bytes[0..end]).to_string();
Ok(s)
}
fn get_u32(cursor: &mut Cursor<&[u8]>) -> Result<u32, DecodeError> {
let bytes_vec = get_bytes(cursor, 4)?;
let mut bytes: &[u8] = bytes_vec.as_ref();
Ok(bytes.read_u32::<LittleEndian>()?)
}
/// a VALV file entry, in a JSAV file.
#[derive(Clone)]
struct JsavValvEntry {
name: String,
data: Vec<u8>,
}
impl fmt::Debug for JsavValvEntry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "JsavValvEntry {{ name: {:?}, data: [{} bytes] }}", self.name, self.data.len())
}
}
#[derive(Debug, Clone)]
struct JsavFile {
version: u32,
tokens: Vec<String>,
map_name: String,
save_name: String,
content: Vec<JsavValvEntry>,
}
impl JsavFile {
pub fn load(data: &[u8]) -> Result<Self, DecodeError> {
check_input_size(data, 20)?;
let header = &data[0..4];
if header != b"JSAV" {
return Err(DecodeError::InvalidFileType(header.to_vec(), b"JSAV".to_vec()));
}
let mut cursor = Cursor::new(&data[4 .. ]);
let version = get_u32(&mut cursor)?;
trace!("JSAV: Version 0x{:04x} (known: 0x0073)", version);
let size = get_u32(&mut cursor)? as usize;
let token_count = get_u32(&mut cursor)? as usize;
let token_size = get_u32(&mut cursor)? as usize;
trace!("JSAV: size={}, token_count={}, token_size={}", size, token_count, token_size);
let token_buffer = get_bytes(&mut cursor, token_size)?;
let tokens: Vec<String> = token_buffer.iter()
.group_by(|&&c| c == 0)
.into_iter()
.filter(|(key, _)| *key == false)
.map(|(_, group)| {
let bytes: Vec<u8> = group.cloned().collect();
String::from_utf8_lossy(&bytes).into_owned()
})
.collect();
trace!("found {} tokens", tokens.len());
//let _unused = data[token_size.. token_size + 20];
let unk1 = get_bytes(&mut cursor, 12)?;
trace!("read unk1: {:?}", unk1);
let mapname = get_string(&mut cursor, 32)?;
trace!("read mapname: {:?}", mapname);
let unk2 = get_bytes(&mut cursor, 4)?;
trace!("read unk2: {:?} (50 00 AE 0F ?)", unk2);
let savename = get_string(&mut cursor, 80)?;
trace!("read savename: {:?}", savename);
let unk3 = get_bytes(&mut cursor, 2168)?;
trace!("read unk3 ({} bytes)", unk3.len());
let mut entries = Vec::new();
loop {
let offset = cursor.position() as usize;
if offset >= cursor.get_ref().len() {
break;
}
let valv_pre = get_string(&mut cursor, 260)?;
let valv_size = get_u32(&mut cursor)? as usize;
let valv_content = get_bytes(&mut cursor, valv_size)?;
trace!("read VALV file (offset=0x{:06x} size={} bytes): {}", offset, valv_size, valv_pre);
entries.push(JsavValvEntry {
name: valv_pre,
data: valv_content,
})
}
Ok(JsavFile {
version: version,
tokens: tokens,
map_name: mapname,
save_name: savename,
content: entries,
})
}
}
#[derive(StructOpt, Debug)]
struct Opt {
#[structopt(short = "d", long = "debug")]
debug: bool,
#[structopt(short = "v", long = "verbose", parse(from_occurrences))]
verbose: u8,
/// Save file (.sav)
#[structopt(name = "SAVFILE", parse(from_os_str))]
file: PathBuf,
}
fn main() {
pretty_env_logger::init();
let opt = Opt::from_args();
let mut f = File::open(opt.file).expect("file not found");
let mut data: Vec<u8> = Vec::new();
f.read_to_end(&mut data)
.expect("something went wrong reading the file");
println!("{:#?}", JsavFile::load(&data));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment