pub(super) mod chunk;
pub mod integrity;
mod lists;
mod numbers;
pub(crate) mod reader;
pub(super) mod resources;
use std::path::Path;
use crate::prelude::*;
use crate::util::bench::Stopwatch;
use crate::util::fmt::hexdump;
use crate::wad::chunk::ChunkName;
use crate::wad::data::Endianness;
use crate::wad::data::GMData;
use crate::wad::data::Metadata;
use crate::wad::elem::animation_curve::AnimationCurves;
use crate::wad::elem::audio::Audios;
use crate::wad::elem::audio_group::AudioGroups;
use crate::wad::elem::background::Tilesets;
use crate::wad::elem::code::Codes;
use crate::wad::elem::code::check_yyc;
use crate::wad::elem::data_file::DataFiles;
use crate::wad::elem::embedded_image::EmbeddedImages;
use crate::wad::elem::extension::Extensions;
use crate::wad::elem::feature_flag::FeatureFlags;
use crate::wad::elem::filter_effect::FilterEffects;
use crate::wad::elem::font::Fonts;
use crate::wad::elem::function::Functions;
use crate::wad::elem::game_end::GameEndScripts;
use crate::wad::elem::game_object::GameObjects;
use crate::wad::elem::general_info::GeneralInfo;
use crate::wad::elem::global_init::GlobalInitScripts;
use crate::wad::elem::language::LanguageInfo;
use crate::wad::elem::options::Options;
use crate::wad::elem::particle_emitter::ParticleEmitters;
use crate::wad::elem::particle_system::ParticleSystems;
use crate::wad::elem::path::Paths;
use crate::wad::elem::room::Rooms;
use crate::wad::elem::script::Scripts;
use crate::wad::elem::sequence::Sequences;
use crate::wad::elem::shader::Shaders;
use crate::wad::elem::sound::Sounds;
use crate::wad::elem::sprite::Sprites;
use crate::wad::elem::tag::Tags;
use crate::wad::elem::texture_group_info::TextureGroupInfos;
use crate::wad::elem::texture_page::TexturePages;
use crate::wad::elem::texture_page_item::TexturePageItems;
use crate::wad::elem::timeline::Timelines;
use crate::wad::elem::ui_node::UINodes;
use crate::wad::elem::variable::Variables;
use crate::wad::parse::chunk::ChunkBounds;
use crate::wad::parse::chunk::ChunkMap;
use crate::wad::parse::reader::DataReader;
use crate::wad::version::GMVersion;
use crate::wad::version_detection::detect_gamemaker_version;
const ERR_TOO_BIG: &str =
"Data file is bigger than 2,147,483,646 bytes which will lead to bugs in LibGM and the runner";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsingOptions {
pub verify_alignment: bool,
pub verify_constants: bool,
pub allow_unknown_chunks: bool,
}
impl Default for ParsingOptions {
fn default() -> Self {
Self::STRICT
}
}
impl ParsingOptions {
pub const LENIENT: Self = Self {
verify_alignment: false,
verify_constants: false,
allow_unknown_chunks: true,
};
pub const STRICT: Self = Self {
verify_alignment: true,
verify_constants: true,
allow_unknown_chunks: false,
};
#[inline]
#[must_use]
pub const fn new() -> Self {
Self::STRICT
}
#[inline]
#[must_use]
pub const fn verify_alignment(mut self, enabled: bool) -> Self {
self.verify_alignment = enabled;
self
}
#[inline]
#[must_use]
pub const fn verify_constants(mut self, enabled: bool) -> Self {
self.verify_constants = enabled;
self
}
#[inline]
#[must_use]
pub const fn allow_unknown_chunks(mut self, enabled: bool) -> Self {
self.allow_unknown_chunks = enabled;
self
}
pub fn parse_bytes(&self, raw_data: impl AsRef<[u8]>) -> Result<GMData> {
self.parse(raw_data.as_ref())
.ctx("parsing GameMaker data bytes")
}
pub fn parse_general_info(&self, raw_data: impl AsRef<[u8]>) -> Result<GeneralInfo> {
let mut reader = parse_form(raw_data.as_ref()).ctx("parsing FORM")?;
init_reader(&mut reader).ctx("initializing reader")?;
Ok(reader.general_info)
}
pub fn parse_file(&self, data_file_path: impl AsRef<Path>) -> Result<GMData> {
let path = data_file_path.as_ref();
let meta = std::fs::metadata(path)
.ctx_any(|| format!("reading metadata of data file {}", path.display()))?;
if meta.len() >= i32::MAX as u64 {
bail!("{ERR_TOO_BIG}");
}
let stopwatch = Stopwatch::start();
let raw_data: Vec<u8> =
std::fs::read(path).ctx_any(|| format!("reading data file {}", path.display()))?;
log::trace!("Reading data file bytes took {stopwatch}");
let mut gm_data = self
.parse(&raw_data)
.ctx(|| format!("parsing GameMaker data file {}", path.display()))?;
gm_data.meta.location = Some(path.to_path_buf());
Ok(gm_data)
}
fn parse(&self, raw_data: &[u8]) -> Result<GMData> {
if cfg!(feature = "catch-panic") {
crate::util::unwind::catch(|| parse(raw_data, self))
} else {
parse(raw_data, self)
}
}
}
pub fn parse_bytes(raw_data: impl AsRef<[u8]>) -> Result<GMData> {
ParsingOptions::new().parse_bytes(raw_data)
}
pub fn parse_file(data_file_path: impl AsRef<Path>) -> Result<GMData> {
ParsingOptions::new().parse_file(data_file_path)
}
fn parse_form(raw_data: &'_ [u8]) -> Result<DataReader<'_>> {
if raw_data.len() >= i32::MAX as usize {
bail!("{ERR_TOO_BIG}");
}
let mut reader = DataReader::new(raw_data);
let bytes: &[u8; 4] = reader.read_bytes_const().ctx("reading root chunk name")?;
reader.endianness = match bytes {
b"FORM" => Endianness::Little,
b"MROF" => Endianness::Big,
_ => {
let hex = hexdump(bytes);
let msg = "Expected root chunk to be 'FORM' but found";
if let Ok(string) = str::from_utf8(bytes) {
bail!("{msg} {string:?} ({hex})")
}
bail!("{msg} {hex}");
}
};
if reader.endianness == Endianness::Big {
log::warn!("Big endian format might not work, proceed with caution");
}
let remaining_data_len = reader.read_u32().ctx("reading root chunk length")?;
let total_data_len = remaining_data_len + reader.cur_pos;
if total_data_len as usize != raw_data.len() {
bail!(
"Specified FORM data length is {} but data is actually {} bytes long",
total_data_len,
raw_data.len(),
);
}
while reader.cur_pos + 8 < total_data_len {
let name = reader.read_chunk_name()?;
let chunk_length = reader.read_u32()?;
let start_pos = reader.cur_pos;
reader.cur_pos = reader
.cur_pos
.checked_add(chunk_length)
.filter(|&pos| pos <= total_data_len)
.ok_or_else(|| {
format!(
"Chunk {name} out of bounds: specified length {chunk_length} would exceed \
total length {total_data_len}"
)
})?;
let end_pos = reader.cur_pos;
reader.last_chunk = name;
let chunk_bounds = ChunkBounds { start_pos, end_pos };
reader.chunks.push(name, chunk_bounds)?;
reader.chunk_order.push(name);
}
Ok(reader)
}
fn init_reader(reader: &mut DataReader) -> Result<()> {
reader.specified_version = reader.read_gen8_version()?;
reader.string_chunk = reader
.chunks
.get(ChunkName::STRG)
.ok_or("Chunk STRG does not exist")?;
reader.strings = reader.read_chunk()?;
reader.general_info = reader.read_chunk()?;
if !reader.general_info.exists {
bail!("GEN8 chunk does not exist");
}
if reader.specified_version == GMVersion::GMS2 {
let stopwatch = Stopwatch::start();
detect_gamemaker_version(reader).ctx("detecting GameMaker version")?;
log::trace!("Detecting GameMaker Version took {stopwatch}");
}
let game = reader.general_info.display_name.display(&reader.strings);
let version = reader.general_info.version;
let wad_version = reader.general_info.wad_version;
log::info!("Loading {game:?} (GM {version}, WAD {wad_version})");
Ok(())
}
fn read_bytecode_chunks(reader: &mut DataReader) -> Result<(Codes, Functions, Variables)> {
let is_yyc: bool = match check_yyc(reader) {
Ok(yyc) => yyc,
Err(e) if reader.options.verify_constants => return Err(e).ctx("Checking YYC"),
Err(e) => {
log::warn!("YYC integrity check failed: {e}");
false
}
};
if is_yyc {
log::warn!("YYC is untested, issues may occur");
Ok((Codes::default(), Functions::default(), Variables::default()))
} else {
let variables = reader.read_chunk()?; let functions = reader.read_chunk()?; let codes = reader.read_chunk()?;
Ok((codes, functions, variables))
}
}
fn parse(raw_data: &[u8], options: &ParsingOptions) -> Result<GMData> {
let stopwatch = Stopwatch::start();
let mut reader: DataReader = parse_form(raw_data).ctx("parsing FORM")?;
reader.options = options.clone();
init_reader(&mut reader).ctx("initializing reader")?;
let texture_page_items: TexturePageItems = reader.read_chunk()?;
let codes: Codes;
let functions: Functions;
let variables: Variables;
(codes, functions, variables) = read_bytecode_chunks(&mut reader)?;
let stopwatch2 = Stopwatch::start();
let texture_pages: TexturePages = reader.read_chunk()?;
let scripts: Scripts = reader.read_chunk()?;
let fonts: Fonts = reader.read_chunk()?;
let sprites: Sprites = reader.read_chunk()?;
let game_objects: GameObjects = reader.read_chunk()?;
let rooms: Rooms = reader.read_chunk()?;
let backgrounds: Tilesets = reader.read_chunk()?;
let audios: Audios = reader.read_chunk()?;
let sounds: Sounds = reader.read_chunk()?;
let paths: Paths = reader.read_chunk()?;
let options: Options = reader.read_chunk()?;
let sequences: Sequences = reader.read_chunk()?;
let particle_systems: ParticleSystems = reader.read_chunk()?;
let particle_emitters: ParticleEmitters = reader.read_chunk()?;
let language_info: LanguageInfo = reader.read_chunk()?;
let extensions: Extensions = reader.read_chunk()?;
let audio_groups: AudioGroups = reader.read_chunk()?;
let global_init_scripts: GlobalInitScripts = reader.read_chunk()?;
let game_end_scripts: GameEndScripts = reader.read_chunk()?;
let shaders: Shaders = reader.read_chunk()?;
let ui_nodes: UINodes = reader.read_chunk()?;
let timelines: Timelines = reader.read_chunk()?;
let embedded_images: EmbeddedImages = reader.read_chunk()?;
let texture_group_infos: TextureGroupInfos = reader.read_chunk()?;
let tags: Tags = reader.read_chunk()?;
let feature_flags: FeatureFlags = reader.read_chunk()?;
let filter_effects: FilterEffects = reader.read_chunk()?;
let animation_curves: AnimationCurves = reader.read_chunk()?;
let data_files: DataFiles = reader.read_chunk()?;
log::trace!("Reading independent chunks took {stopwatch2}");
if !options.exists {
bail!("Required chunk OPTN does not exist");
}
handle_unread_chunks(&reader.chunks, reader.options.allow_unknown_chunks)?;
let meta = Metadata {
location: None,
chunk_padding: reader.chunk_padding,
endianness: reader.endianness,
original_data_size: reader.size(),
chunk_order: reader.chunk_order,
};
let data = GMData {
meta,
animation_curves,
audio_groups,
audios,
tilesets: backgrounds,
codes,
data_files,
embedded_images,
extensions,
feature_flags,
filter_effects,
fonts,
functions,
game_end_scripts,
general_info: reader.general_info,
global_init_scripts,
language_info,
options,
particle_emitters,
particle_systems,
paths,
rooms,
ui_nodes,
scripts,
sequences,
shaders,
sounds,
sprites,
strings: reader.strings,
tags,
texture_group_infos,
texture_page_items,
timelines,
texture_pages,
game_objects,
variables,
};
log::trace!("Parsing data took {stopwatch}");
Ok(data)
}
fn handle_unread_chunks(chunks: &ChunkMap, allow_unknown: bool) -> Result<()> {
if chunks.is_empty() {
return Ok(());
}
let count: usize = chunks.count();
let mut buffer = String::with_capacity(count * 6);
for chunk_name in chunks.chunk_names() {
use core::fmt::Write;
let _ = write!(buffer, "{chunk_name}");
buffer.push_str(", ");
}
buffer.pop();
buffer.pop();
let noun: &str = if count == 1 { "chunk" } else { "chunks" };
let message = format!(
"{count} unprocessed {noun} detected: {buffer}\nThese unknown chunks will be lost when \
rebuilding data.",
);
if allow_unknown {
log::warn!("{message}");
Ok(())
} else {
bail!("{message}");
}
}