Skip to main content

maa_framework/
custom.rs

1//! Custom recognizer and action components.
2//!
3//! This module provides traits for implementing custom recognition algorithms
4//! and actions that integrate with the MaaFramework pipeline system.
5//!
6//! # Usage
7//!
8//! 1. Implement [`CustomRecognition`] or [`CustomAction`] trait
9//! 2. Register with [`Resource::register_custom_recognition`] or [`Resource::register_custom_action`]
10//! 3. Reference in pipeline JSON via `"recognition": "Custom"` or `"action": "Custom"`
11
12use crate::context::Context;
13use crate::resource::Resource;
14use crate::{common, sys, MaaError, MaaResult};
15use std::ffi::{CStr, CString};
16use std::os::raw::c_void;
17
18// === Trait Definitions ===
19
20/// Trait for implementing custom actions.
21///
22/// Actions are executed after a successful recognition and can perform
23/// clicks, swipes, or custom logic.
24pub trait CustomAction: Send + Sync {
25    /// Execute the custom action.
26    ///
27    /// # Arguments
28    /// * `context` - The current execution context
29    /// * `task_id` - ID of the current task
30    /// * `node_name` - Name of the current node
31    /// * `custom_action_name` - Name of this action (as registered)
32    /// * `custom_action_param` - JSON parameters for this action
33    /// * `reco_id` - ID of the preceding recognition result
34    /// * `box_rect` - Target region found by recognition
35    ///
36    /// # Returns
37    /// `true` if the action succeeded, `false` otherwise
38    fn run(
39        &self,
40        context: &Context,
41        task_id: common::MaaId,
42        node_name: &str,
43        custom_action_name: &str,
44        custom_action_param: &str,
45        reco_id: common::MaaId,
46        box_rect: &common::Rect,
47    ) -> bool;
48}
49
50/// Trait for implementing custom recognizers.
51///
52/// Recognizers analyze screenshots to find targets in the UI.
53pub trait CustomRecognition: Send + Sync {
54    /// Analyze an image to find the target.
55    ///
56    /// # Arguments
57    /// * `context` - The current execution context
58    /// * `task_id` - ID of the current task
59    /// * `node_name` - Name of the current node
60    /// * `custom_recognition_name` - Name of this recognizer
61    /// * `custom_recognition_param` - JSON parameters for this recognizer
62    /// * `image` - The image to analyze
63    /// * `roi` - Region of Interest to restrict analysis
64    ///
65    /// # Returns
66    /// `Some((rect, detail))` if target found, `None` otherwise
67    fn analyze(
68        &self,
69        context: &Context,
70        task_id: common::MaaId,
71        node_name: &str,
72        custom_recognition_name: &str,
73        custom_recognition_param: &str,
74        image: &crate::buffer::MaaImageBuffer,
75        roi: &common::Rect,
76    ) -> Option<(common::Rect, String)>;
77}
78
79// === Function Wrapper for Recognition ===
80
81/// Wrapper to use a closure as a custom recognition.
82///
83/// This allows using a simple closure instead of implementing a full trait.
84pub struct FnRecognition<F>
85where
86    F: Fn(&Context, &RecognitionArgs) -> Option<(common::Rect, String)> + Send + Sync,
87{
88    func: F,
89}
90
91/// Arguments bundle for custom recognition closure.
92pub struct RecognitionArgs<'a> {
93    pub task_id: common::MaaId,
94    pub node_name: &'a str,
95    pub name: &'a str,
96    pub param: &'a str,
97    pub image: &'a crate::buffer::MaaImageBuffer,
98    pub roi: &'a common::Rect,
99}
100
101impl<F> FnRecognition<F>
102where
103    F: Fn(&Context, &RecognitionArgs) -> Option<(common::Rect, String)> + Send + Sync,
104{
105    pub fn new(func: F) -> Self {
106        Self { func }
107    }
108}
109
110impl<F> CustomRecognition for FnRecognition<F>
111where
112    F: Fn(&Context, &RecognitionArgs) -> Option<(common::Rect, String)> + Send + Sync,
113{
114    fn analyze(
115        &self,
116        context: &Context,
117        task_id: common::MaaId,
118        node_name: &str,
119        custom_recognition_name: &str,
120        custom_recognition_param: &str,
121        image: &crate::buffer::MaaImageBuffer,
122        roi: &common::Rect,
123    ) -> Option<(common::Rect, String)> {
124        let args = RecognitionArgs {
125            task_id,
126            node_name,
127            name: custom_recognition_name,
128            param: custom_recognition_param,
129            image,
130            roi,
131        };
132        (self.func)(context, &args)
133    }
134}
135
136// === Function Wrapper for Action ===
137
138/// Wrapper to use a closure as a custom action.
139pub struct FnAction<F>
140where
141    F: Fn(&Context, &ActionArgs) -> bool + Send + Sync,
142{
143    func: F,
144}
145
146/// Arguments bundle for custom action closure.
147pub struct ActionArgs<'a> {
148    pub task_id: common::MaaId,
149    pub node_name: &'a str,
150    pub name: &'a str,
151    pub param: &'a str,
152    pub reco_id: common::MaaId,
153    pub box_rect: &'a common::Rect,
154}
155
156impl<F> FnAction<F>
157where
158    F: Fn(&Context, &ActionArgs) -> bool + Send + Sync,
159{
160    pub fn new(func: F) -> Self {
161        Self { func }
162    }
163}
164
165impl<F> CustomAction for FnAction<F>
166where
167    F: Fn(&Context, &ActionArgs) -> bool + Send + Sync,
168{
169    fn run(
170        &self,
171        context: &Context,
172        task_id: common::MaaId,
173        node_name: &str,
174        custom_action_name: &str,
175        custom_action_param: &str,
176        reco_id: common::MaaId,
177        box_rect: &common::Rect,
178    ) -> bool {
179        let args = ActionArgs {
180            task_id,
181            node_name,
182            name: custom_action_name,
183            param: custom_action_param,
184            reco_id,
185            box_rect,
186        };
187        (self.func)(context, &args)
188    }
189}
190
191// === Trampolines ===
192
193pub(crate) unsafe extern "C" fn custom_action_trampoline(
194    context: *mut sys::MaaContext,
195    task_id: sys::MaaTaskId,
196    node_name: *const std::os::raw::c_char,
197    custom_action_name: *const std::os::raw::c_char,
198    custom_action_param: *const std::os::raw::c_char,
199    reco_id: sys::MaaRecoId,
200    box_: *const sys::MaaRect,
201    trans_arg: *mut std::os::raw::c_void,
202) -> sys::MaaBool {
203    if trans_arg.is_null() {
204        return 0; // Failure
205    }
206
207    let action = &*(trans_arg as *mut Box<dyn CustomAction>);
208
209    let ctx = match Context::from_raw(context) {
210        Some(c) => c,
211        None => return 0,
212    };
213
214    let get_str = |ptr: *const std::os::raw::c_char| -> &str {
215        if ptr.is_null() {
216            ""
217        } else {
218            CStr::from_ptr(ptr).to_str().unwrap_or("")
219        }
220    };
221
222    let node = get_str(node_name);
223    let name = get_str(custom_action_name);
224    let param = get_str(custom_action_param);
225
226    let rect = if !box_.is_null() {
227        crate::buffer::MaaRectBuffer::from_handle(box_ as *mut sys::MaaRect)
228            .map(|buf| buf.get())
229            .unwrap_or(common::Rect::default())
230    } else {
231        common::Rect::default()
232    };
233
234    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
235        action.run(&ctx, task_id, node, name, param, reco_id, &rect)
236    }));
237
238    match result {
239        Ok(true) => 1,
240        Ok(false) => 0,
241        Err(_) => {
242            eprintln!("MaaFramework Rust Binding: Panic caught in custom action callback");
243            0
244        }
245    }
246}
247
248pub(crate) unsafe extern "C" fn custom_recognition_trampoline(
249    context: *mut sys::MaaContext,
250    task_id: sys::MaaTaskId,
251    node_name: *const std::os::raw::c_char,
252    custom_recognition_name: *const std::os::raw::c_char,
253    custom_recognition_param: *const std::os::raw::c_char,
254    image: *const sys::MaaImageBuffer,
255    roi: *const sys::MaaRect,
256    trans_arg: *mut std::os::raw::c_void,
257    out_box: *mut sys::MaaRect,
258    out_detail: *mut sys::MaaStringBuffer,
259) -> sys::MaaBool {
260    if trans_arg.is_null() {
261        return 0;
262    }
263    let reco = &*(trans_arg as *mut Box<dyn CustomRecognition>);
264
265    let ctx = match Context::from_raw(context) {
266        Some(c) => c,
267        None => return 0,
268    };
269
270    let get_str = |ptr: *const std::os::raw::c_char| -> &str {
271        if ptr.is_null() {
272            ""
273        } else {
274            CStr::from_ptr(ptr).to_str().unwrap_or("")
275        }
276    };
277
278    let node = get_str(node_name);
279    let name = get_str(custom_recognition_name);
280    let param = get_str(custom_recognition_param);
281
282    // Image logic: the pointer passed by C is valid for the duration of the call.
283    // We wrap it safely without taking ownership.
284    let img_buf = crate::buffer::MaaImageBuffer::from_handle(image as *mut sys::MaaImageBuffer);
285    if img_buf.is_none() {
286        return 0;
287    }
288    let img_buf = img_buf.unwrap();
289
290    // Rect logic
291    let roi_rect = if !roi.is_null() {
292        crate::buffer::MaaRectBuffer::from_handle(roi as *mut sys::MaaRect)
293            .map(|buf| buf.get())
294            .unwrap_or(common::Rect::default())
295    } else {
296        common::Rect::default()
297    };
298
299    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
300        reco.analyze(&ctx, task_id, node, name, param, &img_buf, &roi_rect)
301    }));
302
303    match result {
304        Ok(Some((res_rect, res_detail))) => {
305            if !out_box.is_null() {
306                if let Some(mut out_rect_buf) = crate::buffer::MaaRectBuffer::from_handle(out_box) {
307                    let _ = out_rect_buf.set(&res_rect);
308                }
309            }
310            if !out_detail.is_null() {
311                if let Some(mut out_str_buf) =
312                    crate::buffer::MaaStringBuffer::from_handle(out_detail)
313                {
314                    let _ = out_str_buf.set(&res_detail);
315                }
316            }
317            1
318        }
319        Ok(None) => 0,
320        Err(_) => {
321            eprintln!("MaaFramework Rust Binding: Panic caught in custom recognition callback");
322            0
323        }
324    }
325}
326
327// === Resource Extension impls ===
328
329impl Resource {
330    /// Register a custom action implementation.
331    ///
332    /// The action will be kept alive as long as it is registered in the Resource.
333    /// Re-registering with the same name will drop the previous implementation.
334    pub fn register_custom_action(
335        &self,
336        name: &str,
337        action: Box<dyn CustomAction>,
338    ) -> MaaResult<()> {
339        let c_name = CString::new(name)?;
340        let action_ptr = Box::into_raw(Box::new(action));
341        let action_ptr_void = action_ptr as *mut c_void;
342
343        unsafe {
344            let ret = sys::MaaResourceRegisterCustomAction(
345                self.raw(),
346                c_name.as_ptr(),
347                Some(custom_action_trampoline),
348                action_ptr_void,
349            );
350            if ret == 0 {
351                let _ = Box::from_raw(action_ptr);
352                return Err(MaaError::FrameworkError(0));
353            }
354        }
355
356        let mut map = self.custom_actions().lock().unwrap();
357        if let Some(old_ptr) = map.insert(name.to_string(), action_ptr as usize) {
358            unsafe {
359                let _ = Box::from_raw(old_ptr as *mut Box<dyn CustomAction>);
360            }
361        }
362
363        Ok(())
364    }
365
366    /// Register a custom recognition implementation.
367    ///
368    /// The recognizer will be kept alive as long as it is registered in the Resource.
369    /// Re-registering with the same name will drop the previous implementation.
370    pub fn register_custom_recognition(
371        &self,
372        name: &str,
373        reco: Box<dyn CustomRecognition>,
374    ) -> MaaResult<()> {
375        let c_name = CString::new(name)?;
376        let reco_ptr = Box::into_raw(Box::new(reco));
377        let reco_ptr_void = reco_ptr as *mut c_void;
378
379        unsafe {
380            let ret = sys::MaaResourceRegisterCustomRecognition(
381                self.raw(),
382                c_name.as_ptr(),
383                Some(custom_recognition_trampoline),
384                reco_ptr_void,
385            );
386            if ret == 0 {
387                let _ = Box::from_raw(reco_ptr);
388                return Err(MaaError::FrameworkError(0));
389            }
390        }
391
392        let mut map = self.custom_recognitions().lock().unwrap();
393        if let Some(old_ptr) = map.insert(name.to_string(), reco_ptr as usize) {
394            unsafe {
395                let _ = Box::from_raw(old_ptr as *mut Box<dyn CustomRecognition>);
396            }
397        }
398
399        Ok(())
400    }
401}