Skip to main content

siglus_scene_vm/runtime/commands/
misc.rs

1use anyhow::Result;
2
3use crate::runtime::commands::util;
4use crate::runtime::forms::stage;
5use crate::runtime::forms::syscom as syscom_form;
6use crate::runtime::globals::WipeState;
7use crate::runtime::{Command, CommandContext, Value};
8use std::path::{Path, PathBuf};
9
10fn is_noop_cmd(name: &str) -> bool {
11    matches!(name, "NOP" | "VIBRATE")
12}
13
14fn is_clear_cmd(name: &str) -> bool {
15    matches!(
16        name,
17        "CLS" | "CLEAR" | "CLEARALL" | "ALL_CLEAR" | "ALLCLEAR" | "RESET"
18    )
19}
20
21/// Catch-all helpers for script commands handled as no-ops.
22pub fn handle(ctx: &mut CommandContext, cmd: &Command) -> Result<bool> {
23    let name = cmd.name.to_ascii_uppercase();
24    let args = util::strip_vm_meta(&cmd.args);
25
26    let mut pos: Vec<&Value> = Vec::new();
27    let mut named: Vec<(i32, &Value)> = Vec::new();
28    for a in args {
29        if let Value::NamedArg { id, value } = a {
30            named.push((*id, value.as_ref()));
31        } else {
32            pos.push(a);
33        }
34    }
35
36    let parse_i32 = |v: &Value| -> Option<i32> { v.as_i64().and_then(|x| i32::try_from(x).ok()) };
37
38    let parse_bool = |v: &Value| -> Option<bool> { parse_i32(v).map(|x| x != 0) };
39
40    let parse_list_i32 = |v: &Value| -> Vec<i32> {
41        match v {
42            Value::List(xs) => xs
43                .iter()
44                .filter_map(|x| x.as_i64().and_then(|n| i32::try_from(n).ok()))
45                .collect(),
46            _ => Vec::new(),
47        }
48    };
49
50    // WAIT family: block VM execution.
51    match name.as_str() {
52        // ------------------------------------------------------------------
53        // WIPE family
54        // ------------------------------------------------------------------
55        "WIPE" | "WIPE_ALL" | "MASK_WIPE" | "MASK_WIPE_ALL" => {
56            let is_mask = matches!(name.as_str(), "MASK_WIPE" | "MASK_WIPE_ALL");
57            let is_all = matches!(name.as_str(), "WIPE_ALL" | "MASK_WIPE_ALL");
58
59            let mut mask_file: Option<String> = None;
60            let mut wipe_type: i32 = 0;
61            let mut wipe_time: i32 = 500;
62            let mut speed_mode: i32 = 0;
63            let mut start_time: i32 = 0;
64            let mut option: Vec<i32> = Vec::new();
65
66            let mut begin_order: i32 = 0;
67            let mut end_order: i32 = if is_all { i32::MAX } else { 0 };
68            let mut begin_layer: i32 = i32::MIN;
69            let mut end_layer: i32 = i32::MAX;
70            let mut wait_flag: bool = true;
71            let mut key_wait_mode: i32 = -1;
72            let mut with_low_order: i32 = 0;
73
74            if is_mask {
75                mask_file = pos.get(0).and_then(|v| v.as_str()).map(|s| s.to_string());
76                if let Some(v) = pos.get(1).and_then(|v| parse_i32(v)) {
77                    wipe_type = v;
78                }
79                if let Some(v) = pos.get(2).and_then(|v| parse_i32(v)) {
80                    wipe_time = v;
81                }
82                if let Some(v) = pos.get(3).and_then(|v| parse_i32(v)) {
83                    speed_mode = v;
84                }
85                if let Some(v) = pos.get(4) {
86                    option = parse_list_i32(v);
87                }
88            } else {
89                if let Some(v) = pos.get(0).and_then(|v| parse_i32(v)) {
90                    wipe_type = v;
91                }
92                if let Some(v) = pos.get(1).and_then(|v| parse_i32(v)) {
93                    wipe_time = v;
94                }
95                if let Some(v) = pos.get(2).and_then(|v| parse_i32(v)) {
96                    speed_mode = v;
97                }
98                if let Some(v) = pos.get(3) {
99                    option = parse_list_i32(v);
100                }
101            }
102
103            // Named args override positional args.
104            for &(id, v) in &named {
105                match id {
106                    0 => {
107                        if let Some(x) = parse_i32(v) {
108                            wipe_type = x;
109                        }
110                    }
111                    1 => {
112                        if let Some(x) = parse_i32(v) {
113                            wipe_time = x;
114                        }
115                    }
116                    2 => {
117                        if let Some(x) = parse_i32(v) {
118                            speed_mode = x;
119                        }
120                    }
121                    3 => {
122                        option = parse_list_i32(v);
123                    }
124                    4 => {
125                        if let Some(x) = parse_i32(v) {
126                            begin_order = x;
127                        }
128                    }
129                    5 => {
130                        if let Some(x) = parse_i32(v) {
131                            end_order = x;
132                        }
133                    }
134                    6 => {
135                        if let Some(x) = parse_i32(v) {
136                            begin_layer = x;
137                        }
138                    }
139                    7 => {
140                        if let Some(x) = parse_i32(v) {
141                            end_layer = x;
142                        }
143                    }
144                    8 => {
145                        if let Some(x) = parse_bool(v) {
146                            wait_flag = x;
147                        }
148                    }
149                    9 => {
150                        if let Some(x) = parse_i32(v) {
151                            key_wait_mode = x;
152                        }
153                    }
154                    10 => {
155                        if let Some(x) = parse_i32(v) {
156                            with_low_order = x;
157                        }
158                    }
159                    11 => {
160                        if let Some(x) = parse_i32(v) {
161                            start_time = x;
162                        }
163                    }
164                    _ => {}
165                }
166            }
167
168            if is_all {
169                end_order = i32::MAX;
170            }
171
172            let mask_image_id = if let Some(ref f) = mask_file {
173                resolve_mask_path(&ctx.project_dir, f)
174                    .and_then(|p| ctx.images.load_file(&p, 0).ok())
175            } else {
176                None
177            };
178            stage::apply_stage_wipe(ctx, begin_order, end_order, begin_layer, end_layer);
179            ctx.globals.start_wipe(WipeState::new(
180                mask_file,
181                mask_image_id,
182                wipe_type,
183                wipe_time,
184                start_time,
185                speed_mode,
186                option,
187                begin_order,
188                end_order,
189                begin_layer,
190                end_layer,
191                wait_flag,
192                key_wait_mode,
193                with_low_order,
194            ));
195
196            if wait_flag {
197                let key_skip = match key_wait_mode {
198                    0 => false,
199                    1 => true,
200                    _ => {
201                        ctx.globals
202                            .syscom
203                            .config_int
204                            .get(&197)
205                            .copied()
206                            .unwrap_or(0)
207                            != 0
208                    }
209                };
210                ctx.wait.wait_wipe(key_skip);
211            }
212            return Ok(true);
213        }
214        "WAIT_WIPE" | "WAITWIPE" => {
215            let mut key_wait_mode: i32 = -1;
216            for &(id, v) in &named {
217                if id == 0 {
218                    if let Some(x) = parse_i32(v) {
219                        key_wait_mode = x;
220                    }
221                }
222            }
223            let key_skip = match key_wait_mode {
224                0 => false,
225                1 => true,
226                _ => {
227                    ctx.globals
228                        .syscom
229                        .config_int
230                        .get(&197)
231                        .copied()
232                        .unwrap_or(0)
233                        != 0
234                }
235            };
236            ctx.wait.wait_wipe(key_skip);
237            return Ok(true);
238        }
239
240        "WAIT" | "SLEEP" => {
241            // Convention: WAIT(ms)
242            let ms = args
243                .iter()
244                .rev()
245                .find_map(|v| match v {
246                    Value::Int(x) => u64::try_from(*x).ok(),
247                    _ => None,
248                })
249                .unwrap_or(0);
250            if ms > 0 {
251                ctx.wait.wait_ms(ms);
252            }
253            return Ok(true);
254        }
255        "WAITKEY" | "WAIT_KEY" | "WAIT_KEYDOWN" | "WAITCLICK" | "WAIT_CLICK" => {
256            // Click and key share the same runtime path here.
257            ctx.wait.wait_key();
258            return Ok(true);
259        }
260        // Transition-ish commands: we can't animate yet, but we should honor timing.
261        "FADE" | "FADEIN" | "FADE_OUT" | "FADEOUT" | "TRANS" | "TRANSITION" | "DISSOLVE"
262        | "CROSSFADE" => {
263            let ms = args
264                .iter()
265                .rev()
266                .find_map(|v| match v {
267                    Value::Int(x) => u64::try_from(*x).ok(),
268                    _ => None,
269                })
270                .unwrap_or(0);
271            if ms > 0 {
272                ctx.wait.wait_ms(ms);
273            }
274            return Ok(true);
275        }
276        _ => {}
277    }
278
279    match name.as_str() {
280        "PAUSE" | "YIELD" => {
281            if let Some(ms) = pos
282                .first()
283                .and_then(|v| parse_i32(v))
284                .map(|v| v.max(0) as u64)
285            {
286                if ms > 0 {
287                    ctx.wait.wait_ms(ms);
288                } else {
289                    ctx.wait.wait_key();
290                }
291            } else {
292                ctx.wait.wait_key();
293            }
294            return Ok(true);
295        }
296        "AUTO" | "AUTO_ON" => {
297            ctx.globals.script.auto_mode_flag = true;
298            ctx.globals.script.skip_trigger = false;
299            return Ok(true);
300        }
301        "AUTO_OFF" => {
302            ctx.globals.script.auto_mode_flag = false;
303            return Ok(true);
304        }
305        "SKIP" | "SKIP_ON" => {
306            if !ctx.globals.script.skip_disable {
307                ctx.globals.script.skip_trigger = true;
308                ctx.globals.script.auto_mode_flag = false;
309            }
310            return Ok(true);
311        }
312        "SKIP_OFF" => {
313            ctx.globals.script.skip_trigger = false;
314            return Ok(true);
315        }
316        "SOUND" | "SOUND_ON" => {
317            for key in [212, 213, 214, 215, 216, 217, 218] {
318                ctx.globals.syscom.config_int.insert(key, 1);
319            }
320            syscom_form::apply_audio_config(ctx);
321            return Ok(true);
322        }
323        "SOUND_OFF" => {
324            for key in [212, 213, 214, 215, 216, 217, 218] {
325                ctx.globals.syscom.config_int.insert(key, 0);
326            }
327            syscom_form::apply_audio_config(ctx);
328            return Ok(true);
329        }
330        "LOG" | "PRINT" | "DEBUG" | "TRACE" => {
331            let mut parts: Vec<String> = Vec::new();
332            for v in &pos {
333                parts.push(match v {
334                    Value::Int(x) => x.to_string(),
335                    Value::Str(s) => s.clone(),
336                    Value::List(xs) => format!("{:?}", xs),
337                    _ => String::new(),
338                });
339            }
340            if parts.is_empty() {
341                parts.push(name.clone());
342            }
343            ctx.globals.system.debug_logs.push(parts.join(" "));
344            return Ok(true);
345        }
346        _ => {}
347    }
348
349    if is_noop_cmd(name.as_str()) {
350        return Ok(true);
351    }
352
353    if is_clear_cmd(name.as_str()) {
354        // Best-effort: clear render state.
355        ctx.layers.clear_all();
356        ctx.gfx = crate::runtime::graphics::GfxRuntime::new();
357        return Ok(true);
358    }
359
360    Ok(false)
361}
362
363fn resolve_mask_path(project_dir: &Path, raw: &str) -> Option<PathBuf> {
364    if raw.is_empty() {
365        return None;
366    }
367    let norm = raw.replace('\\', "/");
368    let p = Path::new(&norm);
369    if p.is_absolute() && p.is_file() {
370        return Some(p.to_path_buf());
371    }
372    let mut candidates = Vec::new();
373    candidates.push(project_dir.join(&norm));
374    candidates.push(project_dir.join("dat").join(&norm));
375    if p.extension().is_none() {
376        for ext in ["png", "bmp", "jpg"] {
377            candidates.push(project_dir.join(format!("{}.{}", norm, ext)));
378            candidates.push(project_dir.join("dat").join(format!("{}.{}", norm, ext)));
379        }
380    }
381    for c in candidates {
382        if c.is_file() {
383            return Some(c);
384        }
385    }
386    None
387}