use crate::gml::Code;
use crate::gml::instruction::AssetReference;
use crate::gml::instruction::CodeVariable;
use crate::gml::instruction::ComparisonType;
use crate::gml::instruction::DataType;
use crate::gml::instruction::InstanceType;
use crate::gml::instruction::Instruction;
use crate::gml::instruction::PushValue;
use crate::gml::instruction::VariableType;
use crate::prelude::*;
use crate::util::fmt::typename;
use crate::wad::data::GMData;
use crate::wad::elem::GMNamedElement;
use crate::wad::elem::function::Function;
use crate::wad::elem::game_object::GameObject;
use crate::wad::elem::string::Strings;
use crate::wad::elem::variable::Variable;
macro_rules! write {
($buffer:ident, $($args:tt)*) => {{
use std::fmt::Write as _;
let _ = $buffer.write_fmt(format_args!($($args)*));
}};
}
fn slice_instructions_by_bytes(
instructions: &[Instruction],
start_offset: u32,
) -> Result<&[Instruction]> {
let mut index = 0;
let mut offset = 0;
while offset < start_offset {
#[rustfmt::skip] let instr = instructions.get(index).ok_or_else(||
format!("Given start byte offset {start_offset} is out of range in instructions with byte length {offset}"
))?;
index += 1;
offset += instr.size();
}
if offset != start_offset {
bail!(
"Given start byte offset {start_offset} is misaligned (reached offset {offset} \
instead)"
);
}
Ok(&instructions[index..])
}
pub fn disassemble_code(code: &Code, gm_data: &GMData) -> Result<String> {
if let Some(data) = &code.modern_data {
if data.parent.is_some() {
if data.execution_offset == 0 {
bail!("Child code entry has byte offset zero");
}
let parent: &Code = gm_data
.codes
.by_ref(data.parent)
.ctx("resolving parent code entry")?;
let instrs = slice_instructions_by_bytes(&parent.instructions, data.execution_offset)?;
return disassemble_instructions(instrs, gm_data);
} else if data.execution_offset != 0 {
bail!(
"Root code entry has non-zero byte offset {}",
data.execution_offset
);
}
}
disassemble_instructions(&code.instructions, gm_data)
.ctx(|| format!("disassembling code entry {:?}", code.name))
}
pub fn disassemble_instructions(instructions: &[Instruction], gm_data: &GMData) -> Result<String> {
let mut assembly: String = String::new();
for instruction in instructions {
disassemble_instruction_to_buffer(&mut assembly, instruction, gm_data)?;
assembly.push('\n');
}
Ok(assembly)
}
pub fn disassemble_instruction(instruction: &Instruction, gm_data: &GMData) -> Result<String> {
let mut buffer = String::new();
disassemble_instruction_to_buffer(&mut buffer, instruction, gm_data)?;
Ok(buffer)
}
#[allow(clippy::too_many_lines)]
pub fn disassemble_instruction_to_buffer(
buffer: &mut String,
instruction: &Instruction,
gm_data: &GMData,
) -> Result<()> {
let mnemonic: &str = instruction.mnemonic();
match *instruction {
Instruction::Exit
| Instruction::Return
| Instruction::PopSwap { .. }
| Instruction::PopWithContextExit
| Instruction::CheckArrayIndex
| Instruction::PushArrayFinal
| Instruction::PopArrayFinal
| Instruction::PushArrayContainer
| Instruction::SetArrayOwner
| Instruction::HasStaticInitialized
| Instruction::SetStaticInitialized
| Instruction::SaveArrayReference
| Instruction::RestoreArrayReference
| Instruction::IsNullishValue => {
buffer.push_str(mnemonic);
}
Instruction::Negate { data_type }
| Instruction::Not { data_type }
| Instruction::PopDiscard { data_type } => {
write!(buffer, "{}.{}", mnemonic, data_type.as_char());
}
Instruction::CallVariable { arg_count } => {
write!(buffer, "{mnemonic} {arg_count}");
}
Instruction::Duplicate { data_type, size } => {
write!(buffer, "{}.{} {}", mnemonic, data_type.as_char(), size);
}
Instruction::DuplicateSwap { data_type, size1, size2 } => {
write!(
buffer,
"{}.{} {} {}",
mnemonic,
data_type.as_char(),
size1,
size2,
);
}
Instruction::Branch { jump_offset }
| Instruction::BranchIf { jump_offset }
| Instruction::BranchUnless { jump_offset }
| Instruction::PushWithContext { jump_offset }
| Instruction::PopWithContext { jump_offset } => {
write!(buffer, "{mnemonic} {jump_offset}");
}
Instruction::Convert { from: type1, to: type2 }
| Instruction::Multiply { lhs: type2, rhs: type1 }
| Instruction::Divide { lhs: type2, rhs: type1 }
| Instruction::Remainder { lhs: type2, rhs: type1 }
| Instruction::Modulus { lhs: type2, rhs: type1 }
| Instruction::Add { lhs: type2, rhs: type1 }
| Instruction::Subtract { lhs: type2, rhs: type1 }
| Instruction::And { lhs: type2, rhs: type1 }
| Instruction::Or { lhs: type2, rhs: type1 }
| Instruction::Xor { lhs: type2, rhs: type1 }
| Instruction::ShiftLeft { lhs: type2, rhs: type1 }
| Instruction::ShiftRight { lhs: type2, rhs: type1 } => {
write!(
buffer,
"{}.{}.{}",
mnemonic,
type1.as_char(),
type2.as_char()
);
}
Instruction::Compare { lhs, rhs, comparison_type } => {
write!(
buffer,
"{}.{}.{} {}",
mnemonic,
rhs.as_char(),
lhs.as_char(),
comparison_type.as_str(),
);
}
Instruction::Pop { variable, type1, type2 } => {
write!(
buffer,
"{}.{}.{} ",
mnemonic,
type1.as_char(),
type2.as_char()
);
write_variable(variable, buffer, gm_data)?;
}
Instruction::Push { value } => {
write!(buffer, "{}.{} ", mnemonic, value.data_type().as_char());
write_push_instruction(value, buffer, gm_data)?;
}
Instruction::PushLocal { variable }
| Instruction::PushGlobal { variable }
| Instruction::PushBuiltin { variable } => {
write!(buffer, "{mnemonic} ");
write_variable(variable, buffer, gm_data)?;
}
Instruction::PushImmediate { integer } => {
write!(buffer, "{mnemonic} {integer}");
}
Instruction::Call { function, arg_count } => {
write!(
buffer,
"{} {} {}",
mnemonic,
resolve_function_name(function, gm_data)?,
arg_count,
);
}
Instruction::PushReference { asset_reference } => {
write!(buffer, "{mnemonic} ");
write_asset_reference(asset_reference, buffer, gm_data)?;
}
}
Ok(())
}
impl Instruction {
#[must_use]
const fn mnemonic(&self) -> &'static str {
match self {
Self::Convert { .. } => "conv",
Self::Multiply { .. } => "mul",
Self::Divide { .. } => "div",
Self::Remainder { .. } => "rem",
Self::Modulus { .. } => "mod",
Self::Add { .. } => "add",
Self::Subtract { .. } => "sub",
Self::And { .. } => "and",
Self::Or { .. } => "or",
Self::Xor { .. } => "xor",
Self::Negate { .. } => "neg",
Self::Not { .. } => "not",
Self::ShiftLeft { .. } => "shl",
Self::ShiftRight { .. } => "shr",
Self::Compare { .. } => "cmp",
Self::Pop { .. } => "pop",
Self::PopSwap { is_array: false } => "popswap",
Self::PopSwap { is_array: true } => "popswaparr",
Self::Duplicate { .. } => "dup",
Self::DuplicateSwap { .. } => "dupswap",
Self::Return => "ret",
Self::Exit => "exit",
Self::PopDiscard { .. } => "popz",
Self::Branch { .. } => "br",
Self::BranchIf { .. } => "bt",
Self::BranchUnless { .. } => "bf",
Self::PushWithContext { .. } => "pushenv",
Self::PopWithContext { .. } => "popenv",
Self::PopWithContextExit => "popenvexit",
Self::Push { .. } => "push",
Self::PushLocal { .. } => "pushloc",
Self::PushGlobal { .. } => "pushglb",
Self::PushBuiltin { .. } => "pushbltn",
Self::PushImmediate { .. } => "pushim",
Self::Call { .. } => "call",
Self::CallVariable { .. } => "callvar",
Self::CheckArrayIndex => "chkindex",
Self::PushArrayFinal => "pushaf",
Self::PopArrayFinal => "popaf",
Self::PushArrayContainer => "pushac",
Self::SetArrayOwner => "setowner",
Self::HasStaticInitialized => "isstaticok",
Self::SetStaticInitialized => "setstatic",
Self::SaveArrayReference => "savearef",
Self::RestoreArrayReference => "restorearef",
Self::IsNullishValue => "isnullish",
Self::PushReference { .. } => "pushref",
}
}
}
impl DataType {
#[must_use]
const fn as_char(self) -> char {
match self {
Self::Int16 => 'e',
Self::Int32 => 'i',
Self::Int64 => 'l',
Self::Double => 'd',
Self::Bool => 'b',
Self::String => 's',
Self::Variable => 'v',
}
}
}
impl ComparisonType {
#[must_use]
const fn as_str(self) -> &'static str {
match self {
Self::LessThan => "LT",
Self::LessOrEqual => "LTE",
Self::Equal => "EQ",
Self::NotEqual => "NEQ",
Self::GreaterOrEqual => "GTE",
Self::GreaterThan => "GT",
}
}
}
impl VariableType {
#[must_use]
const fn as_str(self) -> &'static str {
match self {
Self::Normal | Self::Instance => "",
Self::Array => "[array]",
Self::StackTop => "[stacktop]",
Self::MultiPush => "[arraypushaf]",
Self::MultiPop => "[arraypopaf]",
}
}
}
fn write_push_instruction(value: PushValue, buffer: &mut String, gm_data: &GMData) -> Result<()> {
match value {
PushValue::Int16(integer) => {
write!(buffer, "{integer}");
}
PushValue::Int32(integer) => {
write!(buffer, "{integer}");
}
PushValue::Int64(integer) => {
write!(buffer, "{integer}");
}
PushValue::Double(float) => {
write!(buffer, "{float}");
}
PushValue::Bool(true) => {
write!(buffer, "true");
}
PushValue::Bool(false) => {
write!(buffer, "false");
}
PushValue::String(string) => write_gm_string(string, &gm_data.strings, buffer)?,
PushValue::Variable(code_variable) => {
write_variable(code_variable, buffer, gm_data)?;
}
PushValue::Function(function_ref) => {
write!(
buffer,
"(function){}",
resolve_function_name(function_ref, gm_data)?
);
}
}
Ok(())
}
fn asset_get_name<'a, T, C>(
chunk: &C,
gm_ref: GMRef<T>,
gm_strings: &'a Strings,
) -> Result<&'a String>
where
T: GMNamedElement + 'a,
C: GMNamedListChunk<Element = T>,
{
const CTX: &str = "resolving asset reference for PushReference Instruction";
let element: &T = chunk.by_ref(gm_ref).ctx(CTX)?;
element
.validate_name(gm_strings)
.ctx(|| format!("validating name of {}", typename::<T>()))
.ctx(CTX)?;
let name: &'a String = element.name(gm_strings)?;
Ok(name)
}
#[expect(clippy::too_many_lines)]
fn write_asset_reference(
asset_ref: AssetReference,
buffer: &mut String,
gm_data: &GMData,
) -> Result<()> {
let s = &gm_data.strings;
match asset_ref {
AssetReference::Object(gm_ref) => {
write!(
buffer,
"(object){}",
asset_get_name(&gm_data.game_objects, gm_ref, s)?
);
}
AssetReference::Sprite(gm_ref) => {
write!(
buffer,
"(sprite){}",
asset_get_name(&gm_data.sprites, gm_ref, s)?
);
}
AssetReference::Sound(gm_ref) => {
write!(
buffer,
"(sound){}",
asset_get_name(&gm_data.sounds, gm_ref, s)?
);
}
AssetReference::Room(gm_ref) => {
write!(
buffer,
"(room){}",
asset_get_name(&gm_data.rooms, gm_ref, s)?
);
}
AssetReference::Background(gm_ref) => {
write!(
buffer,
"(background){}",
asset_get_name(&gm_data.tilesets, gm_ref, s)?
);
}
AssetReference::Path(gm_ref) => {
write!(
buffer,
"(path){}",
asset_get_name(&gm_data.paths, gm_ref, s)?
);
}
AssetReference::Script(gm_ref) => {
write!(
buffer,
"(script){}",
asset_get_name(&gm_data.scripts, gm_ref, s)?
);
}
AssetReference::Font(gm_ref) => {
write!(
buffer,
"(font){}",
asset_get_name(&gm_data.fonts, gm_ref, s)?
);
}
AssetReference::Timeline(gm_ref) => {
write!(
buffer,
"(timeline){}",
asset_get_name(&gm_data.timelines, gm_ref, s)?
);
}
AssetReference::Shader(gm_ref) => {
write!(
buffer,
"(shader){}",
asset_get_name(&gm_data.shaders, gm_ref, s)?
);
}
AssetReference::Sequence(gm_ref) => {
write!(
buffer,
"(sequence){}",
asset_get_name(&gm_data.sequences, gm_ref, s)?
);
}
AssetReference::AnimCurve(gm_ref) => {
write!(
buffer,
"(animcurve){}",
asset_get_name(&gm_data.animation_curves, gm_ref, s)?
);
}
AssetReference::ParticleSystem(gm_ref) => {
write!(
buffer,
"(particlesystem){}",
asset_get_name(&gm_data.particle_systems, gm_ref, s)?
);
}
AssetReference::RoomInstance(id) => {
write!(buffer, "(roominstance){id}");
}
AssetReference::Function(gm_ref) => {
write!(
buffer,
"(function){}",
resolve_function_name(gm_ref, gm_data)?
);
}
}
Ok(())
}
fn write_instance_type(
instance_type: InstanceType,
buffer: &mut String,
variable_ref: GMRef<Variable>,
gm_data: &GMData,
) -> Result<()> {
match instance_type {
InstanceType::Self_ => write!(buffer, "self"),
InstanceType::GameObject(obj_ref) => {
let obj: &GameObject = gm_data.game_objects.by_ref(obj_ref)?;
obj.validate_name(&gm_data.strings)
.ctx("validating game object name")?;
write!(buffer, "object<{}>", obj.name(&gm_data.strings)?);
}
InstanceType::RoomInstance(instance_id) => {
write!(buffer, "roominstance<{}>", instance_id.0);
}
InstanceType::Local => write!(buffer, "local<{}>", variable_ref.index),
InstanceType::Other => write!(buffer, "other"),
InstanceType::All => write!(buffer, "all"),
InstanceType::None => write!(buffer, "none"),
InstanceType::Global => write!(buffer, "global"),
InstanceType::Builtin => write!(buffer, "builtin"),
InstanceType::StackTop => write!(buffer, "stacktop"),
InstanceType::Argument => write!(buffer, "arg"),
InstanceType::Static => write!(buffer, "static"),
}
Ok(())
}
fn write_variable(
code_variable: CodeVariable,
buffer: &mut String,
gm_data: &GMData,
) -> Result<()> {
let variable: &Variable = gm_data.variables.by_ref(code_variable.variable)?;
variable
.validate_name(&gm_data.strings)
.ctx("validating variable identifier")?;
let name: &String = variable.name(&gm_data.strings)?;
if code_variable.is_int32 {
write!(buffer, "(variable)");
}
write!(buffer, "{}", code_variable.variable_type.as_str());
write_instance_type(
code_variable.instance_type,
buffer,
code_variable.variable,
gm_data,
)?;
write!(buffer, ".{name}");
Ok(())
}
fn resolve_function_name(function_ref: GMRef<Function>, gm_data: &GMData) -> Result<&String> {
let function: &Function = gm_data.functions.by_ref(function_ref)?;
function
.validate_name(&gm_data.strings)
.ctx("validating function identifier")?;
let name: &String = function.name(&gm_data.strings)?;
Ok(name)
}
fn write_gm_string(
string_ref: GMRef<String>,
gm_strings: &Strings,
buffer: &mut String,
) -> Result<()> {
let string: &String = gm_strings.by_ref(string_ref)?;
write_literal_string(string, buffer);
write!(buffer, "@{}", string_ref.index);
Ok(())
}
fn write_literal_string(string_lit: &str, buffer: &mut String) {
if !string_lit
.bytes()
.any(|b| matches!(b, b'\\' | b'"' | b'\n' | b'\r' | b'\t'))
{
write!(buffer, "\"{string_lit}\"");
return;
}
buffer.reserve(string_lit.len() + string_lit.len() / 4 + 2);
buffer.push('"');
for c in string_lit.chars() {
match c {
'\\' => buffer.push_str("\\\\"),
'"' => buffer.push_str("\\\""),
'\x07' => buffer.push_str("\\a"), '\x08' => buffer.push_str("\\b"), '\t' => buffer.push_str("\\t"), '\n' => buffer.push_str("\\n"), '\x0B' => buffer.push_str("\\v"), '\x0C' => buffer.push_str("\\f"), '\r' => buffer.push_str("\\r"), _ => buffer.push(c),
}
}
buffer.push('"');
}