libgm 0.7.2

A tool for unpacking and modding GameMaker games
Documentation
// SPDX-License-Identifier: GPL-3.0-only
//! Everything related to GML (GameMaker language) bytecode.
//!
//! DOCME: explain more.

pub mod analysis;
pub mod assembly;
pub mod instruction;
mod name_validation;
pub mod opcodes;

use std::ops::Range;

pub use crate::gml::instruction::Instruction;
use crate::prelude::*;
use crate::wad::elem::function::CodeLocal;

/// A code entry in a GameMaker data file.
#[derive(Debug, Clone, PartialEq)]
pub struct Code {
    /// A mangled name for this code entry.
    ///
    /// This will be something like `gml_Script_my_script123` or `gml_Object_my_object456_Step_0`.
    /// The runner uses this name for stack traces in error messages.
    /// Changing this name will not change the script name, event type or similar.
    /// This name exists purely for debugging purposes.
    /// It is not an asset name like sprites or objects have one.
    /// In some cases, it can even be ambiguous (Collision events)!
    /// A demangler is coming soon(TM).
    pub name: GMRef<String>,

    /// A list of VM instructions this code entry has.
    ///
    /// This will be empty for child code entries.
    /// The actual instructions will be stored in the referenced parent code.
    pub instructions: Vec<Instruction>,

    /// Extra data for WAD 15+.
    pub modern_data: Option<ModernData>,
}

impl Code {
    /// Find child code entries of this code entry.
    ///
    /// This is always `false` before WAD 15, since child/parent code entries
    /// did not exist then.
    ///
    /// This function has to compare the names of code entries and is also
    /// failable. If you have access to a [`GMRef`] to this code entry
    /// instead, consider using [`Self::find_children`] instead.
    ///
    /// This has to iterate over all code entries in the data file,
    /// so it's a good idea to cache this if possible.
    pub fn find_children_by_name(&self, data: &GMData) -> Result<Vec<GMRef<Self>>> {
        if data.general_info.wad_version < 15 {
            return Ok(Vec::new());
        }

        let mut children: Vec<GMRef<Self>> = Vec::new();

        for (gmref, code_entry) in data.codes.element_refs() {
            if !code_entry.is_root() {
                continue;
            }
            let parent = data.codes.by_ref(code_entry.parent())?;
            if self.name == parent.name {
                children.push(gmref);
            }
        }

        Ok(children)
    }

    /// Find child code entries of this code entry.
    ///
    /// This is always `false` before WAD 15, since child/parent code entries
    /// did not exist then.
    ///
    /// This function takes a `GMRef<Code>` instead of `&Code`.
    /// If you only have a [`Code`] available, you'll have to
    /// use [`Self::find_children_by_name`] instead.
    ///
    /// This has to iterate over all code entries in the data file,
    /// so it's a good idea to cache this if possible.
    #[must_use]
    pub fn find_children(code_ref: GMRef<Self>, data: &GMData) -> Vec<GMRef<Self>> {
        if data.general_info.wad_version < 15 || code_ref.is_none() {
            return Vec::new();
        }

        let mut children: Vec<GMRef<Self>> = Vec::new();

        for (gmref, code_entry) in data.codes.element_refs() {
            if code_entry.parent() == code_ref {
                children.push(gmref);
            }
        }

        children
    }

    /// Gets the total (cumulative) size of all instructions, in bytes.
    ///
    /// This function simply calls [`Instruction::size`] on each instruction and
    /// sums up the sizes.
    #[must_use]
    pub fn length(&self) -> u32 {
        instructions_size(&self.instructions)
    }

    /// The parent code entry of this code entry, if it has one.
    ///
    /// This will always be [`GMRef::none`] for WAD < 15.
    #[must_use]
    pub const fn parent(&self) -> GMRef<Self> {
        match &self.modern_data {
            Some(data) => data.parent,
            None => GMRef::none(),
        }
    }

    /// Whether this code entry is a root entry, meaning it has no parent code
    /// entries.
    ///
    /// This will always be `true` for WAD < 15.
    #[must_use]
    pub const fn is_root(&self) -> bool {
        self.parent().is_none()
    }

    /// The offset, **in bytes**, where code should begin
    /// executing from within the bytecode of this code entry.
    ///
    /// This will always be zero for root code entries and before WAD 15.
    #[must_use]
    pub const fn execution_offset(&self) -> u32 {
        match &self.modern_data {
            Some(data) => data.execution_offset,
            None => 0,
        }
    }
}

