mod parser;
mod strings;
mod translate;
mod writer;
mod stats;
mod esl;
pub use stats::PluginStats;
use crate::group::Group;
use crate::record::Record;
use crate::string_file::{StringFileSet, StringFileType};
use crate::string_routes::StringRouter;
use memmap2::Mmap;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Debug)]
pub struct Plugin {
pub path: PathBuf,
pub header: Record,
pub groups: Vec<Group>,
pub masters: Vec<String>,
#[deprecated(since = "0.6.0", note = "Use string_router instead")]
pub string_records: HashMap<String, Vec<String>>,
string_router: Arc<dyn StringRouter>,
string_files: Option<StringFileSet>,
#[allow(dead_code)]
language: String,
#[allow(dead_code)]
mmap: Option<Arc<Mmap>>,
}
impl Plugin {
pub fn get_name(&self) -> &str {
self.path.file_name().unwrap().to_str().unwrap()
}
pub fn get_type(&self) -> &str {
match self.path.extension().and_then(|ext| ext.to_str()) {
Some("esp") => "插件 (ESP)",
Some("esm") => "主文件 (ESM)",
Some("esl") => "轻量级文件 (ESL)",
_ => "未知",
}
}
pub fn is_master(&self) -> bool {
self.path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase() == "esm")
.unwrap_or(false)
}
pub fn is_localized(&self) -> bool {
self.header.flags & 0x00000080 != 0
}
pub fn string_router(&self) -> &dyn StringRouter {
self.string_router.as_ref()
}
pub fn set_string_files(&mut self, string_files: StringFileSet) {
self.string_files = Some(string_files);
}
pub fn is_light(&self) -> bool {
if let Some(ext) = self.path.extension() {
if ext.to_string_lossy().to_lowercase() == "esl" {
return true;
}
}
const LIGHT_MASTER_FLAG: u32 = 0x00000200;
(self.header.flags & LIGHT_MASTER_FLAG) != 0
}
pub(crate) fn format_form_id(&self, form_id: u32) -> String {
let master_index = (form_id >> 24) as usize;
let master_file = if master_index < self.masters.len() {
&self.masters[master_index]
} else {
self.path.file_name().unwrap().to_str().unwrap()
};
format!("{:08X}|{}", form_id, master_file)
}
pub(crate) fn determine_string_file_type(
record_type: &str,
subrecord_type: &str,
) -> StringFileType {
if record_type == "INFO" {
return StringFileType::ILSTRINGS;
}
if matches!(subrecord_type, "DESC" | "CNAM") {
return StringFileType::DLSTRINGS;
}
StringFileType::STRINGS
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_info_routes_to_ilstrings() {
let file_type = Plugin::determine_string_file_type("INFO", "NAM1");
assert_eq!(
file_type,
StringFileType::ILSTRINGS,
"INFO记录应该路由到ILSTRINGS(对话信息)"
);
}
#[test]
fn test_desc_routes_to_dlstrings() {
let file_type = Plugin::determine_string_file_type("PERK", "DESC");
assert_eq!(
file_type,
StringFileType::DLSTRINGS,
"PERK DESC应该路由到DLSTRINGS"
);
let file_type = Plugin::determine_string_file_type("WEAP", "DESC");
assert_eq!(
file_type,
StringFileType::DLSTRINGS,
"WEAP DESC应该路由到DLSTRINGS"
);
let file_type = Plugin::determine_string_file_type("MESG", "DESC");
assert_eq!(
file_type,
StringFileType::DLSTRINGS,
"MESG DESC应该路由到DLSTRINGS"
);
}
#[test]
fn test_cnam_routes_to_dlstrings() {
let file_type = Plugin::determine_string_file_type("QUST", "CNAM");
assert_eq!(
file_type,
StringFileType::DLSTRINGS,
"QUST CNAM应该路由到DLSTRINGS"
);
let file_type = Plugin::determine_string_file_type("BOOK", "CNAM");
assert_eq!(
file_type,
StringFileType::DLSTRINGS,
"BOOK CNAM应该路由到DLSTRINGS"
);
}
#[test]
fn test_full_routes_to_strings() {
let file_type = Plugin::determine_string_file_type("WEAP", "FULL");
assert_eq!(
file_type,
StringFileType::STRINGS,
"WEAP FULL应该路由到STRINGS"
);
let file_type = Plugin::determine_string_file_type("PERK", "FULL");
assert_eq!(
file_type,
StringFileType::STRINGS,
"PERK FULL应该路由到STRINGS"
);
let file_type = Plugin::determine_string_file_type("DIAL", "FULL");
assert_eq!(
file_type,
StringFileType::STRINGS,
"DIAL FULL应该路由到STRINGS"
);
}
}