Skip to main content

main/
main.rs

1//! Comprehensive MaaFramework Rust SDK Demo
2//!
3//! This example demonstrates all major features of the Rust binding,
4//! matching the functionality of the official Python demo.
5//!
6//! # Features Covered
7//! - Toolkit initialization and device discovery
8//! - ADB and Win32 controller creation
9//! - Resource loading and custom component registration
10//! - Tasker binding and task execution
11//! - Custom Recognition and Custom Action implementation
12//! - Context API usage within custom components
13//! - Event sink callbacks
14
15use maa_framework::context::Context;
16use maa_framework::controller::Controller;
17use maa_framework::custom::{CustomAction, CustomRecognition};
18use maa_framework::resource::Resource;
19use maa_framework::tasker::Tasker;
20use maa_framework::toolkit::Toolkit;
21use maa_framework::{buffer, common, sys};
22
23// ============================================================================
24// Custom Recognition Implementation
25// ============================================================================
26
27/// Example custom recognition demonstrating Context API usage.
28///
29/// This shows how to:
30/// - Run sub-pipeline recognition
31/// - Clone context for independent operations
32/// - Override pipeline configurations
33/// - Check tasker stopping state
34struct MyRecognition;
35
36impl CustomRecognition for MyRecognition {
37    fn analyze(
38        &self,
39        context: &Context,
40        _task_id: sys::MaaTaskId,
41        node_name: &str,
42        custom_recognition_name: &str,
43        custom_recognition_param: &str,
44        _image: &maa_framework::buffer::MaaImageBuffer,
45        _roi: &maa_framework::common::Rect,
46    ) -> Option<(maa_framework::common::Rect, String)> {
47        println!(
48            "[MyRecognition] analyze called: node={}, name={}, param={}",
49            node_name, custom_recognition_name, custom_recognition_param
50        );
51
52        // --- Context API Demo ---
53
54        // 1. Run sub-pipeline recognition with override
55        let pp_override = r#"{"MyCustomOCR": {"recognition": "OCR", "roi": [100, 100, 200, 300]}}"#;
56        if let Ok(img_buf) = buffer::MaaImageBuffer::new() {
57            let reco_id = context.run_recognition("MyCustomOCR", pp_override, &img_buf);
58            println!("  run_recognition result: {:?}", reco_id);
59        }
60
61        // 2. Take a new screenshot via tasker's controller
62        let tasker_ptr = context.tasker_handle();
63        if !tasker_ptr.is_null() {
64            // Note: In real usage, you would use the Tasker's controller reference
65            println!("  tasker handle available for controller access");
66        }
67
68        // 3. Async click - post now, will wait later
69        // (In real code, you'd get controller from context.tasker)
70        println!("  [Demo] Would post async click at (10, 20)");
71
72        // 4. Override pipeline for all subsequent operations in this context
73        let _ = context.override_pipeline(r#"{"MyCustomOCR": {"roi": [1, 1, 114, 514]}}"#);
74
75        // 5. Check if tasker is stopping - IMPORTANT for responsive cancellation
76        // Note: Context provides tasker_handle() which returns a raw pointer.
77        // In production code, you should wrap this or use the Tasker instance directly.
78        // Here we demonstrate the raw API call for completeness.
79        let tasker_ptr = context.tasker_handle();
80        if !tasker_ptr.is_null() {
81            // Using sys:: directly requires unsafe - this matches C API usage
82            let is_stopping = unsafe { sys::MaaTaskerStopping(tasker_ptr) != 0 };
83            if is_stopping {
84                println!("  Task is stopping, returning early!");
85                return Some((
86                    common::Rect {
87                        x: 0,
88                        y: 0,
89                        width: 0,
90                        height: 0,
91                    },
92                    r#"{"status": "Task Stopped"}"#.to_string(),
93                ));
94            }
95        }
96
97        // 6. Wait for the async click to complete
98        println!("  [Demo] Async click would complete here");
99
100        // 7. Clone context for independent operations (modifications won't affect original)
101        if let Ok(new_ctx) = context.clone_context() {
102            let _ = new_ctx.override_pipeline(r#"{"MyCustomOCR": {"roi": [100, 200, 300, 400]}}"#);
103
104            // Run recognition and use the result
105            if let Ok(img_buf) = buffer::MaaImageBuffer::new() {
106                let reco_id = new_ctx.run_recognition("MyCustomOCR", "{}", &img_buf);
107
108                // If recognition succeeded, get the tasker to retrieve details
109                if reco_id.is_ok() {
110                    // In a real scenario, you would:
111                    // 1. Get recognition detail from tasker
112                    // 2. Check if it hit
113                    // 3. Use the box coordinates to click
114                    println!("  [Demo] Would get reco detail and click on result box");
115
116                    // Example of clicking on recognition result box:
117                    // if let Ok(Some(detail)) = tasker.get_recognition_detail(reco_id) {
118                    //     if detail.hit {
119                    //         let box_rect = detail.box_rect;
120                    //         controller.post_click(box_rect.x, box_rect.y).wait();
121                    //     }
122                    // }
123                }
124            }
125            // new_ctx changes don't affect original context
126        }
127
128        // 8. Get current task ID
129        let task_id = context.task_id();
130        println!("  current task_id: {}", task_id);
131
132        // 9. Get task job for result retrieval
133        let task_job = context.get_task_job();
134        if let Ok(Some(detail)) = task_job.get(false) {
135            println!("  task entry: {}", detail.entry);
136        }
137
138        // Return recognition result: bounding box + detail JSON
139        Some((
140            common::Rect {
141                x: 0,
142                y: 0,
143                width: 100,
144                height: 100,
145            },
146            r#"{"message": "Hello World!"}"#.to_string(),
147        ))
148    }
149}
150
151// ============================================================================
152// Custom Action Implementation
153// ============================================================================
154
155/// Example custom action.
156///
157/// Actions are executed after a successful recognition.
158struct MyAction;
159
160impl CustomAction for MyAction {
161    fn run(
162        &self,
163        context: &Context,
164        _task_id: sys::MaaTaskId,
165        node_name: &str,
166        custom_action_name: &str,
167        custom_action_param: &str,
168        reco_id: sys::MaaRecoId,
169        box_rect: &maa_framework::common::Rect,
170    ) -> bool {
171        println!(
172            "[MyAction] run called: node={}, name={}, param={}",
173            node_name, custom_action_name, custom_action_param
174        );
175        println!(
176            "  reco_id={}, box=({}, {}, {}, {})",
177            reco_id, box_rect.x, box_rect.y, box_rect.width, box_rect.height
178        );
179
180        // You can use Context API here too
181        let _ = context.run_action(
182            "Click",
183            "{}",
184            &common::Rect {
185                x: 114,
186                y: 514,
187                width: 100,
188                height: 100,
189            },
190            "{}",
191        );
192
193        true // Return true for success, false for failure
194    }
195}
196
197// ============================================================================
198// Main Demo
199// ============================================================================
200
201fn main() -> Result<(), Box<dyn std::error::Error>> {
202    println!("=== MaaFramework Rust SDK Demo ===\n");
203
204    // -------------------------------------------------------------------------
205    // 1. Initialize Toolkit
206    // -------------------------------------------------------------------------
207    let user_path = "./";
208    Toolkit::init_option(user_path, "{}")?;
209    println!("[1] Toolkit initialized with user_path: {}", user_path);
210
211    // -------------------------------------------------------------------------
212    // 2. Device Discovery
213    // -------------------------------------------------------------------------
214
215    // ADB devices
216    println!("\n[2] Scanning for ADB devices...");
217    let adb_devices = Toolkit::find_adb_devices()?;
218    if adb_devices.is_empty() {
219        println!("    No ADB device found.");
220    } else {
221        for device in &adb_devices {
222            println!("    Found: {} ({})", device.name, device.address);
223        }
224    }
225
226    // Win32 windows (Windows only)
227    #[cfg(target_os = "windows")]
228    {
229        println!("\n    Scanning for desktop windows...");
230        match Toolkit::find_desktop_windows() {
231            Ok(windows) => {
232                if windows.is_empty() {
233                    println!("    No window found.");
234                } else {
235                    for (i, win) in windows.iter().take(3).enumerate() {
236                        println!("    [{}] hwnd={:?}, class={}", i, win.hwnd, win.class_name);
237                    }
238                    if windows.len() > 3 {
239                        println!("    ... and {} more", windows.len() - 3);
240                    }
241                }
242            }
243            Err(e) => println!("    Failed to find windows: {}", e),
244        }
245    }
246
247    // -------------------------------------------------------------------------
248    // 3. Create Controller (choose one)
249    // -------------------------------------------------------------------------
250    println!("\n[3] Creating controller...");
251
252    let controller: Option<Controller>;
253
254    // Option A: ADB Controller
255    #[cfg(feature = "adb")]
256    if let Some(device) = adb_devices.first() {
257        println!("    Using ADB device: {}", device.name);
258        let config_str = serde_json::to_string(&device.config)?;
259        controller = Some(Controller::new_adb(
260            device.adb_path.to_str().unwrap(),
261            &device.address,
262            &config_str,
263            None,
264        )?);
265    } else {
266        controller = None;
267    }
268
269    #[cfg(not(feature = "adb"))]
270    {
271        controller = None;
272    }
273
274    // Option B: Win32 Controller (uncomment to use)
275    // #[cfg(target_os = "windows")]
276    // if let Some(window) = windows.first() {
277    //     controller = Some(Controller::new_win32(
278    //         window.hwnd as isize,
279    //         common::Win32ScreencapMethod::FramePool as i32,
280    //         common::Win32InputMethod::PostMessage as i32,
281    //         common::Win32InputMethod::PostMessage as i32,
282    //     )?);
283    // }
284
285    // -------------------------------------------------------------------------
286    // 4. Controller Operations
287    // -------------------------------------------------------------------------
288    if let Some(ref ctrl) = controller {
289        println!("\n[4] Controller operations...");
290
291        // Connect
292        let conn_id = ctrl.post_connection()?;
293        ctrl.wait(conn_id);
294        println!("    Connected: {}", ctrl.connected());
295
296        // Screenshot
297        let cap_id = ctrl.post_screencap()?;
298        ctrl.wait(cap_id);
299        println!("    Screenshot captured");
300
301        // Click (sync)
302        let click_id = ctrl.post_click(114, 514)?;
303        ctrl.wait(click_id);
304        println!("    Clicked at (114, 514)");
305
306        // Click (async) - post now, wait later
307        let click_id2 = ctrl.post_click(514, 114)?;
308        // ... do other work here ...
309        ctrl.wait(click_id2);
310        println!("    Async click completed");
311
312        // Input text
313        let input_id = ctrl.post_input_text("Hello MAA!")?;
314        ctrl.wait(input_id);
315        println!("    Text input sent");
316
317        // Get resolution
318        if let Ok((w, h)) = ctrl.resolution() {
319            println!("    Resolution: {}x{}", w, h);
320        }
321    } else {
322        println!("\n[4] Skipping controller operations (no device available)");
323    }
324
325    // -------------------------------------------------------------------------
326    // 5. Resource Setup
327    // -------------------------------------------------------------------------
328    println!("\n[5] Setting up resource...");
329
330    let resource = Resource::new()?;
331
332    // Register custom recognition and action
333    resource.register_custom_recognition("MyRecognition", Box::new(MyRecognition))?;
334    resource.register_custom_action("MyAction", Box::new(MyAction))?;
335    println!("    Registered: MyRecognition, MyAction");
336
337    // List registered components
338    let reco_list = resource.custom_recognition_list()?;
339    let action_list = resource.custom_action_list()?;
340    println!("    Recognition list: {:?}", reco_list);
341    println!("    Action list: {:?}", action_list);
342
343    // Load resource bundle
344    let resource_path = "sample/resource";
345    println!("    Loading resource from: {}", resource_path);
346    match resource.post_bundle(resource_path) {
347        Ok(job) => {
348            let status = job.wait();
349            println!("    Load status: {:?}", status);
350        }
351        Err(e) => println!("    Load failed: {} (OK for demo)", e),
352    }
353
354    // Add event sink
355    resource.add_sink(|msg, details| {
356        println!(
357            "    [ResourceEvent] {}: {}",
358            msg,
359            &details[..details.len().min(50)]
360        );
361    })?;
362
363    // -------------------------------------------------------------------------
364    // 6. Tasker Setup and Execution
365    // -------------------------------------------------------------------------
366    println!("\n[6] Setting up tasker...");
367
368    let tasker = Tasker::new()?;
369
370    // Bind resource
371    tasker.bind_resource(&resource)?;
372    println!("    Resource bound");
373
374    // Bind controller (if available)
375    if let Some(ref ctrl) = controller {
376        tasker.bind_controller(ctrl)?;
377        println!("    Controller bound");
378    }
379
380    // Check initialization
381    if tasker.inited() {
382        println!("    Tasker initialized successfully!");
383
384        // Add event sink
385        tasker.add_sink(|msg, details| {
386            println!(
387                "    [TaskerEvent] {}: {}",
388                msg,
389                &details[..details.len().min(50)]
390            );
391        })?;
392
393        // Execute task with pipeline override
394        let pipeline_override = r#"{
395            "MyCustomEntry": {
396                "recognition": "Custom",
397                "custom_recognition": "MyRecognition",
398                "action": "Custom",
399                "custom_action": "MyAction"
400            }
401        }"#;
402
403        println!("\n[7] Executing task...");
404        match tasker.post_task("MyCustomEntry", pipeline_override) {
405            Ok(job) => {
406                let status = job.wait();
407                println!("    Task status: {:?}", status);
408
409                if let Ok(Some(detail)) = job.get(false) {
410                    println!("    Entry: {}", detail.entry);
411                    println!("    Nodes executed: {}", detail.nodes.len());
412
413                    for node_opt in detail.nodes {
414                        if let Some(node) = node_opt {
415                            if let Some(reco) = node.recognition {
416                                println!(
417                                    "      Node: {}, Algo: {:?}",
418                                    node.node_name, reco.algorithm
419                                );
420                            }
421                        }
422                    }
423                }
424            }
425            Err(e) => println!("    Task failed: {}", e),
426        }
427    } else {
428        println!("    Tasker not fully initialized (missing controller binding)");
429    }
430
431    // -------------------------------------------------------------------------
432    // 8. Cleanup
433    // -------------------------------------------------------------------------
434    println!("\n[8] Demo completed!");
435    println!("    - Toolkit, Controller, Resource, Tasker all demonstrated");
436    println!("    - Custom Recognition and Action registered and explained");
437    println!("    - Context API documented in custom component implementations");
438
439    Ok(())
440}