libgm 0.7.2

A tool for unpacking and modding GameMaker games
Documentation
// SPDX-License-Identifier: GPL-3.0-only
//! Contains GameMaker IDE Version types and abstractions to check and set versions.

use core::cmp::Ordering;
use std::fmt::Display;
use std::fmt::Formatter;

use crate::prelude::*;
use crate::wad::build::builder::DataBuilder;
use crate::wad::elem::GMElement;
use crate::wad::parse::reader::DataReader;

/// Different GameMaker release branches. LTS has some but not all features of
/// equivalent newer versions.
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
pub enum LtsBranch {
    /// Before LTS was even introduced (`major < 2022`).
    Pre2022,

    /// The 2022 Long-Term Support branch.
    /// YoYoGames updates minor bugfixes here, but doesn't make breaking changes
    /// (except in 2023.6?).
    Lts2022,

    /// New Version but not the 2022 Long-Term Support branch.
    /// YoYo Games introduces all new features here, some of which may break
    /// your project.
    PostLts,
}

impl Display for LtsBranch {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        let string = match self {
            Self::Pre2022 => "Pre LTS",
            Self::Lts2022 => "2022 LTS",
            Self::PostLts => "Post LTS",
        };
        f.write_str(string)
    }
}

/// A GameMaker Studio Version.
///
/// Theoretically, this is only the version of the IDE this game was made in.
/// However, this version struct is also used for file format purposes in this
/// library. This is because it is more accurate than the `WAD Version` in
/// `GMGeneralInfo`, which is no longer updated (stuck since WAD 17).
///
/// This version struct is also not updated by YoYo Games since GM:S 2 and its
/// raw `GEN8` version is stuck on `2.0.0.0`.
/// This library uses version detection to detect the approximate GameMaker
/// version so that the file format can be deserialized properly.
#[derive(Debug, Clone, Copy)]
pub struct GMVersion {
    /// The most significant version part.
    /// This can be 1, 2, or a year after 2021.
    ///
    /// If greater than 1, serialization produces "2.0.0.0" due to the flag no
    /// longer updating in `data.win`.
    pub major: u32,

    /// The second-most significant version part.
    pub minor: u32,

    /// The third-most significant version part.
    pub release: u32,

    /// The fourth-most (least) significant version part.
    pub build: u32,

    /// Different GameMaker release branches.
    /// LTS has some but not all features of equivalent newer versions.
    /// See [`LTSBranch`] for more information.
    pub branch: LtsBranch,
}

impl GMVersion {
    // HACK: Make these functions `const` when Into, PartialEq and PartialOrd are const-stable.

    /// The GameMaker Version 2.0.0.0 (Pre LTS).
    pub const GMS2: Self = Self::new(2, 0, 0, 0, LtsBranch::Pre2022);
    /// The pseudo-version "infinity" (highest possible values).
    ///
    /// This is only useful for comparing against other `GMVersion`s dynamically.
    pub const INF: Self = Self::new(u32::MAX, u32::MAX, u32::MAX, u32::MAX, LtsBranch::PostLts);
    /// The pseudo-version 0.0.0.0 (Pre LTS).
    ///
    /// This is only useful for comparing against other `GMVersion`s dynamically.
    pub const ZERO: Self = Self::new(0, 0, 0, 0, LtsBranch::Pre2022);

    /// Creates a new [`GMVersion`] with the given version parts and branch.
    #[must_use]
    pub const fn new(major: u32, minor: u32, release: u32, build: u32, branch: LtsBranch) -> Self {
        Self { major, minor, release, build, branch }
    }

    /// Sets this [`GMVersion`] to the specified [`GMVersion`].
    ///
    /// The LTS branch is only updated if the new branch
    /// is greater than the current branch.
    pub fn set_version(&mut self, new_version: impl ToGMVersion) {
        let new: Self = new_version.into_gm_version();
        self.major = new.major;
        self.minor = new.minor;
        self.release = new.release;
        self.build = new.build;
        if new.branch != LtsBranch::Pre2022 {
            self.branch = new.branch;
        }

        // Set the LTS branch properly.
        if *self >= (2023, 1) && self.branch == LtsBranch::Pre2022 {
            self.branch = LtsBranch::Lts2022;
        }
    }
}

