Parse WRLD records & link cells to worlds

Cell coordinates are useless without knowing the worldspace that they index into.
This commit is contained in:
Tyler Hallada 2021-07-19 00:19:26 -04:00
parent c20f4f71dd
commit 66da86fed9
2 changed files with 98 additions and 17 deletions

View File

@ -47,12 +47,19 @@ The pretty JSON format looks something like:
"Dragonborn.esm" "Dragonborn.esm"
] ]
}, },
"worlds": [
{
"form_id": 60,
"editor_id": "Tamriel"
}
],
"cells": [ "cells": [
{ {
"form_id": 100000001, "form_id": 100000001,
"editor_id": "SomeInterior", "editor_id": "SomeInterior",
"x": null, "x": null,
"y": null, "y": null,
"world_form_id": null,
"is_persistent": false "is_persistent": false
}, },
{ {
@ -60,6 +67,7 @@ The pretty JSON format looks something like:
"editor_id": null, "editor_id": null,
"x": 0, "x": 0,
"y": 0, "y": 0,
"world_form_id": 60,
"is_persistent": true "is_persistent": true
}, },
{ {
@ -67,6 +75,7 @@ The pretty JSON format looks something like:
"editor_id": "SomeExterior01", "editor_id": "SomeExterior01",
"x": 32, "x": 32,
"y": 3, "y": 3,
"world_form_id": 60,
"is_persistent": false "is_persistent": false
}, },
{ {
@ -74,6 +83,7 @@ The pretty JSON format looks something like:
"editor_id": "SomeExterior02", "editor_id": "SomeExterior02",
"x": 33, "x": 33,
"y": 2, "y": 2,
"world_form_id": 60,
"is_persistent": false "is_persistent": false
}, },
{ {
@ -81,6 +91,7 @@ The pretty JSON format looks something like:
"editor_id": null, "editor_id": null,
"x": 32, "x": 32,
"y": 1, "y": 1,
"world_form_id": 60,
"is_persistent": false "is_persistent": false
} }
] ]

View File

