From c6541ff867b75ab2d645b1755394f33031daa9d7 Mon Sep 17 00:00:00 2001 From: SARDONYX-sard <68905624+SARDONYX-sard@users.noreply.github.com> Date: Fri, 5 Apr 2024 00:24:41 +0900 Subject: [PATCH] feat(core): support `ActorBase` DAR path format --- dar2oar_core/src/error.rs | 6 + dar2oar_core/src/fs/converter/common.rs | 59 ++++++++- dar2oar_core/src/fs/converter/mod.rs | 3 +- dar2oar_core/src/fs/path_changer.rs | 160 +++++++++++++++++++----- 4 files changed, 191 insertions(+), 37 deletions(-) diff --git a/dar2oar_core/src/error.rs b/dar2oar_core/src/error.rs index 2b3f2e8..b564b94 100644 --- a/dar2oar_core/src/error.rs +++ b/dar2oar_core/src/error.rs @@ -6,6 +6,12 @@ /// such as error messages or other relevant information. #[derive(Debug, thiserror::Error)] pub enum ConvertError { + /// Failed to write section config target. + #[error( + "Path was interpreted as the path to ActorBase, but the ID directory is missing. expected: [..]/DynamicAnimationReplacer/{{ESP name}}/{{ID Number}}, actual: {0}" + )] + MissingBaseId(String), + /// Failed to write section config target. #[error("Failed to write section config target: {0}")] FailedWriteSectionConfig(String), diff --git a/dar2oar_core/src/fs/converter/common.rs b/dar2oar_core/src/fs/converter/common.rs index a70844b..96507b2 100644 --- a/dar2oar_core/src/fs/converter/common.rs +++ b/dar2oar_core/src/fs/converter/common.rs @@ -39,6 +39,8 @@ where actor_name, priority, remain_dir, + esp_dir, + base_id, .. } = parsed_path; @@ -95,21 +97,72 @@ where match priority { Ok(priority) => { + // There are two types of pass patterns in the DAR. Process them here. + let priority_str = priority.to_string(); + // Examples + // ActorBase: 0001A692 / Condition pattern: 00111000 + let base_id_or_priority_str = base_id.as_ref().unwrap_or(&priority_str); + + //? # Why do you use base_id instead of priority for section names in ActorBase format? + // In ActorBase format, priority is always 0. If priority is used as it is, it will be indistinguishable + // from motions for actors with other IDs. To prevent this, use base_id for the section name in ActorBase format. let section_name = match is_1st_person { true => section_1person_table .as_ref() - .and_then(|table| table.get(priority_str.as_str())), + .and_then(|table| table.get(base_id_or_priority_str.as_str())), false => section_table .as_ref() - .and_then(|table| table.get(priority_str.as_str())), + .and_then(|table| table.get(base_id_or_priority_str.as_str())), } - .unwrap_or(&priority_str); + .unwrap_or(base_id_or_priority_str); // e.g. mesh/[..]/OpenAnimationReplacer/ModName/SectionName/ let section_root = oar_name_space.join(section_name); fs::create_dir_all(§ion_root).await?; + // - This block is ActorBase pattern + if esp_dir.is_some() { + tracing::debug!("This path is ActorBase: {path:?}"); + + let esp_dir = esp_dir + .as_ref() + .ok_or(ConvertError::MissingBaseId(path.display().to_string()))?; + let base_id = base_id + .as_ref() + .ok_or(ConvertError::MissingBaseId(path.display().to_string()))?; + + let content = format!("IsActorBase ( \"{esp_dir}\" | 0x{base_id} )"); + tracing::debug!( + "DAR syntax content auto-generated for ActorBase paths:\n{content}" + ); + + let config_json = ConditionsConfig { + name: section_name.into(), + priority: *priority, + conditions: parse_dar2oar(&content)?, + ..Default::default() + }; + + if !section_root.join("config.json").exists() { + write_section_config(§ion_root, config_json).await?; + } + + // # Ordering validity: + // Use `AcqRel` to `happened before relationship`(form a memory read/write order between threads) of cas(compare_and_swap), + // so that other threads read after writing true to memory. + // - In case of cas failure, use `Relaxed` because the order is unimportant. + let _ = is_converted_once.compare_exchange(false, true, AcqRel, Relaxed); + + // NOTE: If you call the function only once with this is_converted_once flag, + // the 1st_person&3person conversion will not work! + write_name_space_config(&oar_name_space, &parsed_mod_name, author.as_deref()) + .await?; + }; + + // - This block is Condition pattern + // If `_condition.txt` exists in the ActorBase path, the `_condition.txt` file will be overwritten by `_config.json`, + // but this problem is not considered in ActorBase because `_condition.txt` should not exist. if file_name == "_conditions.txt" { let content = fs::read_to_string(path).await?; tracing::debug!("{path:?} Content:\n{}", content); diff --git a/dar2oar_core/src/fs/converter/mod.rs b/dar2oar_core/src/fs/converter/mod.rs index c5f3ed5..2b2cc40 100644 --- a/dar2oar_core/src/fs/converter/mod.rs +++ b/dar2oar_core/src/fs/converter/mod.rs @@ -112,7 +112,8 @@ mod test { use anyhow::Result; // const DAR_DIR: &str = "../test/data/UNDERDOG - Animations"; - const DAR_DIR: &str = "../test/data/Delia"; + // const DAR_DIR: &str = "../test/data/Delia"; + const DAR_DIR: &str = "../test/data/Axarien's Animations - The Companions (DAR)"; // const OAR_DIR: &str = // "../test/data/Delia/meshes/actors/character/animations\\OpenAnimationReplacer"; // const TABLE_PATH: &str = "../test/mapping_tables/UnderDog Animations_v1.9.6_mapping_table.txt"; diff --git a/dar2oar_core/src/fs/path_changer.rs b/dar2oar_core/src/fs/path_changer.rs index 2e49682..574ae9c 100644 --- a/dar2oar_core/src/fs/path_changer.rs +++ b/dar2oar_core/src/fs/path_changer.rs @@ -6,15 +6,18 @@ //! ### OAR //! - Common + "`OptionAnimationReplacer`/\<`NameSpace`\>/\<`EachSectionName`\>" //! -//! ### DAR: (Only priority order assignment is taken into account. In other words, actor-based allocation is not considered.) +//! ### DAR Condition path format: (Only priority order assignment is taken into account. In other words, actor-based allocation is not considered.) //! - Common + "`DynamicAnimationReplacer`/`_CustomConditions`/\/_conditions.txt" +//! +//! ### DAR `ActorBase` path format: +//! - Common + `DynamicAnimationReplacer///` use crate::error::{ConvertError, Result}; use std::ffi::OsStr; use std::path::{Path, PathBuf}; /// The information necessary for the conversion -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ParsedPath { /// `"ModName/meshes/actors/character/animations/DynamicAnimationReplacer"` pub dar_root: PathBuf, @@ -31,6 +34,33 @@ pub struct ParsedPath { pub priority: Result, /// `male`, `female`, others dir pub remain_dir: Option, + + /// Appears only in the actor_base directory format. + /// + /// # Examples. + /// `Skyrim.esm`. + pub esp_dir: Option, + /// Appears only in the actor_base directory format. + /// + /// # Examples. + /// `0001A692`. + pub base_id: Option, +} + +impl Default for ParsedPath { + fn default() -> Self { + Self { + dar_root: Default::default(), + oar_root: Default::default(), + is_1st_person: Default::default(), + mod_name: Default::default(), + actor_name: Default::default(), + priority: Err("".into()), + remain_dir: Default::default(), + esp_dir: Default::default(), + base_id: Default::default(), + } + } } /// Parses the DAR path and returns the information necessary for conversion. @@ -46,19 +76,41 @@ pub fn parse_dar_path(path: impl AsRef) -> Result { let path = path.as_ref(); let paths: Vec<&OsStr> = path.iter().collect(); + let dar_pos = path + .iter() + .position(|os_str| os_str.eq_ignore_ascii_case(OsStr::new("DynamicAnimationReplacer"))) + .ok_or(ConvertError::NotFoundDarDir)?; + + // ActorBase pattern only + let esp_dir = paths.get(dar_pos + 1).and_then(|name| { + let lower_name = name.to_str()?.to_lowercase(); + if lower_name.ends_with(".esm") + || lower_name.ends_with(".esp") + || lower_name.ends_with(".esl") + { + Some(name.to_str()?.to_string()) + } else { + None + } + }); + + let base_id = esp_dir.as_ref().and_then(|_| { + paths + .get(dar_pos + 2) + .and_then(|base_id| Some(base_id.to_str()?.to_string())) + }); + let is_1st_person = path.iter().any(|os_str| os_str == OsStr::new("_1stperson")); - let (dar_root, oar_root) = path - .iter() - .position(|os_str| os_str == OsStr::new("DynamicAnimationReplacer")) - .and_then(|idx| { - paths.get(0..idx).map(|str_paths| { - let mut dar = Path::new(&str_paths.join(OsStr::new("/"))).to_path_buf(); - let mut oar = dar.clone(); - dar.push("DynamicAnimationReplacer"); - oar.push("OpenAnimationReplacer"); - (dar, oar) - }) + // Condition pattern + let (dar_root, oar_root) = paths + .get(0..dar_pos) + .map(|str_paths| { + let mut dar = Path::new(&str_paths.join(OsStr::new("/"))).to_path_buf(); + let mut oar = dar.clone(); + dar.push("DynamicAnimationReplacer"); + oar.push("OpenAnimationReplacer"); + (dar, oar) }) .ok_or(ConvertError::NotFoundDarDir)?; @@ -80,26 +132,35 @@ pub fn parse_dar_path(path: impl AsRef) -> Result { .and_then(|name| name.to_str().map(str::to_owned)) }); - let priority = path - .iter() - .position(|os_str| os_str == OsStr::new("_CustomConditions")) - .and_then(|idx| paths.get(idx + 1).and_then(|priority| priority.to_str())) - .ok_or(ConvertError::NotFoundPriorityDir)?; + let priority = if esp_dir.is_some() { + "0" + } else { + path.iter() + .position(|os_str| os_str == OsStr::new("_CustomConditions")) + .and_then(|idx| paths.get(idx + 1).and_then(|priority| priority.to_str())) + .ok_or(ConvertError::NotFoundPriorityDir)? + }; let priority = priority.parse::().map_err(|_err| priority.into()); - let remain_dir = path - .iter() - .position(|os_str| os_str == OsStr::new("_CustomConditions")) - .and_then(|idx| { - paths.get(idx + 2..paths.len() - 1).and_then(|str_paths| { - let string = str_paths.join(OsStr::new("/")); - match string.is_empty() { - true => None, - false => Some(PathBuf::from(string)), - } - }) - }); + let before_remain_pos = match esp_dir { + Some(_) => Some(dar_pos + 3), // e.g. DynamicAnimationReplacer/Skyrim.esm/00AC/male/ + None => path + .iter() + .position(|os_str| os_str == OsStr::new("_CustomConditions")) + .map(|idx| idx + 2), // e.g. DynamicAnimationReplacer/_CustomConditions/8107000/InnerDir/_conditions.txt" + }; + + // male, female, etc dir + let remain_dir = before_remain_pos.and_then(|idx| { + paths.get(idx..paths.len() - 1).and_then(|str_paths| { + let string = str_paths.join(OsStr::new("/")); + match string.is_empty() { + true => None, + false => Some(PathBuf::from(string)), + } + }) + }); Ok(ParsedPath { dar_root, @@ -109,6 +170,8 @@ pub fn parse_dar_path(path: impl AsRef) -> Result { actor_name, priority, remain_dir, + esp_dir, + base_id, }) } @@ -119,7 +182,7 @@ mod test { use pretty_assertions::assert_eq; #[test] - fn test_parse_dar_path_1st_person() -> Result<()> { + fn should_parse_dar_path_1st_person() -> Result<()> { let path = Path::new("../ModName/Meshes/actors/character/_1stperson/animations/DynamicAnimationReplacer/_CustomConditions/8107000/_conditions.txt"); let result = parse_dar_path(path); @@ -132,6 +195,7 @@ mod test { actor_name, priority, remain_dir, + .. } = result?; assert_eq!( @@ -155,7 +219,7 @@ mod test { } #[test] - fn test_parse_dar_path_3rd_person() -> Result<()> { + fn should_parse_dar_path_3rd_person() -> Result<()> { let path = Path::new("../ModName/meshes/actors/falmer/animations/DynamicAnimationReplacer/_CustomConditions/8107000/InnerDir/_conditions.txt"); let result = parse_dar_path(path); @@ -168,6 +232,7 @@ mod test { actor_name, priority, remain_dir, + .. } = result?; assert_eq!( @@ -187,7 +252,36 @@ mod test { } #[test] - fn test_parse_dar_path_invalid_utf8() { + fn should_error_invalid_utf8() { assert!(parse_dar_path("invalid_path").is_err()); } + + #[test] + fn should_parse_actor_base_path() -> Result<()> { + let path = Path::new("../ModName/meshes/actors/character/animations/DynamicAnimationReplacer/Mod.esp/00123456/male/1hm.hkx"); + let result = parse_dar_path(path); + let parsed_path = result?; + + assert_eq!( + parsed_path.dar_root, + PathBuf::from("../ModName/meshes/actors/character/animations/DynamicAnimationReplacer") + ); + assert_eq!(parsed_path.esp_dir, Some("Mod.esp".into())); + assert_eq!(parsed_path.base_id, Some("00123456".into())); + assert_eq!(parsed_path.remain_dir, Some("male".into())); + Ok(()) + } + + #[test] + fn should_error_invalid_actor_base_path() { + // Missing DynamicAnimationReplacer + let path1 = Path::new("../ModName/meshes/actors/character/animations/Mod.esp/00123456/"); + assert!(parse_dar_path(path1).is_err()); + + // Invalid ESP name + let path2 = Path::new( + "../ModName/meshes/actors/character/animations/DynamicAnimationReplacer/00123456/", + ); + assert!(parse_dar_path(path2).is_err()); + } }