use crate::sys;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct MaaStatus(pub i32);
pub type MaaId = i64;
impl MaaStatus {
pub const INVALID: Self = Self(sys::MaaStatusEnum_MaaStatus_Invalid as i32);
pub const PENDING: Self = Self(sys::MaaStatusEnum_MaaStatus_Pending as i32);
pub const RUNNING: Self = Self(sys::MaaStatusEnum_MaaStatus_Running as i32);
pub const SUCCEEDED: Self = Self(sys::MaaStatusEnum_MaaStatus_Succeeded as i32);
pub const FAILED: Self = Self(sys::MaaStatusEnum_MaaStatus_Failed as i32);
pub fn is_success(&self) -> bool {
*self == Self::SUCCEEDED
}
pub fn succeeded(&self) -> bool {
*self == Self::SUCCEEDED
}
pub fn is_failed(&self) -> bool {
*self == Self::FAILED
}
pub fn failed(&self) -> bool {
*self == Self::FAILED
}
pub fn done(&self) -> bool {
*self == Self::SUCCEEDED || *self == Self::FAILED
}
pub fn pending(&self) -> bool {
*self == Self::PENDING
}
pub fn running(&self) -> bool {
*self == Self::RUNNING
}
}
impl fmt::Display for MaaStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::INVALID => write!(f, "Invalid"),
Self::PENDING => write!(f, "Pending"),
Self::RUNNING => write!(f, "Running"),
Self::SUCCEEDED => write!(f, "Succeeded"),
Self::FAILED => write!(f, "Failed"),
_ => write!(f, "Unknown({})", self.0),
}
}
}
pub fn check_bool(ret: sys::MaaBool) -> crate::MaaResult<()> {
if ret != 0 {
Ok(())
} else {
Err(crate::MaaError::FrameworkError(0))
}
}
impl From<i32> for MaaStatus {
fn from(value: i32) -> Self {
Self(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
#[serde(from = "RectDef")]
pub struct Rect {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
impl Serialize for Rect {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
(self.x, self.y, self.width, self.height).serialize(serializer)
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum RectDef {
Map {
x: i32,
y: i32,
#[serde(alias = "w")]
width: i32,
#[serde(alias = "h")]
height: i32,
},
Array(i32, i32, i32, i32),
}
impl From<RectDef> for Rect {
fn from(def: RectDef) -> Self {
match def {
RectDef::Map {
x,
y,
width,
height,
} => Rect {
x,
y,
width,
height,
},
RectDef::Array(x, y, w, h) => Rect {
x,
y,
width: w,
height: h,
},
}
}
}
impl From<(i32, i32, i32, i32)> for Rect {
fn from(tuple: (i32, i32, i32, i32)) -> Self {
Self {
x: tuple.0,
y: tuple.1,
width: tuple.2,
height: tuple.3,
}
}
}
impl From<sys::MaaRect> for Rect {
fn from(r: sys::MaaRect) -> Self {
Self {
x: r.x,
y: r.y,
width: r.width,
height: r.height,
}
}
}
impl Rect {
pub fn to_tuple(&self) -> (i32, i32, i32, i32) {
(self.x, self.y, self.width, self.height)
}
}
impl PartialEq<(i32, i32, i32, i32)> for Rect {
fn eq(&self, other: &(i32, i32, i32, i32)) -> bool {
self.x == other.0 && self.y == other.1 && self.width == other.2 && self.height == other.3
}
}
impl PartialEq<Rect> for (i32, i32, i32, i32) {
fn eq(&self, other: &Rect) -> bool {
self.0 == other.x && self.1 == other.y && self.2 == other.width && self.3 == other.height
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct Point {
pub x: i32,
pub y: i32,
}
impl Point {
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u64)]
#[non_exhaustive]
pub enum GamepadType {
Xbox360 = 0,
DualShock4 = 1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
#[non_exhaustive]
pub enum GamepadContact {
LeftStick = 0,
RightStick = 1,
LeftTrigger = 2,
RightTrigger = 3,
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GamepadButton: u32 {
const DPAD_UP = 0x0001;
const DPAD_DOWN = 0x0002;
const DPAD_LEFT = 0x0004;
const DPAD_RIGHT = 0x0008;
const START = 0x0010;
const BACK = 0x0020;
const LEFT_THUMB = 0x0040; const RIGHT_THUMB = 0x0080;
const LB = 0x0100; const RB = 0x0200;
const GUIDE = 0x0400;
const A = 0x1000;
const B = 0x2000;
const X = 0x4000;
const Y = 0x8000;
const PS = 0x10000;
const TOUCHPAD = 0x20000;
}
}
impl GamepadButton {
pub const CROSS: Self = Self::A;
pub const CIRCLE: Self = Self::B;
pub const SQUARE: Self = Self::X;
pub const TRIANGLE: Self = Self::Y;
pub const L1: Self = Self::LB;
pub const R1: Self = Self::RB;
pub const L3: Self = Self::LEFT_THUMB;
pub const R3: Self = Self::RIGHT_THUMB;
pub const OPTIONS: Self = Self::START;
pub const SHARE: Self = Self::BACK;
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ControllerFeature: u64 {
const USE_MOUSE_DOWN_UP_INSTEAD_OF_CLICK = 1;
const USE_KEY_DOWN_UP_INSTEAD_OF_CLICK = 1 << 1;
const NO_SCALING_TOUCH_POINTS = 1 << 2;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AndroidScreenResolution {
pub width: i32,
pub height: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AndroidNativeControllerConfig {
pub library_path: String,
pub screen_resolution: AndroidScreenResolution,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_id: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub force_stop: Option<bool>,
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AdbScreencapMethod: u64 {
const ENCODE_TO_FILE_AND_PULL = 1;
const ENCODE = 1 << 1;
const RAW_WITH_GZIP = 1 << 2;
const RAW_BY_NETCAT = 1 << 3;
const MINICAP_DIRECT = 1 << 4;
const MINICAP_STREAM = 1 << 5;
const EMULATOR_EXTRAS = 1 << 6;
const ALL = !0;
}
}
impl AdbScreencapMethod {
pub const DEFAULT: Self = Self::from_bits_truncate(
Self::ALL.bits()
& !Self::RAW_BY_NETCAT.bits()
& !Self::MINICAP_DIRECT.bits()
& !Self::MINICAP_STREAM.bits(),
);
}
impl Default for AdbScreencapMethod {
fn default() -> Self {
Self::DEFAULT
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AdbInputMethod: u64 {
const ADB_SHELL = 1;
const MINITOUCH_AND_ADB_KEY = 1 << 1;
const MAATOUCH = 1 << 2;
const EMULATOR_EXTRAS = 1 << 3;
const ALL = !0;
}
}
impl AdbInputMethod {
pub const DEFAULT: Self =
Self::from_bits_truncate(Self::ALL.bits() & !Self::EMULATOR_EXTRAS.bits());
}
impl Default for AdbInputMethod {
fn default() -> Self {
Self::DEFAULT
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Win32ScreencapMethod: u64 {
const GDI = 1;
const FRAME_POOL = 1 << 1;
const DXGI_DESKTOP_DUP = 1 << 2;
const DXGI_DESKTOP_DUP_WINDOW = 1 << 3;
const PRINT_WINDOW = 1 << 4;
const SCREEN_DC = 1 << 5;
const ALL = !0;
const FOREGROUND = Self::DXGI_DESKTOP_DUP_WINDOW.bits() | Self::SCREEN_DC.bits();
const BACKGROUND = Self::FRAME_POOL.bits() | Self::PRINT_WINDOW.bits();
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Win32InputMethod: u64 {
const SEIZE = 1;
const SEND_MESSAGE = 1 << 1;
const POST_MESSAGE = 1 << 2;
const LEGACY_EVENT = 1 << 3;
const POST_THREAD_MESSAGE = 1 << 4;
const SEND_MESSAGE_WITH_CURSOR_POS = 1 << 5;
const POST_MESSAGE_WITH_CURSOR_POS = 1 << 6;
const SEND_MESSAGE_WITH_WINDOW_POS = 1 << 7;
const POST_MESSAGE_WITH_WINDOW_POS = 1 << 8;
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RecognitionDetail {
pub node_name: String,
pub algorithm: AlgorithmEnum,
pub hit: bool,
pub box_rect: Rect,
pub detail: serde_json::Value,
#[serde(skip)]
pub raw_image: Option<Vec<u8>>,
#[serde(skip)]
pub draw_images: Vec<Vec<u8>>,
#[serde(default)]
pub sub_details: Vec<RecognitionDetail>,
}
impl RecognitionDetail {
pub fn as_template_match_result(&self) -> Option<TemplateMatchResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_feature_match_result(&self) -> Option<FeatureMatchResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_color_match_result(&self) -> Option<ColorMatchResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_ocr_result(&self) -> Option<OCRResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_neural_network_result(&self) -> Option<NeuralNetworkResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_custom_result(&self) -> Option<CustomRecognitionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ActionDetail {
pub node_name: String,
pub action: ActionEnum,
pub box_rect: Rect,
pub success: bool,
pub detail: serde_json::Value,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct WaitFreezesDetail {
pub wf_id: MaaId,
pub name: String,
pub phase: String,
pub success: bool,
pub elapsed_ms: u64,
#[serde(default)]
pub reco_id_list: Vec<MaaId>,
pub roi: Rect,
}
impl ActionDetail {
pub fn as_click_result(&self) -> Option<ClickActionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_long_press_result(&self) -> Option<LongPressActionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_swipe_result(&self) -> Option<SwipeActionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_multi_swipe_result(&self) -> Option<MultiSwipeActionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_click_key_result(&self) -> Option<ClickKeyActionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_input_text_result(&self) -> Option<InputTextActionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_app_result(&self) -> Option<AppActionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_scroll_result(&self) -> Option<ScrollActionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_touch_result(&self) -> Option<TouchActionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
pub fn as_shell_result(&self) -> Option<ShellActionResult> {
serde_json::from_value(self.detail.clone()).ok()
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ClickActionResult {
pub point: Point,
pub contact: i32,
#[serde(default)]
pub pressure: i32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LongPressActionResult {
pub point: Point,
pub duration: i32,
pub contact: i32,
#[serde(default)]
pub pressure: i32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SwipeActionResult {
pub begin: Point,
pub end: Vec<Point>,
#[serde(default)]
pub end_hold: Vec<i32>,
#[serde(default)]
pub duration: Vec<i32>,
#[serde(default)]
pub only_hover: bool,
#[serde(default)]
pub starting: i32,
pub contact: i32,
#[serde(default)]
pub pressure: i32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MultiSwipeActionResult {
pub swipes: Vec<SwipeActionResult>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ClickKeyActionResult {
pub keycode: Vec<i32>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LongPressKeyActionResult {
pub keycode: Vec<i32>,
pub duration: i32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct InputTextActionResult {
pub text: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AppActionResult {
pub package: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ScrollActionResult {
#[serde(default)]
pub point: Point,
pub dx: i32,
pub dy: i32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TouchActionResult {
pub contact: i32,
pub point: Point,
#[serde(default)]
pub pressure: i32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ShellActionResult {
pub cmd: String,
pub shell_timeout: i32,
pub success: bool,
pub output: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct NodeDetail {
pub node_name: String,
pub reco_id: MaaId,
pub act_id: MaaId,
#[serde(default)]
pub recognition: Option<RecognitionDetail>,
#[serde(default)]
pub action: Option<ActionDetail>,
pub completed: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TaskDetail {
pub entry: String,
pub node_id_list: Vec<MaaId>,
pub status: MaaStatus,
#[serde(default)]
pub nodes: Vec<Option<NodeDetail>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(into = "String", from = "String")]
pub enum AlgorithmEnum {
DirectHit,
TemplateMatch,
FeatureMatch,
ColorMatch,
OCR,
NeuralNetworkClassify,
NeuralNetworkDetect,
And,
Or,
Custom,
Other(String),
}
impl From<String> for AlgorithmEnum {
fn from(s: String) -> Self {
match s.as_str() {
"DirectHit" => Self::DirectHit,
"TemplateMatch" => Self::TemplateMatch,
"FeatureMatch" => Self::FeatureMatch,
"ColorMatch" => Self::ColorMatch,
"OCR" => Self::OCR,
"NeuralNetworkClassify" => Self::NeuralNetworkClassify,
"NeuralNetworkDetect" => Self::NeuralNetworkDetect,
"And" => Self::And,
"Or" => Self::Or,
"Custom" => Self::Custom,
_ => Self::Other(s),
}
}
}
impl From<AlgorithmEnum> for String {
fn from(algo: AlgorithmEnum) -> Self {
match algo {
AlgorithmEnum::DirectHit => "DirectHit".to_string(),
AlgorithmEnum::TemplateMatch => "TemplateMatch".to_string(),
AlgorithmEnum::FeatureMatch => "FeatureMatch".to_string(),
AlgorithmEnum::ColorMatch => "ColorMatch".to_string(),
AlgorithmEnum::OCR => "OCR".to_string(),
AlgorithmEnum::NeuralNetworkClassify => "NeuralNetworkClassify".to_string(),
AlgorithmEnum::NeuralNetworkDetect => "NeuralNetworkDetect".to_string(),
AlgorithmEnum::And => "And".to_string(),
AlgorithmEnum::Or => "Or".to_string(),
AlgorithmEnum::Custom => "Custom".to_string(),
AlgorithmEnum::Other(s) => s,
}
}
}
impl AlgorithmEnum {
pub fn as_str(&self) -> &str {
match self {
Self::DirectHit => "DirectHit",
Self::TemplateMatch => "TemplateMatch",
Self::FeatureMatch => "FeatureMatch",
Self::ColorMatch => "ColorMatch",
Self::OCR => "OCR",
Self::NeuralNetworkClassify => "NeuralNetworkClassify",
Self::NeuralNetworkDetect => "NeuralNetworkDetect",
Self::And => "And",
Self::Or => "Or",
Self::Custom => "Custom",
Self::Other(s) => s.as_str(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(into = "String", from = "String")]
pub enum ActionEnum {
DoNothing,
Click,
LongPress,
Swipe,
MultiSwipe,
TouchDown,
TouchMove,
TouchUp,
ClickKey,
LongPressKey,
KeyDown,
KeyUp,
InputText,
StartApp,
StopApp,
StopTask,
Scroll,
Command,
Shell,
Custom,
Other(String),
}
impl From<String> for ActionEnum {
fn from(s: String) -> Self {
match s.as_str() {
"DoNothing" => Self::DoNothing,
"Click" => Self::Click,
"LongPress" => Self::LongPress,
"Swipe" => Self::Swipe,
"MultiSwipe" => Self::MultiSwipe,
"TouchDown" => Self::TouchDown,
"TouchMove" => Self::TouchMove,
"TouchUp" => Self::TouchUp,
"ClickKey" => Self::ClickKey,
"LongPressKey" => Self::LongPressKey,
"KeyDown" => Self::KeyDown,
"KeyUp" => Self::KeyUp,
"InputText" => Self::InputText,
"StartApp" => Self::StartApp,
"StopApp" => Self::StopApp,
"StopTask" => Self::StopTask,
"Scroll" => Self::Scroll,
"Command" => Self::Command,
"Shell" => Self::Shell,
"Custom" => Self::Custom,
_ => Self::Other(s),
}
}
}
impl From<ActionEnum> for String {
fn from(act: ActionEnum) -> Self {
match act {
ActionEnum::DoNothing => "DoNothing".to_string(),
ActionEnum::Click => "Click".to_string(),
ActionEnum::LongPress => "LongPress".to_string(),
ActionEnum::Swipe => "Swipe".to_string(),
ActionEnum::MultiSwipe => "MultiSwipe".to_string(),
ActionEnum::TouchDown => "TouchDown".to_string(),
ActionEnum::TouchMove => "TouchMove".to_string(),
ActionEnum::TouchUp => "TouchUp".to_string(),
ActionEnum::ClickKey => "ClickKey".to_string(),
ActionEnum::LongPressKey => "LongPressKey".to_string(),
ActionEnum::KeyDown => "KeyDown".to_string(),
ActionEnum::KeyUp => "KeyUp".to_string(),
ActionEnum::InputText => "InputText".to_string(),
ActionEnum::StartApp => "StartApp".to_string(),
ActionEnum::StopApp => "StopApp".to_string(),
ActionEnum::StopTask => "StopTask".to_string(),
ActionEnum::Scroll => "Scroll".to_string(),
ActionEnum::Command => "Command".to_string(),
ActionEnum::Shell => "Shell".to_string(),
ActionEnum::Custom => "Custom".to_string(),
ActionEnum::Other(s) => s,
}
}
}
impl ActionEnum {
pub fn as_str(&self) -> &str {
match self {
Self::DoNothing => "DoNothing",
Self::Click => "Click",
Self::LongPress => "LongPress",
Self::Swipe => "Swipe",
Self::MultiSwipe => "MultiSwipe",
Self::TouchDown => "TouchDown",
Self::TouchMove => "TouchMove",
Self::TouchUp => "TouchUp",
Self::ClickKey => "ClickKey",
Self::LongPressKey => "LongPressKey",
Self::KeyDown => "KeyDown",
Self::KeyUp => "KeyUp",
Self::InputText => "InputText",
Self::StartApp => "StartApp",
Self::StopApp => "StopApp",
Self::StopTask => "StopTask",
Self::Scroll => "Scroll",
Self::Command => "Command",
Self::Shell => "Shell",
Self::Custom => "Custom",
Self::Other(s) => s.as_str(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum NotificationType {
Starting,
Succeeded,
Failed,
Unknown,
}
impl NotificationType {
pub fn from_message(msg: &str) -> Self {
if msg.ends_with(".Starting") {
Self::Starting
} else if msg.ends_with(".Succeeded") {
Self::Succeeded
} else if msg.ends_with(".Failed") {
Self::Failed
} else {
Self::Unknown
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BoxAndScore {
#[serde(rename = "box")]
pub box_rect: (i32, i32, i32, i32),
pub score: f64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BoxAndCount {
#[serde(rename = "box")]
pub box_rect: (i32, i32, i32, i32),
pub count: i32,
}
pub type TemplateMatchResult = BoxAndScore;
pub type FeatureMatchResult = BoxAndCount;
pub type ColorMatchResult = BoxAndCount;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OCRResult {
#[serde(flatten)]
pub base: BoxAndScore,
pub text: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct NeuralNetworkResult {
#[serde(flatten)]
pub base: BoxAndScore,
pub cls_index: i32,
pub label: String,
}
pub type NeuralNetworkClassifyResult = NeuralNetworkResult;
pub type NeuralNetworkDetectResult = NeuralNetworkResult;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CustomRecognitionResult {
#[serde(rename = "box")]
pub box_rect: (i32, i32, i32, i32),
pub detail: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::{AndroidNativeControllerConfig, AndroidScreenResolution};
use serde_json::json;
#[test]
fn android_native_controller_config_serializes_expected_shape() {
let config = AndroidNativeControllerConfig {
library_path: "/data/local/tmp/libmaa_unit.so".to_string(),
screen_resolution: AndroidScreenResolution {
width: 1920,
height: 1080,
},
display_id: Some(1),
force_stop: Some(true),
};
let value = serde_json::to_value(config).unwrap();
assert_eq!(
value,
json!({
"library_path": "/data/local/tmp/libmaa_unit.so",
"screen_resolution": {
"width": 1920,
"height": 1080
},
"display_id": 1,
"force_stop": true
})
);
}
}