1use std::ffi::{c_char, c_void, CStr, CString};
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use crate::platform_time::Instant;
13
14use anyhow::{Context, Result};
15use siglus_assets::gameexe::{decode_gameexe_dat_bytes, GameexeConfig, GameexeDecodeOptions};
16use siglus_assets::scene_pck::{find_scene_pck_in_project, ScenePck, ScenePckDecodeOptions};
17
18use crate::render::Renderer;
19use crate::runtime::globals::{
20 SyscomPendingProc, SyscomPendingProcKind, SystemMessageBoxButton, SystemMessageBoxModalState,
21 WipeState,
22};
23use crate::runtime::input::{VmKey, VmMouseButton};
24use crate::runtime::wait::VmWait;
25use crate::runtime::forms::syscom as syscom_form;
26use crate::runtime::{native_ui, CommandContext, ProcKind};
27use crate::scene_stream::SceneStream;
28use crate::vm::{SceneVm, VmConfig};
29
30const FRAME_INTERVAL_MS: u32 = 16;
31
32
33#[derive(Debug, Clone)]
34pub struct SiglusHostConfig {
35 pub project_dir: PathBuf,
36 pub scene_name: Option<String>,
37 pub scene_id: Option<usize>,
38 pub width: Option<u32>,
39 pub height: Option<u32>,
40}
41
42impl SiglusHostConfig {
43 pub fn new(project_dir: PathBuf) -> Self {
44 Self {
45 project_dir,
46 scene_name: None,
47 scene_id: None,
48 width: None,
49 height: None,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
55struct BootConfig {
56 start_scene: String,
57 start_z: i32,
58 menu_scene: Option<String>,
59 menu_z: i32,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63enum ProcType {
64 Script,
65 StartWarning,
66 SyscomWarning,
67 MsgBack,
68 ReturnToMenu,
69 GameEndWipe,
70 Disp,
71 EndGame,
72 GameTimerStart,
73 TimeWait,
74}
75
76#[derive(Debug, Clone)]
77struct ProcFrame {
78 ty: ProcType,
79 option: i32,
80 deadline_frame: Option<u32>,
81}
82
83#[derive(Debug, Default)]
84struct ProcFlow {
85 stack: Vec<ProcFrame>,
86 booted_menu: bool,
87 pending_syscom_proc: Option<SyscomPendingProc>,
88}
89
90impl ProcFlow {
91 fn push(&mut self, ty: ProcType, option: i32) {
92 self.stack.push(ProcFrame {
93 ty,
94 option,
95 deadline_frame: None,
96 });
97 }
98
99 fn pop(&mut self) {
100 let _ = self.stack.pop();
101 }
102
103 fn top(&self) -> Option<&ProcFrame> {
104 self.stack.last()
105 }
106
107 fn top_mut(&mut self) -> Option<&mut ProcFrame> {
108 self.stack.last_mut()
109 }
110}
111
112#[repr(i32)]
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum SiglusNativeMessageBoxKind {
116 Ok = 0,
117 OkCancel = 1,
118 YesNo = 2,
119 YesNoCancel = 3,
120}
121
122impl From<native_ui::NativeMessageBoxKind> for SiglusNativeMessageBoxKind {
123 fn from(value: native_ui::NativeMessageBoxKind) -> Self {
124 match value {
125 native_ui::NativeMessageBoxKind::Ok => Self::Ok,
126 native_ui::NativeMessageBoxKind::OkCancel => Self::OkCancel,
127 native_ui::NativeMessageBoxKind::YesNo => Self::YesNo,
128 native_ui::NativeMessageBoxKind::YesNoCancel => Self::YesNoCancel,
129 }
130 }
131}
132
133pub type SiglusNativeMessageBoxCallback = unsafe extern "C" fn(
139 user_data: *mut c_void,
140 request_id: u64,
141 kind: i32,
142 title_utf8: *const c_char,
143 message_utf8: *const c_char,
144);
145
146struct CNativeUiBackend {
147 callback: SiglusNativeMessageBoxCallback,
148 user_data: usize,
149}
150
151unsafe impl Send for CNativeUiBackend {}
152unsafe impl Sync for CNativeUiBackend {}
153
154impl native_ui::NativeUiBackend for CNativeUiBackend {
155 fn show_system_messagebox(&self, request: native_ui::NativeMessageBoxRequest) {
156 let title = CString::new(request.title).unwrap_or_else(|_| CString::new("Siglus").unwrap());
157 let message = CString::new(request.message).unwrap_or_else(|_| CString::new("").unwrap());
158 let kind: SiglusNativeMessageBoxKind = request.kind.into();
159 unsafe {
160 (self.callback)(
161 self.user_data as *mut c_void,
162 request.request_id,
163 kind as i32,
164 title.as_ptr(),
165 message.as_ptr(),
166 );
167 }
168 }
169}
170
171pub struct SiglusHost {
172 config: SiglusHostConfig,
173 boot: BootConfig,
174 flow: ProcFlow,
175 renderer: Renderer,
176 vm: SceneVm<'static>,
177 redraw_count: u32,
178 script_needs_pump: bool,
179 script_resume_after_redraw: bool,
180 suppress_render_once: bool,
181 syscom_suspended_waits: Vec<(usize, VmWait)>,
182 paused: bool,
183 pending_exit: bool,
184 last_step: Option<Instant>,
185}
186
187
188fn find_scene_pck_for_host(project_dir: &Path) -> Result<PathBuf> {
189 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
190 {
191 for name in ["Scene.pck", "scene.pck"] {
192 let p = project_dir.join(name);
193 if crate::resource::wasm_path_is_file(&p) {
194 return Ok(p);
195 }
196 }
197 anyhow::bail!("Scene.pck not found in wasm directory");
198 }
199
200 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
201 {
202 Ok(find_scene_pck_in_project(project_dir)?)
203 }
204}
205
206fn load_key_toml_config(project_dir: &Path) -> Result<Option<siglus_assets::key_toml::KeyTomlConfig>> {
207 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
208 {
209 for name in ["key.toml", "Key.toml"] {
210 let p = project_dir.join(name);
211 if crate::resource::wasm_path_is_file(&p) {
212 let text = crate::resource::read_file_to_string(&p)?;
213 return Ok(Some(siglus_assets::key_toml::parse_key_toml(&text)?));
214 }
215 }
216 Ok(None)
217 }
218
219 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
220 {
221 Ok(siglus_assets::key_toml::load_key_toml_from_project_dir(project_dir)?)
222 }
223}
224
225fn load_gameexe_decode_options(project_dir: &Path) -> Result<GameexeDecodeOptions> {
226 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
227 {
228 let mut opt = GameexeDecodeOptions::default();
229 opt.game_angou_code = Some(siglus_assets::keys::GAMEEXE_KEY.to_vec());
230 if let Some(cfg) = load_key_toml_config(project_dir)? {
231 opt.exe_key16 = cfg.exe_key16;
232 opt.base_angou_code = cfg.base_angou_code;
233 if cfg.game_angou_code.is_some() {
234 opt.game_angou_code = cfg.game_angou_code;
235 }
236 if let Some(order) = cfg.chain_order {
237 opt.chain_order = order;
238 }
239 }
240 Ok(opt)
241 }
242
243 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
244 {
245 GameexeDecodeOptions::from_project_dir(project_dir)
246 }
247}
248
249fn load_scene_pck_decode_options(project_dir: &Path) -> Result<ScenePckDecodeOptions> {
250 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
251 {
252 let exe = load_key_toml_config(project_dir)?
253 .and_then(|cfg| cfg.exe_key16)
254 .map(|v| v.to_vec());
255 Ok(ScenePckDecodeOptions {
256 exe_angou_element: exe,
257 easy_angou_code: Some(siglus_assets::keys::SCENE_KEY.to_vec()),
258 })
259 }
260
261 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
262 {
263 ScenePckDecodeOptions::from_project_dir(project_dir)
264 }
265}
266
267impl SiglusHost {
268 pub async fn new_with_renderer(config: SiglusHostConfig, renderer: Renderer) -> Result<Self> {
269 let initial_size = Self::resolve_initial_size(&config);
270 let boot = Self::resolve_boot_config(&config);
271 let mut flow = ProcFlow::default();
272 flow.push(ProcType::Script, 0);
273 flow.push(ProcType::StartWarning, 0);
274 let vm = Self::init_vm(&config, &boot, initial_size)?;
275 Ok(Self {
276 config,
277 boot,
278 flow,
279 renderer,
280 vm,
281 redraw_count: 0,
282 script_needs_pump: true,
283 script_resume_after_redraw: false,
284 suppress_render_once: false,
285 syscom_suspended_waits: Vec::new(),
286 paused: false,
287 pending_exit: false,
288 last_step: None,
289 })
290 }
291
292 pub fn set_native_messagebox_callback(
293 &mut self,
294 callback: Option<SiglusNativeMessageBoxCallback>,
295 user_data: *mut c_void,
296 ) {
297 let backend = callback.map(|cb| {
298 Arc::new(CNativeUiBackend {
299 callback: cb,
300 user_data: user_data as usize,
301 }) as Arc<dyn native_ui::NativeUiBackend>
302 });
303 self.vm.ctx.set_native_ui_backend(backend);
304 }
305
306 pub fn submit_native_messagebox_result(&mut self, request_id: u64, value: i64) {
307 self.vm.ctx.submit_native_messagebox_result(request_id, value);
308 self.script_needs_pump = true;
309 }
310
311 pub fn resize(&mut self, width: u32, height: u32, scale_factor: f32) {
312 self.renderer.resize_with_scale(width, height, scale_factor.max(1.0));
313 let logical_w = ((width as f32) / scale_factor.max(1.0)).max(1.0).round() as u32;
314 let logical_h = ((height as f32) / scale_factor.max(1.0)).max(1.0).round() as u32;
315 self.vm.ctx.set_screen_size(logical_w, logical_h);
316 self.script_needs_pump = true;
317 }
318
319 pub fn resize_with_logical_viewport(
320 &mut self,
321 surface_width: u32,
322 surface_height: u32,
323 scale_factor: f32,
324 logical_width: u32,
325 logical_height: u32,
326 viewport_x: u32,
327 viewport_y: u32,
328 viewport_width: u32,
329 viewport_height: u32,
330 ) {
331 self.renderer.resize_with_logical_viewport(
332 surface_width,
333 surface_height,
334 scale_factor.max(1.0),
335 logical_width.max(1),
336 logical_height.max(1),
337 viewport_x,
338 viewport_y,
339 viewport_width.max(1),
340 viewport_height.max(1),
341 );
342 self.vm
343 .ctx
344 .set_screen_size(logical_width.max(1), logical_height.max(1));
345 self.script_needs_pump = true;
346 }
347
348 pub fn logical_size(&self) -> (u32, u32) {
349 (
350 self.vm.ctx.screen_w.max(1),
351 self.vm.ctx.screen_h.max(1),
352 )
353 }
354
355 pub fn debug_status_summary(&mut self) -> String {
356 let blocked = self.vm.is_blocked();
357 let movie_playing = self.vm.ctx.globals.mov.playing;
358 let movie_file = self.vm.ctx.globals.mov.file_name.clone();
359 let movie_timer = self.vm.ctx.globals.mov.timer_ms;
360 let movie_frame = self.vm.ctx.globals.mov.last_frame_idx;
361 format!(
362 "scene={:?} line={} pending_exit={} vm_halted={} active_flag={} flow={:?} blocked={} movie_playing={} movie_file={:?} movie_timer={} movie_frame={:?}",
363 self.vm.current_scene_name(),
364 self.vm.current_line_no(),
365 self.pending_exit,
366 self.vm.is_halted(),
367 self.vm.ctx.globals.system.active_flag,
368 self.flow.stack,
369 blocked,
370 movie_playing,
371 movie_file,
372 movie_timer,
373 movie_frame,
374 )
375 }
376
377 pub fn step(&mut self, dt_ms: u32) -> Result<bool> {
379 let _ = dt_ms;
380 self.last_step = Some(Instant::now());
381 if self.script_needs_pump || self.vm.ctx.wait.needs_runtime_poll() {
382 self.pump_vm()?;
383 }
384 self.redraw()?;
385 Ok(self.pending_exit || (self.vm.is_halted() && self.flow.stack.is_empty()))
386 }
387
388 pub fn mouse_move(&mut self, x: f64, y: f64) {
389 self.vm.ctx.on_mouse_move(x.round() as i32, y.round() as i32);
390 self.script_needs_pump = true;
391 }
392
393 pub fn mouse_down(&mut self, button: VmMouseButton) {
394 self.vm.ctx.on_mouse_down(button);
395 self.script_needs_pump = true;
396 }
397
398 pub fn mouse_up(&mut self, button: VmMouseButton) {
399 self.vm.ctx.on_mouse_up(button);
400 self.script_needs_pump = true;
401 }
402
403 pub fn mouse_wheel(&mut self, delta_y: i32) {
404 self.vm.ctx.on_mouse_wheel(delta_y);
405 self.script_needs_pump = true;
406 }
407
408 pub fn touch(&mut self, phase: i32, x: f64, y: f64) {
409 self.mouse_move(x, y);
410 match phase {
411 0 => self.mouse_down(VmMouseButton::Left),
412 1 => {}
413 2 | 3 => self.mouse_up(VmMouseButton::Left),
414 _ => {}
415 }
416 }
417
418 pub fn key_down(&mut self, key: VmKey) {
419 self.vm.ctx.on_key_down(key);
420 self.script_needs_pump = true;
421 }
422
423 pub fn key_down_code(&mut self, code: i32) {
424 if let Some(key) = vm_key_from_platform_code(code) {
425 self.key_down(key);
426 }
427 }
428
429 pub fn key_up(&mut self, key: VmKey) {
430 self.vm.ctx.on_key_up(key);
431 self.script_needs_pump = true;
432 }
433
434 pub fn key_up_code(&mut self, code: i32) {
435 if let Some(key) = vm_key_from_platform_code(code) {
436 self.key_up(key);
437 }
438 }
439
440 pub fn text_input(&mut self, text: &str) {
441 self.vm.ctx.on_text_input(text);
442 self.script_needs_pump = true;
443 }
444
445 pub fn renderer_mut(&mut self) -> &mut Renderer {
446 &mut self.renderer
447 }
448
449 pub fn vm_mut(&mut self) -> &mut SceneVm<'static> {
450 &mut self.vm
451 }
452
453 fn resolve_initial_size(config: &SiglusHostConfig) -> (u32, u32) {
454 let cfg_size = Self::try_load_gameexe(&config.project_dir)
455 .as_ref()
456 .and_then(Self::gameexe_screen_size)
457 .unwrap_or((1280, 720));
458 (
459 config.width.unwrap_or(cfg_size.0),
460 config.height.unwrap_or(cfg_size.1),
461 )
462 }
463
464 fn gameexe_screen_size(cfg: &GameexeConfig) -> Option<(u32, u32)> {
465 let entry = cfg.get_entry("SCREEN_SIZE")?;
466 let w = entry.item_unquoted(0)?.trim().parse::<u32>().ok()?;
467 let h = entry.item_unquoted(1)?.trim().parse::<u32>().ok()?;
468 if w == 0 || h == 0 {
469 return None;
470 }
471 Some((w, h))
472 }
473
474 fn gameexe_scene_entry(cfg: &GameexeConfig, key: &str) -> Option<(String, i32)> {
475 let entry = cfg.get_entry(key)?;
476 let scene = entry.item_unquoted(0)?.trim().trim_matches('"').to_string();
477 if scene.is_empty() {
478 return None;
479 }
480 let z = entry
481 .item_unquoted(1)
482 .and_then(|s| s.trim().parse::<i32>().ok())
483 .unwrap_or(0);
484 Some((scene, z))
485 }
486
487 fn resolve_boot_config(config: &SiglusHostConfig) -> BootConfig {
488 let cfg = Self::try_load_gameexe(&config.project_dir);
489 let (default_start, default_start_z) = cfg
490 .as_ref()
491 .and_then(|cfg| Self::gameexe_scene_entry(cfg, "START_SCENE"))
492 .unwrap_or_else(|| ("_start".to_string(), 0));
493 let (menu_scene, menu_z) = cfg
494 .as_ref()
495 .and_then(|cfg| Self::gameexe_scene_entry(cfg, "MENU_SCENE"))
496 .map(|(s, z)| (Some(s), z))
497 .unwrap_or((None, 0));
498 BootConfig {
499 start_scene: config.scene_name.clone().unwrap_or(default_start),
500 start_z: default_start_z,
501 menu_scene,
502 menu_z,
503 }
504 }
505
506 fn find_gameexe_path(project_dir: &Path) -> Option<PathBuf> {
507 let candidates = [
508 "Gameexe.dat", "Gameexe.ini", "gameexe.dat", "gameexe.ini", "GameexeEN.dat",
509 "GameexeEN.ini", "GameexeZH.dat", "GameexeZH.ini", "GameexeZHTW.dat",
510 "GameexeZHTW.ini", "GameexeDE.dat", "GameexeDE.ini", "GameexeES.dat",
511 "GameexeES.ini", "GameexeFR.dat", "GameexeFR.ini", "GameexeID.dat",
512 "GameexeID.ini",
513 ];
514 for name in candidates {
515 let p = project_dir.join(name);
516 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
517 if crate::resource::wasm_path_is_file(&p) {
518 return Some(p);
519 }
520 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
521 if p.is_file() {
522 return Some(p);
523 }
524 }
525 None
526 }
527
528 fn try_load_gameexe(project_dir: &Path) -> Option<GameexeConfig> {
529 let path = Self::find_gameexe_path(project_dir)?;
530 let raw = crate::resource::read_file_bytes(&path).ok()?;
531 if path
532 .extension()
533 .and_then(|s| s.to_str())
534 .is_some_and(|ext| ext.eq_ignore_ascii_case("ini"))
535 {
536 let text = String::from_utf8(raw).ok()?;
537 return Some(GameexeConfig::from_text(&text));
538 }
539 let opt = load_gameexe_decode_options(project_dir).ok()?;
540 let (text, _report) = decode_gameexe_dat_bytes(&raw, &opt).ok()?;
541 Some(GameexeConfig::from_text(&text))
542 }
543
544 fn init_vm(
545 config: &SiglusHostConfig,
546 boot: &BootConfig,
547 initial_size: (u32, u32),
548 ) -> Result<SceneVm<'static>> {
549 let project_dir = config.project_dir.clone();
550 let scene_pck_path = find_scene_pck_for_host(&project_dir)?;
551 let opt = load_scene_pck_decode_options(&project_dir)?;
552 let pck = {
553 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
554 {
555 let bytes = crate::resource::read_file_bytes(&scene_pck_path)
556 .with_context(|| format!("read scene.pck: {}", scene_pck_path.display()))?;
557 ScenePck::load_and_rebuild_from_bytes(bytes, &opt)
558 .with_context(|| format!("open scene.pck: {}", scene_pck_path.display()))?
559 }
560 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
561 {
562 ScenePck::load_and_rebuild(&scene_pck_path, &opt)
563 .with_context(|| format!("open scene.pck: {}", scene_pck_path.display()))?
564 }
565 };
566
567 let scene_no = if let Some(id) = config.scene_id {
568 id
569 } else if let Some(name) = config.scene_name.as_ref() {
570 pck.find_scene_no(name).unwrap_or(0)
571 } else {
572 pck.find_scene_no(&boot.start_scene).unwrap_or(0)
573 };
574
575 let chunk = pck
576 .scn_data_slice(scene_no)
577 .with_context(|| format!("scene_id out of range: {}", scene_no))?;
578 let chunk_leaked: &'static [u8] = Box::leak(chunk.to_vec().into_boxed_slice());
579 let mut stream = SceneStream::new(chunk_leaked)?;
580 let start_z = if config.scene_id.is_some() || config.scene_name.is_some() {
581 0
582 } else {
583 boot.start_z
584 };
585 stream.jump_to_z_label(start_z.max(0) as usize)?;
586 let mut ctx = CommandContext::new(project_dir);
587 ctx.screen_w = initial_size.0;
588 ctx.screen_h = initial_size.1;
589 let mut vm = SceneVm::with_config(VmConfig::from_env(), stream, ctx);
590 if config.scene_id.is_none() {
591 let scene_name = config
592 .scene_name
593 .clone()
594 .unwrap_or_else(|| boot.start_scene.clone());
595 vm.restart_scene_name(&scene_name, start_z)?;
596 }
597 Ok(vm)
598 }
599
600 fn suspend_wait_for_syscom_excall(&mut self, key: &str) {
601 let flow_depth = self.flow.stack.len();
602 let saved_wait = std::mem::take(&mut self.vm.ctx.wait);
603 self.vm.ctx.input.use_current();
604 self.vm.ctx.script_input.use_current();
605 self.syscom_suspended_waits.push((flow_depth, saved_wait));
606 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
607 eprintln!(
608 "[SG_PROC_FLOW] host suspend_wait_for_syscom_excall key={} flow_depth={} saved_count={} scene={:?} line={}",
609 key,
610 flow_depth,
611 self.syscom_suspended_waits.len(),
612 self.vm.current_scene_name(),
613 self.vm.current_line_no()
614 );
615 }
616 }
617
618 fn restore_wait_after_syscom_excall(&mut self, popped_depth: usize) {
619 let should_restore = self
620 .syscom_suspended_waits
621 .last()
622 .map(|(depth, _)| *depth == popped_depth)
623 .unwrap_or(false);
624 if !should_restore {
625 return;
626 }
627 let Some((_depth, saved_wait)) = self.syscom_suspended_waits.pop() else {
628 return;
629 };
630 self.vm.ctx.wait = saved_wait;
631 self.vm.ctx.input.clear_all();
632 self.vm.ctx.script_input.clear_all();
633 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
634 eprintln!(
635 "[SG_PROC_FLOW] host restore_wait_after_syscom_excall popped_depth={} remaining={} scene={:?} line={}",
636 popped_depth,
637 self.syscom_suspended_waits.len(),
638 self.vm.current_scene_name(),
639 self.vm.current_line_no()
640 );
641 }
642 }
643
644 fn consume_syscom_pending_proc(&mut self) -> Result<bool> {
645 let Some(proc) = self.vm.ctx.globals.syscom.pending_proc.take() else {
646 return Ok(false);
647 };
648
649 self.vm.ctx.globals.syscom.menu_open = false;
650 self.vm.ctx.globals.syscom.menu_kind = None;
651 if proc.kind != SyscomPendingProcKind::MsgBack {
652 self.vm.ctx.globals.syscom.msg_back_open = false;
653 }
654
655 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
656 eprintln!(
657 "[SG_PROC_FLOW] host consume_syscom_pending kind={:?} before scene={:?} line={} flow={:?}",
658 proc.kind,
659 self.vm.current_scene_name(),
660 self.vm.current_line_no(),
661 self.flow.stack
662 );
663 }
664
665 match proc.kind {
666 SyscomPendingProcKind::EndGame => {
667 if proc.warning {
668 self.begin_syscom_warning(proc);
669 } else {
670 self.queue_end_game_proc(proc);
671 }
672 Ok(true)
673 }
674 SyscomPendingProcKind::ReturnToMenu => {
675 if proc.warning {
676 self.begin_syscom_warning(proc);
677 } else {
678 self.queue_return_to_menu_proc(proc);
679 }
680 Ok(true)
681 }
682 SyscomPendingProcKind::ReturnToSel => {
683 if self.vm.restore_last_sel_point() {
684 self.flow.stack.clear();
685 self.flow.push(ProcType::GameTimerStart, 0);
686 self.flow.push(ProcType::Script, 0);
687 Ok(true)
688 } else {
689 self.vm.ctx.unknown.record_note(
690 "SYSCOM.RETURN_TO_SEL requested without an in-memory SELPOINT snapshot",
691 );
692 Ok(false)
693 }
694 }
695 SyscomPendingProcKind::Save => {
696 if proc.warning {
697 self.begin_syscom_warning(proc);
698 } else {
699 crate::runtime::forms::syscom::menu_save_slot(
700 &mut self.vm.ctx,
701 false,
702 proc.save_id.max(0) as usize,
703 );
704 crate::runtime::forms::syscom::write_global_save(&mut self.vm.ctx);
705 }
706 Ok(true)
707 }
708 SyscomPendingProcKind::Load => {
709 if proc.warning {
710 self.begin_syscom_warning(proc);
711 } else {
712 crate::runtime::forms::syscom::menu_load_slot(
713 &mut self.vm.ctx,
714 false,
715 proc.save_id.max(0) as usize,
716 );
717 }
718 Ok(true)
719 }
720 SyscomPendingProcKind::QuickSave => {
721 if proc.warning {
722 self.begin_syscom_warning(proc);
723 } else {
724 crate::runtime::forms::syscom::menu_save_slot(
725 &mut self.vm.ctx,
726 true,
727 proc.save_id.max(0) as usize,
728 );
729 crate::runtime::forms::syscom::write_global_save(&mut self.vm.ctx);
730 }
731 Ok(true)
732 }
733 SyscomPendingProcKind::QuickLoad => {
734 if proc.warning {
735 self.begin_syscom_warning(proc);
736 } else {
737 crate::runtime::forms::syscom::menu_load_slot(
738 &mut self.vm.ctx,
739 true,
740 proc.save_id.max(0) as usize,
741 );
742 }
743 Ok(true)
744 }
745 SyscomPendingProcKind::BacklogLoad => {
746 if self.vm.restore_last_sel_point() {
747 self.flow.stack.clear();
748 self.flow.push(ProcType::GameTimerStart, 0);
749 self.flow.push(ProcType::Script, 0);
750 Ok(true)
751 } else {
752 self.vm.ctx.unknown.record_note(&format!(
753 "SYSCOM.MSG_BACK_LOAD requested but backlog save {} is not materialized without SAVE/LOAD support",
754 proc.save_id
755 ));
756 Ok(false)
757 }
758 }
759 SyscomPendingProcKind::MsgBack => {
760 if self.vm.ctx.globals.syscom.msg_back_open {
761 self.flow.push(ProcType::MsgBack, 0);
762 Ok(true)
763 } else {
764 Ok(false)
765 }
766 }
767 SyscomPendingProcKind::OpenSyscomMenu => {
768 if self.vm.call_syscom_configured_scene("CANCEL_SCENE")? {
769 self.ensure_requested_script_proc();
770 self.suspend_wait_for_syscom_excall("CANCEL_SCENE");
771 Ok(true)
772 } else {
773 log::error!("SYSCOM MENU native popup is not implemented and CANCEL_SCENE is not configured");
774 Ok(false)
775 }
776 }
777 SyscomPendingProcKind::OpenSave => {
778 syscom_form::sync_save_slots_from_disk(&mut self.vm.ctx, false);
779 if self.vm.call_syscom_configured_scene("SAVE_SCENE")? {
780 self.ensure_requested_script_proc();
781 self.suspend_wait_for_syscom_excall("SAVE_SCENE");
782 Ok(true)
783 } else {
784 log::error!("SYSCOM SAVE native dialog is not implemented and SAVE_SCENE is not configured");
785 Ok(false)
786 }
787 }
788 SyscomPendingProcKind::OpenLoad => {
789 syscom_form::sync_save_slots_from_disk(&mut self.vm.ctx, false);
790 if self.vm.call_syscom_configured_scene("LOAD_SCENE")? {
791 self.ensure_requested_script_proc();
792 self.suspend_wait_for_syscom_excall("LOAD_SCENE");
793 Ok(true)
794 } else {
795 log::error!("SYSCOM LOAD native dialog is not implemented and LOAD_SCENE is not configured");
796 Ok(false)
797 }
798 }
799 SyscomPendingProcKind::OpenConfig => {
800 if self.vm.call_syscom_configured_scene("CONFIG_SCENE")? {
801 self.ensure_requested_script_proc();
802 self.suspend_wait_for_syscom_excall("CONFIG_SCENE");
803 Ok(true)
804 } else {
805 log::error!("SYSCOM CONFIG native dialog is not implemented and CONFIG_SCENE is not configured");
806 Ok(false)
807 }
808 }
809 }
810 }
811
812 fn ensure_requested_script_proc(&mut self) {
813 let requested = self.vm.take_script_proc_request();
814 if requested {
815 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
816 eprintln!(
817 "[SG_PROC_FLOW] host ensure_requested_script_proc push before scene={:?} line={} flow={:?}",
818 self.vm.current_scene_name(),
819 self.vm.current_line_no(),
820 self.flow.stack
821 );
822 }
823 self.flow.push(ProcType::Script, 0);
824 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
825 eprintln!("[SG_PROC_FLOW] host ensure_requested_script_proc push after flow={:?}", self.flow.stack);
826 }
827 }
828 }
829
830 fn begin_syscom_warning(&mut self, mut proc: SyscomPendingProc) {
831 let kind = proc.kind;
832 proc.warning = false;
833 self.flow.pending_syscom_proc = Some(proc);
834 self.vm.ctx.globals.system.messagebox_modal_result = None;
835 let request_id = self.vm.ctx.native_ui.next_messagebox_request_id();
836 let buttons = vec![
837 SystemMessageBoxButton {
838 label: "YES".to_string(),
839 value: 0,
840 },
841 SystemMessageBoxButton {
842 label: "NO".to_string(),
843 value: 1,
844 },
845 ];
846 let text = self.syscom_warning_text(kind);
847 let title = self.vm.ctx.game_title();
848 if let Some(backend) = self.vm.ctx.native_ui_backend.as_ref() {
849 self.vm.ctx.globals.system.messagebox_modal = Some(SystemMessageBoxModalState {
850 request_id,
851 kind: 19,
852 text: text.clone(),
853 debug_only: false,
854 buttons: buttons.clone(),
855 cursor: 1,
856 native_pending: true,
857 complete_wait_with_value: false,
858 });
859 backend.show_system_messagebox(native_ui::NativeMessageBoxRequest {
860 request_id,
861 kind: native_ui::NativeMessageBoxKind::YesNo,
862 title,
863 message: text,
864 buttons: buttons
865 .into_iter()
866 .map(|button| native_ui::NativeMessageBoxButton {
867 label: button.label,
868 value: button.value,
869 })
870 .collect(),
871 debug_only: false,
872 });
873 } else {
874 self.vm.ctx.globals.system.messagebox_modal = Some(SystemMessageBoxModalState {
875 request_id,
876 kind: 19,
877 text,
878 debug_only: false,
879 buttons,
880 cursor: 1,
881 native_pending: false,
882 complete_wait_with_value: false,
883 });
884 }
885 self.flow.push(ProcType::SyscomWarning, 0);
886 }
887
888 fn syscom_warning_text(&self, kind: SyscomPendingProcKind) -> String {
889 let keys: &[&str] = match kind {
890 SyscomPendingProcKind::EndGame => &[
891 "#WARNINGINFO.GAMEEND_WARNING_STR",
892 "WARNINGINFO.GAMEEND_WARNING_STR",
893 ],
894 SyscomPendingProcKind::ReturnToSel => &[
895 "#WARNINGINFO.RETURNSEL_WARNING_STR",
896 "WARNINGINFO.RETURNSEL_WARNING_STR",
897 "#WARNINGINFO.RETURNMENU_WARNING_STR",
898 "WARNINGINFO.RETURNMENU_WARNING_STR",
899 ],
900 SyscomPendingProcKind::Save | SyscomPendingProcKind::QuickSave => &[
901 "#WARNINGINFO.SAVE_WARNING_STR",
902 "WARNINGINFO.SAVE_WARNING_STR",
903 ],
904 SyscomPendingProcKind::Load | SyscomPendingProcKind::QuickLoad => &[
905 "#WARNINGINFO.LOAD_WARNING_STR",
906 "WARNINGINFO.LOAD_WARNING_STR",
907 ],
908 _ => &[
909 "#WARNINGINFO.RETURNMENU_WARNING_STR",
910 "WARNINGINFO.RETURNMENU_WARNING_STR",
911 ],
912 };
913 let default = match kind {
914 SyscomPendingProcKind::EndGame => "終了してもよろしいですか?",
915 SyscomPendingProcKind::ReturnToSel => "前の選択肢に戻ってもよろしいですか?",
916 SyscomPendingProcKind::Save | SyscomPendingProcKind::QuickSave => "セーブデータを上書きしてもよろしいですか?",
917 SyscomPendingProcKind::Load | SyscomPendingProcKind::QuickLoad => "セーブデータをロードしてもよろしいですか?",
918 _ => "タイトルに戻ってもよろしいですか?",
919 };
920 let cfg = self.vm.ctx.tables.gameexe.as_ref();
921 keys.iter()
922 .find_map(|key| cfg.and_then(|c| c.get_unquoted(key)))
923 .filter(|s| !s.is_empty())
924 .map(str::to_string)
925 .unwrap_or_else(|| default.to_string())
926 }
927
928 fn return_to_menu_warning_text(&self) -> String {
929 let cfg = self.vm.ctx.tables.gameexe.as_ref();
930 ["#WARNINGINFO.RETURNMENU_WARNING_STR", "WARNINGINFO.RETURNMENU_WARNING_STR"]
931 .iter()
932 .find_map(|key| cfg.and_then(|c| c.get_unquoted(key)))
933 .filter(|s| !s.is_empty())
934 .map(str::to_string)
935 .unwrap_or_else(|| "タイトルに戻ってもよろしいですか?".to_string())
936 }
937
938 fn load_wipe_params(&self) -> (i32, i32) {
939 fn parse_pair(raw: &str) -> Option<(i32, i32)> {
940 let nums: Vec<i32> = raw
941 .split(|c: char| !(c == '-' || c.is_ascii_digit()))
942 .filter(|s| !s.is_empty())
943 .filter_map(|s| s.parse::<i32>().ok())
944 .collect();
945 if nums.len() >= 2 {
946 Some((nums[0], nums[1]))
947 } else {
948 None
949 }
950 }
951 let cfg = self.vm.ctx.tables.gameexe.as_ref();
952 for key in ["LOAD.WIPE", "LOAD . WIPE", "#LOAD.WIPE", "#LOAD . WIPE"] {
953 if let Some(pair) = cfg.and_then(|c| c.get_value(key)).and_then(parse_pair) {
954 return pair;
955 }
956 }
957 (0, 1000)
958 }
959
960 fn queue_end_game_proc(&mut self, proc: SyscomPendingProc) {
961 self.flow.pending_syscom_proc = None;
962 self.flow.push(ProcType::EndGame, 0);
963 if proc.fade_out {
964 self.flow.push(ProcType::GameEndWipe, 0);
965 self.flow.push(ProcType::Disp, 0);
966 } else {
967 self.flow.push(ProcType::Disp, 0);
968 }
969 }
970
971 fn queue_return_to_menu_proc(&mut self, proc: SyscomPendingProc) {
972 let option = if proc.leave_msgbk { 1 } else { 0 };
973 self.flow.pending_syscom_proc = Some(proc.clone());
974 self.flow.push(ProcType::ReturnToMenu, option);
975 if proc.fade_out {
976 self.flow.push(ProcType::GameEndWipe, 0);
977 self.flow.push(ProcType::Disp, 0);
978 }
979 }
980
981 fn start_game_end_wipe(&mut self) {
982 let (wipe_type, wipe_time) = self.load_wipe_params();
983 self.vm.ctx.globals.start_wipe(WipeState::new(
984 None,
985 None,
986 wipe_type,
987 wipe_time,
988 0,
989 0,
990 Vec::new(),
991 i32::MIN,
992 i32::MAX,
993 i32::MIN,
994 i32::MAX,
995 false,
996 0,
997 0,
998 ));
999 }
1000
1001 fn finish_runtime_load(&mut self) {
1002 self.renderer.clear_runtime_image_textures();
1003 self.flow.stack.clear();
1004 self.flow.pending_syscom_proc = None;
1005 self.syscom_suspended_waits.clear();
1006 self.paused = false;
1007 self.script_resume_after_redraw = false;
1008 self.suppress_render_once = true;
1009 self.vm.ctx.globals.syscom.pending_proc = None;
1010 self.vm.ctx.globals.syscom.menu_open = false;
1011 self.vm.ctx.globals.syscom.menu_kind = None;
1012 self.vm.ctx.globals.syscom.msg_back_open = false;
1013 self.flow.push(ProcType::GameTimerStart, 0);
1014 self.flow.push(ProcType::Script, 0);
1015 self.script_needs_pump = true;
1016 }
1017
1018 fn perform_return_to_menu(&mut self, leave_msgbk: bool) -> Result<()> {
1019 let target_scene = self
1020 .boot
1021 .menu_scene
1022 .as_deref()
1023 .unwrap_or(self.boot.start_scene.as_str())
1024 .to_string();
1025 let target_z = if self.boot.menu_scene.is_some() {
1026 self.boot.menu_z
1027 } else {
1028 self.boot.start_z
1029 };
1030 let saved_msgbk = if leave_msgbk {
1031 Some(self.vm.ctx.globals.msgbk_forms.clone())
1032 } else {
1033 None
1034 };
1035 self.vm.restart_scene_name(&target_scene, target_z)?;
1036 self.renderer.clear_runtime_image_textures();
1037 if let Some(msgbk) = saved_msgbk {
1038 self.vm.ctx.globals.msgbk_forms = msgbk;
1039 }
1040 self.vm.ctx.globals.finish_wipe();
1041 self.flow.stack.clear();
1042 self.flow.pending_syscom_proc = None;
1043 self.flow.booted_menu = true;
1044 self.flow.push(ProcType::GameTimerStart, 0);
1045 self.flow.push(ProcType::Script, 0);
1046 Ok(())
1047 }
1048
1049 fn pump_vm(&mut self) -> Result<()> {
1050 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
1051 eprintln!(
1052 "[SG_PROC_FLOW] host pump_vm start paused={} script_needs_pump={} scene={:?} line={} flow={:?} pending_proc={:?}",
1053 self.paused,
1054 self.script_needs_pump,
1055 self.vm.current_scene_name(),
1056 self.vm.current_line_no(),
1057 self.flow.stack,
1058 self.vm.ctx.globals.syscom.pending_proc
1059 );
1060 }
1061 self.script_needs_pump = false;
1062 self.ensure_requested_script_proc();
1063 if self.paused {
1064 return Ok(());
1065 }
1066
1067 self.vm.process_pending_button_actions()?;
1068 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
1069 eprintln!(
1070 "[SG_PROC_FLOW] host pump_vm after_process_button_actions scene={:?} line={} flow={:?} pending_proc={:?}",
1071 self.vm.current_scene_name(),
1072 self.vm.current_line_no(),
1073 self.flow.stack,
1074 self.vm.ctx.globals.syscom.pending_proc
1075 );
1076 }
1077 if self.vm.ctx.globals.syscom.pending_proc.is_some() {
1078 self.consume_syscom_pending_proc()?;
1079 self.ensure_requested_script_proc();
1080 }
1081
1082 self.vm.begin_script_proc_pump();
1083
1084 loop {
1085 let Some(proc) = self.flow.top().cloned() else {
1086 self.paused = true;
1087 break;
1088 };
1089 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
1090 eprintln!(
1091 "[SG_PROC_FLOW] host pump_vm loop top proc={:?} scene={:?} line={} flow={:?}",
1092 proc,
1093 self.vm.current_scene_name(),
1094 self.vm.current_line_no(),
1095 self.flow.stack
1096 );
1097 }
1098
1099 match proc.ty {
1100 ProcType::Script => {
1101 let proc_gen_before = self.vm.proc_generation();
1102 let running = self.vm.run_script_proc_continue()?;
1103 if self.vm.take_runtime_load_completed() {
1104 self.finish_runtime_load();
1105 continue;
1106 }
1107 let proc_boundary = self.vm.proc_generation() != proc_gen_before;
1108 let boundary_kind = self.vm.last_proc_kind();
1109 let pop_script_proc = self.vm.take_script_proc_pop_request();
1110 let halted = self.vm.is_halted();
1111 let cur_scene = self
1112 .vm
1113 .current_scene_name()
1114 .map(|s| s.to_string())
1115 .unwrap_or_else(|| self.boot.start_scene.clone());
1116 let pending = self.vm.ctx.globals.syscom.pending_proc.is_some();
1117 let blocked = if pending { false } else { self.vm.is_blocked() };
1118
1119 self.ensure_requested_script_proc();
1120 if pop_script_proc {
1121 let popped_depth = self.flow.stack.len();
1122 self.flow.pop();
1123 self.restore_wait_after_syscom_excall(popped_depth);
1124 continue;
1125 }
1126 if !running || halted {
1127 self.flow.pop();
1128 if !self.flow.booted_menu
1129 && cur_scene == self.boot.start_scene
1130 && self.boot.menu_scene.is_some()
1131 {
1132 self.flow.push(ProcType::ReturnToMenu, 0);
1133 }
1134 continue;
1135 }
1136 if pending {
1137 if self.consume_syscom_pending_proc()? {
1138 continue;
1139 }
1140 if self.vm.is_blocked() {
1141 break;
1142 }
1143 } else if proc_boundary {
1144 match boundary_kind {
1145 ProcKind::Disp => {
1146 self.script_resume_after_redraw = true;
1147 break;
1148 }
1149 ProcKind::Frame => {
1150 self.script_resume_after_redraw = true;
1151 self.suppress_render_once = true;
1152 break;
1153 }
1154 ProcKind::Command
1155 | ProcKind::MessageBlock
1156 | ProcKind::MessageWait
1157 | ProcKind::KeyWait
1158 | ProcKind::TimeWait
1159 | ProcKind::MovieWait
1160 | ProcKind::WipeWait
1161 | ProcKind::AudioWait
1162 | ProcKind::EventWait
1163 | ProcKind::Selection
1164 | ProcKind::SystemModal
1165 | ProcKind::Script => {
1166 if blocked {
1167 break;
1168 }
1169 continue;
1170 }
1171 }
1172 } else if blocked {
1173 break;
1174 }
1175 }
1176 ProcType::StartWarning => {
1177 let warning_exists = self
1178 .vm
1179 .ctx
1180 .images
1181 .project_dir()
1182 .join("g00")
1183 .join("___SYSEVE_WARNING.g00")
1184 .exists()
1185 || self
1186 .vm
1187 .ctx
1188 .images
1189 .project_dir()
1190 .join("g00")
1191 .join("___SYSEVE_WARNING.g01")
1192 .exists();
1193 if !warning_exists {
1194 self.flow.pop();
1195 continue;
1196 }
1197 let cur = self.redraw_count;
1198 let top = self.flow.top_mut().expect("proc top");
1199 match top.option {
1200 0 => {
1201 top.option = 1;
1202 self.flow.push(ProcType::TimeWait, 0);
1203 if let Some(wait) = self.flow.top_mut() {
1204 wait.deadline_frame = Some(cur.saturating_add(60));
1205 }
1206 }
1207 _ => {
1208 self.flow.pop();
1209 }
1210 }
1211 break;
1212 }
1213 ProcType::SyscomWarning => {
1214 if self.vm.ctx.globals.system.messagebox_modal.is_some() {
1215 break;
1216 }
1217 let result = self
1218 .vm
1219 .ctx
1220 .globals
1221 .system
1222 .messagebox_modal_result
1223 .take()
1224 .unwrap_or(1);
1225 let pending = self.flow.pending_syscom_proc.take();
1226 self.flow.pop();
1227 if result == 0 {
1228 if let Some(proc) = pending {
1229 match proc.kind {
1230 SyscomPendingProcKind::EndGame => {
1231 self.queue_end_game_proc(proc);
1232 }
1233 SyscomPendingProcKind::ReturnToMenu => {
1234 self.queue_return_to_menu_proc(proc);
1235 }
1236 SyscomPendingProcKind::Save => {
1237 crate::runtime::forms::syscom::menu_save_slot(&mut self.vm.ctx, false, proc.save_id.max(0) as usize);
1238 crate::runtime::forms::syscom::write_global_save(&mut self.vm.ctx);
1239 }
1240 SyscomPendingProcKind::Load => {
1241 crate::runtime::forms::syscom::menu_load_slot(&mut self.vm.ctx, false, proc.save_id.max(0) as usize);
1242 }
1243 SyscomPendingProcKind::QuickSave => {
1244 crate::runtime::forms::syscom::menu_save_slot(&mut self.vm.ctx, true, proc.save_id.max(0) as usize);
1245 crate::runtime::forms::syscom::write_global_save(&mut self.vm.ctx);
1246 }
1247 SyscomPendingProcKind::QuickLoad => {
1248 crate::runtime::forms::syscom::menu_load_slot(&mut self.vm.ctx, true, proc.save_id.max(0) as usize);
1249 }
1250 _ => {}
1251 }
1252 }
1253 }
1254 continue;
1255 }
1256 ProcType::MsgBack => {
1257 if !self.vm.ctx.globals.syscom.msg_back_open {
1258 self.flow.pop();
1259 continue;
1260 }
1261 break;
1262 }
1263 ProcType::Disp => {
1264 self.flow.pop();
1265 self.script_resume_after_redraw = true;
1266 break;
1267 }
1268 ProcType::GameEndWipe => {
1269 let mut start = false;
1270 if let Some(top) = self.flow.top_mut() {
1271 if top.option == 0 {
1272 top.option = 1;
1273 start = true;
1274 }
1275 }
1276 if start {
1277 self.start_game_end_wipe();
1278 break;
1279 }
1280 if self.vm.ctx.globals.wipe_done() {
1281 self.flow.pop();
1282 continue;
1283 }
1284 break;
1285 }
1286 ProcType::ReturnToMenu => {
1287 let leave_msgbk = proc.option != 0;
1288 self.perform_return_to_menu(leave_msgbk)?;
1289 continue;
1290 }
1291 ProcType::EndGame => {
1292 self.flow.pop();
1293 self.vm.ctx.globals.system.active_flag = false;
1294 continue;
1295 }
1296 ProcType::GameTimerStart => {
1297 self.flow.pop();
1298 continue;
1299 }
1300 ProcType::TimeWait => {
1301 let deadline = proc.deadline_frame.unwrap_or(self.redraw_count);
1302 if self.redraw_count >= deadline {
1303 self.flow.pop();
1304 continue;
1305 }
1306 break;
1307 }
1308 }
1309 break;
1310 }
1311 Ok(())
1312 }
1313
1314 fn redraw(&mut self) -> Result<()> {
1315 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
1316 eprintln!(
1317 "[SG_PROC_FLOW] host redraw start scene={:?} line={} flow={:?} pending_proc={:?}",
1318 self.vm.current_scene_name(),
1319 self.vm.current_line_no(),
1320 self.flow.stack,
1321 self.vm.ctx.globals.syscom.pending_proc
1322 );
1323 }
1324 if self.script_needs_pump {
1329 self.pump_vm()?;
1330 }
1331 let wait_poll_needed = self.vm.ctx.wait.needs_runtime_poll();
1332 self.vm.tick_frame()?;
1333 if self.vm.take_runtime_load_completed() {
1334 self.finish_runtime_load();
1335 return Ok(());
1336 }
1337 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
1338 eprintln!(
1339 "[SG_PROC_FLOW] host redraw after_tick scene={:?} line={} flow={:?} pending_proc={:?}",
1340 self.vm.current_scene_name(),
1341 self.vm.current_line_no(),
1342 self.flow.stack,
1343 self.vm.ctx.globals.syscom.pending_proc
1344 );
1345 }
1346 if self.vm.ctx.globals.syscom.pending_proc.is_some() {
1347 self.consume_syscom_pending_proc()?;
1348 self.ensure_requested_script_proc();
1349 self.script_needs_pump = true;
1350 }
1351 if wait_poll_needed && !self.vm.is_blocked() {
1352 self.script_needs_pump = true;
1353 }
1354 self.ensure_requested_script_proc();
1355 let render_suppressed = self.suppress_render_once;
1356 self.suppress_render_once = false;
1357 if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
1358 eprintln!(
1359 "[SG_PROC_FLOW] host redraw render_decision render_suppressed={} scene={:?} line={} flow={:?}",
1360 render_suppressed,
1361 self.vm.current_scene_name(),
1362 self.vm.current_line_no(),
1363 self.flow.stack
1364 );
1365 }
1366 if !render_suppressed {
1367 let list = self.vm.ctx.render_list_with_effects();
1368 self.renderer.render_sprites(&self.vm.ctx.images, &list)?;
1369 }
1370 if self.script_resume_after_redraw {
1371 self.script_resume_after_redraw = false;
1372 self.script_needs_pump = true;
1373 }
1374 self.redraw_count = self.redraw_count.saturating_add(1);
1375 Ok(())
1376 }
1377}
1378
1379fn vm_key_from_platform_code(code: i32) -> Option<VmKey> {
1380 match code {
1381 0x1B => Some(VmKey::Escape),
1382 0x0D => Some(VmKey::Enter),
1383 0x20 => Some(VmKey::Space),
1384 0x08 => Some(VmKey::Backspace),
1385 0x09 => Some(VmKey::Tab),
1386 0x10 => Some(VmKey::Shift),
1387 0x12 => Some(VmKey::Alt),
1388 0x25 => Some(VmKey::ArrowLeft),
1389 0x26 => Some(VmKey::ArrowUp),
1390 0x27 => Some(VmKey::ArrowRight),
1391 0x28 => Some(VmKey::ArrowDown),
1392 0x30..=0x39 => Some(VmKey::Digit((code - 0x30) as u8)),
1393 0x41..=0x5A => Some(VmKey::Letter((code as u8 as char).to_ascii_uppercase())),
1394 0x61..=0x7A => Some(VmKey::Letter((code as u8 as char).to_ascii_uppercase())),
1395 0x70..=0x7B => Some(VmKey::F((code - 0x6F) as u8)),
1396 _ => None,
1397 }
1398}
1399
1400pub unsafe fn cstr_opt(ptr: *const c_char) -> Option<String> {
1401 if ptr.is_null() {
1402 return None;
1403 }
1404 let s = CStr::from_ptr(ptr).to_string_lossy().to_string();
1405 if s.is_empty() { None } else { Some(s) }
1406}
1407
1408pub unsafe fn cstr_required(ptr: *const c_char, what: &str) -> Result<String> {
1409 if ptr.is_null() {
1410 anyhow::bail!("{what} is null");
1411 }
1412 Ok(CStr::from_ptr(ptr).to_str()?.to_string())
1413}
1414
1415pub fn parse_bool_exit(result: Result<bool>, context: &str) -> i32 {
1416 match result {
1417 Ok(true) => 1,
1418 Ok(false) => 0,
1419 Err(e) => {
1420 log::error!("{context}: {e:?}");
1421 1
1422 }
1423 }
1424}
1425
1426pub fn default_frame_interval_ms(dt_ms: u32) -> u32 {
1427 if dt_ms == 0 { FRAME_INTERVAL_MS } else { dt_ms }
1428}