1use crate::platform_time::Instant;
10use std::cell::{RefCell, RefMut};
11use std::ffi::{CStr, CString, c_char, c_void};
12use std::path::{Path, PathBuf};
13use std::rc::Rc;
14use std::sync::Arc;
15
16use anyhow::{Context, Result};
17use siglus_assets::gameexe::{GameexeConfig, GameexeDecodeOptions, decode_gameexe_dat_bytes};
18use siglus_assets::scene_pck::{ScenePck, ScenePckDecodeOptions};
19
20use crate::render::Renderer;
21use crate::runtime::forms::syscom as syscom_form;
22use crate::runtime::globals::{
23 SyscomPendingProc, SyscomPendingProcKind, SystemMessageBoxButton, SystemMessageBoxModalState,
24 WipeState,
25};
26use crate::runtime::input::{VmKey, VmMouseButton};
27use crate::runtime::wait::VmWait;
28use crate::runtime::{CommandContext, FrameCaptureBackendRef, ProcKind, native_ui};
29use crate::scene_stream::SceneStream;
30use crate::vm::{SceneVm, VmConfig};
31
32const FRAME_INTERVAL_MS: u32 = 16;
33
34#[cfg(target_os = "horizon")]
38macro_rules! switch_startup_marker {
39 ($message:expr) => {
40 crate::switch_host::report_switch_marker($message)
41 };
42}
43#[cfg(target_os = "vita")]
44unsafe extern "C" {
45 fn siglus_vita_log_marker(message: *const u8);
46}
47#[cfg(target_os = "vita")]
48macro_rules! switch_startup_marker {
49 ($message:expr) => {
50 unsafe { siglus_vita_log_marker($message.as_ptr()) }
51 };
52}
53#[cfg(not(any(target_os = "horizon", target_os = "vita")))]
54macro_rules! switch_startup_marker {
55 ($message:expr) => {};
56}
57
58#[derive(Debug, Clone)]
59pub struct SiglusHostConfig {
60 pub project_dir: PathBuf,
61 pub scene_name: Option<String>,
62 pub scene_id: Option<usize>,
63 pub width: Option<u32>,
64 pub height: Option<u32>,
65}
66
67impl SiglusHostConfig {
68 pub fn new(project_dir: PathBuf) -> Self {
69 Self {
70 project_dir,
71 scene_name: None,
72 scene_id: None,
73 width: None,
74 height: None,
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
80struct BootConfig {
81 start_scene: String,
82 start_z: i32,
83 menu_scene: String,
84 menu_z: i32,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88enum ProcType {
89 Script,
90 StartWarning,
91 SyscomWarning,
92 MsgBack,
93 ReturnToMenu,
94 GameEndWipe,
95 Disp,
96 EndGame,
97 GameTimerStart,
98 TimeWait,
99}
100
101#[derive(Debug, Clone)]
102struct ProcFrame {
103 ty: ProcType,
104 option: i32,
105 deadline_frame: Option<u32>,
106}
107
108#[derive(Debug, Default)]
109struct ProcFlow {
110 stack: Vec<ProcFrame>,
111 booted_menu: bool,
112 pending_syscom_proc: Option<SyscomPendingProc>,
113}
114
115impl ProcFlow {
116 fn push(&mut self, ty: ProcType, option: i32) {
117 self.stack.push(ProcFrame {
118 ty,
119 option,
120 deadline_frame: None,
121 });
122 }
123
124 fn pop(&mut self) {
125 let _ = self.stack.pop();
126 }
127
128 fn top(&self) -> Option<&ProcFrame> {
129 self.stack.last()
130 }
131
132 fn top_mut(&mut self) -> Option<&mut ProcFrame> {
133 self.stack.last_mut()
134 }
135}
136
137#[repr(i32)]
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum SiglusNativeMessageBoxKind {
141 Ok = 0,
142 OkCancel = 1,
143 YesNo = 2,
144 YesNoCancel = 3,
145}
146
147impl From<native_ui::NativeMessageBoxKind> for SiglusNativeMessageBoxKind {
148 fn from(value: native_ui::NativeMessageBoxKind) -> Self {
149 match value {
150 native_ui::NativeMessageBoxKind::Ok => Self::Ok,
151 native_ui::NativeMessageBoxKind::OkCancel => Self::OkCancel,
152 native_ui::NativeMessageBoxKind::YesNo => Self::YesNo,
153 native_ui::NativeMessageBoxKind::YesNoCancel => Self::YesNoCancel,
154 }
155 }
156}
157
158pub type SiglusNativeMessageBoxCallback = unsafe extern "C" fn(
164 user_data: *mut c_void,
165 request_id: u64,
166 kind: i32,
167 title_utf8: *const c_char,
168 message_utf8: *const c_char,
169);
170
171struct CNativeUiBackend {
172 callback: SiglusNativeMessageBoxCallback,
173 user_data: usize,
174}
175
176unsafe impl Send for CNativeUiBackend {}
177unsafe impl Sync for CNativeUiBackend {}
178
179impl native_ui::NativeUiBackend for CNativeUiBackend {
180 fn show_system_messagebox(&self, request: native_ui::NativeMessageBoxRequest) {
181 let title = CString::new(request.title).unwrap_or_else(|_| CString::new("Siglus").unwrap());
182 let message = CString::new(request.message).unwrap_or_else(|_| CString::new("").unwrap());
183 let kind: SiglusNativeMessageBoxKind = request.kind.into();
184 unsafe {
185 (self.callback)(
186 self.user_data as *mut c_void,
187 request.request_id,
188 kind as i32,
189 title.as_ptr(),
190 message.as_ptr(),
191 );
192 }
193 }
194}
195
196pub struct SiglusHost {
197 config: SiglusHostConfig,
198 boot: BootConfig,
199 flow: ProcFlow,
200 renderer: Rc<RefCell<Renderer>>,
201 vm: SceneVm<'static>,
202 redraw_count: u32,
203 script_needs_pump: bool,
204 script_resume_after_redraw: bool,
205 suppress_render_once: bool,
206 syscom_suspended_waits: Vec<(usize, VmWait, String)>,
207 paused: bool,
208 pending_exit: bool,
209 last_step: Option<Instant>,
210}
211
212fn find_scene_pck_for_host(project_dir: &Path) -> Result<PathBuf> {
213 crate::resource::find_scene_pck_path(project_dir)
214}
215
216fn load_key_toml_config(
217 project_dir: &Path,
218) -> Result<Option<siglus_assets::key_toml::KeyTomlConfig>> {
219 crate::resource::load_project_key_toml(project_dir)
220}
221
222fn load_gameexe_decode_options(project_dir: &Path) -> Result<GameexeDecodeOptions> {
223 crate::resource::load_gameexe_decode_options(project_dir)
224}
225
226fn load_scene_pck_decode_options(project_dir: &Path) -> Result<ScenePckDecodeOptions> {
227 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
228 {
229 let cfg = load_key_toml_config(project_dir)?;
230 let exe = cfg
231 .as_ref()
232 .and_then(|cfg| cfg.exe_key16)
233 .map(|v| v.to_vec());
234 let string_encryption_override = cfg
235 .map(|cfg| cfg.override_string_encryption)
236 .unwrap_or_default();
237 Ok(ScenePckDecodeOptions {
238 exe_angou_element: exe,
239 easy_angou_code: Some(siglus_assets::keys::SCENE_KEY.to_vec()),
240 string_encryption_override,
241 })
242 }
243
244 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
245 {
246 crate::resource::load_scene_pck_decode_options(project_dir)
247 }
248}
249
250impl SiglusHost {
251 pub async fn new_with_renderer(config: SiglusHostConfig, renderer: Renderer) -> Result<Self> {
252 Self::new_with_renderer_sync(config, renderer)
253 }
254
255 pub fn new_with_renderer_sync(config: SiglusHostConfig, renderer: Renderer) -> Result<Self> {
258 switch_startup_marker!(b"siglus_switch: host initial-size begin\n\0");
259 let initial_size = Self::resolve_initial_size(&config);
260 switch_startup_marker!(b"siglus_switch: host initial-size complete\n\0");
261 switch_startup_marker!(b"siglus_switch: host boot-config begin\n\0");
262 let boot = Self::resolve_boot_config(&config);
263 switch_startup_marker!(b"siglus_switch: host boot-config complete\n\0");
264 let mut flow = ProcFlow::default();
265 flow.push(ProcType::Script, 0);
266 flow.push(ProcType::StartWarning, 0);
267 let chihaya_display_adapter_name = renderer.adapter_name();
268 let renderer = Rc::new(RefCell::new(renderer));
269 switch_startup_marker!(b"siglus_switch: host vm-init begin\n\0");
270 let mut vm = Self::init_vm(&config, &boot, initial_size)?;
271 switch_startup_marker!(b"siglus_switch: host vm-init complete\n\0");
272 vm.ctx.globals.system.chihaya_display_adapter_name = chihaya_display_adapter_name;
273 let capture_backend: FrameCaptureBackendRef = renderer.clone();
274 vm.ctx.set_frame_capture_backend(Some(capture_backend));
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
308 .ctx
309 .submit_native_messagebox_result(request_id, value);
310 self.script_needs_pump = true;
311 }
312
313 pub fn resize(&mut self, width: u32, height: u32, scale_factor: f32) {
314 self.renderer
315 .borrow_mut()
316 .resize_with_scale(width, height, scale_factor.max(1.0));
317 let logical_w = ((width as f32) / scale_factor.max(1.0)).max(1.0).round() as u32;
318 let logical_h = ((height as f32) / scale_factor.max(1.0)).max(1.0).round() as u32;
319 self.vm.ctx.set_screen_size(logical_w, logical_h);
320 self.script_needs_pump = true;
321 }
322
323 pub fn resize_with_logical_viewport(
324 &mut self,
325 surface_width: u32,
326 surface_height: u32,
327 scale_factor: f32,
328 logical_width: u32,
329 logical_height: u32,
330 viewport_x: u32,
331 viewport_y: u32,
332 viewport_width: u32,
333 viewport_height: u32,
334 ) {
335 self.renderer.borrow_mut().resize_with_logical_viewport(
336 surface_width,
337 surface_height,
338 scale_factor.max(1.0),
339 logical_width.max(1),
340 logical_height.max(1),
341 viewport_x,
342 viewport_y,
343 viewport_width.max(1),
344 viewport_height.max(1),
345 );
346 self.vm
347 .ctx
348 .set_screen_size(logical_width.max(1), logical_height.max(1));
349 self.script_needs_pump = true;
350 }
351
352 pub fn logical_size(&self) -> (u32, u32) {
353 (self.vm.ctx.screen_w.max(1), self.vm.ctx.screen_h.max(1))
354 }
355
356 pub fn movie_memory_stats(&self) -> crate::movie::MovieMemoryStats {
358 self.vm.ctx.movie.debug_memory_stats()
359 }
360
361 pub fn debug_status_summary(&mut self) -> String {
362 let blocked = self.vm.is_blocked();
363 let movie_playing = self.vm.ctx.globals.mov.playing;
364 let movie_file = self.vm.ctx.globals.mov.file_name.clone();
365 let movie_timer = self.vm.ctx.globals.mov.timer_ms;
366 let movie_frame = self.vm.ctx.globals.mov.last_frame_idx;
367 format!(
368 "scene={:?} line={} pending_exit={} vm_halted={} active_flag={} flow={:?} blocked={} movie_playing={} movie_file={:?} movie_timer={} movie_frame={:?}",
369 self.vm.current_scene_name(),
370 self.vm.current_line_no(),
371 self.pending_exit,
372 self.vm.is_halted(),
373 self.vm.ctx.globals.system.active_flag,
374 self.flow.stack,
375 blocked,
376 movie_playing,
377 movie_file,
378 movie_timer,
379 movie_frame,
380 )
381 }
382
383 fn native_messagebox_pending(&self) -> bool {
384 self.vm
385 .ctx
386 .globals
387 .system
388 .messagebox_modal
389 .as_ref()
390 .map(|modal| modal.native_pending)
391 .unwrap_or(false)
392 }
393
394 pub fn step(&mut self, dt_ms: u32) -> Result<bool> {
396 let _ = dt_ms;
397 self.last_step = Some(Instant::now());
398 if self.native_messagebox_pending() {
399 return Ok(false);
400 }
401 if self.script_needs_pump || self.vm.ctx.wait.needs_runtime_poll() {
402 #[cfg(target_os = "vita")]
403 let start = Instant::now();
404 self.pump_vm()?;
405 #[cfg(target_os = "vita")]
406 crate::render::vita_stats::phase(crate::render::vita_stats::PUMP, start);
407 }
408 self.redraw()?;
409 Ok(self.pending_exit || (self.vm.is_halted() && self.flow.stack.is_empty()))
410 }
411
412 pub fn mouse_move(&mut self, x: f64, y: f64) {
413 if self.native_messagebox_pending() {
414 return;
415 }
416 self.vm
417 .ctx
418 .on_mouse_move(x.round() as i32, y.round() as i32);
419 self.script_needs_pump = true;
420 }
421
422 pub fn mouse_down(&mut self, button: VmMouseButton) {
423 if self.native_messagebox_pending() {
424 return;
425 }
426 self.vm.ctx.on_mouse_down(button);
427 self.script_needs_pump = true;
428 }
429
430 pub fn mouse_up(&mut self, button: VmMouseButton) {
431 if self.native_messagebox_pending() {
432 return;
433 }
434 self.vm.ctx.on_mouse_up(button);
435 self.script_needs_pump = true;
436 }
437
438 pub fn mouse_wheel(&mut self, delta_y: i32) {
439 if self.native_messagebox_pending() {
440 return;
441 }
442 self.vm.ctx.on_mouse_wheel(delta_y);
443 self.script_needs_pump = true;
444 }
445
446 pub fn touch(&mut self, phase: i32, x: f64, y: f64) {
447 self.mouse_move(x, y);
448 match phase {
449 0 => self.mouse_down(VmMouseButton::Left),
450 1 => {}
451 2 | 3 => self.mouse_up(VmMouseButton::Left),
452 _ => {}
453 }
454 }
455
456 pub fn key_down(&mut self, key: VmKey) {
457 if self.native_messagebox_pending() {
458 return;
459 }
460 self.vm.ctx.on_key_down(key);
461 self.script_needs_pump = true;
462 }
463
464 pub fn key_down_code(&mut self, code: i32) {
465 if let Some(key) = vm_key_from_platform_code(code) {
466 self.key_down(key);
467 }
468 }
469
470 pub fn key_up(&mut self, key: VmKey) {
471 if self.native_messagebox_pending() {
472 return;
473 }
474 self.vm.ctx.on_key_up(key);
475 self.script_needs_pump = true;
476 }
477
478 pub fn joypad_button(&mut self, button: usize, down: bool) {
482 if self.native_messagebox_pending() {
483 return;
484 }
485 if down {
486 self.vm.ctx.script_input.on_joypad_key_down(button);
487 } else {
488 self.vm.ctx.script_input.on_joypad_key_up(button);
489 }
490 self.script_needs_pump = true;
491 }
492
493 pub fn key_up_code(&mut self, code: i32) {
494 if let Some(key) = vm_key_from_platform_code(code) {
495 self.key_up(key);
496 }
497 }
498
499 pub fn key_event(&mut self, code: i32, text: Option<&str>, is_repeat: bool) {
505 if self.native_messagebox_pending() {
506 return;
507 }
508 match vm_key_from_platform_code(code) {
509 Some(key) => {
510 if !is_repeat || !matches!(key, VmKey::Enter | VmKey::Space | VmKey::Escape) {
511 self.vm.ctx.on_key_down(key);
512 }
513 }
514 None => {
515 if !self.vm.ctx.editbox_accepts_keyboard_input() {
516 self.vm.ctx.notify_wait_key();
517 }
518 }
519 }
520 if self.vm.ctx.editbox_accepts_direct_text()
521 && let Some(text) = text
522 && !text.is_empty()
523 {
524 self.vm.ctx.on_text_input(text);
525 }
526 self.script_needs_pump = true;
527 }
528
529 pub fn editbox_accepts_direct_text(&self) -> bool {
530 self.vm.ctx.editbox_accepts_direct_text()
531 }
532
533 pub fn focused_editbox_ime_area(&self) -> Option<(i32, i32, i32, i32)> {
536 self.vm.ctx.focused_editbox_ime_area()
537 }
538
539 pub fn notify_wait_key(&mut self) {
540 if self.native_messagebox_pending() {
541 return;
542 }
543 self.vm.ctx.notify_wait_key();
544 self.script_needs_pump = true;
545 }
546
547 pub fn text_input(&mut self, text: &str) {
548 if self.native_messagebox_pending() {
549 return;
550 }
551 self.vm.ctx.on_text_input(text);
552 self.script_needs_pump = true;
553 }
554
555 pub fn ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) {
556 if self.native_messagebox_pending() {
557 return;
558 }
559 self.vm.ctx.on_ime_preedit(text, cursor);
560 self.script_needs_pump = true;
561 }
562
563 pub fn ime_disabled(&mut self) {
564 if self.native_messagebox_pending() {
565 return;
566 }
567 self.vm.ctx.on_ime_disabled();
568 self.script_needs_pump = true;
569 }
570
571 pub fn renderer_mut(&mut self) -> RefMut<'_, Renderer> {
572 self.renderer.borrow_mut()
573 }
574
575 pub fn persist_global_if_changed(&mut self, last: Option<u64>) -> u64 {
578 let now = crate::runtime::forms::syscom::global_save_fingerprint(&self.vm.ctx);
579 if last.is_some_and(|last| last != now) {
580 crate::runtime::forms::syscom::write_global_save(&self.vm.ctx);
581 }
582 now
583 }
584
585 pub fn vm_mut(&mut self) -> &mut SceneVm<'static> {
586 &mut self.vm
587 }
588
589 fn resolve_initial_size(config: &SiglusHostConfig) -> (u32, u32) {
590 let cfg_size = Self::try_load_gameexe(&config.project_dir)
591 .as_ref()
592 .and_then(Self::gameexe_screen_size)
593 .unwrap_or((1280, 720));
594 (
595 config.width.unwrap_or(cfg_size.0),
596 config.height.unwrap_or(cfg_size.1),
597 )
598 }
599
600 fn gameexe_screen_size(cfg: &GameexeConfig) -> Option<(u32, u32)> {
601 let entry = cfg.get_entry("SCREEN_SIZE")?;
602 let w = entry.item_unquoted(0)?.trim().parse::<u32>().ok()?;
603 let h = entry.item_unquoted(1)?.trim().parse::<u32>().ok()?;
604 if w == 0 || h == 0 {
605 return None;
606 }
607 Some((w, h))
608 }
609
610 fn gameexe_scene_entry(cfg: &GameexeConfig, key: &str) -> Option<(String, i32)> {
611 let entry = cfg.get_entry(key)?;
612 let scene = entry.item_unquoted(0)?.trim().trim_matches('"').to_string();
613 if scene.is_empty() {
614 return None;
615 }
616 let z = entry
617 .item_unquoted(1)
618 .and_then(|s| s.trim().parse::<i32>().ok())
619 .unwrap_or(0);
620 Some((scene, z))
621 }
622
623 fn resolve_boot_config(config: &SiglusHostConfig) -> BootConfig {
624 let cfg = Self::try_load_gameexe(&config.project_dir);
625 let (default_start, default_start_z) = cfg
626 .as_ref()
627 .and_then(|cfg| Self::gameexe_scene_entry(cfg, "START_SCENE"))
628 .unwrap_or_else(|| ("_start".to_string(), 0));
629 let (menu_scene, menu_z) = cfg
632 .as_ref()
633 .and_then(|cfg| Self::gameexe_scene_entry(cfg, "MENU_SCENE"))
634 .unwrap_or_else(|| ("_menu".to_string(), 0));
635 BootConfig {
636 start_scene: config.scene_name.clone().unwrap_or(default_start),
637 start_z: default_start_z,
638 menu_scene,
639 menu_z,
640 }
641 }
642
643 fn try_load_gameexe(project_dir: &Path) -> Option<GameexeConfig> {
644 let path = crate::resource::find_initial_gameexe_path(project_dir).ok()?;
645 let raw = crate::resource::read_file_bytes(&path).ok()?;
646 if path
647 .extension()
648 .and_then(|s| s.to_str())
649 .is_some_and(|ext| ext.eq_ignore_ascii_case("ini"))
650 {
651 let text = String::from_utf8(raw).ok()?;
652 return Some(GameexeConfig::from_text(&text));
653 }
654 let opt = load_gameexe_decode_options(project_dir).ok()?;
655 let (text, _report) = decode_gameexe_dat_bytes(&raw, &opt).ok()?;
656 Some(GameexeConfig::from_text(&text))
657 }
658
659 fn init_vm(
660 config: &SiglusHostConfig,
661 boot: &BootConfig,
662 initial_size: (u32, u32),
663 ) -> Result<SceneVm<'static>> {
664 let project_dir = config.project_dir.clone();
665 switch_startup_marker!(b"siglus_switch: vm scene-pck-path begin\n\0");
666 let scene_pck_path = find_scene_pck_for_host(&project_dir)?;
667 switch_startup_marker!(b"siglus_switch: vm scene-pck-path complete\n\0");
668 let opt = load_scene_pck_decode_options(&project_dir)?;
669 switch_startup_marker!(b"siglus_switch: vm scene-pck-open begin\n\0");
670 let pck = {
671 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
672 {
673 let bytes = crate::resource::read_file_bytes(&scene_pck_path)
674 .with_context(|| format!("read scene.pck: {}", scene_pck_path.display()))?;
675 ScenePck::load_lazy_from_bytes(bytes, &opt)
676 .with_context(|| format!("open scene.pck: {}", scene_pck_path.display()))?
677 }
678 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
679 {
680 ScenePck::load_lazy(&scene_pck_path, &opt)
681 .with_context(|| format!("open scene.pck: {}", scene_pck_path.display()))?
682 }
683 };
684 switch_startup_marker!(b"siglus_switch: vm scene-pck-open complete\n\0");
685
686 let scene_no = if let Some(id) = config.scene_id {
687 id
688 } else if let Some(name) = config.scene_name.as_ref() {
689 pck.find_scene_no(name).unwrap_or(0)
690 } else {
691 pck.find_scene_no(&boot.start_scene).unwrap_or(0)
692 };
693
694 let (owner, range) = pck
695 .scn_data_shared(scene_no)
696 .with_context(|| format!("scene_id out of range: {}", scene_no))?;
697 switch_startup_marker!(b"siglus_switch: vm scene-stream begin\n\0");
698 let mut stream =
699 SceneStream::new_shared_range_with_string_codec(owner, range, pck.string_codec)?;
700 switch_startup_marker!(b"siglus_switch: vm scene-stream complete\n\0");
701 let start_z = if config.scene_id.is_some() || config.scene_name.is_some() {
702 0
703 } else {
704 boot.start_z
705 };
706 stream.jump_to_z_label(start_z.max(0) as usize)?;
707 switch_startup_marker!(b"siglus_switch: vm command-context begin\n\0");
708 let mut ctx = CommandContext::new(project_dir);
709 switch_startup_marker!(b"siglus_switch: vm command-context complete\n\0");
710 let active_append = ctx.globals.append_dir.clone();
711 switch_startup_marker!(b"siglus_switch: vm scene-metadata begin\n\0");
712 ctx.install_scene_metadata(&active_append, &pck)?;
713 switch_startup_marker!(b"siglus_switch: vm scene-metadata complete\n\0");
714 ctx.screen_w = initial_size.0;
715 ctx.screen_h = initial_size.1;
716 switch_startup_marker!(b"siglus_switch: vm create begin\n\0");
717 let mut vm = SceneVm::with_config(VmConfig::from_env(), stream, ctx);
718 vm.install_initial_scene_pck(pck, active_append);
719 switch_startup_marker!(b"siglus_switch: vm create complete\n\0");
720 if config.scene_id.is_none() && config.scene_name.is_none() {
723 switch_startup_marker!(b"siglus_switch: vm global-save begin\n\0");
724 crate::runtime::forms::syscom::load_global_save(&mut vm.ctx)
725 .context("load global save during engine initialization")?;
726 switch_startup_marker!(b"siglus_switch: vm global-save complete\n\0");
727 if env_is_set!("SG_BOOT_TRACE") {
728 let g1000 = vm
729 .ctx
730 .globals
731 .int_lists
732 .get(&(crate::runtime::forms::codes::ELM_GLOBAL_G as u32))
733 .and_then(|g| g.get(1000))
734 .copied()
735 .unwrap_or(0);
736 eprintln!("[SG_BOOT] global save initialization complete, g[1000]={g1000}");
737 }
738 }
739 if config.scene_id.is_none() {
740 switch_startup_marker!(b"siglus_switch: vm restart-scene begin\n\0");
741 let scene_name = config
742 .scene_name
743 .clone()
744 .unwrap_or_else(|| boot.start_scene.clone());
745 vm.restart_scene_name(&scene_name, start_z)?;
746 switch_startup_marker!(b"siglus_switch: vm restart-scene complete\n\0");
747 }
748 Ok(vm)
749 }
750
751 fn suspend_wait_for_syscom_excall(&mut self, key: &str) {
752 let flow_depth = self.flow.stack.len();
753 let saved_wait = std::mem::take(&mut self.vm.ctx.wait);
754 self.vm.ctx.input.use_current();
755 self.vm.ctx.script_input.use_current();
756 self.syscom_suspended_waits
757 .push((flow_depth, saved_wait, key.to_string()));
758 if env_is_set!("SG_PROC_FLOW_TRACE") {
759 eprintln!(
760 "[SG_PROC_FLOW] host suspend_wait_for_syscom_excall key={} flow_depth={} saved_count={} scene={:?} line={}",
761 key,
762 flow_depth,
763 self.syscom_suspended_waits.len(),
764 self.vm.current_scene_name(),
765 self.vm.current_line_no()
766 );
767 }
768 }
769
770 fn restore_wait_after_syscom_excall(&mut self, popped_depth: usize) {
771 let should_restore = self
772 .syscom_suspended_waits
773 .last()
774 .map(|(depth, _, _)| *depth == popped_depth)
775 .unwrap_or(false);
776 if !should_restore {
777 return;
778 }
779 let Some((_depth, saved_wait, key)) = self.syscom_suspended_waits.pop() else {
780 return;
781 };
782 self.vm.ctx.wait = saved_wait;
783 self.vm.ctx.input.clear_all();
784 self.vm.ctx.script_input.clear_all();
785 if key == "SAVE_SCENE" {
786 crate::runtime::forms::syscom::free_runtime_save_thumb_capture(
787 &mut self.vm.ctx,
788 crate::runtime::forms::syscom::CAPTURE_PRIOR_SAVE,
789 );
790 }
791 if env_is_set!("SG_PROC_FLOW_TRACE") {
792 eprintln!(
793 "[SG_PROC_FLOW] host restore_wait_after_syscom_excall popped_depth={} remaining={} scene={:?} line={}",
794 popped_depth,
795 self.syscom_suspended_waits.len(),
796 self.vm.current_scene_name(),
797 self.vm.current_line_no()
798 );
799 }
800 }
801
802 fn consume_syscom_pending_proc(&mut self) -> Result<bool> {
803 let Some(proc) = self.vm.ctx.globals.syscom.pending_proc.take() else {
804 return Ok(false);
805 };
806
807 self.vm.ctx.globals.syscom.menu_open = false;
808 self.vm.ctx.globals.syscom.menu_kind = None;
809 if proc.kind != SyscomPendingProcKind::MsgBack {
810 self.vm.ctx.globals.syscom.msg_back_open = false;
811 }
812
813 if env_is_set!("SG_PROC_FLOW_TRACE") {
814 eprintln!(
815 "[SG_PROC_FLOW] host consume_syscom_pending kind={:?} before scene={:?} line={} flow={:?}",
816 proc.kind,
817 self.vm.current_scene_name(),
818 self.vm.current_line_no(),
819 self.flow.stack
820 );
821 }
822
823 match proc.kind {
824 SyscomPendingProcKind::EndGame => {
825 if proc.warning {
826 self.begin_syscom_warning(proc);
827 } else {
828 self.queue_end_game_proc(proc);
829 }
830 Ok(true)
831 }
832 SyscomPendingProcKind::ReturnToMenu => {
833 if proc.warning {
834 self.begin_syscom_warning(proc);
835 } else {
836 self.queue_return_to_menu_proc(proc);
837 }
838 Ok(true)
839 }
840 SyscomPendingProcKind::RestartScene => {
841 if proc.warning {
842 self.begin_syscom_warning(proc);
843 } else {
844 self.perform_restart_from_scene()?;
845 }
846 Ok(true)
847 }
848 SyscomPendingProcKind::ReturnToSel => {
849 if self.vm.restore_last_sel_point() {
850 self.flow.stack.clear();
851 self.flow.push(ProcType::GameTimerStart, 0);
852 self.flow.push(ProcType::Script, 0);
853 Ok(true)
854 } else {
855 self.vm.ctx.unknown.record_note(
856 "SYSCOM.RETURN_TO_SEL requested without an in-memory SELPOINT snapshot",
857 );
858 Ok(false)
859 }
860 }
861 SyscomPendingProcKind::Save => {
862 if proc.warning {
863 self.begin_syscom_warning(proc);
864 } else {
865 crate::runtime::forms::syscom::menu_save_slot(
866 &mut self.vm.ctx,
867 false,
868 proc.save_id.max(0) as usize,
869 );
870 crate::runtime::forms::syscom::write_global_save(&self.vm.ctx);
871 }
872 Ok(true)
873 }
874 SyscomPendingProcKind::Load => {
875 if proc.warning {
876 self.begin_syscom_warning(proc);
877 } else {
878 crate::runtime::forms::syscom::menu_load_slot(
879 &mut self.vm.ctx,
880 false,
881 proc.save_id.max(0) as usize,
882 );
883 }
884 Ok(true)
885 }
886 SyscomPendingProcKind::QuickSave => {
887 if proc.warning {
888 self.begin_syscom_warning(proc);
889 } else {
890 crate::runtime::forms::syscom::menu_save_slot(
891 &mut self.vm.ctx,
892 true,
893 proc.save_id.max(0) as usize,
894 );
895 crate::runtime::forms::syscom::write_global_save(&self.vm.ctx);
896 }
897 Ok(true)
898 }
899 SyscomPendingProcKind::QuickLoad => {
900 if proc.warning {
901 self.begin_syscom_warning(proc);
902 } else {
903 crate::runtime::forms::syscom::menu_load_slot(
904 &mut self.vm.ctx,
905 true,
906 proc.save_id.max(0) as usize,
907 );
908 }
909 Ok(true)
910 }
911 SyscomPendingProcKind::BacklogLoad => {
912 if self.vm.restore_last_sel_point() {
913 self.flow.stack.clear();
914 self.flow.push(ProcType::GameTimerStart, 0);
915 self.flow.push(ProcType::Script, 0);
916 Ok(true)
917 } else {
918 self.vm.ctx.unknown.record_note(&format!(
919 "SYSCOM.MSG_BACK_LOAD requested but backlog save {} is not materialized without SAVE/LOAD support",
920 proc.save_id
921 ));
922 Ok(false)
923 }
924 }
925 SyscomPendingProcKind::MsgBack => {
926 if self.vm.ctx.globals.syscom.msg_back_open {
927 self.flow.push(ProcType::MsgBack, 0);
928 Ok(true)
929 } else {
930 Ok(false)
931 }
932 }
933 SyscomPendingProcKind::OpenSyscomMenu => {
934 if self.vm.call_syscom_configured_scene("CANCEL_SCENE")? {
935 self.ensure_requested_script_proc();
936 self.suspend_wait_for_syscom_excall("CANCEL_SCENE");
937 Ok(true)
938 } else {
939 syscom_form::open_fallback_dialog(
940 &mut self.vm.ctx,
941 SyscomPendingProcKind::OpenSyscomMenu,
942 );
943 Ok(true)
944 }
945 }
946 SyscomPendingProcKind::OpenSave => {
947 if self.vm.call_syscom_configured_scene("SAVE_SCENE")? {
948 self.ensure_requested_script_proc();
949 self.suspend_wait_for_syscom_excall("SAVE_SCENE");
950 Ok(true)
951 } else {
952 syscom_form::open_fallback_dialog(
953 &mut self.vm.ctx,
954 SyscomPendingProcKind::OpenSave,
955 );
956 Ok(true)
957 }
958 }
959 SyscomPendingProcKind::OpenLoad => {
960 if self.vm.call_syscom_configured_scene("LOAD_SCENE")? {
961 self.ensure_requested_script_proc();
962 self.suspend_wait_for_syscom_excall("LOAD_SCENE");
963 Ok(true)
964 } else {
965 syscom_form::open_fallback_dialog(
966 &mut self.vm.ctx,
967 SyscomPendingProcKind::OpenLoad,
968 );
969 Ok(true)
970 }
971 }
972 SyscomPendingProcKind::OpenConfig | SyscomPendingProcKind::OpenConfigDialog => {
973 if proc.kind == SyscomPendingProcKind::OpenConfig
974 && self.vm.call_syscom_configured_scene("CONFIG_SCENE")?
975 {
976 self.ensure_requested_script_proc();
977 self.suspend_wait_for_syscom_excall("CONFIG_SCENE");
978 Ok(true)
979 } else {
980 syscom_form::open_fallback_dialog(
981 &mut self.vm.ctx,
982 SyscomPendingProcKind::OpenConfig,
983 );
984 Ok(true)
985 }
986 }
987 }
988 }
989
990 fn ensure_requested_script_proc(&mut self) {
991 let requested = self.vm.take_script_proc_request();
992 if requested {
993 if env_is_set!("SG_PROC_FLOW_TRACE") {
994 eprintln!(
995 "[SG_PROC_FLOW] host ensure_requested_script_proc push before scene={:?} line={} flow={:?}",
996 self.vm.current_scene_name(),
997 self.vm.current_line_no(),
998 self.flow.stack
999 );
1000 }
1001 self.flow.push(ProcType::Script, 0);
1002 if env_is_set!("SG_PROC_FLOW_TRACE") {
1003 eprintln!(
1004 "[SG_PROC_FLOW] host ensure_requested_script_proc push after flow={:?}",
1005 self.flow.stack
1006 );
1007 }
1008 }
1009 }
1010
1011 fn begin_syscom_warning(&mut self, mut proc: SyscomPendingProc) {
1012 let kind = proc.kind;
1013 proc.warning = false;
1014 self.flow.pending_syscom_proc = Some(proc);
1015 self.vm.ctx.globals.system.messagebox_modal_result = None;
1016 let request_id = self.vm.ctx.native_ui.next_messagebox_request_id();
1017 let buttons = vec![
1018 SystemMessageBoxButton {
1019 label: "YES".to_string(),
1020 value: 0,
1021 },
1022 SystemMessageBoxButton {
1023 label: "NO".to_string(),
1024 value: 1,
1025 },
1026 ];
1027 let text = self.syscom_warning_text(kind);
1028 let title = self.vm.ctx.game_title();
1029 if let Some(backend) = self.vm.ctx.native_ui_backend.as_ref() {
1030 self.vm.ctx.globals.system.messagebox_modal = Some(SystemMessageBoxModalState {
1031 request_id,
1032 kind: 19,
1033 text: text.clone(),
1034 debug_only: false,
1035 buttons: buttons.clone(),
1036 cursor: 1,
1037 native_pending: true,
1038 complete_wait_with_value: false,
1039 });
1040 backend.show_system_messagebox(native_ui::NativeMessageBoxRequest {
1041 request_id,
1042 kind: native_ui::NativeMessageBoxKind::YesNo,
1043 title,
1044 message: text,
1045 buttons: buttons
1046 .into_iter()
1047 .map(|button| native_ui::NativeMessageBoxButton {
1048 label: button.label,
1049 value: button.value,
1050 })
1051 .collect(),
1052 debug_only: false,
1053 });
1054 } else {
1055 self.vm.ctx.globals.system.messagebox_modal = Some(SystemMessageBoxModalState {
1056 request_id,
1057 kind: 19,
1058 text,
1059 debug_only: false,
1060 buttons,
1061 cursor: 1,
1062 native_pending: false,
1063 complete_wait_with_value: false,
1064 });
1065 }
1066 self.flow.push(ProcType::SyscomWarning, 0);
1067 }
1068
1069 fn syscom_warning_text(&self, kind: SyscomPendingProcKind) -> String {
1070 let keys: &[&str] = match kind {
1071 SyscomPendingProcKind::EndGame => &[
1072 "#WARNINGINFO.GAMEEND_WARNING_STR",
1073 "WARNINGINFO.GAMEEND_WARNING_STR",
1074 ],
1075 SyscomPendingProcKind::ReturnToSel => &[
1076 "#WARNINGINFO.RETURNSEL_WARNING_STR",
1077 "WARNINGINFO.RETURNSEL_WARNING_STR",
1078 "#WARNINGINFO.RETURNMENU_WARNING_STR",
1079 "WARNINGINFO.RETURNMENU_WARNING_STR",
1080 ],
1081 SyscomPendingProcKind::RestartScene => &[
1082 "#WARNINGINFO.SCENESTART_WARNING_STR",
1083 "WARNINGINFO.SCENESTART_WARNING_STR",
1084 ],
1085 SyscomPendingProcKind::Save | SyscomPendingProcKind::QuickSave => &[
1086 "#WARNINGINFO.SAVE_WARNING_STR",
1087 "WARNINGINFO.SAVE_WARNING_STR",
1088 ],
1089 SyscomPendingProcKind::Load | SyscomPendingProcKind::QuickLoad => &[
1090 "#WARNINGINFO.LOAD_WARNING_STR",
1091 "WARNINGINFO.LOAD_WARNING_STR",
1092 ],
1093 _ => &[
1094 "#WARNINGINFO.RETURNMENU_WARNING_STR",
1095 "WARNINGINFO.RETURNMENU_WARNING_STR",
1096 ],
1097 };
1098 let default = match kind {
1099 SyscomPendingProcKind::EndGame => "終了してもよろしいですか?",
1100 SyscomPendingProcKind::ReturnToSel => "前の選択肢に戻ってもよろしいですか?",
1101 SyscomPendingProcKind::RestartScene => "途中から始めてもよろしいですか?",
1102 SyscomPendingProcKind::Save | SyscomPendingProcKind::QuickSave => {
1103 "セーブデータを上書きしてもよろしいですか?"
1104 }
1105 SyscomPendingProcKind::Load | SyscomPendingProcKind::QuickLoad => {
1106 "セーブデータをロードしてもよろしいですか?"
1107 }
1108 _ => "タイトルに戻ってもよろしいですか?",
1109 };
1110 let cfg = self.vm.ctx.tables.gameexe.as_ref();
1111 keys.iter()
1112 .find_map(|key| cfg.and_then(|c| c.get_unquoted(key)))
1113 .filter(|s| !s.is_empty())
1114 .map(str::to_string)
1115 .unwrap_or_else(|| default.to_string())
1116 }
1117
1118 fn return_to_menu_warning_text(&self) -> String {
1119 let cfg = self.vm.ctx.tables.gameexe.as_ref();
1120 [
1121 "#WARNINGINFO.RETURNMENU_WARNING_STR",
1122 "WARNINGINFO.RETURNMENU_WARNING_STR",
1123 ]
1124 .iter()
1125 .find_map(|key| cfg.and_then(|c| c.get_unquoted(key)))
1126 .filter(|s| !s.is_empty())
1127 .map(str::to_string)
1128 .unwrap_or_else(|| "タイトルに戻ってもよろしいですか?".to_string())
1129 }
1130
1131 fn load_wipe_params(&self) -> (i32, i32) {
1132 fn parse_pair(raw: &str) -> Option<(i32, i32)> {
1133 let nums: Vec<i32> = raw
1134 .split(|c: char| !(c == '-' || c.is_ascii_digit()))
1135 .filter(|s| !s.is_empty())
1136 .filter_map(|s| s.parse::<i32>().ok())
1137 .collect();
1138 if nums.len() >= 2 {
1139 Some((nums[0], nums[1]))
1140 } else {
1141 None
1142 }
1143 }
1144 let cfg = self.vm.ctx.tables.gameexe.as_ref();
1145 for key in ["LOAD.WIPE", "LOAD . WIPE", "#LOAD.WIPE", "#LOAD . WIPE"] {
1146 if let Some(pair) = cfg.and_then(|c| c.get_value(key)).and_then(parse_pair) {
1147 return pair;
1148 }
1149 }
1150 (0, 1000)
1151 }
1152
1153 fn queue_end_game_proc(&mut self, proc: SyscomPendingProc) {
1154 self.flow.pending_syscom_proc = None;
1155 self.flow.push(ProcType::EndGame, 0);
1156 if proc.fade_out {
1157 self.flow.push(ProcType::GameEndWipe, 0);
1158 self.flow.push(ProcType::Disp, 0);
1159 } else {
1160 self.flow.push(ProcType::Disp, 0);
1161 }
1162 }
1163
1164 fn queue_return_to_menu_proc(&mut self, proc: SyscomPendingProc) {
1165 crate::runtime::forms::syscom::write_global_save(&self.vm.ctx);
1168 let option = if proc.leave_msgbk { 1 } else { 0 };
1169 self.flow.pending_syscom_proc = Some(proc.clone());
1170 self.flow.push(ProcType::ReturnToMenu, option);
1171 if proc.fade_out {
1172 self.flow.push(ProcType::GameEndWipe, 0);
1173 self.flow.push(ProcType::Disp, 0);
1174 }
1175 }
1176
1177 fn start_game_end_wipe(&mut self) {
1178 let (wipe_type, wipe_time) = self.load_wipe_params();
1179 if self.vm.ctx.globals.wipe.is_some() {
1180 self.vm.ctx.finish_wipe_runtime();
1181 }
1182 let stage_form_id = if self.vm.ctx.ids.form_global_stage != 0 {
1183 self.vm.ctx.ids.form_global_stage
1184 } else {
1185 crate::runtime::forms::codes::FORM_GLOBAL_STAGE
1186 };
1187 self.vm.ctx.globals.start_wipe(WipeState::new(
1188 stage_form_id,
1189 None,
1190 None,
1191 wipe_type,
1192 wipe_time,
1193 0,
1194 0,
1195 Vec::new(),
1196 i32::MIN,
1197 i32::MAX,
1198 i32::MIN,
1199 i32::MAX,
1200 false,
1201 0,
1202 0,
1203 ));
1204 }
1205
1206 fn finish_runtime_load(&mut self) {
1207 self.renderer.borrow_mut().clear_runtime_image_textures();
1208 self.flow.stack.clear();
1209 self.flow.pending_syscom_proc = None;
1210 self.syscom_suspended_waits.clear();
1211 self.paused = false;
1212 self.script_resume_after_redraw = false;
1213 self.suppress_render_once = true;
1214 self.vm.ctx.globals.syscom.pending_proc = None;
1215 self.vm.ctx.globals.syscom.menu_open = false;
1216 self.vm.ctx.globals.syscom.menu_kind = None;
1217 self.vm.ctx.globals.syscom.msg_back_open = false;
1218 self.flow.push(ProcType::GameTimerStart, 0);
1219 self.flow.push(ProcType::Script, 0);
1220 self.script_needs_pump = true;
1221 }
1222
1223 fn perform_return_to_menu(&mut self, leave_msgbk: bool) -> Result<()> {
1224 let target_scene = self.boot.menu_scene.clone();
1225 let target_z = self.boot.menu_z;
1226 let saved_msgbk = if leave_msgbk {
1227 Some(self.vm.ctx.globals.msgbk_forms.clone())
1228 } else {
1229 None
1230 };
1231 self.vm.ctx.reset_active_append_to_initial();
1236 self.vm.restart_scene_name(&target_scene, target_z)?;
1237 self.renderer.borrow_mut().clear_runtime_image_textures();
1238 if let Some(msgbk) = saved_msgbk {
1239 self.vm.ctx.globals.msgbk_forms = msgbk;
1240 }
1241 self.vm.ctx.globals.finish_wipe();
1242 self.flow.stack.clear();
1243 self.flow.pending_syscom_proc = None;
1244 self.flow.booted_menu = true;
1245 self.flow.push(ProcType::Script, 0);
1248 self.flow.push(ProcType::GameTimerStart, 0);
1249 Ok(())
1250 }
1251
1252 fn perform_restart_from_scene(&mut self) -> Result<()> {
1253 let (target_scene, target_z) = self
1254 .vm
1255 .ctx
1256 .pending_scene_restart
1257 .take()
1258 .ok_or_else(|| anyhow::anyhow!("GLOBAL.RETURNMENU scene restart missing target"))?;
1259
1260 crate::runtime::forms::syscom::write_global_save(&self.vm.ctx);
1264 self.vm.restart_scene_name(&target_scene, target_z)?;
1265 self.renderer.borrow_mut().clear_runtime_image_textures();
1266 self.vm.ctx.globals.finish_wipe();
1267 self.flow.stack.clear();
1268 self.flow.pending_syscom_proc = None;
1269 self.flow.push(ProcType::Script, 0);
1270 self.script_needs_pump = true;
1271 Ok(())
1272 }
1273
1274 fn pump_vm(&mut self) -> Result<()> {
1275 if env_is_set!("SG_PROC_FLOW_TRACE") {
1276 eprintln!(
1277 "[SG_PROC_FLOW] host pump_vm start paused={} script_needs_pump={} scene={:?} line={} flow={:?} pending_proc={:?}",
1278 self.paused,
1279 self.script_needs_pump,
1280 self.vm.current_scene_name(),
1281 self.vm.current_line_no(),
1282 self.flow.stack,
1283 self.vm.ctx.globals.syscom.pending_proc
1284 );
1285 }
1286 self.script_needs_pump = false;
1287 self.ensure_requested_script_proc();
1288 if self.paused {
1289 return Ok(());
1290 }
1291
1292 self.vm.process_pending_button_actions()?;
1293 if env_is_set!("SG_PROC_FLOW_TRACE") {
1294 eprintln!(
1295 "[SG_PROC_FLOW] host pump_vm after_process_button_actions scene={:?} line={} flow={:?} pending_proc={:?}",
1296 self.vm.current_scene_name(),
1297 self.vm.current_line_no(),
1298 self.flow.stack,
1299 self.vm.ctx.globals.syscom.pending_proc
1300 );
1301 }
1302 if self.vm.ctx.globals.syscom.pending_proc.is_some() {
1303 self.consume_syscom_pending_proc()?;
1304 self.ensure_requested_script_proc();
1305 }
1306
1307 self.vm.begin_script_proc_pump();
1308
1309 loop {
1310 let Some(proc) = self.flow.top().cloned() else {
1311 self.paused = true;
1312 break;
1313 };
1314 if env_is_set!("SG_PROC_FLOW_TRACE") {
1315 eprintln!(
1316 "[SG_PROC_FLOW] host pump_vm loop top proc={:?} scene={:?} line={} flow={:?}",
1317 proc,
1318 self.vm.current_scene_name(),
1319 self.vm.current_line_no(),
1320 self.flow.stack
1321 );
1322 }
1323
1324 match proc.ty {
1325 ProcType::Script => {
1326 let proc_gen_before = self.vm.proc_generation();
1327 let running = self.vm.run_script_proc_continue()?;
1328 if self.vm.take_runtime_load_completed() {
1329 self.finish_runtime_load();
1330 continue;
1331 }
1332 let proc_boundary = self.vm.proc_generation() != proc_gen_before;
1333 let boundary_kind = self.vm.last_proc_kind();
1334 let pop_script_proc = self.vm.take_script_proc_pop_request();
1335 let halted = self.vm.is_halted();
1336 let cur_scene = self
1337 .vm
1338 .current_scene_name()
1339 .map(|s| s.to_string())
1340 .unwrap_or_else(|| self.boot.start_scene.clone());
1341 let pending = self.vm.ctx.globals.syscom.pending_proc.is_some();
1342 let blocked = if pending { false } else { self.vm.is_blocked() };
1343
1344 self.ensure_requested_script_proc();
1345 if pop_script_proc {
1346 let popped_depth = self.flow.stack.len();
1347 self.flow.pop();
1348 self.restore_wait_after_syscom_excall(popped_depth);
1349 continue;
1350 }
1351 if !running || halted {
1352 self.flow.pop();
1353 if !self.flow.booted_menu && cur_scene == self.boot.start_scene {
1354 self.flow.push(ProcType::ReturnToMenu, 0);
1355 }
1356 continue;
1357 }
1358 if pending {
1359 if self.consume_syscom_pending_proc()? {
1360 continue;
1361 }
1362 if self.vm.is_blocked() {
1363 break;
1364 }
1365 } else if proc_boundary {
1366 match boundary_kind {
1367 ProcKind::Disp => {
1368 self.script_resume_after_redraw = true;
1369 break;
1370 }
1371 ProcKind::Frame => {
1372 self.script_resume_after_redraw = true;
1373 self.suppress_render_once = true;
1374 break;
1375 }
1376 ProcKind::Command
1377 | ProcKind::MessageBlock
1378 | ProcKind::MessageWait
1379 | ProcKind::KeyWait
1380 | ProcKind::TimeWait
1381 | ProcKind::MovieWait
1382 | ProcKind::WipeWait
1383 | ProcKind::AudioWait
1384 | ProcKind::EventWait
1385 | ProcKind::Selection
1386 | ProcKind::SystemModal
1387 | ProcKind::Script => {
1388 if blocked {
1389 break;
1390 }
1391 continue;
1392 }
1393 }
1394 } else if blocked {
1395 break;
1396 }
1397 }
1398 ProcType::StartWarning => {
1399 let warning_exists = crate::resource::game_file_exists(
1400 &self
1401 .vm
1402 .ctx
1403 .images
1404 .project_dir()
1405 .join("g00")
1406 .join("___SYSEVE_WARNING.g00"),
1407 ) || crate::resource::game_file_exists(
1408 &self
1409 .vm
1410 .ctx
1411 .images
1412 .project_dir()
1413 .join("g00")
1414 .join("___SYSEVE_WARNING.g01"),
1415 );
1416 if !warning_exists {
1417 self.flow.pop();
1418 continue;
1419 }
1420 let cur = self.redraw_count;
1421 let top = self.flow.top_mut().expect("proc top");
1422 match top.option {
1423 0 => {
1424 top.option = 1;
1425 self.flow.push(ProcType::TimeWait, 0);
1426 if let Some(wait) = self.flow.top_mut() {
1427 wait.deadline_frame = Some(cur.saturating_add(60));
1428 }
1429 }
1430 _ => {
1431 self.flow.pop();
1432 }
1433 }
1434 break;
1435 }
1436 ProcType::SyscomWarning => {
1437 if self.vm.ctx.globals.system.messagebox_modal.is_some() {
1438 break;
1439 }
1440 let result = self
1441 .vm
1442 .ctx
1443 .globals
1444 .system
1445 .messagebox_modal_result
1446 .take()
1447 .unwrap_or(1);
1448 let pending = self.flow.pending_syscom_proc.take();
1449 self.flow.pop();
1450 if result == 0 {
1451 if let Some(proc) = pending {
1452 match proc.kind {
1453 SyscomPendingProcKind::EndGame => {
1454 self.queue_end_game_proc(proc);
1455 }
1456 SyscomPendingProcKind::ReturnToMenu => {
1457 self.queue_return_to_menu_proc(proc);
1458 }
1459 SyscomPendingProcKind::RestartScene => {
1460 self.perform_restart_from_scene()?;
1461 }
1462 SyscomPendingProcKind::Save => {
1463 crate::runtime::forms::syscom::menu_save_slot(
1464 &mut self.vm.ctx,
1465 false,
1466 proc.save_id.max(0) as usize,
1467 );
1468 crate::runtime::forms::syscom::write_global_save(&self.vm.ctx);
1469 }
1470 SyscomPendingProcKind::Load => {
1471 crate::runtime::forms::syscom::menu_load_slot(
1472 &mut self.vm.ctx,
1473 false,
1474 proc.save_id.max(0) as usize,
1475 );
1476 }
1477 SyscomPendingProcKind::QuickSave => {
1478 crate::runtime::forms::syscom::menu_save_slot(
1479 &mut self.vm.ctx,
1480 true,
1481 proc.save_id.max(0) as usize,
1482 );
1483 crate::runtime::forms::syscom::write_global_save(&self.vm.ctx);
1484 }
1485 SyscomPendingProcKind::QuickLoad => {
1486 crate::runtime::forms::syscom::menu_load_slot(
1487 &mut self.vm.ctx,
1488 true,
1489 proc.save_id.max(0) as usize,
1490 );
1491 }
1492 _ => {}
1493 }
1494 }
1495 } else if matches!(
1496 pending.as_ref().map(|proc| proc.kind),
1497 Some(SyscomPendingProcKind::RestartScene)
1498 ) {
1499 self.vm.ctx.pending_scene_restart = None;
1500 } else if matches!(
1501 pending.as_ref().map(|proc| proc.kind),
1502 Some(SyscomPendingProcKind::Save)
1503 ) {
1504 crate::runtime::forms::syscom::free_runtime_save_thumb_capture(
1505 &mut self.vm.ctx,
1506 crate::runtime::forms::syscom::CAPTURE_PRIOR_SAVE,
1507 );
1508 }
1509 continue;
1510 }
1511 ProcType::MsgBack => {
1512 if !self.vm.ctx.globals.syscom.msg_back_open {
1513 self.flow.pop();
1514 continue;
1515 }
1516 break;
1517 }
1518 ProcType::Disp => {
1519 self.flow.pop();
1520 self.script_resume_after_redraw = true;
1521 break;
1522 }
1523 ProcType::GameEndWipe => {
1524 let mut start = false;
1525 if let Some(top) = self.flow.top_mut()
1526 && top.option == 0
1527 {
1528 top.option = 1;
1529 start = true;
1530 }
1531 if start {
1532 self.start_game_end_wipe();
1533 break;
1534 }
1535 if self.vm.ctx.globals.wipe_done() {
1536 self.flow.pop();
1537 continue;
1538 }
1539 break;
1540 }
1541 ProcType::ReturnToMenu => {
1542 let leave_msgbk = proc.option != 0;
1543 self.perform_return_to_menu(leave_msgbk)?;
1544 self.script_resume_after_redraw = true;
1548 break;
1549 }
1550 ProcType::EndGame => {
1551 self.flow.pop();
1552 crate::runtime::forms::syscom::write_global_save(&self.vm.ctx);
1553 self.vm.ctx.globals.system.active_flag = false;
1554 continue;
1555 }
1556 ProcType::GameTimerStart => {
1557 self.flow.pop();
1558 continue;
1559 }
1560 ProcType::TimeWait => {
1561 let deadline = proc.deadline_frame.unwrap_or(self.redraw_count);
1562 if self.redraw_count >= deadline {
1563 self.flow.pop();
1564 continue;
1565 }
1566 break;
1567 }
1568 }
1569 break;
1570 }
1571 Ok(())
1572 }
1573
1574 fn redraw(&mut self) -> Result<()> {
1575 #[cfg(target_os = "horizon")]
1576 let (switch_trace_frame, switch_trace_enabled) = {
1577 (0_u32, false)
1582 };
1583 #[cfg(target_os = "horizon")]
1584 if switch_trace_enabled {
1585 let scene = self.vm.current_scene_name().map(str::to_owned);
1586 let line = self.vm.current_line_no();
1587 let blocked = self.vm.is_blocked();
1588 crate::switch_host::report_switch_diagnostic(&format!(
1589 "siglus_switch: host-trace frame={} phase=redraw-begin scene={:?} line={} blocked={}\n",
1590 switch_trace_frame, scene, line, blocked,
1591 ));
1592 }
1593 if env_is_set!("SG_PROC_FLOW_TRACE") {
1594 eprintln!(
1595 "[SG_PROC_FLOW] host redraw start scene={:?} line={} flow={:?} pending_proc={:?}",
1596 self.vm.current_scene_name(),
1597 self.vm.current_line_no(),
1598 self.flow.stack,
1599 self.vm.ctx.globals.syscom.pending_proc
1600 );
1601 }
1602 if self.script_needs_pump {
1607 #[cfg(target_os = "horizon")]
1608 if switch_trace_enabled {
1609 crate::switch_host::report_switch_diagnostic(&format!(
1610 "siglus_switch: host-trace frame={} phase=pump-vm-begin\n",
1611 switch_trace_frame
1612 ));
1613 }
1614 #[cfg(target_os = "vita")]
1615 let start = Instant::now();
1616 self.pump_vm()?;
1617 #[cfg(target_os = "vita")]
1618 crate::render::vita_stats::phase(crate::render::vita_stats::PUMP, start);
1619 #[cfg(target_os = "horizon")]
1620 if switch_trace_enabled {
1621 crate::switch_host::report_switch_diagnostic(&format!(
1622 "siglus_switch: host-trace frame={} phase=pump-vm-complete\n",
1623 switch_trace_frame
1624 ));
1625 }
1626 }
1627 self.renderer
1631 .borrow_mut()
1632 .set_wait_display_vsync(!self.vm.ctx.globals.script.wait_display_vsync_off_flag);
1633 let wait_poll_needed = self.vm.ctx.wait.needs_runtime_poll();
1634 #[cfg(target_os = "horizon")]
1635 if switch_trace_enabled {
1636 crate::switch_host::report_switch_diagnostic(&format!(
1637 "siglus_switch: host-trace frame={} phase=tick-frame-begin\n",
1638 switch_trace_frame
1639 ));
1640 }
1641 #[cfg(target_os = "vita")]
1642 let start = Instant::now();
1643 self.vm.tick_frame()?;
1644 #[cfg(target_os = "vita")]
1645 crate::render::vita_stats::phase(crate::render::vita_stats::TICK, start);
1646 #[cfg(target_os = "horizon")]
1647 if switch_trace_enabled {
1648 crate::switch_host::report_switch_diagnostic(&format!(
1649 "siglus_switch: host-trace frame={} phase=tick-frame-complete\n",
1650 switch_trace_frame
1651 ));
1652 }
1653 if self.vm.take_runtime_load_completed() {
1654 self.finish_runtime_load();
1655 return Ok(());
1656 }
1657 if env_is_set!("SG_PROC_FLOW_TRACE") {
1658 eprintln!(
1659 "[SG_PROC_FLOW] host redraw after_tick scene={:?} line={} flow={:?} pending_proc={:?}",
1660 self.vm.current_scene_name(),
1661 self.vm.current_line_no(),
1662 self.flow.stack,
1663 self.vm.ctx.globals.syscom.pending_proc
1664 );
1665 }
1666 if self.vm.ctx.globals.syscom.pending_proc.is_some() {
1667 self.consume_syscom_pending_proc()?;
1668 self.ensure_requested_script_proc();
1669 self.script_needs_pump = true;
1670 }
1671 if wait_poll_needed && !self.vm.is_blocked() {
1672 self.script_needs_pump = true;
1673 }
1674 self.ensure_requested_script_proc();
1675 let render_suppressed = self.suppress_render_once;
1676 self.suppress_render_once = false;
1677 if env_is_set!("SG_PROC_FLOW_TRACE") {
1678 eprintln!(
1679 "[SG_PROC_FLOW] host redraw render_decision render_suppressed={} scene={:?} line={} flow={:?}",
1680 render_suppressed,
1681 self.vm.current_scene_name(),
1682 self.vm.current_line_no(),
1683 self.flow.stack
1684 );
1685 }
1686 if !render_suppressed {
1687 #[cfg(target_os = "horizon")]
1688 if switch_trace_enabled {
1689 crate::switch_host::report_switch_diagnostic(&format!(
1690 "siglus_switch: host-trace frame={} phase=frame-build-begin\n",
1691 switch_trace_frame
1692 ));
1693 }
1694 #[cfg(target_os = "vita")]
1695 let start = Instant::now();
1696 let frame = self.vm.ctx.render_frame_with_effects();
1697 #[cfg(target_os = "vita")]
1698 crate::render::vita_stats::phase(crate::render::vita_stats::BUILD, start);
1699 #[cfg(target_os = "horizon")]
1700 if switch_trace_enabled {
1701 let (wipe_type, wipe_progress, emote_count) = if let Some(wipe) = &frame.wipe {
1702 let emotes = wipe
1703 .under
1704 .iter()
1705 .chain(&wipe.current)
1706 .chain(&wipe.next)
1707 .chain(&wipe.over)
1708 .filter(|item| item.sprite.emote_render.is_some())
1709 .count();
1710 (wipe.wipe_type, wipe.progress, emotes)
1711 } else {
1712 let emotes = frame
1713 .sprites
1714 .iter()
1715 .filter(|item| item.sprite.emote_render.is_some())
1716 .count();
1717 (-1, -1.0, emotes)
1718 };
1719 let movie = self.vm.ctx.movie.debug_memory_stats();
1720 crate::switch_host::report_switch_diagnostic(&format!(
1721 "siglus_switch: host-trace frame={} phase=frame-build-complete sprites={} wipe-type={} wipe-progress={:.4} emotes={} movie-streams={} movie-frames={} movie-bytes={}\n",
1722 switch_trace_frame,
1723 frame.submitted_sprite_count(),
1724 wipe_type,
1725 wipe_progress,
1726 emote_count,
1727 movie.active_streams,
1728 movie.video_frames,
1729 movie.video_bytes,
1730 ));
1731 crate::switch_host::report_switch_diagnostic(&format!(
1732 "siglus_switch: host-trace frame={} phase=cpu-render-begin\n",
1733 switch_trace_frame
1734 ));
1735 }
1736 self.renderer
1737 .borrow_mut()
1738 .render_frame(&self.vm.ctx.images, &frame)?;
1739 #[cfg(target_os = "horizon")]
1740 if switch_trace_enabled {
1741 crate::switch_host::report_switch_diagnostic(&format!(
1742 "siglus_switch: host-trace frame={} phase=cpu-render-complete\n",
1743 switch_trace_frame
1744 ));
1745 }
1746 }
1747 if self.script_resume_after_redraw {
1748 self.script_resume_after_redraw = false;
1749 self.script_needs_pump = true;
1750 }
1751 self.redraw_count = self.redraw_count.saturating_add(1);
1752 Ok(())
1753 }
1754}
1755
1756fn vm_key_from_platform_code(code: i32) -> Option<VmKey> {
1757 match code {
1758 0x1B => Some(VmKey::Escape),
1759 0x0D => Some(VmKey::Enter),
1760 0x20 => Some(VmKey::Space),
1761 0x08 => Some(VmKey::Backspace),
1762 0x2E => Some(VmKey::Delete),
1763 0x09 => Some(VmKey::Tab),
1764 0x10 => Some(VmKey::Shift),
1765 0x11 => Some(VmKey::Control),
1766 0x5B | 0x5C => Some(VmKey::Meta),
1767 0x12 => Some(VmKey::Alt),
1768 0x24 => Some(VmKey::Home),
1769 0x23 => Some(VmKey::End),
1770 0x25 => Some(VmKey::ArrowLeft),
1771 0x26 => Some(VmKey::ArrowUp),
1772 0x27 => Some(VmKey::ArrowRight),
1773 0x28 => Some(VmKey::ArrowDown),
1774 0x30..=0x39 => Some(VmKey::Digit((code - 0x30) as u8)),
1775 0x41..=0x5A => Some(VmKey::Letter((code as u8 as char).to_ascii_uppercase())),
1776 0x61..=0x7A => Some(VmKey::Letter((code as u8 as char).to_ascii_uppercase())),
1777 0x7B => Some(VmKey::F(12)),
1779 _ => None,
1780 }
1781}
1782
1783pub unsafe fn cstr_opt(ptr: *const c_char) -> Option<String> {
1791 if ptr.is_null() {
1792 return None;
1793 }
1794 let s = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().to_string();
1795 if s.is_empty() { None } else { Some(s) }
1796}
1797
1798pub unsafe fn cstr_required(ptr: *const c_char, what: &str) -> Result<String> {
1806 if ptr.is_null() {
1807 anyhow::bail!("{what} is null");
1808 }
1809 Ok(unsafe { CStr::from_ptr(ptr) }.to_str()?.to_string())
1810}
1811
1812pub fn parse_bool_exit(result: Result<bool>, context: &str) -> i32 {
1813 match result {
1814 Ok(true) => 1,
1815 Ok(false) => 0,
1816 Err(e) => {
1817 log::error!("{context}: {e:?}");
1818 1
1819 }
1820 }
1821}
1822
1823pub fn default_frame_interval_ms(dt_ms: u32) -> u32 {
1824 if dt_ms == 0 { FRAME_INTERVAL_MS } else { dt_ms }
1825}