diff --git a/README.md b/README.md index 58c4743..f4ae02c 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,19 @@ The pretty JSON format looks something like: "Dragonborn.esm" ] }, + "worlds": [ + { + "form_id": 60, + "editor_id": "Tamriel" + } + ], "cells": [ { "form_id": 100000001, "editor_id": "SomeInterior", "x": null, "y": null, + "world_form_id": null, "is_persistent": false }, { @@ -60,6 +67,7 @@ The pretty JSON format looks something like: "editor_id": null, "x": 0, "y": 0, + "world_form_id": 60, "is_persistent": true }, { @@ -67,6 +75,7 @@ The pretty JSON format looks something like: "editor_id": "SomeExterior01", "x": 32, "y": 3, + "world_form_id": 60, "is_persistent": false }, { @@ -74,6 +83,7 @@ The pretty JSON format looks something like: "editor_id": "SomeExterior02", "x": 33, "y": 2, + "world_form_id": 60, "is_persistent": false }, { @@ -81,6 +91,7 @@ The pretty JSON format looks something like: "editor_id": null, "x": 32, "y": 1, + "world_form_id": 60, "is_persistent": false } ] diff --git a/src/parser.rs b/src/parser.rs index 712adc8..757331c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -12,13 +12,16 @@ use nom::{ }; use serde::Serialize; -const HEADER_SIZE: u32 = 24; +const RECORD_HEADER_SIZE: u32 = 24; +const FIELD_HEADER_SIZE: u32 = 6; /// A parsed TES5 Skyrim plugin file #[derive(Debug, PartialEq, Serialize)] pub struct Plugin<'a> { /// Parsed [TES4 header record](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/TES4) with metadata about the plugin pub header: PluginHeader<'a>, + /// Parsed [WRLD records](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/WRLD) contained in the plugin + pub worlds: Vec, /// Parsed [CELL records](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/CELL) contained in the plugin pub cells: Vec, } @@ -41,6 +44,8 @@ pub struct Cell { pub editor_id: Option, pub x: Option, pub y: Option, + /// The [`World`] that this cell belongs to. + pub world_form_id: Option, /// Indicates that this cell is a special persistent worldspace cell where all persistent references for the worldspace are stored pub is_persistent: bool, } @@ -55,6 +60,7 @@ struct CellData { #[derive(Debug)] pub struct UnparsedCell<'a> { form_id: u32, + world_form_id: Option, is_compressed: bool, is_persistent: bool, data: &'a [u8], @@ -64,10 +70,23 @@ pub struct UnparsedCell<'a> { #[derive(Debug)] struct DecompressedCell { pub form_id: u32, + world_form_id: Option, pub is_persistent: bool, pub data: Vec, } +/// Parsed [WRLD records](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/WRLD) +#[derive(Debug, PartialEq, Serialize)] +pub struct World { + /// Note that this `form_id` is relative to the plugin file, not what it would be in-game. + /// The first byte of the `form_id` can be interpreted as an index into the `masters` array of the [`PluginHeader`]. + /// That master plugin is the "owner" of the `World` and this plugin is editing it. + /// + /// If the first byte of the `form_id` is the length of the `masters` array, then this plugin owns the `World`. + pub form_id: u32, + pub editor_id: String, +} + #[derive(Debug)] struct GroupHeader<'a> { size: u32, @@ -130,7 +149,12 @@ struct FieldHeader<'a> { } /// Parses fields from the decompressed bytes of a CELL record. Returns remaining bytes of the input after parsing and the parsed Cell struct. -fn parse_cell<'a>(input: &'a [u8], form_id: u32, is_persistent: bool) -> IResult<&'a [u8], Cell> { +fn parse_cell<'a>( + input: &'a [u8], + form_id: u32, + is_persistent: bool, + world_form_id: Option, +) -> IResult<&'a [u8], Cell> { let (input, cell_data) = parse_cell_fields(input)?; Ok(( input, @@ -139,6 +163,7 @@ fn parse_cell<'a>(input: &'a [u8], form_id: u32, is_persistent: bool) -> IResult editor_id: cell_data.editor_id, x: cell_data.x, y: cell_data.y, + world_form_id, is_persistent, }, )) @@ -158,6 +183,7 @@ fn decompress_cells(unparsed_cells: Vec) -> Result) -> Result IResult<&[u8], (PluginHeader, Vec)> { +fn parse_header_and_cell_bytes( + input: &[u8], +) -> IResult<&[u8], (PluginHeader, Vec, Vec)> { let (input, header) = parse_plugin_header(input)?; - let (input, unparsed_cells) = parse_group_data(input, input.len() as u32, 0)?; - Ok((input, (header, unparsed_cells))) + let (input, (worlds, unparsed_cells)) = parse_group_data(input, input.len() as u32, 0, None)?; + Ok((input, (header, worlds, unparsed_cells))) } /// Parses header and cell records from input bytes of a plugin file and outputs `Plugin` struct with extracted fields. @@ -187,7 +215,7 @@ fn parse_header_and_cell_bytes(input: &[u8]) -> IResult<&[u8], (PluginHeader, Ve /// let plugin = parse_plugin(&plugin_contents).unwrap(); /// ``` pub fn parse_plugin(input: &[u8]) -> Result { - let (_, (header, unparsed_cells)) = parse_header_and_cell_bytes(&input) + let (_, (header, worlds, unparsed_cells)) = parse_header_and_cell_bytes(&input) .map_err(|_err| anyhow!("Failed to parse plugin header and find CELL data"))?; let decompressed_cells = decompress_cells(unparsed_cells)?; @@ -197,22 +225,30 @@ pub fn parse_plugin(input: &[u8]) -> Result { &decompressed_cell.data, decompressed_cell.form_id, decompressed_cell.is_persistent, + decompressed_cell.world_form_id, ) .unwrap(); cells.push(cell); } - Ok(Plugin { header, cells }) + Ok(Plugin { + header, + worlds, + cells, + }) } fn parse_group_data<'a>( input: &'a [u8], remaining_bytes: u32, depth: usize, -) -> IResult<&'a [u8], Vec> { + world_form_id: Option, +) -> IResult<&'a [u8], (Vec, Vec)> { let mut input = input; + let mut worlds = vec![]; let mut cells = vec![]; let mut consumed_bytes = 0; + let mut world_form_id = world_form_id; while !input.is_empty() && consumed_bytes < remaining_bytes { let (remaining, record_header) = parse_header(input)?; match record_header { @@ -221,20 +257,29 @@ fn parse_group_data<'a>( // TODO: get rid of unwrap let label = str::from_utf8(group_header.label).unwrap(); if label != "WRLD" && label != "CELL" { - let (remaining, _) = take(group_header.size - HEADER_SIZE)(remaining)?; + let (remaining, _) = + take(group_header.size - RECORD_HEADER_SIZE)(remaining)?; input = remaining; consumed_bytes += group_header.size; continue; + } else { + // reset world_form_id when entering new worldspace/cell group + world_form_id = None; } } else if group_header.group_type == 7 { // TODO: DRY - let (remaining, _) = take(group_header.size - HEADER_SIZE)(remaining)?; + let (remaining, _) = take(group_header.size - RECORD_HEADER_SIZE)(remaining)?; input = remaining; consumed_bytes += group_header.size; continue; } - let (remaining, mut inner_cells) = - parse_group_data(remaining, group_header.size - HEADER_SIZE, depth + 1)?; + let (remaining, (mut inner_worlds, mut inner_cells)) = parse_group_data( + remaining, + group_header.size - RECORD_HEADER_SIZE, + depth + 1, + world_form_id, + )?; + worlds.append(&mut inner_worlds); cells.append(&mut inner_cells); input = remaining; consumed_bytes += group_header.size; @@ -244,22 +289,33 @@ fn parse_group_data<'a>( let (remaining, data) = take(record_header.size)(remaining)?; cells.push(UnparsedCell { form_id: record_header.id, + world_form_id, is_compressed: record_header.flags.contains(RecordFlags::COMPRESSED), is_persistent: record_header.flags.contains(RecordFlags::PERSISTENT_REFR), data, }); input = remaining; - consumed_bytes += record_header.size + HEADER_SIZE; + consumed_bytes += record_header.size + RECORD_HEADER_SIZE; + } + "WRLD" => { + world_form_id = Some(record_header.id); + let (remaining, editor_id) = parse_world_fields(remaining, &record_header)?; + worlds.push(World { + form_id: record_header.id, + editor_id, + }); + input = remaining; + consumed_bytes += record_header.size + RECORD_HEADER_SIZE; } _ => { let (remaining, _) = take(record_header.size)(remaining)?; input = remaining; - consumed_bytes += record_header.size + HEADER_SIZE; + consumed_bytes += record_header.size + RECORD_HEADER_SIZE; } }, } } - Ok((input, cells)) + Ok((input, (worlds, cells))) } fn parse_plugin_header(input: &[u8]) -> IResult<&[u8], PluginHeader> { @@ -270,7 +326,7 @@ fn parse_plugin_header(input: &[u8]) -> IResult<&[u8], PluginHeader> { let (remaining, hedr) = verify(parse_field_header, |field_header| { field_header.field_type == "HEDR" })(input)?; - consumed_bytes += hedr.size as u32 + 6; + consumed_bytes += hedr.size as u32 + FIELD_HEADER_SIZE; input = remaining; let (remaining, (version, num_records_and_groups, next_object_id)) = parse_hedr_fields(input)?; input = remaining; @@ -280,7 +336,7 @@ fn parse_plugin_header(input: &[u8]) -> IResult<&[u8], PluginHeader> { let mut large_size = None; while consumed_bytes < tes4.size as u32 { let (remaining, field) = parse_field_header(input)?; - consumed_bytes += field.size as u32 + 6; + consumed_bytes += field.size as u32 + FIELD_HEADER_SIZE; input = remaining; match field.field_type { "CNAM" => { @@ -448,6 +504,20 @@ fn parse_cell_fields<'a>(input: &'a [u8]) -> IResult<&'a [u8], CellData> { Ok((input, cell_data)) } +fn parse_world_fields<'a>( + input: &'a [u8], + record_header: &RecordHeader, +) -> IResult<&'a [u8], String> { + let (remaining, field) = verify(parse_field_header, |field_header| { + field_header.field_type == "EDID" + })(input)?; + let (remaining, editor_id) = parse_zstring(remaining)?; + let record_bytes_left = + record_header.size as usize - field.size as usize - FIELD_HEADER_SIZE as usize; + let (remaining, _) = take(record_bytes_left)(remaining)?; + Ok((remaining, editor_id.to_string())) +} + fn parse_4char(input: &[u8]) -> IResult<&[u8], &str> { map_res(take(4usize), |bytes: &[u8]| str::from_utf8(bytes))(input) }