impl GMData {
    pub fn make_code(&mut self, name: &str, instructions: Vec<Instruction>) -> GMRef<Code> {
        if let Ok(code) = self.codes.ref_by_name(name, &self.strings) {
            return code;
        }

        let name = self.strings.make(name);
        let modern_data = if self.general_info.wad_version >= 15 {
            Some(ModernData::default())
        } else {
            None
        };
        let code = Code { name, instructions, modern_data };
        let code_ref = self.codes.push(code);
        self.functions
            .code_locals
            .push(CodeLocal { name, variables: Vec::new() });
        code_ref
    }
}

/// Extra data for code entries in WAD Version 15 and higher.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ModernData {
    /// The amount of local variables this code entry has.
    pub local_count: u16,

    /// The amount of arguments this code entry accepts.
    pub argument_count: u16,

    /// A flag set on certain code entries, which usually don't have locals attached to them.
    ///
    /// DOCME: more info pls
    pub weird_local_flag: bool,

    /// Offset, **in bytes**, where code should begin executing from within the
    /// bytecode of this code entry. Should be 0 for root-level (parent)
    /// code entries, and nonzero for child code entries.
    pub execution_offset: u32,

    /// Parent entry of this code entry, if this is a child entry,
    pub parent: GMRef<Code>,
}

/// Gets the total (cumulative) size of all instructions, in bytes.
///
/// This function simply calls [`Instruction::size`] on each instruction and
/// sums up the sizes.
#[must_use]
pub fn instructions_size(instructions: &[Instruction]) -> u32 {
    let mut size: u32 = 0;
    for instruction in instructions {
        size += instruction.size();
    }
    size
}

fn splice_instructions(
    haystack: &mut Vec<Instruction>,
    range: Range<u32>,
    replace_with: &[Instruction],
    needs_result: bool,
) -> Result<Option<Vec<Instruction>>> {
    let start = range.start as usize;
    let end = range.end as usize;
    let len = haystack.len();

    if start >= len {
        bail!("Start index {start} out of bounds for vector with instruction count {len}");
    }

    if start > end {
        bail!("Start index {start} is greater than the end index {end}");
    }

    let insertion_size = instructions_size(replace_with);
    let removal_size = instructions_size(&haystack[start..end]);
    let fixed_offset = (insertion_size - removal_size) as i32 / 4;

    let first_half_size = instructions_size(&haystack[..start]) as i32 / 4;

    let mut cur_pos: u32 = 0;
    for (i, instr) in haystack.iter_mut().enumerate() {
        // (this technically ignores stuff that is neither half 1 nor 2 if range is
        // nonzero but it shouldnt make a different i think)
        cur_pos += instr.size4();
        let Some(offset) = instr.jump_offset_mut() else {
            continue;
        };

        let branch_target_pos = cur_pos as i32 + *offset;
        let origin_is_first_half = i < start;
        let target_is_first_half = branch_target_pos < first_half_size;

        // if branching withing their half, everything is fine.
        if origin_is_first_half == target_is_first_half {
            continue;
        }

        // branch crosses boundary; fix the jump offsets.
        if origin_is_first_half {
            *offset += fixed_offset;
        } else {
            *offset -= fixed_offset;
        }
    }

    // now perform the actual splice
    let iter = haystack.splice(start..end, replace_with.iter().cloned());
    if needs_result {
        Ok(Some(iter.collect()))
    } else {
        Ok(None)
    }
}

pub fn insert_instructions(
    haystack: &mut Vec<Instruction>,
    index: u32,
    insertion: &[Instruction],
) -> Result<()> {
    splice_instructions(haystack, index..index, insertion, false).ctx(|| {
        format!(
            "inserting {} instructions at index {} into vector with {} instructions",
            insertion.len(),
            index,
            haystack.len(),
        )
    })?;
    Ok(())
}

pub fn insert_instruction(
    haystack: &mut Vec<Instruction>,
    index: u32,
    insertion: &Instruction,
) -> Result<()> {
    insert_instructions(haystack, index, std::slice::from_ref(insertion))
        .ctx(|| format!("inserting single instruction {insertion:?}"))
}

#[allow(clippy::missing_panics_doc)]
pub fn remove_instructions(
    haystack: &mut Vec<Instruction>,
    range: Range<u32>,
) -> Result<Vec<Instruction>> {
    let removal_len = range.len();
    let index = range.start;
    let old_instrs = splice_instructions(haystack, range, &[], true).ctx(|| {
        format!(
            "removing {} instructions at index {} of vector with {} instructions",
            removal_len,
            index,
            haystack.len(),
        )
    })?;
    Ok(old_instrs.unwrap())
}

pub fn remove_instruction(haystack: &mut Vec<Instruction>, index: u32) -> Result<Vec<Instruction>> {
    remove_instructions(haystack, index..index + 1)
}