halbu 0.3.0

Diablo II save file parsing library.
Documentation
use crate::character::codec::CharacterCodec;
use crate::character::v105::{MODE_ROTW, OFFSET_MODE_MARKER};
use crate::character::*;
use crate::format::FormatId;
use crate::ExpansionType;

#[test]
fn character_parse_test() {
    let bytes: [u8; 319] = [
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x0F, 0x00, 0x00, 0x01, 0x10, 0x1E, 0x5C, 0x00, 0x00,
        0x00, 0x00, 0xBB, 0x29, 0xBD, 0x64, 0xFF, 0xFF, 0xFF, 0xFF, 0x28, 0x00, 0x00, 0x00, 0x3B,
        0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x2B, 0x00, 0x00, 0x00,
        0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x9B, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00,
        0x00, 0x34, 0x00, 0x00, 0x00, 0xDC, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
        0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x37,
        0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00,
        0x39, 0x03, 0x02, 0x02, 0x02, 0x35, 0xFF, 0x51, 0x02, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0x4D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0x00, 0x00, 0x80, 0x43, 0x2D, 0x95, 0x53, 0x00, 0x00, 0x00, 0x00, 0x19, 0x50,
        0x40, 0x5C, 0x07, 0x00, 0x23, 0x00, 0xD6, 0x9B, 0x19, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, 0x62, 0x61, 0x20, 0xFF, 0x07, 0x1C,
        0x01, 0x04, 0x00, 0x00, 0x00, 0x75, 0x69, 0x74, 0x20, 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x78, 0x70, 0x6C, 0x20, 0xFF, 0x07, 0xD9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75,
        0x61, 0x70, 0x20, 0x4D, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x79, 0x61, 0x68,
        0x61, 0x6C, 0x6C, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
    ];

    let expected_result = Character {
        weapon_switch: false,
        status: Status { expansion: true, hardcore: false, ladder: false, died: true },
        class: Class::Sorceress,
        level: 92,
        last_played: 1690118587,
        progression: 15,
        assigned_skills: [
            40, 59, 54, 42, 43, 65535, 65535, 155, 149, 52, 220, 65535, 65535, 65535, 65535, 65535,
        ],
        left_mouse_skill: 55,
        right_mouse_skill: 54,
        left_mouse_switch_skill: 0,
        right_mouse_switch_skill: 54,
        menu_appearance: [
            57, 3, 2, 2, 2, 53, 255, 81, 2, 2, 255, 255, 255, 255, 255, 255, 77, 255, 255, 255,
            255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
        ],
        difficulty: Difficulty::Hell,
        act: Act::Act1,
        map_seed: 1402285379,
        mercenary: Mercenary {
            is_dead: false,
            id: 1547718681,
            name_id: 7,
            variant_id: 35,
            experience: 102341590,
        },
        resurrected_menu_appearance: [
            111, 98, 97, 32, 255, 7, 28, 1, 4, 0, 0, 0, 117, 105, 116, 32, 255, 2, 0, 0, 0, 0, 0,
            0, 120, 112, 108, 32, 255, 7, 217, 0, 0, 0, 0, 0, 117, 97, 112, 32, 77, 7, 248, 0, 0,
            0, 0, 0,
        ],
        name: String::from("Nyahallo"),
        raw_section: bytes.to_vec(),
    };
    let parsed_result = CharacterCodecV99::decode(&bytes).expect("character should parse");
    assert_eq!(parsed_result, expected_result);
}

