redguard-preservation 0.10.0

A CLI tool to preserve Redguard files
Documentation
use rgpre::import::{
    rgm::script_program::{CallKind, Object, Operation, Place, Value, decode_script_step},
    soup_def::parse_soup_def,
};

fn definition() -> rgpre::import::soup_def::SoupDef {
    parse_soup_def(
        "[functions]\ntask Wait parms 2\nfunction Query parms 0\n[flags]\nNUMBER Counter 3",
    )
    .unwrap()
}

#[test]
fn parameter_modes_and_nested_functions_preserve_values() {
    let step = decode_script_step(
        &[0, 1, 0, 2, 6, 0, 0, 0, 0, 10, 3, 0, 0, 0],
        0,
        &definition(),
    )
    .unwrap();
    assert_eq!(step.next, 14);
    let Operation::Call { call } = step.operation else {
        panic!("call expected")
    };
    assert_eq!(call.kind, CallKind::Task);
    assert_eq!(call.target, Object::Me);
    assert_eq!(
        call.arguments,
        [
            Value::Read {
                place: Place::Flag { index: 0 }
            },
            Value::Read {
                place: Place::Local { index: 3 }
            }
        ]
    );
    let step = decode_script_step(&[6, 0, 0, 2, 2, 0, 0, 0], 0, &definition()).unwrap();
    let Operation::Assign { formula, .. } = step.operation else {
        panic!("assignment expected")
    };
    assert!(matches!(formula.first, Value::Call { .. }));
    assert_eq!(step.next, 8);
}

#[test]
fn target_calls_preserve_dispatch_and_low_reference_byte() {
    let step = decode_script_step(&[25, 4, 3, 2, 2, 0, 0, 99], 0, &definition()).unwrap();
    assert_eq!(step.next, 7);
    let Operation::ObjectCall { call } = step.operation else {
        panic!("call expected")
    };
    assert_eq!(call.target, Object::Named { string: 3 });
    assert_eq!(call.kind, CallKind::Function);
    let step = decode_script_step(&[15, 0, 0, 9, 255], 0, &definition()).unwrap();
    assert_eq!(
        step.operation,
        Operation::Increment {
            destination: Place::Property {
                object: Object::Me,
                reference: 9
            },
            delta: 1
        }
    );
}

#[test]
fn branch_decoding_does_not_consume_its_body() {
    let step = decode_script_step(
        &[3, 7, 1, 0, 0, 0, 0, 7, 2, 0, 0, 0, 0, 18, 0, 0, 0, 18],
        0,
        &definition(),
    )
    .unwrap();
    assert_eq!(step.next, 17);
    let Operation::If { conditions, target } = step.operation else {
        panic!("if expected")
    };
    assert_eq!(target, 18);
    assert_eq!(conditions.len(), 1);
}

#[test]
fn truncation_wrong_counts_and_unknown_encodings_fail_in_context() {
    for bytes in [&[0, 1, 0, 1][..], &[5, 1][..], &[255][..], &[30, 0][..]] {
        let error = decode_script_step(bytes, 0, &definition()).unwrap_err();
        assert!(error.contains("instruction 0"), "{error}");
    }
}

#[test]
fn program_traversal_rejects_branches_into_operands_and_keeps_unknowns() {
    use rgpre::import::rgm::script_program::decode_script_program;
    let malformed = decode_script_program(&[4, 1, 0, 0, 0], 0, &definition()).unwrap_err();
    assert!(malformed.contains("inside instruction"));
    let steps = decode_script_program(&[255], 0, &definition()).unwrap();
    assert!(matches!(steps[&0].operation, Operation::Unsupported { .. }));
}

