Parse WRLD records & link cells to worlds
Cell coordinates are useless without knowing the worldspace that they index into.
This commit is contained in:
parent
c20f4f71dd
commit
66da86fed9
11
README.md
11
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
|
||||
}
|
||||
]
|
||||
|
104
src/parser.rs
104
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<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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user