Skip to content

Commit

Permalink
add army decoder and encoder (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgi388 authored Jun 26, 2024
1 parent 9f7d92a commit 9d6f060
Show file tree
Hide file tree
Showing 6 changed files with 828 additions and 0 deletions.
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ license = "MIT OR Apache-2.0"

[dependencies]
bitflags = { version = "2.3", features = ["serde"] }
encoding_rs = "0.8.34"
encoding_rs_io = "0.1.7"
glam = { version = "0.27.0", default-features = false, features = ["serde"] }
image = "0.25.1"
indexmap = { version = "2.2.6", features = ["serde"] }
Expand Down
277 changes: 277 additions & 0 deletions src/army/decoder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
use super::*;
use encoding_rs::WINDOWS_1252;
use encoding_rs_io::DecodeReaderBytesBuilder;
use std::{
fmt,
io::{Error as IoError, Read, Seek, SeekFrom},
mem::size_of,
};

#[derive(Debug)]
pub enum DecodeError {
IoError(IoError),
InvalidFormat(u32),
InvalidString,
}

impl std::error::Error for DecodeError {}

impl From<IoError> for DecodeError {
fn from(error: IoError) -> Self {
DecodeError::IoError(error)
}
}

impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DecodeError::IoError(e) => write!(f, "IO error: {}", e),
DecodeError::InvalidFormat(format) => write!(f, "invalid format: {}", format),
DecodeError::InvalidString => write!(f, "invalid string"),
}
}
}

pub(crate) const FORMAT: u32 = 0x0000029e;
pub(crate) const HEADER_SIZE: usize = 192;
const SAVE_HEADER_SIZE: usize = 504;
pub(crate) const REGIMENT_BLOCK_SIZE: usize = 188;

pub(crate) struct Header {
_format: u32,
regiment_count: u32,
/// The size of each regiment block in bytes.
///
/// This is always 188 despite being encoded in the header.
_regiment_block_size: u32,
race: u8,
unknown1: [u8; 3], // purpose of bytes at index 13, 14, 15 is unknown
unknown2: [u8; 34], // purpose of bytes at index 16-50 is unknown
small_banner_path: String,
/// There are some bytes after the null-terminated string. Not sure what
/// they are for.
small_banner_path_remainder: Vec<u8>,
small_banner_disabled_path: String,
/// There are some bytes after the null-terminated string. Not sure what
/// they are for.
small_banner_disabled_path_remainder: Vec<u8>,
large_banner_path: String,
/// There are some bytes after the null-terminated string. Not sure what
/// they are for.
large_banner_path_remainder: Vec<u8>,
gold_from_treasures: u16,
gold_in_coffers: u16,
magic_items: [u8; 40],
unknown3: [u8; 2], // purpose of bytes at index 190 and 191 is unknown
}

pub struct Decoder<R>
where
R: Read + Seek,
{
reader: R,
}