@ -12,13 +12,16 @@ use nom::{
}; };
use serde::Serialize; 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 /// A parsed TES5 Skyrim plugin file
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
pub struct Plugin<'a> { pub struct Plugin<'a> {
/// Parsed [TES4 header record](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/TES4) with metadata about the plugin /// Parsed [TES4 header record](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/TES4) with metadata about the plugin
pub header: PluginHeader<'a>, 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<World>,
/// Parsed [CELL records](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/CELL) contained in the plugin /// Parsed [CELL records](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/CELL) contained in the plugin
pub cells: Vec<Cell>, pub cells: Vec<Cell>,
} }
@ -41,6 +44,8 @@ pub struct Cell {
pub editor_id: Option<String>, pub editor_id: Option<String>,
pub x: Option<i32>, pub x: Option<i32>,
pub y: Option<i32>, pub y: Option<i32>,
/// The [`World`] that this cell belongs to.
pub world_form_id: Option<u32>,
/// Indicates that this cell is a special persistent worldspace cell where all persistent references for the worldspace are stored /// Indicates that this cell is a special persistent worldspace cell where all persistent references for the worldspace are stored
pub is_persistent: bool, pub is_persistent: bool,
} }
@ -55,6 +60,7 @@ struct CellData {
#[derive(Debug)] #[derive(Debug)]
pub struct UnparsedCell<'a> { pub struct UnparsedCell<'a> {
form_id: u32, form_id: u32,
world_form_id: Option<u32>,
is_compressed: bool, is_compressed: bool,
is_persistent: bool, is_persistent: bool,
data: &'a [u8], data: &'a [u8],
@ -64,10 +70,23 @@ pub struct UnparsedCell<'a> {
#[derive(Debug)] #[derive(Debug)]
struct DecompressedCell { struct DecompressedCell {
pub form_id: u32, pub form_id: u32,
world_form_id: Option<u32>,
pub is_persistent: bool, pub is_persistent: bool,
pub data: Vec<u8>, pub data: Vec<u8>,
} }
/// 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)] #[derive(Debug)]
struct GroupHeader<'a> { struct GroupHeader<'a> {
size: u32, 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. /// 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<u32>,
) -> IResult<&'a [u8], Cell> {
let (input, cell_data) = parse_cell_fields(input)?; let (input, cell_data) = parse_cell_fields(input)?;
Ok(( Ok((
input, 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, editor_id: cell_data.editor_id,
x: cell_data.x, x: cell_data.x,
y: cell_data.y, y: cell_data.y,
world_form_id,
is_persistent, is_persistent,
}, },
)) ))
@ -158,6 +183,7 @@ fn decompress_cells(unparsed_cells: Vec<UnparsedCell>) -> Result<Vec<Decompresse
}; };
decompressed_cells.push(DecompressedCell { decompressed_cells.push(DecompressedCell {
form_id: unparsed_cell.form_id, form_id: unparsed_cell.form_id,
world_form_id: unparsed_cell.world_form_id,
is_persistent: unparsed_cell.is_persistent, is_persistent: unparsed_cell.is_persistent,
data: decompressed_data, data: decompressed_data,
}); });
@ -166,10 +192,12 @@ fn decompress_cells(unparsed_cells: Vec<UnparsedCell>) -> Result<Vec<Decompresse
} }
/// Parses the plugin header and finds and extracts the headers and unparsed (and possibly compressed) data sections of every CELL record in the file. /// Parses the plugin header and finds and extracts the headers and unparsed (and possibly compressed) data sections of every CELL record in the file.
fn parse_header_and_cell_bytes(input: &[u8]) -> IResult<&[u8], (PluginHeader, Vec<UnparsedCell>)> { fn parse_header_and_cell_bytes(
input: &[u8],
) -> IResult<&[u8], (PluginHeader, Vec<World>, Vec<UnparsedCell>)> {
let (input, header) = parse_plugin_header(input)?; let (input, header) = parse_plugin_header(input)?;
let (input, unparsed_cells) = parse_group_data(input, input.len() as u32, 0)?; let (input, (worlds, unparsed_cells)) = parse_group_data(input, input.len() as u32, 0, None)?;
Ok((input, (header, unparsed_cells))) 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. /// 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(); /// let plugin = parse_plugin(&plugin_contents).unwrap();
/// ``` /// ```
pub fn parse_plugin(input: &[u8]) -> Result<Plugin> { pub fn parse_plugin(input: &[u8]) -> Result<Plugin> {
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"))?; .map_err(|_err| anyhow!("Failed to parse plugin header and find CELL data"))?;
let decompressed_cells = decompress_cells(unparsed_cells)?; let decompressed_cells = decompress_cells(unparsed_cells)?;
@ -197,22 +225,30 @@ pub fn parse_plugin(input: &[u8]) -> Result<Plugin> {
&decompressed_cell.data, &decompressed_cell.data,
decompressed_cell.form_id, decompressed_cell.form_id,
decompressed_cell.is_persistent, decompressed_cell.is_persistent,
decompressed_cell.world_form_id,
) )
.unwrap(); .unwrap();
cells.push(cell); cells.push(cell);
} }
Ok(Plugin { header, cells }) Ok(Plugin {
header,
worlds,
cells,
})
} }
fn parse_group_data<'a>( fn parse_group_data<'a>(
input: &'a [u8], input: &'a [u8],
remaining_bytes: u32, remaining_bytes: u32,
depth: usize, depth: usize,
) -> IResult<&'a [u8], Vec<UnparsedCell>> { world_form_id: Option<u32>,
) -> IResult<&'a [u8], (Vec<World>, Vec<UnparsedCell>)> {
let mut input = input; let mut input = input;
let mut worlds = vec![];
let mut cells = vec![]; let mut cells = vec![];
let mut consumed_bytes = 0; let mut consumed_bytes = 0;
let mut world_form_id = world_form_id;
while !input.is_empty() && consumed_bytes < remaining_bytes { while !input.is_empty() && consumed_bytes < remaining_bytes {
let (remaining, record_header) = parse_header(input)?; let (remaining, record_header) = parse_header(input)?;
match record_header { match record_header {
@ -221,20 +257,29 @@ fn parse_group_data<'a>(
// TODO: get rid of unwrap // TODO: get rid of unwrap
let label = str::from_utf8(group_header.label).unwrap(); let label = str::from_utf8(group_header.label).unwrap();
if label != "WRLD" && label != "CELL" { 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; input = remaining;
consumed_bytes += group_header.size; consumed_bytes += group_header.size;
continue; continue;
} else {
// reset world_form_id when entering new worldspace/cell group
world_form_id = None;
} }
} else if group_header.group_type == 7 { } else if group_header.group_type == 7 {
// TODO: DRY // TODO: DRY
let (remaining, _) = take(group_header.size - HEADER_SIZE)(remaining)?; let (remaining, _) = take(group_header.size - RECORD_HEADER_SIZE)(remaining)?;
input = remaining; input = remaining;
consumed_bytes += group_header.size; consumed_bytes += group_header.size;
continue; continue;
} }
let (remaining, mut inner_cells) = let (remaining, (mut inner_worlds, mut inner_cells)) = parse_group_data(
parse_group_data(remaining, group_header.size - HEADER_SIZE, depth + 1)?; remaining,
group_header.size - RECORD_HEADER_SIZE,
depth + 1,
world_form_id,
)?;
worlds.append(&mut inner_worlds);
cells.append(&mut inner_cells); cells.append(&mut inner_cells);
input = remaining; input = remaining;
consumed_bytes += group_header.size; consumed_bytes += group_header.size;
@ -244,22 +289,33 @@ fn parse_group_data<'a>(
let (remaining, data) = take(record_header.size)(remaining)?; let (remaining, data) = take(record_header.size)(remaining)?;
cells.push(UnparsedCell { cells.push(UnparsedCell {
form_id: record_header.id, form_id: record_header.id,
world_form_id,
is_compressed: record_header.flags.contains(RecordFlags::COMPRESSED), is_compressed: record_header.flags.contains(RecordFlags::COMPRESSED),
is_persistent: record_header.flags.contains(RecordFlags::PERSISTENT_REFR), is_persistent: record_header.flags.contains(RecordFlags::PERSISTENT_REFR),
data, data,
}); });
input = remaining; 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)?; let (remaining, _) = take(record_header.size)(remaining)?;
input = 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> { 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| { let (remaining, hedr) = verify(parse_field_header, |field_header| {
field_header.field_type == "HEDR" field_header.field_type == "HEDR"
})(input)?; })(input)?;
consumed_bytes += hedr.size as u32 + 6; consumed_bytes += hedr.size as u32 + FIELD_HEADER_SIZE;
input = remaining; input = remaining;
let (remaining, (version, num_records_and_groups, next_object_id)) = parse_hedr_fields(input)?; let (remaining, (version, num_records_and_groups, next_object_id)) = parse_hedr_fields(input)?;
input = remaining; input = remaining;
@ -280,7 +336,7 @@ fn parse_plugin_header(input: &[u8]) -> IResult<&[u8], PluginHeader> {
let mut large_size = None; let mut large_size = None;
while consumed_bytes < tes4.size as u32 { while consumed_bytes < tes4.size as u32 {
let (remaining, field) = parse_field_header(input)?; 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; input = remaining;
match field.field_type { match field.field_type {
"CNAM" => { "CNAM" => {
@ -448,6 +504,20 @@ fn parse_cell_fields<'a>(input: &'a [u8]) -> IResult<&'a [u8], CellData> {
Ok((input, cell_data)) 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> { fn parse_4char(input: &[u8]) -> IResult<&[u8], &str> {
map_res(take(4usize), |bytes: &[u8]| str::from_utf8(bytes))(input) map_res(take(4usize), |bytes: &[u8]| str::from_utf8(bytes))(input)
} }