Skip to main content

siglus_scene_vm/runtime/forms/
syscom.rs

1use anyhow::Result;
2
3use crate::runtime::globals::{
4    SaveSlotState, SyscomPendingProc, SyscomPendingProcKind, ToggleFeatureState, ValueFeatureState,
5};
6use crate::runtime::{CommandContext, RuntimeSaveKind, Value};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use crate::assets::RgbaImage;
11use crate::original_save::{self, SaveKind};
12
13use super::prop_access;
14
15use super::codes::syscom_op::*;
16
17struct Call<'a> {
18    op: i32,
19    params: &'a [Value],
20}
21
22fn parse_call<'a>(ctx: &CommandContext, form_id: u32, args: &'a [Value]) -> Option<Call<'a>> {
23    let (chain_pos, chain) = prop_access::parse_element_chain_ctx(ctx, form_id, args)?;
24    if chain.len() < 2 {
25        return None;
26    }
27    let params = prop_access::script_args(args, chain_pos);
28    Some(Call {
29        op: chain[1],
30        params,
31    })
32}
33
34fn p_i64(params: &[Value], idx: usize) -> i64 {
35    params.get(idx).and_then(|v| v.as_i64()).unwrap_or(0)
36}
37fn p_bool(params: &[Value], idx: usize) -> bool {
38    p_i64(params, idx) != 0
39}
40
41fn sg_debug_enabled_local() -> bool {
42    std::env::var_os("SG_DEBUG").is_some()
43}
44
45fn set_syscom_pending_proc(ctx: &mut CommandContext, kind: SyscomPendingProcKind) {
46    ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
47        kind,
48        warning: false,
49        se_play: false,
50        fade_out: false,
51        leave_msgbk: false,
52        save_id: 0,
53    });
54    ctx.globals.syscom.menu_open = false;
55    ctx.globals.syscom.menu_kind = None;
56    ctx.globals.syscom.menu_result = None;
57}
58
59fn gameexe_unquoted_owned(ctx: &CommandContext, key: &str) -> String {
60    ctx.tables
61        .gameexe
62        .as_ref()
63        .and_then(|cfg| cfg.get_unquoted(key))
64        .unwrap_or("")
65        .to_string()
66}
67
68
69fn gameexe_value_owned(ctx: &CommandContext, key: &str) -> String {
70    ctx.tables
71        .gameexe
72        .as_ref()
73        .and_then(|cfg| cfg.get_value(key))
74        .unwrap_or("")
75        .to_string()
76}
77
78fn parse_i64_list_local(raw: &str) -> Vec<i64> {
79    raw.split(|c: char| c == ',' || c.is_whitespace())
80        .filter_map(|part| {
81            let t = part.trim();
82            if t.is_empty() {
83                None
84            } else {
85                t.parse::<i64>().ok()
86            }
87        })
88        .collect()
89}
90
91fn parse_first_i64_local(raw: &str) -> Option<i64> {
92    raw.split(|c: char| c == ',' || c.is_whitespace())
93        .find_map(|part| {
94            let t = part.trim();
95            if t.is_empty() { None } else { t.parse::<i64>().ok() }
96        })
97}
98
99fn config_mouse_cursor_hide_onoff_default(ctx: &CommandContext) -> i64 {
100    parse_first_i64_local(&gameexe_value_owned(ctx, "CONFIG.MOUSE_CURSOR_HIDE_ONOFF"))
101        .unwrap_or(0)
102        .clamp(0, 1)
103}
104
105fn config_mouse_cursor_hide_time_default(ctx: &CommandContext) -> i64 {
106    parse_first_i64_local(&gameexe_value_owned(ctx, "CONFIG.MOUSE_CURSOR_HIDE_TIME"))
107        .unwrap_or(5000)
108        .max(0)
109}
110
111fn config_filter_color_default(ctx: &CommandContext) -> (i64, i64, i64, i64) {
112    let raw = gameexe_value_owned(ctx, "CONFIG.FILTER_COLOR");
113    let vals = parse_i64_list_local(&raw);
114    if vals.len() >= 4 {
115        (
116            vals[0].clamp(0, 255),
117            vals[1].clamp(0, 255),
118            vals[2].clamp(0, 255),
119            vals[3].clamp(0, 255),
120        )
121    } else {
122        (0, 0, 0, 128)
123    }
124}
125
126
127fn local_extra_index(params: &[Value]) -> usize {
128    p_i64(params, 0).clamp(0, 3) as usize
129}
130
131fn local_extra_value_param(params: &[Value]) -> bool {
132    let value_idx = if params.len() >= 2 { 1 } else { 0 };
133    p_bool(params, value_idx)
134}
135
136fn local_extra_i64_param(params: &[Value]) -> i64 {
137    let value_idx = if params.len() >= 2 { 1 } else { 0 };
138    p_i64(params, value_idx)
139}
140
141fn get_local_extra(op: i32, params: &[Value], st: &crate::runtime::globals::SyscomRuntimeState) -> Option<i64> {
142    let idx = local_extra_index(params);
143    let sw = st.local_extra_switches.get(idx).copied().unwrap_or(st.local_extra_switch);
144    let mode = st.local_extra_modes.get(idx).copied().unwrap_or(st.local_extra_mode);
145    Some(match op {
146        GET_LOCAL_EXTRA_SWITCH_ONOFF_FLAG => if sw.onoff { 1 } else { 0 },
147        GET_LOCAL_EXTRA_SWITCH_ENABLE_FLAG => if sw.enable { 1 } else { 0 },
148        GET_LOCAL_EXTRA_SWITCH_EXIST_FLAG => if sw.exist { 1 } else { 0 },
149        CHECK_LOCAL_EXTRA_SWITCH_ENABLE => sw.check_enabled(),
150        GET_LOCAL_EXTRA_MODE_VALUE => mode.value,
151        GET_LOCAL_EXTRA_MODE_ENABLE_FLAG => if mode.enable { 1 } else { 0 },
152        GET_LOCAL_EXTRA_MODE_EXIST_FLAG => if mode.exist { 1 } else { 0 },
153        CHECK_LOCAL_EXTRA_MODE_ENABLE => mode.check_enabled(),
154        _ => return None,
155    })
156}
157
158fn set_local_extra(op: i32, params: &[Value], st: &mut crate::runtime::globals::SyscomRuntimeState) -> bool {
159    let idx = local_extra_index(params);
160    let value = local_extra_value_param(params);
161    match op {
162        SET_LOCAL_EXTRA_SWITCH_ONOFF_FLAG => st.local_extra_switches[idx].onoff = value,
163        SET_LOCAL_EXTRA_SWITCH_ENABLE_FLAG => st.local_extra_switches[idx].enable = value,
164        SET_LOCAL_EXTRA_SWITCH_EXIST_FLAG => st.local_extra_switches[idx].exist = value,
165        SET_LOCAL_EXTRA_MODE_ENABLE_FLAG => st.local_extra_modes[idx].enable = value,
166        SET_LOCAL_EXTRA_MODE_EXIST_FLAG => st.local_extra_modes[idx].exist = value,
167        SET_LOCAL_EXTRA_MODE_VALUE => st.local_extra_modes[idx].value = local_extra_i64_param(params),
168        _ => return false,
169    }
170    st.local_extra_switch = st.local_extra_switches[0];
171    st.local_extra_mode = st.local_extra_modes[0];
172    true
173}
174
175fn get_toggle_get(op: i32, st: &crate::runtime::globals::SyscomRuntimeState) -> Option<i64> {
176    Some(match op {
177        GET_READ_SKIP_ONOFF_FLAG => {
178            if st.read_skip.onoff {
179                1
180            } else {
181                0
182            }
183        }
184        GET_READ_SKIP_ENABLE_FLAG => {
185            if st.read_skip.enable {
186                1
187            } else {
188                0
189            }
190        }
191        GET_READ_SKIP_EXIST_FLAG => {
192            if st.read_skip.exist {
193                1
194            } else {
195                0
196            }
197        }
198        CHECK_READ_SKIP_ENABLE => st.read_skip.check_enabled(),
199        GET_AUTO_SKIP_ONOFF_FLAG => {
200            if st.auto_skip.onoff {
201                1
202            } else {
203                0
204            }
205        }
206        GET_AUTO_SKIP_ENABLE_FLAG => {
207            if st.auto_skip.enable {
208                1
209            } else {
210                0
211            }
212        }
213        GET_AUTO_SKIP_EXIST_FLAG => {
214            if st.auto_skip.exist {
215                1
216            } else {
217                0
218            }
219        }
220        CHECK_AUTO_SKIP_ENABLE => st.auto_skip.check_enabled(),
221        GET_AUTO_MODE_ONOFF_FLAG => {
222            if st.auto_mode.onoff {
223                1
224            } else {
225                0
226            }
227        }
228        GET_AUTO_MODE_ENABLE_FLAG => {
229            if st.auto_mode.enable {
230                1
231            } else {
232                0
233            }
234        }
235        GET_AUTO_MODE_EXIST_FLAG => {
236            if st.auto_mode.exist {
237                1
238            } else {
239                0
240            }
241        }
242        CHECK_AUTO_MODE_ENABLE => st.auto_mode.check_enabled(),
243        GET_HIDE_MWND_ONOFF_FLAG => {
244            if st.hide_mwnd.onoff {
245                1
246            } else {
247                0
248            }
249        }
250        GET_HIDE_MWND_ENABLE_FLAG => {
251            if st.hide_mwnd.enable {
252                1
253            } else {
254                0
255            }
256        }
257        GET_HIDE_MWND_EXIST_FLAG => {
258            if st.hide_mwnd.exist {
259                1
260            } else {
261                0
262            }
263        }
264        CHECK_HIDE_MWND_ENABLE => st.hide_mwnd.check_enabled(),
265        GET_MSG_BACK_ENABLE_FLAG => {
266            if st.msg_back.enable {
267                1
268            } else {
269                0
270            }
271        }
272        GET_MSG_BACK_EXIST_FLAG => {
273            if st.msg_back.exist {
274                1
275            } else {
276                0
277            }
278        }
279        CHECK_MSG_BACK_ENABLE => st.msg_back.check_enabled(),
280        CHECK_MSG_BACK_OPEN => {
281            if st.msg_back_open {
282                1
283            } else {
284                0
285            }
286        }
287        GET_RETURN_TO_SEL_ENABLE_FLAG => {
288            if st.return_to_sel.enable {
289                1
290            } else {
291                0
292            }
293        }
294        GET_RETURN_TO_SEL_EXIST_FLAG => {
295            if st.return_to_sel.exist {
296                1
297            } else {
298                0
299            }
300        }
301        CHECK_RETURN_TO_SEL_ENABLE => st.return_to_sel.check_enabled(),
302        GET_RETURN_TO_MENU_ENABLE_FLAG => {
303            if st.return_to_menu.enable {
304                1
305            } else {
306                0
307            }
308        }
309        GET_RETURN_TO_MENU_EXIST_FLAG => {
310            if st.return_to_menu.exist {
311                1
312            } else {
313                0
314            }
315        }
316        CHECK_RETURN_TO_MENU_ENABLE => st.return_to_menu.check_enabled(),
317        GET_END_GAME_ENABLE_FLAG => {
318            if st.end_game.enable {
319                1
320            } else {
321                0
322            }
323        }
324        GET_END_GAME_EXIST_FLAG => {
325            if st.end_game.exist {
326                1
327            } else {
328                0
329            }
330        }
331        CHECK_END_GAME_ENABLE => st.end_game.check_enabled(),
332        GET_SAVE_ENABLE_FLAG => {
333            if st.save_feature.enable {
334                1
335            } else {
336                0
337            }
338        }
339        GET_SAVE_EXIST_FLAG => {
340            if st.save_feature.exist {
341                1
342            } else {
343                0
344            }
345        }
346        CHECK_SAVE_ENABLE => st.save_feature.check_enabled(),
347        GET_LOAD_ENABLE_FLAG => {
348            if st.load_feature.enable {
349                1
350            } else {
351                0
352            }
353        }
354        GET_LOAD_EXIST_FLAG => {
355            if st.load_feature.exist {
356                1
357            } else {
358                0
359            }
360        }
361        CHECK_LOAD_ENABLE => st.load_feature.check_enabled(),
362        _ => return None,
363    })
364}
365
366fn apply_toggle_set(
367    op: i32,
368    v: bool,
369    st: &mut crate::runtime::globals::SyscomRuntimeState,
370) -> bool {
371    match op {
372        SET_READ_SKIP_ONOFF_FLAG => st.read_skip.onoff = v,
373        SET_READ_SKIP_ENABLE_FLAG => st.read_skip.enable = v,
374        SET_READ_SKIP_EXIST_FLAG => st.read_skip.exist = v,
375        SET_AUTO_SKIP_ONOFF_FLAG => st.auto_skip.onoff = v,
376        SET_AUTO_SKIP_ENABLE_FLAG => st.auto_skip.enable = v,
377        SET_AUTO_SKIP_EXIST_FLAG => st.auto_skip.exist = v,
378        SET_AUTO_MODE_ONOFF_FLAG => st.auto_mode.onoff = v,
379        SET_AUTO_MODE_ENABLE_FLAG => st.auto_mode.enable = v,
380        SET_AUTO_MODE_EXIST_FLAG => st.auto_mode.exist = v,
381        SET_HIDE_MWND_ONOFF_FLAG => st.hide_mwnd.onoff = v,
382        SET_HIDE_MWND_ENABLE_FLAG => st.hide_mwnd.enable = v,
383        SET_HIDE_MWND_EXIST_FLAG => st.hide_mwnd.exist = v,
384        SET_MSG_BACK_ENABLE_FLAG => st.msg_back.enable = v,
385        SET_MSG_BACK_EXIST_FLAG => st.msg_back.exist = v,
386        SET_RETURN_TO_SEL_ENABLE_FLAG => st.return_to_sel.enable = v,
387        SET_RETURN_TO_SEL_EXIST_FLAG => st.return_to_sel.exist = v,
388        SET_RETURN_TO_MENU_ENABLE_FLAG => st.return_to_menu.enable = v,
389        SET_RETURN_TO_MENU_EXIST_FLAG => st.return_to_menu.exist = v,
390        SET_END_GAME_ENABLE_FLAG => st.end_game.enable = v,
391        SET_END_GAME_EXIST_FLAG => st.end_game.exist = v,
392        SET_SAVE_ENABLE_FLAG => st.save_feature.enable = v,
393        SET_SAVE_EXIST_FLAG => st.save_feature.exist = v,
394        SET_LOAD_ENABLE_FLAG => st.load_feature.enable = v,
395        SET_LOAD_EXIST_FLAG => st.load_feature.exist = v,
396        _ => return false,
397    }
398    true
399}
400
401
402fn truncate_to_chars(s: &mut String, max_chars: usize) {
403    if s.chars().count() > max_chars {
404        *s = s.chars().take(max_chars).collect();
405    }
406}
407
408/// Equivalent to C++ `C_tnm_eng::save_local_msg`. Appends to the live
409/// `current_save_message` / `current_save_full_message` and, when a savepoint
410/// snapshot exists, mirrors the append into `m_local_save.save_msg`/`save_full_msg`
411/// without rebuilding the saved stream. Length caps mirror `TNM_SAVE_MESSAGE_MAX_LEN`
412/// / `TNM_SAVE_FULL_MESSAGE_MAX_LEN` (256 TCHARs).
413pub(crate) fn append_current_save_message(ctx: &mut CommandContext, msg: &str) {
414    if msg.is_empty() {
415        return;
416    }
417    ctx.globals.syscom.current_save_message.push_str(msg);
418    truncate_to_chars(&mut ctx.globals.syscom.current_save_message, 256);
419    ctx.globals.syscom.current_save_full_message.push_str(msg);
420    truncate_to_chars(&mut ctx.globals.syscom.current_save_full_message, 256);
421
422    if let Some(snapshot) = ctx.local_save_snapshot.as_mut() {
423        snapshot.save_msg.push_str(msg);
424        truncate_to_chars(&mut snapshot.save_msg, 256);
425        snapshot.save_full_msg.push_str(msg);
426        truncate_to_chars(&mut snapshot.save_full_msg, 256);
427    }
428}
429
430fn save_load_trace_enabled() -> bool {
431    std::env::var_os("SG_SAVELOAD_TRACE").is_some()
432}
433
434fn trace_save_load_event(ctx: &CommandContext, label: &str, quick: bool, idx: usize, path: Option<&Path>) {
435    if !save_load_trace_enabled() {
436        return;
437    }
438    let kind = if quick { "quick" } else { "normal" };
439    if let Some(path) = path {
440        eprintln!(
441            "[SG_SAVELOAD_TRACE][SYSCOM] {label} kind={kind} idx={idx} path={} exists={}",
442            path.display(),
443            path.exists()
444        );
445    } else {
446        eprintln!("[SG_SAVELOAD_TRACE][SYSCOM] {label} kind={kind} idx={idx}");
447    }
448}
449
450fn ensure_slot(slots: &mut Vec<SaveSlotState>, idx: usize) -> &mut SaveSlotState {
451    if slots.len() <= idx {
452        slots.resize_with(idx + 1, SaveSlotState::default);
453    }
454    &mut slots[idx]
455}
456
457pub fn menu_save_slot(ctx: &mut CommandContext, quick: bool, idx: usize) {
458    let path = slot_path_with_counts(
459        &ctx.project_dir,
460        quick,
461        idx,
462        configured_save_count(ctx, false),
463        configured_save_count(ctx, true),
464    );
465    trace_save_load_event(ctx, "menu_save_slot", quick, idx, Some(&path));
466    // Do NOT pre-populate the slot from current runtime state here. The VM-level
467    // perform_runtime_save_request consumes the local_save_snapshot built at
468    // SAVEPOINT time to write both the header and the slot fields. Pre-writing
469    // slot data from the currently-open save/load menu (its title/append/etc.)
470    // is exactly the original bug.
471    let kind = if quick { RuntimeSaveKind::Quick } else { RuntimeSaveKind::Normal };
472    ctx.request_runtime_save(kind, idx);
473}
474
475pub fn menu_load_slot(ctx: &mut CommandContext, quick: bool, idx: usize) {
476    let path = slot_path_with_counts(
477        &ctx.project_dir,
478        quick,
479        idx,
480        configured_save_count(ctx, false),
481        configured_save_count(ctx, true),
482    );
483    trace_save_load_event(ctx, "menu_load_slot", quick, idx, Some(&path));
484    let save_cnt = configured_save_count(ctx, false);
485    let quick_cnt = configured_save_count(ctx, true);
486    if quick {
487        ensure_slot_loaded_with_counts(
488            &ctx.project_dir,
489            true,
490            save_cnt,
491            quick_cnt,
492            &mut ctx.globals.syscom.quick_save_slots,
493            idx,
494        );
495        ctx.request_runtime_load(RuntimeSaveKind::Quick, idx);
496    } else {
497        ensure_slot_loaded_with_counts(
498            &ctx.project_dir,
499            false,
500            save_cnt,
501            quick_cnt,
502            &mut ctx.globals.syscom.save_slots,
503            idx,
504        );
505        ctx.request_runtime_load(RuntimeSaveKind::Normal, idx);
506    }
507}
508
509fn saveload_alert_on(ctx: &CommandContext) -> bool {
510    cfg_get_int(&ctx.globals.syscom, GET_SAVELOAD_ALERT_ONOFF, 1) != 0
511}
512
513fn slot_exists_for_menu_action(ctx: &mut CommandContext, quick: bool, idx: usize) -> bool {
514    let save_cnt = configured_save_count(ctx, false);
515    let quick_cnt = configured_save_count(ctx, true);
516    if quick {
517        ensure_slot_loaded_with_counts(
518            &ctx.project_dir,
519            true,
520            save_cnt,
521            quick_cnt,
522            &mut ctx.globals.syscom.quick_save_slots,
523            idx,
524        );
525        ctx.globals
526            .syscom
527            .quick_save_slots
528            .get(idx)
529            .map(|slot| slot.exist)
530            .unwrap_or(false)
531    } else {
532        ensure_slot_loaded_with_counts(
533            &ctx.project_dir,
534            false,
535            save_cnt,
536            quick_cnt,
537            &mut ctx.globals.syscom.save_slots,
538            idx,
539        );
540        ctx.globals
541            .syscom
542            .save_slots
543            .get(idx)
544            .map(|slot| slot.exist)
545            .unwrap_or(false)
546    }
547}
548
549fn request_confirmed_save_or_load(
550    ctx: &mut CommandContext,
551    kind: SyscomPendingProcKind,
552    idx: usize,
553    warning: bool,
554    needs_existing_slot: bool,
555) -> bool {
556    if warning && saveload_alert_on(ctx) && (!needs_existing_slot || slot_exists_for_menu_action(ctx, matches!(kind, SyscomPendingProcKind::QuickSave | SyscomPendingProcKind::QuickLoad), idx)) {
557        ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
558            kind,
559            warning: true,
560            se_play: false,
561            fade_out: false,
562            leave_msgbk: false,
563            save_id: idx as i64,
564        });
565        ctx.globals.syscom.menu_open = false;
566        ctx.globals.syscom.menu_kind = None;
567        ctx.globals.syscom.menu_result = None;
568        true
569    } else {
570        false
571    }
572}
573
574fn local_save_available(ctx: &CommandContext) -> bool {
575    ctx.local_save_snapshot
576        .as_ref()
577        .map(|s| !s.local_stream.is_empty())
578        .unwrap_or(false)
579        || ctx.pending_auto_savepoint
580}
581
582fn local_save_file_exists(ctx: &CommandContext, kind: SaveKind, idx: usize) -> bool {
583    let path = original_save::save_file_path_with_counts(
584        &ctx.project_dir,
585        configured_save_count(ctx, false),
586        configured_save_count(ctx, true),
587        kind,
588        idx,
589    );
590    original_save::read_slot_from_path(&path)
591        .map(|slot| slot.exist)
592        .unwrap_or(false)
593}
594
595pub(crate) fn save_dir(project_dir: &Path) -> PathBuf {
596    original_save::save_dir(project_dir)
597}
598
599fn slot_path_with_counts(project_dir: &Path, quick: bool, idx: usize, save_cnt: usize, quick_cnt: usize) -> PathBuf {
600    let kind = if quick { SaveKind::Quick } else { SaveKind::Normal };
601    original_save::save_file_path_with_counts(project_dir, save_cnt, quick_cnt, kind, idx)
602}
603
604
605#[derive(Clone, Copy, Debug, PartialEq, Eq)]
606enum SaveThumbType {
607    Bmp,
608    Png,
609}
610
611#[derive(Clone, Copy, Debug)]
612struct SaveThumbConfig {
613    enabled: bool,
614    thumb_type: SaveThumbType,
615    width: u32,
616    height: u32,
617}
618
619fn save_thumb_config(ctx: &CommandContext) -> SaveThumbConfig {
620    let mut out = SaveThumbConfig {
621        enabled: false,
622        thumb_type: SaveThumbType::Bmp,
623        width: 200,
624        height: 150,
625    };
626
627    let Some(cfg) = ctx.tables.gameexe.as_ref() else {
628        return out;
629    };
630
631    out.enabled = cfg
632        .get_usize("#SAVE_THUMB.USE")
633        .or_else(|| cfg.get_usize("SAVE_THUMB.USE"))
634        .unwrap_or(0)
635        != 0;
636    out.thumb_type = match cfg
637        .get_usize("#SAVE_THUMB.TYPE")
638        .or_else(|| cfg.get_usize("SAVE_THUMB.TYPE"))
639        .unwrap_or(0)
640    {
641        1 => SaveThumbType::Png,
642        _ => SaveThumbType::Bmp,
643    };
644
645    if let Some(entry) = cfg.get_entry("#SAVE_THUMB.SIZE").or_else(|| cfg.get_entry("SAVE_THUMB.SIZE")) {
646        let w = entry.item_unquoted(0).and_then(|v| v.trim().parse::<u32>().ok()).unwrap_or(out.width);
647        let h = entry.item_unquoted(1).and_then(|v| v.trim().parse::<u32>().ok()).unwrap_or(out.height);
648        if w != 0 && h != 0 {
649            out.width = w;
650            out.height = h;
651        }
652    }
653
654    out
655}
656
657pub(crate) fn thumb_candidate_paths(dir: &Path, idx: i64) -> [PathBuf; 2] {
658    let project_dir = dir.parent().unwrap_or(dir);
659    original_save::thumb_candidate_paths_for_no(project_dir, idx.max(0) as usize)
660}
661
662fn thumb_path_for_no_with_config(project_dir: &Path, config: SaveThumbConfig, save_no: usize) -> PathBuf {
663    let stem = format!("{save_no:04}");
664    let ext = match config.thumb_type {
665        SaveThumbType::Bmp => "bmp",
666        SaveThumbType::Png => "png",
667    };
668    save_dir(project_dir).join(format!("{stem}.{ext}"))
669}
670
671fn pick_thumb_source_name(ctx: &CommandContext) -> Option<String> {
672    let table = ctx.tables.thumb_table.as_ref()?;
673    let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
674    form_ids.sort_unstable();
675    for form_id in form_ids {
676        let Some(stage) = ctx.globals.stage_forms.get(&form_id) else {
677            continue;
678        };
679        let mut stage_ids: Vec<i64> = stage.object_lists.keys().copied().collect();
680        stage_ids.sort_unstable();
681        for stage_idx in stage_ids {
682            let Some(objs) = stage.object_lists.get(&stage_idx) else {
683                continue;
684            };
685            for obj in objs.iter().rev() {
686                if let Some(file) = obj.file_name.as_deref() {
687                    if let Some(mapped) = table.get_by_file_stem(file) {
688                        return Some(mapped.clone());
689                    }
690                }
691            }
692        }
693    }
694    None
695}
696
697fn capture_slot_thumb(ctx: &mut CommandContext, config: SaveThumbConfig) -> RgbaImage {
698    if let Some(name) = pick_thumb_source_name(ctx) {
699        if let Ok(img_id) = ctx.images.load_g00(&name, 0) {
700            if let Some(img) = ctx.images.get(img_id) {
701                return resize_rgba(img.as_ref(), config.width, config.height);
702            }
703        }
704    }
705
706    let img = ctx.capture_frame_rgba();
707    resize_rgba(&img, config.width, config.height)
708}
709
710pub(crate) fn prepare_runtime_save_thumb_capture(ctx: &mut CommandContext) {
711    let config = save_thumb_config(ctx);
712    if !config.enabled {
713        ctx.globals.save_thumb_capture_image = None;
714        return;
715    }
716
717    ctx.globals.save_thumb_capture_image = Some(capture_slot_thumb(ctx, config));
718}
719
720pub(crate) fn prepare_runtime_save_thumb_capture_from_image(
721    ctx: &mut CommandContext,
722    img: &RgbaImage,
723) {
724    let config = save_thumb_config(ctx);
725    if !config.enabled {
726        ctx.globals.save_thumb_capture_image = None;
727        return;
728    }
729
730    ctx.globals.save_thumb_capture_image = Some(resize_rgba(img, config.width, config.height));
731}
732
733fn write_slot_thumb_for_save_no(ctx: &mut CommandContext, save_no: usize) {
734    let config = save_thumb_config(ctx);
735    if !config.enabled {
736        return;
737    }
738    let Some(img) = ctx.globals.save_thumb_capture_image.clone() else {
739        ctx.unknown
740            .record_note(&format!("save_thumb.capture.missing:{save_no}"));
741        return;
742    };
743    let path = thumb_path_for_no_with_config(&ctx.project_dir, config, save_no);
744    if save_load_trace_enabled() {
745        eprintln!(
746            "[SG_SAVELOAD_TRACE][SYSCOM] write_slot_thumb save_no={} path={} size={}x{} type={:?}",
747            save_no,
748            path.display(),
749            img.width,
750            img.height,
751            config.thumb_type
752        );
753    }
754    let _ = fs::remove_file(&path);
755    let result = match config.thumb_type {
756        SaveThumbType::Bmp => write_rgba_bmp_top_down(&path, &img),
757        SaveThumbType::Png => write_rgba_png_opaque(&path, &img),
758    };
759    if let Err(err) = result {
760        eprintln!("[SG_SAVE] failed to write save thumb {}: {err:#}", path.display());
761    }
762}
763
764pub(crate) fn write_runtime_slot_thumb(ctx: &mut CommandContext, save_no: usize) {
765    write_slot_thumb_for_save_no(ctx, save_no);
766}
767
768fn escape_str(s: &str) -> String {
769    let mut out = String::with_capacity(s.len());
770    for ch in s.chars() {
771        match ch {
772            '\\' => out.push_str("\\\\"),
773            '\n' => out.push_str("\\n"),
774            '\r' => out.push_str("\\r"),
775            '\t' => out.push_str("\\t"),
776            _ => out.push(ch),
777        }
778    }
779    out
780}
781
782fn unescape_str(s: &str) -> String {
783    let mut out = String::with_capacity(s.len());
784    let mut it = s.chars();
785    while let Some(ch) = it.next() {
786        if ch == '\\' {
787            match it.next() {
788                Some('n') => out.push('\n'),
789                Some('r') => out.push('\r'),
790                Some('t') => out.push('\t'),
791                Some('\\') => out.push('\\'),
792                Some(other) => out.push(other),
793                None => break,
794            }
795        } else {
796            out.push(ch);
797        }
798    }
799    out
800}
801
802fn write_slot(path: &Path, slot: &SaveSlotState) {
803    if let Err(err) = original_save::write_slot_file(path, slot) {
804        eprintln!("[SG_SAVE] failed to write original save file {}: {err:#}", path.display());
805    }
806}
807
808fn read_slot(path: &Path) -> Option<SaveSlotState> {
809    original_save::read_slot_from_path(path)
810}
811
812pub fn write_global_save(ctx: &CommandContext) {
813    let mut stream = original_save::OriginalStreamWriter::new();
814    stream.push_i64(ctx.globals.syscom.total_play_time);
815
816    let fixed_flag_cnt = ctx
817        .tables
818        .gameexe
819        .as_ref()
820        .and_then(|cfg| cfg.get_usize("#GLOBAL_FLAG.CNT").or_else(|| cfg.get_usize("GLOBAL_FLAG.CNT")))
821        .unwrap_or(1000)
822        .min(10000);
823    let cg_flag_cnt = ctx
824        .tables
825        .cgtable_flag_cnt
826        .or_else(|| {
827            ctx.tables
828                .gameexe
829                .as_ref()
830                .and_then(|cfg| cfg.get_usize("#CGTABLE_FLAG_CNT").or_else(|| cfg.get_usize("CGTABLE_FLAG_CNT")))
831        })
832        .unwrap_or(ctx.tables.cg_flags.len())
833        .max(ctx.tables.cg_flags.len());
834    let bgm_cnt = ctx
835        .tables
836        .gameexe
837        .as_ref()
838        .and_then(|cfg| cfg.get_usize("#BGM.CNT").or_else(|| cfg.get_usize("BGM.CNT")))
839        .unwrap_or(32)
840        .max(ctx.globals.bgm_table_flags.len());
841    let g = ctx
842        .globals
843        .int_lists
844        .get(&(crate::runtime::forms::codes::ELM_GLOBAL_G as u32))
845        .map(Vec::as_slice)
846        .unwrap_or(&[]);
847    let z = ctx
848        .globals
849        .int_lists
850        .get(&(crate::runtime::forms::codes::ELM_GLOBAL_Z as u32))
851        .map(Vec::as_slice)
852        .unwrap_or(&[]);
853    let m = ctx
854        .globals
855        .str_lists
856        .get(&(crate::runtime::forms::codes::ELM_GLOBAL_M as u32))
857        .map(Vec::as_slice)
858        .unwrap_or(&[]);
859    let namae_global = ctx
860        .globals
861        .str_lists
862        .get(&(crate::runtime::forms::codes::ELM_GLOBAL_NAMAE_GLOBAL as u32))
863        .map(Vec::as_slice)
864        .unwrap_or(&[]);
865
866    stream.push_fixed_i32_list(g, fixed_flag_cnt);
867    stream.push_fixed_i32_list(z, fixed_flag_cnt);
868    stream.push_fixed_str_list(m, fixed_flag_cnt);
869    stream.push_fixed_str_list(namae_global, 26 + 26 * 26);
870    stream.push_i32(0);
871
872    let cg_flags: Vec<i64> = ctx.tables.cg_flags.iter().map(|v| *v as i64).collect();
873    stream.push_fixed_i32_list(&cg_flags, cg_flag_cnt);
874
875    let bgm_flags: Vec<i64> = ctx
876        .globals
877        .bgm_table_flags
878        .iter()
879        .map(|v| if *v { 1 } else { 0 })
880        .collect();
881    stream.push_fixed_i32_list(&bgm_flags, bgm_cnt);
882
883    // C++ twitter_save_state persists registry values only and writes nothing
884    // to this stream; Stream/Twitter remains intentionally unsupported here.
885    stream.push_i32(0);
886
887    let payload = stream.into_inner();
888    if let Err(err) = original_save::write_global_save_file(&ctx.project_dir, &payload) {
889        eprintln!("[SG_SAVE] failed to write global.sav: {err:#}");
890    }
891}
892
893fn load_global_save(ctx: &mut CommandContext) {
894    let Ok(payload) = original_save::read_global_save_file(&ctx.project_dir) else {
895        return;
896    };
897    let mut rd = original_save::OriginalStreamReader::new(&payload);
898    let Ok(total_play_time) = rd.i64() else {
899        return;
900    };
901    ctx.globals.syscom.total_play_time = total_play_time;
902    if let Ok(g) = rd.fixed_i32_list() {
903        ctx.globals
904            .int_lists
905            .insert(crate::runtime::forms::codes::ELM_GLOBAL_G as u32, g);
906    } else {
907        return;
908    }
909    if let Ok(z) = rd.fixed_i32_list() {
910        ctx.globals
911            .int_lists
912            .insert(crate::runtime::forms::codes::ELM_GLOBAL_Z as u32, z);
913    } else {
914        return;
915    }
916    if let Ok(m) = rd.fixed_str_list() {
917        ctx.globals
918            .str_lists
919            .insert(crate::runtime::forms::codes::ELM_GLOBAL_M as u32, m);
920    } else {
921        return;
922    }
923    if let Ok(namae_global) = rd.fixed_str_list() {
924        ctx.globals.str_lists.insert(
925            crate::runtime::forms::codes::ELM_GLOBAL_NAMAE_GLOBAL as u32,
926            namae_global,
927        );
928    }
929    let _ = rd.i32();
930    if let Ok(cg) = rd.fixed_i32_list() {
931        ctx.tables.cg_flags = cg.into_iter().map(|v| if v != 0 { 1 } else { 0 }).collect();
932    }
933    if let Ok(bgm) = rd.fixed_i32_list() {
934        ctx.globals.bgm_table_flags = bgm.into_iter().map(|v| v != 0).collect();
935    }
936    if let Ok(chrkoe_cnt) = rd.i32() {
937        for _ in 0..chrkoe_cnt.max(0) {
938            let _ = rd.string();
939            let _ = rd.i32();
940        }
941    }
942}
943
944
945fn ensure_slot_loaded_with_counts(
946    project_dir: &Path,
947    quick: bool,
948    save_cnt: usize,
949    quick_cnt: usize,
950    slots: &mut Vec<SaveSlotState>,
951    idx: usize,
952) {
953    let path = slot_path_with_counts(project_dir, quick, idx, save_cnt, quick_cnt);
954    if save_load_trace_enabled() {
955        let before_exist = slots.get(idx).map(|s| s.exist).unwrap_or(false);
956        eprintln!(
957            "[SG_SAVELOAD_TRACE][SYSCOM] ensure_slot_loaded kind={} idx={} path={} file_exists={} cached_exist={}",
958            if quick { "quick" } else { "normal" },
959            idx,
960            path.display(),
961            path.exists(),
962            before_exist
963        );
964    }
965    if let Some(slot) = read_slot(&path) {
966        let s = ensure_slot(slots, idx);
967        *s = slot;
968        return;
969    }
970    if !slots.get(idx).map(|s| s.exist).unwrap_or(false) {
971        let s = ensure_slot(slots, idx);
972        *s = SaveSlotState::default();
973    }
974}
975
976
977fn reload_slot_from_disk_with_counts(
978    project_dir: &Path,
979    quick: bool,
980    save_cnt: usize,
981    quick_cnt: usize,
982    slots: &mut Vec<SaveSlotState>,
983    idx: usize,
984) {
985    let path = slot_path_with_counts(project_dir, quick, idx, save_cnt, quick_cnt);
986    let next = read_slot(&path).unwrap_or_default();
987    let s = ensure_slot(slots, idx);
988    *s = next;
989}
990
991fn sync_slots_from_disk_with_counts(
992    project_dir: &Path,
993    quick: bool,
994    save_cnt: usize,
995    quick_cnt: usize,
996    slots: &mut Vec<SaveSlotState>,
997    count: usize,
998) {
999    if slots.len() < count {
1000        slots.resize_with(count, SaveSlotState::default);
1001    }
1002    for idx in 0..count {
1003        reload_slot_from_disk_with_counts(project_dir, quick, save_cnt, quick_cnt, slots, idx);
1004    }
1005}
1006
1007pub(crate) fn sync_save_slots_from_disk(ctx: &mut CommandContext, quick: bool) {
1008    if save_load_trace_enabled() {
1009        eprintln!(
1010            "[SG_SAVELOAD_TRACE][SYSCOM] sync_save_slots_from_disk kind={}",
1011            if quick { "quick" } else { "normal" }
1012        );
1013    }
1014    let project_dir = ctx.project_dir.clone();
1015    let save_cnt = configured_save_count(ctx, false);
1016    let quick_cnt = configured_save_count(ctx, true);
1017    if quick {
1018        sync_slots_from_disk_with_counts(
1019            &project_dir,
1020            true,
1021            save_cnt,
1022            quick_cnt,
1023            &mut ctx.globals.syscom.quick_save_slots,
1024            quick_cnt,
1025        );
1026    } else {
1027        sync_slots_from_disk_with_counts(
1028            &project_dir,
1029            false,
1030            save_cnt,
1031            quick_cnt,
1032            &mut ctx.globals.syscom.save_slots,
1033            save_cnt,
1034        );
1035    }
1036}
1037
1038fn persist_slot_with_counts(
1039    project_dir: &Path,
1040    quick: bool,
1041    save_cnt: usize,
1042    quick_cnt: usize,
1043    slots: &[SaveSlotState],
1044    idx: usize,
1045) {
1046    if let Some(slot) = slots.get(idx) {
1047        let path = slot_path_with_counts(project_dir, quick, idx, save_cnt, quick_cnt);
1048        if path.exists() {
1049            match original_save::read_header_from_path(&path) {
1050                Ok(old_header) => {
1051                    let header = original_save::OriginalSaveHeader::from_slot(
1052                        slot,
1053                        old_header.data_size.max(0) as usize,
1054                    );
1055                    if let Err(err) = original_save::write_header_in_place(&path, &header) {
1056                        eprintln!(
1057                            "[SG_SAVE] failed to update original save header {}: {err:#}",
1058                            path.display()
1059                        );
1060                    }
1061                    return;
1062                }
1063                Err(err) => {
1064                    eprintln!(
1065                        "[SG_SAVE] failed to read original save header {}: {err:#}",
1066                        path.display()
1067                    );
1068                }
1069            }
1070        }
1071        write_slot(&path, slot);
1072    }
1073}
1074
1075
1076fn slot_thumb_save_no(save_cnt: usize, quick_cnt: usize, quick: bool, idx: usize) -> usize {
1077    let kind = if quick { SaveKind::Quick } else { SaveKind::Normal };
1078    original_save::original_save_no(save_cnt, quick_cnt, kind, idx)
1079}
1080
1081fn remove_thumb_file(project_dir: &Path, save_cnt: usize, quick_cnt: usize, quick: bool, config: SaveThumbConfig, idx: usize) {
1082    if !config.enabled {
1083        return;
1084    }
1085    let save_no = slot_thumb_save_no(save_cnt, quick_cnt, quick, idx);
1086    let _ = fs::remove_file(thumb_path_for_no_with_config(project_dir, config, save_no));
1087}
1088
1089fn copy_thumb_file(project_dir: &Path, save_cnt: usize, quick_cnt: usize, quick: bool, config: SaveThumbConfig, src: usize, dst: usize) {
1090    if !config.enabled {
1091        return;
1092    }
1093    let src_no = slot_thumb_save_no(save_cnt, quick_cnt, quick, src);
1094    let dst_no = slot_thumb_save_no(save_cnt, quick_cnt, quick, dst);
1095    let src_path = thumb_path_for_no_with_config(project_dir, config, src_no);
1096    let dst_path = thumb_path_for_no_with_config(project_dir, config, dst_no);
1097    if src_path.exists() {
1098        if let Some(parent) = dst_path.parent() {
1099            let _ = fs::create_dir_all(parent);
1100        }
1101        let _ = fs::copy(src_path, dst_path);
1102    } else {
1103        let _ = fs::remove_file(dst_path);
1104    }
1105}
1106
1107fn swap_thumb_file(project_dir: &Path, save_cnt: usize, quick_cnt: usize, quick: bool, config: SaveThumbConfig, a: usize, b: usize) {
1108    if !config.enabled || a == b {
1109        return;
1110    }
1111    let a_no = slot_thumb_save_no(save_cnt, quick_cnt, quick, a);
1112    let b_no = slot_thumb_save_no(save_cnt, quick_cnt, quick, b);
1113    let pa = thumb_path_for_no_with_config(project_dir, config, a_no);
1114    let pb = thumb_path_for_no_with_config(project_dir, config, b_no);
1115    let tmp = pa.with_extension(format!(
1116        "{}.swap",
1117        pa.extension().and_then(|v| v.to_str()).unwrap_or("tmp")
1118    ));
1119    let a_exists = pa.exists();
1120    let b_exists = pb.exists();
1121    if a_exists {
1122        let _ = fs::rename(&pa, &tmp);
1123    }
1124    if b_exists {
1125        if let Some(parent) = pa.parent() {
1126            let _ = fs::create_dir_all(parent);
1127        }
1128        let _ = fs::rename(&pb, &pa);
1129    } else {
1130        let _ = fs::remove_file(&pa);
1131    }
1132    if a_exists {
1133        if let Some(parent) = pb.parent() {
1134            let _ = fs::create_dir_all(parent);
1135        }
1136        let _ = fs::rename(&tmp, &pb);
1137    } else {
1138        let _ = fs::remove_file(&pb);
1139    }
1140    let _ = fs::remove_file(&tmp);
1141}
1142
1143fn copy_save_file(project_dir: &Path, quick: bool, save_cnt: usize, quick_cnt: usize, src: usize, dst: usize) {
1144    let src_path = slot_path_with_counts(project_dir, quick, src, save_cnt, quick_cnt);
1145    let dst_path = slot_path_with_counts(project_dir, quick, dst, save_cnt, quick_cnt);
1146    if src_path.exists() {
1147        if let Some(parent) = dst_path.parent() {
1148            let _ = fs::create_dir_all(parent);
1149        }
1150        let _ = fs::copy(src_path, dst_path);
1151    } else {
1152        let _ = fs::remove_file(dst_path);
1153    }
1154}
1155
1156fn swap_save_file(project_dir: &Path, quick: bool, save_cnt: usize, quick_cnt: usize, a: usize, b: usize) {
1157    if a == b {
1158        return;
1159    }
1160    let pa = slot_path_with_counts(project_dir, quick, a, save_cnt, quick_cnt);
1161    let pb = slot_path_with_counts(project_dir, quick, b, save_cnt, quick_cnt);
1162    let tmp = pa.with_extension("sav.swap");
1163    let a_exists = pa.exists();
1164    let b_exists = pb.exists();
1165    if a_exists {
1166        let _ = fs::rename(&pa, &tmp);
1167    }
1168    if b_exists {
1169        if let Some(parent) = pa.parent() {
1170            let _ = fs::create_dir_all(parent);
1171        }
1172        let _ = fs::rename(&pb, &pa);
1173    } else {
1174        let _ = fs::remove_file(&pa);
1175    }
1176    if a_exists {
1177        if let Some(parent) = pb.parent() {
1178            let _ = fs::create_dir_all(parent);
1179        }
1180        let _ = fs::rename(&tmp, &pb);
1181    } else {
1182        let _ = fs::remove_file(&pb);
1183    }
1184    let _ = fs::remove_file(&tmp);
1185}
1186
1187fn copy_slot(
1188    project_dir: &Path,
1189    quick: bool,
1190    save_cnt: usize,
1191    quick_cnt: usize,
1192    thumb_config: SaveThumbConfig,
1193    slots: &mut Vec<SaveSlotState>,
1194    src: usize,
1195    dst: usize,
1196) -> bool {
1197    ensure_slot_loaded_with_counts(project_dir, quick, save_cnt, quick_cnt, slots, src);
1198    let Some(src_slot) = slots.get(src).cloned() else {
1199        return false;
1200    };
1201    if !src_slot.exist {
1202        return false;
1203    }
1204    *ensure_slot(slots, dst) = src_slot;
1205    copy_save_file(project_dir, quick, save_cnt, quick_cnt, src, dst);
1206    copy_thumb_file(project_dir, save_cnt, quick_cnt, quick, thumb_config, src, dst);
1207    true
1208}
1209
1210fn change_slot(
1211    project_dir: &Path,
1212    quick: bool,
1213    save_cnt: usize,
1214    quick_cnt: usize,
1215    thumb_config: SaveThumbConfig,
1216    slots: &mut Vec<SaveSlotState>,
1217    a: usize,
1218    b: usize,
1219) -> bool {
1220    ensure_slot_loaded_with_counts(project_dir, quick, save_cnt, quick_cnt, slots, a);
1221    ensure_slot_loaded_with_counts(project_dir, quick, save_cnt, quick_cnt, slots, b);
1222    let max_idx = a.max(b);
1223    if slots.len() <= max_idx {
1224        slots.resize_with(max_idx + 1, SaveSlotState::default);
1225    }
1226    slots.swap(a, b);
1227    swap_save_file(project_dir, quick, save_cnt, quick_cnt, a, b);
1228    swap_thumb_file(project_dir, save_cnt, quick_cnt, quick, thumb_config, a, b);
1229    true
1230}
1231
1232fn delete_slot(
1233    project_dir: &Path,
1234    quick: bool,
1235    save_cnt: usize,
1236    quick_cnt: usize,
1237    thumb_config: SaveThumbConfig,
1238    slots: &mut Vec<SaveSlotState>,
1239    idx: usize,
1240) -> bool {
1241    ensure_slot_loaded_with_counts(project_dir, quick, save_cnt, quick_cnt, slots, idx);
1242    let existed = slots.get(idx).map(|s| s.exist).unwrap_or(false);
1243    *ensure_slot(slots, idx) = SaveSlotState::default();
1244    let path = slot_path_with_counts(project_dir, quick, idx, save_cnt, quick_cnt);
1245    let _ = fs::remove_file(path);
1246    remove_thumb_file(project_dir, save_cnt, quick_cnt, quick, thumb_config, idx);
1247    existed
1248}
1249
1250fn capture_flags_path(image_path: &Path) -> PathBuf {
1251    let mut p = image_path.to_path_buf();
1252    let ext = p
1253        .extension()
1254        .and_then(|v| v.to_str())
1255        .map(|v| format!("{v}.siglus_flags"))
1256        .unwrap_or_else(|| "siglus_flags".to_string());
1257    p.set_extension(ext);
1258    p
1259}
1260
1261fn named_i64(params: &[Value], id: i32, default: i64) -> i64 {
1262    params
1263        .iter()
1264        .find_map(|v| match v {
1265            Value::NamedArg { id: nid, value } if *nid == id => value.as_i64(),
1266            _ => None,
1267        })
1268        .unwrap_or(default)
1269}
1270
1271fn named_element(params: &[Value], id: i32) -> Option<Vec<i32>> {
1272    params.iter().find_map(|v| match v {
1273        Value::NamedArg { id: nid, value } if *nid == id => match value.as_ref() {
1274            Value::Element(chain) => Some(chain.clone()),
1275            _ => None,
1276        },
1277        _ => None,
1278    })
1279}
1280
1281fn save_capture_flags_sidecar(ctx: &CommandContext, image_path: &Path, params: &[Value]) {
1282    let Some(flag_chain) = named_element(params, 2) else {
1283        return;
1284    };
1285    let Some(flag_form) = flag_chain.first().copied() else {
1286        return;
1287    };
1288    let flag_index = named_i64(params, 3, 0).max(0) as usize;
1289    let flag_cnt = named_i64(params, 4, 0).max(0) as usize;
1290    let str_chain = named_element(params, 5);
1291    let str_index = named_i64(params, 6, 0).max(0) as usize;
1292    let str_cnt = named_i64(params, 7, 0).max(0) as usize;
1293
1294    let mut out = String::new();
1295    out.push_str("version=1\n");
1296    out.push_str(&format!("flag_cnt={flag_cnt}\n"));
1297    if let Some(list) = ctx.globals.int_lists.get(&(flag_form as u32)) {
1298        for i in 0..flag_cnt {
1299            let v = list.get(flag_index + i).copied().unwrap_or(0);
1300            out.push_str(&format!("flag.{i}={v}\n"));
1301        }
1302    }
1303    if let Some(str_form) = str_chain.and_then(|v| v.first().copied()) {
1304        out.push_str(&format!("str_cnt={str_cnt}\n"));
1305        if let Some(list) = ctx.globals.str_lists.get(&(str_form as u32)) {
1306            for i in 0..str_cnt {
1307                let v = list.get(str_index + i).cloned().unwrap_or_default();
1308                out.push_str(&format!("str.{i}={}\n", escape_str(&v)));
1309            }
1310        }
1311    }
1312    if let Some(parent) = image_path.parent() {
1313        let _ = fs::create_dir_all(parent);
1314    }
1315    let _ = fs::write(capture_flags_path(image_path), out);
1316}
1317
1318fn load_capture_flags_sidecar(
1319    ctx: &mut CommandContext,
1320    image_path: &Path,
1321    params: &[Value],
1322) -> bool {
1323    let path = capture_flags_path(image_path);
1324    let Ok(data) = fs::read_to_string(path) else {
1325        return image_path.exists();
1326    };
1327    if let Some(flag_chain) = named_element(params, 2) {
1328        if let Some(flag_form) = flag_chain.first().copied() {
1329            let flag_index = named_i64(params, 3, 0).max(0) as usize;
1330            let flag_cnt = named_i64(params, 4, 0).max(0) as usize;
1331            let mut values = vec![0_i64; flag_cnt];
1332            for line in data.lines() {
1333                if let Some((k, v)) = line.split_once('=') {
1334                    if let Some(i) = k
1335                        .strip_prefix("flag.")
1336                        .and_then(|x| x.parse::<usize>().ok())
1337                    {
1338                        if i < values.len() {
1339                            values[i] = v.trim().parse::<i64>().unwrap_or(0);
1340                        }
1341                    }
1342                }
1343            }
1344            let list = ctx.globals.int_lists.entry(flag_form as u32).or_default();
1345            if list.len() < flag_index + flag_cnt {
1346                list.resize(flag_index + flag_cnt, 0);
1347            }
1348            for (i, v) in values.into_iter().enumerate() {
1349                list[flag_index + i] = v;
1350            }
1351        }
1352    }
1353    if let Some(str_chain) = named_element(params, 5) {
1354        if let Some(str_form) = str_chain.first().copied() {
1355            let str_index = named_i64(params, 6, 0).max(0) as usize;
1356            let str_cnt = named_i64(params, 7, 0).max(0) as usize;
1357            let mut values = vec![String::new(); str_cnt];
1358            for line in data.lines() {
1359                if let Some((k, v)) = line.split_once('=') {
1360                    if let Some(i) = k.strip_prefix("str.").and_then(|x| x.parse::<usize>().ok()) {
1361                        if i < values.len() {
1362                            values[i] = unescape_str(v);
1363                        }
1364                    }
1365                }
1366            }
1367            let list = ctx.globals.str_lists.entry(str_form as u32).or_default();
1368            if list.len() < str_index + str_cnt {
1369                list.resize(str_index + str_cnt, String::new());
1370            }
1371            for (i, v) in values.into_iter().enumerate() {
1372                list[str_index + i] = v;
1373            }
1374        }
1375    }
1376    true
1377}
1378
1379fn write_msg_back(ctx: &CommandContext) {
1380    let form_id = ctx.ids.form_global_msgbk;
1381    if form_id == 0 {
1382        return;
1383    }
1384    let Some(st) = ctx.globals.msgbk_forms.get(&form_id) else {
1385        return;
1386    };
1387    let dir = save_dir(&ctx.project_dir);
1388    let path = dir.join("msg_back.txt");
1389
1390    let mut out = String::new();
1391    for idx in st.ordered_history_indices() {
1392        let Some(entry) = st.history.get(idx) else { continue; };
1393        out.push_str(&format!("-- entry {} --\n", idx));
1394        if !entry.original_name.is_empty() || !entry.disp_name.is_empty() {
1395            out.push_str("NAME: ");
1396            out.push_str(&entry.disp_name);
1397            out.push('\n');
1398        }
1399        if !entry.msg_str.is_empty() {
1400            if entry.pct_flag {
1401                out.push_str(&format!(
1402                    "IMG: {} {} {}\n",
1403                    entry.msg_str, entry.pct_pos_x, entry.pct_pos_y
1404                ));
1405            } else {
1406                out.push_str("TEXT: ");
1407                out.push_str(&entry.msg_str);
1408                out.push('\n');
1409            }
1410        }
1411        for (koe_no, chara_no) in entry.koe_no_list.iter().zip(entry.chr_no_list.iter()) {
1412            out.push_str(&format!("KOE: {} {}\n", koe_no, chara_no));
1413        }
1414        if entry.scn_no >= 0 || entry.line_no >= 0 {
1415            out.push_str(&format!("SCENE_LINE: {} {}\n", entry.scn_no, entry.line_no));
1416        }
1417        out.push('\n');
1418    }
1419    let _ = std::fs::write(path, out);
1420}
1421
1422fn open_msg_back_proc(ctx: &mut CommandContext) -> bool {
1423    if ctx.globals.script.msg_back_disable || ctx.globals.syscom.msg_back.check_enabled() == 0 {
1424        return false;
1425    }
1426    ctx.globals.syscom.read_skip.onoff = false;
1427    ctx.globals.syscom.msg_back_open = true;
1428    ctx.globals.syscom.msg_back_proc_initialized = false;
1429    let form_id = ctx.ids.form_global_msgbk;
1430    let (count, target) = ctx
1431        .globals
1432        .msgbk_forms
1433        .get(&form_id)
1434        .map(|st| {
1435            let indices = st.ordered_history_indices();
1436            let target = if indices.contains(&st.history_last_pos) {
1437                st.history_last_pos as isize
1438            } else {
1439                indices.last().copied().map(|idx| idx as isize).unwrap_or(-1)
1440            };
1441            (indices.len(), target)
1442        })
1443        .unwrap_or((0, -1));
1444    ctx.globals.syscom.msg_back_view_pos = count.saturating_sub(1);
1445    ctx.globals.syscom.msg_back_target_no = target;
1446    true
1447}
1448
1449fn configured_save_count(ctx: &CommandContext, quick: bool) -> usize {
1450    let keys: [&str; 2] = if quick {
1451        ["#QUICK_SAVE.CNT", "QUICK_SAVE.CNT"]
1452    } else {
1453        ["#SAVE.CNT", "SAVE.CNT"]
1454    };
1455    let default_count = if quick { 3 } else { 10 };
1456    ctx.tables
1457        .gameexe
1458        .as_ref()
1459        .and_then(|cfg| keys.iter().find_map(|key| cfg.get_usize(*key)))
1460        .unwrap_or(default_count)
1461        .min(10000)
1462}
1463
1464fn first_free_slot(slots: &[SaveSlotState]) -> i64 {
1465    for (i, s) in slots.iter().enumerate() {
1466        if !s.exist {
1467            return i as i64;
1468        }
1469    }
1470    slots.len() as i64
1471}
1472
1473fn slot_i64(slot: &SaveSlotState, op: i32) -> i64 {
1474    match op {
1475        GET_SAVE_EXIST | GET_QUICK_SAVE_EXIST => {
1476            if slot.exist {
1477                1
1478            } else {
1479                0
1480            }
1481        }
1482        GET_SAVE_YEAR | GET_QUICK_SAVE_YEAR => slot.year,
1483        GET_SAVE_MONTH | GET_QUICK_SAVE_MONTH => slot.month,
1484        GET_SAVE_DAY | GET_QUICK_SAVE_DAY => slot.day,
1485        GET_SAVE_WEEKDAY | GET_QUICK_SAVE_WEEKDAY => slot.weekday,
1486        GET_SAVE_HOUR | GET_QUICK_SAVE_HOUR => slot.hour,
1487        GET_SAVE_MINUTE | GET_QUICK_SAVE_MINUTE => slot.minute,
1488        GET_SAVE_SECOND | GET_QUICK_SAVE_SECOND => slot.second,
1489        GET_SAVE_MILLISECOND | GET_QUICK_SAVE_MILLISECOND => slot.millisecond,
1490        _ => 0,
1491    }
1492}
1493
1494fn slot_str(slot: &SaveSlotState, op: i32) -> String {
1495    match op {
1496        GET_SAVE_TITLE | GET_QUICK_SAVE_TITLE => slot.title.clone(),
1497        GET_SAVE_MESSAGE | GET_QUICK_SAVE_MESSAGE => slot.message.clone(),
1498        GET_SAVE_FULL_MESSAGE | GET_QUICK_SAVE_FULL_MESSAGE => slot.full_message.clone(),
1499        GET_SAVE_COMMENT | GET_QUICK_SAVE_COMMENT => slot.comment.clone(),
1500        GET_SAVE_APPEND_DIR | GET_QUICK_SAVE_APPEND_DIR => slot.append_dir.clone(),
1501        GET_SAVE_APPEND_NAME | GET_QUICK_SAVE_APPEND_NAME => slot.append_name.clone(),
1502        _ => String::new(),
1503    }
1504}
1505
1506fn cfg_get_int(st: &crate::runtime::globals::SyscomRuntimeState, key: i32, default: i64) -> i64 {
1507    st.config_int.get(&key).copied().unwrap_or(default)
1508}
1509
1510fn cfg_set_int(st: &mut crate::runtime::globals::SyscomRuntimeState, key: i32, value: i64) {
1511    st.config_int.insert(key, value);
1512}
1513
1514fn volume_to_raw(v: i64) -> u8 {
1515    let v = v.clamp(0, 100);
1516    ((v * 255) / 100) as u8
1517}
1518
1519pub(crate) fn apply_audio_config(ctx: &mut CommandContext) {
1520    use crate::audio::TrackKind;
1521    let all_vol = cfg_get_int(&ctx.globals.syscom, GET_ALL_VOLUME, 100);
1522    let all_on = cfg_get_int(&ctx.globals.syscom, GET_ALL_ONOFF, 1) != 0;
1523    let all_raw = if all_on { volume_to_raw(all_vol) } else { 0 };
1524
1525    let bgm_vol = cfg_get_int(&ctx.globals.syscom, GET_BGM_VOLUME, 100);
1526    let bgm_on = cfg_get_int(&ctx.globals.syscom, GET_BGM_ONOFF, 1) != 0;
1527    let bgm_raw = if bgm_on { volume_to_raw(bgm_vol) } else { 0 };
1528
1529    let se_vol = cfg_get_int(&ctx.globals.syscom, GET_SE_VOLUME, 100);
1530    let se_on = cfg_get_int(&ctx.globals.syscom, GET_SE_ONOFF, 1) != 0;
1531    let se_raw = if se_on { volume_to_raw(se_vol) } else { 0 };
1532
1533    let pcm_vol = cfg_get_int(&ctx.globals.syscom, GET_PCM_VOLUME, 100);
1534    let pcm_on = cfg_get_int(&ctx.globals.syscom, GET_PCM_ONOFF, 1) != 0;
1535    let pcm_raw = if pcm_on { volume_to_raw(pcm_vol) } else { 0 };
1536
1537    let koe_vol = cfg_get_int(&ctx.globals.syscom, GET_KOE_VOLUME, 100);
1538    let koe_on = cfg_get_int(&ctx.globals.syscom, GET_KOE_ONOFF, 1) != 0;
1539    let koe_raw = if koe_on { volume_to_raw(koe_vol) } else { 0 };
1540
1541    let mov_vol = cfg_get_int(&ctx.globals.syscom, GET_MOV_VOLUME, 100);
1542    let mov_on = cfg_get_int(&ctx.globals.syscom, GET_MOV_ONOFF, 1) != 0;
1543    let mov_raw = if mov_on { volume_to_raw(mov_vol) } else { 0 };
1544
1545    let eff_bgm = (all_raw as u16 * bgm_raw as u16 / 255) as u8;
1546    let eff_se = (all_raw as u16 * se_raw as u16 / 255) as u8;
1547    let eff_pcm = (all_raw as u16 * pcm_raw as u16 / 255) as u8;
1548    let eff_koe = (all_raw as u16 * koe_raw as u16 / 255) as u8;
1549    let eff_mov = (all_raw as u16 * mov_raw as u16 / 255) as u8;
1550
1551    ctx.audio
1552        .set_track_master_volume_raw(TrackKind::Bgm, eff_bgm);
1553    ctx.audio.set_track_master_volume_raw(TrackKind::Se, eff_se);
1554    ctx.audio
1555        .set_track_master_volume_raw(TrackKind::Pcm, eff_pcm);
1556    ctx.audio
1557        .set_track_master_volume_raw(TrackKind::Koe, eff_koe);
1558    ctx.audio
1559        .set_track_master_volume_raw(TrackKind::Mov, eff_mov);
1560}
1561
1562fn cfg_get_str(st: &crate::runtime::globals::SyscomRuntimeState, key: i32) -> String {
1563    st.config_str.get(&key).cloned().unwrap_or_default()
1564}
1565
1566fn cfg_set_str(st: &mut crate::runtime::globals::SyscomRuntimeState, key: i32, value: String) {
1567    st.config_str.insert(key, value);
1568}
1569
1570fn join_game_path(base: &Path, raw: &str) -> PathBuf {
1571    if raw.is_empty() {
1572        return base.to_path_buf();
1573    }
1574    let norm = raw.replace('\\', "/");
1575    let p = Path::new(&norm);
1576    if p.is_absolute() {
1577        p.to_path_buf()
1578    } else {
1579        base.join(p)
1580    }
1581}
1582
1583fn opaque_rgba(img: &RgbaImage) -> RgbaImage {
1584    let mut rgba = img.rgba.clone();
1585    for px in rgba.chunks_exact_mut(4) {
1586        px[3] = 255;
1587    }
1588    RgbaImage {
1589        width: img.width,
1590        height: img.height,
1591        center_x: 0,
1592        center_y: 0,
1593        rgba,
1594    }
1595}
1596
1597fn write_rgba_png(path: &Path, img: &RgbaImage) -> Result<()> {
1598    if let Some(parent) = path.parent() {
1599        fs::create_dir_all(parent)?;
1600    }
1601    let Some(buf) = image::RgbaImage::from_raw(img.width, img.height, img.rgba.clone()) else {
1602        anyhow::bail!("invalid rgba buffer for {}x{} image", img.width, img.height);
1603    };
1604    buf.save(path)?;
1605    Ok(())
1606}
1607
1608fn write_rgba_png_opaque(path: &Path, img: &RgbaImage) -> Result<()> {
1609    let opaque = opaque_rgba(img);
1610    write_rgba_png(path, &opaque)
1611}
1612
1613fn push_u16_le(out: &mut Vec<u8>, value: u16) {
1614    out.extend_from_slice(&value.to_le_bytes());
1615}
1616
1617fn push_u32_le(out: &mut Vec<u8>, value: u32) {
1618    out.extend_from_slice(&value.to_le_bytes());
1619}
1620
1621fn push_i32_le(out: &mut Vec<u8>, value: i32) {
1622    out.extend_from_slice(&value.to_le_bytes());
1623}
1624
1625fn write_rgba_bmp_top_down(path: &Path, img: &RgbaImage) -> Result<()> {
1626    if let Some(parent) = path.parent() {
1627        fs::create_dir_all(parent)?;
1628    }
1629    let width = img.width;
1630    let height = img.height;
1631    if width == 0 || height == 0 {
1632        anyhow::bail!("invalid zero-sized bmp image {}x{}", width, height);
1633    }
1634    let pixel_size = width.saturating_mul(height).saturating_mul(4);
1635    let file_size = 14u32.saturating_add(40).saturating_add(pixel_size);
1636    let mut out = Vec::with_capacity(file_size as usize);
1637
1638    out.extend_from_slice(b"BM");
1639    push_u32_le(&mut out, file_size);
1640    push_u16_le(&mut out, 0);
1641    push_u16_le(&mut out, 0);
1642    push_u32_le(&mut out, 14 + 40);
1643
1644    push_u32_le(&mut out, 40);
1645    push_i32_le(&mut out, width as i32);
1646    push_i32_le(&mut out, -(height as i32));
1647    push_u16_le(&mut out, 1);
1648    push_u16_le(&mut out, 32);
1649    push_u32_le(&mut out, 0);
1650    push_u32_le(&mut out, 0);
1651    push_i32_le(&mut out, 0);
1652    push_i32_le(&mut out, 0);
1653    push_u32_le(&mut out, 0);
1654    push_u32_le(&mut out, 0);
1655
1656    for px in img.rgba.chunks_exact(4) {
1657        out.push(px[2]);
1658        out.push(px[1]);
1659        out.push(px[0]);
1660        out.push(px[3]);
1661    }
1662    fs::write(path, out)?;
1663    Ok(())
1664}
1665
1666fn resize_rgba(img: &RgbaImage, w: u32, h: u32) -> RgbaImage {
1667    if img.width == 0 || img.height == 0 || w == 0 || h == 0 {
1668        return img.clone();
1669    }
1670    if img.width == w && img.height == h {
1671        return img.clone();
1672    }
1673    let mut out = vec![0u8; (w * h * 4) as usize];
1674    for y in 0..h {
1675        let src_y = (y as u64 * img.height as u64 / h as u64) as u32;
1676        for x in 0..w {
1677            let src_x = (x as u64 * img.width as u64 / w as u64) as u32;
1678            let si = ((src_y * img.width + src_x) * 4) as usize;
1679            let di = ((y * w + x) * 4) as usize;
1680            out[di..di + 4].copy_from_slice(&img.rgba[si..si + 4]);
1681        }
1682    }
1683    RgbaImage {
1684        width: w,
1685        height: h,
1686        center_x: 0,
1687        center_y: 0,
1688        rgba: out,
1689    }
1690}
1691
1692pub(crate) fn resize_capture_rgba_nearest(img: &RgbaImage, w: u32, h: u32) -> RgbaImage {
1693    resize_rgba(img, w, h)
1694}
1695
1696
1697fn font_exists(project_dir: &Path, name: &str) -> bool {
1698    if name.is_empty() {
1699        return false;
1700    }
1701    if crate::text_render::font_name_matches_embedded_default(name) {
1702        return true;
1703    }
1704
1705    let name_lower = name.to_ascii_lowercase();
1706    for font_dir in [project_dir.join("font"), project_dir.join("fonts")] {
1707        let Ok(entries) = fs::read_dir(font_dir) else {
1708            continue;
1709        };
1710        for entry in entries.flatten() {
1711            let path = entry.path();
1712            if !path.is_file() {
1713                continue;
1714            }
1715            let ext = path
1716                .extension()
1717                .and_then(|s| s.to_str())
1718                .unwrap_or("")
1719                .to_ascii_lowercase();
1720            if ext != "ttf" && ext != "otf" && ext != "ttc" {
1721                continue;
1722            }
1723            let stem = path
1724                .file_stem()
1725                .and_then(|s| s.to_str())
1726                .unwrap_or("")
1727                .to_ascii_lowercase();
1728            if stem == name_lower {
1729                return true;
1730            }
1731        }
1732    }
1733    false
1734}
1735
1736pub fn dispatch(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
1737    let Some(call) = parse_call(ctx, form_id, args) else {
1738        return Ok(false);
1739    };
1740    let op = call.op;
1741    let params = call.params;
1742
1743    {
1744        let st = &ctx.globals.syscom;
1745        if let Some(v) = get_local_extra(op, params, st) {
1746            ctx.push(Value::Int(v));
1747            return Ok(true);
1748        }
1749        if let Some(v) = get_toggle_get(op, st) {
1750            ctx.push(Value::Int(v));
1751            return Ok(true);
1752        }
1753    }
1754    {
1755        let st = &mut ctx.globals.syscom;
1756        if set_local_extra(op, params, st) {
1757            ctx.push(Value::Int(0));
1758            return Ok(true);
1759        }
1760        if apply_toggle_set(op, p_bool(params, 0), st) {
1761            ctx.push(Value::Int(0));
1762            return Ok(true);
1763        }
1764    }
1765
1766    match op {
1767        CALL_EX => {
1768            // C++ routes SYSCOM.CALL_EX to tnm_command_proc_farcall_ex(..., FM_VOID).
1769            // That transfer is implemented in the VM before generic form dispatch,
1770            // because only the VM owns the script call stack and proc boundary.
1771            // Do not fake a return value here.
1772            return Ok(false);
1773        }
1774        CALL_SYSCOM_MENU => {
1775            ctx.globals.syscom.menu_open = false;
1776            ctx.globals.syscom.menu_kind = None;
1777            ctx.globals.syscom.menu_result = None;
1778            ctx.globals.syscom.read_skip.onoff = false;
1779            ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
1780                kind: SyscomPendingProcKind::OpenSyscomMenu,
1781                warning: false,
1782                se_play: false,
1783                fade_out: false,
1784                leave_msgbk: false,
1785                save_id: 0,
1786            });
1787            ctx.globals.syscom.last_menu_call = CALL_SYSCOM_MENU;
1788            return Ok(true);
1789        }
1790        SET_SYSCOM_MENU_ENABLE => ctx.globals.syscom.syscom_menu_disable = false,
1791        SET_SYSCOM_MENU_DISABLE => ctx.globals.syscom.syscom_menu_disable = true,
1792        SET_MWND_BTN_ENABLE => {
1793            if params.is_empty() {
1794                ctx.globals.syscom.mwnd_btn_disable_all = false;
1795                if sg_debug_enabled_local() {
1796                    eprintln!("[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_ENABLE all disable_all=false");
1797                }
1798            } else {
1799                let idx = p_i64(params, 0);
1800                ctx.globals.syscom.mwnd_btn_disable.insert(idx, false);
1801                if sg_debug_enabled_local() {
1802                    eprintln!("[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_ENABLE idx={} disabled=false", idx);
1803                }
1804            }
1805        }
1806        SET_MWND_BTN_DISABLE => {
1807            if params.is_empty() {
1808                ctx.globals.syscom.mwnd_btn_disable_all = true;
1809                if sg_debug_enabled_local() {
1810                    eprintln!("[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_DISABLE all disable_all=true");
1811                }
1812            } else {
1813                let idx = p_i64(params, 0);
1814                ctx.globals.syscom.mwnd_btn_disable.insert(idx, true);
1815                if sg_debug_enabled_local() {
1816                    eprintln!("[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_DISABLE idx={} disabled=true", idx);
1817                }
1818            }
1819        }
1820        SET_MWND_BTN_TOUCH_ENABLE => {
1821            ctx.globals.syscom.mwnd_btn_touch_disable = false;
1822            if sg_debug_enabled_local() {
1823                eprintln!("[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_TOUCH_ENABLE touch_disable=false");
1824            }
1825        }
1826        SET_MWND_BTN_TOUCH_DISABLE => {
1827            ctx.globals.syscom.mwnd_btn_touch_disable = true;
1828            if sg_debug_enabled_local() {
1829                eprintln!("[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_TOUCH_DISABLE touch_disable=true");
1830            }
1831        }
1832        INIT_SYSCOM_FLAG => {
1833            let enabled = ToggleFeatureState { onoff: false, enable: true, exist: true };
1834            ctx.globals.syscom.read_skip = enabled;
1835            ctx.globals.syscom.auto_skip = enabled;
1836            ctx.globals.syscom.auto_mode = enabled;
1837            ctx.globals.syscom.hide_mwnd = enabled;
1838            ctx.globals.syscom.local_extra_switch = enabled;
1839            ctx.globals.syscom.local_extra_mode = ValueFeatureState { value: 0, enable: true, exist: true };
1840            ctx.globals.syscom.local_extra_switches = [enabled; 4];
1841            ctx.globals.syscom.local_extra_modes = [ValueFeatureState { value: 0, enable: true, exist: true }; 4];
1842            ctx.globals.syscom.msg_back = enabled;
1843            ctx.globals.syscom.return_to_sel = enabled;
1844            ctx.globals.syscom.return_to_menu = enabled;
1845            ctx.globals.syscom.end_game = enabled;
1846            ctx.globals.syscom.save_feature = enabled;
1847            ctx.globals.syscom.load_feature = enabled;
1848            ctx.globals.syscom.msg_back_open = false;
1849            load_global_save(ctx);
1850        }
1851        OPEN_MSG_BACK => {
1852            if open_msg_back_proc(ctx) {
1853                ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
1854                    kind: SyscomPendingProcKind::MsgBack,
1855                    warning: false,
1856                    se_play: false,
1857                    fade_out: false,
1858                    leave_msgbk: false,
1859                    save_id: 0,
1860                });
1861            }
1862            ctx.globals.syscom.last_menu_call = OPEN_MSG_BACK;
1863        }
1864        CLOSE_MSG_BACK => {
1865            ctx.globals.syscom.msg_back_open = false;
1866            ctx.globals.syscom.msg_back_proc_initialized = false;
1867            ctx.globals.syscom.last_menu_call = CLOSE_MSG_BACK;
1868        }
1869        RETURN_TO_SEL => {
1870            ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
1871                kind: SyscomPendingProcKind::ReturnToSel,
1872                warning: p_bool(params, 0),
1873                se_play: p_bool(params, 1),
1874                fade_out: p_bool(params, 2),
1875                leave_msgbk: false,
1876                save_id: 0,
1877            });
1878            ctx.globals.syscom.last_menu_call = RETURN_TO_SEL;
1879            ctx.globals.syscom.menu_open = false;
1880        }
1881        RETURN_TO_MENU => {
1882            let leave_msgbk = params
1883                .iter()
1884                .find(|v| v.named_id() == Some(0))
1885                .and_then(Value::as_i64)
1886                .unwrap_or(0)
1887                != 0;
1888            ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
1889                kind: SyscomPendingProcKind::ReturnToMenu,
1890                warning: p_bool(params, 0),
1891                se_play: p_bool(params, 1),
1892                fade_out: p_bool(params, 2),
1893                leave_msgbk,
1894                save_id: 0,
1895            });
1896            ctx.globals.syscom.last_menu_call = RETURN_TO_MENU;
1897            ctx.globals.syscom.menu_open = false;
1898        }
1899        END_GAME => {
1900            ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
1901                kind: SyscomPendingProcKind::EndGame,
1902                warning: p_bool(params, 0),
1903                se_play: p_bool(params, 1),
1904                fade_out: p_bool(params, 2),
1905                leave_msgbk: false,
1906                save_id: 0,
1907            });
1908            ctx.globals.syscom.last_menu_call = END_GAME;
1909            ctx.globals.syscom.menu_open = false;
1910        }
1911        REPLAY_KOE => ctx.globals.syscom.replay_koe = Some((p_i64(params, 0), p_i64(params, 1))),
1912        CHECK_REPLAY_KOE => {
1913            let v = if ctx.globals.syscom.replay_koe.is_some() {
1914                1
1915            } else {
1916                0
1917            };
1918            ctx.push(Value::Int(v));
1919            return Ok(true);
1920        }
1921        GET_REPLAY_KOE_KOE_NO => {
1922            let v = ctx.globals.syscom.replay_koe.map(|v| v.0).unwrap_or(-1);
1923            ctx.push(Value::Int(v));
1924            return Ok(true);
1925        }
1926        GET_REPLAY_KOE_CHARA_NO => {
1927            let v = ctx.globals.syscom.replay_koe.map(|v| v.1).unwrap_or(-1);
1928            ctx.push(Value::Int(v));
1929            return Ok(true);
1930        }
1931        CLEAR_REPLAY_KOE => ctx.globals.syscom.replay_koe = None,
1932        GET_CURRENT_SAVE_SCENE_TITLE => {
1933            let v = ctx.globals.syscom.current_save_scene_title.clone();
1934            ctx.push(Value::Str(v));
1935            return Ok(true);
1936        }
1937        GET_CURRENT_SAVE_MESSAGE => {
1938            let v = ctx.globals.syscom.current_save_message.clone();
1939            ctx.push(Value::Str(v));
1940            return Ok(true);
1941        }
1942        GET_TOTAL_PLAY_TIME => {
1943            let v = ctx.globals.syscom.total_play_time;
1944            ctx.push(Value::Int(v));
1945            return Ok(true);
1946        }
1947        SET_TOTAL_PLAY_TIME => {
1948            ctx.globals.syscom.total_play_time = p_i64(params, 0);
1949            write_global_save(ctx);
1950        },
1951        CALL_SAVE_MENU => {
1952            sync_save_slots_from_disk(ctx, false);
1953            set_syscom_pending_proc(ctx, SyscomPendingProcKind::OpenSave);
1954            ctx.globals.syscom.last_menu_call = CALL_SAVE_MENU;
1955            return Ok(true);
1956        }
1957        CALL_LOAD_MENU => {
1958            sync_save_slots_from_disk(ctx, false);
1959            set_syscom_pending_proc(ctx, SyscomPendingProcKind::OpenLoad);
1960            ctx.globals.syscom.last_menu_call = CALL_LOAD_MENU;
1961            return Ok(true);
1962        }
1963        SAVE => {
1964            let idx = p_i64(params, 0).max(0) as usize;
1965            let warning = p_bool(params, 1);
1966            let ok = idx < configured_save_count(ctx, false) && local_save_available(ctx);
1967            if ok && !request_confirmed_save_or_load(ctx, SyscomPendingProcKind::Save, idx, warning, true) {
1968                if ok {
1969                    menu_save_slot(ctx, false, idx);
1970                }
1971            }
1972            ctx.globals.syscom.last_menu_call = SAVE;
1973            ctx.push(Value::Int(if ok { 1 } else { 0 }));
1974            return Ok(true);
1975        }
1976        LOAD => {
1977            let idx = p_i64(params, 0).max(0) as usize;
1978            let warning = p_bool(params, 1);
1979            let ok = idx < configured_save_count(ctx, false)
1980                && local_save_file_exists(ctx, SaveKind::Normal, idx);
1981            if ok && !request_confirmed_save_or_load(ctx, SyscomPendingProcKind::Load, idx, warning, false) {
1982                menu_load_slot(ctx, false, idx);
1983            }
1984            ctx.globals.syscom.last_menu_call = LOAD;
1985        }
1986        QUICK_SAVE => {
1987            let idx = p_i64(params, 0).max(0) as usize;
1988            let warning = p_bool(params, 1);
1989            let ok = idx < configured_save_count(ctx, true) && local_save_available(ctx);
1990            if ok && !request_confirmed_save_or_load(ctx, SyscomPendingProcKind::QuickSave, idx, warning, true) {
1991                if ok {
1992                    menu_save_slot(ctx, true, idx);
1993                }
1994            }
1995            ctx.globals.syscom.last_menu_call = QUICK_SAVE;
1996            ctx.push(Value::Int(if ok { 1 } else { 0 }));
1997            return Ok(true);
1998        }
1999        QUICK_LOAD => {
2000            let idx = p_i64(params, 0).max(0) as usize;
2001            let warning = p_bool(params, 1);
2002            let ok = idx < configured_save_count(ctx, true)
2003                && local_save_file_exists(ctx, SaveKind::Quick, idx);
2004            if ok && !request_confirmed_save_or_load(ctx, SyscomPendingProcKind::QuickLoad, idx, warning, false) {
2005                menu_load_slot(ctx, true, idx);
2006            }
2007            ctx.globals.syscom.last_menu_call = QUICK_LOAD;
2008        }
2009        END_SAVE => {
2010            // C++ `END_SAVE(warning, se_play)` always targets
2011            // save_cnt + quick_save_cnt; it has no script-supplied index.
2012            let _warning = p_bool(params, 0);
2013            let ok = local_save_available(ctx);
2014            if ok {
2015                ctx.request_runtime_save(RuntimeSaveKind::End, 0);
2016            }
2017            ctx.push(Value::Int(if ok { 1 } else { 0 }));
2018            return Ok(true);
2019        }
2020        END_LOAD => {
2021            // C++ `END_LOAD(warning, se_play, fade_out)` loads the single end-save
2022            // slot at save_cnt + quick_save_cnt.
2023            if local_save_file_exists(ctx, SaveKind::End, 0) {
2024                ctx.request_runtime_load(RuntimeSaveKind::End, 0);
2025            }
2026            ctx.globals.syscom.last_menu_call = END_LOAD;
2027        },
2028        INNER_SAVE => {
2029            let idx = p_i64(params, 0).max(0) as usize;
2030            let ok = local_save_available(ctx);
2031            if ok {
2032                ctx.request_runtime_save(RuntimeSaveKind::Inner, idx);
2033            }
2034            ctx.push(Value::Int(if ok { 1 } else { 0 }));
2035            return Ok(true);
2036        }
2037        INNER_LOAD => {
2038            let idx = p_i64(params, 0).max(0) as usize;
2039            ctx.globals.syscom.last_menu_call = INNER_LOAD;
2040            let exists = ctx
2041                .globals
2042                .syscom
2043                .inner_save_streams
2044                .get(idx)
2045                .map(|s| !s.is_empty())
2046                .unwrap_or(false);
2047            if exists {
2048                ctx.request_runtime_load(RuntimeSaveKind::Inner, idx);
2049            }
2050            ctx.push(Value::Int(if exists { 1 } else { 0 }));
2051            return Ok(true);
2052        }
2053        CLEAR_INNER_SAVE => {
2054            let idx = p_i64(params, 0).max(0) as usize;
2055            let existed = ctx
2056                .globals
2057                .syscom
2058                .inner_save_streams
2059                .get(idx)
2060                .map(|s| !s.is_empty())
2061                .unwrap_or(false);
2062            if let Some(slot) = ctx.globals.syscom.inner_save_streams.get_mut(idx) {
2063                slot.clear();
2064            }
2065            ctx.globals.syscom.inner_save_exists = ctx.globals.syscom.inner_save_streams.iter().any(|s| !s.is_empty());
2066            ctx.push(Value::Int(if existed { 1 } else { 0 }));
2067            return Ok(true);
2068        }
2069        COPY_INNER_SAVE => {
2070            let from = p_i64(params, 0).max(0) as usize;
2071            let to = p_i64(params, 1).max(0) as usize;
2072            let Some(stream) = ctx.globals.syscom.inner_save_streams.get(from).cloned().filter(|s| !s.is_empty()) else {
2073                ctx.push(Value::Int(0));
2074                return Ok(true);
2075            };
2076            if ctx.globals.syscom.inner_save_streams.len() <= to {
2077                ctx.globals.syscom.inner_save_streams.resize_with(to + 1, Vec::new);
2078            }
2079            ctx.globals.syscom.inner_save_streams[to] = stream;
2080            ctx.globals.syscom.inner_save_exists = true;
2081            ctx.push(Value::Int(1));
2082            return Ok(true);
2083        }
2084        CHECK_INNER_SAVE => {
2085            let idx = p_i64(params, 0).max(0) as usize;
2086            let v = if ctx
2087                .globals
2088                .syscom
2089                .inner_save_streams
2090                .get(idx)
2091                .map(|s| !s.is_empty())
2092                .unwrap_or(false)
2093            {
2094                1
2095            } else {
2096                0
2097            };
2098            ctx.push(Value::Int(v));
2099            return Ok(true);
2100        }
2101        MSG_BACK_LOAD => {
2102            ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
2103                kind: SyscomPendingProcKind::BacklogLoad,
2104                warning: p_bool(params, 0),
2105                se_play: p_bool(params, 1),
2106                fade_out: p_bool(params, 2),
2107                leave_msgbk: false,
2108                save_id: ctx.globals.syscom.msg_back_load_tid,
2109            });
2110            ctx.globals.syscom.last_menu_call = MSG_BACK_LOAD;
2111            ctx.globals.syscom.msg_back_open = false;
2112            write_msg_back(ctx);
2113        }
2114        GET_SAVE_CNT => {
2115            let v = configured_save_count(ctx, false) as i64;
2116            ctx.push(Value::Int(v));
2117            return Ok(true);
2118        }
2119        GET_QUICK_SAVE_CNT => {
2120            let v = configured_save_count(ctx, true) as i64;
2121            ctx.push(Value::Int(v));
2122            return Ok(true);
2123        }
2124        GET_SAVE_NEW_NO => {
2125            sync_save_slots_from_disk(ctx, false);
2126            let v = first_free_slot(&ctx.globals.syscom.save_slots);
2127            ctx.push(Value::Int(v));
2128            return Ok(true);
2129        }
2130        GET_QUICK_SAVE_NEW_NO => {
2131            sync_save_slots_from_disk(ctx, true);
2132            let v = first_free_slot(&ctx.globals.syscom.quick_save_slots);
2133            ctx.push(Value::Int(v));
2134            return Ok(true);
2135        }
2136        GET_SAVE_EXIST | GET_SAVE_YEAR | GET_SAVE_MONTH | GET_SAVE_DAY | GET_SAVE_WEEKDAY
2137        | GET_SAVE_HOUR | GET_SAVE_MINUTE | GET_SAVE_SECOND | GET_SAVE_MILLISECOND => {
2138            let idx = p_i64(params, 0).max(0) as usize;
2139            {
2140                let save_cnt = configured_save_count(ctx, false);
2141                let quick_cnt = configured_save_count(ctx, true);
2142                ensure_slot_loaded_with_counts(
2143                    &ctx.project_dir,
2144                    false,
2145                    save_cnt,
2146                    quick_cnt,
2147                    &mut ctx.globals.syscom.save_slots,
2148                    idx,
2149                );
2150            }
2151            let v = ctx
2152                .globals
2153                .syscom
2154                .save_slots
2155                .get(idx)
2156                .map(|s| slot_i64(s, op))
2157                .unwrap_or(0);
2158            ctx.push(Value::Int(v));
2159            return Ok(true);
2160        }
2161        GET_SAVE_TITLE
2162        | GET_SAVE_MESSAGE
2163        | GET_SAVE_FULL_MESSAGE
2164        | GET_SAVE_COMMENT
2165        | GET_SAVE_APPEND_DIR
2166        | GET_SAVE_APPEND_NAME => {
2167            let idx = p_i64(params, 0).max(0) as usize;
2168            {
2169                let save_cnt = configured_save_count(ctx, false);
2170                let quick_cnt = configured_save_count(ctx, true);
2171                ensure_slot_loaded_with_counts(
2172                    &ctx.project_dir,
2173                    false,
2174                    save_cnt,
2175                    quick_cnt,
2176                    &mut ctx.globals.syscom.save_slots,
2177                    idx,
2178                );
2179            }
2180            let v = ctx
2181                .globals
2182                .syscom
2183                .save_slots
2184                .get(idx)
2185                .map(|s| slot_str(s, op))
2186                .unwrap_or_default();
2187            ctx.push(Value::Str(v));
2188            return Ok(true);
2189        }
2190        SET_SAVE_COMMENT => {
2191            let idx = p_i64(params, 0).max(0) as usize;
2192            let comment = params
2193                .get(1)
2194                .and_then(|v| v.as_str())
2195                .unwrap_or("")
2196                .to_string();
2197            let slot = ensure_slot(&mut ctx.globals.syscom.save_slots, idx);
2198            slot.exist = true;
2199            slot.comment = comment;
2200            {
2201                let save_cnt = configured_save_count(ctx, false);
2202                let quick_cnt = configured_save_count(ctx, true);
2203                persist_slot_with_counts(
2204                    &ctx.project_dir,
2205                    false,
2206                    save_cnt,
2207                    quick_cnt,
2208                    &ctx.globals.syscom.save_slots,
2209                    idx,
2210                );
2211            }
2212        }
2213        GET_SAVE_VALUE => {
2214            let idx = p_i64(params, 0).max(0) as usize;
2215            {
2216                let save_cnt = configured_save_count(ctx, false);
2217                let quick_cnt = configured_save_count(ctx, true);
2218                ensure_slot_loaded_with_counts(
2219                    &ctx.project_dir,
2220                    false,
2221                    save_cnt,
2222                    quick_cnt,
2223                    &mut ctx.globals.syscom.save_slots,
2224                    idx,
2225                );
2226            }
2227            if let Some(Value::Element(chain)) = params.get(1).map(|v| v.unwrap_named()) {
2228                let Some(form_id) = chain.first().copied() else {
2229                    ctx.push(Value::Int(0));
2230                    return Ok(true);
2231                };
2232                let flag_index = p_i64(params, 2).max(0) as usize;
2233                let flag_cnt = p_i64(params, 3).max(0) as usize;
2234                let values: Vec<i64> = (0..flag_cnt)
2235                    .map(|i| {
2236                        ctx.globals
2237                            .syscom
2238                            .save_slots
2239                            .get(idx)
2240                            .and_then(|s| s.values.get(&(i as i32)).copied())
2241                            .unwrap_or(0)
2242                    })
2243                    .collect();
2244                let list = ctx.globals.int_lists.entry(form_id as u32).or_default();
2245                if list.len() < flag_index + flag_cnt {
2246                    list.resize(flag_index + flag_cnt, 0);
2247                }
2248                for (i, v) in values.into_iter().enumerate() {
2249                    list[flag_index + i] = v;
2250                }
2251                ctx.push(Value::Int(0));
2252                return Ok(true);
2253            }
2254            let key = p_i64(params, 1) as i32;
2255            let v = ctx
2256                .globals
2257                .syscom
2258                .save_slots
2259                .get(idx)
2260                .and_then(|s| s.values.get(&key).copied())
2261                .unwrap_or(0);
2262            ctx.push(Value::Int(v));
2263            return Ok(true);
2264        }
2265        SET_SAVE_VALUE => {
2266            let idx = p_i64(params, 0).max(0) as usize;
2267            if let Some(Value::Element(chain)) = params.get(1).map(|v| v.unwrap_named()) {
2268                let Some(form_id) = chain.first().copied() else {
2269                    return Ok(true);
2270                };
2271                let flag_index = p_i64(params, 2).max(0) as usize;
2272                let flag_cnt = p_i64(params, 3).max(0) as usize;
2273                let values: Vec<i64> = (0..flag_cnt)
2274                    .map(|i| {
2275                        ctx.globals
2276                            .int_lists
2277                            .get(&(form_id as u32))
2278                            .and_then(|list| list.get(flag_index + i).copied())
2279                            .unwrap_or(0)
2280                    })
2281                    .collect();
2282                let slot = ensure_slot(&mut ctx.globals.syscom.save_slots, idx);
2283                slot.exist = true;
2284                for (i, v) in values.into_iter().enumerate() {
2285                    slot.values.insert(i as i32, v);
2286                }
2287                {
2288                let save_cnt = configured_save_count(ctx, false);
2289                let quick_cnt = configured_save_count(ctx, true);
2290                persist_slot_with_counts(
2291                    &ctx.project_dir,
2292                    false,
2293                    save_cnt,
2294                    quick_cnt,
2295                    &ctx.globals.syscom.save_slots,
2296                    idx,
2297                );
2298            }
2299                return Ok(true);
2300            }
2301            let key = p_i64(params, 1) as i32;
2302            let val = p_i64(params, 2);
2303            let slot = ensure_slot(&mut ctx.globals.syscom.save_slots, idx);
2304            slot.exist = true;
2305            slot.values.insert(key, val);
2306            {
2307                let save_cnt = configured_save_count(ctx, false);
2308                let quick_cnt = configured_save_count(ctx, true);
2309                persist_slot_with_counts(
2310                    &ctx.project_dir,
2311                    false,
2312                    save_cnt,
2313                    quick_cnt,
2314                    &ctx.globals.syscom.save_slots,
2315                    idx,
2316                );
2317            }
2318        }
2319        GET_QUICK_SAVE_EXIST
2320        | GET_QUICK_SAVE_YEAR
2321        | GET_QUICK_SAVE_MONTH
2322        | GET_QUICK_SAVE_DAY
2323        | GET_QUICK_SAVE_WEEKDAY
2324        | GET_QUICK_SAVE_HOUR
2325        | GET_QUICK_SAVE_MINUTE
2326        | GET_QUICK_SAVE_SECOND
2327        | GET_QUICK_SAVE_MILLISECOND => {
2328            let idx = p_i64(params, 0).max(0) as usize;
2329            {
2330                let save_cnt = configured_save_count(ctx, false);
2331                let quick_cnt = configured_save_count(ctx, true);
2332                ensure_slot_loaded_with_counts(
2333                    &ctx.project_dir,
2334                    true,
2335                    save_cnt,
2336                    quick_cnt,
2337                    &mut ctx.globals.syscom.quick_save_slots,
2338                    idx,
2339                );
2340            }
2341            let v = ctx
2342                .globals
2343                .syscom
2344                .quick_save_slots
2345                .get(idx)
2346                .map(|s| slot_i64(s, op))
2347                .unwrap_or(0);
2348            ctx.push(Value::Int(v));
2349            return Ok(true);
2350        }
2351        GET_QUICK_SAVE_TITLE
2352        | GET_QUICK_SAVE_MESSAGE
2353        | GET_QUICK_SAVE_FULL_MESSAGE
2354        | GET_QUICK_SAVE_COMMENT
2355        | GET_QUICK_SAVE_APPEND_DIR
2356        | GET_QUICK_SAVE_APPEND_NAME => {
2357            let idx = p_i64(params, 0).max(0) as usize;
2358            {
2359                let save_cnt = configured_save_count(ctx, false);
2360                let quick_cnt = configured_save_count(ctx, true);
2361                ensure_slot_loaded_with_counts(
2362                    &ctx.project_dir,
2363                    true,
2364                    save_cnt,
2365                    quick_cnt,
2366                    &mut ctx.globals.syscom.quick_save_slots,
2367                    idx,
2368                );
2369            }
2370            let v = ctx
2371                .globals
2372                .syscom
2373                .quick_save_slots
2374                .get(idx)
2375                .map(|s| slot_str(s, op))
2376                .unwrap_or_default();
2377            ctx.push(Value::Str(v));
2378            return Ok(true);
2379        }
2380        SET_QUICK_SAVE_COMMENT => {
2381            let idx = p_i64(params, 0).max(0) as usize;
2382            let comment = params
2383                .get(1)
2384                .and_then(|v| v.as_str())
2385                .unwrap_or("")
2386                .to_string();
2387            let slot = ensure_slot(&mut ctx.globals.syscom.quick_save_slots, idx);
2388            slot.exist = true;
2389            slot.comment = comment;
2390            {
2391                let save_cnt = configured_save_count(ctx, false);
2392                let quick_cnt = configured_save_count(ctx, true);
2393                persist_slot_with_counts(
2394                    &ctx.project_dir,
2395                    true,
2396                    save_cnt,
2397                    quick_cnt,
2398                    &ctx.globals.syscom.quick_save_slots,
2399                    idx,
2400                );
2401            }
2402        }
2403        GET_QUICK_SAVE_VALUE => {
2404            let idx = p_i64(params, 0).max(0) as usize;
2405            {
2406                let save_cnt = configured_save_count(ctx, false);
2407                let quick_cnt = configured_save_count(ctx, true);
2408                ensure_slot_loaded_with_counts(
2409                    &ctx.project_dir,
2410                    true,
2411                    save_cnt,
2412                    quick_cnt,
2413                    &mut ctx.globals.syscom.quick_save_slots,
2414                    idx,
2415                );
2416            }
2417            if let Some(Value::Element(chain)) = params.get(1).map(|v| v.unwrap_named()) {
2418                let Some(form_id) = chain.first().copied() else {
2419                    ctx.push(Value::Int(0));
2420                    return Ok(true);
2421                };
2422                let flag_index = p_i64(params, 2).max(0) as usize;
2423                let flag_cnt = p_i64(params, 3).max(0) as usize;
2424                let values: Vec<i64> = (0..flag_cnt)
2425                    .map(|i| {
2426                        ctx.globals
2427                            .syscom
2428                            .quick_save_slots
2429                            .get(idx)
2430                            .and_then(|s| s.values.get(&(i as i32)).copied())
2431                            .unwrap_or(0)
2432                    })
2433                    .collect();
2434                let list = ctx.globals.int_lists.entry(form_id as u32).or_default();
2435                if list.len() < flag_index + flag_cnt {
2436                    list.resize(flag_index + flag_cnt, 0);
2437                }
2438                for (i, v) in values.into_iter().enumerate() {
2439                    list[flag_index + i] = v;
2440                }
2441                ctx.push(Value::Int(0));
2442                return Ok(true);
2443            }
2444            let key = p_i64(params, 1) as i32;
2445            let v = ctx
2446                .globals
2447                .syscom
2448                .quick_save_slots
2449                .get(idx)
2450                .and_then(|s| s.values.get(&key).copied())
2451                .unwrap_or(0);
2452            ctx.push(Value::Int(v));
2453            return Ok(true);
2454        }
2455        SET_QUICK_SAVE_VALUE => {
2456            let idx = p_i64(params, 0).max(0) as usize;
2457            if let Some(Value::Element(chain)) = params.get(1).map(|v| v.unwrap_named()) {
2458                let Some(form_id) = chain.first().copied() else {
2459                    return Ok(true);
2460                };
2461                let flag_index = p_i64(params, 2).max(0) as usize;
2462                let flag_cnt = p_i64(params, 3).max(0) as usize;
2463                let values: Vec<i64> = (0..flag_cnt)
2464                    .map(|i| {
2465                        ctx.globals
2466                            .int_lists
2467                            .get(&(form_id as u32))
2468                            .and_then(|list| list.get(flag_index + i).copied())
2469                            .unwrap_or(0)
2470                    })
2471                    .collect();
2472                let slot = ensure_slot(&mut ctx.globals.syscom.quick_save_slots, idx);
2473                slot.exist = true;
2474                for (i, v) in values.into_iter().enumerate() {
2475                    slot.values.insert(i as i32, v);
2476                }
2477                {
2478                    let save_cnt = configured_save_count(ctx, false);
2479                    let quick_cnt = configured_save_count(ctx, true);
2480                    persist_slot_with_counts(
2481                        &ctx.project_dir,
2482                        true,
2483                        save_cnt,
2484                        quick_cnt,
2485                        &ctx.globals.syscom.quick_save_slots,
2486                        idx,
2487                    );
2488                }
2489                return Ok(true);
2490            }
2491            let key = p_i64(params, 1) as i32;
2492            let val = p_i64(params, 2);
2493            let slot = ensure_slot(&mut ctx.globals.syscom.quick_save_slots, idx);
2494            slot.exist = true;
2495            slot.values.insert(key, val);
2496            {
2497                let save_cnt = configured_save_count(ctx, false);
2498                let quick_cnt = configured_save_count(ctx, true);
2499                persist_slot_with_counts(
2500                    &ctx.project_dir,
2501                    true,
2502                    save_cnt,
2503                    quick_cnt,
2504                    &ctx.globals.syscom.quick_save_slots,
2505                    idx,
2506                );
2507            }
2508        }
2509        GET_END_SAVE_EXIST => {
2510            let save_cnt = configured_save_count(ctx, false);
2511            let quick_cnt = configured_save_count(ctx, true);
2512            let end_path = original_save::save_file_path_with_counts(
2513                &ctx.project_dir,
2514                save_cnt,
2515                quick_cnt,
2516                SaveKind::End,
2517                0,
2518            );
2519            let v = if ctx.globals.syscom.end_save_exists || end_path.exists() { 1 } else { 0 };
2520            ctx.push(Value::Int(v));
2521            return Ok(true);
2522        }
2523        COPY_SAVE => {
2524            let src = p_i64(params, 0).max(0) as usize;
2525            let dst = p_i64(params, 1).max(0) as usize;
2526            let save_cnt = configured_save_count(ctx, false);
2527            let quick_cnt = configured_save_count(ctx, true);
2528            let thumb_config = save_thumb_config(ctx);
2529            let ok = copy_slot(
2530                &ctx.project_dir,
2531                false,
2532                save_cnt,
2533                quick_cnt,
2534                thumb_config,
2535                &mut ctx.globals.syscom.save_slots,
2536                src,
2537                dst,
2538            );
2539            ctx.globals.syscom.last_menu_call = op;
2540            ctx.push(Value::Int(if ok { 1 } else { 0 }));
2541            return Ok(true);
2542        }
2543        COPY_QUICK_SAVE => {
2544            let src = p_i64(params, 0).max(0) as usize;
2545            let dst = p_i64(params, 1).max(0) as usize;
2546            let save_cnt = configured_save_count(ctx, false);
2547            let quick_cnt = configured_save_count(ctx, true);
2548            let thumb_config = save_thumb_config(ctx);
2549            let ok = copy_slot(
2550                &ctx.project_dir,
2551                true,
2552                save_cnt,
2553                quick_cnt,
2554                thumb_config,
2555                &mut ctx.globals.syscom.quick_save_slots,
2556                src,
2557                dst,
2558            );
2559            ctx.globals.syscom.last_menu_call = op;
2560            ctx.push(Value::Int(if ok { 1 } else { 0 }));
2561            return Ok(true);
2562        }
2563        CHANGE_SAVE => {
2564            let a = p_i64(params, 0).max(0) as usize;
2565            let b = p_i64(params, 1).max(0) as usize;
2566            let save_cnt = configured_save_count(ctx, false);
2567            let quick_cnt = configured_save_count(ctx, true);
2568            let thumb_config = save_thumb_config(ctx);
2569            let ok = change_slot(
2570                &ctx.project_dir,
2571                false,
2572                save_cnt,
2573                quick_cnt,
2574                thumb_config,
2575                &mut ctx.globals.syscom.save_slots,
2576                a,
2577                b,
2578            );
2579            ctx.globals.syscom.last_menu_call = op;
2580            ctx.push(Value::Int(if ok { 1 } else { 0 }));
2581            return Ok(true);
2582        }
2583        CHANGE_QUICK_SAVE => {
2584            let a = p_i64(params, 0).max(0) as usize;
2585            let b = p_i64(params, 1).max(0) as usize;
2586            let save_cnt = configured_save_count(ctx, false);
2587            let quick_cnt = configured_save_count(ctx, true);
2588            let thumb_config = save_thumb_config(ctx);
2589            let ok = change_slot(
2590                &ctx.project_dir,
2591                true,
2592                save_cnt,
2593                quick_cnt,
2594                thumb_config,
2595                &mut ctx.globals.syscom.quick_save_slots,
2596                a,
2597                b,
2598            );
2599            ctx.globals.syscom.last_menu_call = op;
2600            ctx.push(Value::Int(if ok { 1 } else { 0 }));
2601            return Ok(true);
2602        }
2603        DELETE_SAVE => {
2604            let idx = p_i64(params, 0).max(0) as usize;
2605            let save_cnt = configured_save_count(ctx, false);
2606            let quick_cnt = configured_save_count(ctx, true);
2607            let thumb_config = save_thumb_config(ctx);
2608            let ok = delete_slot(
2609                &ctx.project_dir,
2610                false,
2611                save_cnt,
2612                quick_cnt,
2613                thumb_config,
2614                &mut ctx.globals.syscom.save_slots,
2615                idx,
2616            );
2617            ctx.globals.syscom.last_menu_call = op;
2618            ctx.push(Value::Int(if ok { 1 } else { 0 }));
2619            return Ok(true);
2620        }
2621        DELETE_QUICK_SAVE => {
2622            let idx = p_i64(params, 0).max(0) as usize;
2623            let save_cnt = configured_save_count(ctx, false);
2624            let quick_cnt = configured_save_count(ctx, true);
2625            let thumb_config = save_thumb_config(ctx);
2626            let ok = delete_slot(
2627                &ctx.project_dir,
2628                true,
2629                save_cnt,
2630                quick_cnt,
2631                thumb_config,
2632                &mut ctx.globals.syscom.quick_save_slots,
2633                idx,
2634            );
2635            ctx.globals.syscom.last_menu_call = op;
2636            ctx.push(Value::Int(if ok { 1 } else { 0 }));
2637            return Ok(true);
2638        }
2639        CALL_CONFIG_MENU
2640        | CALL_CONFIG_WINDOW_MODE_MENU
2641        | CALL_CONFIG_VOLUME_MENU
2642        | CALL_CONFIG_BGMFADE_MENU
2643        | CALL_CONFIG_KOEMODE_MENU
2644        | CALL_CONFIG_CHARAKOE_MENU
2645        | CALL_CONFIG_JITAN_MENU
2646        | CALL_CONFIG_MESSAGE_SPEED_MENU
2647        | CALL_CONFIG_FILTER_COLOR_MENU
2648        | CALL_CONFIG_AUTO_MODE_MENU
2649        | CALL_CONFIG_FONT_MENU
2650        | CALL_CONFIG_SYSTEM_MENU
2651        | CALL_CONFIG_MOVIE_MENU => {
2652            set_syscom_pending_proc(ctx, SyscomPendingProcKind::OpenConfig);
2653            ctx.globals.syscom.last_menu_call = op;
2654        }
2655        SET_WINDOW_MODE => cfg_set_int(&mut ctx.globals.syscom, GET_WINDOW_MODE, p_i64(params, 0)),
2656        SET_WINDOW_MODE_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_WINDOW_MODE, 0),
2657        GET_WINDOW_MODE => {
2658            let v = cfg_get_int(&ctx.globals.syscom, GET_WINDOW_MODE, 0);
2659            ctx.push(Value::Int(v));
2660            return Ok(true);
2661        }
2662        SET_WINDOW_MODE_SIZE => cfg_set_int(
2663            &mut ctx.globals.syscom,
2664            GET_WINDOW_MODE_SIZE,
2665            p_i64(params, 0),
2666        ),
2667        SET_WINDOW_MODE_SIZE_DEFAULT => {
2668            cfg_set_int(&mut ctx.globals.syscom, GET_WINDOW_MODE_SIZE, 0)
2669        }
2670        GET_WINDOW_MODE_SIZE => {
2671            let v = cfg_get_int(&ctx.globals.syscom, GET_WINDOW_MODE_SIZE, 0);
2672            ctx.push(Value::Int(v));
2673            return Ok(true);
2674        }
2675        CHECK_WINDOW_MODE_SIZE_ENABLE => {
2676            ctx.push(Value::Int(1));
2677            return Ok(true);
2678        }
2679        SET_ALL_VOLUME => {
2680            cfg_set_int(&mut ctx.globals.syscom, GET_ALL_VOLUME, p_i64(params, 0));
2681            apply_audio_config(ctx);
2682        }
2683        SET_BGM_VOLUME => {
2684            cfg_set_int(&mut ctx.globals.syscom, GET_BGM_VOLUME, p_i64(params, 0));
2685            apply_audio_config(ctx);
2686        }
2687        SET_KOE_VOLUME => {
2688            cfg_set_int(&mut ctx.globals.syscom, GET_KOE_VOLUME, p_i64(params, 0));
2689            apply_audio_config(ctx);
2690        }
2691        SET_PCM_VOLUME => {
2692            cfg_set_int(&mut ctx.globals.syscom, GET_PCM_VOLUME, p_i64(params, 0));
2693            apply_audio_config(ctx);
2694        }
2695        SET_SE_VOLUME => {
2696            cfg_set_int(&mut ctx.globals.syscom, GET_SE_VOLUME, p_i64(params, 0));
2697            apply_audio_config(ctx);
2698        }
2699        SET_MOV_VOLUME => {
2700            cfg_set_int(&mut ctx.globals.syscom, GET_MOV_VOLUME, p_i64(params, 0));
2701            apply_audio_config(ctx);
2702        }
2703        SET_SOUND_VOLUME => {
2704            cfg_set_int(&mut ctx.globals.syscom, GET_SOUND_VOLUME, p_i64(params, 0));
2705        }
2706        SET_ALL_VOLUME_DEFAULT => {
2707            cfg_set_int(&mut ctx.globals.syscom, GET_ALL_VOLUME, 100);
2708            apply_audio_config(ctx);
2709        }
2710        SET_BGM_VOLUME_DEFAULT => {
2711            cfg_set_int(&mut ctx.globals.syscom, GET_BGM_VOLUME, 100);
2712            apply_audio_config(ctx);
2713        }
2714        SET_KOE_VOLUME_DEFAULT => {
2715            cfg_set_int(&mut ctx.globals.syscom, GET_KOE_VOLUME, 100);
2716            apply_audio_config(ctx);
2717        }
2718        SET_PCM_VOLUME_DEFAULT => {
2719            cfg_set_int(&mut ctx.globals.syscom, GET_PCM_VOLUME, 100);
2720            apply_audio_config(ctx);
2721        }
2722        SET_SE_VOLUME_DEFAULT => {
2723            cfg_set_int(&mut ctx.globals.syscom, GET_SE_VOLUME, 100);
2724            apply_audio_config(ctx);
2725        }
2726        SET_MOV_VOLUME_DEFAULT => {
2727            cfg_set_int(&mut ctx.globals.syscom, GET_MOV_VOLUME, 100);
2728            apply_audio_config(ctx);
2729        }
2730        SET_SOUND_VOLUME_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_SOUND_VOLUME, 100),
2731        GET_ALL_VOLUME | GET_BGM_VOLUME | GET_KOE_VOLUME | GET_PCM_VOLUME | GET_SE_VOLUME
2732        | GET_MOV_VOLUME | GET_SOUND_VOLUME => {
2733            let v = cfg_get_int(&ctx.globals.syscom, op, 100);
2734            ctx.push(Value::Int(v));
2735            return Ok(true);
2736        }
2737        SET_ALL_ONOFF => {
2738            cfg_set_int(
2739                &mut ctx.globals.syscom,
2740                GET_ALL_ONOFF,
2741                if p_bool(params, 0) { 1 } else { 0 },
2742            );
2743            apply_audio_config(ctx);
2744        }
2745        SET_BGM_ONOFF => {
2746            cfg_set_int(
2747                &mut ctx.globals.syscom,
2748                GET_BGM_ONOFF,
2749                if p_bool(params, 0) { 1 } else { 0 },
2750            );
2751            apply_audio_config(ctx);
2752        }
2753        SET_KOE_ONOFF => {
2754            cfg_set_int(
2755                &mut ctx.globals.syscom,
2756                GET_KOE_ONOFF,
2757                if p_bool(params, 0) { 1 } else { 0 },
2758            );
2759            apply_audio_config(ctx);
2760        }
2761        SET_PCM_ONOFF => {
2762            cfg_set_int(
2763                &mut ctx.globals.syscom,
2764                GET_PCM_ONOFF,
2765                if p_bool(params, 0) { 1 } else { 0 },
2766            );
2767            apply_audio_config(ctx);
2768        }
2769        SET_SE_ONOFF => {
2770            cfg_set_int(
2771                &mut ctx.globals.syscom,
2772                GET_SE_ONOFF,
2773                if p_bool(params, 0) { 1 } else { 0 },
2774            );
2775            apply_audio_config(ctx);
2776        }
2777        SET_MOV_ONOFF => {
2778            cfg_set_int(
2779                &mut ctx.globals.syscom,
2780                GET_MOV_ONOFF,
2781                if p_bool(params, 0) { 1 } else { 0 },
2782            );
2783            apply_audio_config(ctx);
2784        }
2785        SET_SOUND_ONOFF => {
2786            cfg_set_int(
2787                &mut ctx.globals.syscom,
2788                GET_SOUND_ONOFF,
2789                if p_bool(params, 0) { 1 } else { 0 },
2790            );
2791        }
2792        SET_ALL_ONOFF_DEFAULT => {
2793            cfg_set_int(&mut ctx.globals.syscom, GET_ALL_ONOFF, 1);
2794            apply_audio_config(ctx);
2795        }
2796        SET_BGM_ONOFF_DEFAULT => {
2797            cfg_set_int(&mut ctx.globals.syscom, GET_BGM_ONOFF, 1);
2798            apply_audio_config(ctx);
2799        }
2800        SET_KOE_ONOFF_DEFAULT => {
2801            cfg_set_int(&mut ctx.globals.syscom, GET_KOE_ONOFF, 1);
2802            apply_audio_config(ctx);
2803        }
2804        SET_PCM_ONOFF_DEFAULT => {
2805            cfg_set_int(&mut ctx.globals.syscom, GET_PCM_ONOFF, 1);
2806            apply_audio_config(ctx);
2807        }
2808        SET_SE_ONOFF_DEFAULT => {
2809            cfg_set_int(&mut ctx.globals.syscom, GET_SE_ONOFF, 1);
2810            apply_audio_config(ctx);
2811        }
2812        SET_MOV_ONOFF_DEFAULT => {
2813            cfg_set_int(&mut ctx.globals.syscom, GET_MOV_ONOFF, 1);
2814            apply_audio_config(ctx);
2815        }
2816        SET_SOUND_ONOFF_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_SOUND_ONOFF, 1),
2817        GET_ALL_ONOFF | GET_BGM_ONOFF | GET_KOE_ONOFF | GET_PCM_ONOFF | GET_SE_ONOFF
2818        | GET_MOV_ONOFF | GET_SOUND_ONOFF => {
2819            let v = cfg_get_int(&ctx.globals.syscom, op, 1);
2820            ctx.push(Value::Int(v));
2821            return Ok(true);
2822        }
2823        SET_BGMFADE_VOLUME => cfg_set_int(
2824            &mut ctx.globals.syscom,
2825            GET_BGMFADE_VOLUME,
2826            p_i64(params, 0),
2827        ),
2828        SET_BGMFADE_ONOFF => cfg_set_int(
2829            &mut ctx.globals.syscom,
2830            GET_BGMFADE_ONOFF,
2831            if p_bool(params, 0) { 1 } else { 0 },
2832        ),
2833        SET_BGMFADE_VOLUME_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_BGMFADE_VOLUME, 100),
2834        SET_BGMFADE_ONOFF_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_BGMFADE_ONOFF, 1),
2835        GET_BGMFADE_VOLUME | GET_BGMFADE_ONOFF => {
2836            let default = if op == GET_BGMFADE_ONOFF { 1 } else { 100 };
2837            let v = cfg_get_int(&ctx.globals.syscom, op, default);
2838            ctx.push(Value::Int(v));
2839            return Ok(true);
2840        }
2841        SET_KOEMODE => cfg_set_int(&mut ctx.globals.syscom, GET_KOEMODE, p_i64(params, 0)),
2842        SET_KOEMODE_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_KOEMODE, 0),
2843        GET_KOEMODE => {
2844            let v = cfg_get_int(&ctx.globals.syscom, GET_KOEMODE, 0);
2845            ctx.push(Value::Int(v));
2846            return Ok(true);
2847        }
2848        SET_CHARAKOE_ONOFF => cfg_set_int(
2849            &mut ctx.globals.syscom,
2850            GET_CHARAKOE_ONOFF,
2851            if p_bool(params, 0) { 1 } else { 0 },
2852        ),
2853        SET_CHARAKOE_ONOFF_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_CHARAKOE_ONOFF, 1),
2854        GET_CHARAKOE_ONOFF => {
2855            let v = cfg_get_int(&ctx.globals.syscom, GET_CHARAKOE_ONOFF, 1);
2856            ctx.push(Value::Int(v));
2857            return Ok(true);
2858        }
2859        SET_CHARAKOE_VOLUME => cfg_set_int(
2860            &mut ctx.globals.syscom,
2861            GET_CHARAKOE_VOLUME,
2862            p_i64(params, 0),
2863        ),
2864        SET_CHARAKOE_VOLUME_DEFAULT => {
2865            cfg_set_int(&mut ctx.globals.syscom, GET_CHARAKOE_VOLUME, 100)
2866        }
2867        GET_CHARAKOE_VOLUME => {
2868            let v = cfg_get_int(&ctx.globals.syscom, GET_CHARAKOE_VOLUME, 100);
2869            ctx.push(Value::Int(v));
2870            return Ok(true);
2871        }
2872        SET_JITAN_NORMAL_ONOFF => cfg_set_int(
2873            &mut ctx.globals.syscom,
2874            GET_JITAN_NORMAL_ONOFF,
2875            if p_bool(params, 0) { 1 } else { 0 },
2876        ),
2877        SET_JITAN_NORMAL_ONOFF_DEFAULT => {
2878            cfg_set_int(&mut ctx.globals.syscom, GET_JITAN_NORMAL_ONOFF, 0)
2879        }
2880        GET_JITAN_NORMAL_ONOFF => {
2881            let v = cfg_get_int(&ctx.globals.syscom, GET_JITAN_NORMAL_ONOFF, 0);
2882            ctx.push(Value::Int(v));
2883            return Ok(true);
2884        }
2885        SET_JITAN_AUTO_MODE_ONOFF => cfg_set_int(
2886            &mut ctx.globals.syscom,
2887            GET_JITAN_AUTO_MODE_ONOFF,
2888            if p_bool(params, 0) { 1 } else { 0 },
2889        ),
2890        SET_JITAN_AUTO_MODE_ONOFF_DEFAULT => {
2891            cfg_set_int(&mut ctx.globals.syscom, GET_JITAN_AUTO_MODE_ONOFF, 0)
2892        }
2893        GET_JITAN_AUTO_MODE_ONOFF => {
2894            let v = cfg_get_int(&ctx.globals.syscom, GET_JITAN_AUTO_MODE_ONOFF, 0);
2895            ctx.push(Value::Int(v));
2896            return Ok(true);
2897        }
2898        SET_JITAN_KOE_REPLAY_ONOFF => cfg_set_int(
2899            &mut ctx.globals.syscom,
2900            GET_JITAN_KOE_REPLAY_ONOFF,
2901            if p_bool(params, 0) { 1 } else { 0 },
2902        ),
2903        SET_JITAN_KOE_REPLAY_ONOFF_DEFAULT => {
2904            cfg_set_int(&mut ctx.globals.syscom, GET_JITAN_KOE_REPLAY_ONOFF, 0)
2905        }
2906        GET_JITAN_KOE_REPLAY_ONOFF => {
2907            let v = cfg_get_int(&ctx.globals.syscom, GET_JITAN_KOE_REPLAY_ONOFF, 0);
2908            ctx.push(Value::Int(v));
2909            return Ok(true);
2910        }
2911        SET_JITAN_SPEED => cfg_set_int(&mut ctx.globals.syscom, GET_JITAN_SPEED, p_i64(params, 0)),
2912        SET_JITAN_SPEED_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_JITAN_SPEED, 0),
2913        GET_JITAN_SPEED => {
2914            let v = cfg_get_int(&ctx.globals.syscom, GET_JITAN_SPEED, 0);
2915            ctx.push(Value::Int(v));
2916            return Ok(true);
2917        }
2918        SET_MESSAGE_SPEED => {
2919            cfg_set_int(&mut ctx.globals.syscom, GET_MESSAGE_SPEED, p_i64(params, 0))
2920        }
2921        SET_MESSAGE_SPEED_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_MESSAGE_SPEED, 20),
2922        GET_MESSAGE_SPEED => {
2923            let v = cfg_get_int(&ctx.globals.syscom, GET_MESSAGE_SPEED, 20);
2924            ctx.push(Value::Int(v));
2925            return Ok(true);
2926        }
2927        SET_MESSAGE_NOWAIT => {
2928            let v = p_bool(params, 0);
2929            ctx.globals.script.msg_nowait = v;
2930            cfg_set_int(
2931                &mut ctx.globals.syscom,
2932                GET_MESSAGE_NOWAIT,
2933                if v { 1 } else { 0 },
2934            );
2935        }
2936        SET_MESSAGE_NOWAIT_DEFAULT => {
2937            ctx.globals.script.msg_nowait = false;
2938            cfg_set_int(&mut ctx.globals.syscom, GET_MESSAGE_NOWAIT, 0);
2939        }
2940        GET_MESSAGE_NOWAIT => {
2941            let v = if ctx.globals.script.msg_nowait {
2942                1
2943            } else {
2944                cfg_get_int(&ctx.globals.syscom, GET_MESSAGE_NOWAIT, 0)
2945            };
2946            ctx.push(Value::Int(v));
2947            return Ok(true);
2948        }
2949        SET_AUTO_MODE_MOJI_WAIT => {
2950            let v = p_i64(params, 0);
2951            ctx.globals.script.auto_mode_moji_wait = v;
2952            cfg_set_int(&mut ctx.globals.syscom, GET_AUTO_MODE_MOJI_WAIT, v);
2953        }
2954        SET_AUTO_MODE_MOJI_WAIT_DEFAULT => {
2955            ctx.globals.script.auto_mode_moji_wait = -1;
2956            cfg_set_int(&mut ctx.globals.syscom, GET_AUTO_MODE_MOJI_WAIT, -1);
2957        }
2958        GET_AUTO_MODE_MOJI_WAIT => {
2959            let v = ctx.globals.script.auto_mode_moji_wait;
2960            ctx.push(Value::Int(v));
2961            return Ok(true);
2962        }
2963        SET_AUTO_MODE_MIN_WAIT => {
2964            let v = p_i64(params, 0);
2965            ctx.globals.script.auto_mode_min_wait = v;
2966            cfg_set_int(&mut ctx.globals.syscom, GET_AUTO_MODE_MIN_WAIT, v);
2967        }
2968        SET_AUTO_MODE_MIN_WAIT_DEFAULT => {
2969            ctx.globals.script.auto_mode_min_wait = -1;
2970            cfg_set_int(&mut ctx.globals.syscom, GET_AUTO_MODE_MIN_WAIT, -1);
2971        }
2972        GET_AUTO_MODE_MIN_WAIT => {
2973            let v = ctx.globals.script.auto_mode_min_wait;
2974            ctx.push(Value::Int(v));
2975            return Ok(true);
2976        }
2977        SET_MOUSE_CURSOR_HIDE_ONOFF => cfg_set_int(
2978            &mut ctx.globals.syscom,
2979            GET_MOUSE_CURSOR_HIDE_ONOFF,
2980            if p_bool(params, 0) { 1 } else { 0 },
2981        ),
2982        SET_MOUSE_CURSOR_HIDE_ONOFF_DEFAULT => {
2983            let v = config_mouse_cursor_hide_onoff_default(ctx);
2984            cfg_set_int(&mut ctx.globals.syscom, GET_MOUSE_CURSOR_HIDE_ONOFF, v)
2985        }
2986        GET_MOUSE_CURSOR_HIDE_ONOFF => {
2987            let v = cfg_get_int(
2988                &ctx.globals.syscom,
2989                GET_MOUSE_CURSOR_HIDE_ONOFF,
2990                config_mouse_cursor_hide_onoff_default(ctx),
2991            );
2992            ctx.push(Value::Int(v));
2993            return Ok(true);
2994        }
2995        SET_MOUSE_CURSOR_HIDE_TIME => cfg_set_int(
2996            &mut ctx.globals.syscom,
2997            GET_MOUSE_CURSOR_HIDE_TIME,
2998            p_i64(params, 0),
2999        ),
3000        SET_MOUSE_CURSOR_HIDE_TIME_DEFAULT => {
3001            let v = config_mouse_cursor_hide_time_default(ctx);
3002            cfg_set_int(&mut ctx.globals.syscom, GET_MOUSE_CURSOR_HIDE_TIME, v)
3003        }
3004        GET_MOUSE_CURSOR_HIDE_TIME => {
3005            let v = cfg_get_int(
3006                &ctx.globals.syscom,
3007                GET_MOUSE_CURSOR_HIDE_TIME,
3008                config_mouse_cursor_hide_time_default(ctx),
3009            );
3010            ctx.push(Value::Int(v));
3011            return Ok(true);
3012        }
3013        SET_FILTER_COLOR_R => cfg_set_int(
3014            &mut ctx.globals.syscom,
3015            GET_FILTER_COLOR_R,
3016            p_i64(params, 0),
3017        ),
3018        SET_FILTER_COLOR_G => cfg_set_int(
3019            &mut ctx.globals.syscom,
3020            GET_FILTER_COLOR_G,
3021            p_i64(params, 0),
3022        ),
3023        SET_FILTER_COLOR_B => cfg_set_int(
3024            &mut ctx.globals.syscom,
3025            GET_FILTER_COLOR_B,
3026            p_i64(params, 0),
3027        ),
3028        SET_FILTER_COLOR_A => cfg_set_int(
3029            &mut ctx.globals.syscom,
3030            GET_FILTER_COLOR_A,
3031            p_i64(params, 0),
3032        ),
3033        SET_FILTER_COLOR_R_DEFAULT => {
3034            let (r, _, _, _) = config_filter_color_default(ctx);
3035            cfg_set_int(&mut ctx.globals.syscom, GET_FILTER_COLOR_R, r)
3036        }
3037        SET_FILTER_COLOR_G_DEFAULT => {
3038            let (_, g, _, _) = config_filter_color_default(ctx);
3039            cfg_set_int(&mut ctx.globals.syscom, GET_FILTER_COLOR_G, g)
3040        }
3041        SET_FILTER_COLOR_B_DEFAULT => {
3042            let (_, _, b, _) = config_filter_color_default(ctx);
3043            cfg_set_int(&mut ctx.globals.syscom, GET_FILTER_COLOR_B, b)
3044        }
3045        SET_FILTER_COLOR_A_DEFAULT => {
3046            let (_, _, _, a) = config_filter_color_default(ctx);
3047            cfg_set_int(&mut ctx.globals.syscom, GET_FILTER_COLOR_A, a)
3048        }
3049        GET_FILTER_COLOR_R | GET_FILTER_COLOR_G | GET_FILTER_COLOR_B | GET_FILTER_COLOR_A => {
3050            let (r, g, b, a) = config_filter_color_default(ctx);
3051            let default = match op {
3052                GET_FILTER_COLOR_R => r,
3053                GET_FILTER_COLOR_G => g,
3054                GET_FILTER_COLOR_B => b,
3055                GET_FILTER_COLOR_A => a,
3056                _ => 0,
3057            };
3058            let v = cfg_get_int(&ctx.globals.syscom, op, default);
3059            ctx.push(Value::Int(v));
3060            return Ok(true);
3061        }
3062        SET_OBJECT_DISP_ONOFF => cfg_set_int(
3063            &mut ctx.globals.syscom,
3064            GET_OBJECT_DISP_ONOFF,
3065            if p_bool(params, 0) { 1 } else { 0 },
3066        ),
3067        SET_OBJECT_DISP_ONOFF_DEFAULT => {
3068            cfg_set_int(&mut ctx.globals.syscom, GET_OBJECT_DISP_ONOFF, 1)
3069        }
3070        GET_OBJECT_DISP_ONOFF => {
3071            let v = cfg_get_int(&ctx.globals.syscom, GET_OBJECT_DISP_ONOFF, 1);
3072            ctx.push(Value::Int(v));
3073            return Ok(true);
3074        }
3075        SET_GLOBAL_EXTRA_SWITCH_ONOFF => cfg_set_int(
3076            &mut ctx.globals.syscom,
3077            GET_GLOBAL_EXTRA_SWITCH_ONOFF,
3078            if p_bool(params, 0) { 1 } else { 0 },
3079        ),
3080        SET_GLOBAL_EXTRA_SWITCH_ONOFF_DEFAULT => {
3081            cfg_set_int(&mut ctx.globals.syscom, GET_GLOBAL_EXTRA_SWITCH_ONOFF, 0)
3082        }
3083        GET_GLOBAL_EXTRA_SWITCH_ONOFF => {
3084            let v = cfg_get_int(&ctx.globals.syscom, GET_GLOBAL_EXTRA_SWITCH_ONOFF, 0);
3085            ctx.push(Value::Int(v));
3086            return Ok(true);
3087        }
3088        SET_GLOBAL_EXTRA_MODE_VALUE => cfg_set_int(
3089            &mut ctx.globals.syscom,
3090            GET_GLOBAL_EXTRA_MODE_VALUE,
3091            p_i64(params, 0),
3092        ),
3093        SET_GLOBAL_EXTRA_MODE_VALUE_DEFAULT => {
3094            cfg_set_int(&mut ctx.globals.syscom, GET_GLOBAL_EXTRA_MODE_VALUE, 0)
3095        }
3096        GET_GLOBAL_EXTRA_MODE_VALUE => {
3097            let v = cfg_get_int(&ctx.globals.syscom, GET_GLOBAL_EXTRA_MODE_VALUE, 0);
3098            ctx.push(Value::Int(v));
3099            return Ok(true);
3100        }
3101        SET_SAVELOAD_ALERT_ONOFF => cfg_set_int(
3102            &mut ctx.globals.syscom,
3103            GET_SAVELOAD_ALERT_ONOFF,
3104            if p_bool(params, 0) { 1 } else { 0 },
3105        ),
3106        SET_SAVELOAD_ALERT_ONOFF_DEFAULT => {
3107            cfg_set_int(&mut ctx.globals.syscom, GET_SAVELOAD_ALERT_ONOFF, 1)
3108        }
3109        GET_SAVELOAD_ALERT_ONOFF => {
3110            let v = cfg_get_int(&ctx.globals.syscom, GET_SAVELOAD_ALERT_ONOFF, 1);
3111            ctx.push(Value::Int(v));
3112            return Ok(true);
3113        }
3114        SET_SLEEP_ONOFF => cfg_set_int(
3115            &mut ctx.globals.syscom,
3116            GET_SLEEP_ONOFF,
3117            if p_bool(params, 0) { 1 } else { 0 },
3118        ),
3119        SET_SLEEP_ONOFF_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_SLEEP_ONOFF, 1),
3120        GET_SLEEP_ONOFF => {
3121            let v = cfg_get_int(&ctx.globals.syscom, GET_SLEEP_ONOFF, 1);
3122            ctx.push(Value::Int(v));
3123            return Ok(true);
3124        }
3125        SET_NO_WIPE_ANIME_ONOFF => cfg_set_int(
3126            &mut ctx.globals.syscom,
3127            GET_NO_WIPE_ANIME_ONOFF,
3128            if p_bool(params, 0) { 1 } else { 0 },
3129        ),
3130        SET_NO_WIPE_ANIME_ONOFF_DEFAULT => {
3131            cfg_set_int(&mut ctx.globals.syscom, GET_NO_WIPE_ANIME_ONOFF, 0)
3132        }
3133        GET_NO_WIPE_ANIME_ONOFF => {
3134            let v = cfg_get_int(&ctx.globals.syscom, GET_NO_WIPE_ANIME_ONOFF, 0);
3135            ctx.push(Value::Int(v));
3136            return Ok(true);
3137        }
3138        SET_SKIP_WIPE_ANIME_ONOFF => cfg_set_int(
3139            &mut ctx.globals.syscom,
3140            GET_SKIP_WIPE_ANIME_ONOFF,
3141            if p_bool(params, 0) { 1 } else { 0 },
3142        ),
3143        SET_SKIP_WIPE_ANIME_ONOFF_DEFAULT => {
3144            cfg_set_int(&mut ctx.globals.syscom, GET_SKIP_WIPE_ANIME_ONOFF, 0)
3145        }
3146        GET_SKIP_WIPE_ANIME_ONOFF => {
3147            let v = cfg_get_int(&ctx.globals.syscom, GET_SKIP_WIPE_ANIME_ONOFF, 0);
3148            ctx.push(Value::Int(v));
3149            return Ok(true);
3150        }
3151        SET_NO_MWND_ANIME_ONOFF => cfg_set_int(
3152            &mut ctx.globals.syscom,
3153            GET_NO_MWND_ANIME_ONOFF,
3154            if p_bool(params, 0) { 1 } else { 0 },
3155        ),
3156        SET_NO_MWND_ANIME_ONOFF_DEFAULT => {
3157            cfg_set_int(&mut ctx.globals.syscom, GET_NO_MWND_ANIME_ONOFF, 0)
3158        }
3159        GET_NO_MWND_ANIME_ONOFF => {
3160            let v = cfg_get_int(&ctx.globals.syscom, GET_NO_MWND_ANIME_ONOFF, 0);
3161            ctx.push(Value::Int(v));
3162            return Ok(true);
3163        }
3164        SET_WHEEL_NEXT_MESSAGE_ONOFF => cfg_set_int(
3165            &mut ctx.globals.syscom,
3166            GET_WHEEL_NEXT_MESSAGE_ONOFF,
3167            if p_bool(params, 0) { 1 } else { 0 },
3168        ),
3169        SET_WHEEL_NEXT_MESSAGE_ONOFF_DEFAULT => {
3170            cfg_set_int(&mut ctx.globals.syscom, GET_WHEEL_NEXT_MESSAGE_ONOFF, 1)
3171        }
3172        GET_WHEEL_NEXT_MESSAGE_ONOFF => {
3173            let v = cfg_get_int(&ctx.globals.syscom, GET_WHEEL_NEXT_MESSAGE_ONOFF, 1);
3174            ctx.push(Value::Int(v));
3175            return Ok(true);
3176        }
3177        SET_KOE_DONT_STOP_ONOFF => cfg_set_int(
3178            &mut ctx.globals.syscom,
3179            GET_KOE_DONT_STOP_ONOFF,
3180            if p_bool(params, 0) { 1 } else { 0 },
3181        ),
3182        SET_KOE_DONT_STOP_ONOFF_DEFAULT => {
3183            cfg_set_int(&mut ctx.globals.syscom, GET_KOE_DONT_STOP_ONOFF, 0)
3184        }
3185        GET_KOE_DONT_STOP_ONOFF => {
3186            let v = cfg_get_int(&ctx.globals.syscom, GET_KOE_DONT_STOP_ONOFF, 0);
3187            ctx.push(Value::Int(v));
3188            return Ok(true);
3189        }
3190        SET_SKIP_UNREAD_MESSAGE_ONOFF => cfg_set_int(
3191            &mut ctx.globals.syscom,
3192            GET_SKIP_UNREAD_MESSAGE_ONOFF,
3193            if p_bool(params, 0) { 1 } else { 0 },
3194        ),
3195        SET_SKIP_UNREAD_MESSAGE_ONOFF_DEFAULT => {
3196            cfg_set_int(&mut ctx.globals.syscom, GET_SKIP_UNREAD_MESSAGE_ONOFF, 0)
3197        }
3198        GET_SKIP_UNREAD_MESSAGE_ONOFF => {
3199            let v = cfg_get_int(&ctx.globals.syscom, GET_SKIP_UNREAD_MESSAGE_ONOFF, 0);
3200            ctx.push(Value::Int(v));
3201            return Ok(true);
3202        }
3203        SET_PLAY_SILENT_SOUND_ONOFF => cfg_set_int(
3204            &mut ctx.globals.syscom,
3205            GET_PLAY_SILENT_SOUND_ONOFF,
3206            if p_bool(params, 0) { 1 } else { 0 },
3207        ),
3208        SET_PLAY_SILENT_SOUND_ONOFF_DEFAULT => {
3209            cfg_set_int(&mut ctx.globals.syscom, GET_PLAY_SILENT_SOUND_ONOFF, 0)
3210        }
3211        GET_PLAY_SILENT_SOUND_ONOFF => {
3212            let v = cfg_get_int(&ctx.globals.syscom, GET_PLAY_SILENT_SOUND_ONOFF, 0);
3213            ctx.push(Value::Int(v));
3214            return Ok(true);
3215        }
3216        SET_FONT_NAME => {
3217            let v = params
3218                .get(0)
3219                .and_then(|v| v.as_str())
3220                .unwrap_or("")
3221                .to_string();
3222            cfg_set_str(&mut ctx.globals.syscom, GET_FONT_NAME, v);
3223        }
3224        SET_FONT_NAME_DEFAULT => cfg_set_str(&mut ctx.globals.syscom, GET_FONT_NAME, String::new()),
3225        GET_FONT_NAME => {
3226            let v = cfg_get_str(&ctx.globals.syscom, GET_FONT_NAME);
3227            ctx.push(Value::Str(v));
3228            return Ok(true);
3229        }
3230        IS_FONT_EXIST => {
3231            let name = params.get(0).and_then(|v| v.as_str()).unwrap_or("");
3232            let exists = font_exists(&ctx.project_dir, name);
3233            ctx.push(Value::Int(if exists { 1 } else { 0 }));
3234            return Ok(true);
3235        }
3236        SET_FONT_BOLD => cfg_set_int(
3237            &mut ctx.globals.syscom,
3238            GET_FONT_BOLD,
3239            if p_bool(params, 0) { 1 } else { 0 },
3240        ),
3241        SET_FONT_BOLD_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_FONT_BOLD, 0),
3242        GET_FONT_BOLD => {
3243            let v = cfg_get_int(&ctx.globals.syscom, GET_FONT_BOLD, 0);
3244            ctx.push(Value::Int(v));
3245            return Ok(true);
3246        }
3247        SET_FONT_DECORATION => cfg_set_int(
3248            &mut ctx.globals.syscom,
3249            GET_FONT_DECORATION,
3250            p_i64(params, 0),
3251        ),
3252        SET_FONT_DECORATION_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_FONT_DECORATION, 0),
3253        GET_FONT_DECORATION => {
3254            let v = cfg_get_int(&ctx.globals.syscom, GET_FONT_DECORATION, 0);
3255            ctx.push(Value::Int(v));
3256            return Ok(true);
3257        }
3258        CREATE_CAPTURE_BUFFER => {
3259            let w = p_i64(params, 0).max(1) as u32;
3260            let h = p_i64(params, 1).max(1) as u32;
3261            ctx.globals.syscom.capture_size = Some((w, h));
3262            ctx.globals.syscom.capture_buffer = None;
3263        }
3264        DESTROY_CAPTURE_BUFFER => {
3265            ctx.globals.syscom.capture_buffer = None;
3266            ctx.globals.syscom.capture_size = None;
3267        }
3268        CAPTURE_TO_CAPTURE_BUFFER => {
3269            let mut img = ctx.capture_frame_rgba();
3270            if let Some((w, h)) = ctx.globals.syscom.capture_size {
3271                img = resize_rgba(&img, w, h);
3272            }
3273            ctx.globals.syscom.capture_buffer = Some(img);
3274        }
3275        SAVE_CAPTURE_BUFFER_TO_FILE => {
3276            let file_name = params.get(0).and_then(|v| v.as_str()).unwrap_or("");
3277            let extension = params.get(1).and_then(|v| v.as_str()).unwrap_or("");
3278            let mut name = file_name.to_string();
3279            if !extension.is_empty()
3280                && !name
3281                    .to_ascii_lowercase()
3282                    .ends_with(&format!(".{}", extension.to_ascii_lowercase()))
3283            {
3284                name.push('.');
3285                name.push_str(extension);
3286            }
3287            let path = join_game_path(&ctx.project_dir, &name);
3288            if ctx.globals.syscom.capture_buffer.is_none() {
3289                let mut img = ctx.capture_frame_rgba();
3290                if let Some((w, h)) = ctx.globals.syscom.capture_size {
3291                    img = resize_rgba(&img, w, h);
3292                }
3293                ctx.globals.syscom.capture_buffer = Some(img);
3294            }
3295            if let Some(img) = ctx.globals.syscom.capture_buffer.as_ref() {
3296                write_rgba_png(&path, img);
3297                save_capture_flags_sidecar(ctx, &path, params);
3298                ctx.push(Value::Int(1));
3299            } else {
3300                ctx.push(Value::Int(0));
3301            }
3302            return Ok(true);
3303        }
3304        LOAD_FLAG_FROM_CAPTURE_FILE => {
3305            let file_name = params.get(0).and_then(|v| v.as_str()).unwrap_or("");
3306            let extension = params.get(1).and_then(|v| v.as_str()).unwrap_or("");
3307            let mut name = file_name.to_string();
3308            if !extension.is_empty()
3309                && !name
3310                    .to_ascii_lowercase()
3311                    .ends_with(&format!(".{}", extension.to_ascii_lowercase()))
3312            {
3313                name.push('.');
3314                name.push_str(extension);
3315            }
3316            let path = join_game_path(&ctx.project_dir, &name);
3317            let ok = load_capture_flags_sidecar(ctx, &path, params);
3318            ctx.push(Value::Int(if ok { 1 } else { 0 }));
3319            return Ok(true);
3320        }
3321        CAPTURE_AND_SAVE_BUFFER_TO_PNG => {
3322            let file_name = params.get(2).and_then(|v| v.as_str()).unwrap_or("");
3323            let path = join_game_path(&ctx.project_dir, file_name);
3324            let mut img = ctx.capture_frame_rgba();
3325            if let Some((w, h)) = ctx.globals.syscom.capture_size {
3326                img = resize_rgba(&img, w, h);
3327            }
3328            write_rgba_png(&path, &img);
3329        }
3330        OPEN_TWEET_DIALOG => {
3331            log::error!("SYSCOM.OPEN_TWEET_DIALOG is not implemented in this port");
3332        }
3333        SET_RETURN_SCENE_ONCE => {
3334            let name = params
3335                .get(0)
3336                .and_then(|v| v.as_str())
3337                .unwrap_or("")
3338                .to_string();
3339            let z_no = p_i64(params, 1);
3340            ctx.globals.syscom.return_scene_once = Some((name, z_no));
3341        }
3342        GET_SYSTEM_EXTRA_INT_VALUE => {
3343            let v = ctx.globals.syscom.system_extra_int_value;
3344            ctx.push(Value::Int(v));
3345            return Ok(true);
3346        }
3347        GET_SYSTEM_EXTRA_STR_VALUE => {
3348            let v = ctx.globals.syscom.system_extra_str_value.clone();
3349            ctx.push(Value::Str(v));
3350            return Ok(true);
3351        }
3352        _ => {
3353            return Ok(false);
3354        }
3355    }
3356
3357    ctx.push(Value::Int(0));
3358    Ok(true)
3359}