use crc_fast::{checksum, CrcAlgorithm};
use csv::{QuoteStyle, ReaderBuilder, WriterBuilder};
use getset::*;
#[cfg(feature = "integration_log")] use log::warn;
use rayon::prelude::*;
use serde_derive::{Serialize, Deserialize};
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::{fmt, fmt::{Debug, Display}};
use std::fs::{DirBuilder, File};
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom, BufWriter, Write};
use std::path::{Path, PathBuf};
use crate::binary::{ReadBytes, WriteBytes};
use crate::compression::{CompressionFormat, Decompressible};
use crate::encryption::Decryptable;
use crate::error::{Result, RLibError};
use crate::games::{GameInfo, pfh_version::PFHVersion, supported_games::*};
use crate::{REGEX_DB, REGEX_PORTRAIT_SETTINGS};
use crate::schema::{Schema, Definition};
use crate::utils::*;
use self::anim::Anim;
use self::anim_fragment_battle::AnimFragmentBattle;
use self::animpack::AnimPack;
use self::anims_table::AnimsTable;
use self::atlas::Atlas;
use self::audio::Audio;
use self::bmd::Bmd;
use self::bmd_vegetation::BmdVegetation;
use self::dat::Dat;
use self::db::DB;
use self::esf::ESF;
use self::font::Font;
use self::group_formations::GroupFormations;
use self::hlsl_compiled::HlslCompiled;
use self::image::Image;
use self::loc::Loc;
use self::matched_combat::MatchedCombat;
use self::pack::{Pack, RESERVED_NAME_SETTINGS, RESERVED_NAME_NOTES, RESERVED_NAME_DEPENDENCIES_MANAGER, RESERVED_NAME_DEPENDENCIES_MANAGER_V2};
use self::portrait_settings::PortraitSettings;
use self::rigidmodel::RigidModel;
use self::sound_bank::SoundBank;
use self::text::Text;
use self::uic::UIC;
use self::unit_variant::UnitVariant;
use self::unknown::Unknown;
use self::video::Video;
pub mod anim;
pub mod anim_fragment_battle;
pub mod animpack;
pub mod anims_table;
pub mod atlas;
pub mod audio;
#[allow(dead_code)]pub mod bmd;
#[allow(dead_code)]pub mod bmd_vegetation;
pub mod cs2_collision;
pub mod cs2_parsed;
pub mod dat;
pub mod db;
#[allow(dead_code)]pub mod esf;
pub mod font;
pub mod group_formations;
pub mod hlsl_compiled;
pub mod image;
pub mod loc;
pub mod matched_combat;
pub mod pack;
pub mod portrait_settings;
pub mod rigidmodel;
#[allow(dead_code)]pub mod sound_bank;
pub mod sound_bank_database;
pub mod sound_events;
pub mod table;
pub mod text;
pub mod tile_database;
pub mod uic;
pub mod unit_variant;
pub mod unknown;
pub mod video;
#[cfg(test)] mod rfile_test;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RFile {
path: String,
timestamp: Option<u64>,
file_type: FileType,
container_name: Option<String>,
data: RFileInnerData,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
enum RFileInnerData {
Decoded(Box<RFileDecoded>),
Cached(Vec<u8>),
OnDisk(OnDisk)
}
#[derive(Clone, Debug, PartialEq, Getters, Serialize, Deserialize)]
struct OnDisk {
path: String,
timestamp: u64,
start: u64,
size: u64,
is_compressed: bool,
is_encrypted: Option<PFHVersion>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum RFileDecoded {
Anim(Anim),
AnimFragmentBattle(AnimFragmentBattle),
AnimPack(AnimPack),
AnimsTable(AnimsTable),
Atlas(Atlas),
Audio(Audio),
BMD(Box<Bmd>),
BMDVegetation(BmdVegetation),
Dat(Dat),
DB(DB),
ESF(ESF),
Font(Font),
GroupFormations(GroupFormations),
HlslCompiled(HlslCompiled),
Image(Image),
Loc(Loc),
MatchedCombat(MatchedCombat),
Pack(Pack),
PortraitSettings(PortraitSettings),
RigidModel(RigidModel),
SoundBank(SoundBank),
Text(Text),
UIC(UIC),
UnitVariant(UnitVariant),
Unknown(Unknown),
Video(Video),
VMD(Text),
WSModel(Text),
}
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub enum FileType {
Anim,
AnimFragmentBattle,
AnimPack,
AnimsTable,
Atlas,
Audio,
BMD,
BMDVegetation,
Dat,
DB,
ESF,
Font,
GroupFormations,
HlslCompiled,
Image,
Loc,
MatchedCombat,
Pack,
PortraitSettings,
RigidModel,
SoundBank,
Text,
UIC,
UnitVariant,
Video,
VMD,
WSModel,
#[default]
Unknown,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum ContainerPath {
File(String),
Folder(String),
}
#[derive(Clone, Debug, Default, Getters, Setters)]
#[getset(get = "pub", set = "pub")]
pub struct DecodeableExtraData<'a> {
lazy_load: bool,
is_encrypted: bool,
return_incomplete: bool,
schema: Option<&'a Schema>,
disk_file_path: Option<&'a str>,
disk_file_offset: u64,
timestamp: u64,
table_name: Option<&'a str>,
is_dds: bool,
game_info: Option<&'a GameInfo>,
file_name: Option<&'a str>,
data_size: u64,
skip_path_cache_generation: bool,
}
#[derive(Clone, Default, Getters, Setters)]
#[getset(get = "pub", set = "pub")]
pub struct EncodeableExtraData<'a> {
test_mode: bool,
nullify_dates: bool,
table_has_guid: bool,
regenerate_table_guid: bool,
game_info: Option<&'a GameInfo>,
compression_format: CompressionFormat,
disable_compression: bool
}
pub trait Decodeable: Send + Sync {
fn decode<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> where Self: Sized;
}
pub trait Encodeable: Send + Sync {
fn encode<W: WriteBytes>(&mut self, buffer: &mut W, extra_data: &Option<EncodeableExtraData>) -> Result<()>;
}
pub trait Container {
fn extract(&mut self,
container_path: ContainerPath,
destination_path: &Path,
keep_container_path_structure: bool,
schema: &Option<Schema>,
case_insensitive: bool,
keys_first: bool,
extra_data: &Option<EncodeableExtraData>,
keep_data_in_memory: bool
) -> Result<Vec<PathBuf>> {
let mut extracted_paths = vec![];
match container_path {
ContainerPath::File(mut container_path) => {
if container_path.starts_with('/') {
container_path.remove(0);
}
let destination_path = if keep_container_path_structure {
destination_path.to_owned().join(&container_path)
} else {
destination_path.to_owned()
};
let mut destination_folder = destination_path.to_owned();
destination_folder.pop();
DirBuilder::new().recursive(true).create(&destination_folder)?;
let rfile = self.files_mut().get_mut(&container_path).ok_or_else(|| RLibError::FileNotFound(container_path.to_string()))?;
if let RFileInnerData::OnDisk(_) = &rfile.data {
if keep_data_in_memory {
rfile.load()?;
}
}
if let Some(schema) = schema {
if rfile.file_type() == FileType::DB || rfile.file_type() == FileType::Loc {
let mut destination_path_tsv = destination_path.to_owned();
match destination_path_tsv.extension() {
Some(extension) => {
let extension = format!("{}.tsv", extension.to_string_lossy());
destination_path_tsv.set_extension(extension)
},
None => destination_path_tsv.set_extension("tsv"),
};
let result = rfile.tsv_export_to_path(&destination_path_tsv, schema, keys_first);
if result.is_err() {
#[cfg(feature = "integration_log")] {
warn!("File with path {} failed to extract as TSV. Extracting it as binary.", rfile.path_in_container_raw());
}
let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
extracted_paths.push(extracted_path);
} else {
extracted_paths.push(destination_path_tsv);
result?;
}
} else {
let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
extracted_paths.push(extracted_path);
}
}
else {
let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
extracted_paths.push(extracted_path);
}
}
ContainerPath::Folder(mut container_path) => {
if container_path.starts_with('/') {
container_path.remove(0);
}
let mut rfiles = self.files_by_path_mut(&ContainerPath::Folder(container_path.clone()), case_insensitive);
for rfile in &mut rfiles {
let container_path = rfile.path_in_container_raw();
let destination_path = if keep_container_path_structure {
destination_path.to_owned().join(container_path)
} else {
destination_path.to_owned()
};
let mut destination_folder = destination_path.to_owned();
destination_folder.pop();
DirBuilder::new().recursive(true).create(&destination_folder)?;
if let RFileInnerData::OnDisk(_) = &rfile.data {
if keep_data_in_memory {
rfile.load()?;
}
}
if let Some(schema) = schema {
if rfile.file_type() == FileType::DB || rfile.file_type() == FileType::Loc {
let mut destination_path_tsv = destination_path.to_owned();
match destination_path_tsv.extension() {
Some(extension) => {
let extension = format!("{}.tsv", extension.to_string_lossy());
destination_path_tsv.set_extension(extension)
},
None => destination_path_tsv.set_extension("tsv"),
};
let result = rfile.tsv_export_to_path(&destination_path_tsv, schema, keys_first);
if result.is_err() {
#[cfg(feature = "integration_log")] {
warn!("File with path {} failed to extract as TSV. Extracting it as binary.", rfile.path_in_container_raw());
}
let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
extracted_paths.push(extracted_path);
} else {
extracted_paths.push(destination_path_tsv);
result?;
}
} else {
let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
extracted_paths.push(extracted_path);
}
}
else {
let extracted_path = rfile.sanitize_and_create_file(&destination_path, extra_data)?;
extracted_paths.push(extracted_path);
}
}
if container_path.is_empty() {
extracted_paths.append(&mut self.extract_metadata(destination_path)?);
}
}
}
Ok(extracted_paths)
}
fn extract_metadata(&mut self, _destination_path: &Path) -> Result<Vec<PathBuf>> {
Ok(vec![])
}
fn insert(&mut self, file: RFile) -> Result<Option<ContainerPath>> {
let path = file.path_in_container();
let path_raw = file.path_in_container_raw();
self.paths_cache_insert_path(path_raw);
self.files_mut().insert(path_raw.to_owned(), file);
Ok(Some(path))
}
fn insert_file(&mut self, source_path: &Path, container_path_folder: &str, schema: &Option<Schema>) -> Result<Option<ContainerPath>> {
let mut container_path_folder = container_path_folder.replace('\\', "/");
if container_path_folder.starts_with('/') {
container_path_folder.remove(0);
}
if container_path_folder.ends_with('/') || container_path_folder.is_empty() {
let trimmed_path = source_path.file_name()
.ok_or_else(|| RLibError::PathMissingFileName(source_path.to_string_lossy().to_string()))?
.to_string_lossy().to_string();
container_path_folder = container_path_folder.to_owned() + &trimmed_path;
}
let mut tsv_imported = false;
let mut rfile = match source_path.extension() {
Some(extension) => {
if extension.to_string_lossy() == "tsv" {
tsv_imported = true;
let rfile = RFile::tsv_import_from_path(source_path, schema);
if let Err(_error) = rfile {
#[cfg(feature = "integration_log")] {
warn!("File with path {} failed to import as TSV. Importing it as binary. If you're using the CLI - did you forget to provide schema with --tsv-as-binary flag? Error was: {}", &source_path.to_string_lossy(), _error);
}
tsv_imported = false;
RFile::new_from_file_path(source_path)
} else {
rfile
}
} else {
RFile::new_from_file_path(source_path)
}
}
None => {
RFile::new_from_file_path(source_path)
}
}?;
if !tsv_imported {
rfile.set_path_in_container_raw(&container_path_folder);
}
rfile.load()?;
rfile.guess_file_type()?;
self.insert(rfile)
}
fn insert_folder(&mut self, source_path: &Path, container_path_folder: &str, ignored_paths: &Option<Vec<&str>>, schema: &Option<Schema>, include_base_folder: bool) -> Result<Vec<ContainerPath>> {
let mut container_path_folder = container_path_folder.replace('\\', "/");
if !container_path_folder.is_empty() && !container_path_folder.ends_with('/') {
container_path_folder.push('/');
}
if container_path_folder.starts_with('/') {
container_path_folder.remove(0);
}
let mut source_path_without_base_folder = source_path.to_path_buf();
source_path_without_base_folder.pop();
let file_paths = files_from_subdir(source_path, true)?;
let mut inserted_paths = Vec::with_capacity(file_paths.len());
for file_path in file_paths {
let trimmed_path = if include_base_folder {
file_path.strip_prefix(&source_path_without_base_folder)?
} else {
file_path.strip_prefix(source_path)?
}.to_string_lossy().replace('\\', "/");
let file_container_path = container_path_folder.to_owned() + &trimmed_path;
if let Some(ignored_paths) = ignored_paths {
if ignored_paths.iter().any(|x| trimmed_path.starts_with(x)) {
continue;
}
}
let mut tsv_imported = false;
let mut rfile =
match file_path.extension() {
Some(extension) => {
if extension.to_string_lossy() == "tsv" {
tsv_imported = true;
let rfile = RFile::tsv_import_from_path(&file_path, schema);
if let Err(_error) = rfile {
#[cfg(feature = "integration_log")] {
warn!("File with path {} failed to import as TSV. Importing it as binary. If you're using the CLI - did you forget to provide schema with --tsv-as-binary flag? Error was: {}", &file_path.to_string_lossy(), _error);
}
tsv_imported = false;
RFile::new_from_file_path(&file_path)
} else {
rfile
}
} else {
RFile::new_from_file_path(&file_path)
}
}
None => {
RFile::new_from_file_path(&file_path)
}
}?;
if !tsv_imported {
rfile.set_path_in_container_raw(&file_container_path);
}
rfile.load()?;
rfile.guess_file_type()?;
if let Some(path) = self.insert(rfile)? {
inserted_paths.push(path);
}
}
Ok(inserted_paths)
}
fn remove(&mut self, path: &ContainerPath) -> Vec<ContainerPath> {
match path {
ContainerPath::File(path) => {
let mut path = path.to_owned();
if path.starts_with('/') {
path.remove(0);
}
self.paths_cache_remove_path(&path);
self.files_mut().remove(&path);
vec![ContainerPath::File(path.to_owned())]
},
ContainerPath::Folder(path) => {
let mut path = path.to_owned();
if path.starts_with('/') {
path.remove(0);
}
if path.is_empty() {
self.files_mut().clear();
vec![ContainerPath::Folder(String::new()); 1]
}
else {
let mut path_full = path.to_owned();
path_full.push('/');
let paths_to_remove = self.files().par_iter()
.filter_map(|(key, _)| {
if key.starts_with(&path_full) {
Some(key.to_owned())
} else {
None
}
}).collect::<Vec<String>>();
paths_to_remove.iter().for_each(|path| {
self.paths_cache_remove_path(path);
self.files_mut().remove(path);
});
if paths_to_remove.is_empty() {
vec![ContainerPath::Folder(path); 1]
} else {
paths_to_remove.par_iter().map(|path| ContainerPath::File(path.to_string())).collect()
}
}
}
}
}
fn disk_file_path(&self) -> &str;
fn disk_file_name(&self) -> String {
PathBuf::from(self.disk_file_path()).file_name().unwrap_or_default().to_string_lossy().to_string()
}
fn disk_file_offset(&self) -> u64;
fn has_file(&self, path: &str) -> bool {
self.files().get(path).is_some()
}
fn has_folder(&self, path: &str) -> bool {
if path.is_empty() {
false
} else {
let path = if path.ends_with('/') {
path.to_string()
} else {
let mut path = path.to_string();
path.push('/');
path
};
self.files().keys().any(|x| x.starts_with(&path) && x.len() > path.len())
}
}
fn file(&self, path: &str, case_insensitive: bool) -> Option<&RFile> {
if case_insensitive {
let lower = path.to_lowercase();
self.paths_cache().get(&lower).and_then(|paths| self.files().get(&paths[0]))
} else {
self.files().get(path)
}
}
fn file_mut(&mut self, path: &str, case_insensitive: bool) -> Option<&mut RFile> {
if case_insensitive {
let lower = path.to_lowercase();
self.paths_cache().get(&lower).cloned().and_then(|paths| self.files_mut().get_mut(&paths[0]))
} else {
self.files_mut().get_mut(path)
}
}
fn files(&self) -> &HashMap<String, RFile>;
fn files_mut(&mut self) -> &mut HashMap<String, RFile>;
fn files_by_type(&self, file_types: &[FileType]) -> Vec<&RFile> {
self.files().par_iter().filter(|(_, file)| file_types.contains(&file.file_type)).map(|(_, file)| file).collect()
}
fn files_by_type_mut(&mut self, file_types: &[FileType]) -> Vec<&mut RFile> {
self.files_mut().par_iter_mut().filter(|(_, file)| file_types.contains(&file.file_type)).map(|(_, file)| file).collect()
}
fn files_by_path(&self, path: &ContainerPath, case_insensitive: bool) -> Vec<&RFile> {
match path {
ContainerPath::File(path) => self.file(path, case_insensitive).map(|file| vec![file]).unwrap_or(vec![]),
ContainerPath::Folder(path) => {
if path.is_empty() {
self.files().values().collect()
}
else {
let mut path = if case_insensitive { path.to_lowercase() } else { path.to_owned() };
if !path.ends_with('/') {
path.push('/');
}
let real_paths = self.paths_cache()
.par_iter()
.filter_map(|(lower_path, real_paths)| if lower_path.starts_with(&path) { Some(real_paths) } else { None })
.map(|paths| paths.iter().map(|path| ContainerPath::File(path.to_owned())).collect::<Vec<_>>())
.flatten()
.collect::<Vec<_>>();
self.files_by_paths(&real_paths, false)
}
},
}
}
fn files_by_path_mut(&mut self, path: &ContainerPath, case_insensitive: bool) -> Vec<&mut RFile> {
match path {
ContainerPath::File(path) => self.file_mut(path, case_insensitive).map(|file| vec![file]).unwrap_or(vec![]),
ContainerPath::Folder(path) => {
if path.is_empty() {
self.files_mut().values_mut().collect()
}
else {
self.files_mut().par_iter_mut()
.filter_map(|(key, file)|
if case_insensitive {
if starts_with_case_insensitive(key, path) { Some(file) } else { None }
} else if key.starts_with(path) {
Some(file)
} else {
None
}
).collect::<Vec<&mut RFile>>()
}
},
}
}
fn files_by_paths(&self, paths: &[ContainerPath], case_insensitive: bool) -> Vec<&RFile> {
paths.iter()
.flat_map(|path| self.files_by_path(path, case_insensitive))
.collect()
}
fn files_by_paths_mut(&mut self, paths: &[ContainerPath], case_insensitive: bool) -> Vec<&mut RFile> {
self.files_mut()
.iter_mut()
.filter(|(file_path, _)| {
paths.iter().any(|path| {
match path {
ContainerPath::File(path) => {
if case_insensitive {
caseless::canonical_caseless_match_str(file_path, path)
} else {
file_path == &path
}
}
ContainerPath::Folder(path) => {
if case_insensitive {
starts_with_case_insensitive(file_path, path)
} else {
file_path.starts_with(path)
}
}
}
})
})
.map(|(_, file)| file)
.collect()
}
fn files_by_type_and_paths(&self, file_types: &[FileType], paths: &[ContainerPath], case_insensitive: bool) -> Vec<&RFile> {
paths.iter()
.flat_map(|path| self.files_by_path(path, case_insensitive)
.into_iter()
.filter(|file| file_types.contains(&file.file_type()))
.collect::<Vec<_>>()
).collect()
}
fn files_by_type_and_paths_mut(&mut self, file_types: &[FileType], paths: &[ContainerPath], case_insensitive: bool) -> Vec<&mut RFile> {
self.files_by_paths_mut(paths, case_insensitive).into_iter().filter(|file| file_types.contains(&file.file_type())).collect()
}
fn paths_cache_generate(&mut self) {
self.paths_cache_mut().clear();
let mut cache: HashMap<String, Vec<String>> = HashMap::new();
self.files().keys().for_each(|path| {
let lower = path.to_lowercase();
match cache.get_mut(&lower) {
Some(paths) => paths.push(path.to_owned()),
None => { cache.insert(lower, vec![path.to_owned()]); },
}
});
*self.paths_cache_mut() = cache;
}
fn paths_cache_insert_path(&mut self, path: &str) {
let path_lower = path.to_lowercase();
match self.paths_cache_mut().get_mut(&path_lower) {
Some(paths) => if paths.iter().all(|x| x != path) {
paths.push(path.to_owned());
}
None => { self.paths_cache_mut().insert(path_lower, vec![path.to_owned()]); }
}
}
fn paths_cache_remove_path(&mut self, path: &str) {
let path_lower = path.to_lowercase();
if path_lower == RESERVED_NAME_NOTES || path_lower == RESERVED_NAME_SETTINGS {
return;
}
match self.paths_cache_mut().get_mut(&path_lower) {
Some(paths) => {
match paths.iter().position(|x| x == path) {
Some(pos) => {
paths.remove(pos);
if paths.is_empty() {
self.paths_cache_mut().remove(&path_lower);
}
},
#[cfg(feature = "integration_log")]None => { warn!("remove_path received a valid path, but we don't have casing equivalence for it. This is a bug. {path_lower}, {path}"); },
#[cfg(not(feature = "integration_log"))]None => { dbg!("remove_path received a valid path, but we don't have casing equivalence for it. This is a bug. {path_lower}, {path}"); },
}
}
#[cfg(feature = "integration_log")] None => { warn!("remove_path received an invalid path. This is a bug. {path_lower}, {path}"); },
#[cfg(not(feature = "integration_log"))]None => { dbg!("remove_path received an invalid path. This is a bug. {path_lower}, {path}"); },
}
}
fn paths_cache(&self) -> &HashMap<String, Vec<String>>;
fn paths_cache_mut(&mut self) -> &mut HashMap<String, Vec<String>>;
fn paths_folders_raw(&self) -> HashSet<String> {
self.files()
.par_iter()
.filter_map(|(path, _)| {
let file_path_split = path.split('/').collect::<Vec<&str>>();
let folder_path_len = file_path_split.len() - 1;
if folder_path_len == 0 {
None
} else {
let mut paths = Vec::with_capacity(folder_path_len);
for (index, folder) in file_path_split.iter().enumerate() {
if index < path.len() - 1 && !folder.is_empty() {
paths.push(file_path_split[0..=index].join("/"))
}
}
Some(paths)
}
})
.flatten()
.collect::<HashSet<String>>()
}
fn paths(&self) -> Vec<ContainerPath> {
self.files()
.par_iter()
.map(|(path, _)| ContainerPath::File(path.to_owned()))
.collect()
}
fn paths_raw(&self) -> Vec<&str> {
self.files()
.par_iter()
.map(|(path, _)| &**path)
.collect()
}
fn paths_raw_from_container_path(&self, path: &ContainerPath) -> Vec<String> {
match path {
ContainerPath::File(path) => vec![path.to_owned(); 1],
ContainerPath::Folder(path) => {
if path.is_empty() {
self.paths_raw().iter().map(|x| x.to_string()).collect()
}
else {
self.files().par_iter()
.filter_map(|(key, file)|
if key.starts_with(path) {
Some(file.path_in_container_raw().to_owned())
} else {
None
}
).collect::<Vec<String>>()
}
},
}
}
fn internal_timestamp(&self) -> u64 {
0
}
fn local_timestamp(&self) -> u64;
fn preload(&mut self) -> Result<()> {
self.files_mut()
.into_par_iter()
.try_for_each(|(_, rfile)| rfile.encode(&None, false, true, false).map(|_| ()))
}
fn move_paths(&mut self, in_out_paths: &[(ContainerPath, ContainerPath)]) -> Result<Vec<(ContainerPath, ContainerPath)>> {
let mut successes = vec![];
for (source_path, destination_path) in in_out_paths {
successes.append(&mut self.move_path(source_path, destination_path)?);
}
Ok(successes)
}
fn move_path(&mut self, source_path: &ContainerPath, destination_path: &ContainerPath) -> Result<Vec<(ContainerPath, ContainerPath)>> {
match source_path {
ContainerPath::File(source_path) => match destination_path {
ContainerPath::File(destination_path) => {
if destination_path.is_empty() {
return Err(RLibError::EmptyDestiny);
}
self.paths_cache_remove_path(source_path);
let mut moved = self
.files_mut()
.remove(source_path)
.ok_or_else(|| RLibError::FileNotFound(source_path.to_string()))?;
moved.set_path_in_container_raw(destination_path);
self.insert(moved).map(|x| match x {
Some(x) => vec![(ContainerPath::File(source_path.to_string()), x); 1],
None => Vec::with_capacity(0)
})
},
ContainerPath::Folder(_) => unreachable!("move_path_1"),
},
ContainerPath::Folder(source_path) => match destination_path {
ContainerPath::File(_) => unreachable!("move_path_2"),
ContainerPath::Folder(destination_path) => {
if destination_path.is_empty() {
return Err(RLibError::EmptyDestiny);
}
let mut source_path_end = source_path.to_owned();
if !source_path_end.ends_with('/') {
source_path_end.push('/');
}
let moved_paths = self.files()
.par_iter()
.filter_map(|(path, _)| if path.starts_with(&source_path_end) { Some(path.to_owned()) } else { None })
.collect::<Vec<_>>();
let moved = moved_paths.iter()
.filter_map(|x| {
self.paths_cache_remove_path(x);
self.files_mut().remove(x)
})
.collect::<Vec<_>>();
let mut new_paths = Vec::with_capacity(moved.len());
for mut moved in moved {
let old_path = moved.path_in_container();
let new_path = moved.path_in_container_raw().replacen(source_path, destination_path, 1);
moved.set_path_in_container_raw(&new_path);
if let Some(new_path) = self.insert(moved)? {
new_paths.push((old_path, new_path));
}
}
Ok(new_paths)
},
},
}
}
fn clean_undecoded(&mut self) {
self.files_mut().retain(|_, file| file.decoded().is_ok() || file.cached().is_ok());
}
}
impl RFile {
pub fn new_from_container<C: Container>(
container: &C,
size: u64,
is_compressed: bool,
is_encrypted: Option<PFHVersion>,
data_pos: u64,
file_timestamp: u64,
path_in_container: &str,
) -> Result<Self> {
let on_disk = OnDisk {
path: container.disk_file_path().to_owned(),
timestamp: container.local_timestamp(),
start: container.disk_file_offset() + data_pos,
size,
is_compressed,
is_encrypted,
};
let rfile = Self {
path: path_in_container.to_owned(),
timestamp: if file_timestamp == 0 { None } else { Some(file_timestamp) },
file_type: FileType::Unknown,
container_name: Some(container.disk_file_name()),
data: RFileInnerData::OnDisk(on_disk)
};
Ok(rfile)
}
pub fn new_from_file(path: &str) -> Result<Self> {
let path_checked = PathBuf::from(path);
if !path_checked.is_file() {
return Err(RLibError::FileNotFound(path.to_owned()));
}
let mut file = File::open(path)?;
let on_disk = OnDisk {
path: path.to_owned(),
timestamp: last_modified_time_from_file(&file)?,
start: 0,
size: file.len()?,
is_compressed: false,
is_encrypted: None,
};
let rfile = Self {
path: path.to_owned(),
timestamp: Some(on_disk.timestamp),
file_type: FileType::Unknown,
container_name: None,
data: RFileInnerData::OnDisk(on_disk)
};
Ok(rfile)
}
pub fn new_from_file_path(path: &Path) -> Result<Self> {
let path = path.to_string_lossy().to_string();
Self::new_from_file(&path)
}
pub fn new_from_vec(data: &[u8], file_type: FileType, timestamp: u64, path: &str) -> Self {
Self {
path: path.to_owned(),
timestamp: if timestamp == 0 { None } else { Some(timestamp) },
file_type,
container_name: None,
data: RFileInnerData::Cached(data.to_vec())
}
}
pub fn new_from_decoded(data: &RFileDecoded, timestamp: u64, path: &str) -> Self {
Self {
path: path.to_owned(),
timestamp: if timestamp == 0 { None } else { Some(timestamp) },
file_type: FileType::from(data),
container_name: None,
data: RFileInnerData::Decoded(Box::new(data.clone()))
}
}
pub fn cached(&self) -> Result<&[u8]> {
match self.data {
RFileInnerData::Cached(ref data) => Ok(data),
_ => Err(RLibError::FileNotCached(self.path_in_container_raw().to_string()))
}
}
pub fn cached_mut(&mut self) -> Result<&mut Vec<u8>> {
match self.data {
RFileInnerData::Cached(ref mut data) => Ok(data),
_ => Err(RLibError::FileNotCached(self.path_in_container_raw().to_string()))
}
}
pub fn decoded(&self) -> Result<&RFileDecoded> {
match self.data {
RFileInnerData::Decoded(ref data) => Ok(data),
_ => Err(RLibError::FileNotDecoded(self.path_in_container_raw().to_string()))
}
}
pub fn decoded_mut(&mut self) -> Result<&mut RFileDecoded> {
match self.data {
RFileInnerData::Decoded(ref mut data) => Ok(data),
_ => Err(RLibError::FileNotDecoded(self.path_in_container_raw().to_string()))
}
}
pub fn set_cached(&mut self, data: &[u8]) {
self.data = RFileInnerData::Cached(data.to_vec());
}
pub fn set_decoded(&mut self, decoded: RFileDecoded) -> Result<()> {
match (self.file_type(), &decoded) {
(FileType::Anim, &RFileDecoded::Anim(_)) |
(FileType::AnimFragmentBattle, &RFileDecoded::AnimFragmentBattle(_)) |
(FileType::AnimPack, &RFileDecoded::AnimPack(_)) |
(FileType::AnimsTable, &RFileDecoded::AnimsTable(_)) |
(FileType::Atlas, &RFileDecoded::Atlas(_)) |
(FileType::Audio, &RFileDecoded::Audio(_)) |
(FileType::BMD, &RFileDecoded::BMD(_)) |
(FileType::BMDVegetation, &RFileDecoded::BMDVegetation(_)) |
(FileType::Dat, &RFileDecoded::Dat(_)) |
(FileType::DB, &RFileDecoded::DB(_)) |
(FileType::ESF, &RFileDecoded::ESF(_)) |
(FileType::Font, &RFileDecoded::Font(_)) |
(FileType::GroupFormations, &RFileDecoded::GroupFormations(_)) |
(FileType::HlslCompiled, &RFileDecoded::HlslCompiled(_)) |
(FileType::Image, &RFileDecoded::Image(_)) |
(FileType::Loc, &RFileDecoded::Loc(_)) |
(FileType::MatchedCombat, &RFileDecoded::MatchedCombat(_)) |
(FileType::Pack, &RFileDecoded::Pack(_)) |
(FileType::PortraitSettings, &RFileDecoded::PortraitSettings(_)) |
(FileType::RigidModel, &RFileDecoded::RigidModel(_)) |
(FileType::SoundBank, &RFileDecoded::SoundBank(_)) |
(FileType::Text, &RFileDecoded::Text(_)) |
(FileType::UIC, &RFileDecoded::UIC(_)) |
(FileType::UnitVariant, &RFileDecoded::UnitVariant(_)) |
(FileType::Video, &RFileDecoded::Video(_)) |
(FileType::VMD, &RFileDecoded::VMD(_)) |
(FileType::WSModel, &RFileDecoded::WSModel(_)) |
(FileType::Unknown, &RFileDecoded::Unknown(_)) => self.data = RFileInnerData::Decoded(Box::new(decoded)),
_ => return Err(RLibError::DecodedDataDoesNotMatchFileType(self.file_type(), From::from(&decoded)))
}
Ok(())
}
pub fn decode(&mut self, extra_data: &Option<DecodeableExtraData>, keep_in_cache: bool, return_data: bool) -> Result<Option<RFileDecoded>> {
let mut already_decoded = false;
let decoded = match &self.data {
RFileInnerData::Decoded(data) => {
already_decoded = true;
if !return_data {
return Ok(None);
}
*data.clone()
},
RFileInnerData::Cached(data) => {
let mut extra_data = match extra_data {
Some(extra_data) => extra_data.clone(),
None => DecodeableExtraData::default(),
};
extra_data.file_name = self.file_name();
extra_data.data_size = data.len() as u64;
let mut data = Cursor::new(data);
match self.file_type {
FileType::Anim => RFileDecoded::Anim(Anim::decode(&mut data, &Some(extra_data))?),
FileType::AnimFragmentBattle => RFileDecoded::AnimFragmentBattle(AnimFragmentBattle::decode(&mut data, &Some(extra_data))?),
FileType::AnimPack => RFileDecoded::AnimPack(AnimPack::decode(&mut data, &Some(extra_data))?),
FileType::AnimsTable => RFileDecoded::AnimsTable(AnimsTable::decode(&mut data, &Some(extra_data))?),
FileType::Atlas => RFileDecoded::Atlas(Atlas::decode(&mut data, &Some(extra_data))?),
FileType::Audio => RFileDecoded::Audio(Audio::decode(&mut data, &Some(extra_data))?),
FileType::BMD => RFileDecoded::BMD(Box::new(Bmd::decode(&mut data, &Some(extra_data))?)),
FileType::BMDVegetation => RFileDecoded::BMDVegetation(BmdVegetation::decode(&mut data, &Some(extra_data))?),
FileType::Dat => RFileDecoded::Dat(Dat::decode(&mut data, &Some(extra_data))?),
FileType::DB => {
if extra_data.table_name.is_none() {
extra_data.table_name = self.db_table_name_from_path();
}
RFileDecoded::DB(DB::decode(&mut data, &Some(extra_data))?)
},
FileType::ESF => RFileDecoded::ESF(ESF::decode(&mut data, &Some(extra_data))?),
FileType::Font => RFileDecoded::Font(Font::decode(&mut data, &Some(extra_data))?),
FileType::GroupFormations => RFileDecoded::GroupFormations(GroupFormations::decode(&mut data, &Some(extra_data))?),
FileType::HlslCompiled => RFileDecoded::HlslCompiled(HlslCompiled::decode(&mut data, &Some(extra_data))?),
FileType::Image => {
if self.path.ends_with(".dds") {
extra_data.is_dds = true;
}
RFileDecoded::Image(Image::decode(&mut data, &Some(extra_data))?)
},
FileType::Loc => RFileDecoded::Loc(Loc::decode(&mut data, &Some(extra_data))?),
FileType::MatchedCombat => RFileDecoded::MatchedCombat(MatchedCombat::decode(&mut data, &Some(extra_data))?),
FileType::Pack => RFileDecoded::Pack(Pack::decode(&mut data, &Some(extra_data))?),
FileType::PortraitSettings => RFileDecoded::PortraitSettings(PortraitSettings::decode(&mut data, &Some(extra_data))?),
FileType::RigidModel => RFileDecoded::RigidModel(RigidModel::decode(&mut data, &Some(extra_data))?),
FileType::SoundBank => RFileDecoded::SoundBank(SoundBank::decode(&mut data, &Some(extra_data))?),
FileType::Text => RFileDecoded::Text(Text::decode(&mut data, &Some(extra_data))?),
FileType::UIC => RFileDecoded::UIC(UIC::decode(&mut data, &Some(extra_data))?),
FileType::UnitVariant => RFileDecoded::UnitVariant(UnitVariant::decode(&mut data, &Some(extra_data))?),
FileType::Unknown => RFileDecoded::Unknown(Unknown::decode(&mut data, &Some(extra_data))?),
FileType::Video => RFileDecoded::Video(Video::decode(&mut data, &Some(extra_data))?),
FileType::VMD => RFileDecoded::VMD(Text::decode(&mut data, &Some(extra_data))?),
FileType::WSModel => RFileDecoded::WSModel(Text::decode(&mut data, &Some(extra_data))?),
}
},
RFileInnerData::OnDisk(data) => {
let raw_data = data.read()?;
let mut extra_data = match extra_data {
Some(extra_data) => extra_data.clone(),
None => DecodeableExtraData::default(),
};
extra_data.file_name = self.file_name();
extra_data.data_size = raw_data.len() as u64;
let mut data = Cursor::new(raw_data);
match self.file_type {
FileType::Anim => RFileDecoded::Anim(Anim::decode(&mut data, &Some(extra_data))?),
FileType::AnimFragmentBattle => RFileDecoded::AnimFragmentBattle(AnimFragmentBattle::decode(&mut data, &Some(extra_data))?),
FileType::AnimsTable => RFileDecoded::AnimsTable(AnimsTable::decode(&mut data, &Some(extra_data))?),
FileType::AnimPack => RFileDecoded::AnimPack(AnimPack::decode(&mut data, &Some(extra_data))?),
FileType::Atlas => RFileDecoded::Atlas(Atlas::decode(&mut data, &Some(extra_data))?),
FileType::Audio => RFileDecoded::Audio(Audio::decode(&mut data, &Some(extra_data))?),
FileType::BMD => RFileDecoded::BMD(Box::new(Bmd::decode(&mut data, &Some(extra_data))?)),
FileType::BMDVegetation => RFileDecoded::BMDVegetation(BmdVegetation::decode(&mut data, &Some(extra_data))?),
FileType::Dat => RFileDecoded::Dat(Dat::decode(&mut data, &Some(extra_data))?),
FileType::DB => {
if extra_data.table_name.is_none() {
extra_data.table_name = self.db_table_name_from_path();
}
RFileDecoded::DB(DB::decode(&mut data, &Some(extra_data))?)
},
FileType::ESF => RFileDecoded::ESF(ESF::decode(&mut data, &Some(extra_data))?),
FileType::Font => RFileDecoded::Font(Font::decode(&mut data, &Some(extra_data))?),
FileType::GroupFormations => RFileDecoded::GroupFormations(GroupFormations::decode(&mut data, &Some(extra_data))?),
FileType::HlslCompiled => RFileDecoded::HlslCompiled(HlslCompiled::decode(&mut data, &Some(extra_data))?),
FileType::Image => {
if self.path.ends_with(".dds") {
extra_data.is_dds = true;
}
RFileDecoded::Image(Image::decode(&mut data, &Some(extra_data))?)
},
FileType::Loc => RFileDecoded::Loc(Loc::decode(&mut data, &Some(extra_data))?),
FileType::MatchedCombat => RFileDecoded::MatchedCombat(MatchedCombat::decode(&mut data, &Some(extra_data))?),
FileType::Pack => RFileDecoded::Pack(Pack::decode(&mut data, &Some(extra_data))?),
FileType::PortraitSettings => RFileDecoded::PortraitSettings(PortraitSettings::decode(&mut data, &Some(extra_data))?),
FileType::RigidModel => RFileDecoded::RigidModel(RigidModel::decode(&mut data, &Some(extra_data))?),
FileType::SoundBank => RFileDecoded::SoundBank(SoundBank::decode(&mut data, &Some(extra_data))?),
FileType::Text => RFileDecoded::Text(Text::decode(&mut data, &Some(extra_data))?),
FileType::UIC => RFileDecoded::UIC(UIC::decode(&mut data, &Some(extra_data))?),
FileType::UnitVariant => RFileDecoded::UnitVariant(UnitVariant::decode(&mut data, &Some(extra_data))?),
FileType::Unknown => RFileDecoded::Unknown(Unknown::decode(&mut data, &Some(extra_data))?),
FileType::Video => RFileDecoded::Video(Video::decode(&mut data, &Some(extra_data))?),
FileType::VMD => RFileDecoded::VMD(Text::decode(&mut data, &Some(extra_data))?),
FileType::WSModel => RFileDecoded::WSModel(Text::decode(&mut data, &Some(extra_data))?),
}
},
};
if !already_decoded && keep_in_cache && return_data {
self.data = RFileInnerData::Decoded(Box::new(decoded.clone()));
} else if !already_decoded && keep_in_cache && !return_data{
self.data = RFileInnerData::Decoded(Box::new(decoded));
return Ok(None)
}
if return_data {
Ok(Some(decoded))
} else {
Ok(None)
}
}
pub fn encode(&mut self, extra_data: &Option<EncodeableExtraData>, move_decoded_to_cache: bool, move_undecoded_to_cache: bool, return_data: bool) -> Result<Option<Vec<u8>>> {
let mut previously_decoded = false;
let mut already_encoded = false;
let mut previously_undecoded = false;
let encoded = match &mut self.data {
RFileInnerData::Decoded(data) => {
previously_decoded = true;
let mut buffer = vec![];
match &mut **data {
RFileDecoded::Anim(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::AnimFragmentBattle(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::AnimPack(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::AnimsTable(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::Atlas(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::Audio(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::BMD(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::BMDVegetation(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::Dat(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::DB(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::ESF(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::Font(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::GroupFormations(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::HlslCompiled(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::Image(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::Loc(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::MatchedCombat(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::Pack(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::PortraitSettings(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::RigidModel(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::SoundBank(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::Text(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::UIC(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::UnitVariant(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::Unknown(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::Video(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::VMD(data) => data.encode(&mut buffer, extra_data)?,
RFileDecoded::WSModel(data) => data.encode(&mut buffer, extra_data)?,
}
buffer
},
RFileInnerData::Cached(data) => {
already_encoded = true;
data.to_vec()
},
RFileInnerData::OnDisk(data) => {
previously_undecoded = true;
data.read()?
},
};
if previously_decoded {
if move_decoded_to_cache {
if return_data {
self.data = RFileInnerData::Cached(encoded.to_vec());
Ok(Some(encoded))
} else {
self.data = RFileInnerData::Cached(encoded);
Ok(None)
}
} else if return_data {
Ok(Some(encoded))
} else {
Ok(None)
}
}
else if previously_undecoded {
if move_undecoded_to_cache {
if return_data {
self.data = RFileInnerData::Cached(encoded.to_vec());
Ok(Some(encoded))
} else {
self.data = RFileInnerData::Cached(encoded);
Ok(None)
}
} else if return_data {
Ok(Some(encoded))
} else {
Ok(None)
}
}
else if already_encoded && return_data {
Ok(Some(encoded))
} else {
Ok(None)
}
}
pub fn load(&mut self) -> Result<()> {
let loaded = match &self.data {
RFileInnerData::Decoded(_) |
RFileInnerData::Cached(_) => return Ok(()),
RFileInnerData::OnDisk(data) => data.read()?,
};
#[cfg(feature = "enable_content_inspector")]
if self.file_type() == FileType::Unknown && !loaded.is_empty() && content_inspector::inspect(&loaded).is_text() {
dbg!(self.path_in_container_raw());
}
self.data = RFileInnerData::Cached(loaded);
Ok(())
}
pub fn timestamp(&self) -> Option<u64> {
self.timestamp
}
pub fn file_type(&self) -> FileType {
self.file_type
}
pub fn file_name(&self) -> Option<&str> {
self.path_in_container_raw().split('/').next_back()
}
pub fn container_name(&self) -> &Option<String> {
&self.container_name
}
pub fn path_in_container(&self) -> ContainerPath {
ContainerPath::File(self.path.to_owned())
}
pub fn path_in_container_raw(&self) -> &str {
&self.path
}
pub fn path_in_container_split(&self) -> Vec<&str> {
self.path.split('/').collect()
}
pub fn db_table_name_from_path(&self) -> Option<&str> {
let split_path = self.path.split('/').collect::<Vec<_>>();
if split_path.len() == 3 && split_path[0].to_lowercase() == "db" {
Some(split_path[1])
} else {
None
}
}
pub fn set_path_in_container_raw(&mut self, path: &str) {
self.path = path.to_owned();
}
pub fn is_compressible(&self, game_info: &GameInfo) -> bool {
!game_info.compression_formats_supported().is_empty() &&
self.file_name() != Some(RESERVED_NAME_DEPENDENCIES_MANAGER_V2) &&
self.file_name() != Some(RESERVED_NAME_DEPENDENCIES_MANAGER) &&
self.file_name() != Some(RESERVED_NAME_SETTINGS) &&
self.file_name() != Some(RESERVED_NAME_NOTES) &&
!matches!(self.file_type, FileType::Audio | FileType::Dat | FileType::RigidModel | FileType::SoundBank | FileType::Video) &&
(
!matches!(self.file_type, FileType::DB | FileType::Loc) || (
game_info.key() != KEY_PHARAOH_DYNASTIES &&
game_info.key() != KEY_PHARAOH &&
game_info.key() != KEY_TROY &&
game_info.key() != KEY_THREE_KINGDOMS &&
game_info.key() != KEY_WARHAMMER_2
)
)
}
pub fn guess_file_type(&mut self) -> Result<()> {
let path = self.path.to_lowercase();
if path.ends_with(pack::EXTENSION) {
self.file_type = FileType::Pack;
}
else if path.ends_with(loc::EXTENSION) {
self.file_type = FileType::Loc;
}
else if path.ends_with(rigidmodel::EXTENSION) {
self.file_type = FileType::RigidModel
}
else if path.ends_with(animpack::EXTENSION) {
self.file_type = FileType::AnimPack
}
else if path.ends_with(anim::EXTENSION) {
self.file_type = FileType::Anim
}
else if path.ends_with(video::EXTENSION) {
self.file_type = FileType::Video;
}
else if path.ends_with(dat::EXTENSION) {
self.file_type = FileType::Dat;
}
else if path.ends_with(font::EXTENSION) {
self.file_type = FileType::Font;
}
else if audio::EXTENSIONS.iter().any(|x| path.ends_with(x)) {
self.file_type = FileType::Audio;
}
else if bmd::EXTENSIONS.iter().any(|x| path.ends_with(x)) {
self.file_type = FileType::BMD;
}
else if bmd_vegetation::EXTENSIONS.iter().any(|x| path.ends_with(x)) {
self.file_type = FileType::BMDVegetation;
}
else if path.ends_with(sound_bank::EXTENSION) {
self.file_type = FileType::SoundBank;
}
else if image::EXTENSIONS.iter().any(|x| path.ends_with(x)) {
self.file_type = FileType::Image;
}
else if cfg!(feature = "support_uic") && path.starts_with(uic::BASE_PATH) && uic::EXTENSIONS.iter().any(|x| path.ends_with(x) || !path.contains('.')) {
self.file_type = FileType::UIC;
}
else if path.ends_with(text::EXTENSION_VMD.0) {
self.file_type = FileType::VMD;
}
else if path.ends_with(text::EXTENSION_WSMODEL.0) {
self.file_type = FileType::WSModel;
}
else if text::EXTENSIONS.iter().any(|(x, _)| path.ends_with(x)) {
self.file_type = FileType::Text;
}
else if path.ends_with(unit_variant::EXTENSION) {
self.file_type = FileType::UnitVariant
}
else if path == group_formations::PATH {
self.file_type = FileType::GroupFormations;
}
else if esf::EXTENSIONS.iter().any(|x| path.ends_with(x)) {
self.file_type = FileType::ESF;
}
else if matched_combat::BASE_PATHS.iter().any(|x| path.starts_with(*x)) && path.ends_with(matched_combat::EXTENSION) {
self.file_type = FileType::MatchedCombat;
}
else if path.starts_with(anims_table::BASE_PATH) && path.ends_with(anims_table::EXTENSION) {
self.file_type = FileType::AnimsTable;
}
else if path.ends_with(anim_fragment_battle::EXTENSION_OLD) || (path.starts_with(anim_fragment_battle::BASE_PATH) && path.contains(anim_fragment_battle::MID_PATH) && path.ends_with(anim_fragment_battle::EXTENSION_NEW)) {
self.file_type = FileType::AnimFragmentBattle;
}
else if path.starts_with("db/") && REGEX_DB.is_match(&path) {
self.file_type = FileType::DB;
}
else if path.ends_with(portrait_settings::EXTENSION) && REGEX_PORTRAIT_SETTINGS.is_match(&path) {
self.file_type = FileType::PortraitSettings;
}
else if path.ends_with(atlas::EXTENSION) {
self.file_type = FileType::Atlas;
}
else if path.ends_with(hlsl_compiled::EXTENSION) {
self.file_type = FileType::HlslCompiled;
}
else {
self.file_type = FileType::Unknown;
}
Ok(())
}
pub fn tsv_import_from_path(path: &Path, schema: &Option<Schema>) -> Result<Self> {
let mut reader = ReaderBuilder::new()
.delimiter(b'\t')
.quoting(false)
.has_headers(true)
.flexible(true)
.from_path(path)?;
let field_order = reader.headers()?
.iter()
.enumerate()
.map(|(x, y)| (x as u32, y.to_owned()))
.collect::<HashMap<u32, String>>();
let mut records = reader.records();
let (table_type, table_version, file_path) = match records.next() {
Some(Ok(record)) => {
let metadata = match record.get(0) {
Some(metadata) => metadata.split(';').map(|x| x.to_owned()).collect::<Vec<String>>(),
None => return Err(RLibError::ImportTSVWrongTypeTable),
};
let table_type = match metadata.first() {
Some(table_type) => {
let mut table_type = table_type.to_owned();
if table_type.starts_with('#') {
table_type.remove(0);
}
table_type
},
None => return Err(RLibError::ImportTSVWrongTypeTable),
};
let table_version = match metadata.get(1) {
Some(table_version) => table_version.parse::<i32>().map_err(|_| RLibError::ImportTSVInvalidVersion)?,
None => return Err(RLibError::ImportTSVInvalidVersion),
};
let file_path = match metadata.get(2) {
Some(file_path) => file_path.replace('\\', "/"),
None => return Err(RLibError::ImportTSVInvalidOrMissingPath),
};
(table_type, table_version, file_path)
}
Some(Err(_)) |
None => return Err(RLibError::ImportTSVIncorrectRow(1, 0)),
};
let decoded = match &*table_type {
loc::TSV_NAME_LOC | loc::TSV_NAME_LOC_OLD => {
let decoded = Loc::tsv_import(records, &field_order)?;
RFileDecoded::Loc(decoded)
}
_ => {
match schema {
Some(schema) => {
let decoded = DB::tsv_import(records, &field_order, schema, &table_type, table_version)?;
RFileDecoded::DB(decoded)
},
None => return Err(RLibError::SchemaNotProvided),
}
}
};
let rfile = RFile::new_from_decoded(&decoded, 0, &file_path);
Ok(rfile)
}
pub fn tsv_export_to_path(&mut self, path: &Path, schema: &Schema, keys_first: bool) -> Result<()> {
let sanitized_path = sanitize_path(path);
let mut folder_path = sanitized_path.to_path_buf();
folder_path.pop();
DirBuilder::new().recursive(true).create(&folder_path)?;
let mut writer = WriterBuilder::new()
.delimiter(b'\t')
.quote_style(QuoteStyle::Never)
.has_headers(false)
.flexible(true)
.from_path(&sanitized_path)?;
let mut extra_data = DecodeableExtraData::default();
extra_data.set_schema(Some(schema));
let extra_data = Some(extra_data);
let file = self.decode(&extra_data, false, true);
if let Err(error) = file {
let _ = std::fs::remove_file(&sanitized_path);
return Err(error);
}
let file = match file?.unwrap() {
RFileDecoded::DB(table) => table.tsv_export(&mut writer, self.path_in_container_raw(), keys_first),
RFileDecoded::Loc(table) => table.tsv_export(&mut writer, self.path_in_container_raw()),
_ => unimplemented!()
};
if file.is_err() {
let _ = std::fs::remove_file(&sanitized_path);
}
file
}
pub fn merge(sources: &[&Self], path: &str) -> Result<Self> {
if sources.len() <= 1 {
return Err(RLibError::RFileMergeOnlyOneFileProvided);
}
let mut file_types = sources.iter().map(|file| file.file_type()).collect::<Vec<_>>();
file_types.sort();
file_types.dedup();
if file_types.len() > 1 {
return Err(RLibError::RFileMergeDifferentTypes);
}
match file_types[0] {
FileType::DB => {
let files = sources.iter().filter_map(|file| if let Ok(RFileDecoded::DB(table)) = file.decoded() { Some(table) } else { None }).collect::<Vec<_>>();
let data = RFileDecoded::DB(DB::merge(&files)?);
Ok(Self::new_from_decoded(&data, current_time()?, path))
},
FileType::Loc => {
let files = sources.iter().filter_map(|file| if let Ok(RFileDecoded::Loc(table)) = file.decoded() { Some(table) } else { None }).collect::<Vec<_>>();
let data = RFileDecoded::Loc(Loc::merge(&files)?);
Ok(Self::new_from_decoded(&data, current_time()?, path))
},
_ => Err(RLibError::RFileMergeNotSupportedForType(file_types[0].to_string())),
}
}
pub fn update(&mut self, definition: &Option<Definition>) -> Result<()> {
match self.decoded_mut() {
Ok(RFileDecoded::DB(file)) => match definition {
Some(definition) => file.update(definition),
None => return Err(RLibError::RawTableMissingDefinition),
}
_ => return Err(RLibError::FileNotDecoded(self.path_in_container_raw().to_string())),
}
Ok(())
}
pub fn data_hash(&mut self, extra_data: &Option<EncodeableExtraData>) -> Result<u64> {
Ok(match self.data {
RFileInnerData::Decoded(_) => checksum(CrcAlgorithm::Crc32Iscsi, &self.encode(extra_data, false, false, true)?.unwrap()),
RFileInnerData::Cached(ref data) => checksum(CrcAlgorithm::Crc32Iscsi, data),
RFileInnerData::OnDisk(ref on_disk) => checksum(CrcAlgorithm::Crc32Iscsi, &on_disk.read()?),
})
}
pub fn sanitize_and_create_file(&mut self, destination_path: &Path, extra_data: &Option<EncodeableExtraData>) -> Result<PathBuf> {
let sanitized_destination_path = sanitize_path(&destination_path);
if sanitized_destination_path != destination_path {
#[cfg(feature = "integration_log")] {
warn!("Filename sanitized from '{}' to '{}' due to invalid Windows characters",
destination_path.to_owned().file_name().unwrap_or_default().to_string_lossy(),
sanitized_destination_path.file_name().unwrap_or_default().to_string_lossy());
}
}
let mut file = BufWriter::new(File::create(&sanitized_destination_path)?);
let data = self.encode(extra_data, false, false, true)?.unwrap();
file.write_all(&data)?;
Ok(sanitized_destination_path)
}
}
impl OnDisk {
fn read(&self) -> Result<Vec<u8>> {
let mut file = BufReader::new(File::open(&self.path)?);
let timestamp = last_modified_time_from_file(file.get_ref())?;
if timestamp != self.timestamp {
return Err(RLibError::FileSourceChanged(self.path.clone()));
}
let mut data = vec![0; self.size as usize];
file.seek(SeekFrom::Start(self.start))?;
file.read_exact(&mut data)?;
if self.is_encrypted.is_some() {
data = Cursor::new(data).decrypt()?;
}
if self.is_compressed {
data = data.as_slice().decompress()?;
}
Ok(data)
}
}
impl ContainerPath {
pub fn is_file(&self) -> bool {
matches!(self, ContainerPath::File(_))
}
pub fn is_folder(&self) -> bool {
matches!(self, ContainerPath::Folder(_))
}
pub fn is_pack(&self) -> bool {
match self {
ContainerPath::Folder(path) => path.is_empty(),
_ => false,
}
}
pub fn path_raw(&self) -> &str {
match self {
Self::File(ref path) => path,
Self::Folder(ref path) => path,
}
}
pub fn name(&self) -> Option<&str> {
self.path_raw().split('/').next_back()
}
pub fn db_table_name_from_path(&self) -> Option<&str> {
let split_path = self.path_raw().split('/').collect::<Vec<_>>();
if split_path.len() == 3 && split_path[0].to_lowercase() == "db" {
Some(split_path[1])
} else {
None
}
}
pub fn parent_path(&self) -> String {
match self {
ContainerPath::File(path) |
ContainerPath::Folder(path) => {
if path.is_empty() || (path.chars().count() == 1 && path.starts_with('/')) {
path.to_owned()
} else {
let mut path_split = path.split('/').collect::<Vec<_>>();
path_split.pop();
path_split.join("/")
}
},
}
}
pub fn dedup(paths: &[Self]) -> Vec<Self> {
let root = ContainerPath::Folder("".to_string());
if paths.contains(&root) {
return vec![root; 1];
}
if !paths.par_iter().any(|item| matches!(item, ContainerPath::Folder(_))) {
let mut paths = paths.to_vec();
paths.sort();
paths.dedup();
return paths;
}
let items_to_remove = paths.par_iter().filter(|item_type_to_add| {
match item_type_to_add {
ContainerPath::File(ref path_to_add) => {
!paths.par_iter()
.any(|item_type| {
item_type.is_folder() && path_to_add.starts_with(item_type.path_raw())
})
}
ContainerPath::Folder(ref path_to_add) => {
!paths.par_iter()
.any(|item_type| {
let path = item_type.path_raw();
item_type.is_folder() && path_to_add.starts_with(path) && path_to_add.len() > path.len() && path_to_add.is_char_boundary(path.len()) && path_to_add.as_bytes()[path.len()] == b'/'
})
}
}
}).cloned().collect::<Vec<Self>>();
let mut paths = paths.to_vec();
paths.retain(|x| items_to_remove.contains(x));
paths.sort();
paths.dedup();
paths
}
}
impl Ord for ContainerPath {
fn cmp(&self, other: &Self) -> Ordering {
match self {
ContainerPath::File(a) => match other {
ContainerPath::File(b) => a.cmp(b),
ContainerPath::Folder(_) => Ordering::Less,
}
ContainerPath::Folder(a) => match other {
ContainerPath::File(_) => Ordering::Greater,
ContainerPath::Folder(b) => a.cmp(b),
}
}
}
}
impl PartialOrd for ContainerPath {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Display for FileType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
FileType::Anim => write!(f, "Anim"),
FileType::AnimFragmentBattle => write!(f, "AnimFragmentBattle"),
FileType::AnimPack => write!(f, "AnimPack"),
FileType::AnimsTable => write!(f, "AnimsTable"),
FileType::Atlas => write!(f, "Atlas"),
FileType::Audio => write!(f, "Audio"),
FileType::BMD => write!(f, "Battle Map Definition"),
FileType::BMDVegetation => write!(f, "Battle Map Definition (Vegetation)"),
FileType::Dat => write!(f, "Dat Audio File"),
FileType::DB => write!(f, "DB Table"),
FileType::ESF => write!(f, "ESF"),
FileType::Font => write!(f, "Font"),
FileType::HlslCompiled => write!(f, "Hlsl Compiled"),
FileType::GroupFormations => write!(f, "Group Formations"),
FileType::Image => write!(f, "Image"),
FileType::Loc => write!(f, "Loc Table"),
FileType::MatchedCombat => write!(f, "Matched Combat"),
FileType::Pack => write!(f, "PackFile"),
FileType::PortraitSettings => write!(f, "Portrait Settings"),
FileType::RigidModel => write!(f, "RigidModel"),
FileType::SoundBank => write!(f, "SoundBank"),
FileType::Text => write!(f, "Text"),
FileType::UIC => write!(f, "UI Component"),
FileType::UnitVariant => write!(f, "Unit Variant"),
FileType::Unknown => write!(f, "Unknown"),
FileType::Video => write!(f, "Video"),
FileType::VMD => write!(f, "VMD"),
FileType::WSModel => write!(f, "WSModel"),
}
}
}
impl From<&str> for FileType {
fn from(value: &str) -> Self {
match value {
"Anim" => FileType::Anim,
"AnimFragmentBattle" => FileType::AnimFragmentBattle,
"AnimPack" => FileType::AnimPack,
"AnimsTable" => FileType::AnimsTable,
"Atlas" => FileType::Atlas,
"Audio" => FileType::Audio,
"BMD" => FileType::BMD,
"BMDVegetation" => FileType::BMDVegetation,
"Dat" => FileType::Dat,
"DB" => FileType::DB,
"ESF" => FileType::ESF,
"Font" => FileType::Font,
"HlslCompiled" => FileType::HlslCompiled,
"GroupFormations" => FileType::GroupFormations,
"Image" => FileType::Image,
"Loc" => FileType::Loc,
"MatchedCombat" => FileType::MatchedCombat,
"Pack" => FileType::Pack,
"PortraitSettings" => FileType::PortraitSettings,
"RigidModel" => FileType::RigidModel,
"SoundBank" => FileType::SoundBank,
"Text" => FileType::Text,
"UIC" => FileType::UIC,
"UnitVariant" => FileType::UnitVariant,
"Unknown" => FileType::Unknown,
"Video" => FileType::Video,
"VMD" => FileType::VMD,
"WSModel" => FileType::WSModel,
_ => unimplemented!(),
}
}
}
impl From<FileType> for String {
fn from(value: FileType) -> String {
match value {
FileType::Anim => "Anim",
FileType::AnimFragmentBattle => "AnimFragmentBattle",
FileType::AnimPack => "AnimPack",
FileType::AnimsTable => "AnimsTable",
FileType::Atlas => "Atlas",
FileType::Audio => "Audio",
FileType::BMD => "BMD",
FileType::BMDVegetation => "BMD Vegetation",
FileType::Dat => "Dat",
FileType::DB => "DB",
FileType::ESF => "ESF",
FileType::Font => "Font",
FileType::HlslCompiled => "HlslCompiled",
FileType::GroupFormations => "GroupFormations",
FileType::Image => "Image",
FileType::Loc => "Loc",
FileType::MatchedCombat => "MatchedCombat",
FileType::Pack => "Pack",
FileType::PortraitSettings => "PortraitSettings",
FileType::RigidModel => "RigidModel",
FileType::SoundBank => "SoundBank",
FileType::Text => "Text",
FileType::UIC => "UIC",
FileType::UnitVariant => "UnitVariant",
FileType::Unknown => "Unknown",
FileType::Video => "Video",
FileType::VMD => "VMD",
FileType::WSModel => "WSModel",
}.to_owned()
}
}
impl From<&RFileDecoded> for FileType {
fn from(file: &RFileDecoded) -> Self {
match file {
RFileDecoded::Anim(_) => Self::Anim,
RFileDecoded::AnimFragmentBattle(_) => Self::AnimFragmentBattle,
RFileDecoded::AnimPack(_) => Self::AnimPack,
RFileDecoded::AnimsTable(_) => Self::AnimsTable,
RFileDecoded::Atlas(_) => Self::Atlas,
RFileDecoded::Audio(_) => Self::Audio,
RFileDecoded::BMD(_) => Self::BMD,
RFileDecoded::BMDVegetation(_) => Self::BMDVegetation,
RFileDecoded::Dat(_) => Self::Dat,
RFileDecoded::DB(_) => Self::DB,
RFileDecoded::ESF(_) => Self::ESF,
RFileDecoded::Font(_) => Self::Font,
RFileDecoded::HlslCompiled(_) => Self::HlslCompiled,
RFileDecoded::GroupFormations(_) => Self::GroupFormations,
RFileDecoded::Image(_) => Self::Image,
RFileDecoded::Loc(_) => Self::Loc,
RFileDecoded::MatchedCombat(_) => Self::MatchedCombat,
RFileDecoded::Pack(_) => Self::Pack,
RFileDecoded::PortraitSettings(_) => Self::PortraitSettings,
RFileDecoded::RigidModel(_) => Self::RigidModel,
RFileDecoded::SoundBank(_) => Self::SoundBank,
RFileDecoded::Text(_) => Self::Text,
RFileDecoded::UIC(_) => Self::UIC,
RFileDecoded::UnitVariant(_) => Self::UnitVariant,
RFileDecoded::Unknown(_) => Self::Unknown,
RFileDecoded::Video(_) => Self::Video,
RFileDecoded::VMD(_) => Self::VMD,
RFileDecoded::WSModel(_) => Self::WSModel,
}
}
}
impl<'a> EncodeableExtraData<'a> {
pub fn new_from_game_info(game_info: &'a GameInfo) -> Self {
let mut extra_data = Self::default();
extra_data.set_game_info(Some(game_info));
extra_data.set_table_has_guid(*game_info.db_tables_have_guid());
extra_data
}
}