diff --git a/dar2oar_cli/src/main.rs b/dar2oar_cli/src/main.rs index 9e13a74..c03d96d 100644 --- a/dar2oar_cli/src/main.rs +++ b/dar2oar_cli/src/main.rs @@ -54,8 +54,8 @@ async fn main() -> anyhow::Result<()> { let config = ConvertOptions { dar_dir: args.src, oar_dir: args.dist, - mod_name: args.name.as_deref(), - author: args.author.as_deref(), + mod_name: args.name, + author: args.author, section_table: get_mapping_table(args.mapping_file).await, section_1person_table: get_mapping_table(args.mapping_1person_file).await, run_parallel: args.run_parallel, diff --git a/dar2oar_core/benches/convert_n_thread.rs b/dar2oar_core/benches/convert_n_thread.rs index c1c8c0f..35ec953 100644 --- a/dar2oar_core/benches/convert_n_thread.rs +++ b/dar2oar_core/benches/convert_n_thread.rs @@ -25,7 +25,7 @@ fn criterion_benchmark(c: &mut Criterion) { parallel::convert_dar_to_oar( black_box(ConvertOptions { - dar_dir: TARGET, + dar_dir: TARGET.into(), section_table: Some(mapping), ..Default::default() }), @@ -45,7 +45,7 @@ fn criterion_benchmark(c: &mut Criterion) { sequential::convert_dar_to_oar( black_box(ConvertOptions { - dar_dir: TARGET, + dar_dir: TARGET.into(), section_table: Some(mapping), ..Default::default() }), diff --git a/dar2oar_core/src/error.rs b/dar2oar_core/src/error.rs index a2254c9..2b18260 100644 --- a/dar2oar_core/src/error.rs +++ b/dar2oar_core/src/error.rs @@ -10,6 +10,8 @@ pub enum ConvertError { NotFoundOarDir, #[error("Not found \"DynamicAnimationReplacer\" directory")] NotFoundDarDir, + #[error("Never converted.")] + NeverConverted, #[error("Not found file name")] NotFoundFileName, #[error("This is not valid utf8")] @@ -32,6 +34,9 @@ pub enum ConvertError { /// Represents all other cases of `std::io::Error`. #[error(transparent)] IOError(#[from] std::io::Error), + /// Thread join error. + #[error(transparent)] + JoinError(#[from] tokio::task::JoinError), } //? Implemented to facilitate testing with the `assert_eq!` macro. diff --git a/dar2oar_core/src/fs/converter/common.rs b/dar2oar_core/src/fs/converter/common.rs index 351a498..0cc6aee 100644 --- a/dar2oar_core/src/fs/converter/common.rs +++ b/dar2oar_core/src/fs/converter/common.rs @@ -5,16 +5,17 @@ use crate::fs::converter::{ConvertOptions, ConvertedReport}; use crate::fs::path_changer::ParsedPath; use crate::fs::section_writer::{read_file, write_name_space_config, write_section_config}; use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; use tokio::fs; /// Common parts of multi-threaded and single-threaded loop processing. /// # Performance /// - Since dir is created when a file is discovered, performance is improved if path.is_dir() is not put in path. pub async fn convert_inner( - options: &ConvertOptions<'_, impl AsRef>, + options: &ConvertOptions, path: &Path, - parsed_path: ParsedPath, - is_converted_once: &mut bool, + parsed_path: &ParsedPath, + is_converted_once: &AtomicBool, ) -> Result<()> { let ConvertOptions { dar_dir: _, @@ -29,20 +30,19 @@ pub async fn convert_inner( let is_1st_person = parsed_path.is_1st_person; let parsed_mod_name = mod_name - .map(|s| s.to_string()) + .clone() .unwrap_or(parsed_path.mod_name.clone().unwrap_or("Unknown".into())); let oar_name_space_path = oar_dir .as_ref() .map(|path| match is_1st_person { - true => path - .as_ref() + true => Path::new(path) .join("meshes/actors/character/_1stperson/animations/OpenAnimationReplacer"), - false => path - .as_ref() - .join("meshes/actors/character/animations/OpenAnimationReplacer"), + false => { + Path::new(path).join("meshes/actors/character/animations/OpenAnimationReplacer") + } }) .unwrap_or(parsed_path.oar_root.clone()) - .join(mod_name.unwrap_or(&parsed_mod_name)); + .join(&parsed_mod_name); if path.extension().is_some() { tracing::debug!("File: {:?}", path); @@ -79,9 +79,12 @@ pub async fn convert_inner( }; write_section_config(section_root, config_json).await?; - if !*is_converted_once { - write_name_space_config(&oar_name_space_path, &parsed_mod_name, *author).await?; - *is_converted_once = true; + if is_converted_once + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed) + .is_ok() + { + write_name_space_config(&oar_name_space_path, &parsed_mod_name, author.as_deref()) + .await?; } } else { // maybe motion files(.hkx), gender dir diff --git a/dar2oar_core/src/fs/converter/mod.rs b/dar2oar_core/src/fs/converter/mod.rs index 005e3fc..62594ee 100644 --- a/dar2oar_core/src/fs/converter/mod.rs +++ b/dar2oar_core/src/fs/converter/mod.rs @@ -6,7 +6,6 @@ pub mod support_cmd; use crate::error::Result; use std::collections::HashMap; -use std::path::Path; /// # Convert DAR to OAR /// @@ -75,7 +74,7 @@ use std::path::Path; /// } /// ``` pub async fn convert_dar_to_oar( - options: ConvertOptions<'_, impl AsRef>, + options: ConvertOptions, progress_fn: impl FnMut(usize), ) -> Result { match options.run_parallel { @@ -90,15 +89,15 @@ impl Closure { } #[derive(Debug, Clone, Default)] -pub struct ConvertOptions<'a, P: AsRef> { +pub struct ConvertOptions { /// DAR source dir path - pub dar_dir: P, + pub dar_dir: String, /// OAR destination dir path(If not, it is inferred from src) - pub oar_dir: Option

, + pub oar_dir: Option, /// mod name in config.json & directory name(If not, it is inferred from src) - pub mod_name: Option<&'a str>, + pub mod_name: Option, /// mod author in config.json - pub author: Option<&'a str>, + pub author: Option, /// path to section name table pub section_table: Option>, /// path to section name table(For _1st_person) @@ -151,12 +150,12 @@ mod test { }; } - async fn create_options<'a>() -> Result> { + async fn create_options() -> Result { Ok(ConvertOptions { - dar_dir: DAR_DIR, + dar_dir: DAR_DIR.into(), // cannot use include_str! section_table: Some(crate::read_mapping_table(TABLE_PATH).await?), - // run_parallel: true, + run_parallel: true, ..Default::default() }) } diff --git a/dar2oar_core/src/fs/converter/parallel.rs b/dar2oar_core/src/fs/converter/parallel.rs index e0dddcd..e66a348 100644 --- a/dar2oar_core/src/fs/converter/parallel.rs +++ b/dar2oar_core/src/fs/converter/parallel.rs @@ -4,6 +4,8 @@ use crate::fs::converter::{ConvertOptions, ConvertedReport}; use crate::fs::path_changer::parse_dar_path; use jwalk::WalkDirGeneric; use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; /// Multi thread converter /// @@ -18,10 +20,10 @@ use std::path::Path; /// For library reasons, you get the number of DAR dirs and files, not the number of DAR files only /// (i.e., the count is different from the Sequential version) pub async fn convert_dar_to_oar( - options: ConvertOptions<'_, impl AsRef>, + options: ConvertOptions, mut progress_fn: impl FnMut(usize), ) -> Result { - let dar_dir = options.dar_dir.as_ref(); + let dar_dir = options.dar_dir.as_str(); let walk_len = get_dar_files(dar_dir).into_iter().count(); tracing::debug!("Parallel Converter/DAR dir & file counts: {}", walk_len); @@ -29,32 +31,56 @@ pub async fn convert_dar_to_oar( let entires = get_dar_files(dar_dir).into_iter(); let hide_dar = options.hide_dar; + let options = Arc::new(options); + let mut dar_1st_namespace = None; // To need rename to hidden(For _1stperson) let mut dar_namespace = None; // To need rename to hidden - let mut is_converted_once = false; + let is_converted_once = Arc::new(AtomicBool::new(false)); + let mut task_handles: Vec>> = Vec::new(); for (idx, entry) in entires.enumerate() { let path = entry.map_err(|_| ConvertError::NotFoundEntry)?.path(); - let path = path.as_path(); - let parsed_path = match parse_dar_path(path, None) { + let parsed_path = Arc::new(match parse_dar_path(&path, None) { Ok(p) => p, Err(_) => continue, - }; - tracing::debug!("[Start {}th conversion]\n{:?}", idx, &parsed_path); + }); + let path = Arc::new(path); + if dar_1st_namespace.is_none() && parsed_path.is_1st_person { dar_1st_namespace = Some(parsed_path.dar_root.clone()); } else if dar_namespace.is_none() { dar_namespace = Some(parsed_path.dar_root.clone()); } - convert_inner(&options, path, parsed_path, &mut is_converted_once).await?; + + task_handles.push(tokio::spawn({ + let path = Arc::clone(&path); + let parsed_path = Arc::clone(&parsed_path); + let options = Arc::clone(&options); + let is_converted_once = Arc::clone(&is_converted_once); + async move { + tracing::debug!("[Start {}th conversion]\n{:?}", idx, &parsed_path); + convert_inner( + &options, + &path, + parsed_path.as_ref(), + is_converted_once.as_ref(), + ) + .await?; + tracing::debug!("[End {}th conversion]\n\n", idx); + Ok(()) + } + })); + } + + for (idx, task_handle) in task_handles.into_iter().enumerate() { + task_handle.await??; progress_fn(idx); - tracing::debug!("[End {}th conversion]\n\n", idx); } - if is_converted_once { + if is_converted_once.load(Ordering::Acquire) { handle_conversion_results(hide_dar, &dar_namespace, &dar_1st_namespace).await } else { - Err(ConvertError::NotFoundDarDir) + Err(ConvertError::NeverConverted) } } diff --git a/dar2oar_core/src/fs/converter/sequential.rs b/dar2oar_core/src/fs/converter/sequential.rs index 17f3a70..44f1aaa 100644 --- a/dar2oar_core/src/fs/converter/sequential.rs +++ b/dar2oar_core/src/fs/converter/sequential.rs @@ -4,6 +4,7 @@ use crate::fs::converter::{ConvertOptions, ConvertedReport}; use crate::fs::path_changer::parse_dar_path; use async_walkdir::{Filtering, WalkDir}; use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; use tokio_stream::StreamExt; /// Single thread converter @@ -15,10 +16,10 @@ use tokio_stream::StreamExt; /// # Return /// Complete info pub async fn convert_dar_to_oar( - options: ConvertOptions<'_, impl AsRef>, + options: ConvertOptions, mut progress_fn: impl FnMut(usize), ) -> Result { - let dar_dir = options.dar_dir.as_ref(); + let dar_dir = options.dar_dir.as_str(); let walk_len = get_dar_file_count(dar_dir).await?; tracing::debug!("Sequential Converter/DAR file counts: {}", walk_len); @@ -28,7 +29,7 @@ pub async fn convert_dar_to_oar( let mut dar_1st_namespace = None; // To need rename to hidden(For _1stperson) let mut dar_namespace = None; // To need rename to hidden let mut entries = get_dar_files(dar_dir).await; - let mut is_converted_once = false; + let is_converted_once = AtomicBool::new(false); let mut idx = 0usize; while let Some(entry) = entries.next().await { @@ -48,17 +49,17 @@ pub async fn convert_dar_to_oar( } else if dar_namespace.is_none() { dar_namespace = Some(parsed_path.dar_root.clone()); } - convert_inner(&options, path, parsed_path, &mut is_converted_once).await?; + convert_inner(&options, path, &parsed_path, &is_converted_once).await?; progress_fn(idx); tracing::debug!("[End {}th conversion]\n\n", idx); idx += 1; } - if is_converted_once { + if is_converted_once.load(Ordering::Acquire) { handle_conversion_results(hide_dar, &dar_namespace, &dar_1st_namespace).await } else { - Err(ConvertError::NotFoundDarDir) + Err(ConvertError::NeverConverted) } } diff --git a/locales/en-US.json b/locales/en-US.json index 402c7df..ce26ad7 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -30,8 +30,8 @@ "custom-js-label": "Custom JavaScript(Please do not execute untrusted scripts)", "editor-mode-list-label": "Editor Mode", "hide-dar-btn": "Hide DAR", - "hide-dar-btn-tooltip": "After conversion, append \".mohidden\" to the DAR dirname in \"DAR(src) Directory*\" to make it a hidden directory(For MO2 users)", - "hide-dar-btn-tooltip2": "NOTE: Failure to cross the drive or No permission.", + "hide-dar-btn-tooltip": "After conversion, append \".mohidden\" to the DAR dirname in \"DAR(src) Directory*\" to make it a hidden directory.", + "hide-dar-btn-tooltip2": "NOTE: It appears to work on the MO2 Tree view, but it is doubtful that it works in the author's actual experience.", "log-level-list-label": "Log Level", "mapping-wiki-url-leaf": "wiki#what-is-the-mapping-file", "open-log-btn": "Open log", @@ -43,7 +43,7 @@ "remove-oar-success": "Removed OAR directory.", "remove-oar-tooltip": "Find and delete OAR dir from \"DAR(src) Directory*\" or \"OAR(dist) Directory\".", "run-parallel-btn-tooltip": "Use multi-threading.", - "run-parallel-btn-tooltip2": "In most cases, it slows down by tens of ms, but may be effective when there is more weight on CPU processing with fewer files to copy and more logic parsing of \"_condition.txt\"", + "run-parallel-btn-tooltip2": "More than twice the processing speed can be expected, but the logs are difficult to read and may fail due to unexpected bugs.", "run-parallel-label": "Run Parallel", "select-btn": "Select", "unhide-dar-btn": "Unhide DAR", diff --git a/locales/ja-JP.json b/locales/ja-JP.json index 0b6c0d1..429e19c 100644 --- a/locales/ja-JP.json +++ b/locales/ja-JP.json @@ -30,8 +30,8 @@ "custom-js-label": "カスタムJavaScript(信用できないスクリプトは実行しないでください)", "editor-mode-list-label": "編集モード", "hide-dar-btn": "DARを非表示", - "hide-dar-btn-tooltip": "変換後に\"DAR(src) Directory*\"に\".mohidden\"を追加して隠しディレクトリ化(MO2ユーザ向け)", - "hide-dar-btn-tooltip2": "注意: DARの保存先の書き込み権限がない場合、エラーが発生する可能性があります", + "hide-dar-btn-tooltip": "変換後に\"DAR(src) Directory*\"に\".mohidden\"を追加して隠しディレクトリ化します", + "hide-dar-btn-tooltip2": "注意: MO2のTree view上では機能しているように見えますが作者の実体験では機能しているか怪しいです。", "log-level-list-label": "ログレベル", "mapping-wiki-url-leaf": "wiki#what-is-the-mapping-file", "open-log-btn": "ログを開く", @@ -43,7 +43,7 @@ "remove-oar-success": "OARディレクトリを削除しました", "remove-oar-tooltip": "OAR(dist)(なければDAR(src))からOARディレクトリを捜索して削除します", "run-parallel-btn-tooltip": "マルチスレッドを使用します", - "run-parallel-btn-tooltip2": "大抵の場合で数十ミリ秒遅延しますが、コピー対象が少なく、CPU処理の比重が大きい(\"_condition.txt\"の解析が多い)時には有効かもしれません。", + "run-parallel-btn-tooltip2": "2倍以上の処理速度が期待できますが、ログが読みづらく思わぬバグで失敗する可能性があります。", "run-parallel-label": "並列実行", "select-btn": "選択", "unhide-dar-btn": "DAR再表示", diff --git a/src-tauri/src/cmd.rs b/src-tauri/src/cmd.rs index ddc3be5..0495aeb 100644 --- a/src-tauri/src/cmd.rs +++ b/src-tauri/src/cmd.rs @@ -41,14 +41,14 @@ macro_rules! bail { } #[tauri::command] -pub(crate) async fn convert_dar2oar(options: GuiConverterOptions<'_>) -> Result { +pub(crate) async fn convert_dar2oar(options: GuiConverterOptions) -> Result { dar2oar!(options, Closure::default) } #[tauri::command] pub(crate) async fn convert_dar2oar_with_progress( window: Window, - options: GuiConverterOptions<'_>, + options: GuiConverterOptions, ) -> Result { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct Payload { diff --git a/src-tauri/src/convert_option.rs b/src-tauri/src/convert_option.rs index 5a17457..9c8810e 100644 --- a/src-tauri/src/convert_option.rs +++ b/src-tauri/src/convert_option.rs @@ -3,13 +3,13 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct GuiConverterOptions<'a> { - pub(crate) dar_dir: &'a str, - pub(crate) oar_dir: Option<&'a str>, - pub(crate) mod_name: Option<&'a str>, - pub(crate) mod_author: Option<&'a str>, - pub(crate) mapping_path: Option<&'a str>, - pub(crate) mapping_1person_path: Option<&'a str>, +pub(crate) struct GuiConverterOptions { + pub(crate) dar_dir: String, + pub(crate) oar_dir: Option, + pub(crate) mod_name: Option, + pub(crate) mod_author: Option, + pub(crate) mapping_path: Option, + pub(crate) mapping_1person_path: Option, pub(crate) run_parallel: Option, pub(crate) hide_dar: Option, } @@ -20,8 +20,8 @@ pub(crate) trait AsyncFrom { } #[async_trait::async_trait] -impl<'a> AsyncFrom> for ConvertOptions<'a, &'a str> { - async fn async_from(options: GuiConverterOptions<'a>) -> Self { +impl AsyncFrom for ConvertOptions { + async fn async_from(options: GuiConverterOptions) -> Self { let GuiConverterOptions { dar_dir, oar_dir,