1use anyhow::Result;
2
3mod save_versions;
4
5use crate::runtime::globals::{
6 SaveSlotState, SyscomFallbackDialogKind, SyscomFallbackDialogState, SyscomPendingProc,
7 SyscomPendingProcKind, SystemMessageBoxButton, ToggleFeatureState, ValueFeatureState,
8};
9use crate::runtime::{CommandContext, RuntimeSaveKind, Value};
10use std::fs;
11use std::path::{Path, PathBuf};
12
13use crate::assets::RgbaImage;
14use crate::original_save::{self, SaveKind};
15
16use super::prop_access;
17
18use super::codes::syscom_op::*;
19
20struct Call<'a> {
21 op: i32,
22 params: &'a [Value],
23}
24
25fn parse_call<'a>(ctx: &CommandContext, form_id: u32, args: &'a [Value]) -> Option<Call<'a>> {
26 let (chain_pos, chain) = prop_access::parse_element_chain_ctx(ctx, form_id, args)?;
27 if chain.len() < 2 {
28 return None;
29 }
30 let params = prop_access::script_args(args, chain_pos);
31 Some(Call {
32 op: chain[1],
33 params,
34 })
35}
36
37fn p_i64(params: &[Value], idx: usize) -> i64 {
38 params.get(idx).and_then(|v| v.as_i64()).unwrap_or(0)
39}
40fn p_bool(params: &[Value], idx: usize) -> bool {
41 p_i64(params, idx) != 0
42}
43
44fn sg_debug_enabled_local() -> bool {
45 env_is_set!("SG_DEBUG")
46}
47
48fn set_syscom_pending_proc(ctx: &mut CommandContext, kind: SyscomPendingProcKind) {
49 ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
50 kind,
51 warning: false,
52 se_play: false,
53 fade_out: false,
54 leave_msgbk: false,
55 save_id: 0,
56 });
57 ctx.globals.syscom.menu_open = false;
58 ctx.globals.syscom.menu_kind = None;
59 ctx.globals.syscom.menu_result = None;
60}
61
62fn gameexe_unquoted_owned(ctx: &CommandContext, key: &str) -> String {
63 ctx.tables
64 .gameexe
65 .as_ref()
66 .and_then(|cfg| cfg.get_unquoted(key))
67 .unwrap_or("")
68 .to_string()
69}
70
71fn gameexe_value_owned(ctx: &CommandContext, key: &str) -> String {
72 ctx.tables
73 .gameexe
74 .as_ref()
75 .and_then(|cfg| cfg.get_value(key))
76 .unwrap_or("")
77 .to_string()
78}
79
80fn parse_i64_list_local(raw: &str) -> Vec<i64> {
81 raw.split(|c: char| c == ',' || c.is_whitespace())
82 .filter_map(|part| {
83 let t = part.trim();
84 if t.is_empty() {
85 None
86 } else {
87 t.parse::<i64>().ok()
88 }
89 })
90 .collect()
91}
92
93fn parse_first_i64_local(raw: &str) -> Option<i64> {
94 raw.split(|c: char| c == ',' || c.is_whitespace())
95 .find_map(|part| {
96 let t = part.trim();
97 if t.is_empty() {
98 None
99 } else {
100 t.parse::<i64>().ok()
101 }
102 })
103}
104
105fn gameexe_i64_or(ctx: &CommandContext, key: &str, default: i64) -> i64 {
106 ctx.tables
107 .gameexe
108 .as_ref()
109 .and_then(|cfg| cfg.get_i64(key))
110 .unwrap_or(default)
111}
112
113fn gameexe_bool_or(ctx: &CommandContext, key: &str, default: bool) -> bool {
114 gameexe_i64_or(ctx, key, if default { 1 } else { 0 }) != 0
115}
116
117fn config_default_sound_volume(ctx: &CommandContext, sound_type: usize) -> i64 {
118 let key = match sound_type {
119 0 => Some("CONFIG.VOLUME.BGM"),
120 1 => Some("CONFIG.VOLUME.KOE"),
121 2 => Some("CONFIG.VOLUME.PCM"),
122 3 => Some("CONFIG.VOLUME.SE"),
123 4 => Some("CONFIG.VOLUME.MOV"),
124 _ => None,
125 };
126 key.map(|key| gameexe_i64_or(ctx, key, 255))
127 .unwrap_or(255)
128 .clamp(0, 255)
129}
130
131fn config_default_sound_onoff(_ctx: &CommandContext, _sound_type: usize) -> bool {
132 true
135}
136
137fn config_default_chrkoe(
138 ctx: &CommandContext,
139 index: usize,
140) -> crate::runtime::globals::ConfigChrKoeState {
141 let Some(entry) = ctx
142 .tables
143 .gameexe
144 .as_ref()
145 .and_then(|cfg| cfg.get_indexed_entry("CHRKOE", index))
146 else {
147 return crate::runtime::globals::ConfigChrKoeState::default();
148 };
149 let onoff = entry
151 .item_unquoted(3)
152 .and_then(|v| v.parse::<i64>().ok())
153 .map(|v| v != 0)
154 .unwrap_or(true);
155 let volume = entry
156 .item_unquoted(4)
157 .and_then(|v| v.parse::<i64>().ok())
158 .unwrap_or(255)
159 .clamp(0, 255);
160 crate::runtime::globals::ConfigChrKoeState { onoff, volume }
161}
162
163pub fn config_chrkoe_name_visible(ctx: &CommandContext, index: usize) -> bool {
165 let Some(entry) = ctx
166 .tables
167 .gameexe
168 .as_ref()
169 .and_then(|cfg| cfg.get_indexed_entry("CHRKOE", index))
170 else {
171 return true;
172 };
173 let name = entry.item_unquoted(0).unwrap_or("");
174 ctx.globals
175 .syscom
176 .chrkoe_look_flags
177 .get(name)
178 .copied()
179 .unwrap_or_else(|| !matches!(entry.item_unquoted(1), Some("0" | "2")))
180}
181
182pub fn reveal_config_voice_name(ctx: &mut CommandContext, spoken_name: &str) {
183 let Some(cfg) = ctx.tables.gameexe.as_ref() else {
184 return;
185 };
186 for i in 0..configured_chrkoe_count(ctx) {
187 let Some(entry) = cfg.get_indexed_entry("CHRKOE", i) else {
188 continue;
189 };
190 if entry.item_unquoted(1) == Some("2")
191 && (entry.item_unquoted(0) == Some(spoken_name)
192 || entry.item_unquoted(2) == Some(spoken_name))
193 {
194 let name = entry.item_unquoted(0).unwrap_or("").to_owned();
195 ctx.globals.syscom.chrkoe_look_flags.insert(name, true);
196 }
197 }
198}
199
200fn config_default_indexed_bool(
201 ctx: &CommandContext,
202 prefix: &str,
203 index: usize,
204 default: bool,
205) -> bool {
206 ctx.tables
207 .gameexe
208 .as_ref()
209 .and_then(|cfg| cfg.get_indexed_field_unquoted(prefix, index, "ONOFF"))
210 .and_then(|v| v.parse::<i64>().ok())
211 .map(|v| v != 0)
212 .unwrap_or(default)
213}
214
215fn config_default_indexed_mode(ctx: &CommandContext, index: usize) -> i64 {
216 ctx.tables
217 .gameexe
218 .as_ref()
219 .and_then(|cfg| cfg.get_indexed_field_unquoted("CONFIG.GLOBAL_EXTRA_MODE", index, "MODE"))
220 .and_then(|v| v.parse::<i64>().ok())
221 .unwrap_or(0)
222}
223
224fn config_default_font_name(ctx: &CommandContext) -> String {
225 match gameexe_i64_or(ctx, "CONFIG.FONT.TYPE", 0) {
226 0 => "MS ゴシック".to_string(),
227 1 => "MS 明朝".to_string(),
228 2 => "メイリオ".to_string(),
229 3 => {
230 let name = gameexe_unquoted_owned(ctx, "CONFIG.FONT.NAME");
231 if name.is_empty() {
232 "MS ゴシック".to_string()
233 } else {
234 name
235 }
236 }
237 value => {
238 log::error!(
239 "invalid CONFIG.FONT.TYPE value {value}; using original default MS ゴシック"
240 );
241 "MS ゴシック".to_string()
242 }
243 }
244}
245
246pub fn original_config_defaults(
247 ctx: &CommandContext,
248) -> crate::runtime::globals::OriginalConfigRuntimeState {
249 let mut cfg = crate::runtime::globals::OriginalConfigRuntimeState {
250 screen_size_mode: gameexe_i64_or(ctx, "CONFIG.WINDOW_MODE", 0).clamp(0, 1),
251 ..Default::default()
252 };
253
254 let screen_size = (ctx.screen_w.max(1) as i64, ctx.screen_h.max(1) as i64);
255 cfg.screen_size_free = screen_size;
256 cfg.fullscreen_display_cnt = 1;
259 cfg.fullscreen_display_no = 0;
260 cfg.fullscreen_resolution_cnt = 1;
261 cfg.fullscreen_resolution_no = 0;
262 cfg.fullscreen_resolution = screen_size;
263 cfg.all_sound_user_volume = gameexe_i64_or(ctx, "CONFIG.VOLUME.ALL", 255).clamp(0, 255);
264 for index in 0..cfg.sound_user_volume.len() {
265 cfg.sound_user_volume[index] = config_default_sound_volume(ctx, index);
266 cfg.play_sound_check[index] = config_default_sound_onoff(ctx, index);
267 }
268 cfg.bgmfade_volume = gameexe_i64_or(ctx, "CONFIG.BGMFADE_VOLUME", 192).clamp(0, 255);
269 cfg.bgmfade_use_check = gameexe_bool_or(ctx, "CONFIG.BGMFADE_ONOFF", true);
270 let (r, g, b, a) = config_filter_color_default(ctx);
271 cfg.filter_color_argb = ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32;
272 cfg.font_name = config_default_font_name(ctx);
273 cfg.font_futoku = gameexe_bool_or(ctx, "CONFIG.FONT.FUTOKU", false);
274 cfg.font_shadow = gameexe_i64_or(ctx, "CONFIG.FONT.SHADOW", 2).clamp(0, 3);
275 cfg.message_speed = gameexe_i64_or(ctx, "CONFIG.MESSAGE_SPEED", 20);
276 cfg.message_speed_nowait = gameexe_bool_or(ctx, "CONFIG.MESSAGE_SPEED_NOWAIT.ONOFF", false);
277 cfg.mouse_cursor_hide_onoff = config_mouse_cursor_hide_onoff_default(ctx) != 0;
278 cfg.mouse_cursor_hide_time = config_mouse_cursor_hide_time_default(ctx);
279 cfg.chrkoe = (0..configured_chrkoe_count(ctx))
280 .map(|index| config_default_chrkoe(ctx, index))
281 .collect();
282 cfg.message_chrcolor_flag = gameexe_bool_or(ctx, "CONFIG.MESSAGE_CHRCOLOR.ONOFF", true);
283 cfg.object_disp_flag = (0..4)
284 .map(|index| config_default_indexed_bool(ctx, "CONFIG.OBJECT_DISP", index, true))
285 .collect();
286 cfg.global_extra_switch_flag = (0..4)
287 .map(|index| config_default_indexed_bool(ctx, "CONFIG.GLOBAL_EXTRA_SWITCH", index, true))
288 .collect();
289 cfg.global_extra_mode_flag = (0..4)
290 .map(|index| config_default_indexed_mode(ctx, index))
291 .collect();
292 cfg.sleep_flag = gameexe_bool_or(ctx, "CONFIG.SLEEP.ONOFF", false);
293 cfg.no_wipe_anime_flag = gameexe_bool_or(ctx, "CONFIG.NO_WIPE_ANIME.ONOFF", false);
294 cfg.skip_wipe_anime_flag = gameexe_bool_or(ctx, "CONFIG.SKIP_WIPE_ANIME.ONOFF", true);
295 cfg.no_mwnd_anime_flag = gameexe_bool_or(ctx, "CONFIG.NO_MWND_ANIME.ONOFF", false);
296 cfg.wheel_next_message_flag = gameexe_bool_or(ctx, "CONFIG.WHEEL_NEXT_MESSAGE.ONOFF", true);
297 cfg.koe_dont_stop_flag = gameexe_bool_or(ctx, "CONFIG.KOE_DONT_STOP.ONOFF", false);
298 cfg.skip_unread_message_flag = gameexe_bool_or(ctx, "CONFIG.SKIP_UNREAD_MESSAGE.ONOFF", false);
299 cfg
300}
301
302fn config_mouse_cursor_hide_onoff_default(ctx: &CommandContext) -> i64 {
303 parse_first_i64_local(&gameexe_value_owned(ctx, "CONFIG.MOUSE_CURSOR_HIDE_ONOFF"))
304 .unwrap_or(0)
305 .clamp(0, 1)
306}
307
308fn config_mouse_cursor_hide_time_default(ctx: &CommandContext) -> i64 {
309 parse_first_i64_local(&gameexe_value_owned(ctx, "CONFIG.MOUSE_CURSOR_HIDE_TIME"))
310 .unwrap_or(5000)
311 .max(0)
312}
313
314fn config_filter_color_default(ctx: &CommandContext) -> (i64, i64, i64, i64) {
315 let raw = gameexe_value_owned(ctx, "CONFIG.FILTER_COLOR");
316 let vals = parse_i64_list_local(&raw);
317 if vals.len() >= 4 {
318 (
319 vals[0].clamp(0, 255),
320 vals[1].clamp(0, 255),
321 vals[2].clamp(0, 255),
322 vals[3].clamp(0, 255),
323 )
324 } else {
325 (0, 0, 0, 128)
326 }
327}
328
329fn local_extra_index(params: &[Value]) -> usize {
330 p_i64(params, 0).clamp(0, 3) as usize
331}
332
333fn local_extra_value_param(params: &[Value]) -> bool {
334 let value_idx = if params.len() >= 2 { 1 } else { 0 };
335 p_bool(params, value_idx)
336}
337
338fn local_extra_i64_param(params: &[Value]) -> i64 {
339 let value_idx = if params.len() >= 2 { 1 } else { 0 };
340 p_i64(params, value_idx)
341}
342
343fn get_local_extra(
344 op: i32,
345 params: &[Value],
346 st: &crate::runtime::globals::SyscomRuntimeState,
347) -> Option<i64> {
348 let idx = local_extra_index(params);
349 let sw = st
350 .local_extra_switches
351 .get(idx)
352 .copied()
353 .unwrap_or(st.local_extra_switch);
354 let mode = st
355 .local_extra_modes
356 .get(idx)
357 .copied()
358 .unwrap_or(st.local_extra_mode);
359 Some(match op {
360 GET_LOCAL_EXTRA_SWITCH_ONOFF_FLAG => {
361 if sw.onoff {
362 1
363 } else {
364 0
365 }
366 }
367 GET_LOCAL_EXTRA_SWITCH_ENABLE_FLAG => {
368 if sw.enable {
369 1
370 } else {
371 0
372 }
373 }
374 GET_LOCAL_EXTRA_SWITCH_EXIST_FLAG => {
375 if sw.exist {
376 1
377 } else {
378 0
379 }
380 }
381 CHECK_LOCAL_EXTRA_SWITCH_ENABLE => sw.check_enabled(),
382 GET_LOCAL_EXTRA_MODE_VALUE => mode.value,
383 GET_LOCAL_EXTRA_MODE_ENABLE_FLAG => {
384 if mode.enable {
385 1
386 } else {
387 0
388 }
389 }
390 GET_LOCAL_EXTRA_MODE_EXIST_FLAG => {
391 if mode.exist {
392 1
393 } else {
394 0
395 }
396 }
397 CHECK_LOCAL_EXTRA_MODE_ENABLE => mode.check_enabled(),
398 _ => return None,
399 })
400}
401
402fn set_local_extra(
403 op: i32,
404 params: &[Value],
405 st: &mut crate::runtime::globals::SyscomRuntimeState,
406) -> bool {
407 let idx = local_extra_index(params);
408 let value = local_extra_value_param(params);
409 match op {
410 SET_LOCAL_EXTRA_SWITCH_ONOFF_FLAG => st.local_extra_switches[idx].onoff = value,
411 SET_LOCAL_EXTRA_SWITCH_ENABLE_FLAG => st.local_extra_switches[idx].enable = value,
412 SET_LOCAL_EXTRA_SWITCH_EXIST_FLAG => st.local_extra_switches[idx].exist = value,
413 SET_LOCAL_EXTRA_MODE_ENABLE_FLAG => st.local_extra_modes[idx].enable = value,
414 SET_LOCAL_EXTRA_MODE_EXIST_FLAG => st.local_extra_modes[idx].exist = value,
415 SET_LOCAL_EXTRA_MODE_VALUE => {
416 st.local_extra_modes[idx].value = local_extra_i64_param(params)
417 }
418 _ => return false,
419 }
420 st.local_extra_switch = st.local_extra_switches[0];
421 st.local_extra_mode = st.local_extra_modes[0];
422 true
423}
424
425fn get_toggle_get(op: i32, st: &crate::runtime::globals::SyscomRuntimeState) -> Option<i64> {
426 Some(match op {
427 GET_READ_SKIP_ONOFF_FLAG => {
428 if st.read_skip.onoff {
429 1
430 } else {
431 0
432 }
433 }
434 GET_READ_SKIP_ENABLE_FLAG => {
435 if st.read_skip.enable {
436 1
437 } else {
438 0
439 }
440 }
441 GET_READ_SKIP_EXIST_FLAG => {
442 if st.read_skip.exist {
443 1
444 } else {
445 0
446 }
447 }
448 CHECK_READ_SKIP_ENABLE => st.read_skip.check_enabled(),
449 GET_AUTO_SKIP_ONOFF_FLAG => {
450 if st.auto_skip.onoff {
451 1
452 } else {
453 0
454 }
455 }
456 GET_AUTO_SKIP_ENABLE_FLAG => {
457 if st.auto_skip.enable {
458 1
459 } else {
460 0
461 }
462 }
463 GET_AUTO_SKIP_EXIST_FLAG => {
464 if st.auto_skip.exist {
465 1
466 } else {
467 0
468 }
469 }
470 CHECK_AUTO_SKIP_ENABLE => st.auto_skip.check_enabled(),
471 GET_AUTO_MODE_ONOFF_FLAG => {
472 if st.auto_mode.onoff {
473 1
474 } else {
475 0
476 }
477 }
478 GET_AUTO_MODE_ENABLE_FLAG => {
479 if st.auto_mode.enable {
480 1
481 } else {
482 0
483 }
484 }
485 GET_AUTO_MODE_EXIST_FLAG => {
486 if st.auto_mode.exist {
487 1
488 } else {
489 0
490 }
491 }
492 CHECK_AUTO_MODE_ENABLE => st.auto_mode.check_enabled(),
493 GET_HIDE_MWND_ONOFF_FLAG => {
494 if st.hide_mwnd.onoff {
495 1
496 } else {
497 0
498 }
499 }
500 GET_HIDE_MWND_ENABLE_FLAG => {
501 if st.hide_mwnd.enable {
502 1
503 } else {
504 0
505 }
506 }
507 GET_HIDE_MWND_EXIST_FLAG => {
508 if st.hide_mwnd.exist {
509 1
510 } else {
511 0
512 }
513 }
514 CHECK_HIDE_MWND_ENABLE => st.hide_mwnd.check_enabled(),
515 GET_MSG_BACK_ENABLE_FLAG => {
516 if st.msg_back.enable {
517 1
518 } else {
519 0
520 }
521 }
522 GET_MSG_BACK_EXIST_FLAG => {
523 if st.msg_back.exist {
524 1
525 } else {
526 0
527 }
528 }
529 CHECK_MSG_BACK_ENABLE => st.msg_back.check_enabled(),
530 CHECK_MSG_BACK_OPEN => {
531 if st.msg_back_open {
532 1
533 } else {
534 0
535 }
536 }
537 GET_RETURN_TO_SEL_ENABLE_FLAG => {
538 if st.return_to_sel.enable {
539 1
540 } else {
541 0
542 }
543 }
544 GET_RETURN_TO_SEL_EXIST_FLAG => {
545 if st.return_to_sel.exist {
546 1
547 } else {
548 0
549 }
550 }
551 CHECK_RETURN_TO_SEL_ENABLE => st.return_to_sel.check_enabled(),
552 GET_RETURN_TO_MENU_ENABLE_FLAG => {
553 if st.return_to_menu.enable {
554 1
555 } else {
556 0
557 }
558 }
559 GET_RETURN_TO_MENU_EXIST_FLAG => {
560 if st.return_to_menu.exist {
561 1
562 } else {
563 0
564 }
565 }
566 CHECK_RETURN_TO_MENU_ENABLE => st.return_to_menu.check_enabled(),
567 GET_END_GAME_ENABLE_FLAG => {
568 if st.end_game.enable {
569 1
570 } else {
571 0
572 }
573 }
574 GET_END_GAME_EXIST_FLAG => {
575 if st.end_game.exist {
576 1
577 } else {
578 0
579 }
580 }
581 CHECK_END_GAME_ENABLE => st.end_game.check_enabled(),
582 GET_SAVE_ENABLE_FLAG => {
583 if st.save_feature.enable {
584 1
585 } else {
586 0
587 }
588 }
589 GET_SAVE_EXIST_FLAG => {
590 if st.save_feature.exist {
591 1
592 } else {
593 0
594 }
595 }
596 CHECK_SAVE_ENABLE => st.save_feature.check_enabled(),
597 GET_LOAD_ENABLE_FLAG => {
598 if st.load_feature.enable {
599 1
600 } else {
601 0
602 }
603 }
604 GET_LOAD_EXIST_FLAG => {
605 if st.load_feature.exist {
606 1
607 } else {
608 0
609 }
610 }
611 CHECK_LOAD_ENABLE => st.load_feature.check_enabled(),
612 _ => return None,
613 })
614}
615
616fn apply_toggle_set(
617 op: i32,
618 v: bool,
619 st: &mut crate::runtime::globals::SyscomRuntimeState,
620) -> bool {
621 match op {
622 SET_READ_SKIP_ONOFF_FLAG => st.read_skip.onoff = v,
623 SET_READ_SKIP_ENABLE_FLAG => st.read_skip.enable = v,
624 SET_READ_SKIP_EXIST_FLAG => st.read_skip.exist = v,
625 SET_AUTO_SKIP_ONOFF_FLAG => st.auto_skip.onoff = v,
626 SET_AUTO_SKIP_ENABLE_FLAG => st.auto_skip.enable = v,
627 SET_AUTO_SKIP_EXIST_FLAG => st.auto_skip.exist = v,
628 SET_AUTO_MODE_ONOFF_FLAG => st.auto_mode.onoff = v,
629 SET_AUTO_MODE_ENABLE_FLAG => st.auto_mode.enable = v,
630 SET_AUTO_MODE_EXIST_FLAG => st.auto_mode.exist = v,
631 SET_HIDE_MWND_ONOFF_FLAG => st.hide_mwnd.onoff = v,
632 SET_HIDE_MWND_ENABLE_FLAG => st.hide_mwnd.enable = v,
633 SET_HIDE_MWND_EXIST_FLAG => st.hide_mwnd.exist = v,
634 SET_MSG_BACK_ENABLE_FLAG => st.msg_back.enable = v,
635 SET_MSG_BACK_EXIST_FLAG => st.msg_back.exist = v,
636 SET_RETURN_TO_SEL_ENABLE_FLAG => st.return_to_sel.enable = v,
637 SET_RETURN_TO_SEL_EXIST_FLAG => st.return_to_sel.exist = v,
638 SET_RETURN_TO_MENU_ENABLE_FLAG => st.return_to_menu.enable = v,
639 SET_RETURN_TO_MENU_EXIST_FLAG => st.return_to_menu.exist = v,
640 SET_END_GAME_ENABLE_FLAG => st.end_game.enable = v,
641 SET_END_GAME_EXIST_FLAG => st.end_game.exist = v,
642 SET_SAVE_ENABLE_FLAG => st.save_feature.enable = v,
643 SET_SAVE_EXIST_FLAG => st.save_feature.exist = v,
644 SET_LOAD_ENABLE_FLAG => st.load_feature.enable = v,
645 SET_LOAD_EXIST_FLAG => st.load_feature.exist = v,
646 _ => return false,
647 }
648 true
649}
650
651fn truncate_to_chars(s: &mut String, max_chars: usize) {
652 if s.chars().count() > max_chars {
653 *s = s.chars().take(max_chars).collect();
654 }
655}
656
657pub(crate) fn append_current_save_message(ctx: &mut CommandContext, msg: &str) {
663 if msg.is_empty() {
664 return;
665 }
666 ctx.globals.syscom.current_save_message.push_str(msg);
667 truncate_to_chars(&mut ctx.globals.syscom.current_save_message, 256);
668 ctx.globals.syscom.current_save_full_message.push_str(msg);
669 truncate_to_chars(&mut ctx.globals.syscom.current_save_full_message, 256);
670
671 if let Some(snapshot) = ctx.local_save_snapshot.as_mut() {
672 snapshot.save_msg.push_str(msg);
673 truncate_to_chars(&mut snapshot.save_msg, 256);
674 snapshot.save_full_msg.push_str(msg);
675 truncate_to_chars(&mut snapshot.save_full_msg, 256);
676 }
677}
678
679fn save_load_trace_enabled() -> bool {
680 env_is_set!("SG_SAVELOAD_TRACE")
681}
682
683fn trace_save_load_event(
684 ctx: &CommandContext,
685 label: &str,
686 quick: bool,
687 idx: usize,
688 path: Option<&Path>,
689) {
690 if !save_load_trace_enabled() {
691 return;
692 }
693 let kind = if quick { "quick" } else { "normal" };
694 if let Some(path) = path {
695 eprintln!(
696 "[SG_SAVELOAD_TRACE][SYSCOM] {label} kind={kind} idx={idx} path={} exists={}",
697 path.display(),
698 crate::resource::game_path_exists(path)
699 );
700 } else {
701 eprintln!("[SG_SAVELOAD_TRACE][SYSCOM] {label} kind={kind} idx={idx}");
702 }
703}
704
705fn ensure_slot(slots: &mut Vec<SaveSlotState>, idx: usize) -> &mut SaveSlotState {
706 if slots.len() <= idx {
707 slots.resize_with(idx + 1, SaveSlotState::default);
708 }
709 &mut slots[idx]
710}
711
712pub fn menu_save_slot(ctx: &mut CommandContext, quick: bool, idx: usize) {
713 let path = slot_path_with_counts(
714 &ctx.project_dir,
715 quick,
716 idx,
717 configured_save_count(ctx, false),
718 configured_save_count(ctx, true),
719 );
720 trace_save_load_event(ctx, "menu_save_slot", quick, idx, Some(&path));
721 let kind = if quick {
727 RuntimeSaveKind::Quick
728 } else {
729 RuntimeSaveKind::Normal
730 };
731 ctx.request_runtime_save(kind, idx);
732}
733
734pub fn menu_load_slot(ctx: &mut CommandContext, quick: bool, idx: usize) {
735 let path = slot_path_with_counts(
736 &ctx.project_dir,
737 quick,
738 idx,
739 configured_save_count(ctx, false),
740 configured_save_count(ctx, true),
741 );
742 trace_save_load_event(ctx, "menu_load_slot", quick, idx, Some(&path));
743 let save_cnt = configured_save_count(ctx, false);
744 let quick_cnt = configured_save_count(ctx, true);
745 if quick {
746 ensure_slot_loaded_with_counts(
747 &ctx.project_dir,
748 true,
749 save_cnt,
750 quick_cnt,
751 &mut ctx.globals.syscom.quick_save_slots,
752 idx,
753 );
754 ctx.request_runtime_load(RuntimeSaveKind::Quick, idx);
755 } else {
756 ensure_slot_loaded_with_counts(
757 &ctx.project_dir,
758 false,
759 save_cnt,
760 quick_cnt,
761 &mut ctx.globals.syscom.save_slots,
762 idx,
763 );
764 ctx.request_runtime_load(RuntimeSaveKind::Normal, idx);
765 }
766}
767
768fn saveload_alert_on(ctx: &CommandContext) -> bool {
769 cfg_get_int(&ctx.globals.syscom, GET_SAVELOAD_ALERT_ONOFF, 1) != 0
770}
771
772fn slot_exists_for_menu_action(ctx: &mut CommandContext, quick: bool, idx: usize) -> bool {
773 let save_cnt = configured_save_count(ctx, false);
774 let quick_cnt = configured_save_count(ctx, true);
775 if quick {
776 ensure_slot_loaded_with_counts(
777 &ctx.project_dir,
778 true,
779 save_cnt,
780 quick_cnt,
781 &mut ctx.globals.syscom.quick_save_slots,
782 idx,
783 );
784 ctx.globals
785 .syscom
786 .quick_save_slots
787 .get(idx)
788 .map(|slot| slot.exist)
789 .unwrap_or(false)
790 } else {
791 ensure_slot_loaded_with_counts(
792 &ctx.project_dir,
793 false,
794 save_cnt,
795 quick_cnt,
796 &mut ctx.globals.syscom.save_slots,
797 idx,
798 );
799 ctx.globals
800 .syscom
801 .save_slots
802 .get(idx)
803 .map(|slot| slot.exist)
804 .unwrap_or(false)
805 }
806}
807
808fn request_confirmed_save_or_load(
809 ctx: &mut CommandContext,
810 kind: SyscomPendingProcKind,
811 idx: usize,
812 warning: bool,
813 needs_existing_slot: bool,
814) -> bool {
815 if warning
816 && saveload_alert_on(ctx)
817 && (!needs_existing_slot
818 || slot_exists_for_menu_action(
819 ctx,
820 matches!(
821 kind,
822 SyscomPendingProcKind::QuickSave | SyscomPendingProcKind::QuickLoad
823 ),
824 idx,
825 ))
826 {
827 ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
828 kind,
829 warning: true,
830 se_play: false,
831 fade_out: false,
832 leave_msgbk: false,
833 save_id: idx as i64,
834 });
835 ctx.globals.syscom.menu_open = false;
836 ctx.globals.syscom.menu_kind = None;
837 ctx.globals.syscom.menu_result = None;
838 true
839 } else {
840 false
841 }
842}
843
844fn local_save_available(ctx: &CommandContext) -> bool {
845 ctx.local_save_snapshot
846 .as_ref()
847 .map(|s| !s.local_stream.is_empty())
848 .unwrap_or(false)
849 || ctx.pending_auto_savepoint
850}
851
852fn local_save_file_exists(ctx: &CommandContext, kind: SaveKind, idx: usize) -> bool {
853 let path = original_save::save_file_path_with_counts(
854 &ctx.project_dir,
855 configured_save_count(ctx, false),
856 configured_save_count(ctx, true),
857 kind,
858 idx,
859 );
860 original_save::read_slot_from_path(&path)
861 .map(|slot| slot.exist)
862 .unwrap_or(false)
863}
864
865pub(crate) fn save_dir(project_dir: &Path) -> PathBuf {
866 original_save::save_dir(project_dir)
867}
868
869fn slot_path_with_counts(
870 project_dir: &Path,
871 quick: bool,
872 idx: usize,
873 save_cnt: usize,
874 quick_cnt: usize,
875) -> PathBuf {
876 let kind = if quick {
877 SaveKind::Quick
878 } else {
879 SaveKind::Normal
880 };
881 original_save::save_file_path_with_counts(project_dir, save_cnt, quick_cnt, kind, idx)
882}
883
884#[derive(Clone, Copy, Debug, PartialEq, Eq)]
885enum SaveThumbType {
886 Bmp,
887 Png,
888}
889
890#[derive(Clone, Copy, Debug)]
891struct SaveThumbConfig {
892 enabled: bool,
893 thumb_type: SaveThumbType,
894 width: u32,
895 height: u32,
896}
897
898fn save_thumb_config(ctx: &CommandContext) -> SaveThumbConfig {
899 let mut out = SaveThumbConfig {
900 enabled: false,
901 thumb_type: SaveThumbType::Bmp,
902 width: 200,
903 height: 150,
904 };
905
906 let Some(cfg) = ctx.tables.gameexe.as_ref() else {
907 return out;
908 };
909
910 out.enabled = cfg
911 .get_usize("#SAVE_THUMB.USE")
912 .or_else(|| cfg.get_usize("SAVE_THUMB.USE"))
913 .unwrap_or(0)
914 != 0;
915 out.thumb_type = match cfg
916 .get_usize("#SAVE_THUMB.TYPE")
917 .or_else(|| cfg.get_usize("SAVE_THUMB.TYPE"))
918 .unwrap_or(0)
919 {
920 1 => SaveThumbType::Png,
921 _ => SaveThumbType::Bmp,
922 };
923
924 if let Some(entry) = cfg
925 .get_entry("#SAVE_THUMB.SIZE")
926 .or_else(|| cfg.get_entry("SAVE_THUMB.SIZE"))
927 {
928 let w = entry
929 .item_unquoted(0)
930 .and_then(|v| v.trim().parse::<u32>().ok())
931 .unwrap_or(out.width);
932 let h = entry
933 .item_unquoted(1)
934 .and_then(|v| v.trim().parse::<u32>().ok())
935 .unwrap_or(out.height);
936 if w != 0 && h != 0 {
937 out.width = w;
938 out.height = h;
939 }
940 }
941
942 out
943}
944
945pub(crate) fn thumb_candidate_paths(dir: &Path, idx: i64) -> [PathBuf; 2] {
946 let project_dir = dir.parent().unwrap_or(dir);
947 original_save::thumb_candidate_paths_for_no(project_dir, idx.max(0) as usize)
948}
949
950fn thumb_path_for_no_with_config(
951 project_dir: &Path,
952 config: SaveThumbConfig,
953 save_no: usize,
954) -> PathBuf {
955 let stem = format!("{save_no:04}");
956 let ext = match config.thumb_type {
957 SaveThumbType::Bmp => "bmp",
958 SaveThumbType::Png => "png",
959 };
960 save_dir(project_dir).join(format!("{stem}.{ext}"))
961}
962
963fn pick_thumb_source_name(ctx: &CommandContext) -> Option<String> {
964 let table = ctx.tables.thumb_table.as_ref()?;
965 let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
966 form_ids.sort_unstable();
967 for form_id in form_ids {
968 let Some(stage) = ctx.globals.stage_forms.get(&form_id) else {
969 continue;
970 };
971 let mut stage_ids: Vec<i64> = stage.object_lists.keys().copied().collect();
972 stage_ids.sort_unstable();
973 for stage_idx in stage_ids {
974 let Some(objs) = stage.object_lists.get(&stage_idx) else {
975 continue;
976 };
977 for obj in objs.iter().rev() {
978 if let Some(file) = obj.file_name.as_deref()
979 && let Some(mapped) = table.get_by_file_stem(file)
980 {
981 return Some(mapped.clone());
982 }
983 }
984 }
985 }
986 None
987}
988
989fn capture_slot_thumb(
990 ctx: &mut CommandContext,
991 config: SaveThumbConfig,
992) -> anyhow::Result<RgbaImage> {
993 if let Some(name) = pick_thumb_source_name(ctx)
994 && let Ok(img_id) = ctx.images.load_g00(&name, 0)
995 && let Some(img) = ctx.images.get(&img_id)
996 {
997 return Ok(resize_rgba(img.as_ref(), config.width, config.height));
998 }
999
1000 let img = ctx.capture_frame_rgba()?;
1001 Ok(resize_rgba(&img, config.width, config.height))
1002}
1003
1004pub(crate) const CAPTURE_PRIOR_NONE: i32 = 0;
1005pub const CAPTURE_PRIOR_SAVE: i32 = 1;
1006pub(crate) const CAPTURE_PRIOR_END: i32 = 2;
1007pub(crate) const CAPTURE_PRIOR_CAPTURE: i32 = 3;
1008
1009pub(crate) fn prepare_runtime_save_thumb_capture_with_priority(
1010 ctx: &mut CommandContext,
1011 priority: i32,
1012) {
1013 let config = save_thumb_config(ctx);
1014 if !config.enabled {
1015 return;
1016 }
1017 if ctx.globals.save_thumb_capture_image.is_some()
1018 && priority < ctx.globals.save_thumb_capture_prior
1019 {
1020 return;
1021 }
1022
1023 match capture_slot_thumb(ctx, config) {
1024 Ok(image) => {
1025 ctx.globals.save_thumb_capture_image = Some(image);
1026 ctx.globals.save_thumb_capture_prior = priority;
1027 }
1028 Err(err) => {
1029 log::error!("save thumbnail GPU capture failed: {err:#}");
1030 }
1031 }
1032}
1033
1034pub(crate) fn prepare_runtime_save_thumb_capture(ctx: &mut CommandContext) {
1035 prepare_runtime_save_thumb_capture_with_priority(ctx, CAPTURE_PRIOR_SAVE);
1036}
1037
1038pub fn free_runtime_save_thumb_capture(ctx: &mut CommandContext, priority: i32) {
1039 if priority < ctx.globals.save_thumb_capture_prior {
1040 return;
1041 }
1042 ctx.globals.save_thumb_capture_image = None;
1043 ctx.globals.save_thumb_capture_prior = CAPTURE_PRIOR_NONE;
1044}
1045
1046pub(crate) fn prepare_runtime_save_thumb_capture_from_image(
1047 ctx: &mut CommandContext,
1048 img: &RgbaImage,
1049) {
1050 let config = save_thumb_config(ctx);
1051 if !config.enabled {
1052 return;
1053 }
1054
1055 ctx.globals.save_thumb_capture_image = Some(img.clone());
1058}
1059
1060pub(crate) fn capture_for_local_save(
1061 ctx: &mut CommandContext,
1062 img: &RgbaImage,
1063 width: u32,
1064 height: u32,
1065) -> i64 {
1066 use std::time::{SystemTime, UNIX_EPOCH};
1067
1068 let capture_time = match SystemTime::now().duration_since(UNIX_EPOCH) {
1069 Ok(v) => v.as_secs().min(i64::MAX as u64) as i64,
1070 Err(err) => {
1071 log::error!("GLOBAL.CAPTURE_FOR_LOCAL_SAVE cannot obtain UNIX time: {err}");
1072 return 0;
1073 }
1074 };
1075 let resized = resize_rgba(img, width.max(1), height.max(1));
1076 let path = original_save::save_dir(&ctx.project_dir).join(format!("{capture_time}.png"));
1077 if let Err(err) = write_rgba_png_opaque(&path, &resized) {
1078 log::error!(
1079 "GLOBAL.CAPTURE_FOR_LOCAL_SAVE failed to write {}: {err:#}",
1080 path.display()
1081 );
1082 return 0;
1083 }
1084 capture_time
1085}
1086
1087fn write_slot_thumb_for_save_no(ctx: &mut CommandContext, save_no: usize) {
1088 let config = save_thumb_config(ctx);
1089 if !config.enabled {
1090 return;
1091 }
1092 let Some(img) = ctx.globals.save_thumb_capture_image.clone() else {
1093 ctx.unknown
1094 .record_note(&format!("save_thumb.capture.missing:{save_no}"));
1095 return;
1096 };
1097 let path = thumb_path_for_no_with_config(&ctx.project_dir, config, save_no);
1098 if save_load_trace_enabled() {
1099 eprintln!(
1100 "[SG_SAVELOAD_TRACE][SYSCOM] write_slot_thumb save_no={} path={} size={}x{} type={:?}",
1101 save_no,
1102 path.display(),
1103 img.width,
1104 img.height,
1105 config.thumb_type
1106 );
1107 }
1108 remove_game_file(&path);
1109 let result = match config.thumb_type {
1110 SaveThumbType::Bmp => write_rgba_bmp_top_down(&path, &img),
1111 SaveThumbType::Png => write_rgba_png_opaque(&path, &img),
1112 };
1113 if let Err(err) = result {
1114 eprintln!(
1115 "[SG_SAVE] failed to write save thumb {}: {err:#}",
1116 path.display()
1117 );
1118 }
1119}
1120
1121pub(crate) fn write_runtime_slot_thumb(ctx: &mut CommandContext, save_no: usize) {
1122 write_slot_thumb_for_save_no(ctx, save_no);
1123}
1124
1125fn escape_str(s: &str) -> String {
1126 let mut out = String::with_capacity(s.len());
1127 for ch in s.chars() {
1128 match ch {
1129 '\\' => out.push_str("\\\\"),
1130 '\n' => out.push_str("\\n"),
1131 '\r' => out.push_str("\\r"),
1132 '\t' => out.push_str("\\t"),
1133 _ => out.push(ch),
1134 }
1135 }
1136 out
1137}
1138
1139fn unescape_str(s: &str) -> String {
1140 let mut out = String::with_capacity(s.len());
1141 let mut it = s.chars();
1142 while let Some(ch) = it.next() {
1143 if ch == '\\' {
1144 match it.next() {
1145 Some('n') => out.push('\n'),
1146 Some('r') => out.push('\r'),
1147 Some('t') => out.push('\t'),
1148 Some('\\') => out.push('\\'),
1149 Some(other) => out.push(other),
1150 None => break,
1151 }
1152 } else {
1153 out.push(ch);
1154 }
1155 }
1156 out
1157}
1158
1159fn read_slot(path: &Path) -> Option<SaveSlotState> {
1160 original_save::read_slot_from_path(path)
1161}
1162
1163fn configured_chrkoe_count(ctx: &CommandContext) -> usize {
1164 ctx.tables
1165 .gameexe
1166 .as_ref()
1167 .and_then(|cfg| {
1168 cfg.get_usize("#CHRKOE.CNT")
1169 .or_else(|| cfg.get_usize("CHRKOE.CNT"))
1170 })
1171 .unwrap_or(64)
1172 .min(256)
1173}
1174
1175fn resize_original_config_arrays(
1176 _ctx: &CommandContext,
1177 cfg: &mut crate::runtime::globals::OriginalConfigRuntimeState,
1178) {
1179 cfg.object_disp_flag.resize(4, true);
1180 cfg.object_disp_flag.truncate(4);
1181 cfg.global_extra_switch_flag.resize(4, true);
1182 cfg.global_extra_switch_flag.truncate(4);
1183 cfg.global_extra_mode_flag.resize(4, 0);
1184 cfg.global_extra_mode_flag.truncate(4);
1185}
1186
1187pub fn config_state_for_save(
1188 ctx: &CommandContext,
1189) -> crate::runtime::globals::OriginalConfigRuntimeState {
1190 let mut cfg = ctx.globals.syscom.original_config.clone();
1191 resize_original_config_arrays(ctx, &mut cfg);
1192
1193 cfg.screen_size_mode =
1194 cfg_get_int(&ctx.globals.syscom, GET_WINDOW_MODE, cfg.screen_size_mode).clamp(0, 1);
1195 let window_scale = cfg_get_int(
1196 &ctx.globals.syscom,
1197 GET_WINDOW_MODE_SIZE,
1198 cfg.screen_size_scale.0,
1199 )
1200 .max(1);
1201 cfg.screen_size_scale = (window_scale, window_scale);
1202
1203 cfg.all_sound_user_volume = cfg_get_int(
1204 &ctx.globals.syscom,
1205 GET_ALL_VOLUME,
1206 cfg.all_sound_user_volume,
1207 )
1208 .clamp(0, 255);
1209 for (index, key) in [
1210 GET_BGM_VOLUME,
1211 GET_KOE_VOLUME,
1212 GET_PCM_VOLUME,
1213 GET_SE_VOLUME,
1214 GET_MOV_VOLUME,
1215 ]
1216 .into_iter()
1217 .enumerate()
1218 {
1219 cfg.sound_user_volume[index] =
1220 cfg_get_int(&ctx.globals.syscom, key, cfg.sound_user_volume[index]).clamp(0, 255);
1221 }
1222 cfg.play_all_sound_check = cfg_get_int(
1223 &ctx.globals.syscom,
1224 GET_ALL_ONOFF,
1225 if cfg.play_all_sound_check { 1 } else { 0 },
1226 ) != 0;
1227 for (index, key) in [
1228 GET_BGM_ONOFF,
1229 GET_KOE_ONOFF,
1230 GET_PCM_ONOFF,
1231 GET_SE_ONOFF,
1232 GET_MOV_ONOFF,
1233 ]
1234 .into_iter()
1235 .enumerate()
1236 {
1237 cfg.play_sound_check[index] = cfg_get_int(
1238 &ctx.globals.syscom,
1239 key,
1240 if cfg.play_sound_check[index] { 1 } else { 0 },
1241 ) != 0;
1242 }
1243
1244 cfg.bgmfade_volume =
1245 cfg_get_int(&ctx.globals.syscom, GET_BGMFADE_VOLUME, cfg.bgmfade_volume).clamp(0, 255);
1246 cfg.bgmfade_use_check = cfg_get_int(
1247 &ctx.globals.syscom,
1248 GET_BGMFADE_ONOFF,
1249 if cfg.bgmfade_use_check { 1 } else { 0 },
1250 ) != 0;
1251
1252 let r = cfg_get_int(
1253 &ctx.globals.syscom,
1254 GET_FILTER_COLOR_R,
1255 ((cfg.filter_color_argb >> 16) & 0xff) as i64,
1256 )
1257 .clamp(0, 255) as u32;
1258 let g = cfg_get_int(
1259 &ctx.globals.syscom,
1260 GET_FILTER_COLOR_G,
1261 ((cfg.filter_color_argb >> 8) & 0xff) as i64,
1262 )
1263 .clamp(0, 255) as u32;
1264 let b = cfg_get_int(
1265 &ctx.globals.syscom,
1266 GET_FILTER_COLOR_B,
1267 (cfg.filter_color_argb & 0xff) as i64,
1268 )
1269 .clamp(0, 255) as u32;
1270 let a = cfg_get_int(
1271 &ctx.globals.syscom,
1272 GET_FILTER_COLOR_A,
1273 ((cfg.filter_color_argb >> 24) & 0xff) as i64,
1274 )
1275 .clamp(0, 255) as u32;
1276 cfg.filter_color_argb = (a << 24) | (r << 16) | (g << 8) | b;
1277
1278 cfg.font_name = cfg_get_str(&ctx.globals.syscom, GET_FONT_NAME);
1279 cfg.font_futoku = cfg_get_int(
1280 &ctx.globals.syscom,
1281 GET_FONT_BOLD,
1282 if cfg.font_futoku { 1 } else { 0 },
1283 ) != 0;
1284 cfg.font_shadow = cfg_get_int(&ctx.globals.syscom, GET_FONT_DECORATION, cfg.font_shadow);
1285
1286 cfg.message_speed = cfg_get_int(&ctx.globals.syscom, GET_MESSAGE_SPEED, cfg.message_speed);
1287 cfg.message_speed_nowait = ctx.globals.script.msg_nowait;
1288 cfg.auto_mode_onoff = ctx.globals.syscom.auto_mode.onoff;
1289 cfg.auto_mode_moji_wait = ctx.globals.script.auto_mode_moji_wait;
1290 cfg.auto_mode_min_wait = ctx.globals.script.auto_mode_min_wait;
1291 cfg.mouse_cursor_hide_onoff = cfg_get_int(
1292 &ctx.globals.syscom,
1293 GET_MOUSE_CURSOR_HIDE_ONOFF,
1294 if cfg.mouse_cursor_hide_onoff { 1 } else { 0 },
1295 ) != 0;
1296 cfg.mouse_cursor_hide_time = cfg_get_int(
1297 &ctx.globals.syscom,
1298 GET_MOUSE_CURSOR_HIDE_TIME,
1299 cfg.mouse_cursor_hide_time,
1300 )
1301 .max(0);
1302 cfg.jitan_normal_onoff = cfg_get_int(
1303 &ctx.globals.syscom,
1304 GET_JITAN_NORMAL_ONOFF,
1305 if cfg.jitan_normal_onoff { 1 } else { 0 },
1306 ) != 0;
1307 cfg.jitan_auto_mode_onoff = cfg_get_int(
1308 &ctx.globals.syscom,
1309 GET_JITAN_AUTO_MODE_ONOFF,
1310 if cfg.jitan_auto_mode_onoff { 1 } else { 0 },
1311 ) != 0;
1312 cfg.jitan_msgbk_onoff = cfg_get_int(
1313 &ctx.globals.syscom,
1314 GET_JITAN_KOE_REPLAY_ONOFF,
1315 if cfg.jitan_msgbk_onoff { 1 } else { 0 },
1316 ) != 0;
1317 cfg.jitan_speed = cfg_get_int(&ctx.globals.syscom, GET_JITAN_SPEED, cfg.jitan_speed);
1318 cfg.koe_mode = cfg_get_int(&ctx.globals.syscom, GET_KOEMODE, cfg.koe_mode);
1319
1320 cfg.sleep_flag = cfg_get_int(
1321 &ctx.globals.syscom,
1322 GET_SLEEP_ONOFF,
1323 if cfg.sleep_flag { 1 } else { 0 },
1324 ) != 0;
1325 cfg.no_wipe_anime_flag = cfg_get_int(
1326 &ctx.globals.syscom,
1327 GET_NO_WIPE_ANIME_ONOFF,
1328 if cfg.no_wipe_anime_flag { 1 } else { 0 },
1329 ) != 0;
1330 cfg.skip_wipe_anime_flag = cfg_get_int(
1331 &ctx.globals.syscom,
1332 GET_SKIP_WIPE_ANIME_ONOFF,
1333 if cfg.skip_wipe_anime_flag { 1 } else { 0 },
1334 ) != 0;
1335 cfg.no_mwnd_anime_flag = cfg_get_int(
1336 &ctx.globals.syscom,
1337 GET_NO_MWND_ANIME_ONOFF,
1338 if cfg.no_mwnd_anime_flag { 1 } else { 0 },
1339 ) != 0;
1340 cfg.wheel_next_message_flag = cfg_get_int(
1341 &ctx.globals.syscom,
1342 GET_WHEEL_NEXT_MESSAGE_ONOFF,
1343 if cfg.wheel_next_message_flag { 1 } else { 0 },
1344 ) != 0;
1345 cfg.koe_dont_stop_flag = cfg_get_int(
1346 &ctx.globals.syscom,
1347 GET_KOE_DONT_STOP_ONOFF,
1348 if cfg.koe_dont_stop_flag { 1 } else { 0 },
1349 ) != 0;
1350 cfg.skip_unread_message_flag = cfg_get_int(
1351 &ctx.globals.syscom,
1352 GET_SKIP_UNREAD_MESSAGE_ONOFF,
1353 if cfg.skip_unread_message_flag { 1 } else { 0 },
1354 ) != 0;
1355 cfg.saveload_alert_flag = cfg_get_int(
1356 &ctx.globals.syscom,
1357 GET_SAVELOAD_ALERT_ONOFF,
1358 if cfg.saveload_alert_flag { 1 } else { 0 },
1359 ) != 0;
1360
1361 cfg
1362}
1363
1364fn apply_original_config_to_runtime(ctx: &mut CommandContext) {
1365 let cfg = ctx.globals.syscom.original_config.clone();
1366 cfg_set_int(
1367 &mut ctx.globals.syscom,
1368 GET_WINDOW_MODE,
1369 cfg.screen_size_mode.clamp(0, 1),
1370 );
1371 cfg_set_int(
1372 &mut ctx.globals.syscom,
1373 GET_WINDOW_MODE_SIZE,
1374 cfg.screen_size_scale.0.max(1),
1375 );
1376 cfg_set_int(
1377 &mut ctx.globals.syscom,
1378 GET_ALL_VOLUME,
1379 cfg.all_sound_user_volume.clamp(0, 255),
1380 );
1381 for (index, key) in [
1382 GET_BGM_VOLUME,
1383 GET_KOE_VOLUME,
1384 GET_PCM_VOLUME,
1385 GET_SE_VOLUME,
1386 GET_MOV_VOLUME,
1387 ]
1388 .into_iter()
1389 .enumerate()
1390 {
1391 cfg_set_int(
1392 &mut ctx.globals.syscom,
1393 key,
1394 cfg.sound_user_volume[index].clamp(0, 255),
1395 );
1396 }
1397 cfg_set_int(
1398 &mut ctx.globals.syscom,
1399 GET_ALL_ONOFF,
1400 if cfg.play_all_sound_check { 1 } else { 0 },
1401 );
1402 for (index, key) in [
1403 GET_BGM_ONOFF,
1404 GET_KOE_ONOFF,
1405 GET_PCM_ONOFF,
1406 GET_SE_ONOFF,
1407 GET_MOV_ONOFF,
1408 ]
1409 .into_iter()
1410 .enumerate()
1411 {
1412 cfg_set_int(
1413 &mut ctx.globals.syscom,
1414 key,
1415 if cfg.play_sound_check[index] { 1 } else { 0 },
1416 );
1417 }
1418 cfg_set_int(
1419 &mut ctx.globals.syscom,
1420 GET_BGMFADE_VOLUME,
1421 cfg.bgmfade_volume.clamp(0, 255),
1422 );
1423 cfg_set_int(
1424 &mut ctx.globals.syscom,
1425 GET_BGMFADE_ONOFF,
1426 if cfg.bgmfade_use_check { 1 } else { 0 },
1427 );
1428 cfg_set_int(
1429 &mut ctx.globals.syscom,
1430 GET_FILTER_COLOR_A,
1431 ((cfg.filter_color_argb >> 24) & 0xff) as i64,
1432 );
1433 cfg_set_int(
1434 &mut ctx.globals.syscom,
1435 GET_FILTER_COLOR_R,
1436 ((cfg.filter_color_argb >> 16) & 0xff) as i64,
1437 );
1438 cfg_set_int(
1439 &mut ctx.globals.syscom,
1440 GET_FILTER_COLOR_G,
1441 ((cfg.filter_color_argb >> 8) & 0xff) as i64,
1442 );
1443 cfg_set_int(
1444 &mut ctx.globals.syscom,
1445 GET_FILTER_COLOR_B,
1446 (cfg.filter_color_argb & 0xff) as i64,
1447 );
1448 cfg_set_str(&mut ctx.globals.syscom, GET_FONT_NAME, cfg.font_name);
1449 cfg_set_int(
1450 &mut ctx.globals.syscom,
1451 GET_FONT_BOLD,
1452 if cfg.font_futoku { 1 } else { 0 },
1453 );
1454 cfg_set_int(
1455 &mut ctx.globals.syscom,
1456 GET_FONT_DECORATION,
1457 cfg.font_shadow,
1458 );
1459 cfg_set_int(
1460 &mut ctx.globals.syscom,
1461 GET_MESSAGE_SPEED,
1462 cfg.message_speed,
1463 );
1464 cfg_set_int(
1465 &mut ctx.globals.syscom,
1466 GET_MESSAGE_NOWAIT,
1467 if cfg.message_speed_nowait { 1 } else { 0 },
1468 );
1469 ctx.globals.script.msg_nowait = cfg.message_speed_nowait;
1470 ctx.globals.syscom.auto_mode.onoff = cfg.auto_mode_onoff;
1471 ctx.globals.script.auto_mode_moji_wait = cfg.auto_mode_moji_wait;
1472 ctx.globals.script.auto_mode_min_wait = cfg.auto_mode_min_wait;
1473 cfg_set_int(
1474 &mut ctx.globals.syscom,
1475 GET_AUTO_MODE_MOJI_WAIT,
1476 cfg.auto_mode_moji_wait,
1477 );
1478 cfg_set_int(
1479 &mut ctx.globals.syscom,
1480 GET_AUTO_MODE_MIN_WAIT,
1481 cfg.auto_mode_min_wait,
1482 );
1483 cfg_set_int(
1484 &mut ctx.globals.syscom,
1485 GET_MOUSE_CURSOR_HIDE_ONOFF,
1486 if cfg.mouse_cursor_hide_onoff { 1 } else { 0 },
1487 );
1488 cfg_set_int(
1489 &mut ctx.globals.syscom,
1490 GET_MOUSE_CURSOR_HIDE_TIME,
1491 cfg.mouse_cursor_hide_time,
1492 );
1493 cfg_set_int(
1494 &mut ctx.globals.syscom,
1495 GET_JITAN_NORMAL_ONOFF,
1496 if cfg.jitan_normal_onoff { 1 } else { 0 },
1497 );
1498 cfg_set_int(
1499 &mut ctx.globals.syscom,
1500 GET_JITAN_AUTO_MODE_ONOFF,
1501 if cfg.jitan_auto_mode_onoff { 1 } else { 0 },
1502 );
1503 cfg_set_int(
1504 &mut ctx.globals.syscom,
1505 GET_JITAN_KOE_REPLAY_ONOFF,
1506 if cfg.jitan_msgbk_onoff { 1 } else { 0 },
1507 );
1508 cfg_set_int(&mut ctx.globals.syscom, GET_JITAN_SPEED, cfg.jitan_speed);
1509 cfg_set_int(&mut ctx.globals.syscom, GET_KOEMODE, cfg.koe_mode);
1510 cfg_set_int(
1511 &mut ctx.globals.syscom,
1512 GET_SLEEP_ONOFF,
1513 if cfg.sleep_flag { 1 } else { 0 },
1514 );
1515 cfg_set_int(
1516 &mut ctx.globals.syscom,
1517 GET_NO_WIPE_ANIME_ONOFF,
1518 if cfg.no_wipe_anime_flag { 1 } else { 0 },
1519 );
1520 cfg_set_int(
1521 &mut ctx.globals.syscom,
1522 GET_SKIP_WIPE_ANIME_ONOFF,
1523 if cfg.skip_wipe_anime_flag { 1 } else { 0 },
1524 );
1525 cfg_set_int(
1526 &mut ctx.globals.syscom,
1527 GET_NO_MWND_ANIME_ONOFF,
1528 if cfg.no_mwnd_anime_flag { 1 } else { 0 },
1529 );
1530 cfg_set_int(
1531 &mut ctx.globals.syscom,
1532 GET_WHEEL_NEXT_MESSAGE_ONOFF,
1533 if cfg.wheel_next_message_flag { 1 } else { 0 },
1534 );
1535 cfg_set_int(
1536 &mut ctx.globals.syscom,
1537 GET_KOE_DONT_STOP_ONOFF,
1538 if cfg.koe_dont_stop_flag { 1 } else { 0 },
1539 );
1540 cfg_set_int(
1541 &mut ctx.globals.syscom,
1542 GET_SKIP_UNREAD_MESSAGE_ONOFF,
1543 if cfg.skip_unread_message_flag { 1 } else { 0 },
1544 );
1545 cfg_set_int(
1546 &mut ctx.globals.syscom,
1547 GET_SAVELOAD_ALERT_ONOFF,
1548 if cfg.saveload_alert_flag { 1 } else { 0 },
1549 );
1550 apply_audio_config(ctx);
1551}
1552
1553pub fn apply_config_dialog_state(
1555 ctx: &mut CommandContext,
1556 mut config: crate::runtime::globals::OriginalConfigRuntimeState,
1557) {
1558 resize_original_config_arrays(ctx, &mut config);
1559 ctx.globals.syscom.original_config = config;
1560 apply_original_config_to_runtime(ctx);
1561}
1562
1563pub fn write_config_save(ctx: &CommandContext) {
1564 let cfg = config_state_for_save(ctx);
1565 let mut stream = original_save::OriginalStreamWriter::new();
1566 stream.push_i32(cfg.screen_size_mode as i32);
1567 stream.push_i32(cfg.screen_size_mode_window as i32);
1568 stream.push_i32(cfg.screen_size_scale.0 as i32);
1569 stream.push_i32(cfg.screen_size_scale.1 as i32);
1570 stream.push_i32(cfg.screen_size_free.0 as i32);
1571 stream.push_i32(cfg.screen_size_free.1 as i32);
1572 stream.push_bool(cfg.fullscreen_change_resolution);
1573 stream.push_i32(cfg.fullscreen_display_cnt as i32);
1574 stream.push_i32(cfg.fullscreen_display_no as i32);
1575 stream.push_i32(cfg.fullscreen_resolution_cnt as i32);
1576 stream.push_i32(cfg.fullscreen_resolution_no as i32);
1577 stream.push_i32(cfg.fullscreen_resolution.0 as i32);
1578 stream.push_i32(cfg.fullscreen_resolution.1 as i32);
1579 stream.push_i32(cfg.fullscreen_mode as i32);
1580 stream.push_i32(cfg.fullscreen_scale.0 as i32);
1581 stream.push_i32(cfg.fullscreen_scale.1 as i32);
1582 stream.push_bool(cfg.fullscreen_scale_sync_switch);
1583 stream.push_i32(cfg.fullscreen_move.0 as i32);
1584 stream.push_i32(cfg.fullscreen_move.1 as i32);
1585 stream.push_i32(cfg.all_sound_user_volume.clamp(0, 255) as i32);
1586 for value in cfg.sound_user_volume {
1587 stream.push_i32(value.clamp(0, 255) as i32);
1588 }
1589 stream.push_bool(cfg.play_all_sound_check);
1590 for value in cfg.play_sound_check {
1591 stream.push_bool(value);
1592 }
1593 stream.push_i32(cfg.bgmfade_volume.clamp(0, 255) as i32);
1594 stream.push_bool(cfg.bgmfade_use_check);
1595 stream.push_u32(cfg.filter_color_argb);
1596 stream.push_bool(cfg.font_proportional);
1597 stream.push_str(&cfg.font_name);
1598 stream.push_i32(cfg.font_shadow as i32);
1599 stream.push_bool(cfg.font_futoku);
1600 stream.push_i32(cfg.message_speed as i32);
1601 stream.push_bool(cfg.message_speed_nowait);
1602 stream.push_bool(cfg.auto_mode_onoff);
1603 stream.push_i32(cfg.auto_mode_moji_wait as i32);
1604 stream.push_i32(cfg.auto_mode_min_wait as i32);
1605 stream.push_bool(cfg.mouse_cursor_hide_onoff);
1606 stream.push_i32(cfg.mouse_cursor_hide_time as i32);
1607 stream.push_bool(cfg.jitan_normal_onoff);
1608 stream.push_bool(cfg.jitan_auto_mode_onoff);
1609 stream.push_bool(cfg.jitan_msgbk_onoff);
1610 stream.push_i32(cfg.jitan_speed as i32);
1611 stream.push_i32(cfg.koe_mode as i32);
1612 stream.push_i32(cfg.chrkoe.len() as i32);
1613 for item in &cfg.chrkoe {
1614 stream.push_bool(item.onoff);
1615 stream.push_padding(3);
1616 stream.push_i32(item.volume.clamp(0, 255) as i32);
1617 }
1618 stream.push_bool(cfg.message_chrcolor_flag);
1619 stream.push_i32(cfg.object_disp_flag.len() as i32);
1620 for value in &cfg.object_disp_flag {
1621 stream.push_bool(*value);
1622 }
1623 stream.push_i32(cfg.global_extra_switch_flag.len() as i32);
1624 for value in &cfg.global_extra_switch_flag {
1625 stream.push_bool(*value);
1626 }
1627 stream.push_i32(cfg.global_extra_mode_flag.len() as i32);
1628 for value in &cfg.global_extra_mode_flag {
1629 stream.push_i32(*value as i32);
1630 }
1631 for value in [
1632 cfg.sleep_flag,
1633 cfg.no_wipe_anime_flag,
1634 cfg.skip_wipe_anime_flag,
1635 cfg.no_mwnd_anime_flag,
1636 cfg.wheel_next_message_flag,
1637 cfg.koe_dont_stop_flag,
1638 cfg.skip_unread_message_flag,
1639 cfg.saveload_alert_flag,
1640 cfg.saveload_dblclick_flag,
1641 ] {
1642 stream.push_bool(value);
1643 }
1644 stream.push_str(&cfg.ss_path);
1645 stream.push_str(&cfg.editor_path);
1646 stream.push_str(&cfg.koe_path);
1647 stream.push_str(&cfg.koe_tool_path);
1648 stream.push_bool(cfg.joypad_swap_ab);
1649 stream.push_bool(cfg.joypad_swap_xy);
1650
1651 if let Err(err) = original_save::write_config_save_file(&ctx.project_dir, &stream.into_inner())
1652 {
1653 log::error!("[SG_SAVE] failed to write config.sav: {err:#}");
1654 }
1655}
1656
1657fn load_config_save(ctx: &mut CommandContext) -> Result<()> {
1658 let mut cfg = original_config_defaults(ctx);
1659 let config_path = original_save::save_dir(&ctx.project_dir).join("config.sav");
1660 if !crate::resource::game_file_exists(&config_path) {
1661 resize_original_config_arrays(ctx, &mut cfg);
1662 ctx.globals.syscom.original_config = cfg;
1663 apply_original_config_to_runtime(ctx);
1664 return Ok(());
1665 }
1666 let (header, payload) = original_save::read_config_save_file(&ctx.project_dir)?;
1667 save_versions::read_config(
1668 header,
1669 &payload,
1670 &mut cfg,
1671 &gameexe_unquoted_owned(ctx, "GAMEID"),
1672 )?;
1673 resize_original_config_arrays(ctx, &mut cfg);
1674 ctx.globals.syscom.original_config = cfg;
1675 apply_original_config_to_runtime(ctx);
1676 Ok(())
1677}
1678
1679fn write_read_flags(ctx: &CommandContext) -> Result<()> {
1680 let metadata = ctx.scene_metadata()?;
1681 let mut rows = Vec::with_capacity(metadata.rows.len());
1682 for (scene_no, (scene_name, flag_count)) in metadata.rows.iter().enumerate() {
1683 let mut flags = ctx
1684 .globals
1685 .read_flags
1686 .get(&(scene_no as i64))
1687 .cloned()
1688 .unwrap_or_default();
1689 flags.resize(*flag_count, 0);
1690 flags.truncate(*flag_count);
1691 rows.push((scene_name.clone(), flags));
1692 }
1693 original_save::write_read_save_file(&ctx.project_dir, &rows)
1694}
1695
1696fn load_read_flags(ctx: &mut CommandContext) -> Result<()> {
1697 let read_path = original_save::save_dir(&ctx.project_dir).join("read.sav");
1698 if crate::resource::resolve_windows_case_insensitive_file(&read_path)?.is_none() {
1699 return Ok(());
1700 }
1701 let metadata = ctx.scene_metadata()?;
1702 ctx.globals.read_flags.clear();
1703 for (scene_no, (_, flag_count)) in metadata.rows.iter().enumerate() {
1704 ctx.globals
1705 .ensure_read_flag_count(scene_no as i64, *flag_count);
1706 }
1707
1708 let rows = match original_save::read_read_save_file(&ctx.project_dir) {
1709 Ok(rows) => rows,
1710 Err(_) => return Ok(()),
1711 };
1712 for (scene_name, saved_flags) in rows {
1713 let Some(scene_no) = metadata.find_scene_no(&scene_name) else {
1714 continue;
1715 };
1716 let Some((_, real_flag_count)) = metadata.rows.get(scene_no) else {
1717 continue;
1718 };
1719 if saved_flags.len() == *real_flag_count {
1722 ctx.globals.read_flags.insert(scene_no as i64, saved_flags);
1723 }
1724 }
1725 Ok(())
1726}
1727
1728pub fn global_save_fingerprint(ctx: &CommandContext) -> u64 {
1733 use std::hash::{Hash, Hasher};
1734 let mut hasher = std::collections::hash_map::DefaultHasher::new();
1735 for code in [
1736 crate::runtime::forms::codes::ELM_GLOBAL_G,
1737 crate::runtime::forms::codes::ELM_GLOBAL_Z,
1738 ] {
1739 ctx.globals.int_lists.get(&(code as u32)).hash(&mut hasher);
1740 }
1741 for code in [
1742 crate::runtime::forms::codes::ELM_GLOBAL_M,
1743 crate::runtime::forms::codes::ELM_GLOBAL_NAMAE_GLOBAL,
1744 ] {
1745 ctx.globals.str_lists.get(&(code as u32)).hash(&mut hasher);
1746 }
1747 ctx.tables.cg_flags.hash(&mut hasher);
1748 ctx.globals.bgm_table_flags.hash(&mut hasher);
1749 let mut scenes: Vec<_> = ctx.globals.read_flags.iter().collect();
1750 scenes.sort_by_key(|(scene, _)| **scene);
1751 scenes.hash(&mut hasher);
1752 hasher.finish()
1753}
1754
1755pub fn write_global_save(ctx: &CommandContext) {
1756 write_config_save(ctx);
1757 let mut stream = original_save::OriginalStreamWriter::new();
1758 stream.push_i64(ctx.globals.syscom.total_play_time);
1759
1760 let configured_flag_cnt = ctx.tables.gameexe.as_ref().and_then(|cfg| {
1761 cfg.get_usize("#GLOBAL_FLAG.CNT")
1762 .or_else(|| cfg.get_usize("GLOBAL_FLAG.CNT"))
1763 });
1764 let current_flag_cnt = [
1765 ctx.globals
1766 .int_lists
1767 .get(&(crate::runtime::forms::codes::ELM_GLOBAL_G as u32))
1768 .map_or(0, Vec::len),
1769 ctx.globals
1770 .int_lists
1771 .get(&(crate::runtime::forms::codes::ELM_GLOBAL_Z as u32))
1772 .map_or(0, Vec::len),
1773 ctx.globals
1774 .str_lists
1775 .get(&(crate::runtime::forms::codes::ELM_GLOBAL_M as u32))
1776 .map_or(0, Vec::len),
1777 ]
1778 .into_iter()
1779 .max()
1780 .unwrap_or(0);
1781 let fixed_flag_cnt = configured_flag_cnt
1782 .unwrap_or(current_flag_cnt.max(1000))
1783 .min(10000);
1784 let cg_flag_cnt = ctx
1785 .tables
1786 .cgtable_flag_cnt
1787 .or_else(|| {
1788 ctx.tables.gameexe.as_ref().and_then(|cfg| {
1789 cfg.get_usize("#CGTABLE_FLAG_CNT")
1790 .or_else(|| cfg.get_usize("CGTABLE_FLAG_CNT"))
1791 })
1792 })
1793 .unwrap_or(ctx.tables.cg_flags.len())
1794 .max(ctx.tables.cg_flags.len());
1795 let bgm_cnt = ctx
1796 .tables
1797 .gameexe
1798 .as_ref()
1799 .and_then(|cfg| {
1800 cfg.get_usize("#BGM.CNT")
1801 .or_else(|| cfg.get_usize("BGM.CNT"))
1802 })
1803 .unwrap_or(32)
1804 .max(ctx.globals.bgm_table_flags.len());
1805 let g = ctx
1806 .globals
1807 .int_lists
1808 .get(&(crate::runtime::forms::codes::ELM_GLOBAL_G as u32))
1809 .map(Vec::as_slice)
1810 .unwrap_or(&[]);
1811 let z = ctx
1812 .globals
1813 .int_lists
1814 .get(&(crate::runtime::forms::codes::ELM_GLOBAL_Z as u32))
1815 .map(Vec::as_slice)
1816 .unwrap_or(&[]);
1817 let m = ctx
1818 .globals
1819 .str_lists
1820 .get(&(crate::runtime::forms::codes::ELM_GLOBAL_M as u32))
1821 .map(Vec::as_slice)
1822 .unwrap_or(&[]);
1823 let namae_global = ctx
1824 .globals
1825 .str_lists
1826 .get(&(crate::runtime::forms::codes::ELM_GLOBAL_NAMAE_GLOBAL as u32))
1827 .map(Vec::as_slice)
1828 .unwrap_or(&[]);
1829
1830 stream.push_fixed_i32_list(g, fixed_flag_cnt);
1831 stream.push_fixed_i32_list(z, fixed_flag_cnt);
1832 stream.push_fixed_str_list(m, fixed_flag_cnt);
1833 stream.push_fixed_str_list(namae_global, 26 + 26 * 26);
1834 stream.push_i32(0);
1835
1836 let cg_flags: Vec<i64> = ctx.tables.cg_flags.iter().map(|v| *v as i64).collect();
1837 let has_cg_table = ctx.tables.cgtable.is_some()
1840 || ctx
1841 .tables
1842 .gameexe
1843 .as_ref()
1844 .and_then(|cfg| cfg.get_unquoted("CGTABLE_FILE"))
1845 .is_some_and(|name| !name.is_empty());
1846 if !has_cg_table && cg_flags.is_empty() {
1847 stream.push_extend_i32_list(&[]);
1848 } else {
1849 stream.push_fixed_i32_list(&cg_flags, cg_flag_cnt);
1850 }
1851
1852 let bgm_flags: Vec<i64> = ctx
1853 .globals
1854 .bgm_table_flags
1855 .iter()
1856 .map(|v| if *v { 1 } else { 0 })
1857 .collect();
1858 stream.push_fixed_i32_list(&bgm_flags, bgm_cnt);
1859
1860 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
1864 if let Err(err) = crate::runtime::twitter::save_state(ctx) {
1865 eprintln!("[SG_SAVE] failed to write Twitter state: {err:#}");
1866 }
1867
1868 let chrkoe_count = configured_chrkoe_count(ctx);
1869 stream.push_i32(chrkoe_count as i32);
1870 for index in 0..chrkoe_count {
1871 let name = ctx
1872 .tables
1873 .gameexe
1874 .as_ref()
1875 .and_then(|cfg| cfg.get_indexed_item_unquoted("CHRKOE", index, 0))
1876 .unwrap_or("");
1877 stream.push_str(name);
1878 stream.push_bool(config_chrkoe_name_visible(ctx, index));
1879 }
1880
1881 let payload = stream.into_inner();
1882 if let Err(err) = original_save::write_global_save_file(&ctx.project_dir, &payload) {
1883 eprintln!("[SG_SAVE] failed to write global.sav: {err:#}");
1884 }
1885 if let Err(err) = write_read_flags(ctx) {
1886 eprintln!("[SG_SAVE] failed to write read.sav: {err:#}");
1887 }
1888}
1889
1890pub fn load_global_save(ctx: &mut CommandContext) -> Result<()> {
1891 let global_path = original_save::save_dir(&ctx.project_dir).join("global.sav");
1892 if crate::resource::resolve_windows_case_insensitive_file(&global_path)?.is_some() {
1893 let (header, payload) = original_save::read_global_save_file(&ctx.project_dir)?;
1894 let fixed_flag_cnt = ctx
1895 .tables
1896 .gameexe
1897 .as_ref()
1898 .and_then(|cfg| {
1899 cfg.get_usize("#GLOBAL_FLAG.CNT")
1900 .or_else(|| cfg.get_usize("GLOBAL_FLAG.CNT"))
1901 })
1902 .map(|count| count.min(10000));
1903 let save_versions::GlobalSaveData {
1904 total_play_time,
1905 mut g,
1906 mut z,
1907 mut m,
1908 mut namae_global,
1909 cg,
1910 bgm,
1911 chrkoe_look_flags,
1912 } = save_versions::read_global(header, &payload)?;
1913 if let Some(count) = fixed_flag_cnt {
1914 g.resize(count, 0);
1915 z.resize(count, 0);
1916 m.resize_with(count, String::new);
1917 }
1918 namae_global.resize_with(26 + 26 * 26, String::new);
1919 ctx.globals.syscom.chrkoe_look_flags = chrkoe_look_flags.into_iter().collect();
1920
1921 ctx.globals.syscom.total_play_time = total_play_time;
1922 ctx.globals
1923 .int_lists
1924 .insert(crate::runtime::forms::codes::ELM_GLOBAL_G as u32, g);
1925 ctx.globals
1926 .int_lists
1927 .insert(crate::runtime::forms::codes::ELM_GLOBAL_Z as u32, z);
1928 ctx.globals
1929 .str_lists
1930 .insert(crate::runtime::forms::codes::ELM_GLOBAL_M as u32, m);
1931 ctx.globals.str_lists.insert(
1932 crate::runtime::forms::codes::ELM_GLOBAL_NAMAE_GLOBAL as u32,
1933 namae_global,
1934 );
1935 ctx.tables.cg_flags = cg.into_iter().map(|v| v as i32).collect();
1937 ctx.globals.bgm_table_flags = bgm.into_iter().map(|v| v != 0).collect();
1938 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
1939 crate::runtime::twitter::ensure_state_loaded(ctx);
1940 }
1941 load_read_flags(ctx)?;
1942 load_config_save(ctx)?;
1943 Ok(())
1944}
1945
1946fn ensure_slot_loaded_with_counts(
1947 project_dir: &Path,
1948 quick: bool,
1949 save_cnt: usize,
1950 quick_cnt: usize,
1951 slots: &mut Vec<SaveSlotState>,
1952 idx: usize,
1953) {
1954 if slots
1958 .get(idx)
1959 .map(|slot| slot.header_cache_valid)
1960 .unwrap_or(false)
1961 {
1962 return;
1963 }
1964
1965 let path = slot_path_with_counts(project_dir, quick, idx, save_cnt, quick_cnt);
1966 if save_load_trace_enabled() {
1967 let before_exist = slots.get(idx).map(|s| s.exist).unwrap_or(false);
1968 eprintln!(
1969 "[SG_SAVELOAD_TRACE][SYSCOM] ensure_slot_loaded kind={} idx={} path={} file_exists={} cached_exist={}",
1970 if quick { "quick" } else { "normal" },
1971 idx,
1972 path.display(),
1973 crate::resource::game_file_exists(&path),
1974 before_exist
1975 );
1976 }
1977
1978 let next = match read_slot(&path) {
1979 Some(slot) => slot,
1980 None => {
1981 SaveSlotState {
1986 header_cache_valid: true,
1987 ..Default::default()
1988 }
1989 }
1990 };
1991 *ensure_slot(slots, idx) = next;
1992}
1993
1994pub(crate) fn read_save_flag_values(
1995 ctx: &mut CommandContext,
1996 quick: bool,
1997 raw_idx: i64,
1998 flag_cnt: usize,
1999) -> Option<Vec<i64>> {
2000 let project_dir = ctx.project_dir.clone();
2001 let save_cnt = configured_save_count(ctx, false);
2002 let quick_cnt = configured_save_count(ctx, true);
2003 let configured = if quick { quick_cnt } else { save_cnt };
2004 if raw_idx < 0 || raw_idx as usize >= configured {
2005 return None;
2006 }
2007 let idx = raw_idx as usize;
2008 let slots = if quick {
2009 &mut ctx.globals.syscom.quick_save_slots
2010 } else {
2011 &mut ctx.globals.syscom.save_slots
2012 };
2013 ensure_slot_loaded_with_counts(&project_dir, quick, save_cnt, quick_cnt, slots, idx);
2014 let slot = slots.get(idx).filter(|slot| slot.exist)?;
2015 let count = flag_cnt.min(original_save::SAVE_FLAG_MAX_CNT);
2016 Some(
2017 (0..count)
2018 .map(|i| slot.values.get(&(i as i32)).copied().unwrap_or(0))
2019 .collect(),
2020 )
2021}
2022
2023pub(crate) fn write_save_flag_values(
2024 ctx: &mut CommandContext,
2025 quick: bool,
2026 raw_idx: i64,
2027 values: &[i64],
2028) -> bool {
2029 let project_dir = ctx.project_dir.clone();
2030 let save_cnt = configured_save_count(ctx, false);
2031 let quick_cnt = configured_save_count(ctx, true);
2032 let configured = if quick { quick_cnt } else { save_cnt };
2033 if raw_idx < 0 || raw_idx as usize >= configured {
2034 return false;
2035 }
2036 let idx = raw_idx as usize;
2037 let slots = if quick {
2038 &mut ctx.globals.syscom.quick_save_slots
2039 } else {
2040 &mut ctx.globals.syscom.save_slots
2041 };
2042 ensure_slot_loaded_with_counts(&project_dir, quick, save_cnt, quick_cnt, slots, idx);
2043 if !slots.get(idx).map(|slot| slot.exist).unwrap_or(false) {
2044 return false;
2045 }
2046 let count = values.len().min(original_save::SAVE_FLAG_MAX_CNT);
2047 let slot = &mut slots[idx];
2048 for (i, value) in values.iter().copied().take(count).enumerate() {
2049 if value == 0 {
2050 slot.values.remove(&(i as i32));
2051 } else {
2052 slot.values.insert(i as i32, value);
2053 }
2054 }
2055 persist_slot_with_counts(&project_dir, quick, save_cnt, quick_cnt, slots, idx);
2056 true
2057}
2058
2059fn sync_slots_from_disk_with_counts(
2060 project_dir: &Path,
2061 quick: bool,
2062 save_cnt: usize,
2063 quick_cnt: usize,
2064 slots: &mut Vec<SaveSlotState>,
2065 count: usize,
2066) {
2067 if slots.len() < count {
2068 slots.resize_with(count, SaveSlotState::default);
2069 }
2070 for idx in 0..count {
2071 ensure_slot_loaded_with_counts(project_dir, quick, save_cnt, quick_cnt, slots, idx);
2072 }
2073}
2074
2075pub(crate) fn sync_save_slots_from_disk(ctx: &mut CommandContext, quick: bool) {
2076 if save_load_trace_enabled() {
2077 eprintln!(
2078 "[SG_SAVELOAD_TRACE][SYSCOM] sync_save_slots_from_disk kind={}",
2079 if quick { "quick" } else { "normal" }
2080 );
2081 }
2082 let project_dir = ctx.project_dir.clone();
2083 let save_cnt = configured_save_count(ctx, false);
2084 let quick_cnt = configured_save_count(ctx, true);
2085 if quick {
2086 sync_slots_from_disk_with_counts(
2087 &project_dir,
2088 true,
2089 save_cnt,
2090 quick_cnt,
2091 &mut ctx.globals.syscom.quick_save_slots,
2092 quick_cnt,
2093 );
2094 } else {
2095 sync_slots_from_disk_with_counts(
2096 &project_dir,
2097 false,
2098 save_cnt,
2099 quick_cnt,
2100 &mut ctx.globals.syscom.save_slots,
2101 save_cnt,
2102 );
2103 }
2104}
2105
2106fn persist_slot_with_counts(
2107 project_dir: &Path,
2108 quick: bool,
2109 save_cnt: usize,
2110 quick_cnt: usize,
2111 slots: &mut [SaveSlotState],
2112 idx: usize,
2113) {
2114 let Some(slot) = slots.get(idx) else {
2115 return;
2116 };
2117 if !slot.header_cache_valid || !slot.exist {
2120 return;
2121 }
2122
2123 let path = slot_path_with_counts(project_dir, quick, idx, save_cnt, quick_cnt);
2124 let Some(existing_path) = crate::resource::resolve_game_file(&path).ok().flatten() else {
2125 return;
2126 };
2127 let header = original_save::OriginalSaveHeader::from_slot(slot, slot.packed_data_size);
2128
2129 if let Some(slot) = slots.get_mut(idx) {
2135 slot.header_cache_valid = false;
2136 }
2137
2138 if let Err(err) = original_save::write_header_in_place(&existing_path, &header) {
2139 eprintln!(
2140 "[SG_SAVE] failed to update original save header {}: {err:#}",
2141 existing_path.display()
2142 );
2143 }
2144}
2145
2146fn slot_thumb_save_no(save_cnt: usize, quick_cnt: usize, quick: bool, idx: usize) -> usize {
2147 let kind = if quick {
2148 SaveKind::Quick
2149 } else {
2150 SaveKind::Normal
2151 };
2152 original_save::original_save_no(save_cnt, quick_cnt, kind, idx)
2153}
2154
2155fn remove_thumb_file(
2156 project_dir: &Path,
2157 save_cnt: usize,
2158 quick_cnt: usize,
2159 quick: bool,
2160 config: SaveThumbConfig,
2161 idx: usize,
2162) {
2163 if !config.enabled {
2164 return;
2165 }
2166 let save_no = slot_thumb_save_no(save_cnt, quick_cnt, quick, idx);
2167 remove_game_file(&thumb_path_for_no_with_config(project_dir, config, save_no));
2168}
2169
2170fn copy_thumb_file(
2171 project_dir: &Path,
2172 save_cnt: usize,
2173 quick_cnt: usize,
2174 quick: bool,
2175 config: SaveThumbConfig,
2176 src: usize,
2177 dst: usize,
2178) {
2179 if !config.enabled {
2180 return;
2181 }
2182 let src_no = slot_thumb_save_no(save_cnt, quick_cnt, quick, src);
2183 let dst_no = slot_thumb_save_no(save_cnt, quick_cnt, quick, dst);
2184 let src_path = thumb_path_for_no_with_config(project_dir, config, src_no);
2185 let dst_path = thumb_path_for_no_with_config(project_dir, config, dst_no);
2186 if let Some(src_path) = crate::resource::resolve_game_file(&src_path).ok().flatten() {
2187 if let Some(parent) = dst_path.parent() {
2188 let _ = fs::create_dir_all(parent);
2189 }
2190 copy_game_file(&src_path, &dst_path);
2191 } else {
2192 remove_game_file(&dst_path);
2193 }
2194}
2195
2196fn swap_thumb_file(
2197 project_dir: &Path,
2198 save_cnt: usize,
2199 quick_cnt: usize,
2200 quick: bool,
2201 config: SaveThumbConfig,
2202 a: usize,
2203 b: usize,
2204) {
2205 if !config.enabled || a == b {
2206 return;
2207 }
2208 let a_no = slot_thumb_save_no(save_cnt, quick_cnt, quick, a);
2209 let b_no = slot_thumb_save_no(save_cnt, quick_cnt, quick, b);
2210 let pa = thumb_path_for_no_with_config(project_dir, config, a_no);
2211 let pb = thumb_path_for_no_with_config(project_dir, config, b_no);
2212 let tmp = pa.with_extension(format!(
2213 "{}.swap",
2214 pa.extension().and_then(|v| v.to_str()).unwrap_or("tmp")
2215 ));
2216 let pa_existing = crate::resource::resolve_game_file(&pa).ok().flatten();
2217 let pb_existing = crate::resource::resolve_game_file(&pb).ok().flatten();
2218 let a_exists = pa_existing.is_some();
2219 let b_exists = pb_existing.is_some();
2220 if let Some(existing) = pa_existing.as_ref() {
2221 rename_game_file(existing, &tmp);
2222 }
2223 if let Some(existing) = pb_existing.as_ref() {
2224 if let Some(parent) = pa.parent() {
2225 let _ = fs::create_dir_all(parent);
2226 }
2227 rename_game_file(existing, &pa);
2228 } else {
2229 remove_game_file(&pa);
2230 }
2231 if a_exists {
2232 if let Some(parent) = pb.parent() {
2233 let _ = fs::create_dir_all(parent);
2234 }
2235 rename_game_file(&tmp, &pb);
2236 } else {
2237 remove_game_file(&pb);
2238 }
2239 remove_game_file(&tmp);
2240}
2241
2242fn copy_save_file(
2243 project_dir: &Path,
2244 quick: bool,
2245 save_cnt: usize,
2246 quick_cnt: usize,
2247 src: usize,
2248 dst: usize,
2249) {
2250 let src_path = slot_path_with_counts(project_dir, quick, src, save_cnt, quick_cnt);
2251 let dst_path = slot_path_with_counts(project_dir, quick, dst, save_cnt, quick_cnt);
2252 if let Some(src_path) = crate::resource::resolve_game_file(&src_path).ok().flatten() {
2253 if let Some(parent) = dst_path.parent() {
2254 let _ = fs::create_dir_all(parent);
2255 }
2256 copy_game_file(&src_path, &dst_path);
2257 } else {
2258 remove_game_file(&dst_path);
2259 }
2260}
2261
2262fn swap_save_file(
2263 project_dir: &Path,
2264 quick: bool,
2265 save_cnt: usize,
2266 quick_cnt: usize,
2267 a: usize,
2268 b: usize,
2269) {
2270 if a == b {
2271 return;
2272 }
2273 let pa = slot_path_with_counts(project_dir, quick, a, save_cnt, quick_cnt);
2274 let pb = slot_path_with_counts(project_dir, quick, b, save_cnt, quick_cnt);
2275 let tmp = pa.with_extension("sav.swap");
2276 let pa_existing = crate::resource::resolve_game_file(&pa).ok().flatten();
2277 let pb_existing = crate::resource::resolve_game_file(&pb).ok().flatten();
2278 let a_exists = pa_existing.is_some();
2279 let b_exists = pb_existing.is_some();
2280 if let Some(existing) = pa_existing.as_ref() {
2281 rename_game_file(existing, &tmp);
2282 }
2283 if let Some(existing) = pb_existing.as_ref() {
2284 if let Some(parent) = pa.parent() {
2285 let _ = fs::create_dir_all(parent);
2286 }
2287 rename_game_file(existing, &pa);
2288 } else {
2289 remove_game_file(&pa);
2290 }
2291 if a_exists {
2292 if let Some(parent) = pb.parent() {
2293 let _ = fs::create_dir_all(parent);
2294 }
2295 rename_game_file(&tmp, &pb);
2296 } else {
2297 remove_game_file(&pb);
2298 }
2299 remove_game_file(&tmp);
2300}
2301
2302fn copy_slot(
2303 project_dir: &Path,
2304 quick: bool,
2305 save_cnt: usize,
2306 quick_cnt: usize,
2307 thumb_config: SaveThumbConfig,
2308 slots: &mut Vec<SaveSlotState>,
2309 src: usize,
2310 dst: usize,
2311) -> bool {
2312 ensure_slot_loaded_with_counts(project_dir, quick, save_cnt, quick_cnt, slots, src);
2313 let Some(src_slot) = slots.get(src).cloned() else {
2314 return false;
2315 };
2316 if !src_slot.exist {
2317 return false;
2318 }
2319 *ensure_slot(slots, dst) = src_slot;
2320 copy_save_file(project_dir, quick, save_cnt, quick_cnt, src, dst);
2321 copy_thumb_file(
2322 project_dir,
2323 save_cnt,
2324 quick_cnt,
2325 quick,
2326 thumb_config,
2327 src,
2328 dst,
2329 );
2330 true
2331}
2332
2333fn change_slot(
2334 project_dir: &Path,
2335 quick: bool,
2336 save_cnt: usize,
2337 quick_cnt: usize,
2338 thumb_config: SaveThumbConfig,
2339 slots: &mut Vec<SaveSlotState>,
2340 a: usize,
2341 b: usize,
2342) -> bool {
2343 ensure_slot_loaded_with_counts(project_dir, quick, save_cnt, quick_cnt, slots, a);
2344 ensure_slot_loaded_with_counts(project_dir, quick, save_cnt, quick_cnt, slots, b);
2345 let max_idx = a.max(b);
2346 if slots.len() <= max_idx {
2347 slots.resize_with(max_idx + 1, SaveSlotState::default);
2348 }
2349 slots.swap(a, b);
2350 swap_save_file(project_dir, quick, save_cnt, quick_cnt, a, b);
2351 swap_thumb_file(project_dir, save_cnt, quick_cnt, quick, thumb_config, a, b);
2352 true
2353}
2354
2355fn delete_slot(
2356 project_dir: &Path,
2357 quick: bool,
2358 save_cnt: usize,
2359 quick_cnt: usize,
2360 thumb_config: SaveThumbConfig,
2361 slots: &mut Vec<SaveSlotState>,
2362 idx: usize,
2363) -> bool {
2364 ensure_slot_loaded_with_counts(project_dir, quick, save_cnt, quick_cnt, slots, idx);
2365 let existed = slots.get(idx).map(|s| s.exist).unwrap_or(false);
2366 let mut deleted = SaveSlotState {
2367 header_cache_valid: true,
2368 ..Default::default()
2369 };
2370 *ensure_slot(slots, idx) = deleted;
2375 let path = slot_path_with_counts(project_dir, quick, idx, save_cnt, quick_cnt);
2376 remove_game_file(&path);
2377 remove_thumb_file(project_dir, save_cnt, quick_cnt, quick, thumb_config, idx);
2378 existed
2379}
2380
2381fn capture_flags_path(image_path: &Path) -> PathBuf {
2382 let mut p = image_path.to_path_buf();
2383 let ext = p
2384 .extension()
2385 .and_then(|v| v.to_str())
2386 .map(|v| format!("{v}.siglus_flags"))
2387 .unwrap_or_else(|| "siglus_flags".to_string());
2388 p.set_extension(ext);
2389 p
2390}
2391
2392fn named_i64(params: &[Value], id: i32, default: i64) -> i64 {
2393 params
2394 .iter()
2395 .find_map(|v| match v {
2396 Value::NamedArg { id: nid, value } if *nid == id => value.as_i64(),
2397 _ => None,
2398 })
2399 .unwrap_or(default)
2400}
2401
2402fn named_element(params: &[Value], id: i32) -> Option<Vec<i32>> {
2403 params.iter().find_map(|v| match v {
2404 Value::NamedArg { id: nid, value } if *nid == id => match value.as_ref() {
2405 Value::Element(chain) => Some(chain.clone()),
2406 _ => None,
2407 },
2408 _ => None,
2409 })
2410}
2411
2412fn save_capture_flags_sidecar(ctx: &CommandContext, image_path: &Path, params: &[Value]) {
2413 let Some(flag_chain) = named_element(params, 2) else {
2414 return;
2415 };
2416 let Some(flag_form) = flag_chain.first().copied() else {
2417 return;
2418 };
2419 let flag_index = named_i64(params, 3, 0).max(0) as usize;
2420 let flag_cnt = named_i64(params, 4, 0).max(0) as usize;
2421 let str_chain = named_element(params, 5);
2422 let str_index = named_i64(params, 6, 0).max(0) as usize;
2423 let str_cnt = named_i64(params, 7, 0).max(0) as usize;
2424
2425 let mut out = String::new();
2426 out.push_str("version=1\n");
2427 out.push_str(&format!("flag_cnt={flag_cnt}\n"));
2428 if let Some(list) = ctx.globals.int_lists.get(&(flag_form as u32)) {
2429 for i in 0..flag_cnt {
2430 let v = list.get(flag_index + i).copied().unwrap_or(0);
2431 out.push_str(&format!("flag.{i}={v}\n"));
2432 }
2433 }
2434 if let Some(str_form) = str_chain.and_then(|v| v.first().copied()) {
2435 out.push_str(&format!("str_cnt={str_cnt}\n"));
2436 if let Some(list) = ctx.globals.str_lists.get(&(str_form as u32)) {
2437 for i in 0..str_cnt {
2438 let v = list.get(str_index + i).cloned().unwrap_or_default();
2439 out.push_str(&format!("str.{i}={}\n", escape_str(&v)));
2440 }
2441 }
2442 }
2443 if let Some(parent) = image_path.parent() {
2444 let _ = fs::create_dir_all(parent);
2445 }
2446 let sidecar = capture_flags_path(image_path);
2447 if fs::write(&sidecar, out).is_ok() {
2448 mark_game_file_written(&sidecar);
2449 }
2450}
2451
2452fn load_capture_flags_sidecar(
2453 ctx: &mut CommandContext,
2454 image_path: &Path,
2455 params: &[Value],
2456) -> bool {
2457 let path = capture_flags_path(image_path);
2458 let Ok(data) = crate::resource::read_file_to_string(&path) else {
2459 return crate::resource::game_file_exists(image_path);
2460 };
2461 if let Some(flag_chain) = named_element(params, 2)
2462 && let Some(flag_form) = flag_chain.first().copied()
2463 {
2464 let flag_index = named_i64(params, 3, 0).max(0) as usize;
2465 let flag_cnt = named_i64(params, 4, 0).max(0) as usize;
2466 let mut values = vec![0_i64; flag_cnt];
2467 for line in data.lines() {
2468 if let Some((k, v)) = line.split_once('=')
2469 && let Some(i) = k
2470 .strip_prefix("flag.")
2471 .and_then(|x| x.parse::<usize>().ok())
2472 && i < values.len()
2473 {
2474 values[i] = v.trim().parse::<i64>().unwrap_or(0);
2475 }
2476 }
2477 let list = ctx.globals.int_lists.entry(flag_form as u32).or_default();
2478 if list.len() < flag_index + flag_cnt {
2479 list.resize(flag_index + flag_cnt, 0);
2480 }
2481 for (i, v) in values.into_iter().enumerate() {
2482 list[flag_index + i] = v;
2483 }
2484 }
2485 if let Some(str_chain) = named_element(params, 5)
2486 && let Some(str_form) = str_chain.first().copied()
2487 {
2488 let str_index = named_i64(params, 6, 0).max(0) as usize;
2489 let str_cnt = named_i64(params, 7, 0).max(0) as usize;
2490 let mut values = vec![String::new(); str_cnt];
2491 for line in data.lines() {
2492 if let Some((k, v)) = line.split_once('=')
2493 && let Some(i) = k.strip_prefix("str.").and_then(|x| x.parse::<usize>().ok())
2494 && i < values.len()
2495 {
2496 values[i] = unescape_str(v);
2497 }
2498 }
2499 let list = ctx.globals.str_lists.entry(str_form as u32).or_default();
2500 if list.len() < str_index + str_cnt {
2501 list.resize(str_index + str_cnt, String::new());
2502 }
2503 for (i, v) in values.into_iter().enumerate() {
2504 list[str_index + i] = v;
2505 }
2506 }
2507 true
2508}
2509
2510fn write_msg_back(ctx: &CommandContext) {
2511 let form_id = ctx.ids.form_global_msgbk;
2512 if form_id == 0 {
2513 return;
2514 }
2515 let Some(st) = ctx.globals.msgbk_forms.get(&form_id) else {
2516 return;
2517 };
2518 let dir = save_dir(&ctx.project_dir);
2519 let path = dir.join("msg_back.txt");
2520
2521 let mut out = String::new();
2522 for idx in st.ordered_history_indices() {
2523 let Some(entry) = st.history.get(idx) else {
2524 continue;
2525 };
2526 out.push_str(&format!("-- entry {} --\n", idx));
2527 if !entry.original_name.is_empty() || !entry.disp_name.is_empty() {
2528 out.push_str("NAME: ");
2529 out.push_str(&entry.disp_name);
2530 out.push('\n');
2531 }
2532 if !entry.msg_str.is_empty() {
2533 if entry.pct_flag {
2534 out.push_str(&format!(
2535 "IMG: {} {} {}\n",
2536 entry.msg_str, entry.pct_pos_x, entry.pct_pos_y
2537 ));
2538 } else {
2539 out.push_str("TEXT: ");
2540 out.push_str(&entry.msg_str);
2541 out.push('\n');
2542 }
2543 }
2544 for (koe_no, chara_no) in entry.koe_no_list.iter().zip(entry.chr_no_list.iter()) {
2545 out.push_str(&format!("KOE: {} {}\n", koe_no, chara_no));
2546 }
2547 if entry.scn_no >= 0 || entry.line_no >= 0 {
2548 out.push_str(&format!("SCENE_LINE: {} {}\n", entry.scn_no, entry.line_no));
2549 }
2550 out.push('\n');
2551 }
2552 if std::fs::write(&path, out).is_ok() {
2553 mark_game_file_written(&path);
2554 }
2555}
2556
2557fn open_msg_back_proc(ctx: &mut CommandContext) -> bool {
2558 if ctx.globals.script.msg_back_disable || ctx.globals.syscom.msg_back.check_enabled() == 0 {
2559 return false;
2560 }
2561 ctx.globals.syscom.read_skip.onoff = false;
2562 ctx.globals.syscom.msg_back_open = true;
2563 ctx.globals.syscom.msg_back_proc_initialized = false;
2564 let form_id = ctx.ids.form_global_msgbk;
2565 let (count, target) = ctx
2566 .globals
2567 .msgbk_forms
2568 .get(&form_id)
2569 .map(|st| {
2570 let indices = st.ordered_history_indices();
2571 let target = if indices.contains(&st.history_last_pos) {
2572 st.history_last_pos as isize
2573 } else {
2574 indices
2575 .last()
2576 .copied()
2577 .map(|idx| idx as isize)
2578 .unwrap_or(-1)
2579 };
2580 (indices.len(), target)
2581 })
2582 .unwrap_or((0, -1));
2583 ctx.globals.syscom.msg_back_view_pos = count.saturating_sub(1);
2584 ctx.globals.syscom.msg_back_target_no = target;
2585 true
2586}
2587
2588fn configured_save_count(ctx: &CommandContext, quick: bool) -> usize {
2589 let keys: [&str; 2] = if quick {
2590 ["#QUICK_SAVE.CNT", "QUICK_SAVE.CNT"]
2591 } else {
2592 ["#SAVE.CNT", "SAVE.CNT"]
2593 };
2594 let default_count = if quick { 3 } else { 10 };
2595 ctx.tables
2596 .gameexe
2597 .as_ref()
2598 .and_then(|cfg| keys.iter().find_map(|key| cfg.get_usize(key)))
2599 .unwrap_or(default_count)
2600 .min(10000)
2601}
2602
2603const FALLBACK_BACK: i64 = -1;
2604const FALLBACK_PREV: i64 = -2;
2605const FALLBACK_NEXT: i64 = -3;
2606const FALLBACK_CLOSE: i64 = -4;
2607const FALLBACK_SLOTS_PER_PAGE: usize = 5;
2608const FALLBACK_ALL_SOUND: usize = usize::MAX;
2609
2610fn fallback_button(label: impl Into<String>, value: i64) -> SystemMessageBoxButton {
2611 SystemMessageBoxButton {
2612 label: label.into(),
2613 value,
2614 }
2615}
2616
2617fn fallback_onoff(value: bool) -> &'static str {
2618 if value { "ON" } else { "OFF" }
2619}
2620
2621fn fallback_feature_available(feature: &ToggleFeatureState) -> bool {
2622 feature.enable && feature.exist
2623}
2624
2625fn request_fallback_dialog(
2626 ctx: &mut CommandContext,
2627 kind: SyscomFallbackDialogKind,
2628 page: usize,
2629 return_kind: Option<SyscomFallbackDialogKind>,
2630 title: &str,
2631 body: String,
2632 buttons: Vec<SystemMessageBoxButton>,
2633) {
2634 ctx.globals.syscom.menu_open = false;
2635 ctx.globals.syscom.menu_kind = None;
2636 ctx.globals.syscom.menu_result = None;
2637 ctx.globals.syscom.fallback_dialog = Some(SyscomFallbackDialogState {
2638 kind,
2639 page,
2640 awaiting_result: true,
2641 return_kind,
2642 });
2643 let text = if body.is_empty() {
2644 title.to_string()
2645 } else {
2646 format!("{title}\n\n{body}")
2647 };
2648 ctx.request_internal_system_messagebox_no_return(0, false, text, buttons);
2649}
2650
2651fn close_fallback_dialog(ctx: &mut CommandContext, save_config: bool) {
2652 let was_save = matches!(
2653 ctx.globals
2654 .syscom
2655 .fallback_dialog
2656 .as_ref()
2657 .map(|state| state.kind),
2658 Some(SyscomFallbackDialogKind::SaveMenu)
2659 );
2660 ctx.globals.syscom.fallback_dialog = None;
2661 ctx.globals.syscom.fallback_origin = None;
2662 ctx.globals.syscom.menu_open = false;
2663 ctx.globals.syscom.menu_kind = None;
2664 ctx.globals.syscom.menu_result = None;
2665 if save_config {
2666 apply_audio_config(ctx);
2667 write_config_save(ctx);
2668 }
2669 if was_save {
2670 free_runtime_save_thumb_capture(ctx, CAPTURE_PRIOR_SAVE);
2671 }
2672}
2673
2674fn open_fallback_notice(
2675 ctx: &mut CommandContext,
2676 message: impl Into<String>,
2677 return_kind: Option<SyscomFallbackDialogKind>,
2678) {
2679 request_fallback_dialog(
2680 ctx,
2681 SyscomFallbackDialogKind::Notice,
2682 0,
2683 return_kind,
2684 "SYSTEM",
2685 message.into(),
2686 vec![fallback_button("OK", FALLBACK_BACK)],
2687 );
2688}
2689
2690fn open_system_menu_fallback(ctx: &mut CommandContext) {
2691 ctx.globals.syscom.fallback_origin = None;
2692 let mut buttons = Vec::new();
2693 if fallback_feature_available(&ctx.globals.syscom.save_feature) && local_save_available(ctx) {
2694 buttons.push(fallback_button("セーブ / Save", 0));
2695 }
2696 if fallback_feature_available(&ctx.globals.syscom.load_feature) {
2697 buttons.push(fallback_button("ロード / Load", 1));
2698 }
2699 buttons.push(fallback_button("コンフィグ / Config", 2));
2700 if fallback_feature_available(&ctx.globals.syscom.msg_back) {
2701 buttons.push(fallback_button("バックログ / Backlog", 3));
2702 }
2703 if fallback_feature_available(&ctx.globals.syscom.hide_mwnd) {
2704 let label = if ctx.globals.syscom.hide_mwnd.onoff {
2705 "メッセージウィンドウを表示 / Show Message"
2706 } else {
2707 "メッセージウィンドウを隠す / Hide Message"
2708 };
2709 buttons.push(fallback_button(label, 4));
2710 }
2711 if fallback_feature_available(&ctx.globals.syscom.return_to_menu) {
2712 buttons.push(fallback_button("タイトルへ戻る / Return to Title", 5));
2713 }
2714 if fallback_feature_available(&ctx.globals.syscom.end_game) {
2715 buttons.push(fallback_button("ゲーム終了 / Quit", 6));
2716 }
2717 buttons.push(fallback_button("閉じる / Close", FALLBACK_CLOSE));
2718
2719 request_fallback_dialog(
2720 ctx,
2721 SyscomFallbackDialogKind::SystemMenu,
2722 0,
2723 None,
2724 "SYSTEM MENU",
2725 String::new(),
2726 buttons,
2727 );
2728}
2729
2730fn slot_fallback_label(slot_no: usize, slot: Option<&SaveSlotState>) -> String {
2731 let display_no = slot_no + 1;
2732 let Some(slot) = slot.filter(|slot| slot.exist) else {
2733 return format!("{display_no:02}: -- EMPTY --");
2734 };
2735 let title = if slot.title.trim().is_empty() {
2736 if slot.message.trim().is_empty() {
2737 "SAVE DATA"
2738 } else {
2739 slot.message.trim()
2740 }
2741 } else {
2742 slot.title.trim()
2743 };
2744 let title: String = title.chars().take(24).collect();
2745 format!(
2746 "{display_no:02}: {title} {:04}/{:02}/{:02} {:02}:{:02}",
2747 slot.year, slot.month, slot.day, slot.hour, slot.minute
2748 )
2749}
2750
2751fn open_save_load_fallback(
2752 ctx: &mut CommandContext,
2753 save: bool,
2754 page: usize,
2755 return_kind: Option<SyscomFallbackDialogKind>,
2756) {
2757 sync_save_slots_from_disk(ctx, false);
2758 let count = configured_save_count(ctx, false);
2759 if count == 0 {
2760 open_fallback_notice(ctx, "No save slots are configured.", return_kind);
2761 return;
2762 }
2763 let page_count = count.div_ceil(FALLBACK_SLOTS_PER_PAGE).max(1);
2764 let page = page.min(page_count - 1);
2765 let start = page * FALLBACK_SLOTS_PER_PAGE;
2766 let end = (start + FALLBACK_SLOTS_PER_PAGE).min(count);
2767 let mut buttons = Vec::new();
2768 for idx in start..end {
2769 let slot = ctx.globals.syscom.save_slots.get(idx);
2770 buttons.push(fallback_button(slot_fallback_label(idx, slot), idx as i64));
2771 }
2772 if page > 0 {
2773 buttons.push(fallback_button("◀ Previous", FALLBACK_PREV));
2774 }
2775 if page + 1 < page_count {
2776 buttons.push(fallback_button("Next ▶", FALLBACK_NEXT));
2777 }
2778 buttons.push(fallback_button("戻る / Back", FALLBACK_BACK));
2779
2780 let body = if save && !local_save_available(ctx) {
2781 "A SAVEPOINT snapshot is not available yet.\nSaving is disabled until the script reaches SAVEPOINT."
2782 .to_string()
2783 } else {
2784 format!("Page {}/{}", page + 1, page_count)
2785 };
2786 request_fallback_dialog(
2787 ctx,
2788 if save {
2789 SyscomFallbackDialogKind::SaveMenu
2790 } else {
2791 SyscomFallbackDialogKind::LoadMenu
2792 },
2793 page,
2794 return_kind,
2795 if save { "SAVE" } else { "LOAD" },
2796 body,
2797 buttons,
2798 );
2799}
2800
2801fn open_config_root_fallback(
2802 ctx: &mut CommandContext,
2803 return_kind: Option<SyscomFallbackDialogKind>,
2804) {
2805 request_fallback_dialog(
2806 ctx,
2807 SyscomFallbackDialogKind::ConfigRoot,
2808 0,
2809 return_kind,
2810 "CONFIG",
2811 "Settings are applied immediately and written to config.sav when this menu closes."
2812 .to_string(),
2813 vec![
2814 fallback_button("画面 / Display", 0),
2815 fallback_button("音量 / Volume", 1),
2816 fallback_button("メッセージ / Message", 2),
2817 fallback_button("オートモード / Auto Mode", 3),
2818 fallback_button("フォント / Font", 4),
2819 fallback_button("その他 / Other", 5),
2820 fallback_button("設定を保存して戻る / Save & Back", FALLBACK_BACK),
2821 ],
2822 );
2823}
2824
2825fn open_config_window_fallback(ctx: &mut CommandContext) {
2826 let mode = cfg_get_int(&ctx.globals.syscom, GET_WINDOW_MODE, 0).clamp(0, 1);
2827 let size = cfg_get_int(&ctx.globals.syscom, GET_WINDOW_MODE_SIZE, 100).max(1);
2828 request_fallback_dialog(
2829 ctx,
2830 SyscomFallbackDialogKind::ConfigWindow,
2831 0,
2832 Some(SyscomFallbackDialogKind::ConfigRoot),
2833 "CONFIG - DISPLAY",
2834 format!(
2835 "Window mode: {}\nWindow scale: {}%",
2836 if mode == 0 { "Windowed" } else { "Fullscreen" },
2837 size
2838 ),
2839 vec![
2840 fallback_button("ウィンドウ/フルスクリーン切替", 0),
2841 fallback_button("ウィンドウサイズ変更", 1),
2842 fallback_button("戻る / Back", FALLBACK_BACK),
2843 ],
2844 );
2845}
2846
2847fn fallback_sound_name(sound_type: usize) -> &'static str {
2848 match sound_type {
2849 FALLBACK_ALL_SOUND => "ALL",
2850 0 => "BGM",
2851 1 => "VOICE",
2852 2 => "PCM",
2853 3 => "SE",
2854 4 => "MOVIE",
2855 _ => "SOUND",
2856 }
2857}
2858
2859fn fallback_sound_volume(ctx: &CommandContext, sound_type: usize) -> i64 {
2860 if sound_type == FALLBACK_ALL_SOUND {
2861 cfg_get_int(&ctx.globals.syscom, GET_ALL_VOLUME, 255).clamp(0, 255)
2862 } else {
2863 get_sound_volume_by_type(ctx, sound_type)
2864 }
2865}
2866
2867fn fallback_sound_onoff(ctx: &CommandContext, sound_type: usize) -> bool {
2868 if sound_type == FALLBACK_ALL_SOUND {
2869 cfg_get_int(&ctx.globals.syscom, GET_ALL_ONOFF, 1) != 0
2870 } else {
2871 get_sound_onoff_by_type(ctx, sound_type)
2872 }
2873}
2874
2875fn set_fallback_sound_volume(ctx: &mut CommandContext, sound_type: usize, value: i64) {
2876 if sound_type == FALLBACK_ALL_SOUND {
2877 cfg_set_int(&mut ctx.globals.syscom, GET_ALL_VOLUME, value.clamp(0, 255));
2878 } else {
2879 set_sound_volume_by_type(ctx, sound_type, value);
2880 }
2881 apply_audio_config(ctx);
2882}
2883
2884fn set_fallback_sound_onoff(ctx: &mut CommandContext, sound_type: usize, value: bool) {
2885 if sound_type == FALLBACK_ALL_SOUND {
2886 cfg_set_int(
2887 &mut ctx.globals.syscom,
2888 GET_ALL_ONOFF,
2889 if value { 1 } else { 0 },
2890 );
2891 } else {
2892 set_sound_onoff_by_type(ctx, sound_type, value);
2893 }
2894 apply_audio_config(ctx);
2895}
2896
2897fn open_config_volume_root_fallback(ctx: &mut CommandContext) {
2898 request_fallback_dialog(
2899 ctx,
2900 SyscomFallbackDialogKind::ConfigVolumeRoot,
2901 0,
2902 Some(SyscomFallbackDialogKind::ConfigRoot),
2903 "CONFIG - VOLUME",
2904 String::new(),
2905 vec![
2906 fallback_button("Master / All", 0),
2907 fallback_button("BGM", 1),
2908 fallback_button("Voice", 2),
2909 fallback_button("PCM", 3),
2910 fallback_button("SE", 4),
2911 fallback_button("Movie", 5),
2912 fallback_button("戻る / Back", FALLBACK_BACK),
2913 ],
2914 );
2915}
2916
2917fn open_config_volume_fallback(ctx: &mut CommandContext, sound_type: usize) {
2918 let volume = fallback_sound_volume(ctx, sound_type);
2919 let onoff = fallback_sound_onoff(ctx, sound_type);
2920 let name = fallback_sound_name(sound_type);
2921 request_fallback_dialog(
2922 ctx,
2923 SyscomFallbackDialogKind::ConfigVolume(sound_type),
2924 0,
2925 Some(SyscomFallbackDialogKind::ConfigVolumeRoot),
2926 &format!("CONFIG - {name}"),
2927 format!(
2928 "Output: {}\nVolume: {}% ({volume}/255)",
2929 fallback_onoff(onoff),
2930 raw_volume_percent(volume)
2931 ),
2932 vec![
2933 fallback_button("ON/OFF", 0),
2934 fallback_button("Volume 0%", 1),
2935 fallback_button("Volume 25%", 2),
2936 fallback_button("Volume 50%", 3),
2937 fallback_button("Volume 75%", 4),
2938 fallback_button("Volume 100%", 5),
2939 fallback_button("戻る / Back", FALLBACK_BACK),
2940 ],
2941 );
2942}
2943
2944fn open_config_message_fallback(ctx: &mut CommandContext) {
2945 let speed = cfg_get_int(&ctx.globals.syscom, GET_MESSAGE_SPEED, 20).clamp(0, 100);
2946 let nowait = ctx.globals.script.msg_nowait
2947 || cfg_get_int(&ctx.globals.syscom, GET_MESSAGE_NOWAIT, 0) != 0;
2948 request_fallback_dialog(
2949 ctx,
2950 SyscomFallbackDialogKind::ConfigMessage,
2951 0,
2952 Some(SyscomFallbackDialogKind::ConfigRoot),
2953 "CONFIG - MESSAGE",
2954 format!(
2955 "Message speed: {speed}\nInstant message: {}",
2956 fallback_onoff(nowait)
2957 ),
2958 vec![
2959 fallback_button("メッセージ速度 / Message Speed", 0),
2960 fallback_button("瞬間表示 / Instant", 1),
2961 fallback_button("戻る / Back", FALLBACK_BACK),
2962 ],
2963 );
2964}
2965
2966fn open_config_auto_fallback(ctx: &mut CommandContext) {
2967 let moji_wait = ctx.globals.script.auto_mode_moji_wait.clamp(0, 500);
2968 let min_wait = ctx.globals.script.auto_mode_min_wait.clamp(0, 5000);
2969 request_fallback_dialog(
2970 ctx,
2971 SyscomFallbackDialogKind::ConfigAuto,
2972 0,
2973 Some(SyscomFallbackDialogKind::ConfigRoot),
2974 "CONFIG - AUTO MODE",
2975 format!("Per-character wait: {moji_wait} ms\nMinimum wait: {min_wait} ms"),
2976 vec![
2977 fallback_button("文字待ち時間 / Character Wait", 0),
2978 fallback_button("最低待ち時間 / Minimum Wait", 1),
2979 fallback_button("戻る / Back", FALLBACK_BACK),
2980 ],
2981 );
2982}
2983
2984fn fallback_font_names(ctx: &mut CommandContext) -> Vec<String> {
2985 if ctx.globals.syscom.font_list.is_empty() {
2986 ctx.globals.syscom.font_list = vec![
2987 "MS ゴシック".to_string(),
2988 "MS 明朝".to_string(),
2989 "メイリオ".to_string(),
2990 ];
2991 }
2992 let current = {
2993 let value = cfg_get_str(&ctx.globals.syscom, GET_FONT_NAME);
2994 if value.is_empty() {
2995 config_default_font_name(ctx)
2996 } else {
2997 value
2998 }
2999 };
3000 let mut names = ctx.globals.syscom.font_list.clone();
3001 if !names.iter().any(|name| name.eq_ignore_ascii_case(¤t)) {
3002 names.insert(0, current);
3003 }
3004 names
3005}
3006
3007fn open_config_font_fallback(ctx: &mut CommandContext) {
3008 let current = {
3009 let value = cfg_get_str(&ctx.globals.syscom, GET_FONT_NAME);
3010 if value.is_empty() {
3011 config_default_font_name(ctx)
3012 } else {
3013 value
3014 }
3015 };
3016 let bold = cfg_get_int(&ctx.globals.syscom, GET_FONT_BOLD, 0) != 0;
3017 let decoration = cfg_get_int(&ctx.globals.syscom, GET_FONT_DECORATION, 2);
3018 request_fallback_dialog(
3019 ctx,
3020 SyscomFallbackDialogKind::ConfigFont,
3021 0,
3022 Some(SyscomFallbackDialogKind::ConfigRoot),
3023 "CONFIG - FONT",
3024 format!(
3025 "Font face: {current}\nBold: {}\nDecoration: {decoration}",
3026 fallback_onoff(bold)
3027 ),
3028 vec![
3029 fallback_button("フォント切替 / Font Face", 0),
3030 fallback_button("太字 / Bold", 1),
3031 fallback_button("装飾 / Decoration", 2),
3032 fallback_button("戻る / Back", FALLBACK_BACK),
3033 ],
3034 );
3035}
3036
3037fn open_config_other_fallback(ctx: &mut CommandContext) {
3038 let wheel = cfg_get_int(&ctx.globals.syscom, GET_WHEEL_NEXT_MESSAGE_ONOFF, 1) != 0;
3039 let unread = cfg_get_int(&ctx.globals.syscom, GET_SKIP_UNREAD_MESSAGE_ONOFF, 0) != 0;
3040 let mouse_hide = cfg_get_int(&ctx.globals.syscom, GET_MOUSE_CURSOR_HIDE_ONOFF, 0) != 0;
3041 let no_wipe = cfg_get_int(&ctx.globals.syscom, GET_NO_WIPE_ANIME_ONOFF, 0) != 0;
3042 let no_mwnd = cfg_get_int(&ctx.globals.syscom, GET_NO_MWND_ANIME_ONOFF, 0) != 0;
3043 let alert = cfg_get_int(&ctx.globals.syscom, GET_SAVELOAD_ALERT_ONOFF, 1) != 0;
3044 let swap_ab = ctx.globals.syscom.original_config.joypad_swap_ab;
3045 let swap_xy = ctx.globals.syscom.original_config.joypad_swap_xy;
3046 request_fallback_dialog(
3047 ctx,
3048 SyscomFallbackDialogKind::ConfigOther,
3049 0,
3050 Some(SyscomFallbackDialogKind::ConfigRoot),
3051 "CONFIG - OTHER",
3052 format!(
3053 "Wheel advances message: {}\nSkip unread message: {}\nAuto-hide cursor: {}\nDisable wipe animation: {}\nDisable window animation: {}\nSave/load confirmation: {}\nSwap controller A/B: {}\nSwap controller X/Y: {}",
3054 fallback_onoff(wheel),
3055 fallback_onoff(unread),
3056 fallback_onoff(mouse_hide),
3057 fallback_onoff(no_wipe),
3058 fallback_onoff(no_mwnd),
3059 fallback_onoff(alert),
3060 fallback_onoff(swap_ab),
3061 fallback_onoff(swap_xy),
3062 ),
3063 vec![
3064 fallback_button("Mouse wheel message", 0),
3065 fallback_button("Skip unread message", 1),
3066 fallback_button("Auto-hide cursor", 2),
3067 fallback_button("Wipe animation", 3),
3068 fallback_button("Message-window animation", 4),
3069 fallback_button("Save/load confirmation", 5),
3070 fallback_button("Controller A/B swap", 6),
3071 fallback_button("Controller X/Y swap", 7),
3072 fallback_button("戻る / Back", FALLBACK_BACK),
3073 ],
3074 );
3075}
3076
3077fn reopen_fallback_kind(
3078 ctx: &mut CommandContext,
3079 kind: SyscomFallbackDialogKind,
3080 return_kind: Option<SyscomFallbackDialogKind>,
3081) {
3082 match kind {
3083 SyscomFallbackDialogKind::SystemMenu => open_system_menu_fallback(ctx),
3084 SyscomFallbackDialogKind::SaveMenu => open_save_load_fallback(ctx, true, 0, return_kind),
3085 SyscomFallbackDialogKind::LoadMenu => open_save_load_fallback(ctx, false, 0, return_kind),
3086 SyscomFallbackDialogKind::ConfigRoot => open_config_root_fallback(ctx, return_kind),
3087 SyscomFallbackDialogKind::ConfigWindow => open_config_window_fallback(ctx),
3088 SyscomFallbackDialogKind::ConfigVolumeRoot => open_config_volume_root_fallback(ctx),
3089 SyscomFallbackDialogKind::ConfigVolume(sound_type) => {
3090 open_config_volume_fallback(ctx, sound_type)
3091 }
3092 SyscomFallbackDialogKind::ConfigMessage => open_config_message_fallback(ctx),
3093 SyscomFallbackDialogKind::ConfigAuto => open_config_auto_fallback(ctx),
3094 SyscomFallbackDialogKind::ConfigFont => open_config_font_fallback(ctx),
3095 SyscomFallbackDialogKind::ConfigOther => open_config_other_fallback(ctx),
3096 SyscomFallbackDialogKind::Notice => close_fallback_dialog(ctx, false),
3097 }
3098}
3099
3100pub fn open_fallback_dialog(ctx: &mut CommandContext, kind: SyscomPendingProcKind) {
3104 ctx.globals.system.messagebox_modal_result = None;
3105 ctx.globals.syscom.fallback_origin = None;
3106 match kind {
3107 SyscomPendingProcKind::OpenSyscomMenu => open_system_menu_fallback(ctx),
3108 SyscomPendingProcKind::OpenSave => {
3109 prepare_runtime_save_thumb_capture_with_priority(ctx, CAPTURE_PRIOR_SAVE);
3110 open_save_load_fallback(ctx, true, 0, None);
3111 }
3112 SyscomPendingProcKind::OpenLoad => {
3113 open_save_load_fallback(ctx, false, 0, None);
3114 }
3115 SyscomPendingProcKind::OpenConfig | SyscomPendingProcKind::OpenConfigDialog => {
3116 open_config_root_fallback(ctx, None)
3117 }
3118 _ => {
3119 log::error!("unsupported Syscom fallback request: {kind:?}");
3120 }
3121 }
3122}
3123
3124fn queue_fallback_pending_proc(
3125 ctx: &mut CommandContext,
3126 kind: SyscomPendingProcKind,
3127 warning: bool,
3128 save_id: i64,
3129) {
3130 ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
3131 kind,
3132 warning,
3133 se_play: false,
3134 fade_out: false,
3135 leave_msgbk: false,
3136 save_id,
3137 });
3138 ctx.globals.syscom.fallback_dialog = None;
3139 ctx.globals.syscom.fallback_origin = None;
3140}
3141
3142fn cycle_i64(current: i64, values: &[i64]) -> i64 {
3143 let index = values.iter().position(|value| *value == current);
3144 values[index.map_or(0, |index| (index + 1) % values.len())]
3145}
3146
3147pub(crate) fn poll_fallback_dialog(ctx: &mut CommandContext) {
3150 let Some(result) = ctx.globals.system.messagebox_modal_result.take() else {
3151 return;
3152 };
3153 let Some(state) = ctx.globals.syscom.fallback_dialog.take() else {
3154 ctx.globals.system.messagebox_modal_result = Some(result);
3156 return;
3157 };
3158 if !state.awaiting_result {
3159 ctx.globals.system.messagebox_modal_result = Some(result);
3160 ctx.globals.syscom.fallback_dialog = Some(state);
3161 return;
3162 }
3163
3164 match state.kind {
3165 SyscomFallbackDialogKind::Notice => {
3166 if let Some(kind) = state.return_kind {
3167 let origin = ctx.globals.syscom.fallback_origin;
3168 match kind {
3169 SyscomFallbackDialogKind::SaveMenu => {
3170 open_save_load_fallback(ctx, true, 0, origin)
3171 }
3172 SyscomFallbackDialogKind::LoadMenu => {
3173 open_save_load_fallback(ctx, false, 0, origin)
3174 }
3175 _ => reopen_fallback_kind(ctx, kind, origin),
3176 }
3177 } else {
3178 close_fallback_dialog(ctx, false);
3179 }
3180 }
3181 SyscomFallbackDialogKind::SystemMenu => match result {
3182 0 => {
3183 ctx.globals.syscom.fallback_origin = Some(SyscomFallbackDialogKind::SystemMenu);
3184 open_save_load_fallback(ctx, true, 0, Some(SyscomFallbackDialogKind::SystemMenu))
3185 }
3186 1 => {
3187 ctx.globals.syscom.fallback_origin = Some(SyscomFallbackDialogKind::SystemMenu);
3188 open_save_load_fallback(ctx, false, 0, Some(SyscomFallbackDialogKind::SystemMenu))
3189 }
3190 2 => {
3191 ctx.globals.syscom.fallback_origin = Some(SyscomFallbackDialogKind::SystemMenu);
3192 open_config_root_fallback(ctx, Some(SyscomFallbackDialogKind::SystemMenu))
3193 }
3194 3 => {
3195 ctx.globals.syscom.msg_back_open = true;
3196 queue_fallback_pending_proc(ctx, SyscomPendingProcKind::MsgBack, false, 0);
3197 }
3198 4 => {
3199 ctx.globals.syscom.hide_mwnd.onoff = !ctx.globals.syscom.hide_mwnd.onoff;
3200 open_system_menu_fallback(ctx);
3201 }
3202 5 => queue_fallback_pending_proc(ctx, SyscomPendingProcKind::ReturnToMenu, true, 0),
3203 6 => queue_fallback_pending_proc(ctx, SyscomPendingProcKind::EndGame, true, 0),
3204 _ => close_fallback_dialog(ctx, false),
3205 },
3206 SyscomFallbackDialogKind::SaveMenu | SyscomFallbackDialogKind::LoadMenu => {
3207 let save = state.kind == SyscomFallbackDialogKind::SaveMenu;
3208 match result {
3209 FALLBACK_PREV => open_save_load_fallback(
3210 ctx,
3211 save,
3212 state.page.saturating_sub(1),
3213 state.return_kind,
3214 ),
3215 FALLBACK_NEXT => {
3216 open_save_load_fallback(ctx, save, state.page + 1, state.return_kind)
3217 }
3218 FALLBACK_BACK | FALLBACK_CLOSE => {
3219 if save {
3220 free_runtime_save_thumb_capture(ctx, CAPTURE_PRIOR_SAVE);
3221 }
3222 if let Some(kind) = state.return_kind.or(ctx.globals.syscom.fallback_origin) {
3223 reopen_fallback_kind(ctx, kind, None);
3224 } else {
3225 close_fallback_dialog(ctx, false);
3226 }
3227 }
3228 slot @ 0.. => {
3229 let idx = slot as usize;
3230 if idx >= configured_save_count(ctx, false) {
3231 open_save_load_fallback(ctx, save, state.page, state.return_kind);
3232 return;
3233 }
3234 if save {
3235 if !local_save_available(ctx) {
3236 open_fallback_notice(
3237 ctx,
3238 "SAVEPOINT data is not available, so this slot cannot be saved.",
3239 Some(SyscomFallbackDialogKind::SaveMenu),
3240 );
3241 } else {
3242 let queued = request_confirmed_save_or_load(
3243 ctx,
3244 SyscomPendingProcKind::Save,
3245 idx,
3246 true,
3247 true,
3248 );
3249 if queued {
3250 ctx.globals.syscom.fallback_origin = None;
3251 } else {
3252 menu_save_slot(ctx, false, idx);
3253 write_global_save(ctx);
3254 ctx.globals.syscom.fallback_dialog = None;
3255 ctx.globals.syscom.fallback_origin = None;
3256 }
3257 }
3258 } else if local_save_file_exists(ctx, SaveKind::Normal, idx) {
3259 let queued = request_confirmed_save_or_load(
3260 ctx,
3261 SyscomPendingProcKind::Load,
3262 idx,
3263 true,
3264 false,
3265 );
3266 if queued {
3267 ctx.globals.syscom.fallback_origin = None;
3268 } else {
3269 menu_load_slot(ctx, false, idx);
3270 ctx.globals.syscom.fallback_dialog = None;
3271 ctx.globals.syscom.fallback_origin = None;
3272 }
3273 } else {
3274 open_fallback_notice(
3275 ctx,
3276 "The selected save slot is empty.",
3277 Some(SyscomFallbackDialogKind::LoadMenu),
3278 );
3279 }
3280 }
3281 _ => open_save_load_fallback(ctx, save, state.page, state.return_kind),
3282 }
3283 }
3284 SyscomFallbackDialogKind::ConfigRoot => match result {
3285 0 => open_config_window_fallback(ctx),
3286 1 => open_config_volume_root_fallback(ctx),
3287 2 => open_config_message_fallback(ctx),
3288 3 => open_config_auto_fallback(ctx),
3289 4 => open_config_font_fallback(ctx),
3290 5 => open_config_other_fallback(ctx),
3291 _ => {
3292 apply_audio_config(ctx);
3293 write_config_save(ctx);
3294 if let Some(kind) = state.return_kind.or(ctx.globals.syscom.fallback_origin) {
3295 reopen_fallback_kind(ctx, kind, None);
3296 } else {
3297 close_fallback_dialog(ctx, false);
3298 }
3299 }
3300 },
3301 SyscomFallbackDialogKind::ConfigWindow => match result {
3302 0 => {
3303 let next = if cfg_get_int(&ctx.globals.syscom, GET_WINDOW_MODE, 0) == 0 {
3304 1
3305 } else {
3306 0
3307 };
3308 cfg_set_int(&mut ctx.globals.syscom, GET_WINDOW_MODE, next);
3309 open_config_window_fallback(ctx);
3310 }
3311 1 => {
3312 const SIZES: &[i64] = &[50, 75, 100, 150, 200];
3313 let current = cfg_get_int(&ctx.globals.syscom, GET_WINDOW_MODE_SIZE, 100);
3314 let next = cycle_i64(current, SIZES);
3315 cfg_set_int(&mut ctx.globals.syscom, GET_WINDOW_MODE_SIZE, next);
3316 open_config_window_fallback(ctx);
3317 }
3318 _ => {
3319 let origin = ctx.globals.syscom.fallback_origin;
3320 open_config_root_fallback(ctx, origin)
3321 }
3322 },
3323 SyscomFallbackDialogKind::ConfigVolumeRoot => match result {
3324 0 => open_config_volume_fallback(ctx, FALLBACK_ALL_SOUND),
3325 1 => open_config_volume_fallback(ctx, 0),
3326 2 => open_config_volume_fallback(ctx, 1),
3327 3 => open_config_volume_fallback(ctx, 2),
3328 4 => open_config_volume_fallback(ctx, 3),
3329 5 => open_config_volume_fallback(ctx, 4),
3330 _ => {
3331 let origin = ctx.globals.syscom.fallback_origin;
3332 open_config_root_fallback(ctx, origin)
3333 }
3334 },
3335 SyscomFallbackDialogKind::ConfigVolume(sound_type) => match result {
3336 0 => {
3337 let next = !fallback_sound_onoff(ctx, sound_type);
3338 set_fallback_sound_onoff(ctx, sound_type, next);
3339 open_config_volume_fallback(ctx, sound_type);
3340 }
3341 1..=5 => {
3342 const RAW: &[i64] = &[0, 64, 128, 191, 255];
3343 set_fallback_sound_volume(ctx, sound_type, RAW[(result - 1) as usize]);
3344 open_config_volume_fallback(ctx, sound_type);
3345 }
3346 _ => open_config_volume_root_fallback(ctx),
3347 },
3348 SyscomFallbackDialogKind::ConfigMessage => match result {
3349 0 => {
3350 const SPEEDS: &[i64] = &[0, 10, 20, 40, 60, 80, 100];
3351 let current = cfg_get_int(&ctx.globals.syscom, GET_MESSAGE_SPEED, 20);
3352 let next = cycle_i64(current, SPEEDS);
3353 cfg_set_int(&mut ctx.globals.syscom, GET_MESSAGE_SPEED, next);
3354 open_config_message_fallback(ctx);
3355 }
3356 1 => {
3357 let next = !(ctx.globals.script.msg_nowait
3358 || cfg_get_int(&ctx.globals.syscom, GET_MESSAGE_NOWAIT, 0) != 0);
3359 ctx.globals.script.msg_nowait = next;
3360 cfg_set_int(
3361 &mut ctx.globals.syscom,
3362 GET_MESSAGE_NOWAIT,
3363 if next { 1 } else { 0 },
3364 );
3365 open_config_message_fallback(ctx);
3366 }
3367 _ => {
3368 let origin = ctx.globals.syscom.fallback_origin;
3369 open_config_root_fallback(ctx, origin)
3370 }
3371 },
3372 SyscomFallbackDialogKind::ConfigAuto => match result {
3373 0 => {
3374 const WAITS: &[i64] = &[0, 20, 40, 70, 100, 150, 250, 500];
3375 let next = cycle_i64(ctx.globals.script.auto_mode_moji_wait, WAITS);
3376 ctx.globals.script.auto_mode_moji_wait = next;
3377 cfg_set_int(&mut ctx.globals.syscom, GET_AUTO_MODE_MOJI_WAIT, next);
3378 open_config_auto_fallback(ctx);
3379 }
3380 1 => {
3381 const WAITS: &[i64] = &[0, 100, 200, 300, 500, 750, 1000, 1500, 2000, 3000, 5000];
3382 let next = cycle_i64(ctx.globals.script.auto_mode_min_wait, WAITS);
3383 ctx.globals.script.auto_mode_min_wait = next;
3384 cfg_set_int(&mut ctx.globals.syscom, GET_AUTO_MODE_MIN_WAIT, next);
3385 open_config_auto_fallback(ctx);
3386 }
3387 _ => {
3388 let origin = ctx.globals.syscom.fallback_origin;
3389 open_config_root_fallback(ctx, origin)
3390 }
3391 },
3392 SyscomFallbackDialogKind::ConfigFont => match result {
3393 0 => {
3394 let names = fallback_font_names(ctx);
3395 let current = cfg_get_str(&ctx.globals.syscom, GET_FONT_NAME);
3396 let index = names
3397 .iter()
3398 .position(|name| name.eq_ignore_ascii_case(¤t))
3399 .unwrap_or(0);
3400 let next = names[(index + 1) % names.len()].clone();
3401 cfg_set_str(&mut ctx.globals.syscom, GET_FONT_NAME, next);
3402 open_config_font_fallback(ctx);
3403 }
3404 1 => {
3405 let next = cfg_get_int(&ctx.globals.syscom, GET_FONT_BOLD, 0) == 0;
3406 cfg_set_int(
3407 &mut ctx.globals.syscom,
3408 GET_FONT_BOLD,
3409 if next { 1 } else { 0 },
3410 );
3411 open_config_font_fallback(ctx);
3412 }
3413 2 => {
3414 let current = cfg_get_int(&ctx.globals.syscom, GET_FONT_DECORATION, 2);
3415 cfg_set_int(
3416 &mut ctx.globals.syscom,
3417 GET_FONT_DECORATION,
3418 (current + 1).rem_euclid(4),
3419 );
3420 open_config_font_fallback(ctx);
3421 }
3422 _ => {
3423 let origin = ctx.globals.syscom.fallback_origin;
3424 open_config_root_fallback(ctx, origin)
3425 }
3426 },
3427 SyscomFallbackDialogKind::ConfigOther => {
3428 let key = match result {
3429 0 => Some(GET_WHEEL_NEXT_MESSAGE_ONOFF),
3430 1 => Some(GET_SKIP_UNREAD_MESSAGE_ONOFF),
3431 2 => Some(GET_MOUSE_CURSOR_HIDE_ONOFF),
3432 3 => Some(GET_NO_WIPE_ANIME_ONOFF),
3433 4 => Some(GET_NO_MWND_ANIME_ONOFF),
3434 5 => Some(GET_SAVELOAD_ALERT_ONOFF),
3435 _ => None,
3436 };
3437 if let Some(key) = key {
3438 let current = cfg_get_int(
3439 &ctx.globals.syscom,
3440 key,
3441 if key == GET_WHEEL_NEXT_MESSAGE_ONOFF || key == GET_SAVELOAD_ALERT_ONOFF {
3442 1
3443 } else {
3444 0
3445 },
3446 );
3447 cfg_set_int(
3448 &mut ctx.globals.syscom,
3449 key,
3450 if current == 0 { 1 } else { 0 },
3451 );
3452 open_config_other_fallback(ctx);
3453 } else if result == 6 {
3454 ctx.globals.syscom.original_config.joypad_swap_ab =
3455 !ctx.globals.syscom.original_config.joypad_swap_ab;
3456 open_config_other_fallback(ctx);
3457 } else if result == 7 {
3458 ctx.globals.syscom.original_config.joypad_swap_xy =
3459 !ctx.globals.syscom.original_config.joypad_swap_xy;
3460 open_config_other_fallback(ctx);
3461 } else {
3462 let origin = ctx.globals.syscom.fallback_origin;
3463 open_config_root_fallback(ctx, origin);
3464 }
3465 }
3466 }
3467}
3468
3469fn save_header_is_older(lhs: &SaveSlotState, rhs: &SaveSlotState) -> bool {
3470 macro_rules! compare_field {
3471 ($field:ident) => {
3472 if lhs.$field < rhs.$field {
3473 return true;
3474 }
3475 if lhs.$field > rhs.$field {
3476 return false;
3477 }
3478 };
3479 }
3480
3481 compare_field!(year);
3485 compare_field!(month);
3486 compare_field!(day);
3487 compare_field!(hour);
3488 compare_field!(minute);
3489 compare_field!(second);
3490 compare_field!(millisecond);
3491 lhs.title <= rhs.title
3492}
3493
3494fn newest_slot_in_range(
3495 slots: &[SaveSlotState],
3496 start: i64,
3497 cnt: i64,
3498 configured_count: usize,
3499) -> i64 {
3500 if cnt <= 0 || configured_count == 0 {
3501 return -1;
3502 }
3503
3504 let end = start.saturating_add(cnt);
3508 let lo = start.max(0).min(configured_count as i64);
3509 let hi = end.max(0).min(configured_count as i64);
3510 if hi <= lo {
3511 return -1;
3512 }
3513
3514 let mut newest_idx = -1i64;
3515 let mut newest_slot: Option<&SaveSlotState> = None;
3516 for idx in lo as usize..hi as usize {
3517 let Some(slot) = slots.get(idx) else {
3518 continue;
3519 };
3520 if !slot.exist {
3521 continue;
3522 }
3523 if newest_slot
3524 .map(|current| save_header_is_older(current, slot))
3525 .unwrap_or(true)
3526 {
3527 newest_idx = idx as i64;
3528 newest_slot = Some(slot);
3529 }
3530 }
3531 newest_idx
3532}
3533
3534fn slot_i64(slot: &SaveSlotState, op: i32) -> i64 {
3535 match op {
3536 GET_SAVE_EXIST | GET_QUICK_SAVE_EXIST => {
3537 if slot.exist {
3538 1
3539 } else {
3540 0
3541 }
3542 }
3543 GET_SAVE_YEAR | GET_QUICK_SAVE_YEAR => slot.year,
3544 GET_SAVE_MONTH | GET_QUICK_SAVE_MONTH => slot.month,
3545 GET_SAVE_DAY | GET_QUICK_SAVE_DAY => slot.day,
3546 GET_SAVE_WEEKDAY | GET_QUICK_SAVE_WEEKDAY => slot.weekday,
3547 GET_SAVE_HOUR | GET_QUICK_SAVE_HOUR => slot.hour,
3548 GET_SAVE_MINUTE | GET_QUICK_SAVE_MINUTE => slot.minute,
3549 GET_SAVE_SECOND | GET_QUICK_SAVE_SECOND => slot.second,
3550 GET_SAVE_MILLISECOND | GET_QUICK_SAVE_MILLISECOND => slot.millisecond,
3551 _ => 0,
3552 }
3553}
3554
3555fn slot_str(slot: &SaveSlotState, op: i32) -> String {
3556 match op {
3557 GET_SAVE_TITLE | GET_QUICK_SAVE_TITLE => slot.title.clone(),
3558 GET_SAVE_MESSAGE | GET_QUICK_SAVE_MESSAGE => {
3559 if slot.message.is_empty() {
3563 slot.full_message.clone()
3564 } else {
3565 slot.message.clone()
3566 }
3567 }
3568 GET_SAVE_FULL_MESSAGE | GET_QUICK_SAVE_FULL_MESSAGE => slot.full_message.clone(),
3569 GET_SAVE_COMMENT | GET_QUICK_SAVE_COMMENT => slot.comment.clone(),
3570 GET_SAVE_APPEND_DIR | GET_QUICK_SAVE_APPEND_DIR => slot.append_dir.clone(),
3571 GET_SAVE_APPEND_NAME | GET_QUICK_SAVE_APPEND_NAME => slot.append_name.clone(),
3572 _ => String::new(),
3573 }
3574}
3575
3576fn cfg_get_int(st: &crate::runtime::globals::SyscomRuntimeState, key: i32, default: i64) -> i64 {
3577 st.config_int.get(&key).copied().unwrap_or(default)
3578}
3579
3580fn cfg_set_int(st: &mut crate::runtime::globals::SyscomRuntimeState, key: i32, value: i64) {
3581 st.config_int.insert(key, value);
3582}
3583
3584fn volume_to_raw(v: i64) -> u8 {
3585 v.clamp(0, 255) as u8
3586}
3587
3588fn standard_sound_volume_key(sound_type: usize) -> Option<i32> {
3589 match sound_type {
3590 0 => Some(GET_BGM_VOLUME),
3591 1 => Some(GET_KOE_VOLUME),
3592 2 => Some(GET_PCM_VOLUME),
3593 3 => Some(GET_SE_VOLUME),
3594 4 => Some(GET_MOV_VOLUME),
3595 _ => None,
3596 }
3597}
3598
3599fn standard_sound_onoff_key(sound_type: usize) -> Option<i32> {
3600 match sound_type {
3601 0 => Some(GET_BGM_ONOFF),
3602 1 => Some(GET_KOE_ONOFF),
3603 2 => Some(GET_PCM_ONOFF),
3604 3 => Some(GET_SE_ONOFF),
3605 4 => Some(GET_MOV_ONOFF),
3606 _ => None,
3607 }
3608}
3609
3610fn set_sound_volume_by_type(ctx: &mut CommandContext, sound_type: usize, value: i64) {
3611 if sound_type >= ctx.globals.syscom.original_config.sound_user_volume.len() {
3612 return;
3613 }
3614 let value = value.clamp(0, 255);
3615 ctx.globals.syscom.original_config.sound_user_volume[sound_type] = value;
3616 if let Some(key) = standard_sound_volume_key(sound_type) {
3617 cfg_set_int(&mut ctx.globals.syscom, key, value);
3618 }
3619}
3620
3621fn get_sound_volume_by_type(ctx: &CommandContext, sound_type: usize) -> i64 {
3622 if sound_type >= ctx.globals.syscom.original_config.sound_user_volume.len() {
3623 return 255;
3624 }
3625 standard_sound_volume_key(sound_type)
3626 .map(|key| {
3627 cfg_get_int(
3628 &ctx.globals.syscom,
3629 key,
3630 ctx.globals.syscom.original_config.sound_user_volume[sound_type],
3631 )
3632 })
3633 .unwrap_or(ctx.globals.syscom.original_config.sound_user_volume[sound_type])
3634 .clamp(0, 255)
3635}
3636
3637fn set_sound_onoff_by_type(ctx: &mut CommandContext, sound_type: usize, value: bool) {
3638 if sound_type >= ctx.globals.syscom.original_config.play_sound_check.len() {
3639 return;
3640 }
3641 ctx.globals.syscom.original_config.play_sound_check[sound_type] = value;
3642 if let Some(key) = standard_sound_onoff_key(sound_type) {
3643 cfg_set_int(&mut ctx.globals.syscom, key, if value { 1 } else { 0 });
3644 }
3645}
3646
3647fn get_sound_onoff_by_type(ctx: &CommandContext, sound_type: usize) -> bool {
3648 if sound_type >= ctx.globals.syscom.original_config.play_sound_check.len() {
3649 return true;
3650 }
3651 standard_sound_onoff_key(sound_type)
3652 .map(|key| {
3653 cfg_get_int(
3654 &ctx.globals.syscom,
3655 key,
3656 if ctx.globals.syscom.original_config.play_sound_check[sound_type] {
3657 1
3658 } else {
3659 0
3660 },
3661 ) != 0
3662 })
3663 .unwrap_or(ctx.globals.syscom.original_config.play_sound_check[sound_type])
3664}
3665
3666fn raw_volume_percent(value: i64) -> i64 {
3667 (value.clamp(0, 255) * 100 + 127) / 255
3668}
3669
3670fn mul_raw(lhs: i64, rhs: i64) -> i64 {
3671 lhs.clamp(0, 255) * rhs.clamp(0, 255) / 255
3672}
3673
3674fn linear_limit_i64(time: i64, start_time: i64, start: i64, end_time: i64, end: i64) -> i64 {
3675 if end_time <= start_time {
3676 return if time < start_time { start } else { end };
3677 }
3678 if time <= start_time {
3679 start
3680 } else if time >= end_time {
3681 end
3682 } else {
3683 start + (end - start) * (time - start_time) / (end_time - start_time)
3684 }
3685}
3686
3687fn chrkoe_chara_numbers(ctx: &CommandContext, index: usize) -> Vec<i64> {
3688 let Some(entry) = ctx
3689 .tables
3690 .gameexe
3691 .as_ref()
3692 .and_then(|cfg| cfg.get_indexed_entry("CHRKOE", index))
3693 else {
3694 return Vec::new();
3695 };
3696 let Some(open) = entry.value.find('(') else {
3700 return Vec::new();
3701 };
3702 let close = entry.value.rfind(')').unwrap_or(entry.value.len());
3703 if close <= open {
3704 return Vec::new();
3705 }
3706 parse_i64_list_local(&entry.value[open + 1..close])
3707}
3708
3709fn chrkoe_route(
3710 config: &crate::runtime::globals::OriginalConfigRuntimeState,
3711 chara_lists: &[Vec<i64>],
3712 chara_no: i64,
3713) -> (bool, i64) {
3714 if chara_no < 0 {
3715 return (true, 255);
3716 }
3717 let mut onoff = true;
3718 let mut volume = 255;
3719 for (index, state) in config.chrkoe.iter().enumerate() {
3720 if chara_lists
3721 .get(index)
3722 .is_some_and(|numbers| numbers.contains(&chara_no))
3723 {
3724 if !state.onoff {
3725 onoff = false;
3726 }
3727 volume = mul_raw(volume, state.volume);
3728 }
3729 }
3730 (onoff, volume)
3731}
3732
3733fn pcmch_routed_volume(
3734 config: &crate::runtime::globals::OriginalConfigRuntimeState,
3735 chara_lists: &[Vec<i64>],
3736 state: &crate::runtime::globals::PcmChPersistentState,
3737 all_total: i64,
3738 category_total: &[i64; 5],
3739 bgmfade_total: i64,
3740 bgmfade2_total: i64,
3741) -> u8 {
3742 let volume_type = state.volume_type as i32;
3743 let mut volume = match volume_type {
3744 -1 => all_total,
3745 0..=4 => category_total[volume_type as usize],
3746 16..=31 => {
3747 let index = volume_type as usize;
3748 if config.play_sound_check[index] {
3749 mul_raw(all_total, config.sound_user_volume[index])
3750 } else {
3751 0
3752 }
3753 }
3754 _ => 255,
3755 };
3756
3757 if state.bgm_fade_target_flag {
3758 volume = mul_raw(volume, bgmfade_total);
3759 }
3760 if state.bgm_fade2_target_flag {
3761 volume = mul_raw(volume, bgmfade2_total);
3762 }
3763 let (chara_onoff, chara_volume) = chrkoe_route(config, chara_lists, state.chara_no);
3764 volume = mul_raw(volume, chara_volume);
3765 if !chara_onoff {
3766 volume = 0;
3767 }
3768 volume.clamp(0, 255) as u8
3769}
3770
3771pub(crate) fn update_audio_routing(
3777 ctx: &mut CommandContext,
3778 real_delta_ms: i32,
3779 change_force: bool,
3780) {
3781 use crate::audio::TrackKind;
3782
3783 let config = &ctx.globals.syscom.original_config;
3784 let chrkoe_lists = {
3785 if ctx.globals.sound_routing.chrkoe_chara_lists_cache.len() < config.chrkoe.len() {
3786 let lists = (0..config.chrkoe.len())
3787 .map(|index| chrkoe_chara_numbers(ctx, index))
3788 .collect::<Vec<Vec<i64>>>();
3789 ctx.globals.sound_routing.chrkoe_chara_lists_cache = std::sync::Arc::new(lists);
3790 }
3791 std::sync::Arc::clone(&ctx.globals.sound_routing.chrkoe_chara_lists_cache)
3792 };
3793 let all_total = if config.play_all_sound_check {
3794 config.all_sound_user_volume.clamp(0, 255)
3795 } else {
3796 0
3797 };
3798 let mut category_total = [0i64; 5];
3799 for (index, total) in category_total.iter_mut().enumerate() {
3800 *total = if config.play_sound_check[index] {
3801 mul_raw(all_total, config.sound_user_volume[index])
3802 } else {
3803 0
3804 };
3805 }
3806
3807 let old_fade = ctx.globals.sound_routing.bgmfade_total_volume;
3810 let old_fade2 = ctx.globals.sound_routing.bgmfade2_total_volume;
3811 let mut pcmch_bgm_fade_use = false;
3812 let source_channels: Vec<(usize, crate::runtime::globals::PcmChPersistentState)> = ctx
3813 .globals
3814 .pcmch_persistent
3815 .iter()
3816 .cloned()
3817 .enumerate()
3818 .filter(|(_, state)| state.bgm_fade_source_flag)
3819 .collect();
3820 for (channel, state) in source_channels {
3821 let routed = pcmch_routed_volume(
3822 config,
3823 chrkoe_lists.as_slice(),
3824 &state,
3825 all_total,
3826 &category_total,
3827 old_fade,
3828 old_fade2,
3829 );
3830 let _ = ctx.pcm.set_slot_system_volume_raw(channel, routed);
3831 if routed > 0 && ctx.pcm.is_playing_slot(channel) {
3832 pcmch_bgm_fade_use = true;
3833 }
3834 }
3835
3836 let koe_chara_no = ctx.globals.sound_routing.koe_chara_no;
3840 let koe_ex_flag = ctx.globals.sound_routing.koe_ex_flag;
3841 let (koe_chara_onoff, koe_chara_volume) =
3842 chrkoe_route(config, chrkoe_lists.as_slice(), koe_chara_no);
3843 let mut koe_buf_total = if koe_ex_flag {
3844 mul_raw(all_total, config.sound_user_volume[1])
3845 } else {
3846 category_total[1]
3847 };
3848 koe_buf_total = mul_raw(koe_buf_total, koe_chara_volume);
3849 if !koe_ex_flag
3850 && (!koe_chara_onoff
3851 || !config.play_all_sound_check
3852 || !config.play_sound_check[1]
3853 || config.koe_mode == 1)
3854 {
3855 if ctx.koe.is_playing_any() {
3858 let _ = ctx.koe.stop(None);
3859 }
3860 koe_buf_total = 0;
3861 }
3862 let koe_audible = koe_buf_total > 0 && ctx.koe.is_playing_any();
3863 let use_bgmfade = config.bgmfade_use_check
3864 && (ctx.globals.script.bgmfade_flag || koe_audible || pcmch_bgm_fade_use);
3865 let use_bgmfade2 = koe_audible || pcmch_bgm_fade_use;
3866 let bgmfade2_in_start = gameexe_i64_or(ctx, "BGMFADE2.IN_START_TIME", 0);
3867 let bgmfade2_in_len = gameexe_i64_or(ctx, "BGMFADE2.IN_TIME_LEN", 500).max(0);
3868 let bgmfade2_out_start = gameexe_i64_or(ctx, "BGMFADE2.OUT_START_TIME", 0);
3869 let bgmfade2_out_len = gameexe_i64_or(ctx, "BGMFADE2.OUT_TIME_LEN", 500).max(0);
3870 let bgmfade2_volume = gameexe_i64_or(ctx, "BGMFADE2.VOLUME", 0).clamp(0, 255);
3871 let bgmfade2_need = ctx.globals.sound_routing.bgmfade2_need_flag;
3872
3873 let (fade, fade2) = {
3876 let routing = &mut ctx.globals.sound_routing;
3877 routing.bgmfade_cur_time = routing
3878 .bgmfade_cur_time
3879 .saturating_add(real_delta_ms.max(0) as i64);
3880 routing.bgmfade2_cur_time = routing
3881 .bgmfade2_cur_time
3882 .saturating_add(real_delta_ms.max(0) as i64);
3883
3884 if routing.bgmfade_flag != use_bgmfade {
3885 routing.bgmfade_cur_time = 0;
3886 routing.bgmfade_start_value = routing.bgmfade_total_volume;
3887 routing.bgmfade_flag = use_bgmfade;
3888 }
3889 if routing.bgmfade2_flag != use_bgmfade2 {
3890 routing.bgmfade2_cur_time = 0;
3891 routing.bgmfade2_start_value = routing.bgmfade2_total_volume;
3892 routing.bgmfade2_flag = use_bgmfade2;
3893 }
3894
3895 routing.bgmfade_total_volume = if routing.bgmfade_flag {
3896 linear_limit_i64(
3897 routing.bgmfade_cur_time,
3898 0,
3899 routing.bgmfade_start_value,
3900 1000,
3901 config.bgmfade_volume,
3902 )
3903 } else {
3904 linear_limit_i64(
3905 routing.bgmfade_cur_time,
3906 2000,
3907 routing.bgmfade_start_value,
3908 3000,
3909 255,
3910 )
3911 }
3912 .clamp(0, 255);
3913
3914 routing.bgmfade2_total_volume = if routing.bgmfade2_flag {
3915 linear_limit_i64(
3916 routing.bgmfade2_cur_time,
3917 bgmfade2_in_start,
3918 routing.bgmfade2_start_value,
3919 bgmfade2_in_start.saturating_add(bgmfade2_in_len),
3920 bgmfade2_volume,
3921 )
3922 } else if bgmfade2_need {
3923 linear_limit_i64(
3924 routing.bgmfade2_cur_time,
3925 bgmfade2_out_start,
3926 routing.bgmfade2_start_value,
3927 bgmfade2_out_start.saturating_add(bgmfade2_out_len),
3928 255,
3929 )
3930 } else {
3931 linear_limit_i64(
3934 routing.bgmfade2_cur_time,
3935 500,
3936 routing.bgmfade2_start_value,
3937 500i64.saturating_add(bgmfade2_out_len),
3938 255,
3939 )
3940 }
3941 .clamp(0, 255);
3942
3943 (routing.bgmfade_total_volume, routing.bgmfade2_total_volume)
3944 };
3945
3946 let bgm_total = mul_raw(category_total[0], fade) as u8;
3947 ctx.audio
3948 .set_track_master_volume_raw(TrackKind::Bgm, bgm_total);
3949 ctx.audio
3950 .set_track_master_volume_raw(TrackKind::Koe, koe_buf_total.clamp(0, 255) as u8);
3951 ctx.audio.set_track_master_volume_raw(TrackKind::Pcm, 255);
3954 ctx.audio
3955 .set_track_master_volume_raw(TrackKind::Se, category_total[3] as u8);
3956 ctx.audio
3957 .set_track_master_volume_raw(TrackKind::Mov, category_total[4] as u8);
3958 let _ = ctx
3959 .pcm
3960 .set_global_system_volume_raw(category_total[2] as u8);
3961
3962 let normal_channels: Vec<(usize, crate::runtime::globals::PcmChPersistentState)> = ctx
3963 .globals
3964 .pcmch_persistent
3965 .iter()
3966 .cloned()
3967 .enumerate()
3968 .filter(|(_, state)| !state.bgm_fade_source_flag)
3969 .collect();
3970 for (channel, state) in normal_channels {
3971 let routed = pcmch_routed_volume(
3972 config,
3973 chrkoe_lists.as_slice(),
3974 &state,
3975 all_total,
3976 &category_total,
3977 fade,
3978 fade2,
3979 );
3980 let _ = ctx.pcm.set_slot_system_volume_raw(channel, routed);
3981 }
3982
3983 if change_force {
3984 let sources: Vec<(usize, crate::runtime::globals::PcmChPersistentState)> = ctx
3989 .globals
3990 .pcmch_persistent
3991 .iter()
3992 .cloned()
3993 .enumerate()
3994 .filter(|(_, state)| state.bgm_fade_source_flag)
3995 .collect();
3996 for (channel, state) in sources {
3997 let routed = pcmch_routed_volume(
3998 config,
3999 chrkoe_lists.as_slice(),
4000 &state,
4001 all_total,
4002 &category_total,
4003 fade,
4004 fade2,
4005 );
4006 let _ = ctx.pcm.set_slot_system_volume_raw(channel, routed);
4007 }
4008 }
4009}
4010
4011pub(crate) fn apply_audio_config(ctx: &mut CommandContext) {
4012 update_audio_routing(ctx, 0, true);
4013}
4014
4015fn cfg_get_str(st: &crate::runtime::globals::SyscomRuntimeState, key: i32) -> String {
4016 st.config_str.get(&key).cloned().unwrap_or_default()
4017}
4018
4019fn cfg_set_str(st: &mut crate::runtime::globals::SyscomRuntimeState, key: i32, value: String) {
4020 st.config_str.insert(key, value);
4021}
4022
4023fn join_game_path(base: &Path, raw: &str) -> PathBuf {
4024 if raw.is_empty() {
4025 return base.to_path_buf();
4026 }
4027 let norm = raw.replace('\\', "/");
4028 let p = Path::new(&norm);
4029 if p.is_absolute() {
4030 p.to_path_buf()
4031 } else {
4032 base.join(p)
4033 }
4034}
4035
4036fn remove_game_file(path: &Path) {
4037 let _ = fs::remove_file(path);
4038 crate::resource::invalidate_game_path_cache(path);
4039}
4040
4041fn copy_game_file(src: &Path, dst: &Path) {
4042 let _ = fs::copy(src, dst);
4043 crate::resource::invalidate_game_path_cache(dst);
4044}
4045
4046fn rename_game_file(src: &Path, dst: &Path) {
4047 let _ = fs::rename(src, dst);
4048 crate::resource::invalidate_game_path_cache(src);
4049 crate::resource::invalidate_game_path_cache(dst);
4050}
4051
4052fn mark_game_file_written(path: &Path) {
4053 crate::resource::invalidate_game_path_cache(path);
4054}
4055
4056fn opaque_rgba(img: &RgbaImage) -> RgbaImage {
4057 let mut rgba = img.rgba.clone();
4058 for px in rgba.as_chunks_mut::<4>().0.iter_mut() {
4059 px[3] = 255;
4060 }
4061 RgbaImage {
4062 width: img.width,
4063 height: img.height,
4064 center_x: 0,
4065 center_y: 0,
4066 rgba,
4067 }
4068}
4069
4070fn write_rgba_png(path: &Path, img: &RgbaImage) -> Result<()> {
4071 if let Some(parent) = path.parent() {
4072 fs::create_dir_all(parent)?;
4073 }
4074 let Some(buf) = image::RgbaImage::from_raw(img.width, img.height, img.rgba.clone()) else {
4075 anyhow::bail!("invalid rgba buffer for {}x{} image", img.width, img.height);
4076 };
4077 buf.save(path)?;
4078 mark_game_file_written(path);
4079 Ok(())
4080}
4081
4082fn write_rgba_png_opaque(path: &Path, img: &RgbaImage) -> Result<()> {
4083 let opaque = opaque_rgba(img);
4084 write_rgba_png(path, &opaque)
4085}
4086
4087fn push_u16_le(out: &mut Vec<u8>, value: u16) {
4088 out.extend_from_slice(&value.to_le_bytes());
4089}
4090
4091fn push_u32_le(out: &mut Vec<u8>, value: u32) {
4092 out.extend_from_slice(&value.to_le_bytes());
4093}
4094
4095fn push_i32_le(out: &mut Vec<u8>, value: i32) {
4096 out.extend_from_slice(&value.to_le_bytes());
4097}
4098
4099fn write_rgba_bmp_top_down(path: &Path, img: &RgbaImage) -> Result<()> {
4100 if let Some(parent) = path.parent() {
4101 fs::create_dir_all(parent)?;
4102 }
4103 let width = img.width;
4104 let height = img.height;
4105 if width == 0 || height == 0 {
4106 anyhow::bail!("invalid zero-sized bmp image {}x{}", width, height);
4107 }
4108 let pixel_size = width.saturating_mul(height).saturating_mul(4);
4109 let file_size = 14u32.saturating_add(40).saturating_add(pixel_size);
4110 let mut out = Vec::with_capacity(file_size as usize);
4111
4112 out.extend_from_slice(b"BM");
4113 push_u32_le(&mut out, file_size);
4114 push_u16_le(&mut out, 0);
4115 push_u16_le(&mut out, 0);
4116 push_u32_le(&mut out, 14 + 40);
4117
4118 push_u32_le(&mut out, 40);
4119 push_i32_le(&mut out, width as i32);
4120 push_i32_le(&mut out, -(height as i32));
4121 push_u16_le(&mut out, 1);
4122 push_u16_le(&mut out, 32);
4123 push_u32_le(&mut out, 0);
4124 push_u32_le(&mut out, 0);
4125 push_i32_le(&mut out, 0);
4126 push_i32_le(&mut out, 0);
4127 push_u32_le(&mut out, 0);
4128 push_u32_le(&mut out, 0);
4129
4130 for px in img.rgba.as_chunks::<4>().0.iter() {
4131 out.push(px[2]);
4132 out.push(px[1]);
4133 out.push(px[0]);
4134 out.push(px[3]);
4135 }
4136 fs::write(path, out)?;
4137 mark_game_file_written(path);
4138 Ok(())
4139}
4140
4141fn resize_rgba(img: &RgbaImage, w: u32, h: u32) -> RgbaImage {
4142 if img.width == 0 || img.height == 0 || w == 0 || h == 0 {
4143 return img.clone();
4144 }
4145 if img.width == w && img.height == h {
4146 return img.clone();
4147 }
4148 let mut out = vec![0u8; (w * h * 4) as usize];
4149 for y in 0..h {
4150 let src_y = (y as u64 * img.height as u64 / h as u64) as u32;
4151 for x in 0..w {
4152 let src_x = (x as u64 * img.width as u64 / w as u64) as u32;
4153 let si = ((src_y * img.width + src_x) * 4) as usize;
4154 let di = ((y * w + x) * 4) as usize;
4155 out[di..di + 4].copy_from_slice(&img.rgba[si..si + 4]);
4156 }
4157 }
4158 RgbaImage {
4159 width: w,
4160 height: h,
4161 center_x: 0,
4162 center_y: 0,
4163 rgba: out,
4164 }
4165}
4166
4167pub(crate) fn resize_capture_rgba_nearest(img: &RgbaImage, w: u32, h: u32) -> RgbaImage {
4168 resize_rgba(img, w, h)
4169}
4170
4171fn blend_tweet_overlay_fullscreen(base: &mut RgbaImage, overlay: &RgbaImage) {
4172 if base.width == 0 || base.height == 0 || overlay.width == 0 || overlay.height == 0 {
4173 return;
4174 }
4175
4176 let scaled = resize_rgba(overlay, base.width, base.height);
4182 for (dst, src) in base
4183 .rgba
4184 .as_chunks_mut::<4>()
4185 .0
4186 .iter_mut()
4187 .zip(scaled.rgba.as_chunks::<4>().0.iter())
4188 {
4189 let alpha = u32::from(src[3]);
4190 if alpha == 0 {
4191 dst[3] = 255;
4192 continue;
4193 }
4194 let inv_alpha = 255 - alpha;
4195 for channel in 0..3 {
4196 let value = u32::from(src[channel]) * alpha + u32::from(dst[channel]) * inv_alpha + 127;
4197 dst[channel] = (value / 255) as u8;
4198 }
4199 dst[3] = 255;
4200 }
4201}
4202
4203pub fn capture_for_tweet(ctx: &mut CommandContext) -> Result<RgbaImage> {
4209 let mut capture = ctx.capture_frame_rgba()?;
4210 let overlap_name = gameexe_unquoted_owned(ctx, "TWITTER.OVERLAP_IMAGE");
4211 if overlap_name.is_empty() {
4212 return Ok(capture);
4213 }
4214
4215 match ctx.images.load_g00(&overlap_name, 0) {
4216 Ok(image_id) => {
4217 if let Some(overlay) = ctx.images.get(&image_id).map(|image| (*image).clone()) {
4218 blend_tweet_overlay_fullscreen(&mut capture, &overlay);
4219 }
4220 }
4221 Err(err) => {
4222 log::debug!(
4226 "TWITTER.OVERLAP_IMAGE {:?} is not available; capturing without overlay: {err:#}",
4227 overlap_name
4228 );
4229 }
4230 }
4231
4232 Ok(capture)
4233}
4234
4235#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
4236fn open_tweet_dialog(ctx: &mut CommandContext) -> Result<()> {
4237 let Some(capture) = ctx.globals.capture_image.clone() else {
4238 log::error!("SYSCOM.OPEN_TWEET_DIALOG called without GLOBAL.CAPTURE_FOR_TWEET capture");
4239 return Ok(());
4240 };
4241
4242 let out_path = save_dir(&ctx.project_dir).join("tweet.png");
4243 write_rgba_png_opaque(&out_path, &capture)?;
4244 crate::runtime::twitter::ensure_state_loaded(ctx);
4245 let initial_text = gameexe_unquoted_owned(ctx, "TWITTER.INITIAL_TWEET_TEXT");
4246 ctx.globals.twitter_dialog_request = Some(crate::runtime::twitter::TwitterDialogRequest {
4247 image_path: out_path.clone(),
4248 image_rgba: capture.rgba.clone(),
4249 image_width: capture.width,
4250 image_height: capture.height,
4251 initial_text,
4252 });
4253 ctx.globals
4254 .system
4255 .debug_logs
4256 .push(format!("open_tweet_dialog:{}", out_path.display()));
4257 Ok(())
4258}
4259
4260#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
4261fn open_tweet_dialog(_ctx: &mut CommandContext) -> Result<()> {
4262 log::error!("SYSCOM.OPEN_TWEET_DIALOG is not implemented on this platform");
4263 Ok(())
4264}
4265
4266fn font_exists(project_dir: &Path, name: &str) -> bool {
4267 if name.is_empty() {
4268 return false;
4269 }
4270 if crate::text_render::font_name_matches_embedded_default(name) {
4271 return true;
4272 }
4273
4274 let name_lower = name.to_ascii_lowercase();
4275 for font_dir in [project_dir.join("font"), project_dir.join("fonts")] {
4276 let Some(font_dir) = crate::resource::resolve_game_path(&font_dir).ok().flatten() else {
4277 continue;
4278 };
4279 let Ok(entries) = fs::read_dir(font_dir) else {
4280 continue;
4281 };
4282 for entry in entries.flatten() {
4283 let path = entry.path();
4284 if !path.is_file() {
4285 continue;
4286 }
4287 let ext = path
4288 .extension()
4289 .and_then(|s| s.to_str())
4290 .unwrap_or("")
4291 .to_ascii_lowercase();
4292 if ext != "ttf" && ext != "otf" && ext != "ttc" {
4293 continue;
4294 }
4295 let stem = path
4296 .file_stem()
4297 .and_then(|s| s.to_str())
4298 .unwrap_or("")
4299 .to_ascii_lowercase();
4300 if stem == name_lower {
4301 return true;
4302 }
4303 }
4304 }
4305 false
4306}
4307
4308pub fn dispatch(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
4309 let Some(call) = parse_call(ctx, form_id, args) else {
4310 return Ok(false);
4311 };
4312 let op = call.op;
4313 let params = call.params;
4314
4315 if op == CHECK_READ_SKIP_ENABLE {
4318 ctx.push(Value::Int(if ctx.runtime_read_skip_is_enable() {
4319 1
4320 } else {
4321 0
4322 }));
4323 return Ok(true);
4324 }
4325 if matches!(
4326 op,
4327 SET_READ_SKIP_ONOFF_FLAG | SET_READ_SKIP_ENABLE_FLAG | SET_READ_SKIP_EXIST_FLAG
4328 ) {
4329 let value = p_bool(params, 0);
4330 match op {
4331 SET_READ_SKIP_ONOFF_FLAG => ctx.globals.syscom.read_skip.onoff = value,
4332 SET_READ_SKIP_ENABLE_FLAG => ctx.globals.syscom.read_skip.enable = value,
4333 SET_READ_SKIP_EXIST_FLAG => ctx.globals.syscom.read_skip.exist = value,
4334 _ => unreachable!(),
4335 }
4336 ctx.runtime_update_read_skip_menu();
4337 ctx.push(Value::Int(0));
4338 return Ok(true);
4339 }
4340
4341 {
4342 let st = &ctx.globals.syscom;
4343 if let Some(v) = get_local_extra(op, params, st) {
4344 ctx.push(Value::Int(v));
4345 return Ok(true);
4346 }
4347 if let Some(v) = get_toggle_get(op, st) {
4348 ctx.push(Value::Int(v));
4349 return Ok(true);
4350 }
4351 }
4352 {
4353 let st = &mut ctx.globals.syscom;
4354 if set_local_extra(op, params, st) {
4355 ctx.push(Value::Int(0));
4356 return Ok(true);
4357 }
4358 if apply_toggle_set(op, p_bool(params, 0), st) {
4359 ctx.push(Value::Int(0));
4360 return Ok(true);
4361 }
4362 }
4363
4364 match op {
4365 CALL_EX => {
4366 return Ok(false);
4371 }
4372 CALL_SYSCOM_MENU => {
4373 ctx.globals.syscom.menu_open = false;
4374 ctx.globals.syscom.menu_kind = None;
4375 ctx.globals.syscom.menu_result = None;
4376 ctx.globals.syscom.read_skip.onoff = false;
4377 ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
4378 kind: SyscomPendingProcKind::OpenSyscomMenu,
4379 warning: false,
4380 se_play: false,
4381 fade_out: false,
4382 leave_msgbk: false,
4383 save_id: 0,
4384 });
4385 ctx.globals.syscom.last_menu_call = CALL_SYSCOM_MENU;
4386 return Ok(true);
4387 }
4388 SET_SYSCOM_MENU_ENABLE => ctx.globals.syscom.syscom_menu_disable = false,
4389 SET_SYSCOM_MENU_DISABLE => ctx.globals.syscom.syscom_menu_disable = true,
4390 SET_MWND_BTN_ENABLE => {
4391 if params.is_empty() {
4392 ctx.globals.syscom.mwnd_btn_disable_all = false;
4393 if sg_debug_enabled_local() {
4394 eprintln!(
4395 "[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_ENABLE all disable_all=false"
4396 );
4397 }
4398 } else {
4399 let idx = p_i64(params, 0);
4400 ctx.globals.syscom.mwnd_btn_disable.insert(idx, false);
4401 if sg_debug_enabled_local() {
4402 eprintln!(
4403 "[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_ENABLE idx={} disabled=false",
4404 idx
4405 );
4406 }
4407 }
4408 }
4409 SET_MWND_BTN_DISABLE => {
4410 if params.is_empty() {
4411 ctx.globals.syscom.mwnd_btn_disable_all = true;
4412 if sg_debug_enabled_local() {
4413 eprintln!(
4414 "[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_DISABLE all disable_all=true"
4415 );
4416 }
4417 } else {
4418 let idx = p_i64(params, 0);
4419 ctx.globals.syscom.mwnd_btn_disable.insert(idx, true);
4420 if sg_debug_enabled_local() {
4421 eprintln!(
4422 "[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_DISABLE idx={} disabled=true",
4423 idx
4424 );
4425 }
4426 }
4427 }
4428 SET_MWND_BTN_TOUCH_ENABLE => {
4429 ctx.globals.syscom.mwnd_btn_touch_disable = false;
4430 if sg_debug_enabled_local() {
4431 eprintln!(
4432 "[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_TOUCH_ENABLE touch_disable=false"
4433 );
4434 }
4435 }
4436 SET_MWND_BTN_TOUCH_DISABLE => {
4437 ctx.globals.syscom.mwnd_btn_touch_disable = true;
4438 if sg_debug_enabled_local() {
4439 eprintln!(
4440 "[SG_DEBUG][BUTTON_TRACE][SYSCOM] SET_MWND_BTN_TOUCH_DISABLE touch_disable=true"
4441 );
4442 }
4443 }
4444 INIT_SYSCOM_FLAG => {
4445 let enabled = ToggleFeatureState {
4446 onoff: false,
4447 enable: true,
4448 exist: true,
4449 };
4450 ctx.globals.syscom.read_skip = enabled;
4451 ctx.globals.syscom.auto_skip = enabled;
4452 ctx.globals.syscom.auto_mode = enabled;
4453 ctx.globals.syscom.hide_mwnd = enabled;
4454 ctx.globals.syscom.local_extra_switch = enabled;
4455 ctx.globals.syscom.local_extra_mode = ValueFeatureState {
4456 value: 0,
4457 enable: true,
4458 exist: true,
4459 };
4460 ctx.globals.syscom.local_extra_switches = [enabled; 4];
4461 ctx.globals.syscom.local_extra_modes = [ValueFeatureState {
4462 value: 0,
4463 enable: true,
4464 exist: true,
4465 }; 4];
4466 ctx.globals.syscom.msg_back = enabled;
4467 ctx.globals.syscom.return_to_sel = enabled;
4468 ctx.globals.syscom.return_to_menu = enabled;
4469 ctx.globals.syscom.end_game = enabled;
4470 ctx.globals.syscom.save_feature = enabled;
4471 ctx.globals.syscom.load_feature = enabled;
4472 ctx.globals.syscom.msg_back_open = false;
4473 ctx.runtime_update_read_skip_menu();
4474 }
4475 OPEN_MSG_BACK => {
4476 if open_msg_back_proc(ctx) {
4477 ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
4478 kind: SyscomPendingProcKind::MsgBack,
4479 warning: false,
4480 se_play: false,
4481 fade_out: false,
4482 leave_msgbk: false,
4483 save_id: 0,
4484 });
4485 }
4486 ctx.globals.syscom.last_menu_call = OPEN_MSG_BACK;
4487 }
4488 CLOSE_MSG_BACK => {
4489 ctx.globals.syscom.msg_back_open = false;
4490 ctx.globals.syscom.msg_back_proc_initialized = false;
4491 ctx.globals.syscom.last_menu_call = CLOSE_MSG_BACK;
4492 }
4493 RETURN_TO_SEL => {
4494 ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
4495 kind: SyscomPendingProcKind::ReturnToSel,
4496 warning: p_bool(params, 0),
4497 se_play: p_bool(params, 1),
4498 fade_out: p_bool(params, 2),
4499 leave_msgbk: false,
4500 save_id: 0,
4501 });
4502 ctx.globals.syscom.last_menu_call = RETURN_TO_SEL;
4503 ctx.globals.syscom.menu_open = false;
4504 }
4505 RETURN_TO_MENU => {
4506 let leave_msgbk = params
4507 .iter()
4508 .find(|v| v.named_id() == Some(0))
4509 .and_then(Value::as_i64)
4510 .unwrap_or(0)
4511 != 0;
4512 ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
4513 kind: SyscomPendingProcKind::ReturnToMenu,
4514 warning: p_bool(params, 0),
4515 se_play: p_bool(params, 1),
4516 fade_out: p_bool(params, 2),
4517 leave_msgbk,
4518 save_id: 0,
4519 });
4520 ctx.globals.syscom.last_menu_call = RETURN_TO_MENU;
4521 ctx.globals.syscom.menu_open = false;
4522 }
4523 END_GAME => {
4524 ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
4525 kind: SyscomPendingProcKind::EndGame,
4526 warning: p_bool(params, 0),
4527 se_play: p_bool(params, 1),
4528 fade_out: p_bool(params, 2),
4529 leave_msgbk: false,
4530 save_id: 0,
4531 });
4532 ctx.globals.syscom.last_menu_call = END_GAME;
4533 ctx.globals.syscom.menu_open = false;
4534 }
4535 REPLAY_KOE => {
4536 let koe_no = ctx.globals.script.cur_koe_no;
4537 let chara_no = ctx.globals.script.cur_chr_no;
4538 if koe_no >= 0 {
4539 crate::runtime::forms::global::remember_global_koe(ctx, koe_no, chara_no, false);
4543 let append_dir = ctx.globals.append_dir.clone();
4544 let jitan_rate = ctx.koe_jitan_rate(None, true);
4545 if let Err(err) = {
4546 let (koe, audio) = (&mut ctx.koe, &mut ctx.audio);
4547 koe.play_koe_no_with_rate(audio, koe_no, &append_dir, jitan_rate)
4548 } {
4549 log::error!(
4550 "SYSCOM.REPLAY_KOE failed koe_no={koe_no} chara_no={chara_no}: {err:#}"
4551 );
4552 }
4553 ctx.globals.syscom.replay_koe = Some((koe_no, chara_no));
4554 }
4555 }
4556 CHECK_REPLAY_KOE => {
4557 let v = if ctx.globals.script.cur_koe_no >= 0 {
4558 1
4559 } else {
4560 0
4561 };
4562 ctx.push(Value::Int(v));
4563 return Ok(true);
4564 }
4565 GET_REPLAY_KOE_KOE_NO => {
4566 let v = ctx.globals.script.cur_koe_no;
4567 ctx.push(Value::Int(v));
4568 return Ok(true);
4569 }
4570 GET_REPLAY_KOE_CHARA_NO => {
4571 let v = ctx.globals.script.cur_chr_no;
4572 ctx.push(Value::Int(v));
4573 return Ok(true);
4574 }
4575 CLEAR_REPLAY_KOE => {
4576 ctx.globals.script.cur_koe_no = -1;
4577 ctx.globals.script.cur_chr_no = -1;
4578 ctx.globals.syscom.replay_koe = None;
4579 }
4580 GET_CURRENT_SAVE_SCENE_TITLE => {
4581 let v = ctx.globals.syscom.current_save_scene_title.clone();
4582 ctx.push(Value::Str(v));
4583 return Ok(true);
4584 }
4585 GET_CURRENT_SAVE_MESSAGE => {
4586 let v = ctx.globals.syscom.current_save_message.clone();
4587 ctx.push(Value::Str(v));
4588 return Ok(true);
4589 }
4590 GET_TOTAL_PLAY_TIME => {
4591 let v = ctx.globals.syscom.total_play_time;
4592 ctx.push(Value::Int(v));
4593 return Ok(true);
4594 }
4595 SET_TOTAL_PLAY_TIME => {
4596 ctx.globals.syscom.total_play_time = p_i64(params, 0);
4597 write_global_save(ctx);
4598 }
4599 CALL_SAVE_MENU => {
4600 prepare_runtime_save_thumb_capture_with_priority(ctx, CAPTURE_PRIOR_SAVE);
4601 set_syscom_pending_proc(ctx, SyscomPendingProcKind::OpenSave);
4602 ctx.globals.syscom.last_menu_call = CALL_SAVE_MENU;
4603 return Ok(true);
4604 }
4605 CALL_LOAD_MENU => {
4606 set_syscom_pending_proc(ctx, SyscomPendingProcKind::OpenLoad);
4607 ctx.globals.syscom.last_menu_call = CALL_LOAD_MENU;
4608 return Ok(true);
4609 }
4610 SAVE => {
4611 let idx = p_i64(params, 0).max(0) as usize;
4612 let warning = p_bool(params, 1);
4613 let ok = idx < configured_save_count(ctx, false) && local_save_available(ctx);
4614 if ok
4615 && !request_confirmed_save_or_load(
4616 ctx,
4617 SyscomPendingProcKind::Save,
4618 idx,
4619 warning,
4620 true,
4621 )
4622 && ok
4623 {
4624 menu_save_slot(ctx, false, idx);
4625 }
4626 ctx.globals.syscom.last_menu_call = SAVE;
4627 ctx.push(Value::Int(if ok { 1 } else { 0 }));
4628 return Ok(true);
4629 }
4630 LOAD => {
4631 let idx = p_i64(params, 0).max(0) as usize;
4632 let warning = p_bool(params, 1);
4633 let ok = idx < configured_save_count(ctx, false)
4634 && local_save_file_exists(ctx, SaveKind::Normal, idx);
4635 if ok
4636 && !request_confirmed_save_or_load(
4637 ctx,
4638 SyscomPendingProcKind::Load,
4639 idx,
4640 warning,
4641 false,
4642 )
4643 {
4644 menu_load_slot(ctx, false, idx);
4645 }
4646 ctx.globals.syscom.last_menu_call = LOAD;
4647 }
4648 QUICK_SAVE => {
4649 let idx = p_i64(params, 0).max(0) as usize;
4650 let warning = p_bool(params, 1);
4651 let ok = idx < configured_save_count(ctx, true) && local_save_available(ctx);
4652 if ok
4653 && !request_confirmed_save_or_load(
4654 ctx,
4655 SyscomPendingProcKind::QuickSave,
4656 idx,
4657 warning,
4658 true,
4659 )
4660 && ok
4661 {
4662 menu_save_slot(ctx, true, idx);
4663 }
4664 ctx.globals.syscom.last_menu_call = QUICK_SAVE;
4665 ctx.push(Value::Int(if ok { 1 } else { 0 }));
4666 return Ok(true);
4667 }
4668 QUICK_LOAD => {
4669 let idx = p_i64(params, 0).max(0) as usize;
4670 let warning = p_bool(params, 1);
4671 let ok = idx < configured_save_count(ctx, true)
4672 && local_save_file_exists(ctx, SaveKind::Quick, idx);
4673 if ok
4674 && !request_confirmed_save_or_load(
4675 ctx,
4676 SyscomPendingProcKind::QuickLoad,
4677 idx,
4678 warning,
4679 false,
4680 )
4681 {
4682 menu_load_slot(ctx, true, idx);
4683 }
4684 ctx.globals.syscom.last_menu_call = QUICK_LOAD;
4685 }
4686 END_SAVE => {
4687 let _warning = p_bool(params, 0);
4690 let ok = local_save_available(ctx);
4691 if ok {
4692 prepare_runtime_save_thumb_capture_with_priority(ctx, CAPTURE_PRIOR_END);
4693 ctx.request_runtime_save(RuntimeSaveKind::End, 0);
4694 }
4695 ctx.push(Value::Int(if ok { 1 } else { 0 }));
4696 return Ok(true);
4697 }
4698 END_LOAD => {
4699 if local_save_file_exists(ctx, SaveKind::End, 0) {
4702 ctx.request_runtime_load(RuntimeSaveKind::End, 0);
4703 }
4704 ctx.globals.syscom.last_menu_call = END_LOAD;
4705 }
4706 INNER_SAVE => {
4707 let idx = p_i64(params, 0).max(0) as usize;
4708 let ok = local_save_available(ctx);
4709 if ok {
4710 ctx.request_runtime_save(RuntimeSaveKind::Inner, idx);
4711 }
4712 ctx.push(Value::Int(if ok { 1 } else { 0 }));
4713 return Ok(true);
4714 }
4715 INNER_LOAD => {
4716 let idx = p_i64(params, 0).max(0) as usize;
4717 ctx.globals.syscom.last_menu_call = INNER_LOAD;
4718 let exists = ctx
4719 .globals
4720 .syscom
4721 .inner_save_streams
4722 .get(idx)
4723 .map(|s| !s.is_empty())
4724 .unwrap_or(false);
4725 if exists {
4726 ctx.request_runtime_load(RuntimeSaveKind::Inner, idx);
4727 }
4728 ctx.push(Value::Int(if exists { 1 } else { 0 }));
4729 return Ok(true);
4730 }
4731 CLEAR_INNER_SAVE => {
4732 let idx = p_i64(params, 0).max(0) as usize;
4733 let existed = ctx
4734 .globals
4735 .syscom
4736 .inner_save_streams
4737 .get(idx)
4738 .map(|s| !s.is_empty())
4739 .unwrap_or(false);
4740 if let Some(slot) = ctx.globals.syscom.inner_save_streams.get_mut(idx) {
4741 slot.clear();
4742 }
4743 ctx.globals.syscom.inner_save_exists = ctx
4744 .globals
4745 .syscom
4746 .inner_save_streams
4747 .iter()
4748 .any(|s| !s.is_empty());
4749 ctx.push(Value::Int(if existed { 1 } else { 0 }));
4750 return Ok(true);
4751 }
4752 COPY_INNER_SAVE => {
4753 let from = p_i64(params, 0).max(0) as usize;
4754 let to = p_i64(params, 1).max(0) as usize;
4755 let Some(stream) = ctx
4756 .globals
4757 .syscom
4758 .inner_save_streams
4759 .get(from)
4760 .cloned()
4761 .filter(|s| !s.is_empty())
4762 else {
4763 ctx.push(Value::Int(0));
4764 return Ok(true);
4765 };
4766 if ctx.globals.syscom.inner_save_streams.len() <= to {
4767 ctx.globals
4768 .syscom
4769 .inner_save_streams
4770 .resize_with(to + 1, Vec::new);
4771 }
4772 ctx.globals.syscom.inner_save_streams[to] = stream;
4773 ctx.globals.syscom.inner_save_exists = true;
4774 ctx.push(Value::Int(1));
4775 return Ok(true);
4776 }
4777 CHECK_INNER_SAVE => {
4778 let idx = p_i64(params, 0).max(0) as usize;
4779 let v = if ctx
4780 .globals
4781 .syscom
4782 .inner_save_streams
4783 .get(idx)
4784 .map(|s| !s.is_empty())
4785 .unwrap_or(false)
4786 {
4787 1
4788 } else {
4789 0
4790 };
4791 ctx.push(Value::Int(v));
4792 return Ok(true);
4793 }
4794 MSG_BACK_LOAD => {
4795 ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
4796 kind: SyscomPendingProcKind::BacklogLoad,
4797 warning: p_bool(params, 0),
4798 se_play: p_bool(params, 1),
4799 fade_out: p_bool(params, 2),
4800 leave_msgbk: false,
4801 save_id: ctx.globals.syscom.msg_back_load_tid,
4802 });
4803 ctx.globals.syscom.last_menu_call = MSG_BACK_LOAD;
4804 ctx.globals.syscom.msg_back_open = false;
4805 write_msg_back(ctx);
4806 }
4807 GET_SAVE_CNT => {
4808 let v = configured_save_count(ctx, false) as i64;
4809 ctx.push(Value::Int(v));
4810 return Ok(true);
4811 }
4812 GET_QUICK_SAVE_CNT => {
4813 let v = configured_save_count(ctx, true) as i64;
4814 ctx.push(Value::Int(v));
4815 return Ok(true);
4816 }
4817 GET_SAVE_NEW_NO => {
4818 sync_save_slots_from_disk(ctx, false);
4819 let save_cnt = configured_save_count(ctx, false);
4820 let v = match params.len() {
4821 0 => newest_slot_in_range(
4822 &ctx.globals.syscom.save_slots,
4823 0,
4824 save_cnt as i64,
4825 save_cnt,
4826 ),
4827 2 => newest_slot_in_range(
4828 &ctx.globals.syscom.save_slots,
4829 p_i64(params, 0),
4830 p_i64(params, 1),
4831 save_cnt,
4832 ),
4833 _ => -1,
4834 };
4835 ctx.push(Value::Int(v));
4836 return Ok(true);
4837 }
4838 GET_QUICK_SAVE_NEW_NO => {
4839 sync_save_slots_from_disk(ctx, true);
4840 let quick_cnt = configured_save_count(ctx, true);
4841 let v = match params.len() {
4842 0 => newest_slot_in_range(
4843 &ctx.globals.syscom.quick_save_slots,
4844 0,
4845 quick_cnt as i64,
4846 quick_cnt,
4847 ),
4848 2 => newest_slot_in_range(
4849 &ctx.globals.syscom.quick_save_slots,
4850 p_i64(params, 0),
4851 p_i64(params, 1),
4852 quick_cnt,
4853 ),
4854 _ => -1,
4855 };
4856 ctx.push(Value::Int(v));
4857 return Ok(true);
4858 }
4859 GET_SAVE_EXIST | GET_SAVE_YEAR | GET_SAVE_MONTH | GET_SAVE_DAY | GET_SAVE_WEEKDAY
4860 | GET_SAVE_HOUR | GET_SAVE_MINUTE | GET_SAVE_SECOND | GET_SAVE_MILLISECOND => {
4861 let raw_idx = p_i64(params, 0);
4862 let save_cnt = configured_save_count(ctx, false);
4863 if raw_idx < 0 || raw_idx as usize >= save_cnt {
4864 ctx.push(Value::Int(0));
4865 return Ok(true);
4866 }
4867 let idx = raw_idx as usize;
4868 {
4869 let quick_cnt = configured_save_count(ctx, true);
4870 ensure_slot_loaded_with_counts(
4871 &ctx.project_dir,
4872 false,
4873 save_cnt,
4874 quick_cnt,
4875 &mut ctx.globals.syscom.save_slots,
4876 idx,
4877 );
4878 }
4879 let v = ctx
4880 .globals
4881 .syscom
4882 .save_slots
4883 .get(idx)
4884 .map(|s| slot_i64(s, op))
4885 .unwrap_or(0);
4886 ctx.push(Value::Int(v));
4887 return Ok(true);
4888 }
4889 GET_SAVE_TITLE
4890 | GET_SAVE_MESSAGE
4891 | GET_SAVE_FULL_MESSAGE
4892 | GET_SAVE_COMMENT
4893 | GET_SAVE_APPEND_DIR
4894 | GET_SAVE_APPEND_NAME => {
4895 let raw_idx = p_i64(params, 0);
4896 let save_cnt = configured_save_count(ctx, false);
4897 if raw_idx < 0 || raw_idx as usize >= save_cnt {
4898 ctx.push(Value::Str(String::new()));
4899 return Ok(true);
4900 }
4901 let idx = raw_idx as usize;
4902 {
4903 let quick_cnt = configured_save_count(ctx, true);
4904 ensure_slot_loaded_with_counts(
4905 &ctx.project_dir,
4906 false,
4907 save_cnt,
4908 quick_cnt,
4909 &mut ctx.globals.syscom.save_slots,
4910 idx,
4911 );
4912 }
4913 let v = ctx
4914 .globals
4915 .syscom
4916 .save_slots
4917 .get(idx)
4918 .map(|s| slot_str(s, op))
4919 .unwrap_or_default();
4920 ctx.push(Value::Str(v));
4921 return Ok(true);
4922 }
4923 SET_SAVE_COMMENT => {
4924 let raw_idx = p_i64(params, 0);
4925 let save_cnt = configured_save_count(ctx, false);
4926 if raw_idx < 0 || raw_idx as usize >= save_cnt {
4927 return Ok(true);
4928 }
4929 let idx = raw_idx as usize;
4930 let quick_cnt = configured_save_count(ctx, true);
4931 ensure_slot_loaded_with_counts(
4932 &ctx.project_dir,
4933 false,
4934 save_cnt,
4935 quick_cnt,
4936 &mut ctx.globals.syscom.save_slots,
4937 idx,
4938 );
4939 if !ctx
4942 .globals
4943 .syscom
4944 .save_slots
4945 .get(idx)
4946 .map(|slot| slot.exist)
4947 .unwrap_or(false)
4948 {
4949 return Ok(true);
4950 }
4951 let comment = params
4952 .get(1)
4953 .and_then(|v| v.as_str())
4954 .unwrap_or("")
4955 .to_string();
4956 ctx.globals.syscom.save_slots[idx].comment = comment;
4957 persist_slot_with_counts(
4958 &ctx.project_dir,
4959 false,
4960 save_cnt,
4961 quick_cnt,
4962 &mut ctx.globals.syscom.save_slots,
4963 idx,
4964 );
4965 return Ok(true);
4966 }
4967 GET_SAVE_VALUE => {
4968 let raw_idx = p_i64(params, 0);
4969 let save_cnt = configured_save_count(ctx, false);
4970 if raw_idx < 0 || raw_idx as usize >= save_cnt {
4971 return Ok(true);
4972 }
4973 let idx = raw_idx as usize;
4974 let quick_cnt = configured_save_count(ctx, true);
4975 ensure_slot_loaded_with_counts(
4976 &ctx.project_dir,
4977 false,
4978 save_cnt,
4979 quick_cnt,
4980 &mut ctx.globals.syscom.save_slots,
4981 idx,
4982 );
4983 let Some(slot) = ctx
4984 .globals
4985 .syscom
4986 .save_slots
4987 .get(idx)
4988 .filter(|slot| slot.exist)
4989 else {
4990 return Ok(true);
4991 };
4992 let Some(Value::Element(chain)) = params.get(1).map(|v| v.unwrap_named()) else {
4993 return Ok(true);
4994 };
4995 let Some(form_id) = chain.first().copied() else {
4996 return Ok(true);
4997 };
4998 let flag_index = p_i64(params, 2).max(0) as usize;
4999 let flag_cnt = (p_i64(params, 3).max(0) as usize).min(original_save::SAVE_FLAG_MAX_CNT);
5000 let values: Vec<i64> = (0..flag_cnt)
5001 .map(|i| slot.values.get(&(i as i32)).copied().unwrap_or(0))
5002 .collect();
5003 let list = ctx.globals.int_lists.entry(form_id as u32).or_default();
5004 if list.len() < flag_index + flag_cnt {
5005 list.resize(flag_index + flag_cnt, 0);
5006 }
5007 for (i, value) in values.into_iter().enumerate() {
5008 list[flag_index + i] = value;
5009 }
5010 return Ok(true);
5013 }
5014 SET_SAVE_VALUE => {
5015 let raw_idx = p_i64(params, 0);
5016 let save_cnt = configured_save_count(ctx, false);
5017 if raw_idx < 0 || raw_idx as usize >= save_cnt {
5018 return Ok(true);
5019 }
5020 let idx = raw_idx as usize;
5021 let quick_cnt = configured_save_count(ctx, true);
5022 ensure_slot_loaded_with_counts(
5023 &ctx.project_dir,
5024 false,
5025 save_cnt,
5026 quick_cnt,
5027 &mut ctx.globals.syscom.save_slots,
5028 idx,
5029 );
5030 if !ctx
5031 .globals
5032 .syscom
5033 .save_slots
5034 .get(idx)
5035 .map(|slot| slot.exist)
5036 .unwrap_or(false)
5037 {
5038 return Ok(true);
5039 }
5040 let Some(Value::Element(chain)) = params.get(1).map(|v| v.unwrap_named()) else {
5041 return Ok(true);
5042 };
5043 let Some(form_id) = chain.first().copied() else {
5044 return Ok(true);
5045 };
5046 let flag_index = p_i64(params, 2).max(0) as usize;
5047 let flag_cnt = (p_i64(params, 3).max(0) as usize).min(original_save::SAVE_FLAG_MAX_CNT);
5048 let values: Vec<i64> = (0..flag_cnt)
5049 .map(|i| {
5050 ctx.globals
5051 .int_lists
5052 .get(&(form_id as u32))
5053 .and_then(|list| list.get(flag_index + i).copied())
5054 .unwrap_or(0)
5055 })
5056 .collect();
5057 let slot = &mut ctx.globals.syscom.save_slots[idx];
5058 for (i, value) in values.into_iter().enumerate() {
5059 if value == 0 {
5060 slot.values.remove(&(i as i32));
5061 } else {
5062 slot.values.insert(i as i32, value);
5063 }
5064 }
5065 persist_slot_with_counts(
5066 &ctx.project_dir,
5067 false,
5068 save_cnt,
5069 quick_cnt,
5070 &mut ctx.globals.syscom.save_slots,
5071 idx,
5072 );
5073 return Ok(true);
5074 }
5075 GET_QUICK_SAVE_EXIST
5076 | GET_QUICK_SAVE_YEAR
5077 | GET_QUICK_SAVE_MONTH
5078 | GET_QUICK_SAVE_DAY
5079 | GET_QUICK_SAVE_WEEKDAY
5080 | GET_QUICK_SAVE_HOUR
5081 | GET_QUICK_SAVE_MINUTE
5082 | GET_QUICK_SAVE_SECOND
5083 | GET_QUICK_SAVE_MILLISECOND => {
5084 let raw_idx = p_i64(params, 0);
5085 let quick_cnt = configured_save_count(ctx, true);
5086 if raw_idx < 0 || raw_idx as usize >= quick_cnt {
5087 ctx.push(Value::Int(0));
5088 return Ok(true);
5089 }
5090 let idx = raw_idx as usize;
5091 {
5092 let save_cnt = configured_save_count(ctx, false);
5093 ensure_slot_loaded_with_counts(
5094 &ctx.project_dir,
5095 true,
5096 save_cnt,
5097 quick_cnt,
5098 &mut ctx.globals.syscom.quick_save_slots,
5099 idx,
5100 );
5101 }
5102 let v = ctx
5103 .globals
5104 .syscom
5105 .quick_save_slots
5106 .get(idx)
5107 .map(|s| slot_i64(s, op))
5108 .unwrap_or(0);
5109 ctx.push(Value::Int(v));
5110 return Ok(true);
5111 }
5112 GET_QUICK_SAVE_TITLE
5113 | GET_QUICK_SAVE_MESSAGE
5114 | GET_QUICK_SAVE_FULL_MESSAGE
5115 | GET_QUICK_SAVE_COMMENT
5116 | GET_QUICK_SAVE_APPEND_DIR
5117 | GET_QUICK_SAVE_APPEND_NAME => {
5118 let raw_idx = p_i64(params, 0);
5119 let quick_cnt = configured_save_count(ctx, true);
5120 if raw_idx < 0 || raw_idx as usize >= quick_cnt {
5121 ctx.push(Value::Str(String::new()));
5122 return Ok(true);
5123 }
5124 let idx = raw_idx as usize;
5125 {
5126 let save_cnt = configured_save_count(ctx, false);
5127 ensure_slot_loaded_with_counts(
5128 &ctx.project_dir,
5129 true,
5130 save_cnt,
5131 quick_cnt,
5132 &mut ctx.globals.syscom.quick_save_slots,
5133 idx,
5134 );
5135 }
5136 let v = ctx
5137 .globals
5138 .syscom
5139 .quick_save_slots
5140 .get(idx)
5141 .map(|s| slot_str(s, op))
5142 .unwrap_or_default();
5143 ctx.push(Value::Str(v));
5144 return Ok(true);
5145 }
5146 SET_QUICK_SAVE_COMMENT => {
5147 let raw_idx = p_i64(params, 0);
5148 let quick_cnt = configured_save_count(ctx, true);
5149 if raw_idx < 0 || raw_idx as usize >= quick_cnt {
5150 return Ok(true);
5151 }
5152 let idx = raw_idx as usize;
5153 let save_cnt = configured_save_count(ctx, false);
5154 ensure_slot_loaded_with_counts(
5155 &ctx.project_dir,
5156 true,
5157 save_cnt,
5158 quick_cnt,
5159 &mut ctx.globals.syscom.quick_save_slots,
5160 idx,
5161 );
5162 if !ctx
5163 .globals
5164 .syscom
5165 .quick_save_slots
5166 .get(idx)
5167 .map(|slot| slot.exist)
5168 .unwrap_or(false)
5169 {
5170 return Ok(true);
5171 }
5172 let comment = params
5173 .get(1)
5174 .and_then(|v| v.as_str())
5175 .unwrap_or("")
5176 .to_string();
5177 ctx.globals.syscom.quick_save_slots[idx].comment = comment;
5178 persist_slot_with_counts(
5179 &ctx.project_dir,
5180 true,
5181 save_cnt,
5182 quick_cnt,
5183 &mut ctx.globals.syscom.quick_save_slots,
5184 idx,
5185 );
5186 return Ok(true);
5187 }
5188 GET_QUICK_SAVE_VALUE => {
5189 let raw_idx = p_i64(params, 0);
5190 let quick_cnt = configured_save_count(ctx, true);
5191 if raw_idx < 0 || raw_idx as usize >= quick_cnt {
5192 return Ok(true);
5193 }
5194 let idx = raw_idx as usize;
5195 let save_cnt = configured_save_count(ctx, false);
5196 ensure_slot_loaded_with_counts(
5197 &ctx.project_dir,
5198 true,
5199 save_cnt,
5200 quick_cnt,
5201 &mut ctx.globals.syscom.quick_save_slots,
5202 idx,
5203 );
5204 let Some(slot) = ctx
5205 .globals
5206 .syscom
5207 .quick_save_slots
5208 .get(idx)
5209 .filter(|slot| slot.exist)
5210 else {
5211 return Ok(true);
5212 };
5213 let Some(Value::Element(chain)) = params.get(1).map(|v| v.unwrap_named()) else {
5214 return Ok(true);
5215 };
5216 let Some(form_id) = chain.first().copied() else {
5217 return Ok(true);
5218 };
5219 let flag_index = p_i64(params, 2).max(0) as usize;
5220 let flag_cnt = (p_i64(params, 3).max(0) as usize).min(original_save::SAVE_FLAG_MAX_CNT);
5221 let values: Vec<i64> = (0..flag_cnt)
5222 .map(|i| slot.values.get(&(i as i32)).copied().unwrap_or(0))
5223 .collect();
5224 let list = ctx.globals.int_lists.entry(form_id as u32).or_default();
5225 if list.len() < flag_index + flag_cnt {
5226 list.resize(flag_index + flag_cnt, 0);
5227 }
5228 for (i, value) in values.into_iter().enumerate() {
5229 list[flag_index + i] = value;
5230 }
5231 return Ok(true);
5232 }
5233 SET_QUICK_SAVE_VALUE => {
5234 let raw_idx = p_i64(params, 0);
5235 let quick_cnt = configured_save_count(ctx, true);
5236 if raw_idx < 0 || raw_idx as usize >= quick_cnt {
5237 return Ok(true);
5238 }
5239 let idx = raw_idx as usize;
5240 let save_cnt = configured_save_count(ctx, false);
5241 ensure_slot_loaded_with_counts(
5242 &ctx.project_dir,
5243 true,
5244 save_cnt,
5245 quick_cnt,
5246 &mut ctx.globals.syscom.quick_save_slots,
5247 idx,
5248 );
5249 if !ctx
5250 .globals
5251 .syscom
5252 .quick_save_slots
5253 .get(idx)
5254 .map(|slot| slot.exist)
5255 .unwrap_or(false)
5256 {
5257 return Ok(true);
5258 }
5259 let Some(Value::Element(chain)) = params.get(1).map(|v| v.unwrap_named()) else {
5260 return Ok(true);
5261 };
5262 let Some(form_id) = chain.first().copied() else {
5263 return Ok(true);
5264 };
5265 let flag_index = p_i64(params, 2).max(0) as usize;
5266 let flag_cnt = (p_i64(params, 3).max(0) as usize).min(original_save::SAVE_FLAG_MAX_CNT);
5267 let values: Vec<i64> = (0..flag_cnt)
5268 .map(|i| {
5269 ctx.globals
5270 .int_lists
5271 .get(&(form_id as u32))
5272 .and_then(|list| list.get(flag_index + i).copied())
5273 .unwrap_or(0)
5274 })
5275 .collect();
5276 let slot = &mut ctx.globals.syscom.quick_save_slots[idx];
5277 for (i, value) in values.into_iter().enumerate() {
5278 if value == 0 {
5279 slot.values.remove(&(i as i32));
5280 } else {
5281 slot.values.insert(i as i32, value);
5282 }
5283 }
5284 persist_slot_with_counts(
5285 &ctx.project_dir,
5286 true,
5287 save_cnt,
5288 quick_cnt,
5289 &mut ctx.globals.syscom.quick_save_slots,
5290 idx,
5291 );
5292 return Ok(true);
5293 }
5294 GET_END_SAVE_EXIST => {
5295 let save_cnt = configured_save_count(ctx, false);
5296 let quick_cnt = configured_save_count(ctx, true);
5297 let end_path = original_save::save_file_path_with_counts(
5298 &ctx.project_dir,
5299 save_cnt,
5300 quick_cnt,
5301 SaveKind::End,
5302 0,
5303 );
5304 let v = if ctx.globals.syscom.end_save_exists
5305 || crate::resource::game_file_exists(&end_path)
5306 {
5307 1
5308 } else {
5309 0
5310 };
5311 ctx.push(Value::Int(v));
5312 return Ok(true);
5313 }
5314 COPY_SAVE => {
5315 let src = p_i64(params, 0).max(0) as usize;
5316 let dst = p_i64(params, 1).max(0) as usize;
5317 let save_cnt = configured_save_count(ctx, false);
5318 let quick_cnt = configured_save_count(ctx, true);
5319 let thumb_config = save_thumb_config(ctx);
5320 let ok = copy_slot(
5321 &ctx.project_dir,
5322 false,
5323 save_cnt,
5324 quick_cnt,
5325 thumb_config,
5326 &mut ctx.globals.syscom.save_slots,
5327 src,
5328 dst,
5329 );
5330 ctx.globals.syscom.last_menu_call = op;
5331 ctx.push(Value::Int(if ok { 1 } else { 0 }));
5332 return Ok(true);
5333 }
5334 COPY_QUICK_SAVE => {
5335 let src = p_i64(params, 0).max(0) as usize;
5336 let dst = p_i64(params, 1).max(0) as usize;
5337 let save_cnt = configured_save_count(ctx, false);
5338 let quick_cnt = configured_save_count(ctx, true);
5339 let thumb_config = save_thumb_config(ctx);
5340 let ok = copy_slot(
5341 &ctx.project_dir,
5342 true,
5343 save_cnt,
5344 quick_cnt,
5345 thumb_config,
5346 &mut ctx.globals.syscom.quick_save_slots,
5347 src,
5348 dst,
5349 );
5350 ctx.globals.syscom.last_menu_call = op;
5351 ctx.push(Value::Int(if ok { 1 } else { 0 }));
5352 return Ok(true);
5353 }
5354 CHANGE_SAVE => {
5355 let a = p_i64(params, 0).max(0) as usize;
5356 let b = p_i64(params, 1).max(0) as usize;
5357 let save_cnt = configured_save_count(ctx, false);
5358 let quick_cnt = configured_save_count(ctx, true);
5359 let thumb_config = save_thumb_config(ctx);
5360 let ok = change_slot(
5361 &ctx.project_dir,
5362 false,
5363 save_cnt,
5364 quick_cnt,
5365 thumb_config,
5366 &mut ctx.globals.syscom.save_slots,
5367 a,
5368 b,
5369 );
5370 ctx.globals.syscom.last_menu_call = op;
5371 ctx.push(Value::Int(if ok { 1 } else { 0 }));
5372 return Ok(true);
5373 }
5374 CHANGE_QUICK_SAVE => {
5375 let a = p_i64(params, 0).max(0) as usize;
5376 let b = p_i64(params, 1).max(0) as usize;
5377 let save_cnt = configured_save_count(ctx, false);
5378 let quick_cnt = configured_save_count(ctx, true);
5379 let thumb_config = save_thumb_config(ctx);
5380 let ok = change_slot(
5381 &ctx.project_dir,
5382 true,
5383 save_cnt,
5384 quick_cnt,
5385 thumb_config,
5386 &mut ctx.globals.syscom.quick_save_slots,
5387 a,
5388 b,
5389 );
5390 ctx.globals.syscom.last_menu_call = op;
5391 ctx.push(Value::Int(if ok { 1 } else { 0 }));
5392 return Ok(true);
5393 }
5394 DELETE_SAVE => {
5395 let idx = p_i64(params, 0).max(0) as usize;
5396 let save_cnt = configured_save_count(ctx, false);
5397 let quick_cnt = configured_save_count(ctx, true);
5398 let thumb_config = save_thumb_config(ctx);
5399 let ok = delete_slot(
5400 &ctx.project_dir,
5401 false,
5402 save_cnt,
5403 quick_cnt,
5404 thumb_config,
5405 &mut ctx.globals.syscom.save_slots,
5406 idx,
5407 );
5408 ctx.globals.syscom.last_menu_call = op;
5409 ctx.push(Value::Int(if ok { 1 } else { 0 }));
5410 return Ok(true);
5411 }
5412 DELETE_QUICK_SAVE => {
5413 let idx = p_i64(params, 0).max(0) as usize;
5414 let save_cnt = configured_save_count(ctx, false);
5415 let quick_cnt = configured_save_count(ctx, true);
5416 let thumb_config = save_thumb_config(ctx);
5417 let ok = delete_slot(
5418 &ctx.project_dir,
5419 true,
5420 save_cnt,
5421 quick_cnt,
5422 thumb_config,
5423 &mut ctx.globals.syscom.quick_save_slots,
5424 idx,
5425 );
5426 ctx.globals.syscom.last_menu_call = op;
5427 ctx.push(Value::Int(if ok { 1 } else { 0 }));
5428 return Ok(true);
5429 }
5430 CALL_CONFIG_MENU => {
5431 set_syscom_pending_proc(ctx, SyscomPendingProcKind::OpenConfig);
5432 ctx.globals.syscom.last_menu_call = op;
5433 }
5434 CALL_CONFIG_WINDOW_MODE_MENU
5435 | CALL_CONFIG_VOLUME_MENU
5436 | CALL_CONFIG_BGMFADE_MENU
5437 | CALL_CONFIG_KOEMODE_MENU
5438 | CALL_CONFIG_CHARAKOE_MENU
5439 | CALL_CONFIG_JITAN_MENU
5440 | CALL_CONFIG_MESSAGE_SPEED_MENU
5441 | CALL_CONFIG_FILTER_COLOR_MENU
5442 | CALL_CONFIG_AUTO_MODE_MENU
5443 | CALL_CONFIG_FONT_MENU
5444 | CALL_CONFIG_SYSTEM_MENU
5445 | CALL_CONFIG_MOVIE_MENU => {
5446 set_syscom_pending_proc(ctx, SyscomPendingProcKind::OpenConfigDialog);
5450 ctx.globals.syscom.last_menu_call = op;
5451 }
5452 SET_WINDOW_MODE => cfg_set_int(&mut ctx.globals.syscom, GET_WINDOW_MODE, p_i64(params, 0)),
5453 SET_WINDOW_MODE_DEFAULT => {
5454 let value = gameexe_i64_or(ctx, "CONFIG.WINDOW_MODE", 0).clamp(0, 1);
5455 cfg_set_int(&mut ctx.globals.syscom, GET_WINDOW_MODE, value);
5456 }
5457 GET_WINDOW_MODE => {
5458 let v = cfg_get_int(&ctx.globals.syscom, GET_WINDOW_MODE, 0);
5459 ctx.push(Value::Int(v));
5460 return Ok(true);
5461 }
5462 SET_WINDOW_MODE_SIZE => cfg_set_int(
5463 &mut ctx.globals.syscom,
5464 GET_WINDOW_MODE_SIZE,
5465 p_i64(params, 0),
5466 ),
5467 SET_WINDOW_MODE_SIZE_DEFAULT => {
5468 let value = ctx
5469 .globals
5470 .syscom
5471 .original_config
5472 .screen_size_scale
5473 .0
5474 .max(1);
5475 cfg_set_int(&mut ctx.globals.syscom, GET_WINDOW_MODE_SIZE, value);
5476 }
5477 GET_WINDOW_MODE_SIZE => {
5478 let v = cfg_get_int(&ctx.globals.syscom, GET_WINDOW_MODE_SIZE, 100);
5479 ctx.push(Value::Int(v));
5480 return Ok(true);
5481 }
5482 CHECK_WINDOW_MODE_SIZE_ENABLE => {
5483 ctx.push(Value::Int(1));
5484 return Ok(true);
5485 }
5486 SET_ALL_VOLUME => {
5487 let value = p_i64(params, 0).clamp(0, 255);
5488 ctx.globals.syscom.original_config.all_sound_user_volume = value;
5489 cfg_set_int(&mut ctx.globals.syscom, GET_ALL_VOLUME, value);
5490 apply_audio_config(ctx);
5491 }
5492 SET_BGM_VOLUME => {
5493 set_sound_volume_by_type(ctx, 0, p_i64(params, 0));
5494 apply_audio_config(ctx);
5495 }
5496 SET_KOE_VOLUME => {
5497 set_sound_volume_by_type(ctx, 1, p_i64(params, 0));
5498 apply_audio_config(ctx);
5499 }
5500 SET_PCM_VOLUME => {
5501 set_sound_volume_by_type(ctx, 2, p_i64(params, 0));
5502 apply_audio_config(ctx);
5503 }
5504 SET_SE_VOLUME => {
5505 set_sound_volume_by_type(ctx, 3, p_i64(params, 0));
5506 apply_audio_config(ctx);
5507 }
5508 SET_MOV_VOLUME => {
5509 set_sound_volume_by_type(ctx, 4, p_i64(params, 0));
5510 apply_audio_config(ctx);
5511 }
5512 SET_SOUND_VOLUME => {
5513 let sound_type = p_i64(params, 0);
5514 if (0..32).contains(&sound_type) {
5515 set_sound_volume_by_type(ctx, sound_type as usize, p_i64(params, 1));
5516 apply_audio_config(ctx);
5517 }
5518 }
5519 SET_ALL_VOLUME_DEFAULT => {
5520 let value = gameexe_i64_or(ctx, "CONFIG.VOLUME.ALL", 255).clamp(0, 255);
5521 ctx.globals.syscom.original_config.all_sound_user_volume = value;
5522 cfg_set_int(&mut ctx.globals.syscom, GET_ALL_VOLUME, value);
5523 apply_audio_config(ctx);
5524 }
5525 SET_BGM_VOLUME_DEFAULT => {
5526 let value = config_default_sound_volume(ctx, 0);
5527 set_sound_volume_by_type(ctx, 0, value);
5528 apply_audio_config(ctx);
5529 }
5530 SET_KOE_VOLUME_DEFAULT => {
5531 let value = config_default_sound_volume(ctx, 1);
5532 set_sound_volume_by_type(ctx, 1, value);
5533 apply_audio_config(ctx);
5534 }
5535 SET_PCM_VOLUME_DEFAULT => {
5536 let value = config_default_sound_volume(ctx, 2);
5537 set_sound_volume_by_type(ctx, 2, value);
5538 apply_audio_config(ctx);
5539 }
5540 SET_SE_VOLUME_DEFAULT => {
5541 let value = config_default_sound_volume(ctx, 3);
5542 set_sound_volume_by_type(ctx, 3, value);
5543 apply_audio_config(ctx);
5544 }
5545 SET_MOV_VOLUME_DEFAULT => {
5546 let value = config_default_sound_volume(ctx, 4);
5547 set_sound_volume_by_type(ctx, 4, value);
5548 apply_audio_config(ctx);
5549 }
5550 SET_SOUND_VOLUME_DEFAULT => {
5551 let sound_type = p_i64(params, 0);
5552 if (0..32).contains(&sound_type) {
5553 let value = config_default_sound_volume(ctx, sound_type as usize);
5554 set_sound_volume_by_type(ctx, sound_type as usize, value);
5555 apply_audio_config(ctx);
5556 }
5557 }
5558 GET_ALL_VOLUME | GET_BGM_VOLUME | GET_KOE_VOLUME | GET_PCM_VOLUME | GET_SE_VOLUME
5559 | GET_MOV_VOLUME => {
5560 let v = cfg_get_int(&ctx.globals.syscom, op, 255).clamp(0, 255);
5561 ctx.push(Value::Int(v));
5562 return Ok(true);
5563 }
5564 GET_SOUND_VOLUME => {
5565 let sound_type = p_i64(params, 0);
5566 let value = if (0..32).contains(&sound_type) {
5567 get_sound_volume_by_type(ctx, sound_type as usize)
5568 } else {
5569 255
5570 };
5571 ctx.push(Value::Int(value));
5572 return Ok(true);
5573 }
5574 SET_ALL_ONOFF => {
5575 let value = p_bool(params, 0);
5576 ctx.globals.syscom.original_config.play_all_sound_check = value;
5577 cfg_set_int(
5578 &mut ctx.globals.syscom,
5579 GET_ALL_ONOFF,
5580 if value { 1 } else { 0 },
5581 );
5582 apply_audio_config(ctx);
5583 }
5584 SET_BGM_ONOFF => {
5585 set_sound_onoff_by_type(ctx, 0, p_bool(params, 0));
5586 apply_audio_config(ctx);
5587 }
5588 SET_KOE_ONOFF => {
5589 set_sound_onoff_by_type(ctx, 1, p_bool(params, 0));
5590 apply_audio_config(ctx);
5591 }
5592 SET_PCM_ONOFF => {
5593 set_sound_onoff_by_type(ctx, 2, p_bool(params, 0));
5594 apply_audio_config(ctx);
5595 }
5596 SET_SE_ONOFF => {
5597 set_sound_onoff_by_type(ctx, 3, p_bool(params, 0));
5598 apply_audio_config(ctx);
5599 }
5600 SET_MOV_ONOFF => {
5601 set_sound_onoff_by_type(ctx, 4, p_bool(params, 0));
5602 apply_audio_config(ctx);
5603 }
5604 SET_SOUND_ONOFF => {
5605 let sound_type = p_i64(params, 0);
5606 if (0..32).contains(&sound_type) {
5607 set_sound_onoff_by_type(ctx, sound_type as usize, p_bool(params, 1));
5608 apply_audio_config(ctx);
5609 }
5610 }
5611 SET_ALL_ONOFF_DEFAULT => {
5612 ctx.globals.syscom.original_config.play_all_sound_check = true;
5613 cfg_set_int(&mut ctx.globals.syscom, GET_ALL_ONOFF, 1);
5614 apply_audio_config(ctx);
5615 }
5616 SET_BGM_ONOFF_DEFAULT => {
5617 let value = config_default_sound_onoff(ctx, 0);
5618 set_sound_onoff_by_type(ctx, 0, value);
5619 apply_audio_config(ctx);
5620 }
5621 SET_KOE_ONOFF_DEFAULT => {
5622 let value = config_default_sound_onoff(ctx, 1);
5623 set_sound_onoff_by_type(ctx, 1, value);
5624 apply_audio_config(ctx);
5625 }
5626 SET_PCM_ONOFF_DEFAULT => {
5627 let value = config_default_sound_onoff(ctx, 2);
5628 set_sound_onoff_by_type(ctx, 2, value);
5629 apply_audio_config(ctx);
5630 }
5631 SET_SE_ONOFF_DEFAULT => {
5632 let value = config_default_sound_onoff(ctx, 3);
5633 set_sound_onoff_by_type(ctx, 3, value);
5634 apply_audio_config(ctx);
5635 }
5636 SET_MOV_ONOFF_DEFAULT => {
5637 let value = config_default_sound_onoff(ctx, 4);
5638 set_sound_onoff_by_type(ctx, 4, value);
5639 apply_audio_config(ctx);
5640 }
5641 SET_SOUND_ONOFF_DEFAULT => {
5642 let sound_type = p_i64(params, 0);
5643 if (0..32).contains(&sound_type) {
5644 let value = config_default_sound_onoff(ctx, sound_type as usize);
5645 set_sound_onoff_by_type(ctx, sound_type as usize, value);
5646 apply_audio_config(ctx);
5647 }
5648 }
5649 GET_ALL_ONOFF | GET_BGM_ONOFF | GET_KOE_ONOFF | GET_PCM_ONOFF | GET_SE_ONOFF
5650 | GET_MOV_ONOFF => {
5651 let v = cfg_get_int(&ctx.globals.syscom, op, 1);
5652 ctx.push(Value::Int(if v != 0 { 1 } else { 0 }));
5653 return Ok(true);
5654 }
5655 GET_SOUND_ONOFF => {
5656 let sound_type = p_i64(params, 0);
5657 let value = if (0..32).contains(&sound_type) {
5658 get_sound_onoff_by_type(ctx, sound_type as usize)
5659 } else {
5660 true
5661 };
5662 ctx.push(Value::Int(if value { 1 } else { 0 }));
5663 return Ok(true);
5664 }
5665 SET_BGMFADE_VOLUME => {
5666 let value = p_i64(params, 0).clamp(0, 255);
5667 ctx.globals.syscom.original_config.bgmfade_volume = value;
5668 cfg_set_int(&mut ctx.globals.syscom, GET_BGMFADE_VOLUME, value);
5669 apply_audio_config(ctx);
5670 }
5671 SET_BGMFADE_ONOFF => {
5672 let value = p_bool(params, 0);
5673 ctx.globals.syscom.original_config.bgmfade_use_check = value;
5674 cfg_set_int(
5675 &mut ctx.globals.syscom,
5676 GET_BGMFADE_ONOFF,
5677 if value { 1 } else { 0 },
5678 );
5679 apply_audio_config(ctx);
5680 }
5681 SET_BGMFADE_VOLUME_DEFAULT => {
5682 let value = gameexe_i64_or(ctx, "CONFIG.BGMFADE_VOLUME", 192).clamp(0, 255);
5683 ctx.globals.syscom.original_config.bgmfade_volume = value;
5684 cfg_set_int(&mut ctx.globals.syscom, GET_BGMFADE_VOLUME, value);
5685 apply_audio_config(ctx);
5686 }
5687 SET_BGMFADE_ONOFF_DEFAULT => {
5688 let value = gameexe_bool_or(ctx, "CONFIG.BGMFADE_ONOFF", true);
5689 ctx.globals.syscom.original_config.bgmfade_use_check = value;
5690 cfg_set_int(
5691 &mut ctx.globals.syscom,
5692 GET_BGMFADE_ONOFF,
5693 if value { 1 } else { 0 },
5694 );
5695 apply_audio_config(ctx);
5696 }
5697 GET_BGMFADE_VOLUME | GET_BGMFADE_ONOFF => {
5698 let default = if op == GET_BGMFADE_ONOFF { 1 } else { 192 };
5699 let v = cfg_get_int(&ctx.globals.syscom, op, default);
5700 ctx.push(Value::Int(v));
5701 return Ok(true);
5702 }
5703 SET_KOEMODE => {
5704 let value = match p_i64(params, 0) {
5705 0..=2 => p_i64(params, 0),
5706 _ => 0,
5707 };
5708 ctx.globals.syscom.original_config.koe_mode = value;
5709 cfg_set_int(&mut ctx.globals.syscom, GET_KOEMODE, value);
5710 apply_audio_config(ctx);
5711 }
5712 SET_KOEMODE_DEFAULT => {
5713 ctx.globals.syscom.original_config.koe_mode = 0;
5714 cfg_set_int(&mut ctx.globals.syscom, GET_KOEMODE, 0);
5715 apply_audio_config(ctx);
5716 }
5717 GET_KOEMODE => {
5718 let v = cfg_get_int(&ctx.globals.syscom, GET_KOEMODE, 0);
5719 ctx.push(Value::Int(v));
5720 return Ok(true);
5721 }
5722 SET_CHARAKOE_ONOFF => {
5723 let index = p_i64(params, 0);
5724 let count = configured_chrkoe_count(ctx);
5725 ctx.globals
5726 .syscom
5727 .original_config
5728 .chrkoe
5729 .resize_with(count, crate::runtime::globals::ConfigChrKoeState::default);
5730 if index >= 0
5731 && let Some(item) = ctx
5732 .globals
5733 .syscom
5734 .original_config
5735 .chrkoe
5736 .get_mut(index as usize)
5737 {
5738 item.onoff = p_bool(params, 1);
5739 }
5740 apply_audio_config(ctx);
5741 }
5742 SET_CHARAKOE_ONOFF_DEFAULT => {
5743 let index = p_i64(params, 0);
5744 let count = configured_chrkoe_count(ctx);
5745 ctx.globals
5746 .syscom
5747 .original_config
5748 .chrkoe
5749 .resize_with(count, crate::runtime::globals::ConfigChrKoeState::default);
5750 if index >= 0 && (index as usize) < count {
5751 let default = config_default_chrkoe(ctx, index as usize);
5752 if let Some(item) = ctx
5753 .globals
5754 .syscom
5755 .original_config
5756 .chrkoe
5757 .get_mut(index as usize)
5758 {
5759 item.onoff = default.onoff;
5760 }
5761 }
5762 apply_audio_config(ctx);
5763 }
5764 GET_CHARAKOE_ONOFF => {
5765 let index = p_i64(params, 0);
5766 let value = if index >= 0 {
5767 ctx.globals
5768 .syscom
5769 .original_config
5770 .chrkoe
5771 .get(index as usize)
5772 .map(|item| item.onoff)
5773 .unwrap_or(false)
5774 } else {
5775 false
5776 };
5777 ctx.push(Value::Int(if value { 1 } else { 0 }));
5778 return Ok(true);
5779 }
5780 SET_CHARAKOE_VOLUME => {
5781 let index = p_i64(params, 0);
5782 let count = configured_chrkoe_count(ctx);
5783 ctx.globals
5784 .syscom
5785 .original_config
5786 .chrkoe
5787 .resize_with(count, crate::runtime::globals::ConfigChrKoeState::default);
5788 if index >= 0
5789 && let Some(item) = ctx
5790 .globals
5791 .syscom
5792 .original_config
5793 .chrkoe
5794 .get_mut(index as usize)
5795 {
5796 item.volume = p_i64(params, 1).clamp(0, 255);
5797 }
5798 apply_audio_config(ctx);
5799 }
5800 SET_CHARAKOE_VOLUME_DEFAULT => {
5801 let index = p_i64(params, 0);
5802 let count = configured_chrkoe_count(ctx);
5803 ctx.globals
5804 .syscom
5805 .original_config
5806 .chrkoe
5807 .resize_with(count, crate::runtime::globals::ConfigChrKoeState::default);
5808 if index >= 0 && (index as usize) < count {
5809 let default = config_default_chrkoe(ctx, index as usize);
5810 if let Some(item) = ctx
5811 .globals
5812 .syscom
5813 .original_config
5814 .chrkoe
5815 .get_mut(index as usize)
5816 {
5817 item.volume = default.volume;
5818 }
5819 }
5820 apply_audio_config(ctx);
5821 }
5822 GET_CHARAKOE_VOLUME => {
5823 let index = p_i64(params, 0);
5824 let value = if index >= 0 {
5825 ctx.globals
5826 .syscom
5827 .original_config
5828 .chrkoe
5829 .get(index as usize)
5830 .map(|item| item.volume)
5831 .unwrap_or(0)
5832 } else {
5833 0
5834 };
5835 ctx.push(Value::Int(value.clamp(0, 255)));
5836 return Ok(true);
5837 }
5838 SET_JITAN_NORMAL_ONOFF => cfg_set_int(
5839 &mut ctx.globals.syscom,
5840 GET_JITAN_NORMAL_ONOFF,
5841 if p_bool(params, 0) { 1 } else { 0 },
5842 ),
5843 SET_JITAN_NORMAL_ONOFF_DEFAULT => {
5844 cfg_set_int(&mut ctx.globals.syscom, GET_JITAN_NORMAL_ONOFF, 0)
5845 }
5846 GET_JITAN_NORMAL_ONOFF => {
5847 let v = cfg_get_int(&ctx.globals.syscom, GET_JITAN_NORMAL_ONOFF, 0);
5848 ctx.push(Value::Int(v));
5849 return Ok(true);
5850 }
5851 SET_JITAN_AUTO_MODE_ONOFF => cfg_set_int(
5852 &mut ctx.globals.syscom,
5853 GET_JITAN_AUTO_MODE_ONOFF,
5854 if p_bool(params, 0) { 1 } else { 0 },
5855 ),
5856 SET_JITAN_AUTO_MODE_ONOFF_DEFAULT => {
5857 cfg_set_int(&mut ctx.globals.syscom, GET_JITAN_AUTO_MODE_ONOFF, 0)
5858 }
5859 GET_JITAN_AUTO_MODE_ONOFF => {
5860 let v = cfg_get_int(&ctx.globals.syscom, GET_JITAN_AUTO_MODE_ONOFF, 0);
5861 ctx.push(Value::Int(v));
5862 return Ok(true);
5863 }
5864 SET_JITAN_KOE_REPLAY_ONOFF => cfg_set_int(
5865 &mut ctx.globals.syscom,
5866 GET_JITAN_KOE_REPLAY_ONOFF,
5867 if p_bool(params, 0) { 1 } else { 0 },
5868 ),
5869 SET_JITAN_KOE_REPLAY_ONOFF_DEFAULT => {
5870 cfg_set_int(&mut ctx.globals.syscom, GET_JITAN_KOE_REPLAY_ONOFF, 0)
5871 }
5872 GET_JITAN_KOE_REPLAY_ONOFF => {
5873 let v = cfg_get_int(&ctx.globals.syscom, GET_JITAN_KOE_REPLAY_ONOFF, 0);
5874 ctx.push(Value::Int(v));
5875 return Ok(true);
5876 }
5877 SET_JITAN_SPEED => cfg_set_int(
5878 &mut ctx.globals.syscom,
5879 GET_JITAN_SPEED,
5880 p_i64(params, 0).clamp(100, 300),
5881 ),
5882 SET_JITAN_SPEED_DEFAULT => cfg_set_int(&mut ctx.globals.syscom, GET_JITAN_SPEED, 100),
5883 GET_JITAN_SPEED => {
5884 let v = cfg_get_int(&ctx.globals.syscom, GET_JITAN_SPEED, 100);
5885 ctx.push(Value::Int(v));
5886 return Ok(true);
5887 }
5888 SET_MESSAGE_SPEED => cfg_set_int(
5889 &mut ctx.globals.syscom,
5890 GET_MESSAGE_SPEED,
5891 p_i64(params, 0).clamp(0, 100),
5892 ),
5893 SET_MESSAGE_SPEED_DEFAULT => {
5894 let value = gameexe_i64_or(ctx, "CONFIG.MESSAGE_SPEED", 20);
5895 cfg_set_int(&mut ctx.globals.syscom, GET_MESSAGE_SPEED, value);
5896 }
5897 GET_MESSAGE_SPEED => {
5898 let v = cfg_get_int(&ctx.globals.syscom, GET_MESSAGE_SPEED, 20);
5899 ctx.push(Value::Int(v));
5900 return Ok(true);
5901 }
5902 SET_MESSAGE_NOWAIT => {
5903 let v = p_bool(params, 0);
5904 ctx.globals.script.msg_nowait = v;
5905 cfg_set_int(
5906 &mut ctx.globals.syscom,
5907 GET_MESSAGE_NOWAIT,
5908 if v { 1 } else { 0 },
5909 );
5910 }
5911 SET_MESSAGE_NOWAIT_DEFAULT => {
5912 let value = gameexe_bool_or(ctx, "CONFIG.MESSAGE_SPEED_NOWAIT.ONOFF", false);
5913 ctx.globals.script.msg_nowait = value;
5914 cfg_set_int(
5915 &mut ctx.globals.syscom,
5916 GET_MESSAGE_NOWAIT,
5917 if value { 1 } else { 0 },
5918 );
5919 }
5920 GET_MESSAGE_NOWAIT => {
5921 let v = if ctx.globals.script.msg_nowait {
5922 1
5923 } else {
5924 cfg_get_int(&ctx.globals.syscom, GET_MESSAGE_NOWAIT, 0)
5925 };
5926 ctx.push(Value::Int(v));
5927 return Ok(true);
5928 }
5929 SET_AUTO_MODE_MOJI_WAIT => {
5930 let v = p_i64(params, 0).clamp(0, 500);
5931 ctx.globals.script.auto_mode_moji_wait = v;
5932 cfg_set_int(&mut ctx.globals.syscom, GET_AUTO_MODE_MOJI_WAIT, v);
5933 }
5934 SET_AUTO_MODE_MOJI_WAIT_DEFAULT => {
5935 let value = 70;
5936 ctx.globals.script.auto_mode_moji_wait = value;
5937 cfg_set_int(&mut ctx.globals.syscom, GET_AUTO_MODE_MOJI_WAIT, value);
5938 }
5939 GET_AUTO_MODE_MOJI_WAIT => {
5940 let v = ctx.globals.script.auto_mode_moji_wait;
5941 ctx.push(Value::Int(v));
5942 return Ok(true);
5943 }
5944 SET_AUTO_MODE_MIN_WAIT => {
5945 let v = p_i64(params, 0).clamp(0, 5000);
5946 ctx.globals.script.auto_mode_min_wait = v;
5947 cfg_set_int(&mut ctx.globals.syscom, GET_AUTO_MODE_MIN_WAIT, v);
5948 }
5949 SET_AUTO_MODE_MIN_WAIT_DEFAULT => {
5950 let value = 300;
5951 ctx.globals.script.auto_mode_min_wait = value;
5952 cfg_set_int(&mut ctx.globals.syscom, GET_AUTO_MODE_MIN_WAIT, value);
5953 }
5954 GET_AUTO_MODE_MIN_WAIT => {
5955 let v = ctx.globals.script.auto_mode_min_wait;
5956 ctx.push(Value::Int(v));
5957 return Ok(true);
5958 }
5959 SET_MOUSE_CURSOR_HIDE_ONOFF => cfg_set_int(
5960 &mut ctx.globals.syscom,
5961 GET_MOUSE_CURSOR_HIDE_ONOFF,
5962 if p_bool(params, 0) { 1 } else { 0 },
5963 ),
5964 SET_MOUSE_CURSOR_HIDE_ONOFF_DEFAULT => {
5965 let v = config_mouse_cursor_hide_onoff_default(ctx);
5966 cfg_set_int(&mut ctx.globals.syscom, GET_MOUSE_CURSOR_HIDE_ONOFF, v)
5967 }
5968 GET_MOUSE_CURSOR_HIDE_ONOFF => {
5969 let v = cfg_get_int(
5970 &ctx.globals.syscom,
5971 GET_MOUSE_CURSOR_HIDE_ONOFF,
5972 config_mouse_cursor_hide_onoff_default(ctx),
5973 );
5974 ctx.push(Value::Int(v));
5975 return Ok(true);
5976 }
5977 SET_MOUSE_CURSOR_HIDE_TIME => cfg_set_int(
5978 &mut ctx.globals.syscom,
5979 GET_MOUSE_CURSOR_HIDE_TIME,
5980 p_i64(params, 0),
5981 ),
5982 SET_MOUSE_CURSOR_HIDE_TIME_DEFAULT => {
5983 let v = config_mouse_cursor_hide_time_default(ctx);
5984 cfg_set_int(&mut ctx.globals.syscom, GET_MOUSE_CURSOR_HIDE_TIME, v)
5985 }
5986 GET_MOUSE_CURSOR_HIDE_TIME => {
5987 let v = cfg_get_int(
5988 &ctx.globals.syscom,
5989 GET_MOUSE_CURSOR_HIDE_TIME,
5990 config_mouse_cursor_hide_time_default(ctx),
5991 );
5992 ctx.push(Value::Int(v));
5993 return Ok(true);
5994 }
5995 SET_FILTER_COLOR_R => cfg_set_int(
5996 &mut ctx.globals.syscom,
5997 GET_FILTER_COLOR_R,
5998 p_i64(params, 0),
5999 ),
6000 SET_FILTER_COLOR_G => cfg_set_int(
6001 &mut ctx.globals.syscom,
6002 GET_FILTER_COLOR_G,
6003 p_i64(params, 0),
6004 ),
6005 SET_FILTER_COLOR_B => cfg_set_int(
6006 &mut ctx.globals.syscom,
6007 GET_FILTER_COLOR_B,
6008 p_i64(params, 0),
6009 ),
6010 SET_FILTER_COLOR_A => cfg_set_int(
6011 &mut ctx.globals.syscom,
6012 GET_FILTER_COLOR_A,
6013 p_i64(params, 0),
6014 ),
6015 SET_FILTER_COLOR_R_DEFAULT => {
6016 let (r, _, _, _) = config_filter_color_default(ctx);
6017 cfg_set_int(&mut ctx.globals.syscom, GET_FILTER_COLOR_R, r)
6018 }
6019 SET_FILTER_COLOR_G_DEFAULT => {
6020 let (_, g, _, _) = config_filter_color_default(ctx);
6021 cfg_set_int(&mut ctx.globals.syscom, GET_FILTER_COLOR_G, g)
6022 }
6023 SET_FILTER_COLOR_B_DEFAULT => {
6024 let (_, _, b, _) = config_filter_color_default(ctx);
6025 cfg_set_int(&mut ctx.globals.syscom, GET_FILTER_COLOR_B, b)
6026 }
6027 SET_FILTER_COLOR_A_DEFAULT => {
6028 let (_, _, _, a) = config_filter_color_default(ctx);
6029 cfg_set_int(&mut ctx.globals.syscom, GET_FILTER_COLOR_A, a)
6030 }
6031 GET_FILTER_COLOR_R | GET_FILTER_COLOR_G | GET_FILTER_COLOR_B | GET_FILTER_COLOR_A => {
6032 let (r, g, b, a) = config_filter_color_default(ctx);
6033 let default = match op {
6034 GET_FILTER_COLOR_R => r,
6035 GET_FILTER_COLOR_G => g,
6036 GET_FILTER_COLOR_B => b,
6037 GET_FILTER_COLOR_A => a,
6038 _ => 0,
6039 };
6040 let v = cfg_get_int(&ctx.globals.syscom, op, default);
6041 ctx.push(Value::Int(v));
6042 return Ok(true);
6043 }
6044 SET_OBJECT_DISP_ONOFF => {
6045 let index = p_i64(params, 0);
6046 if (0..4).contains(&index) {
6047 ctx.globals.syscom.original_config.object_disp_flag[index as usize] =
6048 p_bool(params, 1);
6049 }
6050 }
6051 SET_OBJECT_DISP_ONOFF_DEFAULT => {
6052 let index = p_i64(params, 0);
6053 if (0..4).contains(&index) {
6054 let value = config_default_indexed_bool(
6056 ctx,
6057 "CONFIG.GLOBAL_EXTRA_SWITCH",
6058 index as usize,
6059 true,
6060 );
6061 ctx.globals.syscom.original_config.object_disp_flag[index as usize] = value;
6062 }
6063 }
6064 GET_OBJECT_DISP_ONOFF => {
6065 let index = p_i64(params, 0);
6066 let value = if (0..4).contains(&index) {
6067 ctx.globals.syscom.original_config.object_disp_flag[index as usize]
6068 } else {
6069 false
6070 };
6071 ctx.push(Value::Int(if value { 1 } else { 0 }));
6072 return Ok(true);
6073 }
6074 SET_GLOBAL_EXTRA_SWITCH_ONOFF => {
6075 let index = p_i64(params, 0);
6076 if (0..4).contains(&index) {
6077 ctx.globals.syscom.original_config.global_extra_switch_flag[index as usize] =
6078 p_bool(params, 1);
6079 }
6080 }
6081 SET_GLOBAL_EXTRA_SWITCH_ONOFF_DEFAULT => {
6082 let index = p_i64(params, 0);
6083 if (0..4).contains(&index) {
6084 let value = config_default_indexed_bool(
6085 ctx,
6086 "CONFIG.GLOBAL_EXTRA_SWITCH",
6087 index as usize,
6088 true,
6089 );
6090 ctx.globals.syscom.original_config.global_extra_switch_flag[index as usize] = value;
6091 }
6092 }
6093 GET_GLOBAL_EXTRA_SWITCH_ONOFF => {
6094 let index = p_i64(params, 0);
6095 let value = if (0..4).contains(&index) {
6096 ctx.globals.syscom.original_config.global_extra_switch_flag[index as usize]
6097 } else {
6098 false
6099 };
6100 ctx.push(Value::Int(if value { 1 } else { 0 }));
6101 return Ok(true);
6102 }
6103 SET_GLOBAL_EXTRA_MODE_VALUE => {
6104 let index = p_i64(params, 0);
6105 if (0..4).contains(&index) {
6106 ctx.globals.syscom.original_config.global_extra_mode_flag[index as usize] =
6107 p_i64(params, 1);
6108 }
6109 }
6110 SET_GLOBAL_EXTRA_MODE_VALUE_DEFAULT => {
6111 let index = p_i64(params, 0);
6112 if (0..4).contains(&index) {
6113 let value = config_default_indexed_mode(ctx, index as usize);
6114 ctx.globals.syscom.original_config.global_extra_mode_flag[index as usize] = value;
6115 }
6116 }
6117 GET_GLOBAL_EXTRA_MODE_VALUE => {
6118 let index = p_i64(params, 0);
6119 let value = if (0..4).contains(&index) {
6120 ctx.globals.syscom.original_config.global_extra_mode_flag[index as usize]
6121 } else {
6122 0
6123 };
6124 ctx.push(Value::Int(value));
6125 return Ok(true);
6126 }
6127 SET_SAVELOAD_ALERT_ONOFF => cfg_set_int(
6128 &mut ctx.globals.syscom,
6129 GET_SAVELOAD_ALERT_ONOFF,
6130 if p_bool(params, 0) { 1 } else { 0 },
6131 ),
6132 SET_SAVELOAD_ALERT_ONOFF_DEFAULT => {
6133 cfg_set_int(&mut ctx.globals.syscom, GET_SAVELOAD_ALERT_ONOFF, 1)
6134 }
6135 GET_SAVELOAD_ALERT_ONOFF => {
6136 let v = cfg_get_int(&ctx.globals.syscom, GET_SAVELOAD_ALERT_ONOFF, 1);
6137 ctx.push(Value::Int(v));
6138 return Ok(true);
6139 }
6140 SET_SLEEP_ONOFF => cfg_set_int(
6141 &mut ctx.globals.syscom,
6142 GET_SLEEP_ONOFF,
6143 if p_bool(params, 0) { 1 } else { 0 },
6144 ),
6145 SET_SLEEP_ONOFF_DEFAULT => {
6146 let value = gameexe_bool_or(ctx, "CONFIG.SLEEP.ONOFF", false);
6147 cfg_set_int(
6148 &mut ctx.globals.syscom,
6149 GET_SLEEP_ONOFF,
6150 if value { 1 } else { 0 },
6151 );
6152 }
6153 GET_SLEEP_ONOFF => {
6154 let v = cfg_get_int(&ctx.globals.syscom, GET_SLEEP_ONOFF, 0);
6155 ctx.push(Value::Int(v));
6156 return Ok(true);
6157 }
6158 SET_NO_WIPE_ANIME_ONOFF => cfg_set_int(
6159 &mut ctx.globals.syscom,
6160 GET_NO_WIPE_ANIME_ONOFF,
6161 if p_bool(params, 0) { 1 } else { 0 },
6162 ),
6163 SET_NO_WIPE_ANIME_ONOFF_DEFAULT => {
6164 let value = gameexe_bool_or(ctx, "CONFIG.NO_WIPE_ANIME.ONOFF", false);
6165 cfg_set_int(
6166 &mut ctx.globals.syscom,
6167 GET_NO_WIPE_ANIME_ONOFF,
6168 if value { 1 } else { 0 },
6169 );
6170 }
6171 GET_NO_WIPE_ANIME_ONOFF => {
6172 let v = cfg_get_int(&ctx.globals.syscom, GET_NO_WIPE_ANIME_ONOFF, 0);
6173 ctx.push(Value::Int(v));
6174 return Ok(true);
6175 }
6176 SET_SKIP_WIPE_ANIME_ONOFF => cfg_set_int(
6177 &mut ctx.globals.syscom,
6178 GET_SKIP_WIPE_ANIME_ONOFF,
6179 if p_bool(params, 0) { 1 } else { 0 },
6180 ),
6181 SET_SKIP_WIPE_ANIME_ONOFF_DEFAULT => {
6182 let value = gameexe_bool_or(ctx, "CONFIG.SKIP_WIPE_ANIME.ONOFF", true);
6183 cfg_set_int(
6184 &mut ctx.globals.syscom,
6185 GET_SKIP_WIPE_ANIME_ONOFF,
6186 if value { 1 } else { 0 },
6187 );
6188 }
6189 GET_SKIP_WIPE_ANIME_ONOFF => {
6190 let v = cfg_get_int(&ctx.globals.syscom, GET_SKIP_WIPE_ANIME_ONOFF, 1);
6191 ctx.push(Value::Int(v));
6192 return Ok(true);
6193 }
6194 SET_NO_MWND_ANIME_ONOFF => cfg_set_int(
6195 &mut ctx.globals.syscom,
6196 GET_NO_MWND_ANIME_ONOFF,
6197 if p_bool(params, 0) { 1 } else { 0 },
6198 ),
6199 SET_NO_MWND_ANIME_ONOFF_DEFAULT => {
6200 let value = gameexe_bool_or(ctx, "CONFIG.NO_MWND_ANIME.ONOFF", false);
6201 cfg_set_int(
6202 &mut ctx.globals.syscom,
6203 GET_NO_MWND_ANIME_ONOFF,
6204 if value { 1 } else { 0 },
6205 );
6206 }
6207 GET_NO_MWND_ANIME_ONOFF => {
6208 let v = cfg_get_int(&ctx.globals.syscom, GET_NO_MWND_ANIME_ONOFF, 0);
6209 ctx.push(Value::Int(v));
6210 return Ok(true);
6211 }
6212 SET_WHEEL_NEXT_MESSAGE_ONOFF => cfg_set_int(
6213 &mut ctx.globals.syscom,
6214 GET_WHEEL_NEXT_MESSAGE_ONOFF,
6215 if p_bool(params, 0) { 1 } else { 0 },
6216 ),
6217 SET_WHEEL_NEXT_MESSAGE_ONOFF_DEFAULT => {
6218 let value = gameexe_bool_or(ctx, "CONFIG.WHEEL_NEXT_MESSAGE.ONOFF", true);
6219 cfg_set_int(
6220 &mut ctx.globals.syscom,
6221 GET_WHEEL_NEXT_MESSAGE_ONOFF,
6222 if value { 1 } else { 0 },
6223 );
6224 }
6225 GET_WHEEL_NEXT_MESSAGE_ONOFF => {
6226 let v = cfg_get_int(&ctx.globals.syscom, GET_WHEEL_NEXT_MESSAGE_ONOFF, 1);
6227 ctx.push(Value::Int(v));
6228 return Ok(true);
6229 }
6230 SET_KOE_DONT_STOP_ONOFF => cfg_set_int(
6231 &mut ctx.globals.syscom,
6232 GET_KOE_DONT_STOP_ONOFF,
6233 if p_bool(params, 0) { 1 } else { 0 },
6234 ),
6235 SET_KOE_DONT_STOP_ONOFF_DEFAULT => {
6236 let value = gameexe_bool_or(ctx, "CONFIG.KOE_DONT_STOP.ONOFF", false);
6237 cfg_set_int(
6238 &mut ctx.globals.syscom,
6239 GET_KOE_DONT_STOP_ONOFF,
6240 if value { 1 } else { 0 },
6241 );
6242 }
6243 GET_KOE_DONT_STOP_ONOFF => {
6244 let v = cfg_get_int(&ctx.globals.syscom, GET_KOE_DONT_STOP_ONOFF, 0);
6245 ctx.push(Value::Int(v));
6246 return Ok(true);
6247 }
6248 SET_SKIP_UNREAD_MESSAGE_ONOFF => cfg_set_int(
6249 &mut ctx.globals.syscom,
6250 GET_SKIP_UNREAD_MESSAGE_ONOFF,
6251 if p_bool(params, 0) { 1 } else { 0 },
6252 ),
6253 SET_SKIP_UNREAD_MESSAGE_ONOFF_DEFAULT => {
6254 let value = gameexe_bool_or(ctx, "CONFIG.SKIP_UNREAD_MESSAGE.ONOFF", false);
6255 cfg_set_int(
6256 &mut ctx.globals.syscom,
6257 GET_SKIP_UNREAD_MESSAGE_ONOFF,
6258 if value { 1 } else { 0 },
6259 );
6260 }
6261 GET_SKIP_UNREAD_MESSAGE_ONOFF => {
6262 let v = cfg_get_int(&ctx.globals.syscom, GET_SKIP_UNREAD_MESSAGE_ONOFF, 0);
6263 ctx.push(Value::Int(v));
6264 return Ok(true);
6265 }
6266 SET_PLAY_SILENT_SOUND_ONOFF => cfg_set_int(
6267 &mut ctx.globals.syscom,
6268 GET_PLAY_SILENT_SOUND_ONOFF,
6269 if p_bool(params, 0) { 1 } else { 0 },
6270 ),
6271 SET_PLAY_SILENT_SOUND_ONOFF_DEFAULT => {
6272 cfg_set_int(&mut ctx.globals.syscom, GET_PLAY_SILENT_SOUND_ONOFF, 0)
6273 }
6274 GET_PLAY_SILENT_SOUND_ONOFF => {
6275 let v = cfg_get_int(&ctx.globals.syscom, GET_PLAY_SILENT_SOUND_ONOFF, 0);
6276 ctx.push(Value::Int(v));
6277 return Ok(true);
6278 }
6279 SET_FONT_NAME => {
6280 let v = params
6281 .first()
6282 .and_then(|v| v.as_str())
6283 .unwrap_or("")
6284 .to_string();
6285 ctx.globals.syscom.original_config.font_name = v.clone();
6286 cfg_set_str(&mut ctx.globals.syscom, GET_FONT_NAME, v);
6287 }
6288 SET_FONT_NAME_DEFAULT => {
6289 let value = config_default_font_name(ctx);
6290 ctx.globals.syscom.original_config.font_name = value.clone();
6291 cfg_set_str(&mut ctx.globals.syscom, GET_FONT_NAME, value);
6292 }
6293 GET_FONT_NAME => {
6294 let mut v = cfg_get_str(&ctx.globals.syscom, GET_FONT_NAME);
6295 if v.is_empty() {
6296 v = config_default_font_name(ctx);
6297 }
6298 ctx.push(Value::Str(v));
6299 return Ok(true);
6300 }
6301 IS_FONT_EXIST => {
6302 let name = params.first().and_then(|v| v.as_str()).unwrap_or("");
6303 let exists = font_exists(&ctx.project_dir, name);
6304 ctx.push(Value::Int(if exists { 1 } else { 0 }));
6305 return Ok(true);
6306 }
6307 SET_FONT_BOLD => {
6308 let value = p_bool(params, 0);
6309 ctx.globals.syscom.original_config.font_futoku = value;
6310 cfg_set_int(
6311 &mut ctx.globals.syscom,
6312 GET_FONT_BOLD,
6313 if value { 1 } else { 0 },
6314 );
6315 }
6316 SET_FONT_BOLD_DEFAULT => {
6317 let value = gameexe_bool_or(ctx, "CONFIG.FONT.FUTOKU", false);
6318 ctx.globals.syscom.original_config.font_futoku = value;
6319 cfg_set_int(
6320 &mut ctx.globals.syscom,
6321 GET_FONT_BOLD,
6322 if value { 1 } else { 0 },
6323 );
6324 }
6325 GET_FONT_BOLD => {
6326 let v = cfg_get_int(&ctx.globals.syscom, GET_FONT_BOLD, 0);
6327 ctx.push(Value::Int(v));
6328 return Ok(true);
6329 }
6330 SET_FONT_DECORATION => {
6331 let value = p_i64(params, 0).clamp(0, 3);
6332 ctx.globals.syscom.original_config.font_shadow = value;
6333 cfg_set_int(&mut ctx.globals.syscom, GET_FONT_DECORATION, value);
6334 }
6335 SET_FONT_DECORATION_DEFAULT => {
6336 let value = gameexe_i64_or(ctx, "CONFIG.FONT.SHADOW", 2).clamp(0, 3);
6337 ctx.globals.syscom.original_config.font_shadow = value;
6338 cfg_set_int(&mut ctx.globals.syscom, GET_FONT_DECORATION, value);
6339 }
6340 GET_FONT_DECORATION => {
6341 let v = cfg_get_int(&ctx.globals.syscom, GET_FONT_DECORATION, 2);
6342 ctx.push(Value::Int(v));
6343 return Ok(true);
6344 }
6345 CREATE_CAPTURE_BUFFER => {
6346 let w = p_i64(params, 0).max(1) as u32;
6347 let h = p_i64(params, 1).max(1) as u32;
6348 ctx.globals.syscom.capture_size = Some((w, h));
6349 ctx.globals.syscom.capture_buffer = None;
6350 }
6351 DESTROY_CAPTURE_BUFFER => {
6352 ctx.globals.syscom.capture_buffer = None;
6353 ctx.globals.syscom.capture_size = None;
6354 }
6355 CAPTURE_TO_CAPTURE_BUFFER => {
6356 let mut img = ctx.capture_frame_rgba()?;
6357 if let Some((w, h)) = ctx.globals.syscom.capture_size {
6358 img = resize_rgba(&img, w, h);
6359 }
6360 ctx.globals.syscom.capture_buffer = Some(img);
6361 }
6362 SAVE_CAPTURE_BUFFER_TO_FILE => {
6363 let file_name = params.first().and_then(|v| v.as_str()).unwrap_or("");
6364 let extension = params.get(1).and_then(|v| v.as_str()).unwrap_or("");
6365 let mut name = file_name.to_string();
6366 if !extension.is_empty()
6367 && !name
6368 .to_ascii_lowercase()
6369 .ends_with(&format!(".{}", extension.to_ascii_lowercase()))
6370 {
6371 name.push('.');
6372 name.push_str(extension);
6373 }
6374 let path = join_game_path(&ctx.project_dir, &name);
6375 if ctx.globals.syscom.capture_buffer.is_none() {
6376 let mut img = ctx.capture_frame_rgba()?;
6377 if let Some((w, h)) = ctx.globals.syscom.capture_size {
6378 img = resize_rgba(&img, w, h);
6379 }
6380 ctx.globals.syscom.capture_buffer = Some(img);
6381 }
6382 if let Some(img) = ctx.globals.syscom.capture_buffer.as_ref() {
6383 write_rgba_png(&path, img);
6384 save_capture_flags_sidecar(ctx, &path, params);
6385 ctx.push(Value::Int(1));
6386 } else {
6387 ctx.push(Value::Int(0));
6388 }
6389 return Ok(true);
6390 }
6391 LOAD_FLAG_FROM_CAPTURE_FILE => {
6392 let file_name = params.first().and_then(|v| v.as_str()).unwrap_or("");
6393 let extension = params.get(1).and_then(|v| v.as_str()).unwrap_or("");
6394 let mut name = file_name.to_string();
6395 if !extension.is_empty()
6396 && !name
6397 .to_ascii_lowercase()
6398 .ends_with(&format!(".{}", extension.to_ascii_lowercase()))
6399 {
6400 name.push('.');
6401 name.push_str(extension);
6402 }
6403 let path = join_game_path(&ctx.project_dir, &name);
6404 let ok = load_capture_flags_sidecar(ctx, &path, params);
6405 ctx.push(Value::Int(if ok { 1 } else { 0 }));
6406 return Ok(true);
6407 }
6408 CAPTURE_AND_SAVE_BUFFER_TO_PNG => {
6409 let file_name = params.get(2).and_then(|v| v.as_str()).unwrap_or("");
6410 let path = join_game_path(&ctx.project_dir, file_name);
6411 let mut img = ctx.capture_frame_rgba()?;
6412 if let Some((w, h)) = ctx.globals.syscom.capture_size {
6413 img = resize_rgba(&img, w, h);
6414 }
6415 write_rgba_png(&path, &img);
6416 }
6417 OPEN_TWEET_DIALOG => {
6418 open_tweet_dialog(ctx)?;
6419 }
6420 SET_RETURN_SCENE_ONCE => {
6421 let name = params
6422 .first()
6423 .and_then(|v| v.as_str())
6424 .unwrap_or("")
6425 .to_string();
6426 let z_no = p_i64(params, 1);
6427 ctx.globals.syscom.return_scene_once = Some((name, z_no));
6428 }
6429 GET_SYSTEM_EXTRA_INT_VALUE => {
6430 let v = ctx.globals.syscom.system_extra_int_value;
6431 ctx.push(Value::Int(v));
6432 return Ok(true);
6433 }
6434 GET_SYSTEM_EXTRA_STR_VALUE => {
6435 let v = ctx.globals.syscom.system_extra_str_value.clone();
6436 ctx.push(Value::Str(v));
6437 return Ok(true);
6438 }
6439 JOYPAD_MODE_ACTIVE => {
6440 let joypad_mode_override = if ctx.excall_state.ex_call_flag {
6444 ctx.excall_state.joypad_mode_override
6445 } else {
6446 ctx.globals.script.joypad_mode_override
6447 };
6448 let allow = match joypad_mode_override {
6449 0 => false,
6450 1 => true,
6451 _ => gameexe_bool_or(ctx, "JOYPAD.ALLOW_JOYPAD_MODE", true),
6452 };
6453 ctx.push(Value::Int(if allow && ctx.input.joypad_mode_active() {
6454 1
6455 } else {
6456 0
6457 }));
6458 return Ok(true);
6459 }
6460 OPEN_JOYPAD_CONFIG => {
6461 log::error!("SYSCOM.334 native Joypad configuration window is not implemented");
6466 }
6467 _ => {
6468 return Ok(false);
6469 }
6470 }
6471
6472 ctx.push(Value::Int(0));
6473 Ok(true)
6474}
6475
6476#[cfg(test)]
6477mod tweet_compat_tests {
6478 use super::*;
6479
6480 #[test]
6481 fn tweet_overlap_is_stretched_and_source_alpha_blended() {
6482 let mut base = RgbaImage {
6483 width: 2,
6484 height: 2,
6485 center_x: 0,
6486 center_y: 0,
6487 rgba: [0, 0, 0, 255].repeat(4),
6488 };
6489 let overlay = RgbaImage {
6490 width: 1,
6491 height: 1,
6492 center_x: 0,
6493 center_y: 0,
6494 rgba: vec![255, 64, 0, 128],
6495 };
6496
6497 blend_tweet_overlay_fullscreen(&mut base, &overlay);
6498 for pixel in base.rgba.as_chunks::<4>().0.iter() {
6499 assert_eq!(pixel, &[128, 32, 0, 255]);
6500 }
6501 }
6502}
6503
6504#[cfg(test)]
6505mod global_save_init_tests {
6506 use super::*;
6507 use crate::runtime::forms::codes;
6508 use std::fs;
6509 use std::sync::atomic::{AtomicU64, Ordering};
6510
6511 #[test]
6512 fn save_summary_uses_retained_page_only_when_short_summary_is_empty() {
6513 let mut slot = SaveSlotState::default();
6514 for op in [GET_SAVE_MESSAGE, GET_QUICK_SAVE_MESSAGE] {
6515 assert_eq!(slot_str(&slot, op), "");
6516 }
6517 slot.full_message = "retained page".into();
6518 for op in [GET_SAVE_MESSAGE, GET_QUICK_SAVE_MESSAGE] {
6519 assert_eq!(slot_str(&slot, op), "retained page");
6520 }
6521 slot.message = "current block".into();
6522 for op in [GET_SAVE_MESSAGE, GET_QUICK_SAVE_MESSAGE] {
6523 assert_eq!(slot_str(&slot, op), "current block");
6524 }
6525 assert_eq!(slot_str(&slot, GET_SAVE_FULL_MESSAGE), "retained page");
6526 }
6527
6528 fn test_project_dir() -> std::path::PathBuf {
6529 static NEXT: AtomicU64 = AtomicU64::new(0);
6530 std::env::temp_dir().join(format!(
6531 "siglus-global-save-test-{}-{}",
6532 std::process::id(),
6533 NEXT.fetch_add(1, Ordering::Relaxed)
6534 ))
6535 }
6536
6537 #[test]
6538 fn config_save_versions_preserve_settings_after_optional_fields() {
6539 for (minor_version, planetarian, sound_count) in [
6540 (0, false, 4),
6541 (0, false, 5),
6542 (1, false, 32),
6543 (2, false, 32),
6544 (2, true, 32),
6545 (3, false, 32),
6546 (4, false, 32),
6547 ] {
6548 let project_dir = test_project_dir();
6549 let mut stream = original_save::OriginalStreamWriter::new();
6550 stream.push_i32(0); if minor_version >= 3 || planetarian {
6552 stream.push_i32(1); }
6554 stream.push_i32(80);
6555 stream.push_i32(90);
6556 if minor_version >= 3 || planetarian {
6557 stream.push_i32(1280);
6558 stream.push_i32(720);
6559 }
6560 stream.push_bool(false); for value in [1, 0, 28, 27, 1920, 1080, 1, 100, 100] {
6562 stream.push_i32(value);
6563 }
6564 stream.push_bool(true); stream.push_i32(0);
6566 stream.push_i32(0);
6567 stream.push_i32(180); for _ in 0..sound_count {
6569 stream.push_i32(200);
6570 }
6571 for _ in 0..=sound_count {
6572 stream.push_bool(true);
6573 }
6574 stream.push_i32(175); stream.push_bool(true);
6576 stream.push_u32(0xe61d0818); stream.push_bool(false); stream.push_str("Test Font");
6579 stream.push_i32(2); stream.push_bool(false); stream.push_i32(20); stream.push_bool(false); stream.push_bool(false); stream.push_i32(70);
6585 stream.push_i32(300);
6586 if minor_version >= 2 {
6587 stream.push_bool(true); stream.push_i32(2468);
6589 }
6590 stream.push_bool(true); stream.push_bool(false); stream.push_bool(true); stream.push_i32(125); stream.push_i32(1); stream.push_i32(1); stream.push_bool(false);
6597 stream.push_padding(3);
6598 stream.push_i32(170);
6599 stream.push_bool(true); for _ in 0..2 {
6601 stream.push_i32(4);
6603 for flag in [true, false, true, false] {
6604 stream.push_bool(flag);
6605 }
6606 }
6607 stream.push_i32(4); for value in [0, 1, 2, 3] {
6609 stream.push_i32(value);
6610 }
6611 for flag in [false, false, false, false, true, false, false] {
6612 stream.push_bool(flag);
6613 }
6614 if minor_version == 0 {
6615 if sound_count == 5 {
6616 stream.push_bool(true);
6617 }
6618 stream.push_i32(0x12345678);
6619 }
6620 stream.push_bool(true); stream.push_bool(false); for path in ["screenshots", "editor"] {
6623 stream.push_str(path);
6624 }
6625 if minor_version >= 1 {
6626 stream.push_str("voices");
6627 stream.push_str("voice-tool");
6628 }
6629 if minor_version >= 4 {
6630 stream.push_bool(true); stream.push_bool(false); }
6633 let payload = stream.into_inner();
6634 let packed = original_save::pack_buffer(&payload);
6635 let mut data = original_save::OriginalConfigSaveHeader {
6636 major_version: 1,
6637 minor_version,
6638 config_data_size: packed.len() as i32,
6639 }
6640 .to_bytes();
6641 data.extend_from_slice(&packed);
6642 fs::create_dir_all(project_dir.join("savedata")).unwrap();
6643 fs::write(project_dir.join("savedata/config.sav"), data).unwrap();
6644 let mut ctx = CommandContext::new(project_dir.clone());
6645 if planetarian {
6646 ctx.tables.gameexe = Some(crate::formats::gameexe::GameexeConfig::from_text(
6647 "#GAMEID=\"planetarian [HD Edition]\"\n",
6648 ));
6649 }
6650 let defaults = original_config_defaults(&ctx);
6651
6652 load_config_save(&mut ctx).unwrap();
6653 let cfg = &ctx.globals.syscom.original_config;
6654 assert_eq!(cfg.screen_size_scale, (80, 90));
6655 if minor_version >= 3 || planetarian {
6656 assert_eq!(cfg.screen_size_mode_window, 1);
6657 assert_eq!(cfg.screen_size_free, (1280, 720));
6658 }
6659 assert_eq!(cfg.all_sound_user_volume, 180);
6660 assert!(
6661 cfg.sound_user_volume[..sound_count]
6662 .iter()
6663 .all(|&value| value == 200)
6664 );
6665 if minor_version == 0 {
6666 assert_eq!(
6667 &cfg.sound_user_volume[sound_count..],
6668 &defaults.sound_user_volume[sound_count..]
6669 );
6670 assert_eq!(
6671 &cfg.play_sound_check[sound_count..],
6672 &defaults.play_sound_check[sound_count..]
6673 );
6674 }
6675 assert_eq!(cfg.font_name, "Test Font");
6676 assert_eq!(cfg.auto_mode_min_wait, 300);
6677 assert_eq!(
6678 cfg.mouse_cursor_hide_onoff,
6679 if minor_version < 2 {
6680 defaults.mouse_cursor_hide_onoff
6681 } else {
6682 true
6683 }
6684 );
6685 assert_eq!(
6686 cfg.mouse_cursor_hide_time,
6687 if minor_version < 2 {
6688 defaults.mouse_cursor_hide_time
6689 } else {
6690 2468
6691 }
6692 );
6693 assert!(cfg.jitan_normal_onoff && cfg.jitan_msgbk_onoff);
6694 assert!(!cfg.jitan_auto_mode_onoff);
6695 assert_eq!(cfg.jitan_speed, 125);
6696 assert_eq!(cfg.koe_mode, 1);
6697 assert!(!cfg.chrkoe[0].onoff);
6698 assert_eq!(cfg.chrkoe[0].volume, 170);
6699 assert_eq!(cfg.global_extra_mode_flag, [0, 1, 2, 3]);
6700 assert!(cfg.saveload_alert_flag);
6701 assert!(!cfg.saveload_dblclick_flag);
6702 assert_eq!(cfg.ss_path, "screenshots");
6703 assert_eq!(cfg.editor_path, "editor");
6704 assert_eq!(
6705 cfg.koe_path,
6706 if minor_version == 0 {
6707 defaults.koe_path.as_str()
6708 } else {
6709 "voices"
6710 }
6711 );
6712 assert_eq!(
6713 cfg.koe_tool_path,
6714 if minor_version == 0 {
6715 defaults.koe_tool_path.as_str()
6716 } else {
6717 "voice-tool"
6718 }
6719 );
6720 assert_eq!(cfg.joypad_swap_ab, minor_version >= 4);
6721 assert!(!cfg.joypad_swap_xy);
6722 let header = original_save::OriginalConfigSaveHeader {
6723 major_version: 1,
6724 minor_version,
6725 config_data_size: 0,
6726 };
6727 let game_id = gameexe_unquoted_owned(&ctx, "GAMEID");
6728 assert!(
6729 save_versions::read_config(
6730 header,
6731 &payload[..payload.len() - 1],
6732 &mut defaults.clone(),
6733 &game_id
6734 )
6735 .is_err()
6736 );
6737 let mut trailing = payload.clone();
6738 trailing.push(0);
6739 assert!(
6740 save_versions::read_config(header, &trailing, &mut defaults.clone(), &game_id)
6741 .is_err()
6742 );
6743 let unknown = original_save::OriginalConfigSaveHeader {
6744 minor_version: 99,
6745 ..header
6746 };
6747 assert!(
6748 save_versions::read_config(unknown, &payload, &mut defaults.clone(), &game_id)
6749 .unwrap_err()
6750 .to_string()
6751 .contains("unsupported config save version")
6752 );
6753 fs::remove_dir_all(project_dir).unwrap();
6754 }
6755 }
6756
6757 #[test]
6758 fn config_dialog_edits_update_script_state_and_survive_reopening() {
6759 let project_dir = test_project_dir();
6760 fs::create_dir_all(&project_dir).unwrap();
6761 let mut ctx = CommandContext::new(project_dir.clone());
6762 let gameexe = crate::formats::gameexe::GameexeConfig::from_text(
6763 "#CONFIG.VOLUME.BGM=165\n#CHRKOE.000=\"Hidden\",2,\"Alias\",1,235,(0)\n",
6764 );
6765 ctx.tables.gameexe = Some(gameexe.clone());
6766 load_global_save(&mut ctx).unwrap();
6767 let mut config = config_state_for_save(&ctx);
6768 config.sound_user_volume[0] = 87;
6769 config.play_sound_check[3] = false;
6770 config.message_speed = 31;
6771 config.auto_mode_moji_wait = 90;
6772 config.chrkoe[0].volume = 170;
6773 config.global_extra_switch_flag[0] = false;
6774 config.skip_unread_message_flag = true;
6775 config.joypad_swap_ab = true;
6776 config.joypad_swap_xy = true;
6777 apply_config_dialog_state(&mut ctx, config.clone());
6778 assert_eq!(cfg_get_int(&ctx.globals.syscom, GET_BGM_VOLUME, -1), 87);
6779 assert_eq!(ctx.globals.script.auto_mode_moji_wait, 90);
6780 assert_eq!(config_state_for_save(&ctx), config);
6781 write_config_save(&ctx);
6782 reveal_config_voice_name(&mut ctx, "Alias");
6783 write_global_save(&ctx);
6784
6785 let mut reopened = CommandContext::new(project_dir.clone());
6786 reopened.tables.gameexe = Some(gameexe);
6787 load_global_save(&mut reopened).unwrap();
6788 assert_eq!(config_state_for_save(&reopened), config);
6789 assert!(config_chrkoe_name_visible(&reopened, 0));
6790 fs::remove_dir_all(project_dir).unwrap();
6791 }
6792
6793 #[test]
6794 fn missing_global_save_keeps_zero_initialized_flags() {
6795 let project_dir = test_project_dir();
6796 fs::create_dir_all(&project_dir).expect("test project dir");
6797 let mut ctx = CommandContext::new(project_dir.clone());
6798
6799 load_global_save(&mut ctx).expect("missing global save is valid");
6800
6801 let g = &ctx.globals.int_lists[&(codes::ELM_GLOBAL_G as u32)];
6802 let z = &ctx.globals.int_lists[&(codes::ELM_GLOBAL_Z as u32)];
6803 let m = &ctx.globals.str_lists[&(codes::ELM_GLOBAL_M as u32)];
6804 assert!(!g.is_empty() && g.iter().all(|value| *value == 0));
6805 assert!(!z.is_empty() && z.iter().all(|value| *value == 0));
6806 assert!(!m.is_empty() && m.iter().all(String::is_empty));
6807
6808 let _ = fs::remove_dir_all(project_dir);
6809 }
6810
6811 #[test]
6812 fn corrupt_existing_global_save_is_an_error() {
6813 let project_dir = test_project_dir();
6814 let save_dir = project_dir.join("savedata");
6815 fs::create_dir_all(&save_dir).expect("test save dir");
6816 fs::write(save_dir.join("global.sav"), b"invalid").expect("corrupt global save");
6817 let mut ctx = CommandContext::new(project_dir.clone());
6818
6819 assert!(load_global_save(&mut ctx).is_err());
6820
6821 let _ = fs::remove_dir_all(project_dir);
6822 }
6823
6824 #[test]
6825 fn global_save_with_uninitialized_cg_table_preserves_following_fields() {
6826 let project_dir = test_project_dir();
6827 let mut stream = original_save::OriginalStreamWriter::new();
6828 stream.push_i64(61151);
6829 stream.push_fixed_i32_list(&[42], 10000);
6830 stream.push_fixed_i32_list(&[7], 10000);
6831 stream.push_fixed_str_list(&["global".to_string()], 10000);
6832 stream.push_fixed_str_list(&[], 702);
6833 stream.push_i32(0);
6834 stream.push_extend_i32_list(&[]);
6835 stream.push_fixed_i32_list(&[1, 0, 1], 20);
6836 stream.push_i32(2);
6837 stream.push_str("first");
6838 stream.push_bool(true);
6839 stream.push_str("second");
6840 stream.push_bool(false);
6841 let payload = stream.into_inner();
6842 original_save::write_global_save_file(&project_dir, &payload).unwrap();
6843
6844 let mut ctx = CommandContext::new(project_dir.clone());
6845 load_global_save(&mut ctx).expect("native empty extendable CG table");
6846 assert_eq!(ctx.globals.syscom.total_play_time, 61151);
6847 assert_eq!(ctx.globals.int_lists[&(codes::ELM_GLOBAL_G as u32)][0], 42);
6848 assert_eq!(ctx.globals.int_lists[&(codes::ELM_GLOBAL_Z as u32)][0], 7);
6849 assert_eq!(
6850 ctx.globals.str_lists[&(codes::ELM_GLOBAL_M as u32)][0],
6851 "global"
6852 );
6853 assert!(ctx.tables.cg_flags.is_empty());
6854 assert_eq!(ctx.globals.bgm_table_flags.len(), 20);
6855 assert_eq!(&ctx.globals.bgm_table_flags[..3], &[true, false, true]);
6856 assert_eq!(
6857 ctx.globals.syscom.chrkoe_look_flags.get("first"),
6858 Some(&true)
6859 );
6860 assert_eq!(
6861 ctx.globals.syscom.chrkoe_look_flags.get("second"),
6862 Some(&false)
6863 );
6864
6865 original_save::write_global_save_file(&project_dir, &payload[..payload.len() - 1]).unwrap();
6867 assert!(load_global_save(&mut ctx).is_err());
6868 fs::remove_dir_all(project_dir).unwrap();
6869 }
6870
6871 #[test]
6872 fn global_save_writer_uses_native_empty_cg_layout() {
6873 let project_dir = test_project_dir();
6874 fs::create_dir_all(&project_dir).unwrap();
6875 let mut ctx = CommandContext::new(project_dir.clone());
6876 ctx.tables.gameexe = Some(crate::formats::gameexe::GameexeConfig::from_text(
6877 "#CGTABLE_FILE=\"\"\n#CGTABLE_FLAG_CNT=1000\n",
6878 ));
6879 ctx.globals.bgm_table_flags = vec![true, false, true];
6880 write_global_save(&ctx);
6881 let (_, payload) = original_save::read_global_save_file(&project_dir).unwrap();
6882 let mut rd = original_save::OriginalStreamReader::new(&payload);
6883 rd.i64().unwrap();
6884 rd.fixed_i32_list().unwrap();
6885 rd.fixed_i32_list().unwrap();
6886 rd.fixed_str_list().unwrap();
6887 rd.fixed_str_list().unwrap();
6888 rd.i32().unwrap();
6889 assert_eq!(rd.i32().unwrap(), 0, "CG count has no preceding jump");
6890 assert_eq!(&rd.fixed_i32_list().unwrap()[..3], &[1, 0, 1]);
6891 fs::remove_dir_all(project_dir).unwrap();
6892 }
6893
6894 #[test]
6895 fn original_global_save_uses_one_byte_character_voice_flags() {
6896 for (major, minor) in [(1, 2), (1, 5), (2, 0)] {
6897 for empty_cg in [false, true] {
6898 check_original_global_save(major, minor, empty_cg);
6899 }
6900 }
6901 }
6902
6903 fn check_original_global_save(major_version: i32, minor_version: i32, empty_cg: bool) {
6904 let project_dir = test_project_dir();
6905 fs::create_dir_all(&project_dir).expect("test project dir");
6906
6907 let mut stream = original_save::OriginalStreamWriter::new();
6908 stream.push_i64(1234);
6909 stream.push_fixed_i32_list(&[42], 1);
6910 stream.push_fixed_i32_list(&[7], 1);
6911 stream.push_fixed_str_list(&["global".to_string()], 1);
6912 stream.push_fixed_str_list(&["名前".to_string()], 702);
6913 stream.push_i32(1359557559);
6914 if empty_cg {
6915 stream.push_extend_i32_list(&[]);
6916 } else {
6917 stream.push_fixed_i32_list(&[1, 0, 1], 1000);
6918 }
6919 stream.push_fixed_i32_list(&[0, 1], 52);
6920 stream.push_i32(2);
6921 stream.push_str("桜");
6922 stream.push_bool(true);
6923 stream.push_str("second");
6924 stream.push_bool(false);
6925 let payload = stream.into_inner();
6926 let packed = original_save::pack_buffer(&payload);
6927 let mut data = original_save::OriginalGlobalSaveHeader {
6928 major_version,
6929 minor_version,
6930 global_data_size: packed.len() as i32,
6931 }
6932 .to_bytes();
6933 data.extend_from_slice(&packed);
6934 let path = project_dir.join("savedata/global.sav");
6935 fs::create_dir_all(path.parent().unwrap()).unwrap();
6936 fs::write(&path, &data).expect("synthetic global save");
6937
6938 let mut ctx = CommandContext::new(project_dir.clone());
6939 load_global_save(&mut ctx).expect("one-byte chrkoe flags");
6940 assert_eq!(ctx.globals.syscom.total_play_time, 1234);
6941 assert_eq!(ctx.globals.int_lists[&(codes::ELM_GLOBAL_G as u32)][0], 42);
6942 assert_eq!(ctx.globals.int_lists[&(codes::ELM_GLOBAL_Z as u32)][0], 7);
6943 assert_eq!(
6944 ctx.globals.str_lists[&(codes::ELM_GLOBAL_M as u32)][0],
6945 "global"
6946 );
6947 assert_eq!(
6948 ctx.globals.str_lists[&(codes::ELM_GLOBAL_NAMAE_GLOBAL as u32)][0],
6949 "名前"
6950 );
6951 if empty_cg {
6952 assert!(ctx.tables.cg_flags.is_empty());
6953 } else {
6954 assert_eq!(&ctx.tables.cg_flags[..3], &[1, 0, 1]);
6955 }
6956 assert_eq!(&ctx.globals.bgm_table_flags[..2], &[false, true]);
6957 assert_eq!(ctx.globals.syscom.chrkoe_look_flags.get("桜"), Some(&true));
6958 assert_eq!(
6959 ctx.globals.syscom.chrkoe_look_flags.get("second"),
6960 Some(&false)
6961 );
6962
6963 let header = original_save::OriginalGlobalSaveHeader {
6964 major_version,
6965 minor_version,
6966 global_data_size: 0,
6967 };
6968 assert!(save_versions::read_global(header, &payload[..payload.len() - 1]).is_err());
6969 let mut trailing = payload;
6970 trailing.push(0);
6971 assert!(save_versions::read_global(header, &trailing).is_err());
6972
6973 fs::write(&path, &data[..data.len() - 1]).unwrap();
6974 assert!(
6975 load_global_save(&mut ctx)
6976 .unwrap_err()
6977 .to_string()
6978 .contains("payload truncated")
6979 );
6980 data[4..8].copy_from_slice(&99i32.to_le_bytes());
6981 fs::write(&path, &data).unwrap();
6982 assert!(
6983 load_global_save(&mut ctx)
6984 .unwrap_err()
6985 .to_string()
6986 .contains("unsupported global save version")
6987 );
6988
6989 let _ = fs::remove_dir_all(project_dir);
6990 }
6991}