Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions src-tauri/src/commands/maa_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ pub async fn start_tasks_impl(
cwd: String,
tcp_compat_mode: bool,
pi_envs: Option<HashMap<String, String>>,
reset_state: bool,
) -> Result<Vec<i64>, String> {
info!("start_tasks_impl called");

Expand Down Expand Up @@ -700,19 +701,30 @@ pub async fn start_tasks_impl(
task_ids.len()
);

// 初始化后端 TaskRunState(单一真相来源)并缓存 task_ids
debug!("[start_tasks] Initializing TaskRunState...");
// 初始化/追加后端 TaskRunState(单一真相来源)并缓存 task_ids
debug!(
"[start_tasks] Updating TaskRunState (reset_state={})...",
reset_state
);
{
let mut instances = maa_state.instances.lock().map_err(|e| e.to_string())?;
if let Some(instance) = instances.get_mut(&instance_id) {
instance.task_ids = task_ids.clone();
if reset_state {
// 首批:重置任务运行状态
instance.task_ids = task_ids.clone();
let state = &mut instance.task_run_state;
state.statuses.clear();
state.mappings.clear();
state.pending_task_ids = task_ids.clone();
state.current_task_index = 0;
} else {
// 追加批次(分段运行):保留已完成状态,仅追加新任务
instance.task_ids.extend(task_ids.iter().copied());
let state = &mut instance.task_run_state;
state.pending_task_ids.extend(task_ids.iter().copied());
}

// 重置任务运行状态
let state = &mut instance.task_run_state;
state.statuses.clear();
state.mappings.clear();
state.pending_task_ids = task_ids.clone();
state.current_task_index = 0;
state.overall_status = Some("Running".to_string());

// 建立 maaTaskId -> selectedTaskId 映射,并将有映射的任务初始化为 "pending"
Expand All @@ -724,7 +736,7 @@ pub async fn start_tasks_impl(
}
}
}
debug!("[start_tasks] TaskRunState initialized");
debug!("[start_tasks] TaskRunState updated");

info!(
"[start_tasks] start_tasks_impl completed successfully, returning {} task_ids",
Expand All @@ -748,6 +760,7 @@ pub async fn maa_start_tasks(
cwd: String,
tcp_compat_mode: bool,
pi_envs: Option<HashMap<String, String>>,
reset_state: Option<bool>,
) -> Result<Vec<i64>, String> {
start_tasks_impl(
app,
Expand All @@ -758,6 +771,7 @@ pub async fn maa_start_tasks(
cwd,
tcp_compat_mode,
pi_envs,
reset_state.unwrap_or(true),
)
.await
}
Expand Down
10 changes: 10 additions & 0 deletions src-tauri/src/commands/maa_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,13 @@ pub async fn connect_controller_impl(
let uuid_str = uuid.as_deref().unwrap_or("");
Controller::new_playcover(address, uuid_str).map_err(|e| e.to_string())?
}
ControllerConfig::Dummy {
display_short_side, ..
} => {
let short = display_short_side.unwrap_or(720);
Controller::new_custom(crate::dummy_controller::DummyController::new(short))
.map_err(|e| e.to_string())?
}
ControllerConfig::Gamepad {
handle,
gamepad_type,
Expand Down Expand Up @@ -619,6 +626,9 @@ pub async fn connect_controller_impl(
}
| ControllerConfig::PlayCover {
display_short_side, ..
}
| ControllerConfig::Dummy {
display_short_side, ..
} => display_short_side.unwrap_or(720),
};

Expand Down
6 changes: 6 additions & 0 deletions src-tauri/src/commands/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ pub enum ControllerConfig {
#[serde(default)]
display_short_side: Option<i32>,
},
/// 空 controller:截图返回纯黑图、输入 no-op。
/// 用于在游戏未连接/已关闭时执行不依赖游戏画面的 MXU 特殊任务。
Dummy {
#[serde(default)]
display_short_side: Option<i32>,
},
}

/// 连接状态
Expand Down
145 changes: 145 additions & 0 deletions src-tauri/src/dummy_controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//! MXU 空 controller(Dummy Controller)
//!
//! 用于在游戏未连接 / 已被关闭时仍能执行不依赖游戏画面的 MXU 特殊任务
//! (Notify / Power / KillProc / Sleep 等)。
//!
//! MaaFramework 任务主循环在任何识别(含 DirectHit)前都会先 `screencap()`,
//! 图像为空就跳过识别、不执行动作。真实 controller 在游戏窗口消失后截图为空,
//! 会导致后续特殊任务卡死/超时。本 controller 的 `screencap` 始终返回一张纯黑图,
//! 让主循环越过该门槛,从而使 DirectHit + Custom Action 正常执行。
//!
//! 所有输入/应用控制均为 no-op:空 controller 只承载不需要真实画面/输入的任务。

use std::io::Write;

use flate2::write::ZlibEncoder;
use flate2::Compression;
use flate2::Crc;
use log::debug;
use maa_framework::custom_controller::CustomControllerCallback;

/// 返回纯黑图、输入全部 no-op 的自定义 controller。
pub struct DummyController {
/// 预先编码好的纯黑 PNG 图像。
png: Vec<u8>,
}

impl DummyController {
/// 按目标短边构建一张 16:9 的纯黑图(默认 1280x720)。
pub fn new(display_short_side: i32) -> Self {
let short: u32 = if display_short_side <= 0 {
720
} else {
display_short_side as u32
};
let height = short.max(1);
let width = ((u64::from(height) * 16) / 9).max(1) as u32;
let png = build_black_png(width, height);
debug!(
"[MXU_DUMMY] dummy controller created, image {}x{}, png {} bytes",
width,
height,
png.len()
);
Self { png }
}
}

impl CustomControllerCallback for DummyController {
fn connect(&self) -> bool {
true
}

fn connected(&self) -> bool {
true
}

fn request_uuid(&self) -> Option<String> {
Some("MXU-DUMMY".to_string())
}

fn screencap(&self) -> Option<Vec<u8>> {
Some(self.png.clone())
}

fn get_info(&self) -> String {
"{\"type\":\"MXU_DUMMY\"}".to_string()
}

// 以下输入/应用控制全部 no-op:空 controller 不承载需要真实交互的任务。
fn click(&self, _x: i32, _y: i32) -> bool {
true
}

fn swipe(&self, _x1: i32, _y1: i32, _x2: i32, _y2: i32, _duration: i32) -> bool {
true
}

fn click_key(&self, _keycode: i32) -> bool {
true
}

fn input_text(&self, _text: &str) -> bool {
true
}

fn start_app(&self, _intent: &str) -> bool {
true
}

fn stop_app(&self, _intent: &str) -> bool {
true
}
}

/// 构建一张 `width`x`height` 的纯黑 PNG(8-bit RGB)。
///
/// 不引入额外图像库:原始扫描线全 0(黑色),用 flate2 完成 zlib 压缩与 CRC32。
fn build_black_png(width: u32, height: u32) -> Vec<u8> {
// 每行 = 1 字节 filter(0) + width*3 字节 RGB(全 0 即黑色)
let row_len = 1 + (width as usize) * 3;
let raw = vec![0u8; row_len * height as usize];

let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
encoder
.write_all(&raw)
.expect("zlib write of dummy image should not fail");
let idat = encoder
.finish()
.expect("zlib finish of dummy image should not fail");

let mut png = Vec::new();
// PNG 签名
png.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);

// IHDR
let mut ihdr = Vec::with_capacity(13);
ihdr.extend_from_slice(&width.to_be_bytes());
ihdr.extend_from_slice(&height.to_be_bytes());
ihdr.push(8); // bit depth
ihdr.push(2); // color type: Truecolor(RGB)
ihdr.push(0); // compression method
ihdr.push(0); // filter method
ihdr.push(0); // interlace method
write_png_chunk(&mut png, b"IHDR", &ihdr);

// IDAT
write_png_chunk(&mut png, b"IDAT", &idat);

// IEND
write_png_chunk(&mut png, b"IEND", &[]);

png
}

/// 写入一个 PNG chunk:长度(BE) + 类型 + 数据 + CRC32(类型+数据, BE)。
fn write_png_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
out.extend_from_slice(chunk_type);
out.extend_from_slice(data);

let mut crc = Crc::new();
crc.update(chunk_type);
crc.update(data);
out.extend_from_slice(&crc.sum().to_be_bytes());
}
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod commands;
mod dummy_controller;
mod mxu_actions;
pub mod screenshot_service;
mod tray;
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/web_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,8 @@ struct StartTasksRequest {
tcp_compat_mode: Option<bool>,
#[serde(default)]
pi_envs: Option<std::collections::HashMap<String, String>>,
#[serde(default)]
reset_state: Option<bool>,
}

/// POST /api/maa/instances/:id/tasks/start
Expand Down Expand Up @@ -923,6 +925,7 @@ async fn handle_start_tasks(
cwd,
body.tcp_compat_mode.unwrap_or(false),
body.pi_envs,
body.reset_state.unwrap_or(true),
)
.await
{
Expand Down
Loading
Loading