From ce444aebfc0e160a1fc17a94c90764c7c9c2bdfa Mon Sep 17 00:00:00 2001 From: Loong Date: Tue, 12 Dec 2023 03:01:52 +0800 Subject: [PATCH] refactor and add more tests --- maa-cli/Cargo.toml | 6 +- maa-cli/src/config/asst.rs | 556 ++++++++++++++++++++++--- maa-cli/src/config/cli/maa_cli.rs | 39 +- maa-cli/src/config/task/client_type.rs | 115 ++++- maa-cli/src/config/task/mod.rs | 310 +++++++++----- maa-cli/src/dirs.rs | 22 +- maa-cli/src/run/mod.rs | 87 ++-- maa-cli/src/run/playcover.rs | 112 +++-- 8 files changed, 954 insertions(+), 293 deletions(-) diff --git a/maa-cli/Cargo.toml b/maa-cli/Cargo.toml index 88482394..b0ba0588 100644 --- a/maa-cli/Cargo.toml +++ b/maa-cli/Cargo.toml @@ -90,9 +90,9 @@ features = ["rt"] version = "0.11" features = ["blocking", "json"] -[dev-dependencies] -serde_test = "1.0.176" - [target.'cfg(windows)'.dependencies.windows-sys] version = "0.48.0" features = ["Win32_System_LibraryLoader"] + +[dev-dependencies] +serde_test = "1" diff --git a/maa-cli/src/config/asst.rs b/maa-cli/src/config/asst.rs index 4efd4bf7..06994a76 100644 --- a/maa-cli/src/config/asst.rs +++ b/maa-cli/src/config/asst.rs @@ -97,6 +97,8 @@ pub enum ConnectionConfig { }, } +const EMPTY_STR: &str = ""; + impl ConnectionConfig { pub fn set_address(&mut self, addr: impl Into) -> &Self { match self { @@ -110,7 +112,7 @@ impl ConnectionConfig { self } - pub fn connect(&self, asst: &Assistant) -> maa_sys::Result<()> { + pub fn connect_args(&self) -> (&str, &str, &str) { match self { ConnectionConfig::ADB { adb_path, @@ -121,17 +123,48 @@ impl ConnectionConfig { "Connecting to {} with config {} via {}", device, config, adb_path )); - Assistant::async_connect(asst, adb_path, device, config, true)?; + (adb_path, device, config) } ConnectionConfig::PlayTools { address, config } => { debug!(format!( "Connecting to {} with config {} via PlayTools", address, config )); - Assistant::async_connect(asst, String::new(), address, config, true)?; + (EMPTY_STR, address, config) } } - Ok(()) + } +} + +impl Default for ConnectionConfig { + fn default() -> Self { + ConnectionConfig::ADB { + adb_path: default_adb_path(), + device: default_device(), + config: default_config(), + } + } +} + +fn default_adb_path() -> String { + String::from("adb") +} + +fn default_device() -> String { + String::from("emulator-5554") +} + +fn default_playcover_address() -> String { + String::from("localhost:1717") +} + +fn default_config() -> String { + if cfg!(target_os = "macos") { + String::from("CompatMac") + } else if cfg!(target_os = "linux") { + String::from("CompatPOSIXShell") + } else { + String::from("General") } } @@ -269,8 +302,8 @@ impl ResourceConfig { } /// Get all resource directories, including global and platform diff resources - pub fn resource_dirs(&self) -> Result> { - let base_dirs = &self.resource_base_dirs; + pub fn resource_dirs(&self) -> Vec { + let base_dirs = self.base_dirs(); let mut resource_dirs = base_dirs.clone(); if let Some(global_resource) = self.global_resource.as_ref() { let global_resource_dir = PathBuf::from("global") @@ -301,11 +334,11 @@ impl ResourceConfig { } } - Ok(resource_dirs) + resource_dirs } pub fn load(&self) -> Result<()> { - let resource_dirs = self.resource_dirs()?; + let resource_dirs = self.resource_dirs(); for resource_dir in resource_dirs { debug!("Loading resource from", resource_dir.display()); Assistant::load_resource(resource_dir.parent().unwrap())?; @@ -316,14 +349,18 @@ impl ResourceConfig { } fn push_user_resource(resource_dirs: &mut Vec) -> &mut Vec { - let user_resource_dir = dirs::config().join("resource"); - if !user_resource_dir.exists() { + push_resource(resource_dirs, dirs::config().join("resource")) +} + +fn push_resource(resource_dirs: &mut Vec, dir: impl Into) -> &mut Vec { + let dir = dir.into(); + if dir.exists() { + resource_dirs.push(dir); + } else { warning!(format!( - "User resource directory {} not found, ignoring", - user_resource_dir.display(), + "Resource directory {} not found, ignoring", + dir.display(), )); - } else { - resource_dirs.push(user_resource_dir); } resource_dirs @@ -333,9 +370,9 @@ fn push_user_resource(resource_dirs: &mut Vec) -> &mut Vec { #[derive(Deserialize, Default, Clone)] pub struct StaticOptions { #[serde(default)] - pub cpu_ocr: Option, + cpu_ocr: Option, #[serde(default)] - pub gpu_ocr: Option, + gpu_ocr: Option, } impl StaticOptions { @@ -374,7 +411,7 @@ pub struct InstanceOptions { } impl InstanceOptions { - pub fn force_playtools(&mut self) -> &mut Self { + fn force_playtools(&mut self) -> &mut Self { match self.touch_mode { Some(touch_mode) if !matches!(touch_mode, TouchMode::MacPlayTools) => { warning!("Connect with PlayTools force touch mode to MacPlayTools"); @@ -419,38 +456,6 @@ impl InstanceOptions { } } -impl Default for ConnectionConfig { - fn default() -> Self { - ConnectionConfig::ADB { - adb_path: default_adb_path(), - device: default_device(), - config: default_config(), - } - } -} - -pub fn default_adb_path() -> String { - String::from("adb") -} - -pub fn default_device() -> String { - String::from("emulator-5554") -} - -pub fn default_playcover_address() -> String { - String::from("localhost:1717") -} - -pub fn default_config() -> String { - if cfg!(target_os = "macos") { - String::from("CompatMac") - } else if cfg!(target_os = "linux") { - String::from("CompatPOSIXShell") - } else { - String::from("General") - } -} - #[cfg(test)] mod tests { use super::*; @@ -458,8 +463,14 @@ mod tests { mod serde { use super::*; + use crate::dirs::Ensure; + + use serde_test::{assert_de_tokens, Token}; + #[test] fn deserialize_example() { + dirs::config().join("resource").ensure().unwrap(); + let config: AsstConfig = toml::from_str(&std::fs::read_to_string("../config_examples/asst.toml").unwrap()) .unwrap(); @@ -497,21 +508,190 @@ mod tests { } #[test] - fn deserialize_empty() { - let config: AsstConfig = toml::from_str("").unwrap(); - assert_eq!( - config, - AsstConfig { + fn connection_config() { + assert_de_tokens( + &ConnectionConfig::ADB { + adb_path: default_adb_path(), + device: default_device(), + config: default_config(), + }, + &[ + Token::Map { len: Some(1) }, + Token::Str("type"), + Token::Str("ADB"), + Token::MapEnd, + ], + ); + + assert_de_tokens( + &ConnectionConfig::ADB { + adb_path: String::from("/path/to/adb"), + device: String::from("127.0.0.1:5555"), + config: String::from("SomeConfig"), + }, + &[ + Token::Map { len: Some(4) }, + Token::Str("type"), + Token::Str("ADB"), + Token::Str("adb_path"), + Token::Str("/path/to/adb"), + Token::Str("device"), + Token::Str("127.0.0.1:5555"), + Token::Str("config"), + Token::Str("SomeConfig"), + Token::MapEnd, + ], + ); + + assert_de_tokens( + &ConnectionConfig::PlayTools { + address: default_playcover_address(), + config: default_config(), + }, + &[ + Token::Map { len: Some(1) }, + Token::Str("type"), + Token::Str("PlayTools"), + Token::MapEnd, + ], + ); + + assert_de_tokens( + &ConnectionConfig::PlayTools { + address: String::from("localhost:7777"), + config: String::from("SomeConfig"), + }, + &[ + Token::Map { len: Some(3) }, + Token::Str("type"), + Token::Str("PlayTools"), + Token::Str("address"), + Token::Str("localhost:7777"), + Token::Str("config"), + Token::Str("SomeConfig"), + Token::MapEnd, + ], + ) + } + + #[test] + fn resource_config() { + assert_de_tokens( + &ResourceConfig { + resource_base_dirs: default_resource_base_dirs(), + global_resource: None, + platform_diff_resource: None, + user_resource: false, + }, + &[Token::Map { len: Some(0) }, Token::MapEnd], + ); + + let user_resource_dir = dirs::config().join("resource"); + user_resource_dir.ensure().unwrap(); + + assert_de_tokens( + &ResourceConfig { + resource_base_dirs: { + let mut base_dirs = default_resource_base_dirs(); + base_dirs.push(user_resource_dir.to_path_buf()); + base_dirs + }, + global_resource: Some(PathBuf::from("YoStarEN")), + platform_diff_resource: Some(PathBuf::from("iOS")), + user_resource: true, + }, + &[ + Token::Map { len: Some(4) }, + Token::Str("global_resource"), + Token::Some, + Token::Str("YoStarEN"), + Token::Str("platform_diff_resource"), + Token::Some, + Token::Str("iOS"), + Token::Str("user_resource"), + Token::Bool(true), + Token::MapEnd, + ], + ); + } + + #[test] + fn static_options() { + assert_de_tokens( + &StaticOptions { + cpu_ocr: None, + gpu_ocr: None, + }, + &[Token::Map { len: Some(0) }, Token::MapEnd], + ); + + assert_de_tokens( + &StaticOptions { + cpu_ocr: Some(false), + gpu_ocr: Some(1), + }, + &[ + Token::Map { len: Some(2) }, + Token::Str("cpu_ocr"), + Token::Some, + Token::Bool(false), + Token::Str("gpu_ocr"), + Token::Some, + Token::U32(1), + Token::MapEnd, + ], + ); + } + + #[test] + fn instance_options() { + assert_de_tokens( + &InstanceOptions { + touch_mode: None, + deployment_with_pause: None, + adb_lite_enabled: None, + kill_adb_on_exit: None, + }, + &[Token::Map { len: Some(0) }, Token::MapEnd], + ); + + assert_de_tokens( + &InstanceOptions { + touch_mode: Some(TouchMode::ADB), + deployment_with_pause: Some(false), + adb_lite_enabled: Some(false), + kill_adb_on_exit: Some(false), + }, + &[ + Token::Map { len: Some(4) }, + Token::Str("touch_mode"), + Token::Some, + Token::UnitVariant { + name: "TouchMode", + variant: "ADB", + }, + Token::Str("deployment_with_pause"), + Token::Some, + Token::Bool(false), + Token::Str("adb_lite_enabled"), + Token::Some, + Token::Bool(false), + Token::Str("kill_adb_on_exit"), + Token::Some, + Token::Bool(false), + Token::MapEnd, + ], + ); + } + + #[test] + fn asst_config() { + assert_de_tokens( + &AsstConfig { connection: ConnectionConfig::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") - }, + adb_path: default_adb_path(), + device: default_device(), + config: default_config(), }, resource: ResourceConfig { resource_base_dirs: default_resource_base_dirs(), @@ -529,8 +709,258 @@ mod tests { adb_lite_enabled: None, kill_adb_on_exit: None, }, + }, + &[Token::Map { len: Some(0) }, Token::MapEnd], + ); + + // Auto load iOS resource and set touch mode to MacPlayTools + assert_de_tokens( + &AsstConfig { + connection: ConnectionConfig::PlayTools { + address: default_playcover_address(), + config: default_config(), + }, + resource: ResourceConfig { + platform_diff_resource: Some(PathBuf::from("iOS")), + ..Default::default() + }, + static_options: Default::default(), + instance_options: InstanceOptions { + touch_mode: Some(TouchMode::MacPlayTools), + ..Default::default() + }, + }, + &[ + Token::Map { len: Some(1) }, + Token::Str("connection"), + Token::Map { len: Some(1) }, + Token::Str("type"), + Token::Str("PlayTools"), + Token::MapEnd, + Token::MapEnd, + ], + ); + } + } + + mod connection_config { + use super::*; + + use crate::assert_matches; + + #[test] + fn default() { + assert_matches!( + ConnectionConfig::default(), + ConnectionConfig::ADB { + adb_path, + device, + config, + } if adb_path == default_adb_path() + && device == default_device() + && config == default_config() + ); + } + + #[test] + fn set_address() { + assert_matches!( + ConnectionConfig::default().set_address("127.0.0.1:5555"), + ConnectionConfig::ADB { + device, + .. + } if device == "127.0.0.1:5555" + ); + + assert_matches!( + ConnectionConfig::PlayTools { + address: "localhost:7777".to_owned(), + config: default_config(), + }, + ConnectionConfig::PlayTools { + address, + .. + } if address == "localhost:7777" + ) + } + + #[test] + fn connect_args() { + assert_eq!( + ConnectionConfig::ADB { + adb_path: "adb".to_owned(), + device: "emulator-5554".to_owned(), + config: "General".to_owned(), + } + .connect_args(), + ("adb", "emulator-5554", "General") + ); + + assert_eq!( + ConnectionConfig::PlayTools { + address: "localhost:7777".to_owned(), + config: "SomeConfig".to_owned(), + } + .connect_args(), + (EMPTY_STR, "localhost:7777", "SomeConfig") + ); + } + } + + mod resource_config { + use super::*; + + use crate::{assert_matches, dirs::Ensure}; + + use std::{env::temp_dir, fs}; + + #[test] + fn default() { + assert_eq!( + ResourceConfig::default(), + ResourceConfig { + resource_base_dirs: default_resource_base_dirs(), + global_resource: None, + platform_diff_resource: None, + user_resource: false, + } + ); + } + + #[test] + fn use_user_resource() { + let user_resource_dir = dirs::config().join("resource"); + user_resource_dir.ensure().unwrap(); + + assert_eq!( + *ResourceConfig::default().use_user_resource(), + ResourceConfig { + resource_base_dirs: { + let mut base_dirs = default_resource_base_dirs(); + base_dirs.push(user_resource_dir.to_path_buf()); + base_dirs + }, + global_resource: None, + platform_diff_resource: None, + user_resource: true, } ); } + + #[test] + fn use_global_resource() { + assert_eq!( + *ResourceConfig::default().use_global_resource("YoStarEN"), + ResourceConfig { + resource_base_dirs: default_resource_base_dirs(), + global_resource: Some(PathBuf::from("YoStarEN")), + platform_diff_resource: None, + user_resource: false, + } + ); + + assert_eq!( + *ResourceConfig::default() + .use_global_resource("YoStarEN") + .use_global_resource("YostarJP"), + ResourceConfig { + resource_base_dirs: default_resource_base_dirs(), + global_resource: Some(PathBuf::from("YoStarEN")), + platform_diff_resource: None, + user_resource: false, + } + ); + } + + #[test] + fn use_platform_diff_resource() { + assert_matches!( + ResourceConfig::default().use_platform_diff_resource("iOS"), + ResourceConfig { + platform_diff_resource: Some(path), + .. + } if *path == PathBuf::from("iOS") + ); + } + + #[test] + fn base_dirs() { + assert_eq!( + *ResourceConfig { + resource_base_dirs: vec![PathBuf::from("resource")], + ..Default::default() + } + .base_dirs(), + [PathBuf::from("resource")] + ); + } + + #[test] + fn resource_dir() { + let test_root = temp_dir().join("resource_config"); + let resource_dir = test_root.join("resource"); + let yostar_en_dir = resource_dir + .join("global") + .join("YoStarEN") + .join("resource"); + let ios_dir = resource_dir + .join("platform_diff") + .join("iOS") + .join("resource"); + + yostar_en_dir.ensure().unwrap(); + ios_dir.ensure().unwrap(); + + assert_eq!( + ResourceConfig { + resource_base_dirs: vec![resource_dir.clone()], + ..Default::default() + } + .resource_dirs(), + [resource_dir.clone()] + ); + + assert_eq!( + ResourceConfig { + resource_base_dirs: vec![resource_dir.clone()], + global_resource: Some(PathBuf::from("YoStarEN")), + ..Default::default() + } + .resource_dirs(), + [resource_dir.clone(), yostar_en_dir.clone()] + ); + + assert_eq!( + ResourceConfig { + resource_base_dirs: vec![resource_dir.clone()], + global_resource: Some(PathBuf::from("NotExists")), + ..Default::default() + } + .resource_dirs(), + [resource_dir.clone()] + ); + + assert_eq!( + ResourceConfig { + resource_base_dirs: vec![resource_dir.clone()], + platform_diff_resource: Some(PathBuf::from("iOS")), + ..Default::default() + } + .resource_dirs(), + [resource_dir.clone(), ios_dir.clone()] + ); + + assert_eq!( + ResourceConfig { + resource_base_dirs: vec![resource_dir.clone()], + platform_diff_resource: Some(PathBuf::from("NotExists")), + ..Default::default() + } + .resource_dirs(), + [resource_dir.clone()] + ); + + fs::remove_dir_all(test_root).unwrap(); + } } } diff --git a/maa-cli/src/config/cli/maa_cli.rs b/maa-cli/src/config/cli/maa_cli.rs index 0d09721f..3f57dda5 100644 --- a/maa-cli/src/config/cli/maa_cli.rs +++ b/maa-cli/src/config/cli/maa_cli.rs @@ -32,7 +32,7 @@ impl Config { self.channel } - pub fn set_channel(&mut self, channel: Channel) -> &Self { + pub fn set_channel(&mut self, channel: Channel) -> &mut Self { self.channel = channel; self } @@ -41,7 +41,7 @@ impl Config { format!("{}{}.json", normalize_url(&self.api_url), self.channel()) } - pub fn set_api_url(&mut self, api_url: impl ToString) -> &Self { + pub fn set_api_url(&mut self, api_url: impl ToString) -> &mut Self { self.api_url = api_url.to_string(); self } @@ -50,7 +50,7 @@ impl Config { format!("{}{}/{}", normalize_url(&self.download_url), tag, name) } - pub fn set_download_url(&mut self, download_url: impl ToString) -> &Self { + pub fn set_download_url(&mut self, download_url: impl ToString) -> &mut Self { self.download_url = download_url.to_string(); self } @@ -205,6 +205,13 @@ pub mod tests { "https://github.com/MaaAssistantArknights/maa-cli/raw/version/stable.json", ); + assert_eq!( + Config::default() + .set_api_url("https://foo.bar/cli/") + .api_url(), + "https://foo.bar/cli/stable.json", + ); + assert_eq!( Config { channel: Channel::Alpha, @@ -247,5 +254,31 @@ pub mod tests { &CLIComponents { binary: false }, ); } + + #[test] + fn with_args() { + assert_eq!( + Config::default().with_args(&CommonArgs { + channel: None, + api_url: None, + download_url: None, + }), + Config::default(), + ); + + assert_eq!( + Config::default().with_args(&CommonArgs { + channel: Some(Channel::Alpha), + api_url: Some("https://foo.bar/api/".to_string()), + download_url: Some("https://foo.bar/download/".to_string()), + }), + Config { + channel: Channel::Alpha, + api_url: "https://foo.bar/api/".to_string(), + download_url: "https://foo.bar/download/".to_string(), + ..Default::default() + }, + ); + } } } diff --git a/maa-cli/src/config/task/client_type.rs b/maa-cli/src/config/task/client_type.rs index 7159961e..ab7db712 100644 --- a/maa-cli/src/config/task/client_type.rs +++ b/maa-cli/src/config/task/client_type.rs @@ -1,17 +1,25 @@ use serde::Deserialize; #[cfg_attr(test, derive(Debug, PartialEq))] -#[derive(Clone, Copy, Deserialize)] +#[derive(Clone, Copy, Default)] pub enum ClientType { + #[default] Official, Bilibili, - #[serde(alias = "txwy", alias = "TXWY")] Txwy, YoStarEN, YoStarJP, YoStarKR, } +impl<'de> Deserialize<'de> for ClientType { + fn deserialize>(deserializer: D) -> Result { + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) + } +} + impl ClientType { pub fn resource(self) -> Option<&'static str> { match self { @@ -22,6 +30,15 @@ impl ClientType { _ => None, } } + + pub fn app(self) -> &'static str { + match self { + ClientType::Official | ClientType::Bilibili | ClientType::Txwy => "明日方舟", + ClientType::YoStarEN => "Arknights", + ClientType::YoStarJP => "アークナイツ", + ClientType::YoStarKR => "명일방주", + } + } } impl AsRef for ClientType { @@ -50,7 +67,7 @@ impl std::str::FromStr for ClientType { match s { "Official" | "" => Ok(ClientType::Official), "Bilibili" => Ok(ClientType::Bilibili), - "txwy" => Ok(ClientType::Txwy), + "Txwy" | "TXWY" | "txwy" => Ok(ClientType::Txwy), "YoStarEN" => Ok(ClientType::YoStarEN), "YoStarJP" => Ok(ClientType::YoStarJP), "YoStarKR" => Ok(ClientType::YoStarKR), @@ -59,6 +76,14 @@ impl std::str::FromStr for ClientType { } } +impl TryFrom<&str> for ClientType { + type Error = Error; + + fn try_from(s: &str) -> Result { + s.parse() + } +} + #[derive(Debug)] pub enum Error { UnknownClientType, @@ -76,17 +101,65 @@ impl std::error::Error for Error {} #[cfg(test)] mod tests { + use crate::assert_matches; + use super::*; + use serde_test::{assert_de_tokens, Token}; + + impl ClientType { + fn to_token(self) -> Token { + match self { + ClientType::Official => Token::Str("Official"), + ClientType::Bilibili => Token::Str("Bilibili"), + ClientType::Txwy => Token::Str("txwy"), + ClientType::YoStarEN => Token::Str("YoStarEN"), + ClientType::YoStarJP => Token::Str("YoStarJP"), + ClientType::YoStarKR => Token::Str("YoStarKR"), + } + } + } + #[test] - fn parse_client() { - assert_eq!(ClientType::Official, "Official".parse().unwrap()); - assert_eq!(ClientType::Official, "".parse().unwrap()); - assert_eq!(ClientType::Bilibili, "Bilibili".parse().unwrap()); - assert_eq!(ClientType::Txwy, "txwy".parse().unwrap()); - assert_eq!(ClientType::YoStarEN, "YoStarEN".parse().unwrap()); - assert_eq!(ClientType::YoStarJP, "YoStarJP".parse().unwrap()); - assert_eq!(ClientType::YoStarKR, "YoStarKR".parse().unwrap()); + fn deserialize() { + assert_de_tokens(&ClientType::Official, &[ClientType::Official.to_token()]); + assert_de_tokens(&ClientType::Bilibili, &[ClientType::Bilibili.to_token()]); + assert_de_tokens(&ClientType::Txwy, &[ClientType::Txwy.to_token()]); + assert_de_tokens(&ClientType::YoStarEN, &[ClientType::YoStarEN.to_token()]); + assert_de_tokens(&ClientType::YoStarJP, &[ClientType::YoStarJP.to_token()]); + assert_de_tokens(&ClientType::YoStarKR, &[ClientType::YoStarKR.to_token()]); + } + + #[test] + fn parse() { + assert_matches!("".parse::().unwrap(), ClientType::Official); + assert_matches!( + "Official".parse::().unwrap(), + ClientType::Official + ); + assert_matches!( + "Bilibili".parse::().unwrap(), + ClientType::Bilibili + ); + assert_matches!("txwy".parse::().unwrap(), ClientType::Txwy); + assert_matches!("TXWY".parse::().unwrap(), ClientType::Txwy); + assert_matches!( + "YoStarEN".parse::().unwrap(), + ClientType::YoStarEN + ); + assert_matches!( + "YoStarJP".parse::().unwrap(), + ClientType::YoStarJP + ); + assert_matches!( + "YoStarKR".parse::().unwrap(), + ClientType::YoStarKR + ); + + assert_matches!( + "UnknownClientType".parse::().unwrap_err(), + Error::UnknownClientType, + ); } #[test] @@ -98,4 +171,24 @@ mod tests { assert_eq!(ClientType::YoStarJP.resource(), Some("YoStarJP")); assert_eq!(ClientType::YoStarKR.resource(), Some("YoStarKR")); } + + #[test] + fn client_to_app() { + assert_eq!(ClientType::Official.app(), "明日方舟"); + assert_eq!(ClientType::Bilibili.app(), "明日方舟"); + assert_eq!(ClientType::Txwy.app(), "明日方舟"); + assert_eq!(ClientType::YoStarEN.app(), "Arknights"); + assert_eq!(ClientType::YoStarJP.app(), "アークナイツ"); + assert_eq!(ClientType::YoStarKR.app(), "명일방주"); + } + + #[test] + fn client_to_string() { + assert_eq!(ClientType::Official.to_string(), "Official"); + assert_eq!(ClientType::Bilibili.to_string(), "Bilibili"); + assert_eq!(ClientType::Txwy.to_string(), "txwy"); + assert_eq!(ClientType::YoStarEN.to_string(), "YoStarEN"); + assert_eq!(ClientType::YoStarJP.to_string(), "YoStarJP"); + assert_eq!(ClientType::YoStarKR.to_string(), "YoStarKR"); + } } diff --git a/maa-cli/src/config/task/mod.rs b/maa-cli/src/config/task/mod.rs index d72cc0cf..b7c8ae8a 100644 --- a/maa-cli/src/config/task/mod.rs +++ b/maa-cli/src/config/task/mod.rs @@ -10,11 +10,9 @@ pub use client_type::ClientType; mod condition; use condition::Condition; -use super::FindFile; - use crate::{dirs, object}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use anyhow::Context; use serde::Deserialize; @@ -158,15 +156,6 @@ impl TaskConfig { } } - pub fn from_file(path: impl AsRef) -> Result { - let path = path.as_ref(); - if let Some(abs_path) = dirs::config_path(path, Some("tasks")) { - TaskConfig::find_file(abs_path) - } else { - TaskConfig::find_file(path) - } - } - pub fn push(&mut self, task: Task) { self.tasks.push(task); } @@ -240,7 +229,7 @@ impl TaskConfig { if let Some(v) = params.get("filename") { let file = PathBuf::from(v.as_string()?); let sub_dir = task_type.as_ref().to_lowercase(); - if let Some(path) = dirs::config_path(file, Some(sub_dir)) { + if let Some(path) = dirs::abs_config(file, Some(sub_dir)) { params.insert("filename", path.to_str().context("Invilid UTF-8")?) } } @@ -251,15 +240,20 @@ impl TaskConfig { } if prepend_startup { - let mut params = object!("enable" => true, "start_game_enabled" => true); - if let Some(client_type) = self.client_type { - params.insert("client_type", client_type.to_string()); - }; - tasks.insert(0, (MAATask::StartUp.into(), params)) + tasks.insert( + 0, + ( + MAATask::StartUp.into(), + object!( + "start_game_enabled" => true, + "client_type" => self.client_type.unwrap_or_default().to_string(), + ), + ), + ); } if append_closedown { - tasks.push((MAATask::CloseDown.into(), object!("enable" => true))); + tasks.push((MAATask::CloseDown.into(), object!())); } Ok(InitializedTaskConfig { @@ -273,29 +267,12 @@ impl TaskConfig { impl super::FromFile for TaskConfig {} +#[cfg_attr(test, derive(PartialEq, Debug))] pub struct InitializedTaskConfig { - client_type: Option, - start_app: bool, - close_app: bool, - tasks: Vec<(TaskOrUnknown, Value)>, -} - -impl InitializedTaskConfig { - pub fn client_type(&self) -> Option { - self.client_type - } - - pub fn start_app(&self) -> bool { - self.start_app - } - - pub fn close_app(&self) -> bool { - self.close_app - } - - pub fn tasks(&self) -> &[(TaskOrUnknown, Value)] { - &self.tasks - } + pub client_type: Option, + pub start_app: bool, + pub close_app: bool, + pub tasks: Vec<(TaskOrUnknown, Value)>, } #[cfg(test)] @@ -531,35 +508,45 @@ mod tests { } } - mod deserialize_example { + mod task_config { use super::*; - use value::input::{BoolInput, Input, Select}; - - use chrono::{NaiveDateTime, NaiveTime, TimeZone, Weekday}; - - fn naive_local_datetime(y: i32, m: u32, d: u32, h: u32, mi: u32, s: u32) -> NaiveDateTime { - chrono::Local - .with_ymd_and_hms(y, m, d, h, mi, s) - .unwrap() - .naive_local() - } + mod serde { + use super::*; + + use value::input::{BoolInput, Input, Select}; + + use chrono::{NaiveDateTime, NaiveTime, TimeZone, Weekday}; + + fn naive_local_datetime( + y: i32, + m: u32, + d: u32, + h: u32, + mi: u32, + s: u32, + ) -> NaiveDateTime { + chrono::Local + .with_ymd_and_hms(y, m, d, h, mi, s) + .unwrap() + .naive_local() + } - fn example_task_config() -> TaskConfig { - let mut task_list = TaskConfig::new(); + fn example_task_config() -> TaskConfig { + let mut task_list = TaskConfig::new(); - task_list.push(Task::new_with_default( - MAATask::StartUp, - object!( - "client_type" => "Official", - "start_game_enabled" => BoolInput::new( - Some(true), - Some("start the game"), + task_list.push(Task::new_with_default( + MAATask::StartUp, + object!( + "client_type" => "Official", + "start_game_enabled" => BoolInput::new( + Some(true), + Some("start the game"), + ), ), - ), - )); + )); - task_list.push(Task::new( + task_list.push(Task::new( MAATask::Fight, object!(), Strategy::Merge, @@ -604,61 +591,162 @@ mod tests { ], )); - task_list.push(Task::new( - MAATask::Mall, - object!( - "shopping" => true, - "credit_fight" => true, - "buy_first" => [ - "招聘许可", - "龙门币", - ], - "blacklist" => [ - "碳", - "家具", - "加急许可", - ], - ), - Strategy::default(), - vec![TaskVariant { - condition: Condition::Time { - start: Some(NaiveTime::from_hms_opt(16, 0, 0).unwrap()), - end: None, - }, - params: object!(), - }], - )); + task_list.push(Task::new( + MAATask::Mall, + object!( + "shopping" => true, + "credit_fight" => true, + "buy_first" => [ + "招聘许可", + "龙门币", + ], + "blacklist" => [ + "碳", + "家具", + "加急许可", + ], + ), + Strategy::default(), + vec![TaskVariant { + condition: Condition::Time { + start: Some(NaiveTime::from_hms_opt(16, 0, 0).unwrap()), + end: None, + }, + params: object!(), + }], + )); - task_list.push(Task::new_with_default(MAATask::CloseDown, object!())); + task_list.push(Task::new_with_default(MAATask::CloseDown, object!())); - task_list - } + task_list + } - #[test] - fn json() { - let task_config: TaskConfig = serde_json::from_reader( - std::fs::File::open("../config_examples/tasks/daily.json").unwrap(), - ) - .unwrap(); - assert_eq!(task_config.tasks, example_task_config().tasks) - } + #[test] + fn json() { + let task_config: TaskConfig = serde_json::from_reader( + std::fs::File::open("../config_examples/tasks/daily.json").unwrap(), + ) + .unwrap(); + assert_eq!(task_config.tasks, example_task_config().tasks) + } - #[test] - fn toml() { - let task_config: TaskConfig = toml::from_str( - &std::fs::read_to_string("../config_examples/tasks/daily.toml").unwrap(), - ) - .unwrap(); - assert_eq!(task_config.tasks, example_task_config().tasks) + #[test] + fn toml() { + let task_config: TaskConfig = toml::from_str( + &std::fs::read_to_string("../config_examples/tasks/daily.toml").unwrap(), + ) + .unwrap(); + assert_eq!(task_config.tasks, example_task_config().tasks) + } + + #[test] + fn yaml() { + let task_config: TaskConfig = serde_yaml::from_reader( + std::fs::File::open("../config_examples/tasks/daily.yml").unwrap(), + ) + .unwrap(); + assert_eq!(task_config.tasks, example_task_config().tasks) + } } #[test] - fn yaml() { - let task_config: TaskConfig = serde_yaml::from_reader( - std::fs::File::open("../config_examples/tasks/daily.yml").unwrap(), + fn init() { + assert_eq!( + TaskConfig { + client_type: None, + startup: None, + closedown: None, + tasks: vec![ + Task::new_with_default( + MAATask::StartUp, + object!( + "client_type" => "Official", + "start_game_enabled" => true, + ), + ), + Task::new_with_default(MAATask::Fight, object!("stage" => "1-7")), + Task::new_with_default(MAATask::CloseDown, object!()), + ], + } + .init() + .unwrap(), + InitializedTaskConfig { + client_type: Some(ClientType::Official), + start_app: true, + close_app: true, + tasks: vec![ + ( + MAATask::StartUp.into(), + object!( + "client_type" => "Official", + "start_game_enabled" => true, + ) + ), + (MAATask::Fight.into(), object!("stage" => "1-7")), + (MAATask::CloseDown.into(), object!()), + ] + } + ); + + assert_eq!( + TaskConfig { + client_type: None, + startup: Some(true), + closedown: Some(true), + tasks: vec![Task::new_with_default( + MAATask::Fight, + object!("stage" => "1-7") + ),], + } + .init() + .unwrap(), + InitializedTaskConfig { + client_type: None, + start_app: true, + close_app: true, + tasks: vec![ + ( + MAATask::StartUp.into(), + object!( + "client_type" => "Official", + "start_game_enabled" => true, + ) + ), + (MAATask::Fight.into(), object!("stage" => "1-7")), + (MAATask::CloseDown.into(), object!()), + ] + }, + ); + + assert_eq!( + TaskConfig { + client_type: Some(ClientType::YoStarEN), + startup: Some(true), + closedown: Some(true), + tasks: vec![Task::new_with_default( + MAATask::Fight, + object!("stage" => "1-7") + ),], + } + .init() + .unwrap(), + InitializedTaskConfig { + client_type: Some(ClientType::YoStarEN), + start_app: true, + close_app: true, + tasks: vec![ + ( + MAATask::StartUp.into(), + object!( + "client_type" => "YoStarEN", + "start_game_enabled" => true, + ) + ), + (MAATask::Fight.into(), object!("stage" => "1-7")), + (MAATask::CloseDown.into(), object!()), + ] + } ) - .unwrap(); - assert_eq!(task_config.tasks, example_task_config().tasks) } } } diff --git a/maa-cli/src/dirs.rs b/maa-cli/src/dirs.rs index 8a30bc6a..4a2f3bfc 100644 --- a/maa-cli/src/dirs.rs +++ b/maa-cli/src/dirs.rs @@ -131,14 +131,14 @@ impl Dirs { &self.config } - /// Find a file in the config directory. + /// Get absolute path in config directory. /// - /// If the path is absolute, return None. + /// If the given path is absolute, return `None`. /// Otherwise, return the path in the config directory. /// The `sub_dir` is the sub directory of the config directory. /// If `sub_dir` is `None`, the path is relative to the config directory. /// Otherwise, the path is relative to the `sub_dir` directory. - pub fn config_path, D: AsRef>( + pub fn abs_config, D: AsRef>( &self, path: P, sub_dir: Option, @@ -247,8 +247,8 @@ pub fn config() -> &'static Path { DIRS.config() } -pub fn config_path, D: AsRef>(path: P, sub_dir: Option) -> Option { - DIRS.config_path(path, sub_dir) +pub fn abs_config, D: AsRef>(path: P, sub_dir: Option) -> Option { + DIRS.abs_config(path, sub_dir) } pub fn cache() -> &'static Path { @@ -574,7 +574,7 @@ mod tests { } #[test] - fn config_dir() { + fn config_relative() { env::remove_var("XDG_CONFIG_HOME"); let project = ProjectDirs::from("com", "loong", "maa"); if cfg!(target_os = "macos") { @@ -586,23 +586,23 @@ mod tests { assert_eq!(TEST_DIRS.config(), HOME.join(".config/maa")); } assert_eq!( - TEST_DIRS.config_path::<&str, &str>("foo", None).unwrap(), + TEST_DIRS.abs_config::<&str, &str>("foo", None).unwrap(), TEST_DIRS.config().join("foo") ); assert_eq!( - TEST_DIRS.config_path("foo", Some("bar")).unwrap(), + TEST_DIRS.abs_config("foo", Some("bar")).unwrap(), TEST_DIRS.config().join("bar").join("foo") ); #[cfg(unix)] { - assert_eq!(TEST_DIRS.config_path::<&str, &str>("/tmp", None), None); - assert_eq!(TEST_DIRS.config_path("/tmp", Some("bar")), None); + assert_eq!(TEST_DIRS.abs_config::<&str, &str>("/tmp", None), None); + assert_eq!(TEST_DIRS.abs_config("/tmp", Some("bar")), None); } assert_eq!(config(), TEST_DIRS.config()); assert_eq!( - config_path("foo", Some("bar")).unwrap(), + abs_config("foo", Some("bar")).unwrap(), config().join("bar").join("foo") ); diff --git a/maa-cli/src/run/mod.rs b/maa-cli/src/run/mod.rs index 26835b3d..2e49aaa0 100644 --- a/maa-cli/src/run/mod.rs +++ b/maa-cli/src/run/mod.rs @@ -13,13 +13,13 @@ pub use copilot::copilot; use crate::{ config::{ asst::{with_asst_config, with_mut_asst_config, AsstConfig}, - task::{InitializedTaskConfig, TaskConfig}, + task::TaskConfig, }, consts::MAA_CORE_LIB, + debug, dirs::{self, Ensure}, installer::resource, log::{set_level, LogLevel}, - {debug, warning}, }; use std::sync::{atomic, Arc}; @@ -97,7 +97,7 @@ where with_mut_asst_config(|config| args.apply_to(config)); let task = with_asst_config(f)?; let task_config = task.init()?; - if let Some(client_type) = task_config.client_type() { + if let Some(client_type) = task_config.client_type { debug!("Detected client type:", client_type.as_ref()); if let Some(resource) = client_type.resource() { with_mut_asst_config(|config| { @@ -121,7 +121,7 @@ where with_asst_config(|config| config.instance_options.apply_to(&asst))?; - for (task_type, params) in task_config.tasks() { + for (task_type, params) in task_config.tasks.iter() { debug!( format!("Adding task {} with params:", task_type.as_ref()), serde_json::to_string_pretty(params)? @@ -129,13 +129,16 @@ where asst.append_task(task_type, serde_json::to_string(params)?)?; } - let playcover = PlayCoverAppConfig::from(&task_config); + let playcover = PlayCoverApp::from(&task_config); if let Some(app) = playcover.as_ref() { app.open()?; } - with_asst_config(|config| config.connection.connect(&asst))?; + with_asst_config(|config| { + let (adb, addr, config) = config.connection.connect_args(); + asst.async_connect(adb, addr, config, true) + })?; asst.start()?; @@ -158,14 +161,32 @@ where Ok(()) } -pub fn run_custom(task: impl AsRef, args: CommonArgs) -> Result<()> { - run(|_| TaskConfig::from_file(task).map_err(Into::into), args) +pub fn run_custom(path: impl AsRef, args: CommonArgs) -> Result<()> { + run( + |_| { + use crate::config::FindFile; + + let path = path.as_ref(); + if let Some(abs_path) = dirs::abs_config(path, Some("tasks")) { + TaskConfig::find_file(abs_path) + } else { + TaskConfig::find_file(path) + } + .context("Failed to find task file!") + }, + args, + ) } -pub fn core_version<'a>() -> Result<&'a str> { +pub fn core_version<'a>() -> Result<&'a str, maa_sys::Error> { load_core(); - Ok(Assistant::get_version()?) + Assistant::get_version() + + // BUG: + // if we call maa_sys::binding::unload() here, + // program will crash with signal SIGSEGV (Address boundary error) + // So we don't unload MaaCore } fn load_core() { @@ -202,52 +223,6 @@ fn setup_core(config: &AsstConfig) -> Result<()> { Ok(()) } -struct PlayCoverAppConfig<'a> { - app: PlayCoverApp<'a>, - start_app: bool, - close_app: bool, -} - -impl PlayCoverAppConfig<'_> { - fn from(task_config: &InitializedTaskConfig) -> Option { - if task_config.start_app() || task_config.close_app() { - let app = if let Some(client_type) = task_config.client_type() { - let app = PlayCoverApp::from(client_type); - debug!("PlayCover app:", app.name()); - app - } else { - let app = PlayCoverApp::default(); - warning!( - "No client type specified,", - format!("using default app name {}", app.name()) - ); - app - }; - Some(Self { - app, - start_app: task_config.start_app(), - close_app: task_config.close_app(), - }) - } else { - None - } - } - - pub fn open(&self) -> Result<()> { - if self.start_app { - self.app.open()?; - } - Ok(()) - } - - pub fn close(&self) -> Result<()> { - if self.close_app { - self.app.close()?; - } - Ok(()) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/maa-cli/src/run/playcover.rs b/maa-cli/src/run/playcover.rs index 945ecaee..957ca81e 100644 --- a/maa-cli/src/run/playcover.rs +++ b/maa-cli/src/run/playcover.rs @@ -1,18 +1,25 @@ -use crate::{config::task::ClientType, info, warning}; +use crate::{config::task::InitializedTaskConfig, info, warning}; use anyhow::{Context, Result}; -pub struct PlayCoverApp<'n> { - name: &'n str, +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct PlayCoverApp<'a> { + name: &'a str, + start_app: bool, + close_app: bool, } impl<'n> PlayCoverApp<'n> { - pub fn new(name: &'n str) -> Self { - Self { name } - } - - pub fn name(&self) -> &'n str { - self.name + pub fn from(task_config: &InitializedTaskConfig) -> Option { + if task_config.start_app || task_config.close_app { + Some(Self { + name: task_config.client_type.unwrap_or_default().app(), + start_app: task_config.start_app, + close_app: task_config.close_app, + }) + } else { + None + } } fn is_running(&self) -> Result { @@ -26,12 +33,16 @@ impl<'n> PlayCoverApp<'n> { } pub fn open(&self) -> Result<()> { + if !self.start_app { + return Ok(()); + } + if self.is_running().unwrap_or(false) { info!("Game is already running!"); return Ok(()); } - info!("Starting game..."); + info!("Starting app: {}", self.name); std::process::Command::new("open") .arg("-a") .arg(self.name) @@ -48,6 +59,10 @@ impl<'n> PlayCoverApp<'n> { } pub fn close(&self) -> Result<()> { + if !self.close_app { + return Ok(()); + } + if !self.is_running().unwrap_or(true) { warning!("Game is not running!"); return Ok(()); @@ -63,37 +78,64 @@ impl<'n> PlayCoverApp<'n> { } } -impl Default for PlayCoverApp<'static> { - fn default() -> Self { - Self::new("明日方舟") - } -} - -impl From for PlayCoverApp<'static> { - fn from(client: ClientType) -> Self { - Self::new(match client { - ClientType::Official | ClientType::Bilibili | ClientType::Txwy => "明日方舟", - ClientType::YoStarEN => "Arknights", - ClientType::YoStarJP => "アークナイツ", - ClientType::YoStarKR => "명일방주", - }) - } -} - #[cfg(test)] mod tests { use super::*; + use crate::config::task::ClientType; + #[test] - fn from_client_type() { - assert_eq!(PlayCoverApp::from(ClientType::Official).name, "明日方舟"); - assert_eq!(PlayCoverApp::from(ClientType::Bilibili).name, "明日方舟"); - assert_eq!(PlayCoverApp::from(ClientType::Txwy).name, "明日方舟"); - assert_eq!(PlayCoverApp::from(ClientType::YoStarEN).name, "Arknights"); + fn from() { + assert_eq!( + PlayCoverApp::from(&InitializedTaskConfig { + start_app: true, + close_app: true, + client_type: Some(ClientType::Official), + tasks: vec![], + }), + Some(PlayCoverApp { + name: "明日方舟", + start_app: true, + close_app: true, + }) + ); + + assert_eq!( + PlayCoverApp::from(&InitializedTaskConfig { + start_app: true, + close_app: false, + client_type: None, + tasks: vec![], + }), + Some(PlayCoverApp { + name: "明日方舟", + start_app: true, + close_app: false, + }) + ); + + assert_eq!( + PlayCoverApp::from(&InitializedTaskConfig { + start_app: true, + close_app: false, + client_type: Some(ClientType::YoStarEN), + tasks: vec![], + }), + Some(PlayCoverApp { + name: "Arknights", + start_app: true, + close_app: false, + }) + ); + assert_eq!( - PlayCoverApp::from(ClientType::YoStarJP).name, - "アークナイツ" + PlayCoverApp::from(&InitializedTaskConfig { + start_app: false, + close_app: false, + client_type: Some(ClientType::Official), + tasks: vec![], + }), + None ); - assert_eq!(PlayCoverApp::from(ClientType::YoStarKR).name, "명일방주"); } }