Add parser and cli, basic extraction done
This commit is contained in:
commit
200cf73248
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
291
Cargo.lock
generated
Normal file
291
Cargo.lock
generated
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argh"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91792f088f87cdc7a2cfb1d617fa5ea18d7f1dc22ef0e1b5f82f3157cdc522be"
|
||||||
|
dependencies = [
|
||||||
|
"argh_derive",
|
||||||
|
"argh_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argh_derive"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4eb0c0c120ad477412dc95a4ce31e38f2113e46bd13511253f79196ca68b067"
|
||||||
|
dependencies = [
|
||||||
|
"argh_shared",
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argh_shared"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "781f336cc9826dbaddb9754cb5db61e64cab4f69668bd19dcc4a0394a86f4cb1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitvec"
|
||||||
|
version = "0.19.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81"
|
||||||
|
dependencies = [
|
||||||
|
"funty",
|
||||||
|
"radium",
|
||||||
|
"tap",
|
||||||
|
"wyz",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crc32fast",
|
||||||
|
"libc",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "funty"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "0.4.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lexical-core"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.87"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "265d751d31d6780a3f956bb5b8022feba2d94eeee5a84ba64f4212eedca42213"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
|
||||||
|
dependencies = [
|
||||||
|
"adler",
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "6.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
|
||||||
|
dependencies = [
|
||||||
|
"bitvec",
|
||||||
|
"funty",
|
||||||
|
"lexical-core",
|
||||||
|
"memchr",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "radium"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.64"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "skyrim_cell_dump"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"argh",
|
||||||
|
"bitflags",
|
||||||
|
"flate2",
|
||||||
|
"nom",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.72"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tap"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wyz"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
|
24
Cargo.toml
Normal file
24
Cargo.toml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "skyrim_cell_dump"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Tyler Hallada <tyler@hallada.net>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
argh = { version = "0.1", optional = true }
|
||||||
|
bitflags = "1.2"
|
||||||
|
flate2 = "1.0"
|
||||||
|
nom = "6"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = { version = "1.0", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
build-binary = ["argh", "serde_json"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "skyrim-cell-dump"
|
||||||
|
path = "src/bin/cli.rs"
|
||||||
|
required-features = ["build-binary"]
|
73
src/bin/cli.rs
Normal file
73
src/bin/cli.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{fs::read, str::FromStr};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error, Result};
|
||||||
|
#[cfg(feature = "build-binary")]
|
||||||
|
use argh::FromArgs;
|
||||||
|
|
||||||
|
use skyrim_cell_dump::parse_plugin;
|
||||||
|
|
||||||
|
enum Format {
|
||||||
|
Json,
|
||||||
|
PlainText,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Format {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s.trim().to_lowercase().as_str() {
|
||||||
|
"json" => Ok(Format::Json),
|
||||||
|
"text" => Ok(Format::PlainText),
|
||||||
|
"plain" => Ok(Format::PlainText),
|
||||||
|
"plain_text" => Ok(Format::PlainText),
|
||||||
|
"plaintext" => Ok(Format::PlainText),
|
||||||
|
_ => Err(anyhow!("Unrecognized format {}", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromArgs)]
|
||||||
|
/// Extracts cell edits from a TES5 Skyrim plugin file
|
||||||
|
struct Args {
|
||||||
|
/// path to the plugin to parse
|
||||||
|
#[argh(positional)]
|
||||||
|
plugin: PathBuf,
|
||||||
|
/// format of the output (json or text)
|
||||||
|
#[argh(option, short = 'f', default = "Format::PlainText")]
|
||||||
|
format: Format,
|
||||||
|
/// pretty print json output
|
||||||
|
#[argh(switch, short = 'p')]
|
||||||
|
pretty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Args = argh::from_env();
|
||||||
|
let plugin_contents = match read(&args.plugin) {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(error) => {
|
||||||
|
return eprintln!(
|
||||||
|
"Failed to read from plugin file {}: {}",
|
||||||
|
&args.plugin.to_string_lossy(),
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let plugin = match parse_plugin(&plugin_contents) {
|
||||||
|
Ok(plugin) => plugin,
|
||||||
|
Err(error) => {
|
||||||
|
return eprintln!(
|
||||||
|
"Failed to parse plugin file {}: {}",
|
||||||
|
&args.plugin.to_string_lossy(),
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match args.format {
|
||||||
|
Format::PlainText => println!("{:#?}", &plugin),
|
||||||
|
Format::Json if args.pretty => {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&plugin).unwrap())
|
||||||
|
}
|
||||||
|
Format::Json => println!("{}", serde_json::to_string(&plugin).unwrap()),
|
||||||
|
}
|
||||||
|
}
|
6
src/lib.rs
Normal file
6
src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#[macro_use]
|
||||||
|
extern crate bitflags;
|
||||||
|
|
||||||
|
mod parser;
|
||||||
|
|
||||||
|
pub use parser::{decompress_cells, parse_cell, parse_plugin};
|
421
src/parser.rs
Normal file
421
src/parser.rs
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
use std::{convert::TryInto, str};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use flate2::read::ZlibDecoder;
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::complete::{take, take_while},
|
||||||
|
combinator::{map, map_res, verify},
|
||||||
|
number::complete::{le_f32, le_i32, le_u16, le_u32},
|
||||||
|
IResult,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
const HEADER_SIZE: u32 = 24;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize)]
|
||||||
|
pub struct Plugin<'a> {
|
||||||
|
pub header: PluginHeader<'a>,
|
||||||
|
pub cells: Vec<Cell>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize)]
|
||||||
|
pub struct PluginHeader<'a> {
|
||||||
|
pub version: f32,
|
||||||
|
pub num_records_and_groups: i32,
|
||||||
|
pub next_object_id: u32,
|
||||||
|
pub author: Option<&'a str>,
|
||||||
|
pub description: Option<&'a str>,
|
||||||
|
pub masters: Vec<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize)]
|
||||||
|
pub struct Cell {
|
||||||
|
pub form_id: u32,
|
||||||
|
pub editor_id: Option<String>,
|
||||||
|
pub x: Option<i32>,
|
||||||
|
pub y: Option<i32>,
|
||||||
|
pub is_persistent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CellData {
|
||||||
|
editor_id: Option<String>,
|
||||||
|
x: Option<i32>,
|
||||||
|
y: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UnparsedCell<'a> {
|
||||||
|
form_id: u32,
|
||||||
|
is_compressed: bool,
|
||||||
|
is_persistent: bool,
|
||||||
|
data: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DecompressedCell {
|
||||||
|
pub form_id: u32,
|
||||||
|
pub is_persistent: bool,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct GroupHeader<'a> {
|
||||||
|
size: u32,
|
||||||
|
label: &'a [u8; 4],
|
||||||
|
group_type: i32,
|
||||||
|
timestamp: u16,
|
||||||
|
version_control_info: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct RecordHeader<'a> {
|
||||||
|
record_type: &'a str,
|
||||||
|
size: u32,
|
||||||
|
flags: RecordFlags,
|
||||||
|
id: u32,
|
||||||
|
timestamp: u16,
|
||||||
|
version_control_info: u16,
|
||||||
|
version: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
struct RecordFlags: u32 {
|
||||||
|
const MASTER_FILE = 0x00000001;
|
||||||
|
const DELETED_GROUP = 0x00000010;
|
||||||
|
const DELETED_RECORD = 0x00000020;
|
||||||
|
const CONSTANT = 0x00000040;
|
||||||
|
const LOCALIZED = 0x00000080;
|
||||||
|
const INACCESSIBLE = 0x00000100;
|
||||||
|
const LIGHT_MASTER_FILE = 0x00000200;
|
||||||
|
const PERSISTENT_REFR = 0x00000400;
|
||||||
|
const INITIALLY_DISABLED = 0x00000800;
|
||||||
|
const IGNORED = 0x00001000;
|
||||||
|
const VISIBLE_WHEN_DISTANT = 0x00008000;
|
||||||
|
const RANDOM_ANIM_START = 0x00010000;
|
||||||
|
const OFF_LIMITS = 0x00020000;
|
||||||
|
const COMPRESSED = 0x00040000;
|
||||||
|
const CANT_WAIT = 0x00080000;
|
||||||
|
const IGNORE_OBJECT_INTERACTION = 0x00100000;
|
||||||
|
const IS_MARKER = 0x00800000;
|
||||||
|
const NO_AI_ACQUIRE = 0x02000000;
|
||||||
|
const NAVMESH_FILTER = 0x04000000;
|
||||||
|
const NAVMESH_BOUNDING_BOX = 0x08000000;
|
||||||
|
const REFLECTED_BY_AUTO_WATER = 0x10000000;
|
||||||
|
const DONT_HAVOK_SETTLE = 0x20000000;
|
||||||
|
const NO_RESPAWN = 0x40000000;
|
||||||
|
const MULTI_BOUND = 0x80000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Header<'a> {
|
||||||
|
Group(GroupHeader<'a>),
|
||||||
|
Record(RecordHeader<'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FieldHeader<'a> {
|
||||||
|
field_type: &'a str,
|
||||||
|
size: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_cell<'a>(
|
||||||
|
input: &'a [u8],
|
||||||
|
form_id: u32,
|
||||||
|
is_persistent: bool,
|
||||||
|
) -> IResult<&'a [u8], Cell> {
|
||||||
|
let (input, cell_data) = parse_cell_fields(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Cell {
|
||||||
|
form_id,
|
||||||
|
editor_id: cell_data.editor_id,
|
||||||
|
x: cell_data.x,
|
||||||
|
y: cell_data.y,
|
||||||
|
is_persistent,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decompress_cells(unparsed_cells: Vec<UnparsedCell>) -> Result<Vec<DecompressedCell>> {
|
||||||
|
let mut decompressed_cells = Vec::new();
|
||||||
|
for unparsed_cell in unparsed_cells {
|
||||||
|
let decompressed_data = if unparsed_cell.is_compressed {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut decoder = ZlibDecoder::new(&unparsed_cell.data[4..]);
|
||||||
|
decoder.read_to_end(&mut buf)?;
|
||||||
|
buf
|
||||||
|
} else {
|
||||||
|
unparsed_cell.data.to_vec()
|
||||||
|
};
|
||||||
|
decompressed_cells.push(DecompressedCell {
|
||||||
|
form_id: unparsed_cell.form_id,
|
||||||
|
is_persistent: unparsed_cell.is_persistent,
|
||||||
|
data: decompressed_data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(decompressed_cells)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_header_and_cell_bytes(
|
||||||
|
input: &[u8],
|
||||||
|
) -> IResult<&[u8], (PluginHeader, 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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_plugin(input: &[u8]) -> Result<Plugin> {
|
||||||
|
let (_, (header, 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)?;
|
||||||
|
|
||||||
|
let mut cells = Vec::new();
|
||||||
|
for decompressed_cell in decompressed_cells {
|
||||||
|
let (_, cell) = parse_cell(
|
||||||
|
&decompressed_cell.data,
|
||||||
|
decompressed_cell.form_id,
|
||||||
|
decompressed_cell.is_persistent,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
cells.push(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Plugin { header, cells })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_group_data<'a>(
|
||||||
|
input: &'a [u8],
|
||||||
|
remaining_bytes: u32,
|
||||||
|
depth: usize,
|
||||||
|
) -> IResult<&'a [u8], Vec<UnparsedCell>> {
|
||||||
|
let mut input = input;
|
||||||
|
let mut cells = vec![];
|
||||||
|
let mut consumed_bytes = 0;
|
||||||
|
while !input.is_empty() && consumed_bytes < remaining_bytes {
|
||||||
|
let (remaining, record_header) = parse_header(input)?;
|
||||||
|
match record_header {
|
||||||
|
Header::Group(group_header) => {
|
||||||
|
if group_header.group_type == 0 {
|
||||||
|
// 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)?;
|
||||||
|
input = remaining;
|
||||||
|
consumed_bytes += group_header.size;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if group_header.group_type == 7 {
|
||||||
|
// TODO: DRY
|
||||||
|
let (remaining, _) = take(group_header.size - 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)?;
|
||||||
|
cells.append(&mut inner_cells);
|
||||||
|
input = remaining;
|
||||||
|
consumed_bytes += group_header.size;
|
||||||
|
}
|
||||||
|
Header::Record(record_header) => match record_header.record_type {
|
||||||
|
"CELL" => {
|
||||||
|
let (remaining, data) = take(record_header.size)(remaining)?;
|
||||||
|
cells.push(UnparsedCell {
|
||||||
|
form_id: record_header.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;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let (remaining, _) = take(record_header.size)(remaining)?;
|
||||||
|
input = remaining;
|
||||||
|
consumed_bytes += record_header.size + HEADER_SIZE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((input, cells))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_plugin_header(input: &[u8]) -> IResult<&[u8], PluginHeader> {
|
||||||
|
let (mut input, _tes4) = verify(parse_record_header, |record_header| {
|
||||||
|
record_header.record_type == "TES4"
|
||||||
|
})(input)?;
|
||||||
|
let (remaining, _hedr) = verify(parse_field_header, |field_header| {
|
||||||
|
field_header.field_type == "HEDR"
|
||||||
|
})(input)?;
|
||||||
|
input = remaining;
|
||||||
|
let (remaining, (version, num_records_and_groups, next_object_id)) = parse_hedr_fields(input)?;
|
||||||
|
input = remaining;
|
||||||
|
let mut author = None;
|
||||||
|
let mut description = None;
|
||||||
|
let mut masters = vec![];
|
||||||
|
loop {
|
||||||
|
let (remaining, field) = parse_field_header(input)?;
|
||||||
|
input = remaining;
|
||||||
|
match field.field_type {
|
||||||
|
"CNAM" => {
|
||||||
|
let (remaining, author_str) = parse_zstring(input)?;
|
||||||
|
input = remaining;
|
||||||
|
author = Some(author_str);
|
||||||
|
}
|
||||||
|
"SNAM" => {
|
||||||
|
let (remaining, desc_str) = parse_zstring(input)?;
|
||||||
|
input = remaining;
|
||||||
|
description = Some(desc_str);
|
||||||
|
}
|
||||||
|
"MAST" => {
|
||||||
|
let (remaining, master_str) = parse_zstring(input)?;
|
||||||
|
input = remaining;
|
||||||
|
masters.push(master_str);
|
||||||
|
}
|
||||||
|
"INTV" => {
|
||||||
|
let (remaining, _) = take(field.size)(input)?;
|
||||||
|
input = remaining;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let (remaining, _) = take(field.size)(input)?;
|
||||||
|
input = remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
PluginHeader {
|
||||||
|
version,
|
||||||
|
num_records_and_groups,
|
||||||
|
next_object_id,
|
||||||
|
author,
|
||||||
|
description,
|
||||||
|
masters,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_group_header(input: &[u8]) -> IResult<&[u8], GroupHeader> {
|
||||||
|
let (input, _record_type) =
|
||||||
|
verify(parse_4char, |record_type: &str| record_type == "GRUP")(input)?;
|
||||||
|
let (input, size) = le_u32(input)?;
|
||||||
|
let (input, label) = map_res(take(4usize), |bytes: &[u8]| bytes.try_into())(input)?;
|
||||||
|
let (input, group_type) = le_i32(input)?;
|
||||||
|
let (input, timestamp) = le_u16(input)?;
|
||||||
|
let (input, version_control_info) = le_u16(input)?;
|
||||||
|
let (input, _) = take(4usize)(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
GroupHeader {
|
||||||
|
size,
|
||||||
|
label,
|
||||||
|
group_type,
|
||||||
|
timestamp,
|
||||||
|
version_control_info,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_record_header(input: &[u8]) -> IResult<&[u8], RecordHeader> {
|
||||||
|
let (input, record_type) =
|
||||||
|
verify(parse_4char, |record_type: &str| record_type != "GRUP")(input)?;
|
||||||
|
let (input, size) = le_u32(input)?;
|
||||||
|
let (input, flags) = map_res(le_u32, |bits| {
|
||||||
|
RecordFlags::from_bits(bits).ok_or("bad record flag")
|
||||||
|
})(input)?;
|
||||||
|
let (input, id) = le_u32(input)?;
|
||||||
|
let (input, timestamp) = le_u16(input)?;
|
||||||
|
let (input, version_control_info) = le_u16(input)?;
|
||||||
|
let (input, version) = le_u16(input)?;
|
||||||
|
let (input, _) = take(2usize)(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
RecordHeader {
|
||||||
|
record_type,
|
||||||
|
size,
|
||||||
|
flags,
|
||||||
|
id,
|
||||||
|
timestamp,
|
||||||
|
version_control_info,
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(input: &[u8]) -> IResult<&[u8], Header> {
|
||||||
|
alt((
|
||||||
|
map(parse_group_header, |group_header| {
|
||||||
|
Header::Group(group_header)
|
||||||
|
}),
|
||||||
|
map(parse_record_header, |record_header| {
|
||||||
|
Header::Record(record_header)
|
||||||
|
}),
|
||||||
|
))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_field_header(input: &[u8]) -> IResult<&[u8], FieldHeader> {
|
||||||
|
let (input, field_type) = parse_4char(input)?;
|
||||||
|
if field_type == "XXXX" {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
let (input, size) = le_u16(input)?;
|
||||||
|
// let (input, data) = take(size)(input)?;
|
||||||
|
Ok((input, FieldHeader { field_type, size }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_hedr_fields(input: &[u8]) -> IResult<&[u8], (f32, i32, u32)> {
|
||||||
|
let (input, version) = le_f32(input)?;
|
||||||
|
let (input, num_records_and_groups) = le_i32(input)?;
|
||||||
|
let (input, next_object_id) = le_u32(input)?;
|
||||||
|
Ok((input, (version, num_records_and_groups, next_object_id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cell_fields<'a>(input: &'a [u8]) -> IResult<&'a [u8], CellData> {
|
||||||
|
let mut cell_data = CellData {
|
||||||
|
editor_id: None,
|
||||||
|
x: None,
|
||||||
|
y: None,
|
||||||
|
};
|
||||||
|
let mut input = input;
|
||||||
|
while !input.is_empty() {
|
||||||
|
let (remaining, field) = parse_field_header(input)?;
|
||||||
|
input = remaining;
|
||||||
|
match field.field_type {
|
||||||
|
"EDID" => {
|
||||||
|
let (remaining, editor_id) = parse_zstring(input)?;
|
||||||
|
cell_data.editor_id = Some(editor_id.to_string());
|
||||||
|
input = remaining;
|
||||||
|
}
|
||||||
|
"XCLC" => {
|
||||||
|
let (remaining, x) = le_i32(input)?;
|
||||||
|
let (remaining, y) = le_i32(remaining)?;
|
||||||
|
cell_data.x = Some(x);
|
||||||
|
cell_data.y = Some(y);
|
||||||
|
let (remaining, _) = take(4usize)(remaining)?;
|
||||||
|
input = remaining;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let (remaining, _) = take(field.size)(input)?;
|
||||||
|
input = remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((input, cell_data))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_4char(input: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
map_res(take(4usize), |bytes: &[u8]| str::from_utf8(bytes))(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_zstring(input: &[u8]) -> IResult<&[u8], &str> {
|
||||||
|
let (input, zstring) = map_res(take_while(|byte| byte != 0), |bytes: &[u8]| {
|
||||||
|
str::from_utf8(bytes)
|
||||||
|
})(input)?;
|
||||||
|
let (input, _) = take(1usize)(input)?;
|
||||||
|
Ok((input, zstring))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user