impl GMElement for GMVersion {
    fn deserialize(reader: &mut DataReader) -> Result<Self> {
        let major = reader.read_u32()?;
        let minor = reader.read_u32()?;
        let release = reader.read_u32()?;
        let build = reader.read_u32()?;
        // Since the GEN8 Version is stuck on maximum 2.0.0.0; LTS will (initially)
        // always be PreLTS
        Ok(Self::new(major, minor, release, build, LtsBranch::Pre2022))
    }

    fn serialize(&self, builder: &mut DataBuilder) -> Result<()> {
        builder.write_u32(self.major);
        builder.write_u32(self.minor);
        builder.write_u32(self.release);
        builder.write_u32(self.build);
        Ok(())
    }
}

impl Default for GMVersion {
    fn default() -> Self {
        Self::GMS2
    }
}

impl Display for GMVersion {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let major = self.major;
        let minor = self.minor;
        let release = self.release;
        let build = self.build;
        write!(f, "{major}.{minor}")?;
        if release != 0 || build != 0 {
            write!(f, ".{release}")?;
            if build != 0 {
                write!(f, ".{build}")?;
            }
        }
        if self.major >= 2022 {
            write!(f, " [{}]", self.branch)?;
        }
        Ok(())
    }
}

impl<T: ToGMVersion> PartialEq<T> for GMVersion {
    fn eq(&self, other: &T) -> bool {
        let a = self;
        let b = other.to_gm_version();
        a.major == b.major
            && a.minor == b.minor
            && a.release == b.release
            && a.build == b.build
            && a.branch == b.branch
    }
}

/// This is not symmetrical due to branches!
/// The predicate should always be RHS.
impl<T: ToGMVersion> PartialOrd<T> for GMVersion {
    fn partial_cmp(&self, other: &T) -> Option<Ordering> {
        compare(self, &other.to_gm_version())
    }
}

fn compare(a: &GMVersion, b: &GMVersion) -> Option<Ordering> {
    if b.branch == LtsBranch::PostLts && a.branch != LtsBranch::PostLts {
        return Some(Ordering::Less);
    }
    match a.major.partial_cmp(&b.major) {
        Some(Ordering::Equal) => {}
        ord => return ord,
    }
    match a.minor.partial_cmp(&b.minor) {
        Some(Ordering::Equal) => {}
        ord => return ord,
    }
    match a.release.partial_cmp(&b.release) {
        Some(Ordering::Equal) => {}
        ord => return ord,
    }
    a.build.partial_cmp(&b.build)
}

pub trait ToGMVersion: Copy {
    #[must_use]
    fn to_gm_version(&self) -> GMVersion;

    #[must_use]
    fn into_gm_version(self) -> GMVersion {
        self.to_gm_version()
    }
}

impl ToGMVersion for GMVersion {
    fn to_gm_version(&self) -> GMVersion {
        *self
    }

    fn into_gm_version(self) -> GMVersion {
        self
    }
}

impl ToGMVersion for &GMVersion {
    fn to_gm_version(&self) -> GMVersion {
        **self
    }
}

impl ToGMVersion for (u32, u32, u32, u32, LtsBranch) {
    fn to_gm_version(&self) -> GMVersion {
        GMVersion::new(self.0, self.1, self.2, self.3, self.4)
    }
}

impl ToGMVersion for (u32, u32, u32, LtsBranch) {
    fn to_gm_version(&self) -> GMVersion {
        GMVersion::new(self.0, self.1, self.2, 0, self.3)
    }
}

impl ToGMVersion for (u32, u32, LtsBranch) {
    fn to_gm_version(&self) -> GMVersion {
        GMVersion::new(self.0, self.1, 0, 0, self.2)
    }
}

impl ToGMVersion for (u32, u32, u32, u32) {
    fn to_gm_version(&self) -> GMVersion {
        GMVersion::new(self.0, self.1, self.2, self.3, LtsBranch::Pre2022)
    }
}

impl ToGMVersion for (u32, u32, u32) {
    fn to_gm_version(&self) -> GMVersion {
        GMVersion::new(self.0, self.1, self.2, 0, LtsBranch::Pre2022)
    }
}

impl ToGMVersion for (u32, u32) {
    fn to_gm_version(&self) -> GMVersion {
        GMVersion::new(self.0, self.1, 0, 0, LtsBranch::Pre2022)
    }
}

impl ToGMVersion for u32 {
    fn to_gm_version(&self) -> GMVersion {
        GMVersion::new(*self, 0, 0, 0, LtsBranch::Pre2022)
    }
}