#[test]
fn actor_headers_retain_authored_order_and_separate_instance_locals() {
    let mut headers = vec![0; 8 + 165];
    headers[..4].copy_from_slice(&1u32.to_le_bytes());
    let row = &mut headers[8..];
    row[4..8].copy_from_slice(b"TEST");
    row[13..15].copy_from_slice(&2u16.to_le_bytes());
    row[0x4D..0x51].copy_from_slice(&5i32.to_le_bytes());
    row[0x75..0x79].copy_from_slice(&1i32.to_le_bytes());
    let variables: Vec<_> = [4i32, 9i32]
        .into_iter()
        .flat_map(i32::to_le_bytes)
        .collect();
    let actors = rgpre::import::rgm::script::disassemble_actor_scripts(
        &headers,
        &[5, 0, 0, 0, 0],
        &[],
        &[],
        &variables,
        Some(&definition()),
    );
    assert_eq!(actors.len(), 1);
    assert_eq!(actors[0].1.header_index, 0);
    assert_eq!(actors[0].1.instances, 2);
    assert_eq!(actors[0].1.instance_variables, [vec![4], vec![9]]);
}

#[test]
fn actor_controls_and_script_return_branches_keep_exact_continuations() {
    for (opcode, operation) in [
        (27, Operation::TaskStop { label: 7 }),
        (28, Operation::TaskPause { label: 7 }),
        (29, Operation::TaskResume { label: 7 }),
    ] {
        let step = decode_script_step(&[opcode, 7, 0, 0, 0], 0, &definition()).unwrap();
        assert_eq!(step.operation, operation);
        assert_eq!(step.next, 5);
    }
    assert_eq!(
        decode_script_step(&[24], 0, &definition())
            .unwrap()
            .operation,
        Operation::Yield
    );
    let step = decode_script_step(&[30, 3, 17, 9, 0, 0, 0], 0, &definition()).unwrap();
    assert_eq!(step.next, 2);
    assert_eq!(
        step.operation,
        Operation::ScriptRv {
            expected: 3,
            target: 7,
            save_return: true
        }
    );
    let step = decode_script_step(&[30, 3, 18, 0, 0, 0, 24], 0, &definition()).unwrap();
    assert_eq!(step.next, 6);
}

#[test]
fn condition_updates_and_parameter_truncation_are_lossless() {
    let step = decode_script_step(
        &[3, 10, 0, 10, 0, 20, 0, 0, 1, 0, 11, 0, 16, 0, 0, 0],
        0,
        &definition(),
    )
    .unwrap();
    assert_eq!(step.next, 16);
    let Operation::If { conditions, .. } = step.operation else {
        panic!("if expected")
    };
    assert_eq!(
        conditions[0].lhs,
        Value::Update {
            place: Place::Local { index: 0 },
            delta: 1
        }
    );
    assert_eq!(
        conditions[0].rhs,
        Value::Update {
            place: Place::Property {
                object: Object::Me,
                reference: 1
            },
            delta: -1
        }
    );
    let step = decode_script_step(
        &[0, 1, 0, 2, 22, 255, 255, 255, 255, 21, 4, 3, 2, 1],
        0,
        &definition(),
    )
    .unwrap();
    let Operation::Call { call } = step.operation else {
        panic!("call expected")
    };
    assert_eq!(
        call.arguments,
        [Value::Integer { value: 65535 }, Value::String { index: 4 }]
    );
}

#[test]
fn nonbinary_formula_operators_follow_the_invalid_encoding_path() {
    for operator in [10, 11, 12] {
        let error =
            decode_script_step(&[10, 0, 7, 1, 0, 0, 0, operator], 0, &definition()).unwrap_err();
        assert!(error.contains("invalid formula operator"), "{error}");
    }
}

#[test]
fn native_calls_are_expression_forms_not_parameter_forms() {
    for parameter in [
        vec![2, 2, 0, 0],
        vec![25, 0, 0, 2, 0, 0],
        vec![26, 0, 0, 2, 0, 0],
    ] {
        let mut bytes = vec![0, 1, 0, 2];
        bytes.extend(parameter);
        bytes.extend([7, 0, 0, 0, 0]);
        let error = decode_script_step(&bytes, 0, &definition()).unwrap_err();
        assert!(error.contains("native call in parameter mode"), "{error}");
    }
    let formula = decode_script_step(&[6, 0, 0, 2, 2, 0, 0, 0], 0, &definition()).unwrap();
    assert!(matches!(formula.operation, Operation::Assign { .. }));
}