From 13de53b00ea0e2ffa8a0e5c3f46c1f0d37ede081 Mon Sep 17 00:00:00 2001 From: Loong Date: Fri, 13 Oct 2023 17:47:57 +0800 Subject: [PATCH] feat: add `--dry-run` option to `run` command (#76) This option is useful user check their config file, and run tests in CI. May be used in unit tests in the future. --- .github/workflows/ci.yml | 4 +- config_examples/asst.toml | 2 +- maa-cli/src/config/asst.rs | 144 +++++++++------ maa-cli/src/main.rs | 262 ++++++++++++++++++++++++++- maa-cli/src/run/mod.rs | 355 ++++++++++++++++++++++--------------- 5 files changed, 559 insertions(+), 208 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26e108b4..620c1a58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,9 +102,11 @@ jobs: echo "MAA_CORE_DIR=$MAA_CORE_DIR" >> $GITHUB_ENV - name: Try run with MaaCore if: matrix.arch == 'x86_64' + env: + MAA_CONFIG_DIR: ${{ github.workspace }}/config_examples run: | - export DYLD_FALLBACK_LIBRARY_PATH="$MAA_CORE_DIR" cargo run -- version + cargo run -- run daily --dry-run --batch - name: Test (maa-sys, static) if: matrix.arch == 'x86_64' run: | diff --git a/config_examples/asst.toml b/config_examples/asst.toml index bd703f33..11732b2d 100644 --- a/config_examples/asst.toml +++ b/config_examples/asst.toml @@ -1,5 +1,5 @@ user_resource = true -resources = ["platform_diff/macOS"] +resources = ["platform_diff/iOS"] [connection] type = "ADB" diff --git a/maa-cli/src/config/asst.rs b/maa-cli/src/config/asst.rs index 29c2f4bc..cf4060b7 100644 --- a/maa-cli/src/config/asst.rs +++ b/maa-cli/src/config/asst.rs @@ -10,12 +10,12 @@ pub struct AsstConfig { #[serde(default)] pub connection: Connection, #[serde(default)] - pub instance_options: InstanceOption, + pub instance_options: InstanceOptions, } #[cfg_attr(test, derive(Debug, PartialEq))] #[derive(Deserialize, Default)] -pub struct InstanceOption { +pub struct InstanceOptions { #[serde(default)] pub touch_mode: Option, pub deployment_with_pause: Option, @@ -33,9 +33,9 @@ pub enum TouchMode { MacPlayTools, } -impl<'a> From for &'a str { - fn from(mode: TouchMode) -> Self { - match mode { +impl AsRef for TouchMode { + fn as_ref(&self) -> &str { + match self { TouchMode::ADB => "adb", TouchMode::MiniTouch => "minitouch", TouchMode::MAATouch => "maatouch", @@ -46,13 +46,13 @@ impl<'a> From for &'a str { impl std::fmt::Display for TouchMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", <&str>::from(*self)) + write!(f, "{}", self.as_ref()) } } impl maa_sys::ToCString for TouchMode { fn to_cstring(self) -> maa_sys::Result { - <&str>::from(self).to_cstring() + self.as_ref().to_cstring() } } @@ -117,57 +117,89 @@ impl super::FromFile for AsstConfig {} mod tests { use super::*; - #[test] - fn deserialize_example() { - let config: AsstConfig = - toml::from_str(&std::fs::read_to_string("../config_examples/asst.toml").unwrap()) - .unwrap(); - assert_eq!( - config, - AsstConfig { - user_resource: true, - resources: vec![String::from("platform_diff/macOS")], - connection: Connection::ADB { - adb_path: String::from("adb"), - device: String::from("emulator-5554"), - config: String::from("CompatMac"), - }, - instance_options: InstanceOption { - touch_mode: Some(TouchMode::MAATouch), - deployment_with_pause: Some(false), - adb_lite_enabled: Some(false), - kill_adb_on_exit: Some(false), - }, - } - ); + mod touch_mode { + use super::*; + + use std::ffi::CString; + + use maa_sys::ToCString; + + #[test] + fn to_cstring() { + assert_eq!( + TouchMode::ADB.to_cstring().unwrap(), + CString::new("adb").unwrap() + ); + assert_eq!( + TouchMode::MiniTouch.to_cstring().unwrap(), + CString::new("minitouch").unwrap() + ); + assert_eq!( + TouchMode::MAATouch.to_cstring().unwrap(), + CString::new("maatouch").unwrap() + ); + assert_eq!( + TouchMode::MacPlayTools.to_cstring().unwrap(), + CString::new("MacPlayTools").unwrap() + ); + } } - #[test] - fn deserialize_empty() { - let config: AsstConfig = toml::from_str("").unwrap(); - assert_eq!( - config, - AsstConfig { - user_resource: false, - resources: vec![], - connection: Connection::ADB { - adb_path: String::from("adb"), - device: String::from("emulator-5554"), - config: if cfg!(target_os = "macos") { - String::from("CompatMac") - } else if cfg!(target_os = "linux") { - String::from("CompatPOSIXShell") - } else { - String::from("General") + mod serde { + use super::*; + + #[test] + fn deserialize_example() { + let config: AsstConfig = + toml::from_str(&std::fs::read_to_string("../config_examples/asst.toml").unwrap()) + .unwrap(); + assert_eq!( + config, + AsstConfig { + user_resource: true, + resources: vec![String::from("platform_diff/iOS")], + connection: Connection::ADB { + adb_path: String::from("adb"), + device: String::from("emulator-5554"), + config: String::from("CompatMac"), + }, + instance_options: InstanceOptions { + touch_mode: Some(TouchMode::MAATouch), + deployment_with_pause: Some(false), + adb_lite_enabled: Some(false), + kill_adb_on_exit: Some(false), }, - }, - instance_options: InstanceOption { - touch_mode: None, - deployment_with_pause: None, - adb_lite_enabled: None, - kill_adb_on_exit: None, - }, - } - ); + } + ); + } + + #[test] + fn deserialize_empty() { + let config: AsstConfig = toml::from_str("").unwrap(); + assert_eq!( + config, + AsstConfig { + user_resource: false, + resources: vec![], + connection: Connection::ADB { + adb_path: String::from("adb"), + device: String::from("emulator-5554"), + config: if cfg!(target_os = "macos") { + String::from("CompatMac") + } else if cfg!(target_os = "linux") { + String::from("CompatPOSIXShell") + } else { + String::from("General") + }, + }, + instance_options: InstanceOptions { + touch_mode: None, + deployment_with_pause: None, + adb_lite_enabled: None, + kill_adb_on_exit: None, + }, + } + ); + } } } diff --git a/maa-cli/src/main.rs b/maa-cli/src/main.rs index b72c84c1..cfe0767d 100644 --- a/maa-cli/src/main.rs +++ b/maa-cli/src/main.rs @@ -195,7 +195,7 @@ enum SubCommand { /// And if you want to use PlayCover, /// you need to set the connection type to PlayCover in the config file /// and then you can specify the address of MaaTools here. - #[clap(short, long, verbatim_doc_comment)] + #[arg(short, long, verbatim_doc_comment)] addr: Option, /// Load resources from the config directory /// @@ -211,10 +211,10 @@ enum SubCommand { /// CLI will load resources shipped with MaaCore firstly, /// then some client specific or platform specific when needed, /// lastly, it will load resources from the config directory. - /// MAACore will overwrite the resources loaded before, + /// MaaCore will overwrite the resources loaded before, /// if there are some resources with the same name. /// Use at your own risk! - #[clap(long, verbatim_doc_comment)] + #[arg(long, verbatim_doc_comment)] user_resource: bool, /// Run tasks in batch mode /// @@ -222,8 +222,16 @@ enum SubCommand { /// some prompts will be displayed to ask for input. /// In batch mode, the prompts will be skipped, /// and parameters will be set to default values. - #[clap(short, long, verbatim_doc_comment)] + #[arg(short, long, verbatim_doc_comment)] batch: bool, + /// Parse the your config but do not connect to the game + /// + /// This option is useful when you want to check your config file. + /// It will parse your config file and set the log level to debug. + /// If there are some errors in your config file, + /// it will print the error message and exit. + #[arg(long, verbatim_doc_comment)] + dry_run: bool, }, /// List all available tasks List, @@ -264,9 +272,8 @@ pub enum Dir { /// Directory of maa-cli's data Data, /// Directory of maa-cli's dynamic library + #[value(alias("lib"))] Library, - /// Directory of maa-cli's dynamic library, alias of library - Lib, /// Directory of maa-cli's config Config, /// Directory of maa-cli's cache @@ -321,7 +328,7 @@ fn main() -> Result<()> { }, SubCommand::Dir { dir_type } => match dir_type { Dir::Data => println!("{}", proj_dirs.data().display()), - Dir::Library | Dir::Lib => { + Dir::Library => { println!("{}", maa_core::find_lib_dir(&proj_dirs).unwrap().display()) } Dir::Config => println!("{}", proj_dirs.config().display()), @@ -348,7 +355,8 @@ fn main() -> Result<()> { addr, user_resource, batch, - } => run::run(&proj_dirs, task, addr, user_resource, batch)?, + dry_run, + } => run::run(&proj_dirs, task, addr, user_resource, batch, dry_run)?, SubCommand::List => { let task_dir = proj_dirs.config().join("tasks"); if !task_dir.exists() { @@ -372,4 +380,240 @@ fn main() -> Result<()> { } #[cfg(test)] -mod test {} +mod test { + use super::*; + + mod parser { + use super::*; + + #[test] + fn log_level() { + assert!(matches!(CLI::parse_from(["maa", "-v", "help"]).verbose, 1)); + assert!(matches!(CLI::parse_from(["maa", "help", "-v"]).verbose, 1)); + assert!(matches!(CLI::parse_from(["maa", "help", "-vv"]).verbose, 2)); + assert!(matches!(CLI::parse_from(["maa", "help", "-q"]).quiet, 1)); + assert!(matches!(CLI::parse_from(["maa", "help", "-qq"]).quiet, 2)); + } + + #[test] + fn install() { + assert!(matches!( + CLI::parse_from(["maa", "install"]).command, + SubCommand::Install { .. } + )); + + assert!(matches!( + CLI::parse_from(["maa", "install", "beta"]).command, + SubCommand::Install { + channel: Some(Channel::Beta), + .. + } + )); + + assert!(matches!( + CLI::parse_from(["maa", "install", "-t5"]).command, + SubCommand::Install { test_time: 5, .. } + )); + assert!(matches!( + CLI::parse_from(["maa", "install", "--test-time", "5"]).command, + SubCommand::Install { test_time: 5, .. } + )); + + assert!(matches!( + CLI::parse_from(["maa", "install", "--force"]).command, + SubCommand::Install { force: true, .. } + )); + + assert!(matches!( + CLI::parse_from(["maa", "install", "--no-resource"]).command, + SubCommand::Install { + no_resource: true, + .. + } + )); + } + + #[test] + fn update() { + assert!(matches!( + CLI::parse_from(["maa", "update"]).command, + SubCommand::Update { + channel: None, + test_time: 3, + no_resource: false, + } + )); + + assert!(matches!( + CLI::parse_from(["maa", "update", "beta"]).command, + SubCommand::Update { + channel: Some(Channel::Beta), + .. + } + )); + + assert!(matches!( + CLI::parse_from(["maa", "update", "-t5"]).command, + SubCommand::Update { test_time: 5, .. } + )); + assert!(matches!( + CLI::parse_from(["maa", "update", "--test-time", "5"]).command, + SubCommand::Update { test_time: 5, .. } + )); + + assert!(matches!( + CLI::parse_from(["maa", "update", "--no-resource"]).command, + SubCommand::Update { + no_resource: true, + .. + } + )); + } + + #[test] + #[cfg(feature = "self")] + fn self_command() { + assert!(matches!( + CLI::parse_from(["maa", "self", "update"]).command, + SubCommand::SelfCommand(SelfCommand::Update) + )); + } + + #[test] + fn dir() { + assert!(matches!( + CLI::parse_from(["maa", "dir", "data"]).command, + SubCommand::Dir { + dir_type: Dir::Data + } + )); + assert!(matches!( + CLI::parse_from(["maa", "dir", "library"]).command, + SubCommand::Dir { + dir_type: Dir::Library + } + )); + assert!(matches!( + CLI::parse_from(["maa", "dir", "lib"]).command, + SubCommand::Dir { + dir_type: Dir::Library + } + )); + assert!(matches!( + CLI::parse_from(["maa", "dir", "config"]).command, + SubCommand::Dir { + dir_type: Dir::Config + } + )); + assert!(matches!( + CLI::parse_from(["maa", "dir", "cache"]).command, + SubCommand::Dir { + dir_type: Dir::Cache + } + )); + assert!(matches!( + CLI::parse_from(["maa", "dir", "resource"]).command, + SubCommand::Dir { + dir_type: Dir::Resource + } + )); + assert!(matches!( + CLI::parse_from(["maa", "dir", "log"]).command, + SubCommand::Dir { dir_type: Dir::Log } + )); + } + + #[test] + fn version() { + assert!(matches!( + CLI::parse_from(["maa", "version"]).command, + SubCommand::Version { + component: Component::All + } + )); + assert!(matches!( + CLI::parse_from(["maa", "version", "all"]).command, + SubCommand::Version { + component: Component::All + } + )); + assert!(matches!( + CLI::parse_from(["maa", "version", "maa-cli"]).command, + SubCommand::Version { + component: Component::MaaCLI + } + )); + assert!(matches!( + CLI::parse_from(["maa", "version", "maa-core"]).command, + SubCommand::Version { + component: Component::MaaCore + } + )); + } + + #[test] + fn run() { + assert!(matches!( + CLI::parse_from(["maa", "run", "task"]).command, + SubCommand::Run { + task, + addr: None, + user_resource: false, + batch: false, + dry_run: false, + } if task == "task" + )); + + assert!(matches!( + CLI::parse_from(["maa", "run", "task", "-a", "addr"]).command, + SubCommand::Run { + task, + addr: Some(addr), + .. + } if task == "task" && addr == "addr" + )); + assert!(matches!( + CLI::parse_from(["maa", "run", "task", "--addr", "addr"]).command, + SubCommand::Run { + task, + addr: Some(addr), + .. + } if task == "task" && addr == "addr" + )); + + assert!(matches!( + CLI::parse_from(["maa", "run", "task", "--user-resource"]).command, + SubCommand::Run { + task, + user_resource: true, + .. + } if task == "task" + )); + + assert!(matches!( + CLI::parse_from(["maa", "run", "task", "--batch"]).command, + SubCommand::Run { + task, + batch: true, + .. + } if task == "task" + )); + } + + #[test] + fn list() { + assert!(matches!( + CLI::parse_from(["maa", "list"]).command, + SubCommand::List + )); + } + + #[test] + fn complete() { + assert!(matches!( + CLI::parse_from(["maa", "complete", "bash"]).command, + SubCommand::Complete { shell: Shell::Bash } + )); + } + } +} diff --git a/maa-cli/src/run/mod.rs b/maa-cli/src/run/mod.rs index f086764e..fbfdff76 100644 --- a/maa-cli/src/run/mod.rs +++ b/maa-cli/src/run/mod.rs @@ -3,7 +3,7 @@ use message::callback; use crate::{ config::{ - asst::{self, AsstConfig, Connection}, + asst::{self, AsstConfig, Connection, TouchMode}, task::{ task_type::{TaskOrUnknown, TaskType}, value::input::enable_batch_mode, @@ -13,11 +13,11 @@ use crate::{ }, dirs::{Dirs, Ensure}, installer::maa_core::{find_maa_core, find_resource, MAA_CORE_NAME}, - {debug, error, normal, warning}, + log::{set_level, LogLevel}, + {debug, normal, warning}, }; -use std::path::PathBuf; -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use anyhow::{anyhow, bail, Context, Result}; use maa_sys::Assistant; @@ -29,35 +29,32 @@ pub fn run( addr: Option, user_resource: bool, batch: bool, + dryrun: bool, ) -> Result<()> { - /*------------------------ Load MaaCore ------------------------*/ - load_core(dirs); - - /*------------------- Setup global log level -------------------*/ - unsafe { - if batch { - enable_batch_mode(); - } + if dryrun { + unsafe { set_level(LogLevel::Debug) }; + debug!("Dryrun mode!"); } - /*--------------------- Setup MaaCore Dirs ---------------------*/ - let state_dir = dirs.state().ensure()?; - debug!("State directory:", state_dir.display()); - Assistant::set_user_dir(state_dir).context("Failed to set user directory!")?; - - let resource_dir = find_resource(dirs).context("Failed to find resource!")?; - debug!("Resources directory:", resource_dir.display()); - Assistant::load_resource(resource_dir.parent().unwrap()).context("Failed to load resource!")?; - - /*--------------------- Load Config Files ---------------------*/ - let config_dir = dirs.config(); - if !config_dir.exists() { - bail!("Config directory not exists!"); + if batch { + unsafe { enable_batch_mode() } + debug!("Running in batch mode!"); } - debug!("Config directory:", config_dir.display()); - // asst.toml - let asst_config = match AsstConfig::find_file(&config_dir.join("asst")) { + // Get directories + let state_dir = dirs.state().ensure()?; + let config_dir = dirs.config().ensure()?; + let base_resource_dir = find_resource(dirs).context("Failed to find resource!")?; + debug!("State Directory:", state_dir.display()); + debug!("Config Directory:", config_dir.display()); + debug!("Base Resource Directory:", base_resource_dir.display()); + + /*------------------- Process Asst Config ----------------------*/ + + // Load asst config from config_dir/asst.(toml|yaml|json) + let asst_file = config_dir.join("asst"); + debug!("Finding asst config file:", asst_file.display()); + let asst_config = match AsstConfig::find_file(&asst_file) { Ok(config) => config, Err(ConfigError::FileNotFound(_)) => { warning!("Failed to find asst config file, using default config!"); @@ -66,17 +63,7 @@ pub fn run( Err(e) => return Err(e.into()), }; - // tasks/.toml - let task_file = config_dir.join("tasks").join(&task); - let task_list = TaskList::find_file(&task_file).with_context(|| { - format!( - "Failed to find task file {} in {}", - task, - task_file.display() - ) - })?; - - /*--------------------- Process Connection ---------------------*/ + // Process connection let mut playtools: bool = false; let (adb_path, address, config) = match asst_config.connection { Connection::ADB { @@ -84,9 +71,11 @@ pub fn run( device, config, } => { - debug!("Setting adb_path to", &adb_path); - debug!("Setting device to", &device); - debug!("Setting config to", &config); + let device = addr.unwrap_or(device); + debug!("Connect to device via ADB"); + debug!("adb_path:", &adb_path); + debug!("device:", &device); + debug!("config:", &config); (adb_path, device, config) } Connection::PlayTools { address, config } => { @@ -98,7 +87,48 @@ pub fn run( } }; + // Process instance options + let mut instance_options = asst_config.instance_options; + if let Some(v) = instance_options.touch_mode { + if playtools && v != asst::TouchMode::MacPlayTools { + warning!("Force set `touch_mode` to `MacPlayTools` when using `PlayTools`"); + instance_options.touch_mode = Some(TouchMode::MacPlayTools); + } else { + debug!("Instance Option `touch_mode`:", v); + } + } else if playtools { + let mode = asst::TouchMode::MacPlayTools; + debug!("Instance Option `touch_mode`:", mode); + instance_options.touch_mode = Some(mode); + } else { + let mode = asst::TouchMode::default(); + debug!("Instance Option `touch_mode`:", mode); + instance_options.touch_mode = Some(mode); + } + + if let Some(v) = instance_options.adb_lite_enabled { + debug!("Instance Option `adb_lite_enabled`:", v); + } + if let Some(v) = instance_options.deployment_with_pause { + debug!("Instance Option `deployment_with_pause`:", v); + } + if let Some(v) = instance_options.kill_adb_on_exit { + debug!("Instance Option `kill_adb_on_exit`:", v); + } + /*----------------------- Process Task -------------------------*/ + + // Load task from tasks/.(toml|yaml|json) + let task_file = config_dir.join("tasks").join(&task); + debug!("Finding task file:", task_file.display()); + let task_list = TaskList::find_file(&task_file).with_context(|| { + format!( + "Failed to find task file {} in {}", + task, + task_file.display() + ) + })?; + let mut tasks: Vec = Vec::new(); let mut task_params: Vec = Vec::new(); @@ -127,8 +157,11 @@ pub fn run( if let Some(client_type) = params.get("client_type") { let client_name = String::try_from(client_type)?; - app_name = match_app_name(&client_name); - client_resource = match_resource(&client_name); + let cilent_type: ClientType = client_name.parse()?; + if playtools { + app_name = Some(cilent_type.app_name()); + }; + client_resource = cilent_type.resource(); }; } TaskType::CloseDown if playtools => { @@ -173,12 +206,12 @@ pub fn run( let app = if start_app || close_app { match app_name { Some(name) => { - debug!("Using PlayCover to launch app", name); + debug!("PlayCover app:", name); Some(PlayCoverApp::new(name)) } None => { warning!( - "No client type specified, ", + "No client type specified,", format!("using default app name {}", "明日方舟") ); Some(PlayCoverApp::new("明日方舟")) @@ -188,46 +221,60 @@ pub fn run( None }; - /*------------------------ Load Resource -----------------------*/ + /*----------------------- Process Resource ---------------------*/ + // Resource directorys + let mut resource_dirs = vec![base_resource_dir.parent().unwrap().to_path_buf()]; + // Cilent specific resource if let Some(resource) = client_resource { - debug!("Loading additional resource for client", resource); - Assistant::load_resource(resource_dir.join("global").join(resource)) - .with_context(|| format!("Failed to load additional resource {}!", resource))?; + debug!("Client specific resource:", resource); + resource_dirs.push(base_resource_dir.join("global").join(resource)); } // Platform specific resource if playtools { - debug!("Load additional resource for PlayTools"); - Assistant::load_resource(resource_dir.join("platform_diff/iOS")) - .context("Failed to load additional resource for iOS App!")?; + debug!("Platform specific resource:", "iOS"); + resource_dirs.push(base_resource_dir.join("platform_diff/iOS")); } // User specified additional resource for resource in asst_config.resources.iter() { let path = PathBuf::from(resource); let path = if path.is_absolute() { - debug!("Loading additional resource:", path.display()); + debug!("User specified additional resource:", resource); path } else { - debug!("Loading additional resource:", resource); - resource_dir.join(resource) + base_resource_dir.join(resource) }; - Assistant::load_resource(&path) - .with_context(|| format!("Failed to load additional resource {}!", path.display()))?; + if let Some(path) = process_resource_dir(path) { + resource_dirs.push(path); + } } // User resource in config directory if user_resource || asst_config.user_resource { - if config_dir.join("resource").exists() { - debug!("Loading user resource:", config_dir.display()); - Assistant::load_resource(config_dir).context("Failed to load user resource!")?; - } else { - warning!("`User resource` is enabled, but no resource directory found!"); + if let Some(path) = process_resource_dir(config_dir.join("resource")) { + resource_dirs.push(path); } } - /*------------------------ Init Assistant ----------------------*/ + /*----------------------- Start Assistant ----------------------*/ + // Load MaaCore + load_core(dirs); + + // TODO: Set static option (used in future version of MaaCore) + + // Set user directory (some debug info and cache will be stored here) + // Must be called before load resource (if not it will be set to resource directory) + Assistant::set_user_dir(state_dir).context("Failed to set user directory!")?; + + // Load Resource + for path in resource_dirs.iter() { + Assistant::load_resource(path) + .with_context(|| format!("Failed to load resource from {}", path.display()))?; + } + + // Init Assistant let stop_bool = Arc::new(std::sync::atomic::AtomicBool::new(false)); for sig in TERM_SIGNALS { signal_hook::flag::register_conditional_default(*sig, Arc::clone(&stop_bool)) @@ -237,84 +284,57 @@ pub fn run( } let assistant = Assistant::new(Some(callback), None); - /*------------------------ Setup Instance ----------------------*/ - let options = asst_config.instance_options; - if let Some(v) = options.touch_mode { - if playtools && v != asst::TouchMode::MacPlayTools { - warning!( - "Wrong touch mode,", - "force set touch_mode to MacPlayTools when using PlayTools" - ); - assistant - .set_instance_option(2, asst::TouchMode::MacPlayTools) - .context("Failed to set touch mode!")?; - } else { - debug!("Setting touch_mode to", v); - assistant - .set_instance_option(2, v) - .context("Failed to set touch mode!")?; - } - } else if playtools { - debug!("Setting touch_mode to MacPlayTools"); - assistant - .set_instance_option(2, asst::TouchMode::MacPlayTools) - .context("Failed to set touch mode!")?; - } else { - let mode = asst::TouchMode::default(); - warning!( - "No touch mode specified,", - format!("using default touch mode {}.", mode) - ); + // Set instance options + if let Some(v) = instance_options.touch_mode { assistant - .set_instance_option(2, mode) - .context("Failed to set touch mode!")?; + .set_instance_option(2, v) + .context("Failed to set instance option `touch_mode`!")?; } - if let Some(v) = options.deployment_with_pause { - debug!("Setting deployment_with_pause to", v); + if let Some(v) = instance_options.deployment_with_pause { assistant .set_instance_option(3, v) - .context("Failed to set deployment with pause!")?; + .context("Failed to set instance option `deployment_with_pause`!")?; } - if let Some(v) = options.adb_lite_enabled { - debug!("Setting adb_lite_enabled to", v); - assistant.set_instance_option(4, v)?; + if let Some(v) = instance_options.adb_lite_enabled { + assistant + .set_instance_option(4, v) + .context("Failed to set instance option `adb_lite_enabled`!")?; } - if let Some(v) = options.kill_adb_on_exit { - debug!("Setting kill_adb_on_exit to", v); - assistant.set_instance_option(5, v)?; + if let Some(v) = instance_options.kill_adb_on_exit { + assistant + .set_instance_option(5, v) + .context("Failed to set instance option `kill_adb_on_exit`!")?; } - /*----------------------- Connect to Game ----------------------*/ - if start_app { - app.as_ref().unwrap().open()?; - std::thread::sleep(std::time::Duration::from_secs(5)); - } + if !dryrun { + if start_app { + app.as_ref().unwrap().open()?; + std::thread::sleep(std::time::Duration::from_secs(5)); + } - assistant.async_connect(adb_path, address, config, true)?; + assistant.async_connect(adb_path, address, config, true)?; - /* ------------------------- Append Tasks ----------------------*/ - for i in 0..tasks.len() { - assistant.append_task(tasks[i].as_str(), task_params[i].as_str())?; - } + for i in 0..tasks.len() { + assistant.append_task(tasks[i].as_str(), task_params[i].as_str())?; + } + + assistant.start()?; + while assistant.running() { + if stop_bool.load(std::sync::atomic::Ordering::Relaxed) { + bail!("Interrupted by user!"); + } + std::thread::sleep(std::time::Duration::from_millis(500)); + } + assistant.stop()?; - /* ------------------------ Run Assistant ----------------------*/ - assistant.start()?; - while assistant.running() { - if stop_bool.load(std::sync::atomic::Ordering::Relaxed) { - bail!("Interrupted by user!"); + if close_app { + app.as_ref().unwrap().close(); } - std::thread::sleep(std::time::Duration::from_millis(500)); } - assistant.stop()?; // TODO: Better ways to restore signal handlers? stop_bool.store(true, std::sync::atomic::Ordering::Relaxed); - /* ------------------------- Close Game ------------------------*/ - if close_app { - app.as_ref().unwrap().close(); - } - Ok(()) } @@ -361,26 +381,79 @@ impl<'n> PlayCoverApp<'n> { } } -fn match_app_name(client: &str) -> Option<&'static str> { - match client { - "Official" | "Bilibili" | "txwy" | "" => None, - "YoStarEN" => Some("Arknights"), - "YoStarJP" => Some("アークナイツ"), - "YoStarKR" => Some("명일방주"), - _ => { - error!("Unknown client type", client); - None +#[derive(Clone, Copy)] +enum ClientType { + Official, + Bilibili, + Txwy, + YoStarEN, + YoStarJP, + YoStarKR, +} + +impl ClientType { + pub fn app_name(self) -> &'static str { + match self { + ClientType::Official | ClientType::Bilibili | ClientType::Txwy => "明日方舟", + ClientType::YoStarEN => "Arknights", + ClientType::YoStarJP => "アークナイツ", + ClientType::YoStarKR => "명일방주", + } + } + + pub fn resource(self) -> Option<&'static str> { + match self { + ClientType::Txwy => Some("txwy"), + ClientType::YoStarEN => Some("YoStarEN"), + ClientType::YoStarJP => Some("YoStarJP"), + ClientType::YoStarKR => Some("YoStarKR"), + _ => None, + } + } +} + +impl std::str::FromStr for ClientType { + type Err = ParseClientTypeError; + + fn from_str(s: &str) -> Result { + match s { + "Official" | "" => Ok(ClientType::Official), + "Bilibili" => Ok(ClientType::Bilibili), + "txwy" => Ok(ClientType::Txwy), + "YoStarEN" => Ok(ClientType::YoStarEN), + "YoStarJP" => Ok(ClientType::YoStarJP), + "YoStarKR" => Ok(ClientType::YoStarKR), + _ => Err(ParseClientTypeError::UnknownClientType), } } } -fn match_resource(client: &str) -> Option<&'static str> { - match client { - "txwy" => Some("txwy"), - "YoStarEN" => Some("YoStarEN"), - "YoStarJP" => Some("YoStarJP"), - "YoStarKR" => Some("YoStarKR"), - _ => None, +#[derive(Debug)] +enum ParseClientTypeError { + UnknownClientType, +} + +impl std::fmt::Display for ParseClientTypeError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + ParseClientTypeError::UnknownClientType => write!(f, "Unknown client type!"), + } + } +} + +impl std::error::Error for ParseClientTypeError {} + +fn process_resource_dir(path: PathBuf) -> Option { + let path = if path.ends_with("resource") { + path + } else { + path.join("resource") + }; + if path.is_dir() { + Some(path.parent().unwrap().to_path_buf()) + } else { + warning!(format!("Resource directory {} not found!", path.display())); + None } }