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"
]
},
"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
}
]

View File

@ -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<World>,
/// Parsed [CELL records](https://en.uesp.net/wiki/Skyrim_Mod:Mod_File_Format/CELL) contained in the plugin
pub cells: Vec<Cell>,
}
@ -41,6 +44,8 @@ pub struct Cell {
pub editor_id: Option<String>,
pub x: 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
pub is_persistent: bool,
}
@ -55,6 +60,7 @@ struct CellData {
#[derive(Debug)]
pub struct UnparsedCell<'a> {
form_id: u32,
world_form_id: Option<u32>,
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<u32>,
pub is_persistent: bool,
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)]
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<u32>,
) -> 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<UnparsedCell>) -> Result<Vec<Decompresse
};
decompressed_cells.push(DecompressedCell {
form_id: unparsed_cell.form_id,
world_form_id: unparsed_cell.world_form_id,
is_persistent: unparsed_cell.is_persistent,
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.
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, 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<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"))?;
let decompressed_cells = decompress_cells(unparsed_cells)?;
@ -197,22 +225,30 @@ pub fn parse_plugin(input: &[u8]) -> Result<Plugin> {
&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<UnparsedCell>> {
world_form_id: Option<u32>,
) -> IResult<&'a [u8], (Vec<World>, Vec<UnparsedCell>)> {
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)
}