libgm 0.7.1

A tool for unpacking and modding GameMaker games
Documentation
// SPDX-License-Identifier: GPL-3.0-only
//! Deltarune: Chapter 3 & 4
//! Chapters 1, 2, 4 (3 needs extra handling)
//! 2025-06-05 to now [2026-01-04]

use crate::gml::Code;
use crate::gml::assembly::assemble_instructions;
use crate::gml::insert_instructions;
use crate::gml::instruction::InstanceType;
use crate::gml::instruction::Instruction;
use crate::gml::instruction::VariableType;
use crate::prelude::*;
use crate::wad::elem::variable::Variable;

pub fn toggle(data: &mut GMData, enable: bool) -> Result<()> {
    log::debug!("Detected Deltarune Chapter 1/2/4");
    let code_ref: GMRef<Code> = data
        .codes
        .ref_by_name("gml_Object_obj_initializer2_Create_0", &data.strings)?;
    super::replace_debug(data, code_ref, enable, InstanceType::Global)?;

    if !enable
        || data
            .codes
            .by_name("gml_Script_scr_flag_name_get", &data.strings)
            .is_err()
    {
        return Ok(());
    }

    // need to modify flag retrieval code (currently only used in chapter 4)
    let code_ref = data
        .codes
        .ref_by_name("gml_GlobalScript_scr_flag_get", &data.strings)?;
    let code = data.codes.by_ref(code_ref)?;

    let index: u32 = find_insertion_point(data, code)?;

    let assembly: &str = r#"
        push.s "flagname"
        conv.s.v
        call variable_global_exists 1
        conv.v.b
        not.b
        bf 5
        push.s ""
        conv.s.v
        ret
    "#;
    let insertion: Vec<Instruction> = assemble_instructions(assembly, data)?;

    let code = data.codes.by_ref_mut(code_ref)?;
    insert_instructions(&mut code.instructions, index, &insertion)?;
    bail!(
        "TODO: debug activation for deltarune is currently broken (need to fix child gml_Script \
         execution offsets)"
    );
}

fn find_insertion_point(data: &GMData, code: &Code) -> Result<u32> {
    for i in 0..code.instructions.len() {
        let instr = &code.instructions[i];
        let Some(code_variable) = instr.variable() else {
            continue;
        };
        let variable: &Variable = data.variables.by_ref(code_variable.variable)?;
        let var_name = data.strings.by_ref(variable.name)?;
        if var_name != "flagname" {
            continue;
        }
        // (assumes instancetype global)

        let ref_type = code_variable.variable_type;
        if ref_type != VariableType::Array {
            bail!("Expected array access for global.flagname, got {ref_type:?}");
        }

        // Found `push.v [array]global.flagname`
        let pred = |i: &Instruction| matches!(i, Instruction::BranchUnless { .. });
        let branch_index = code.instructions[..i].iter().rposition(pred);
        let idx = branch_index.ok_or("No branch instruction before global.flagname push")?;
        if i - idx > 8 {
            // branch is too far away, code is probably malformed
            bail!("Could not find a branch near (before) global.flagname push");
        }

        // nice, found the insertion point
        return Ok(idx as u32 + 1);
    }

    Err(err!("Could not find global.flagname push"))
}

// vanilla assembly looks like this (snippet):
//
// pushglb.v global.is_console
// conv.v.b
// not.b
// bf [10]                       // BRANCH INSTRUCTION
//
// >>>> should insert instructions here <<<<
//
// :[5]
// pushi.e -1
// push.v arg.argument0
// conv.v.i
// push.v [array]self.flagname   // PUSH INTO FLAGNAME
// pop.v.v local.v
// pushloc.v local.v
// call.i is_undefined(argc=1)
// conv.v.b
// bf [7]
//
// :[6]
// push.s "*unknown flag*"@50
// conv.s.v
// b [8]
//
// :[7]
// pushloc.v local.v
//
// :[8]
// ret.v
//