impl<R: Read + Seek> Decoder<R> {
pub fn new(reader: R) -> Self {
Decoder { reader }
}

pub fn decode(&mut self) -> Result<Army, DecodeError> {
let start_pos = self.maybe_read_save_file()?;

let header = self.read_header(start_pos)?;

let regiments = self.read_regiments(&header)?;

Ok(Army {
race: header.race,
unknown1: header.unknown1.to_vec(),
unknown2: header.unknown2.to_vec(),
regiments,
small_banner_path: header.small_banner_path,
small_banner_path_remainder: header.small_banner_path_remainder,
small_banner_disabled_path: header.small_banner_disabled_path,
small_banner_disabled_path_remainder: header.small_banner_disabled_path_remainder,
large_banner_path: header.large_banner_path,
large_banner_path_remainder: header.large_banner_path_remainder,
gold_from_treasures: header.gold_from_treasures,
gold_in_coffers: header.gold_in_coffers,
magic_items: header.magic_items.to_vec(),
unknown3: header.unknown3.to_vec(),
})
}

fn maybe_read_save_file(&mut self) -> Result<u64, DecodeError> {
let mut buf = [0; size_of::<u32>()];
self.reader.read_exact(&mut buf)?;

let mut start_pos = 0;

let format = u32::from_le_bytes(buf[0..size_of::<u32>()].try_into().unwrap());
if format != FORMAT {
// TODO: Skipped over reading save header.
start_pos = SAVE_HEADER_SIZE as u64;
}
Ok(start_pos)
}

fn read_header(&mut self, start_pos: u64) -> Result<Header, DecodeError> {
self.reader.seek(SeekFrom::Start(start_pos))?;

let mut buf = [0; HEADER_SIZE];
self.reader.read_exact(&mut buf)?;

let small_banner_path_buf = &buf[50..82];
let (small_banner_path_buf, small_banner_path_remainder) = small_banner_path_buf
.iter()
.enumerate()
.find(|(_, &b)| b == 0)
.map(|(i, _)| small_banner_path_buf.split_at(i + 1))
.unwrap_or((small_banner_path_buf, &[]));

let small_banner_disabled_path_buf = &buf[82..114];
let (small_banner_disabled_path_buf, small_banner_disabled_path_remainder) =
small_banner_disabled_path_buf
.iter()
.enumerate()
.find(|(_, &b)| b == 0)
.map(|(i, _)| small_banner_disabled_path_buf.split_at(i + 1))
.unwrap_or((small_banner_disabled_path_buf, &[]));

let large_banner_path_buf = &buf[114..146];
let (large_banner_path_buf, large_banner_path_remainder) = large_banner_path_buf
.iter()
.enumerate()
.find(|(_, &b)| b == 0)
.map(|(i, _)| large_banner_path_buf.split_at(i + 1))
.unwrap_or((large_banner_path_buf, &[]));

Ok(Header {
_format: u32::from_le_bytes(buf[0..4].try_into().unwrap()),
regiment_count: u32::from_le_bytes(buf[4..8].try_into().unwrap()),
_regiment_block_size: u32::from_le_bytes(buf[8..12].try_into().unwrap()),
race: buf[12],
unknown1: buf[13..16].try_into().unwrap(),
unknown2: buf[16..50].try_into().unwrap(),
small_banner_path: self.read_string(small_banner_path_buf)?,
small_banner_path_remainder: small_banner_path_remainder.to_vec(),
small_banner_disabled_path: self.read_string(small_banner_disabled_path_buf)?,
small_banner_disabled_path_remainder: small_banner_disabled_path_remainder.to_vec(),
large_banner_path: self.read_string(large_banner_path_buf)?,
large_banner_path_remainder: large_banner_path_remainder.to_vec(),
gold_from_treasures: u16::from_le_bytes(buf[146..148].try_into().unwrap()),
gold_in_coffers: u16::from_le_bytes(buf[148..150].try_into().unwrap()),
magic_items: buf[150..190].try_into().unwrap(),
unknown3: buf[190..192].try_into().unwrap(),
})
}

fn read_regiments(&mut self, header: &Header) -> Result<Vec<Regiment>, DecodeError> {
let mut regiments = Vec::with_capacity(header.regiment_count as usize);

for _ in 0..header.regiment_count {
regiments.push(self.read_regiment()?);
}

Ok(regiments)
}

fn read_regiment(&mut self) -> Result<Regiment, DecodeError> {
let mut buf = vec![0; REGIMENT_BLOCK_SIZE];
self.reader.read_exact(&mut buf)?;

Ok(Regiment {
status: buf[0..2].try_into().unwrap(),
unknown1: buf[2..4].try_into().unwrap(),
id: u16::from_le_bytes(buf[4..6].try_into().unwrap()),
unknown2: buf[6..8].try_into().unwrap(),
wizard_type: buf[8],
max_armor: buf[9],
cost: u16::from_le_bytes(buf[10..12].try_into().unwrap()),
banner_index: u16::from_le_bytes(buf[12..14].try_into().unwrap()),
unknown3: buf[14..16].try_into().unwrap(),
regiment_attributes: buf[16..20].try_into().unwrap(),
sprite_index: u16::from_le_bytes(buf[20..22].try_into().unwrap()),
name: self.read_string(&buf[22..54])?,
name_id: u16::from_le_bytes(buf[54..56].try_into().unwrap()),
alignment: buf[56],
max_troops: buf[57],
alive_troops: buf[58],
ranks: buf[59],
unknown4: buf[60..64].try_into().unwrap(),
troop_attributes: TroopAttributes {
movement: buf[64],
weapon_skill: buf[65],
ballistic_skill: buf[66],
strength: buf[67],
toughness: buf[68],
wounds: buf[69],
initiative: buf[70],
attacks: buf[71],
leadership: buf[72],
},
mount: buf[73],
armor: buf[74],
weapon: buf[75],
typ: buf[76],
point_value: buf[77],
missile_weapon: buf[78],
unknown5: buf[79],
unknown6: buf[80..84].try_into().unwrap(),
leader: Leader {
sprite_index: u16::from_le_bytes(buf[84..86].try_into().unwrap()),
name: self.read_string(&buf[86..118])?,
name_remainder: buf[118..127].to_vec(),
attributes: TroopAttributes {
movement: buf[127],
weapon_skill: buf[128],
ballistic_skill: buf[129],
strength: buf[130],
toughness: buf[131],
wounds: buf[132],
initiative: buf[133],
attacks: buf[134],
leadership: buf[135],
},
mount: buf[136],
armor: buf[137],
weapon: buf[138],
unit_type: buf[139],
point_value: buf[140],
missile_weapon: buf[141],
unknown1: buf[142..146].try_into().unwrap(),
head_id: u16::from_le_bytes(buf[146..148].try_into().unwrap()),
x: buf[148..152].try_into().unwrap(),
y: buf[152..156].try_into().unwrap(),
},
experience: u16::from_le_bytes(buf[156..158].try_into().unwrap()),
duplicate_id: buf[158],
min_armor: buf[159],
magic_book: u16::from_le_bytes(buf[160..162].try_into().unwrap()),
magic_items: [
u16::from_le_bytes(buf[162..164].try_into().unwrap()),
u16::from_le_bytes(buf[164..166].try_into().unwrap()),
u16::from_le_bytes(buf[166..168].try_into().unwrap()),
],
unknown7: buf[168..180].try_into().unwrap(),
purchased_armor: buf[180],
max_purchasable_armor: buf[181],
repurchased_troops: buf[182],
max_purchasable_troops: buf[183],
book_profile: buf[184..188].try_into().unwrap(),
})
}

fn read_string(&mut self, buf: &[u8]) -> Result<String, DecodeError> {
let nul_pos = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
let mut decoder = DecodeReaderBytesBuilder::new()
.encoding(Some(WINDOWS_1252))
.build(&buf[..nul_pos]);
let mut dest = String::new();

decoder.read_to_string(&mut dest)?;

Ok(dest)
}
}
Loading

0 comments on commit 9d6f060

Please sign in to comment.