#[test]
fn character_write_test() {
    let expected_result: [u8; 319] = [
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x0F, 0x00, 0x00, 0x01, 0x10, 0x1E, 0x5C, 0x00, 0x00,
        0x00, 0x00, 0xBB, 0x29, 0xBD, 0x64, 0xFF, 0xFF, 0xFF, 0xFF, 0x28, 0x00, 0x00, 0x00, 0x3B,
        0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x2B, 0x00, 0x00, 0x00,
        0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x9B, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00,
        0x00, 0x34, 0x00, 0x00, 0x00, 0xDC, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
        0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x37,
        0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00,
        0x39, 0x03, 0x02, 0x02, 0x02, 0x35, 0xFF, 0x51, 0x02, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0x4D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
        0xFF, 0xFF, 0x00, 0x00, 0x80, 0x43, 0x2D, 0x95, 0x53, 0x00, 0x00, 0x00, 0x00, 0x19, 0x50,
        0x40, 0x5C, 0x07, 0x00, 0x23, 0x00, 0xD6, 0x9B, 0x19, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6F, 0x62, 0x61, 0x20, 0xFF, 0x07, 0x1C,
        0x01, 0x04, 0x00, 0x00, 0x00, 0x75, 0x69, 0x74, 0x20, 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x78, 0x70, 0x6C, 0x20, 0xFF, 0x07, 0xD9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75,
        0x61, 0x70, 0x20, 0x4D, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4E, 0x79, 0x61, 0x68,
        0x61, 0x6C, 0x6C, 0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
    ];

    let character = Character {
        weapon_switch: false,
        status: Status { expansion: true, hardcore: false, ladder: false, died: true },
        progression: 15,
        class: Class::Sorceress,
        level: 92,
        last_played: 1690118587,
        assigned_skills: [
            40, 59, 54, 42, 43, 65535, 65535, 155, 149, 52, 220, 65535, 65535, 65535, 65535, 65535,
        ],
        left_mouse_skill: 55,
        right_mouse_skill: 54,
        left_mouse_switch_skill: 0,
        right_mouse_switch_skill: 54,
        menu_appearance: [
            57, 3, 2, 2, 2, 53, 255, 81, 2, 2, 255, 255, 255, 255, 255, 255, 77, 255, 255, 255,
            255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
        ],
        difficulty: Difficulty::Hell,
        act: Act::Act1,
        map_seed: 1402285379,
        mercenary: Mercenary {
            is_dead: false,
            id: 1547718681,
            name_id: 7,
            variant_id: 35,
            experience: 102341590,
        },
        resurrected_menu_appearance: [
            111, 98, 97, 32, 255, 7, 28, 1, 4, 0, 0, 0, 117, 105, 116, 32, 255, 2, 0, 0, 0, 0, 0,
            0, 120, 112, 108, 32, 255, 7, 217, 0, 0, 0, 0, 0, 117, 97, 112, 32, 77, 7, 248, 0, 0,
            0, 0, 0,
        ],
        name: String::from("Nyahallo"),
        raw_section: expected_result.to_vec(),
    };
    let generated_result = CharacterCodecV99::encode(&character).expect("character should encode");

    assert_eq!(expected_result.to_vec(), generated_result);
}

#[test]
fn character_v105_parse_test() {
    let save_bytes = include_bytes!("../../assets/test/Warlock_v105.d2s");
    let section_start = 16usize;
    let section_end = section_start + expected_length_for_format(FormatId::V105);
    let character_bytes = &save_bytes[section_start..section_end];

    let parsed = CharacterCodecV105::decode(character_bytes).expect("v105 character should parse");

    assert_eq!(parsed.class, Class::Warlock);
    assert_eq!(parsed.level, 1);
    assert_eq!(parsed.raw_section[OFFSET_MODE_MARKER], MODE_ROTW);
    assert_eq!(parsed.raw_section, character_bytes);
}

#[test]
fn character_v105_write_roundtrip_test() {
    let save_bytes = include_bytes!("../../assets/test/Warlock_v105.d2s");
    let section_start = 16usize;
    let section_end = section_start + expected_length_for_format(FormatId::V105);
    let character_bytes = &save_bytes[section_start..section_end];

    let parsed = CharacterCodecV105::decode(character_bytes).expect("v105 character should parse");
    let encoded = CharacterCodecV105::encode(&parsed).expect("v105 character should encode");

    assert_eq!(encoded, character_bytes);
}

#[test]
fn title_helper_maps_default_d2r_rules() {
    let mut character = Character { class: Class::Amazon, ..Character::default() };

    character.set_legacy_expansion_flag(false);
    character.set_hardcore(false);
    character.progression = 4;
    assert_eq!(character.title_d2r(ExpansionType::Classic), Some("Dame"));

    character.set_legacy_expansion_flag(false);
    character.set_hardcore(true);
    character.progression = 12;
    assert_eq!(character.title_d2r(ExpansionType::Classic), Some("Queen"));

    character.set_legacy_expansion_flag(true);
    character.set_hardcore(false);
    character.progression = 10;
    assert_eq!(character.title_d2r(ExpansionType::Expansion), Some("Champion"));

    character.set_legacy_expansion_flag(true);
    character.set_hardcore(true);
    character.progression = 15;
    assert_eq!(character.title_d2r(ExpansionType::Expansion), Some("Guardian"));

    // Warlock uses male title variants.
    character.class = Class::Warlock;
    character.set_legacy_expansion_flag(false);
    character.set_hardcore(false);
    character.progression = 12;
    assert_eq!(character.title_d2r(ExpansionType::Classic), Some("Baron"));

    // Expansion normally skips these values.
    character.set_legacy_expansion_flag(true);
    character.set_hardcore(false);
    character.progression = 9;
    assert_eq!(character.title_d2r(ExpansionType::Expansion), None);
}