~quf/er-tracker

a921b2ea1073727b3feeea5010424366fe6ad950 — Lukas Himbert 7 months ago 3afba64
rustfmt update
8 files changed, 84 insertions(+), 434 deletions(-)

A rustfmt.toml
M src/bnd4.rs
M src/debug_cli.rs
M src/debug_gui.rs
M src/debug_reader.rs
M src/main.rs
M src/reader.rs
M src/sl2.rs
A rustfmt.toml => rustfmt.toml +1 -0
@@ 0,0 1,1 @@
max_width=200

M src/bnd4.rs => src/bnd4.rs +6 -21
@@ 75,9 75,7 @@ fn read_header(data: &[u8]) -> Result<Header, ParseError> {
        return Err(ParseError::FileHeader);
    }

    let unknown_bytes_1: [u8; 8] = data[4..12]
        .try_into()
        .expect("data is at least 64 bytes long");
    let unknown_bytes_1: [u8; 8] = data[4..12].try_into().expect("data is at least 64 bytes long");
    tracing::event!(tracing::Level::TRACE, ?unknown_bytes_1);

    let number_of_entries = u32::from_le_bytes(data[12..16].try_into().expect("64 bytes"));


@@ 151,10 149,7 @@ fn read_entry(data: &[u8], offset: usize) -> Result<Entry, ParseError> {

    let entry_header = read_entry_header(data, offset)?;

    let name_offset = entry_header
        .name_offset
        .try_into()
        .expect("32 bit usize please");
    let name_offset = entry_header.name_offset.try_into().expect("32 bit usize please");
    let mut name_terminator_offset = name_offset;
    while name_terminator_offset + 2 < data.len() {
        if data[name_terminator_offset] == 0 && data[name_terminator_offset + 1] == 0 {


@@ 166,14 161,10 @@ fn read_entry(data: &[u8], offset: usize) -> Result<Entry, ParseError> {
    }
    tracing::event!(tracing::Level::TRACE, ?name_offset, ?name_terminator_offset);
    let name_bytes = &data[name_offset..name_terminator_offset];
    let name =
        utf16string::WStr::<utf16string::LE>::from_utf16(name_bytes).map_err(ParseError::Utf16)?;
    let name = utf16string::WStr::<utf16string::LE>::from_utf16(name_bytes).map_err(ParseError::Utf16)?;
    tracing::event!(tracing::Level::DEBUG, %name);

    let data_off: usize = entry_header
        .data_offset
        .try_into()
        .expect("32 bit usize please");
    let data_off: usize = entry_header.data_offset.try_into().expect("32 bit usize please");
    let data_size: usize = entry_header.size.try_into().expect("64 bit usize please");
    let data_range = data_off..data_off + data_size;
    let entry_data = &data[data_range.clone()];


@@ 195,10 186,7 @@ fn read_entry(data: &[u8], offset: usize) -> Result<Entry, ParseError> {
pub fn read(data: &[u8]) -> Result<Bnd4, ParseError> {
    let file_header = read_header(data)?;

    let number_of_entries: usize = file_header
        .number_of_entries
        .try_into()
        .map_err(|_| ParseError::TooManyEntries)?;
    let number_of_entries: usize = file_header.number_of_entries.try_into().map_err(|_| ParseError::TooManyEntries)?;

    let mut entries = std::vec::Vec::with_capacity(number_of_entries);



@@ 208,10 196,7 @@ pub fn read(data: &[u8]) -> Result<Bnd4, ParseError> {
        entries.push(read_entry(data, offset)?);
    }

    Ok(Bnd4 {
        header: file_header,
        entries,
    })
    Ok(Bnd4 { header: file_header, entries })
}

impl<'a> Entry<'a> {

M src/debug_cli.rs => src/debug_cli.rs +3 -13
@@ 4,11 4,7 @@ use crate::debug_reader;
pub fn run(file: &std::path::Path, profile: Option<usize>) -> () {
    let raw_data = std::fs::read(file).unwrap();
    let bnd4 = bnd4::read(&raw_data).unwrap();
    let profile = bnd4
        .entry_by_number(
            profile.unwrap_or_else(|| bnd4.entry_by_number(10).unwrap().data()[0x1344].into()),
        )
        .unwrap();
    let profile = bnd4.entry_by_number(profile.unwrap_or_else(|| bnd4.entry_by_number(10).unwrap().data()[0x1344].into())).unwrap();

    let mut reader = debug_reader::DebugReader::new(profile.data());



@@ 119,10 115,7 @@ pub fn run(file: &std::path::Path, profile: Option<usize>) -> () {

    reader.mark("unk60_block_start");
    let number_of_unk1_entries = reader.read_u32_le("number_of_unk1_entries");
    reader.skip(
        "unk60_block",
        (number_of_unk1_entries * 8).try_into().unwrap(),
    );
    reader.skip("unk60_block", (number_of_unk1_entries * 8).try_into().unwrap());
    reader.mark("unk60_block_end");

    reader.mark("equipment2_start");


@@ 189,10 182,7 @@ pub fn run(file: &std::path::Path, profile: Option<usize>) -> () {
    reader.mark("regions_start");
    let number_of_region_entries = reader.read_u32_le("number_of_region_entries");
    assert!(number_of_region_entries < 1000); // arbitrary number, probably large enough for all regions and won't blow up the heap
    reader.skip(
        "regions",
        (number_of_region_entries * 4).try_into().unwrap(),
    );
    reader.skip("regions", (number_of_region_entries * 4).try_into().unwrap());
    reader.mark("regions_end");

    reader.mark("unk_block_100_start");

M src/debug_gui.rs => src/debug_gui.rs +20 -98
@@ 74,23 74,13 @@ impl InventoryTable {
    }

    pub fn append(&mut self, entry: &sl2::InventoryEntry) {
        self.table.append_row(
            "",
            &[
                &hex::encode(entry.lookup_buf),
                &entry.amount.to_string(),
                &entry.handle.to_string(),
            ],
        );
        self.table.append_row("", &[&hex::encode(entry.lookup_buf), &entry.amount.to_string(), &entry.handle.to_string()]);
    }

    pub fn set_row(&mut self, row: usize, entry: &sl2::InventoryEntry) {
        self.table
            .set_cell_value(row.try_into().unwrap(), 0, &hex::encode(entry.lookup_buf));
        self.table
            .set_cell_value(row.try_into().unwrap(), 1, &entry.amount.to_string());
        self.table
            .set_cell_value(row.try_into().unwrap(), 2, &entry.handle.to_string());
        self.table.set_cell_value(row.try_into().unwrap(), 0, &hex::encode(entry.lookup_buf));
        self.table.set_cell_value(row.try_into().unwrap(), 1, &entry.amount.to_string());
        self.table.set_cell_value(row.try_into().unwrap(), 2, &entry.handle.to_string());
    }

    pub fn fit_cells_and_headers(&mut self) {


@@ 108,11 98,7 @@ pub fn run(file: &std::path::Path) -> () {

    let mut win = fltk::window::Window::default()
        .with_size(w, h)
        .with_label(&format!(
            "{} version {}",
            env! {"CARGO_PKG_NAME"},
            env! {"CARGO_PKG_VERSION"}
        ));
        .with_label(&format!("{} version {}", env! {"CARGO_PKG_NAME"}, env! {"CARGO_PKG_VERSION"}));
    win.set_callback(|_| {
        // override close window on pressing escape
        if fltk::app::event() == fltk::enums::Event::Close {


@@ 145,12 131,7 @@ pub fn run(file: &std::path::Path) -> () {
    let mut inventory_changes = fltk::text::TextDisplay::default();
    inventory_changes.set_buffer(fltk::text::TextBuffer::default());
    let mut previous_inventory: std::collections::HashSet<_> = sl2.inventory().copied().collect();
    update_inventory(
        &mut inventory_table,
        &mut inventory_changes,
        &mut previous_inventory,
        &sl2,
    );
    update_inventory(&mut inventory_table, &mut inventory_changes, &mut previous_inventory, &sl2);
    flex_col.end();

    let mut flex_col = fltk::group::Flex::default().size_of_parent();


@@ 159,12 140,7 @@ pub fn run(file: &std::path::Path) -> () {
    let mut key_item_changes = fltk::text::TextDisplay::default();
    key_item_changes.set_buffer(fltk::text::TextBuffer::default());
    let mut previous_key_items: std::collections::HashSet<_> = sl2.key_items().copied().collect();
    update_key_items(
        &mut key_item_table,
        &mut key_item_changes,
        &mut previous_key_items,
        &sl2,
    );
    update_key_items(&mut key_item_table, &mut key_item_changes, &mut previous_key_items, &sl2);
    flex_col.end();

    flex_row.end();


@@ 188,11 164,7 @@ pub fn run(file: &std::path::Path) -> () {
        let t1 = std::time::Instant::now();
        let data_has_changed = loop {
            raw_data.clear();
            match std::fs::File::open(&file)
                .and_then(|mut f| f.read_to_end(&mut raw_data))
                .ok()
                .and_then(|_| sl2.try_update(&raw_data))
            {
            match std::fs::File::open(&file).and_then(|mut f| f.read_to_end(&mut raw_data)).ok().and_then(|_| sl2.try_update(&raw_data)) {
                None => {}          // Error, continue retrying
                Some(x) => break x, // Success
            }


@@ 200,30 172,14 @@ pub fn run(file: &std::path::Path) -> () {
        let t2 = std::time::Instant::now();

        // update displays
        update_time.set_label(&format!(
            "update took {:.1}ms",
            (t2 - t1).as_secs_f64() * 1000.0
        ));
        update_time.set_label(&format!("update took {:.1}ms", (t2 - t1).as_secs_f64() * 1000.0));
        name.set_label(&format!("name: {}", sl2.name()));
        time_played.set_label(&format!(
            "time played: {}s",
            sl2.time_played().as_secs_f64()
        ));
        time_played.set_label(&format!("time played: {}s", sl2.time_played().as_secs_f64()));

        if data_has_changed {
            update_table1(&mut table1, &sl2);
            update_inventory(
                &mut inventory_table,
                &mut inventory_changes,
                &mut previous_inventory,
                &sl2,
            );
            update_key_items(
                &mut key_item_table,
                &mut key_item_changes,
                &mut previous_key_items,
                &sl2,
            );
            update_inventory(&mut inventory_table, &mut inventory_changes, &mut previous_inventory, &sl2);
            update_key_items(&mut key_item_table, &mut key_item_changes, &mut previous_key_items, &sl2);
        }

        win.redraw();


@@ 236,13 192,9 @@ pub fn run(file: &std::path::Path) -> () {
pub fn fit_cells_and_headers(table: &mut fltk_table::SmartTable) {
    let vpad = |x| x + 4;
    let hpad = |x| x + 10;
    let col_header_height = (0..table.column_count())
        .map(|col| fltk::draw::measure(&table.col_header_value(col), false).1)
        .fold(1, std::cmp::max);
    let col_header_height = (0..table.column_count()).map(|col| fltk::draw::measure(&table.col_header_value(col), false).1).fold(1, std::cmp::max);
    table.set_col_header_height(vpad(col_header_height));
    let row_header_height = (0..table.row_count())
        .map(|row| fltk::draw::measure(&table.row_header_value(row), false).0)
        .fold(1, std::cmp::max);
    let row_header_height = (0..table.row_count()).map(|row| fltk::draw::measure(&table.row_header_value(row), false).0).fold(1, std::cmp::max);
    table.set_row_header_width(hpad(row_header_height));
    for col in 0..table.column_count() {
        let header_width = fltk::draw::measure(&table.col_header_value(col), false).0;


@@ 337,12 289,7 @@ fn update_table1(table: &mut Table<&'static str>, sl2: &sl2::Sl2) {
    table.insert("name", sl2.name().to_utf8());
}

fn update_inventory(
    table: &mut InventoryTable,
    buf: &mut fltk::text::TextDisplay,
    previous_inventory: &mut std::collections::HashSet<sl2::InventoryEntry>,
    sl2: &sl2::Sl2,
) {
fn update_inventory(table: &mut InventoryTable, buf: &mut fltk::text::TextDisplay, previous_inventory: &mut std::collections::HashSet<sl2::InventoryEntry>, sl2: &sl2::Sl2) {
    // update table
    table.table.clear();
    sl2.inventory()


@@ 360,30 307,15 @@ fn update_inventory(
    let removed = previous_inventory.difference(&current_inventory);
    let added = current_inventory.difference(&previous_inventory);
    for entry in added {
        buf.insert(&format!(
            "+ {} {} {}\n",
            hex::encode(entry.lookup_buf),
            entry.amount,
            entry.handle
        ));
        buf.insert(&format!("+ {} {} {}\n", hex::encode(entry.lookup_buf), entry.amount, entry.handle));
    }
    for entry in removed {
        buf.insert(&format!(
            "- {} {} {}\n",
            hex::encode(entry.lookup_buf),
            entry.amount,
            entry.handle
        ));
        buf.insert(&format!("- {} {} {}\n", hex::encode(entry.lookup_buf), entry.amount, entry.handle));
    }
    *previous_inventory = current_inventory
}

fn update_key_items(
    table: &mut InventoryTable,
    buf: &mut fltk::text::TextDisplay,
    previous_key_items: &mut std::collections::HashSet<sl2::InventoryEntry>,
    sl2: &sl2::Sl2,
) {
fn update_key_items(table: &mut InventoryTable, buf: &mut fltk::text::TextDisplay, previous_key_items: &mut std::collections::HashSet<sl2::InventoryEntry>, sl2: &sl2::Sl2) {
    // update table
    table.table.clear();
    sl2.key_items()


@@ 401,20 333,10 @@ fn update_key_items(
    let removed = previous_key_items.difference(&current_key_items);
    let added = current_key_items.difference(&previous_key_items);
    for entry in added {
        buf.insert(&format!(
            "+ {} {} {}\n",
            hex::encode(entry.lookup_buf),
            entry.amount,
            entry.handle
        ));
        buf.insert(&format!("+ {} {} {}\n", hex::encode(entry.lookup_buf), entry.amount, entry.handle));
    }
    for entry in removed {
        buf.insert(&format!(
            "- {} {} {}\n",
            hex::encode(entry.lookup_buf),
            entry.amount,
            entry.handle
        ));
        buf.insert(&format!("- {} {} {}\n", hex::encode(entry.lookup_buf), entry.amount, entry.handle));
    }
    *previous_key_items = current_key_items
}

M src/debug_reader.rs => src/debug_reader.rs +4 -20
@@ 98,18 98,11 @@ impl<'a> DebugReader<'a> {
    }

    #[tracing::instrument(skip(self), fields(offset = self.offset_from_marker(), marker = self.marker_label(), offset_from_start = self.offset_from_start()), ret(Display))]
    pub fn read_padded_utf16_le(
        &mut self,
        label: &'static str,
        n: usize,
    ) -> &utf16string::WStr<utf16string::LE> {
    pub fn read_padded_utf16_le(&mut self, label: &'static str, n: usize) -> &utf16string::WStr<utf16string::LE> {
        assert!(self.labels.insert(label));
        let buf = self.do_read_bytes(n);
        let padded = utf16string::WStr::<utf16string::LE>::from_utf16(buf).unwrap();
        let terminator_position = padded
            .char_indices()
            .find(|(_, c)| *c == 0 as char)
            .map(|(i, _)| i);
        let terminator_position = padded.char_indices().find(|(_, c)| *c == 0 as char).map(|(i, _)| i);
        match terminator_position {
            Some(n) => &padded[0..n],
            None => padded,


@@ 151,12 144,7 @@ impl<'a> DebugReader<'a> {
                r#type,
                id: id_no_category_from_bytes(buf),
            },
            _ => LookupTableEntry::Other {
                r#ref,
                unk: unk1,
                r#type,
                buf,
            },
            _ => LookupTableEntry::Other { r#ref, unk: unk1, r#type, buf },
        }
    }



@@ 164,10 152,6 @@ impl<'a> DebugReader<'a> {
        let lookup_buf = self.do_read_bytes(4).try_into().unwrap();
        let amount = u32::from_le_bytes(self.do_read_bytes(4).try_into().unwrap());
        let handle = u32::from_le_bytes(self.do_read_bytes(4).try_into().unwrap());
        InventoryEntry {
            lookup_buf,
            amount,
            handle,
        }
        InventoryEntry { lookup_buf, amount, handle }
    }
}

M src/main.rs => src/main.rs +29 -187
@@ 83,12 83,7 @@ fn main() {
            let raw_data = std::fs::read(file).unwrap();
            let archive = bnd4::read(&raw_data).unwrap();
            for (i, entry) in archive.entries().iter().enumerate() {
                println!(
                    "Slot {}: {} ({} bytes)",
                    i,
                    entry.name(),
                    entry.data().len()
                );
                println!("Slot {}: {} ({} bytes)", i, entry.name(), entry.data().len());
            }
        }
        Commands::Bnd4Extract { file, profile, out } => {


@@ 117,10 112,7 @@ fn main() {
            loop {
                let t1 = std::time::Instant::now();
                raw_data.clear();
                std::fs::File::open(&file)
                    .unwrap()
                    .read_to_end(&mut raw_data)
                    .unwrap();
                std::fs::File::open(&file).unwrap().read_to_end(&mut raw_data).unwrap();
                _ = sl2.try_update(&raw_data);
                let t2 = std::time::Instant::now();
                tracing::event!(


@@ 162,32 154,10 @@ fn main() {

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum LookupTableEntry {
    Weapon {
        r#ref: u16,
        unk: u8,
        r#type: u8,
        id: u32,
        extra: [u8; 13],
    },
    Armor {
        r#ref: u16,
        unk: u8,
        r#type: u8,
        id: u32,
        extra: [u8; 8],
    },
    AshOfWar {
        r#ref: u16,
        unk: u8,
        r#type: u8,
        id: u32,
    },
    Other {
        r#ref: u16,
        unk: u8,
        r#type: u8,
        buf: [u8; 4],
    },
    Weapon { r#ref: u16, unk: u8, r#type: u8, id: u32, extra: [u8; 13] },
    Armor { r#ref: u16, unk: u8, r#type: u8, id: u32, extra: [u8; 8] },
    AshOfWar { r#ref: u16, unk: u8, r#type: u8, id: u32 },
    Other { r#ref: u16, unk: u8, r#type: u8, buf: [u8; 4] },
}

pub fn read_lookup_table_entry(reader: &mut reader::Reader) -> LookupTableEntry {


@@ 204,37 174,15 @@ pub fn read_lookup_table_entry(reader: &mut reader::Reader) -> LookupTableEntry 
        0x80 => {
            // weapon
            let extra: [u8; 13] = reader.read_bytes(13).try_into().unwrap();
            LookupTableEntry::Weapon {
                r#ref,
                unk,
                r#type,
                id,
                extra,
            }
            LookupTableEntry::Weapon { r#ref, unk, r#type, id, extra }
        }
        0x90 => {
            // armor
            let extra: [u8; 8] = reader.read_bytes(8).try_into().unwrap();
            LookupTableEntry::Armor {
                r#ref,
                unk,
                r#type,
                id,
                extra,
            }
            LookupTableEntry::Armor { r#ref, unk, r#type, id, extra }
        }
        0xc0 => LookupTableEntry::AshOfWar {
            r#ref,
            unk,
            r#type,
            id,
        },
        _ => LookupTableEntry::Other {
            r#ref,
            unk,
            r#type,
            buf,
        },
        0xc0 => LookupTableEntry::AshOfWar { r#ref, unk, r#type, id },
        _ => LookupTableEntry::Other { r#ref, unk, r#type, buf },
    }
}



@@ 253,21 201,13 @@ fn foo(data: &[u8]) {
    //tracing::event!(tracing::Level::INFO, "unk" = String::from_utf8_lossy(unk).into_owned());

    let version = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?version,
        "offset after" = reader.offset()
    );
    tracing::event!(tracing::Level::INFO, ?version, "offset after" = reader.offset());

    let unk = reader.read_4_bytes();
    tracing::event!(tracing::Level::INFO, ?unk, "offset after" = reader.offset());

    let time_played_ms = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?time_played_ms,
        "offset after" = reader.offset()
    );
    tracing::event!(tracing::Level::INFO, ?time_played_ms, "offset after" = reader.offset());

    let unk = reader.read_4_bytes();
    tracing::event!(tracing::Level::INFO, ?unk, "offset after" = reader.offset());


@@ 306,12 246,7 @@ fn foo(data: &[u8]) {
            } => {
                number_of_others += 1;
            }
            LookupTableEntry::Other {
                r#ref,
                unk,
                r#type,
                buf,
            } => {
            LookupTableEntry::Other { r#ref, unk, r#type, buf } => {
                tracing::event!(tracing::Level::INFO, ?r#ref, ?unk, ?r#type, ?buf);
                number_of_others += 1;
                lookup_table.insert(r#ref, entry);


@@ 319,13 254,7 @@ fn foo(data: &[u8]) {
        }
    }

    tracing::event!(
        tracing::Level::INFO,
        ?number_of_weapons,
        ?number_of_armors,
        ?number_of_ashes_of_war,
        ?number_of_others
    );
    tracing::event!(tracing::Level::INFO, ?number_of_weapons, ?number_of_armors, ?number_of_ashes_of_war, ?number_of_others);
    tracing::event!(tracing::Level::INFO, "offset" = reader.offset());

    let unk = reader.read_8_bytes();


@@ 349,12 278,7 @@ fn foo(data: &[u8]) {
    let stamina = reader.read_u32_le();
    let max_stamina = reader.read_u32_le();
    let base_max_stamina = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?stamina,
        ?max_stamina,
        ?base_max_stamina
    );
    tracing::event!(tracing::Level::INFO, ?stamina, ?max_stamina, ?base_max_stamina);

    let unk = reader.read_4_bytes();
    tracing::event!(tracing::Level::INFO, ?unk, "offset after" = reader.offset());


@@ 367,17 291,7 @@ fn foo(data: &[u8]) {
    let intelligence = reader.read_u32_le();
    let faith = reader.read_u32_le();
    let arcane = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?vigor,
        ?mind,
        ?endurance,
        ?strength,
        ?dexterity,
        ?intelligence,
        ?faith,
        ?arcane
    );
    tracing::event!(tracing::Level::INFO, ?vigor, ?mind, ?endurance, ?strength, ?dexterity, ?intelligence, ?faith, ?arcane);

    let unk = reader.read_bytes(12);
    tracing::event!(tracing::Level::INFO, ?unk, "offset after" = reader.offset());


@@ 397,27 311,14 @@ fn foo(data: &[u8]) {
    let robustness2 = reader.read_u32_le();
    let focus = reader.read_u32_le();
    let focus2 = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?immunity,
        ?immunity2,
        ?robustness,
        ?vitality,
        ?robustness2,
        ?focus,
        ?focus2
    );
    tracing::event!(tracing::Level::INFO, ?immunity, ?immunity2, ?robustness, ?vitality, ?robustness2, ?focus, ?focus2);

    let unk = reader.read_8_bytes();
    tracing::event!(tracing::Level::INFO, ?unk, "offset after" = reader.offset());

    let name_buffer = reader.read_bytes(32);
    let name_with_null_bytes =
        utf16string::WStr::<utf16string::LE>::from_utf16(name_buffer).unwrap();
    let terminator_position = name_with_null_bytes
        .char_indices()
        .find(|(_, c)| *c == 0 as char)
        .map(|(i, _)| i);
    let name_with_null_bytes = utf16string::WStr::<utf16string::LE>::from_utf16(name_buffer).unwrap();
    let terminator_position = name_with_null_bytes.char_indices().find(|(_, c)| *c == 0 as char).map(|(i, _)| i);
    let name = match terminator_position {
        Some(n) => &name_with_null_bytes[0..n],
        None => name_with_null_bytes,


@@ 453,13 354,7 @@ fn foo(data: &[u8]) {
    let bolt_1_index = reader.read_u32_le();
    let arrow_2_index = reader.read_u32_le();
    let bolt_2_index = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?arrow_1_index,
        ?bolt_1_index,
        ?arrow_2_index,
        ?bolt_2_index
    );
    tracing::event!(tracing::Level::INFO, ?arrow_1_index, ?bolt_1_index, ?arrow_2_index, ?bolt_2_index);

    let unk = reader.read_u32_le();
    tracing::event!(tracing::Level::INFO, ?unk);


@@ 470,13 365,7 @@ fn foo(data: &[u8]) {
    let chest_index = reader.read_u32_le();
    let arms_index = reader.read_u32_le();
    let legs_index = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?head_index,
        ?chest_index,
        ?arms_index,
        ?legs_index
    );
    tracing::event!(tracing::Level::INFO, ?head_index, ?chest_index, ?arms_index, ?legs_index);

    let unk = reader.read_4_bytes();
    tracing::event!(tracing::Level::INFO, ?unk);


@@ 485,13 374,7 @@ fn foo(data: &[u8]) {
    let talisman2_index = reader.read_u32_le();
    let talisman3_index = reader.read_u32_le();
    let talisman4_index = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?talisman1_index,
        ?talisman2_index,
        ?talisman3_index,
        ?talisman4_index
    );
    tracing::event!(tracing::Level::INFO, ?talisman1_index, ?talisman2_index, ?talisman3_index, ?talisman4_index);

    let unk = reader.read_4_bytes();
    tracing::event!(tracing::Level::INFO, ?unk);


@@ 524,13 407,7 @@ fn foo(data: &[u8]) {
    let bolt_1_id = reader.read_u32_le();
    let arrow_2_id = reader.read_u32_le();
    let bolt_2_id = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?arrow_1_id,
        ?bolt_1_id,
        ?arrow_2_id,
        ?bolt_2_id
    );
    tracing::event!(tracing::Level::INFO, ?arrow_1_id, ?bolt_1_id, ?arrow_2_id, ?bolt_2_id);

    let unk = reader.read_u32_le();
    tracing::event!(tracing::Level::INFO, ?unk);


@@ 541,13 418,7 @@ fn foo(data: &[u8]) {
    let chest_id = reader.read_u32_le();
    let arms_id = reader.read_u32_le();
    let legs_id = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?head_id,
        ?chest_id,
        ?arms_id,
        ?legs_id
    );
    tracing::event!(tracing::Level::INFO, ?head_id, ?chest_id, ?arms_id, ?legs_id);

    let unk = reader.read_4_bytes();
    tracing::event!(tracing::Level::INFO, ?unk);


@@ 556,13 427,7 @@ fn foo(data: &[u8]) {
    let talisman2_id = reader.read_u32_le();
    let talisman3_id = reader.read_u32_le();
    let talisman4_id = reader.read_u32_le();
    tracing::event!(
        tracing::Level::INFO,
        ?talisman1_id,
        ?talisman2_id,
        ?talisman3_id,
        ?talisman4_id
    );
    tracing::event!(tracing::Level::INFO, ?talisman1_id, ?talisman2_id, ?talisman3_id, ?talisman4_id);

    let unk = reader.read_4_bytes();
    tracing::event!(tracing::Level::INFO, ?unk);


@@ 589,13 454,7 @@ fn foo(data: &[u8]) {
    let bolt_1_lookup = read_lookup_entry(&mut reader);
    let arrow_2_lookup = read_lookup_entry(&mut reader);
    let bolt_2_lookup = read_lookup_entry(&mut reader);
    tracing::event!(
        tracing::Level::INFO,
        ?arrow_1_lookup,
        ?bolt_1_lookup,
        ?arrow_2_lookup,
        ?bolt_2_lookup
    );
    tracing::event!(tracing::Level::INFO, ?arrow_1_lookup, ?bolt_1_lookup, ?arrow_2_lookup, ?bolt_2_lookup);

    let unk = read_lookup_entry(&mut reader);
    tracing::event!(tracing::Level::INFO, ?unk);


@@ 606,13 465,7 @@ fn foo(data: &[u8]) {
    let chest_lookup = read_lookup_entry(&mut reader);
    let arms_lookup = read_lookup_entry(&mut reader);
    let legs_lookup = read_lookup_entry(&mut reader);
    tracing::event!(
        tracing::Level::INFO,
        ?head_lookup,
        ?chest_lookup,
        ?arms_lookup,
        ?legs_lookup
    );
    tracing::event!(tracing::Level::INFO, ?head_lookup, ?chest_lookup, ?arms_lookup, ?legs_lookup);

    let unk = read_lookup_entry(&mut reader);
    tracing::event!(tracing::Level::INFO, ?unk);


@@ 621,13 474,7 @@ fn foo(data: &[u8]) {
    let talisman2_lookup = read_lookup_entry(&mut reader);
    let talisman3_lookup = read_lookup_entry(&mut reader);
    let talisman4_lookup = read_lookup_entry(&mut reader);
    tracing::event!(
        tracing::Level::INFO,
        ?talisman1_lookup,
        ?talisman2_lookup,
        ?talisman3_lookup,
        ?talisman4_lookup
    );
    tracing::event!(tracing::Level::INFO, ?talisman1_lookup, ?talisman2_lookup, ?talisman3_lookup, ?talisman4_lookup);

    let unk = read_lookup_entry(&mut reader);
    tracing::event!(tracing::Level::INFO, ?unk);


@@ 638,12 485,7 @@ fn foo(data: &[u8]) {
        let (r#ref, unk, r#type) = read_lookup_entry(&mut reader);
        let entry = lookup_table.get(&r#ref).copied().unwrap_or_else(|| {
            let buf = reader.read_4_bytes();
            LookupTableEntry::Other {
                r#ref,
                unk,
                r#type,
                buf,
            }
            LookupTableEntry::Other { r#ref, unk, r#type, buf }
        });
        let amount = reader.read_u32_le();
        let handle = reader.read_u32_le();

M src/reader.rs => src/reader.rs +1 -4
@@ 23,10 23,7 @@ impl<'a> Reader<'a> {
    }

    pub fn with_offset(&self, off: usize) -> Self {
        Reader {
            data: self.data,
            off,
        }
        Reader { data: self.data, off }
    }

    pub fn offset(&self) -> usize {

M src/sl2.rs => src/sl2.rs +20 -91
@@ 88,32 88,10 @@ pub struct Resistance {

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum LookupTableEntry {
    Weapon {
        r#ref: u16,
        unk1: u8,
        r#type: u8,
        id: u32,
        unk2: [u8; 13],
    },
    Armor {
        r#ref: u16,
        unk1: u8,
        r#type: u8,
        id: u32,
        unk2: [u8; 8],
    },
    AshOfWar {
        r#ref: u16,
        unk: u8,
        r#type: u8,
        id: u32,
    },
    Other {
        r#ref: u16,
        unk: u8,
        r#type: u8,
        buf: [u8; 4],
    },
    Weapon { r#ref: u16, unk1: u8, r#type: u8, id: u32, unk2: [u8; 13] },
    Armor { r#ref: u16, unk1: u8, r#type: u8, id: u32, unk2: [u8; 8] },
    AshOfWar { r#ref: u16, unk: u8, r#type: u8, id: u32 },
    Other { r#ref: u16, unk: u8, r#type: u8, buf: [u8; 4] },
}

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]


@@ 186,17 164,11 @@ fn determine_offsets(data: &[u8], scratch: &mut ScratchMem) -> Option<Offsets> {
    }

    let system_save = archive.entry_by_number(10)?;
    tracing::event!(
        tracing::Level::TRACE,
        "system save name" = system_save.name().to_utf8()
    );
    tracing::event!(tracing::Level::TRACE, "system save name" = system_save.name().to_utf8());

    // check system save and get the number of the active save
    let expected_system_save_name = utf16string::WStr::from_utf16(&[
        b'U', 0, b'S', 0, b'E', 0, b'R', 0, b'_', 0, b'D', 0, b'A', 0, b'T', 0, b'A', 0, b'0', 0,
        b'1', 0, b'0', 0,
    ])
    .expect("valid utf16 (LE)");
    let expected_system_save_name =
        utf16string::WStr::from_utf16(&[b'U', 0, b'S', 0, b'E', 0, b'R', 0, b'_', 0, b'D', 0, b'A', 0, b'T', 0, b'A', 0, b'0', 0, b'1', 0, b'0', 0]).expect("valid utf16 (LE)");
    if system_save.name() != expected_system_save_name {
        tracing::event!(tracing::Level::WARN, "unexpected system save name");
        return None;


@@ 225,10 197,7 @@ fn determine_offsets(data: &[u8], scratch: &mut ScratchMem) -> Option<Offsets> {
    let version = reader.read_u32_le();
    tracing::event!(tracing::Level::DEBUG, ?version);
    if ![131 /* 1.08.1 */, 140 /* 1.09 */].contains(&version) {
        tracing::event!(
            tracing::Level::ERROR,
            "unsupported save profile version" = version
        );
        tracing::event!(tracing::Level::ERROR, "unsupported save profile version" = version);
        return None;
    }



@@ 273,14 242,8 @@ fn determine_offsets(data: &[u8], scratch: &mut ScratchMem) -> Option<Offsets> {
            scratch.inventory.push(entry);
        }
    }
    if scratch.inventory.len()
        != usize::try_from(number_of_inventory_entries).expect(">=32 bits usize")
    {
        tracing::event!(
            tracing::Level::ERROR,
            ?number_of_inventory_entries,
            "actual number of entries" = scratch.inventory.len()
        );
    if scratch.inventory.len() != usize::try_from(number_of_inventory_entries).expect(">=32 bits usize") {
        tracing::event!(tracing::Level::ERROR, ?number_of_inventory_entries, "actual number of entries" = scratch.inventory.len());
        return None;
    }



@@ 296,14 259,8 @@ fn determine_offsets(data: &[u8], scratch: &mut ScratchMem) -> Option<Offsets> {
            scratch.key_items.push(entry);
        }
    }
    if scratch.key_items.len()
        != usize::try_from(number_of_key_item_entries).expect(">=32 bits usize")
    {
        tracing::event!(
            tracing::Level::ERROR,
            ?number_of_key_item_entries,
            "actual number of entries" = scratch.key_items.len()
        );
    if scratch.key_items.len() != usize::try_from(number_of_key_item_entries).expect(">=32 bits usize") {
        tracing::event!(tracing::Level::ERROR, ?number_of_key_item_entries, "actual number of entries" = scratch.key_items.len());
        return None;
    }



@@ 316,11 273,7 @@ fn determine_offsets(data: &[u8], scratch: &mut ScratchMem) -> Option<Offsets> {
    reader.skip(0xb8);
    let unknown_block_count = reader.read_u32_le();
    tracing::event!(tracing::Level::TRACE, ?unknown_block_count);
    reader.skip(
        (8 * unknown_block_count)
            .try_into()
            .expect(">=32 bits usize"),
    );
    reader.skip((8 * unknown_block_count).try_into().expect(">=32 bits usize"));

    // current equipment
    let current_equipment_offset = reader.offset();


@@ 382,10 335,7 @@ fn read_lookup_table_entry(reader: &mut reader::Reader) -> Option<LookupTableEnt
    match r#type {
        0x80 => {
            // weapon
            let unk2: [u8; 13] = reader
                .read_bytes(13)
                .try_into()
                .expect("read_bytes should fail if not enough data remains");
            let unk2: [u8; 13] = reader.read_bytes(13).try_into().expect("read_bytes should fail if not enough data remains");
            // TODO: something about ash of war = u16::from_le_bytes(unknown2[8..9])?
            Some(LookupTableEntry::Weapon {
                r#ref,


@@ 412,12 362,7 @@ fn read_lookup_table_entry(reader: &mut reader::Reader) -> Option<LookupTableEnt
            r#type,
            id: id_no_category_from_bytes(buf),
        }),
        _ => Some(LookupTableEntry::Other {
            r#ref,
            unk,
            r#type,
            buf,
        }),
        _ => Some(LookupTableEntry::Other { r#ref, unk, r#type, buf }),
    }
}



@@ 426,11 371,7 @@ fn read_inventory_entry(reader: &mut reader::Reader) -> Option<InventoryEntry> {
    // TODO: maybe look up in the lookup table?
    let amount = reader.read_u32_le();
    let handle = reader.read_u32_le();
    Some(InventoryEntry {
        lookup_buf,
        amount,
        handle,
    })
    Some(InventoryEntry { lookup_buf, amount, handle })
}

impl Sl2 {


@@ 518,12 459,7 @@ impl Sl2 {
    }

    pub fn time_played(&self) -> std::time::Duration {
        std::time::Duration::from_millis(
            self.active_save_reader()
                .with_offset(24)
                .read_u32_le()
                .into(),
        )
        std::time::Duration::from_millis(self.active_save_reader().with_offset(24).read_u32_le().into())
    }

    pub fn base_stats(&self) -> BaseStats {


@@ 583,10 519,7 @@ impl Sl2 {

    pub fn unknown_stats_3(&self) -> [u8; 12] {
        let mut reader = self.stats_block_reader().with_offset(84);
        reader
            .read_bytes(12)
            .try_into()
            .expect("reader should fail if there's not enough data")
        reader.read_bytes(12).try_into().expect("reader should fail if there's not enough data")
    }

    pub fn rune_level(&self) -> u32 {


@@ 650,12 583,8 @@ impl Sl2 {
    pub fn name(&self) -> &utf16string::WStr<utf16string::LE> {
        let mut reader = self.stats_block_reader().with_offset(148);
        let name_buf = &reader.read_bytes(32);
        let name_with_null = &utf16string::WStr::from_utf16(name_buf)
            .expect("update() should have failed if the name isn't valid UTF-16");
        let terminator_position = name_with_null
            .char_indices()
            .find(|(_, c)| *c == 0 as char)
            .map(|(i, _)| i);
        let name_with_null = &utf16string::WStr::from_utf16(name_buf).expect("update() should have failed if the name isn't valid UTF-16");
        let terminator_position = name_with_null.char_indices().find(|(_, c)| *c == 0 as char).map(|(i, _)| i);
        match terminator_position {
            Some(n) => &name_with_null[0..n],
            None => name_with_null,