mod acrv;
mod agrp;
mod bgnd;
mod code;
mod extn;
mod font;
mod func;
mod objt;
mod psem;
mod room;
mod sond;
mod sprt;
mod tgin;
mod txtr;
use crate::prelude::*;
use crate::wad::GMVersion;
use crate::wad::chunk::ChunkName;
use crate::wad::parse::chunk::ChunkBounds;
use crate::wad::parse::chunk::ChunkMap;
use crate::wad::parse::reader::DataReader;
use crate::wad::version::LtsBranch::PostLts;
use crate::wad::version::LtsBranch::Pre2022;
use crate::wad::version::ToGMVersion;
fn try_check(
reader: &mut DataReader,
chunk: ChunkName,
check_fn: fn(&mut DataReader) -> Result<Option<GMVersion>>,
target_version: impl ToGMVersion,
) -> Result<()> {
let target_version = target_version.to_gm_version();
if reader.general_info.version >= target_version {
return Ok(());
}
let Some(chunk_bounds) = reader.chunks.get(chunk) else {
return Ok(());
};
reader.chunk = chunk_bounds;
reader.cur_pos = chunk_bounds.start_pos;
let version_req_opt = check_fn(reader).ctx(|| {
format!("manually detecting GameMaker Version {target_version} in chunk {chunk}")
})?;
let Some(version_req) = version_req_opt else {
return Ok(());
};
if reader.general_info.version >= version_req {
return Ok(());
}
log::debug!(
"Upgraded Version from {} to {} using manual check in chunk {:?}",
reader.general_info.version,
version_req,
chunk,
);
reader.general_info.set_version(version_req);
Ok(())
}
type CheckerFn = fn(&mut DataReader) -> Result<Option<GMVersion>>;
struct VersionCheck {
chunk_name: ChunkName,
checker_fn: CheckerFn,
required_version: GMVersion,
target_version: GMVersion,
}
impl VersionCheck {
fn new(
chunk: ChunkName,
checker_fn: CheckerFn,
req: impl ToGMVersion,
target: impl ToGMVersion,
) -> Self {
Self {
chunk_name: chunk,
checker_fn,
required_version: req.to_gm_version(),
target_version: target.to_gm_version(),
}
}
}
fn upgrade_by_chunk_existence(chunks: &ChunkMap) -> Option<GMVersion> {
const UPGRADES: [(ChunkName, GMVersion); 6] = [
(ChunkName::UILR, GMVersion::new(2024, 13, 0, 0, PostLts)),
(ChunkName::PSEM, GMVersion::new(2023, 2, 0, 0, PostLts)),
(ChunkName::FEAT, GMVersion::new(2022, 8, 0, 0, Pre2022)),
(ChunkName::FEDS, GMVersion::new(2, 3, 6, 0, Pre2022)),
(ChunkName::SEQN, GMVersion::new(2, 3, 0, 0, Pre2022)),
(ChunkName::TGIN, GMVersion::new(2, 2, 1, 0, Pre2022)),
];
for (chunk_name, version) in UPGRADES {
if chunks.contains(chunk_name) {
log::debug!("Existence of chunk {chunk_name} implies a Version of at least {version}");
return Some(version);
}
}
None
}
fn create_version_checks() -> [VersionCheck; 21] {
[
VersionCheck::new(
ChunkName::SOND,
sond::check_2024_6,
(2022, 2, PostLts),
(2024, 6),
),
VersionCheck::new(
ChunkName::SPRT,
sprt::check_2024_6,
(2022, 2, PostLts),
(2024, 6),
),
VersionCheck::new(
ChunkName::FONT,
font::check_2023_6_and_2024_11,
(2024, 6),
(2024, 11),
),
VersionCheck::new(
ChunkName::FONT,
font::check_2023_6_and_2024_11,
(2022, 8),
(2023, 6),
),
VersionCheck::new(ChunkName::ROOM, room::check_2022_1, (2, 3), (2022, 1)),
VersionCheck::new(
ChunkName::ROOM,
room::check_2024_2_and_2024_4,
(2023, 2),
(2024, 4),
),
VersionCheck::new(ChunkName::ROOM, room::check_2_2_2_302, 2, (2, 2, 2, 302)),
VersionCheck::new(ChunkName::EXTN, extn::check_2022_6, (2, 3), (2022, 6)),
VersionCheck::new(ChunkName::TXTR, txtr::check_2022_5, (2022, 3), (2022, 5)),
VersionCheck::new(ChunkName::TXTR, txtr::check_2022_3, (2, 3), (2022, 3)),
VersionCheck::new(ChunkName::FONT, font::check_2024_14, (2024, 13), (2024, 14)),
VersionCheck::new(ChunkName::AGRP, agrp::check_2024_14, (2024, 13), (2024, 14)),
VersionCheck::new(ChunkName::EXTN, extn::check_2023_4, (2022, 6), (2023, 4)),
VersionCheck::new(ChunkName::TGIN, tgin::check_2023_1, (2022, 9), (2023, 1)),
VersionCheck::new(ChunkName::OBJT, objt::check_2022_5, (2, 3), (2022, 5)),
VersionCheck::new(ChunkName::SPRT, sprt::check_2_3_2, 2, (2, 3, 2)),
VersionCheck::new(ChunkName::TGIN, tgin::check_2022_9, (2, 3), (2022, 9)),
VersionCheck::new(ChunkName::TXTR, txtr::check_2_0_6, 2, (2, 0, 6)),
VersionCheck::new(ChunkName::PSEM, psem::check_2023_x, (2023, 2), (2023, 8)),
VersionCheck::new(ChunkName::ACRV, acrv::check_2_3_1, (2, 3), (2, 3, 1)),
VersionCheck::new(
ChunkName::BGND,
bgnd::check_2024_14_1,
(2024, 13),
(2024, 14, 1),
),
]
}
pub fn detect_gamemaker_version(reader: &mut DataReader) -> Result<()> {
let saved_pos = reader.cur_pos;
let saved_chunk: ChunkBounds = reader.chunk;
if let Some(version) = upgrade_by_chunk_existence(&reader.chunks) {
reader.general_info.set_version(version);
}
if reader.general_info.wad_version >= 14 {
try_check(reader, ChunkName::FUNC, func::check_2024_8, (2024, 8))?;
}
if reader.general_info.wad_version >= 15 {
try_check(
reader,
ChunkName::CODE,
code::check_2023_8_and_2024_4,
(2024, 4),
)?;
}
if reader.general_info.wad_version >= 17 {
try_check(reader, ChunkName::FONT, font::check_2022_2, (2022, 2))?;
}
let checks: &[VersionCheck] = &create_version_checks();
loop {
let mut same_version: bool = true;
for check in checks {
if reader.general_info.version >= check.target_version {
continue;
}
if reader.general_info.version < check.required_version {
continue;
}
let Some(chunk) = reader.chunks.get(check.chunk_name) else {
continue;
};
reader.chunk = chunk;
reader.cur_pos = reader.chunk.start_pos;
let detected_version_opt: Option<GMVersion> = (check.checker_fn)(reader).ctx(|| {
format!(
"detecting GameMaker Version {} in chunk {}",
check.target_version, check.chunk_name,
)
})?;
if let Some(detected_version) = detected_version_opt
&& reader.general_info.version < detected_version
{
log::debug!(
"Upgraded Version from {} to {} using check in chunk {}",
reader.general_info.version,
detected_version,
check.chunk_name,
);
reader.general_info.set_version(detected_version);
same_version = false;
}
}
if same_version {
break;
}
}
reader.cur_pos = saved_pos;
reader.chunk = saved_chunk;
Ok(())
}
macro_rules! target_version {
($($part:expr),+ $(,)?) => {
Ok(Some($crate::wad::version::ToGMVersion::into_gm_version(($($part,)+))))
};
}
use target_version;