use crate::prelude::*;
use crate::util::bench::Stopwatch;
use crate::wad::chunk::ChunkName;
use crate::wad::data::Endianness;
use crate::wad::deserialize::reader::DataReader;
use crate::wad::elements::GMChunk;
use crate::wad::elements::GMElement;
use crate::wad::version::GMVersion;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ChunkBounds {
pub start_pos: u32,
pub end_pos: u32,
}
impl ChunkBounds {
#[must_use]
pub const fn length(&self) -> u32 {
self.end_pos - self.start_pos
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.length() == 0
}
}
const KNOWN_CHUNK_COUNT: usize = 35;
#[derive(Debug, Default)]
pub struct ChunkMap(Vec<(ChunkName, ChunkBounds)>);
impl ChunkMap {
#[must_use]
pub fn new() -> Self {
Self(Vec::with_capacity(KNOWN_CHUNK_COUNT))
}
#[must_use]
pub const fn count(&self) -> usize {
self.0.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn contains_name(&self, chunk_name: ChunkName) -> bool {
for (name, _) in &self.0 {
if *name == chunk_name {
return true;
}
}
false
}
#[must_use]
pub fn contains(&self, name: &'static str) -> bool {
self.contains_name(ChunkName::new(name))
}
pub fn push(&mut self, name: ChunkName, chunk: ChunkBounds) -> Result<()> {
if self.contains_name(name) {
bail!("Chunk {name:?} is defined multiple times");
}
self.0.push((name, chunk));
Ok(())
}
#[must_use]
pub fn get_by_name(&self, chunk_name: ChunkName) -> Option<ChunkBounds> {
for (name, bounds) in &self.0 {
if *name == chunk_name {
return Some(bounds.clone());
}
}
None
}
#[must_use]
pub fn get(&self, name: &'static str) -> Option<ChunkBounds> {
self.get_by_name(ChunkName::new(name))
}
pub fn remove(&mut self, name: &'static str) -> Option<ChunkBounds> {
self.remove_name(ChunkName::new(name))
}
pub fn remove_name(&mut self, chunk_name: ChunkName) -> Option<ChunkBounds> {
for i in 0..self.count() {
let name = &self.0[i].0;
if *name == chunk_name {
let bounds = self.0.remove(i).1;
return Some(bounds);
}
}
None
}
#[inline]
pub fn chunk_names(&self) -> impl Iterator<Item = ChunkName> {
self.0.iter().map(|(name, _)| *name)
}
}
impl DataReader<'_> {
pub fn read_chunk_name(&mut self) -> Result<ChunkName> {
let mut bytes: [u8; 4] = self.read_bytes_const().cloned()?;
if self.endianness == Endianness::Big {
bytes.reverse();
}
let chunk_name = ChunkName::from_bytes(bytes)?;
Ok(chunk_name)
}
pub fn read_chunk<T: GMChunk>(&mut self) -> Result<T> {
let Some(chunk) = self.chunks.remove_name(T::NAME) else {
return Ok(T::default());
};
let ctx = || format!("deserializing chunk '{}'", T::NAME);
let stopwatch = Stopwatch::start();
self.cur_pos = chunk.start_pos;
self.chunk = chunk;
let element = T::deserialize(self).with_context(ctx)?;
if T::NAME != self.last_chunk {
self.read_chunk_padding().with_context(ctx)?;
}
if self.cur_pos != self.chunk.end_pos {
self.handle_invalid_align(format!(
"Misaligned chunk '{}': expected chunk end position {} but the reader is actually \
at position {} (diff: {})",
T::NAME,
self.chunk.end_pos,
self.cur_pos,
i64::from(self.chunk.end_pos) - i64::from(self.cur_pos),
))?;
self.cur_pos = self.chunk.end_pos;
}
log::trace!("Parsing chunk '{}' took {stopwatch}", T::NAME);
Ok(element)
}
fn read_chunk_padding(&mut self) -> Result<()> {
let ver: &GMVersion = &self.specified_version;
let padding_eligible = ver.major >= 2 || (ver.major == 1 && ver.build >= 9999);
if !padding_eligible {
return Ok(());
}
while !self.cur_pos.is_multiple_of(self.chunk_padding) {
let byte: u8 = self.read_u8().context("reading chunk padding")?;
if byte == 0 {
continue;
}
self.cur_pos -= 1; self.chunk_padding = if self.cur_pos.is_multiple_of(4) { 4 } else { 1 };
log::debug!("Set chunk padding to {}", self.chunk_padding);
return Ok(());
}
Ok(())
}
pub fn read_gen8_version(&mut self) -> Result<GMVersion> {
const CTX: &str = "trying to read GEN8 GameMaker Version";
let saved_pos = self.cur_pos;
let saved_chunk: ChunkBounds = self.chunk.clone();
self.chunk = self
.chunks
.get("GEN8")
.ok_or("Chunk GEN8 does not exist")
.context(CTX)?;
self.cur_pos = self.chunk.start_pos + 44; let gm_version = GMVersion::deserialize(self).context(CTX)?;
self.cur_pos = saved_pos;
self.chunk = saved_chunk;
Ok(gm_version)
}
}