Skip to content

Commit

Permalink
Merge pull request #59 from SARDONYX-sard/feature/support-actorbase-path
Browse files Browse the repository at this point in the history
feat(core): support `ActorBase` DAR path format
  • Loading branch information
SARDONYX-sard authored Apr 4, 2024
2 parents bf9d448 + c6541ff commit 204fe20
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 37 deletions.
6 changes: 6 additions & 0 deletions dar2oar_core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
59 changes: 56 additions & 3 deletions dar2oar_core/src/fs/converter/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ where
actor_name,
priority,
remain_dir,
esp_dir,
base_id,
..
} = parsed_path;

Expand Down Expand Up @@ -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(&section_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(&section_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);
Expand Down
3 changes: 2 additions & 1 deletion dar2oar_core/src/fs/converter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
160 changes: 127 additions & 33 deletions dar2oar_core/src/fs/path_changer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`/\<priority\>/_conditions.txt"
//!
//! ### DAR `ActorBase` path format:
//! - Common + `DynamicAnimationReplacer/<esp name>/<actor base id>/<animation dirs and files>`
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,
Expand All @@ -31,6 +34,33 @@ pub struct ParsedPath {
pub priority: Result<i32, String>,
/// `male`, `female`, others dir
pub remain_dir: Option<PathBuf>,

/// Appears only in the actor_base directory format.
///
/// # Examples.
/// `Skyrim.esm`.
pub esp_dir: Option<String>,
/// Appears only in the actor_base directory format.
///
/// # Examples.
/// `0001A692`.
pub base_id: Option<String>,
}

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.
Expand All @@ -46,19 +76,41 @@ pub fn parse_dar_path(path: impl AsRef<Path>) -> Result<ParsedPath> {
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)?;

Expand All @@ -80,26 +132,35 @@ pub fn parse_dar_path(path: impl AsRef<Path>) -> Result<ParsedPath> {
.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::<i32>().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,
Expand All @@ -109,6 +170,8 @@ pub fn parse_dar_path(path: impl AsRef<Path>) -> Result<ParsedPath> {
actor_name,
priority,
remain_dir,
esp_dir,
base_id,
})
}

Expand All @@ -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);

Expand All @@ -132,6 +195,7 @@ mod test {
actor_name,
priority,
remain_dir,
..
} = result?;

assert_eq!(
Expand All @@ -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);

Expand All @@ -168,6 +232,7 @@ mod test {
actor_name,
priority,
remain_dir,
..
} = result?;

assert_eq!(
Expand All @@ -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());
}
}

0 comments on commit 204fe20

Please sign in to comment.