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"
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
104
src/parser.rs
104
src/parser.rs
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user