Skip to main content

siglus_scene_vm/
vm.rs

1//! Scene VM
2
3use anyhow::{anyhow, bail, Result};
4use std::collections::BTreeMap;
5use std::fmt::Write as _;
6
7use crate::elm_code;
8use crate::runtime::globals::{
9    ObjectFrameActionState, PendingButtonAction, PendingButtonActionKind, PendingFrameActionFinish,
10};
11use crate::runtime::{self, constants, CommandContext, RuntimeLoadRequest, RuntimeSaveKind, RuntimeSaveRequest, Value};
12use crate::scene_stream::SceneStream;
13use siglus_assets::scene_pck::{find_scene_pck_in_project, ScenePck, ScenePckDecodeOptions};
14
15const CD_NONE: u8 = constants::cd::NONE;
16const CD_NL: u8 = constants::cd::NL;
17const CD_PUSH: u8 = constants::cd::PUSH;
18const CD_POP: u8 = constants::cd::POP;
19const CD_COPY: u8 = constants::cd::COPY;
20const CD_PROPERTY: u8 = constants::cd::PROPERTY;
21const CD_COPY_ELM: u8 = constants::cd::COPY_ELM;
22const CD_DEC_PROP: u8 = constants::cd::DEC_PROP;
23const CD_ELM_POINT: u8 = constants::cd::ELM_POINT;
24const CD_ARG: u8 = constants::cd::ARG;
25
26const CD_GOTO: u8 = constants::cd::GOTO;
27const CD_GOTO_TRUE: u8 = constants::cd::GOTO_TRUE;
28const CD_GOTO_FALSE: u8 = constants::cd::GOTO_FALSE;
29const CD_GOSUB: u8 = constants::cd::GOSUB;
30const CD_GOSUBSTR: u8 = constants::cd::GOSUBSTR;
31const CD_RETURN: u8 = constants::cd::RETURN;
32const CD_EOF: u8 = constants::cd::EOF;
33
34const CD_ASSIGN: u8 = constants::cd::ASSIGN;
35const CD_OPERATE_1: u8 = constants::cd::OPERATE_1;
36const CD_OPERATE_2: u8 = constants::cd::OPERATE_2;
37
38const CD_COMMAND: u8 = constants::cd::COMMAND;
39const CD_TEXT: u8 = constants::cd::TEXT;
40const CD_NAME: u8 = constants::cd::NAME;
41const CD_SEL_BLOCK_START: u8 = constants::cd::SEL_BLOCK_START;
42const CD_SEL_BLOCK_END: u8 = constants::cd::SEL_BLOCK_END;
43
44const OP_PLUS: u8 = constants::op::PLUS;
45const OP_MINUS: u8 = constants::op::MINUS;
46const OP_MULTIPLE: u8 = constants::op::MULTIPLE;
47const OP_DIVIDE: u8 = constants::op::DIVIDE;
48const OP_AMARI: u8 = constants::op::AMARI;
49
50const OP_EQUAL: u8 = constants::op::EQUAL;
51const OP_NOT_EQUAL: u8 = constants::op::NOT_EQUAL;
52const OP_GREATER: u8 = constants::op::GREATER;
53const OP_GREATER_EQUAL: u8 = constants::op::GREATER_EQUAL;
54const OP_LESS: u8 = constants::op::LESS;
55const OP_LESS_EQUAL: u8 = constants::op::LESS_EQUAL;
56
57const OP_LOGICAL_AND: u8 = constants::op::LOGICAL_AND;
58const OP_LOGICAL_OR: u8 = constants::op::LOGICAL_OR;
59
60const OP_TILDE: u8 = constants::op::TILDE;
61const OP_AND: u8 = constants::op::AND;
62const OP_OR: u8 = constants::op::OR;
63const OP_HAT: u8 = constants::op::HAT;
64const OP_SL: u8 = constants::op::SL;
65const OP_SR: u8 = constants::op::SR;
66const OP_SR3: u8 = constants::op::SR3;
67
68// C++ call local scratch lists cur_call.L / cur_call.K are fixed-size 32-slot lists.
69const CALL_SCRATCH_SIZE: usize = 32;
70
71// -----------------------------------------------------------------------------
72// VM configuration (form codes are game-specific, so keep them injectable)
73// -----------------------------------------------------------------------------
74
75#[derive(Debug, Clone, Copy)]
76pub struct VmConfig {
77    pub fm_void: i32,
78    pub fm_int: i32,
79    pub fm_str: i32,
80    pub fm_label: i32,
81    pub fm_list: i32,
82    pub fm_intlist: i32,
83    pub fm_strlist: i32,
84    pub max_steps: u64,
85}
86
87impl VmConfig {
88    pub fn from_env() -> Self {
89        fn env_u64(key: &str, default: u64) -> u64 {
90            std::env::var(key)
91                .ok()
92                .and_then(|v| v.parse::<u64>().ok())
93                .unwrap_or(default)
94        }
95
96        Self {
97            fm_void: constants::fm::VOID,
98            fm_int: constants::fm::INT,
99            fm_str: constants::fm::STR,
100            fm_label: constants::fm::LABEL,
101            fm_list: constants::fm::LIST,
102            fm_intlist: constants::fm::INTLIST,
103            fm_strlist: constants::fm::STRLIST,
104            max_steps: env_u64("SIGLUS_VM_MAX_STEPS", 0),
105        }
106    }
107}
108
109#[derive(Debug, Clone)]
110struct CallProp {
111    prop_id: i32,
112    form: i32,
113    decl_size: usize,
114    element: Vec<i32>,
115    value: CallPropValue,
116}
117
118#[derive(Debug, Clone)]
119enum CallPropValue {
120    Int(i32),
121    Str(String),
122    Element(Vec<i32>),
123    IntList(Vec<i32>),
124    StrList(Vec<String>),
125}
126
127#[derive(Debug, Clone)]
128struct CallFrame {
129    return_pc: usize,
130    ret_form: i32,
131    return_override: Option<(usize, i32)>,
132    excall_proc: bool,
133    frame_action_proc: bool,
134    arg_cnt: usize,
135    delayed_ret_form: Option<i32>,
136    user_props: Vec<CallProp>,
137    int_args: Vec<i32>,
138    str_args: Vec<String>,
139}
140
141#[derive(Debug, Clone)]
142struct UserPropCell {
143    form: i32,
144    int_value: i32,
145    str_value: String,
146    element: Vec<i32>,
147    int_list: Vec<i32>,
148    str_list: Vec<String>,
149    list_items: Vec<UserPropCell>,
150}
151
152impl UserPropCell {
153    fn new(form: i32, element: Vec<i32>) -> Self {
154        Self {
155            form,
156            int_value: 0,
157            str_value: String::new(),
158            element,
159            int_list: Vec::new(),
160            str_list: Vec::new(),
161            list_items: Vec::new(),
162        }
163    }
164}
165
166#[derive(Debug, Clone)]
167struct SceneExecFrame<'a> {
168    stream: SceneStream<'a>,
169    user_cmd_names: std::collections::HashMap<u32, String>,
170    call_cmd_names: std::collections::HashMap<u32, String>,
171    int_stack: Vec<i32>,
172    str_stack: Vec<String>,
173    element_points: Vec<usize>,
174    call_stack: Vec<CallFrame>,
175    gosub_return_stack: Vec<(usize, i32)>,
176    user_props: BTreeMap<u16, UserPropCell>,
177    current_scene_no: Option<usize>,
178    current_scene_name: Option<String>,
179    current_line_no: i32,
180    ret_form: i32,
181    excall_proc: bool,
182}
183
184
185#[derive(Debug, Clone)]
186struct InterpreterExecState<'a> {
187    stream: SceneStream<'a>,
188    user_cmd_names: std::collections::HashMap<u32, String>,
189    call_cmd_names: std::collections::HashMap<u32, String>,
190    int_stack: Vec<i32>,
191    str_stack: Vec<String>,
192    element_points: Vec<usize>,
193    call_stack: Vec<CallFrame>,
194    gosub_return_stack: Vec<(usize, i32)>,
195    user_props: BTreeMap<u16, UserPropCell>,
196    scene_stack: Vec<SceneExecFrame<'a>>,
197    current_scene_no: Option<usize>,
198    current_scene_name: Option<String>,
199    current_line_no: i32,
200}
201
202#[derive(Debug, Clone)]
203struct RuntimeDiskSnapshot {
204    scene_name: String,
205    scene_no: i32,
206    line_no: i32,
207    pc: i32,
208    int_stack: Vec<i32>,
209    str_stack: Vec<String>,
210    element_points: Vec<usize>,
211    call_stack: Vec<CallFrame>,
212}
213
214fn resize_i64_vec(mut v: Vec<i64>, n: usize) -> Vec<i64> {
215    v.resize(n, 0);
216    v
217}
218
219fn resize_string_vec(mut v: Vec<String>, n: usize) -> Vec<String> {
220    v.resize_with(n, String::new);
221    v
222}
223
224#[derive(Clone)]
225struct VmResumePoint<'a> {
226    stream: SceneStream<'a>,
227    user_cmd_names: std::collections::HashMap<u32, String>,
228    call_cmd_names: std::collections::HashMap<u32, String>,
229    int_stack: Vec<i32>,
230    str_stack: Vec<String>,
231    element_points: Vec<usize>,
232    call_stack: Vec<CallFrame>,
233    gosub_return_stack: Vec<(usize, i32)>,
234    user_props: BTreeMap<u16, UserPropCell>,
235    current_scene_no: Option<usize>,
236    current_scene_name: Option<String>,
237    current_line_no: i32,
238    globals: runtime::globals::GlobalState,
239}
240
241pub struct SceneVm<'a> {
242    pub cfg: VmConfig,
243    stream: SceneStream<'a>,
244
245    pub ctx: CommandContext,
246
247    // Stack model: separate int/str stacks plus element point list.
248    int_stack: Vec<i32>,
249    str_stack: Vec<String>,
250    element_points: Vec<usize>,
251
252    call_stack: Vec<CallFrame>,
253    gosub_return_stack: Vec<(usize, i32)>,
254    user_props: BTreeMap<u16, UserPropCell>,
255    scene_stack: Vec<SceneExecFrame<'a>>,
256    save_point: Option<VmResumePoint<'a>>,
257    sel_point_stack: Vec<VmResumePoint<'a>>,
258    current_scene_no: Option<usize>,
259    current_scene_name: Option<String>,
260    current_line_no: i32,
261
262    pub unknown_opcodes: BTreeMap<u8, u64>,
263    pub unknown_forms: BTreeMap<i32, u64>,
264
265    steps: u64,
266    halted: bool,
267
268    // When a command triggers a VM wait (movie wait-key etc.), its return value is produced when the wait completes.
269    delayed_ret_form: Option<i32>,
270    script_input_synced_this_frame: bool,
271    yield_safe_after_step: bool,
272
273    user_cmd_names: std::collections::HashMap<u32, String>,
274    call_cmd_names: std::collections::HashMap<u32, String>,
275
276    // C++ keeps the lexer / scene package resident. Do not reload and rebuild
277    // Scene.pck for frame-action callbacks or scene-local user command calls.
278    scene_pck_cache: Option<ScenePck>,
279    scene_stream_cache: BTreeMap<usize, SceneStream<'a>>,
280}
281
282#[derive(Debug, Clone)]
283struct FrameActionWork {
284    stage_idx: i64,
285    obj_idx: usize,
286    ch_idx: Option<usize>,
287    global_form_id: Option<u32>,
288    object_chain: Option<Vec<i32>>,
289    frame_action_chain: Option<Vec<i32>>,
290    scn_name: String,
291    cmd_name: String,
292    args: Vec<Value>,
293    count: i64,
294    end_time: i64,
295}
296
297impl<'a> SceneVm<'a> {
298    fn trace_unknown_form(&mut self, form_code: i32, site: &str) {
299        *self.unknown_forms.entry(form_code).or_insert(0) += 1;
300        if std::env::var_os("SIGLUS_TRACE_UNKNOWN_FORMS").is_some() {
301            eprintln!(
302                "[vm unknown form] site={} form={} pc=0x{:x}",
303                site,
304                form_code,
305                self.stream.get_prg_cntr()
306            );
307        }
308    }
309
310    fn blank_call_int_args() -> Vec<i32> {
311        vec![0; CALL_SCRATCH_SIZE]
312    }
313
314    fn blank_call_str_args() -> Vec<String> {
315        vec![String::new(); CALL_SCRATCH_SIZE]
316    }
317
318    fn make_call_frame(
319        &self,
320        ret_form: i32,
321        excall_proc: bool,
322        frame_action_proc: bool,
323        arg_cnt: usize,
324        scratch_args: Option<(Vec<i32>, Vec<String>)>,
325    ) -> CallFrame {
326        let (int_args, str_args) = scratch_args
327            .unwrap_or_else(|| (Self::blank_call_int_args(), Self::blank_call_str_args()));
328        CallFrame {
329            return_pc: 0,
330            ret_form,
331            return_override: None,
332            excall_proc,
333            frame_action_proc,
334            arg_cnt,
335            delayed_ret_form: None,
336            user_props: Vec::new(),
337            int_args,
338            str_args,
339        }
340    }
341
342    fn shared_user_prop_count(&self) -> usize {
343        self.scene_pck_cache
344            .as_ref()
345            .map(|pck| pck.inc_props.len())
346            .unwrap_or_else(|| self.stream.header.scn_prop_cnt.max(0) as usize)
347    }
348
349    fn enter_cross_scene_user_prop_scope(&mut self) -> BTreeMap<u16, UserPropCell> {
350        let saved_user_props = std::mem::take(&mut self.user_props);
351        let shared_count = self.shared_user_prop_count();
352        self.user_props = saved_user_props
353            .iter()
354            .filter_map(|(&prop_id, cell)| {
355                if (prop_id as usize) < shared_count {
356                    Some((prop_id, cell.clone()))
357                } else {
358                    None
359                }
360            })
361            .collect();
362        saved_user_props
363    }
364
365    fn restore_cross_scene_user_prop_scope(
366        &mut self,
367        mut saved_user_props: BTreeMap<u16, UserPropCell>,
368    ) {
369        let shared_count = self.shared_user_prop_count();
370        for prop_id in 0..shared_count {
371            saved_user_props.remove(&(prop_id as u16));
372        }
373        for (&prop_id, cell) in self.user_props.iter() {
374            if (prop_id as usize) < shared_count {
375                saved_user_props.insert(prop_id, cell.clone());
376            }
377        }
378        self.user_props = saved_user_props;
379    }
380
381    fn capture_interpreter_exec_state(&self) -> InterpreterExecState<'a> {
382        InterpreterExecState {
383            stream: self.stream.clone(),
384            user_cmd_names: self.user_cmd_names.clone(),
385            call_cmd_names: self.call_cmd_names.clone(),
386            int_stack: self.int_stack.clone(),
387            str_stack: self.str_stack.clone(),
388            element_points: self.element_points.clone(),
389            call_stack: self.call_stack.clone(),
390            gosub_return_stack: self.gosub_return_stack.clone(),
391            user_props: self.user_props.clone(),
392            scene_stack: self.scene_stack.clone(),
393            current_scene_no: self.current_scene_no,
394            current_scene_name: self.current_scene_name.clone(),
395            current_line_no: self.current_line_no,
396        }
397    }
398
399    fn restore_interpreter_exec_state(&mut self, saved: InterpreterExecState<'a>) {
400        self.stream = saved.stream;
401        self.user_cmd_names = saved.user_cmd_names;
402        self.call_cmd_names = saved.call_cmd_names;
403        self.int_stack = saved.int_stack;
404        self.str_stack = saved.str_stack;
405        self.element_points = saved.element_points;
406        self.call_stack = saved.call_stack;
407        self.gosub_return_stack = saved.gosub_return_stack;
408        self.user_props = saved.user_props;
409        self.scene_stack = saved.scene_stack;
410        self.current_scene_no = saved.current_scene_no;
411        self.current_scene_name = saved.current_scene_name;
412        self.current_line_no = saved.current_line_no;
413        self.ctx.current_scene_no = self.current_scene_no.map(|v| v as i64);
414        self.ctx.current_scene_name = self.current_scene_name.clone();
415        self.ctx.current_line_no = self.current_line_no as i64;
416    }
417
418    pub fn new(stream: SceneStream<'a>, ctx: CommandContext) -> Self {
419        let cfg = VmConfig::from_env();
420        let user_cmd_names = stream.scn_cmd_name_map.clone();
421        let base_call = CallFrame {
422            return_pc: 0,
423            ret_form: cfg.fm_void,
424            return_override: None,
425            excall_proc: false,
426            frame_action_proc: false,
427            arg_cnt: 0,
428            delayed_ret_form: None,
429            user_props: Vec::new(),
430            int_args: Self::blank_call_int_args(),
431            str_args: Self::blank_call_str_args(),
432        };
433        Self {
434            cfg,
435            stream,
436            ctx,
437            int_stack: Vec::new(),
438            str_stack: Vec::new(),
439            element_points: Vec::new(),
440            call_stack: vec![base_call],
441            gosub_return_stack: Vec::new(),
442            user_props: BTreeMap::new(),
443            scene_stack: Vec::new(),
444            save_point: None,
445            sel_point_stack: Vec::new(),
446            current_scene_no: None,
447            current_scene_name: None,
448            current_line_no: -1,
449            unknown_opcodes: BTreeMap::new(),
450            unknown_forms: BTreeMap::new(),
451
452            steps: 0,
453            halted: false,
454            delayed_ret_form: None,
455            script_input_synced_this_frame: false,
456            yield_safe_after_step: false,
457            user_cmd_names,
458            call_cmd_names: std::collections::HashMap::new(),
459            scene_pck_cache: None,
460            scene_stream_cache: BTreeMap::new(),
461        }
462    }
463
464    pub fn with_config(cfg: VmConfig, stream: SceneStream<'a>, ctx: CommandContext) -> Self {
465        let user_cmd_names = stream.scn_cmd_name_map.clone();
466        let base_call = CallFrame {
467            return_pc: 0,
468            ret_form: cfg.fm_void,
469            return_override: None,
470            excall_proc: false,
471            frame_action_proc: false,
472            arg_cnt: 0,
473            delayed_ret_form: None,
474            user_props: Vec::new(),
475            int_args: Self::blank_call_int_args(),
476            str_args: Self::blank_call_str_args(),
477        };
478        Self {
479            cfg,
480            stream,
481            ctx,
482            int_stack: Vec::new(),
483            str_stack: Vec::new(),
484            element_points: Vec::new(),
485            call_stack: vec![base_call],
486            gosub_return_stack: Vec::new(),
487            user_props: BTreeMap::new(),
488            scene_stack: Vec::new(),
489            save_point: None,
490            sel_point_stack: Vec::new(),
491            current_scene_no: None,
492            current_scene_name: None,
493            current_line_no: -1,
494            unknown_opcodes: BTreeMap::new(),
495            unknown_forms: BTreeMap::new(),
496
497            steps: 0,
498            halted: false,
499            delayed_ret_form: None,
500            script_input_synced_this_frame: false,
501            yield_safe_after_step: false,
502            user_cmd_names,
503            call_cmd_names: std::collections::HashMap::new(),
504            scene_pck_cache: None,
505            scene_stream_cache: BTreeMap::new(),
506        }
507    }
508
509    pub fn is_blocked(&mut self) -> bool {
510        self.ctx.wait_poll()
511    }
512
513    pub fn is_halted(&self) -> bool {
514        self.halted
515    }
516
517    pub fn proc_generation(&self) -> u64 {
518        self.ctx.proc_generation()
519    }
520
521    pub fn last_proc_kind(&self) -> runtime::ProcKind {
522        self.ctx.last_proc_kind()
523    }
524
525    pub fn current_scene_name(&self) -> Option<&str> {
526        self.current_scene_name.as_deref()
527    }
528
529    pub fn current_line_no(&self) -> i32 {
530        self.current_line_no
531    }
532
533    pub fn current_scene_no(&self) -> Option<usize> {
534        self.current_scene_no
535    }
536
537    pub fn take_runtime_load_completed(&mut self) -> bool {
538        self.ctx.take_runtime_load_completed()
539    }
540
541    pub fn call_syscom_configured_scene(&mut self, key: &str) -> Result<bool> {
542        // Match the original C++ Gp_ini fields: SAVE_SCENE, LOAD_SCENE and
543        // CONFIG_SCENE store both scene name and z label number.  GameexeConfig
544        // get_unquoted() returns only the first item, so using it here loses the
545        // z value and incorrectly calls sys10_sc00,0.  Rewrite's Gameexe has:
546        //   #SAVE_SCENE   = "sys10_sc00",02
547        //   #LOAD_SCENE   = "sys10_sc00",03
548        //   #CONFIG_SCENE = "sys10_sc00",04
549        // The original calls tnm_scene_proc_farcall(name, z, FM_VOID, true, false).
550        let entry = self
551            .ctx
552            .tables
553            .gameexe
554            .as_ref()
555            .and_then(|cfg| cfg.get_entry(key).or_else(|| cfg.get_entry(&format!("#{key}"))));
556        let Some(entry) = entry else {
557            if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
558                eprintln!(
559                    "[SG_PROC_FLOW] syscom_config_scene key={} raw=<missing> scene={:?} line={} pending_proc={:?}",
560                    key,
561                    self.current_scene_name.as_deref(),
562                    self.current_line_no,
563                    self.ctx.globals.syscom.pending_proc
564                );
565            }
566            return Ok(false);
567        };
568
569        let scene_name = entry
570            .item_unquoted(0)
571            .map(|s| s.trim().trim_matches('\"').trim().to_string())
572            .unwrap_or_default();
573        let z_no = entry
574            .item_unquoted(1)
575            .and_then(|s| s.trim().parse::<i32>().ok())
576            .unwrap_or(0);
577        let raw = format!("{scene_name},{z_no}");
578
579        if scene_name.is_empty() {
580            if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
581                eprintln!(
582                    "[SG_PROC_FLOW] syscom_config_scene key={} raw={:?} target=<empty> scene={:?} line={}",
583                    key,
584                    raw,
585                    self.current_scene_name.as_deref(),
586                    self.current_line_no
587                );
588            }
589            return Ok(false);
590        }
591
592        if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
593            eprintln!(
594                "[SG_PROC_FLOW] syscom_config_scene key={} raw={:?} target={} z={} before_scene={:?} line={} scene_stack={} call_depth={}",
595                key,
596                raw,
597                scene_name,
598                z_no,
599                self.current_scene_name.as_deref(),
600                self.current_line_no,
601                self.scene_stack.len(),
602                self.call_stack.len()
603            );
604        }
605        self.farcall_scene_name_ex(&scene_name, z_no, self.cfg.fm_void, true, &[])?;
606        if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
607            eprintln!(
608                "[SG_PROC_FLOW] syscom_config_scene entered key={} now_scene={:?} line={} scene_stack={} call_depth={}",
609                key,
610                self.current_scene_name.as_deref(),
611                self.current_line_no,
612                self.scene_stack.len(),
613                self.call_stack.len()
614            );
615        }
616        Ok(true)
617    }
618
619    fn vm_trace_matches(&self) -> bool {
620        if std::env::var_os("SIGLUS_TRACE_VM").is_none() {
621            return false;
622        }
623        if let Ok(filter) = std::env::var("SIGLUS_TRACE_VM_SCENE") {
624            if !filter.is_empty() && self.current_scene_name.as_deref() != Some(filter.as_str()) {
625                return false;
626            }
627        }
628        if let Ok(range) = std::env::var("SIGLUS_TRACE_VM_PC") {
629            if let Some((start, end)) = range.split_once("..") {
630                let parse = |s: &str| {
631                    usize::from_str_radix(s.trim_start_matches("0x"), 16)
632                        .or_else(|_| s.parse::<usize>())
633                };
634                if let (Ok(start), Ok(end)) = (parse(start), parse(end)) {
635                    let pc = self.stream.get_prg_cntr();
636                    if pc < start || pc > end {
637                        return false;
638                    }
639                }
640            }
641        }
642        true
643    }
644
645    fn vm_trace_stack_summary(&self) -> String {
646        let mut out = String::new();
647        let int_tail_start = self.int_stack.len().saturating_sub(8);
648        let int_tail = &self.int_stack[int_tail_start..];
649        let _ = write!(
650            &mut out,
651            "call_depth={} int_len={} str_len={} elm_points={:?} int_tail={:?}",
652            self.call_stack.len(),
653            self.int_stack.len(),
654            self.str_stack.len(),
655            self.element_points,
656            int_tail
657        );
658        if let Some(last) = self.str_stack.last() {
659            let preview = if last.chars().count() > 48 {
660                let mut tmp = last.chars().take(48).collect::<String>();
661                tmp.push('…');
662                tmp
663            } else {
664                last.clone()
665            };
666            let _ = write!(&mut out, " str_top={:?}", preview);
667        }
668        out
669    }
670
671    fn vm_trace(&self, pc: Option<usize>, msg: impl AsRef<str>) {
672        if !self.vm_trace_matches() {
673            return;
674        }
675        let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
676        let scene_no = self
677            .current_scene_no
678            .map(|v| v.to_string())
679            .unwrap_or_else(|| "-".to_string());
680        let pc_text = pc
681            .map(|v| format!("0x{v:x}"))
682            .unwrap_or_else(|| "-".to_string());
683        eprintln!(
684            "[SG_VM_TRACE] scene={} scene_no={} line={} pc={} {} | {}",
685            scene,
686            scene_no,
687            self.current_line_no,
688            pc_text,
689            msg.as_ref(),
690            self.vm_trace_stack_summary()
691        );
692    }
693
694    fn vm_opcode_name(opcode: u8) -> &'static str {
695        match opcode {
696            CD_NONE => "NONE",
697            CD_NL => "NL",
698            CD_PUSH => "PUSH",
699            CD_POP => "POP",
700            CD_COPY => "COPY",
701            CD_PROPERTY => "PROPERTY",
702            CD_COPY_ELM => "COPY_ELM",
703            CD_DEC_PROP => "DEC_PROP",
704            CD_ELM_POINT => "ELM_POINT",
705            CD_ARG => "ARG",
706            CD_GOTO => "GOTO",
707            CD_GOTO_TRUE => "GOTO_TRUE",
708            CD_GOTO_FALSE => "GOTO_FALSE",
709            CD_GOSUB => "GOSUB",
710            CD_GOSUBSTR => "GOSUBSTR",
711            CD_RETURN => "RETURN",
712            CD_EOF => "EOF",
713            CD_ASSIGN => "ASSIGN",
714            CD_OPERATE_1 => "OPERATE_1",
715            CD_OPERATE_2 => "OPERATE_2",
716            CD_COMMAND => "COMMAND",
717            CD_TEXT => "TEXT",
718            CD_NAME => "NAME",
719            CD_SEL_BLOCK_START => "SEL_BLOCK_START",
720            CD_SEL_BLOCK_END => "SEL_BLOCK_END",
721            _ => "UNKNOWN",
722        }
723    }
724
725    fn vm_trace_opcode(&self, pc: usize, opcode: u8, phase: &str) {
726        if !self.vm_trace_matches() {
727            return;
728        }
729        self.vm_trace(
730            Some(pc),
731            format!(
732                "{} opcode={}({:#04x})",
733                phase,
734                Self::vm_opcode_name(opcode),
735                opcode
736            ),
737        );
738    }
739    fn sg_debug_enabled() -> bool {
740        std::env::var_os("SG_DEBUG").is_some()
741    }
742
743    fn sg_cgm_coord_trace(&self, msg: impl AsRef<str>) {
744        if !Self::sg_debug_enabled() {
745            return;
746        }
747        let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
748        let scene_no = self
749            .current_scene_no
750            .map(|v| v.to_string())
751            .unwrap_or_else(|| "-".to_string());
752        eprintln!(
753            "[SG_DEBUG][CGM_COORD_TRACE][VM] scene={} scene_no={} line={} pc=0x{:x} {}",
754            scene,
755            scene_no,
756            self.current_line_no,
757            self.stream.get_prg_cntr(),
758            msg.as_ref()
759        );
760    }
761
762    fn trace_cgm_coord_assign(&self, elm: &[i32], rhs: &Value) {
763        if !Self::sg_debug_enabled() || elm.len() < 3 {
764            return;
765        }
766        let array_op = if self.ctx.ids.elm_array != 0 {
767            self.ctx.ids.elm_array
768        } else {
769            crate::runtime::forms::codes::ELM_ARRAY
770        };
771        if elm[1] != array_op {
772            return;
773        }
774        let head = elm[0] as u32;
775        let idx = elm[2];
776        if head == crate::runtime::forms::codes::elm_value::GLOBAL_B as u32 {
777            let interesting = (100..=129).contains(&idx)
778                || (140..=169).contains(&idx)
779                || (180..=209).contains(&idx);
780            if interesting {
781                self.sg_cgm_coord_trace(format!("global B[{}] <- {:?}", idx, rhs));
782            }
783        } else if head == crate::runtime::forms::codes::elm_value::GLOBAL_S as u32
784            && (1120..=1139).contains(&idx)
785        {
786            self.sg_cgm_coord_trace(format!("global S[{}] <- {:?}", idx, rhs));
787        }
788    }
789
790
791    fn cf_branch_trace_interesting_line(&self) -> bool {
792        if self.current_scene_name.as_deref() != Some("sys10_cf01") {
793            return false;
794        }
795        matches!(self.current_line_no, 700..=730 | 870..=895)
796    }
797
798    fn cf_condition_trace_interesting_line(&self) -> bool {
799        if !Self::sg_debug_enabled() {
800            return false;
801        }
802        matches!(
803            self.current_scene_name.as_deref(),
804            Some("sys10_cf01")
805        ) && matches!(self.current_line_no, 700..=730 | 870..=895)
806    }
807
808    fn cf_condition_trace_prop_name(prop_id: u16) -> Option<&'static str> {
809        match prop_id {
810            14 => Some("ip_mx"),
811            15 => Some("ip_my"),
812            16 => Some("ip_wheel"),
813            18 => Some("ip_bl_is"),
814            19 => Some("ip_br_is"),
815            20 => Some("ip_bl_on"),
816            21 => Some("ip_br_on"),
817            22 => Some("ip_key_enable_enter"),
818            23 => Some("ip_key_enable_esc"),
819            24 => Some("ip_key_is_enter"),
820            25 => Some("ip_key_is_esc"),
821            26 => Some("ip_key_on_enter"),
822            27 => Some("ip_key_on_esc"),
823            39 => Some("cntr_now"),
824            40 => Some("cntr_exit"),
825            41 => Some("skip_flag"),
826            _ => None,
827        }
828    }
829
830    fn cf_condition_trace_value_summary(&self, cell: &UserPropCell, array_idx: Option<usize>) -> String {
831        if let Some(idx) = array_idx {
832            if cell.form == self.cfg.fm_intlist {
833                return format!("intlist[{}]={}", idx, cell.int_list.get(idx).copied().unwrap_or(0));
834            }
835            if cell.form == self.cfg.fm_strlist {
836                return format!("strlist[{}]={:?}", idx, cell.str_list.get(idx).cloned().unwrap_or_default());
837            }
838            if let Some(slot) = cell.list_items.get(idx) {
839                return format!("list[{}] form={} int={} str={:?} int_list_len={} str_list_len={} items={}",
840                    idx,
841                    slot.form,
842                    slot.int_value,
843                    slot.str_value,
844                    slot.int_list.len(),
845                    slot.str_list.len(),
846                    slot.list_items.len()
847                );
848            }
849            return format!("array[{}] <missing> form={} int_list_len={} str_list_len={} items={}",
850                idx, cell.form, cell.int_list.len(), cell.str_list.len(), cell.list_items.len());
851        }
852        if cell.form == self.cfg.fm_int {
853            return format!("int={}", cell.int_value);
854        }
855        if cell.form == self.cfg.fm_str {
856            return format!("str={:?}", cell.str_value);
857        }
858        if cell.form == self.cfg.fm_intlist {
859            let preview = cell.int_list.iter().take(20).copied().collect::<Vec<_>>();
860            return format!("intlist len={} head={:?}", cell.int_list.len(), preview);
861        }
862        if cell.form == self.cfg.fm_strlist {
863            let preview = cell.str_list.iter().take(6).cloned().collect::<Vec<_>>();
864            return format!("strlist len={} head={:?}", cell.str_list.len(), preview);
865        }
866        format!("form={} int={} str={:?} int_list_len={} str_list_len={} items={}",
867            cell.form, cell.int_value, cell.str_value, cell.int_list.len(), cell.str_list.len(), cell.list_items.len())
868    }
869
870    fn sg_cf_condition_trace(&self, pc: usize, msg: impl AsRef<str>) {
871        if !Self::sg_debug_enabled() {
872            return;
873        }
874        let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
875        let scene_no = self
876            .current_scene_no
877            .map(|v| v.to_string())
878            .unwrap_or_else(|| "-".to_string());
879        let int_tail_start = self.int_stack.len().saturating_sub(12);
880        let int_tail = &self.int_stack[int_tail_start..];
881        eprintln!(
882            "[SG_DEBUG][CF_CONDITION_TRACE] scene={} scene_no={} line={} pc=0x{:x} {} | int_tail={:?}",
883            scene,
884            scene_no,
885            self.current_line_no,
886            pc,
887            msg.as_ref(),
888            int_tail
889        );
890    }
891
892    fn trace_cf_condition_user_prop_read(&self, pc: usize, prop_id: u16, array_idx: Option<usize>, cell: &UserPropCell, elm: &[i32]) {
893        if !self.cf_condition_trace_interesting_line() {
894            return;
895        }
896        let Some(name) = Self::cf_condition_trace_prop_name(prop_id) else {
897            return;
898        };
899        self.sg_cf_condition_trace(
900            pc,
901            format!(
902                "kind=USER_PROP_READ prop={}({}) array={:?} value={} elm={:?}",
903                prop_id,
904                name,
905                array_idx,
906                self.cf_condition_trace_value_summary(cell, array_idx),
907                elm
908            ),
909        );
910    }
911
912    fn trace_cf_condition_user_prop_assign(&self, pc: usize, prop_id: u16, array_idx: Option<usize>, old: Option<&UserPropCell>, new: Option<&UserPropCell>, rhs: &Value, elm: &[i32]) {
913        if !self.cf_condition_trace_interesting_line() {
914            return;
915        }
916        let Some(name) = Self::cf_condition_trace_prop_name(prop_id) else {
917            return;
918        };
919        let old_summary = old
920            .map(|cell| self.cf_condition_trace_value_summary(cell, array_idx))
921            .unwrap_or_else(|| "<default/missing>".to_string());
922        let new_summary = new
923            .map(|cell| self.cf_condition_trace_value_summary(cell, array_idx))
924            .unwrap_or_else(|| "<missing>".to_string());
925        self.sg_cf_condition_trace(
926            pc,
927            format!(
928                "kind=USER_PROP_ASSIGN prop={}({}) array={:?} old={} new={} rhs={:?} elm={:?}",
929                prop_id,
930                name,
931                array_idx,
932                old_summary,
933                new_summary,
934                rhs,
935                elm
936            ),
937        );
938    }
939
940    fn cf_condition_op_name(opr: u8) -> &'static str {
941        match opr {
942            OP_PLUS => "+",
943            OP_MINUS => "-",
944            OP_MULTIPLE => "*",
945            OP_DIVIDE => "/",
946            OP_AMARI => "%",
947            OP_EQUAL => "==",
948            OP_NOT_EQUAL => "!=",
949            OP_GREATER => ">",
950            OP_GREATER_EQUAL => ">=",
951            OP_LESS => "<",
952            OP_LESS_EQUAL => "<=",
953            OP_LOGICAL_AND => "&&",
954            OP_LOGICAL_OR => "||",
955            OP_TILDE => "~",
956            OP_AND => "&",
957            OP_OR => "|",
958            OP_HAT => "^",
959            OP_SL => "<<",
960            OP_SR => ">>",
961            OP_SR3 => ">>>",
962            _ => "?",
963        }
964    }
965
966    fn cf_branch_trace_stack_snapshot(&self) -> String {
967        let int_tail_start = self.int_stack.len().saturating_sub(16);
968        let int_tail = &self.int_stack[int_tail_start..];
969        let str_tail_start = self.str_stack.len().saturating_sub(4);
970        let str_tail = &self.str_stack[str_tail_start..];
971        let (cur_l, cur_s, arg_cnt) = if let Some(frame) = self.call_stack.last() {
972            let l_take = frame.int_args.len().min(16);
973            let s_take = frame.str_args.len().min(6);
974            (
975                format!("{:?}", &frame.int_args[..l_take]),
976                format!("{:?}", &frame.str_args[..s_take]),
977                frame.arg_cnt,
978            )
979        } else {
980            ("[]".to_string(), "[]".to_string(), 0)
981        };
982        format!(
983            "int_len={} int_tail={:?} str_len={} str_tail={:?} elm_points={:?} call_depth={} arg_cnt={} cur_call_l0_15={} cur_call_s0_5={}",
984            self.int_stack.len(),
985            int_tail,
986            self.str_stack.len(),
987            str_tail,
988            self.element_points,
989            self.call_stack.len(),
990            arg_cnt,
991            cur_l,
992            cur_s,
993        )
994    }
995
996    fn sg_cf_branch_trace(&self, pc: usize, msg: impl AsRef<str>) {
997        if !Self::sg_debug_enabled() {
998            return;
999        }
1000        let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
1001        let scene_no = self
1002            .current_scene_no
1003            .map(|v| v.to_string())
1004            .unwrap_or_else(|| "-".to_string());
1005        eprintln!(
1006            "[SG_DEBUG][CF_BRANCH_TRACE] scene={} scene_no={} line={} pc=0x{:x} {} | {}",
1007            scene,
1008            scene_no,
1009            self.current_line_no,
1010            pc,
1011            msg.as_ref(),
1012            self.cf_branch_trace_stack_snapshot(),
1013        );
1014    }
1015
1016    fn trace_cf_branch_goto(
1017        &self,
1018        pc: usize,
1019        opcode_name: &str,
1020        label_no: i32,
1021        cond: i32,
1022        taken: bool,
1023        before_tail: &[i32],
1024    ) {
1025        if self.cf_branch_trace_interesting_line() {
1026            self.sg_cf_branch_trace(
1027                pc,
1028                format!(
1029                    "kind=GOTO opcode={} label={} cond={} taken={} before_int_tail={:?}",
1030                    opcode_name, label_no, cond, taken, before_tail
1031                ),
1032            );
1033        }
1034    }
1035
1036    fn trace_cf_branch_farcall(
1037        &self,
1038        pc: usize,
1039        scene_name: &str,
1040        z_no: i32,
1041        ret_form: i32,
1042        ex_call_proc: bool,
1043        scratch_source_args: &[Value],
1044    ) {
1045        if !(self.current_scene_name.as_deref() == Some("sys10_cf01")
1046            && matches!(self.current_line_no, 700..=730 | 870..=895)
1047            && matches!(scene_name, "sys10_sm00" | "sys10_cf00")
1048            && matches!(z_no, 14 | 15))
1049        {
1050            return;
1051        }
1052        let args_dbg = scratch_source_args
1053            .iter()
1054            .map(|v| format!("{v:?}"))
1055            .collect::<Vec<_>>()
1056            .join(", ");
1057        self.sg_cf_branch_trace(
1058            pc,
1059            format!(
1060                "kind=FARCALL target={} z={} ret_form={} ex_call_proc={} argc={} args=[{}]",
1061                scene_name,
1062                z_no,
1063                ret_form,
1064                ex_call_proc,
1065                scratch_source_args.len(),
1066                args_dbg
1067            ),
1068        );
1069    }
1070
1071    fn sg_omv_trace(&self, msg: impl AsRef<str>) {
1072        if !Self::sg_debug_enabled() {
1073            return;
1074        }
1075        let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
1076        let scene_no = self
1077            .current_scene_no
1078            .map(|v| v.to_string())
1079            .unwrap_or_else(|| "-".to_string());
1080        eprintln!(
1081            "[SG_DEBUG][OMV_TRACE] scene={} scene_no={} line={} pc=0x{:x} {}",
1082            scene,
1083            scene_no,
1084            self.current_line_no,
1085            self.stream.get_prg_cntr(),
1086            msg.as_ref()
1087        );
1088    }
1089
1090    fn sg_omv_trace_command(
1091        &self,
1092        phase: &str,
1093        elm: &[i32],
1094        form_id: i32,
1095        op_id: i32,
1096        al_id: i32,
1097        ret_form: i32,
1098        args: &[Value],
1099    ) {
1100        if !Self::sg_debug_enabled() {
1101            return;
1102        }
1103
1104        let label = if form_id == crate::runtime::forms::codes::elm_value::GLOBAL_JUMP
1105            || (form_id == crate::runtime::forms::codes::FM_GLOBAL
1106                && op_id == crate::runtime::forms::codes::elm_value::GLOBAL_JUMP)
1107        {
1108            Some("GLOBAL.JUMP")
1109        } else if form_id == crate::runtime::forms::codes::elm_value::GLOBAL_FARCALL
1110            || (form_id == crate::runtime::forms::codes::FM_GLOBAL
1111                && op_id == crate::runtime::forms::codes::elm_value::GLOBAL_FARCALL)
1112        {
1113            Some("GLOBAL.FARCALL")
1114        } else if (form_id as u32 == constants::global_form::SYSCOM || form_id == constants::fm::SYSCOM)
1115            && op_id == crate::runtime::forms::codes::elm_value::SYSCOM_CALL_EX
1116        {
1117            Some("SYSCOM.CALL_EX")
1118        } else if form_id as u32 == constants::global_form::MOV || form_id == constants::fm::MOV {
1119            Some("MOV")
1120        } else if form_id == constants::fm::OBJECT
1121            && matches!(
1122                op_id,
1123                crate::runtime::forms::codes::object_op::CREATE_MOVIE
1124                    | crate::runtime::forms::codes::object_op::CREATE_MOVIE_LOOP
1125                    | crate::runtime::forms::codes::object_op::CREATE_MOVIE_WAIT
1126                    | crate::runtime::forms::codes::object_op::CREATE_MOVIE_WAIT_KEY
1127            )
1128        {
1129            Some("OBJECT.CREATE_MOVIE")
1130        } else {
1131            None
1132        };
1133        let Some(label) = label else {
1134            return;
1135        };
1136
1137        let args_dbg = args
1138            .iter()
1139            .take(8)
1140            .map(|v| format!("{v:?}"))
1141            .collect::<Vec<_>>()
1142            .join(", ");
1143        self.sg_omv_trace(format!(
1144            "{} {} form={} op={} al_id={} ret_form={} elm={:?} argc={} args=[{}]",
1145            phase,
1146            label,
1147            form_id,
1148            op_id,
1149            al_id,
1150            ret_form,
1151            elm,
1152            args.len(),
1153            args_dbg
1154        ));
1155    }
1156
1157
1158    fn vm_scn_cmd_context(&self, pc: usize) -> String {
1159        let cnt = self.stream.header.scn_cmd_cnt.max(0) as usize;
1160        let mut prev: Option<(usize, usize)> = None;
1161        let mut next: Option<(usize, usize)> = None;
1162        for cmd_no in 0..cnt {
1163            let Ok(off) = self.stream.scn_cmd_offset(cmd_no) else {
1164                continue;
1165            };
1166            if off <= pc {
1167                prev = Some(match prev {
1168                    Some(cur) if cur.1 > off => cur,
1169                    _ => (cmd_no, off),
1170                });
1171            }
1172            if off > pc {
1173                next = Some(match next {
1174                    Some(cur) if cur.1 < off => cur,
1175                    _ => (cmd_no, off),
1176                });
1177            }
1178        }
1179
1180        let mut out = String::new();
1181        if let Some((cmd_no, off)) = prev {
1182            let name = self.stream.scn_cmd_name_map.get(&(cmd_no as u32)).map(String::as_str).unwrap_or("<unnamed>");
1183            let _ = write!(&mut out, "prev_scn_cmd=#{}:{}@0x{:x} delta={} ", cmd_no, name, off, pc.saturating_sub(off));
1184        } else {
1185            let _ = write!(&mut out, "prev_scn_cmd=<none> " );
1186        }
1187        if let Some((cmd_no, off)) = next {
1188            let name = self.stream.scn_cmd_name_map.get(&(cmd_no as u32)).map(String::as_str).unwrap_or("<unnamed>");
1189            let _ = write!(&mut out, "next_scn_cmd=#{}:{}@0x{:x} distance={}", cmd_no, name, off, off.saturating_sub(pc));
1190        } else {
1191            let _ = write!(&mut out, "next_scn_cmd=<none>" );
1192        }
1193        out
1194    }
1195
1196    pub fn take_script_proc_request(&mut self) -> bool {
1197        let requested = self.ctx.excall_state.script_proc_requested;
1198        self.ctx.excall_state.script_proc_requested = false;
1199        requested
1200    }
1201
1202    pub fn take_script_proc_pop_request(&mut self) -> bool {
1203        let requested = self.ctx.excall_state.script_proc_pop_requested;
1204        self.ctx.excall_state.script_proc_pop_requested = false;
1205        requested
1206    }
1207
1208    fn mark_excall_script_proc_requested(&mut self) {
1209        self.halted = false;
1210        self.ctx.excall_state.ex_call_flag = true;
1211        self.ctx.excall_state.script_proc_requested = true;
1212    }
1213
1214    fn mark_excall_script_proc_pop_requested(&mut self) {
1215        self.ctx.excall_state.ex_call_flag = false;
1216        self.ctx.excall_state.script_proc_pop_requested = true;
1217        self.ctx.input.clear_all();
1218    }
1219
1220    fn push_call_arg_value(&mut self, arg: &Value) {
1221        match arg {
1222            Value::NamedArg { value, .. } => self.push_call_arg_value(value),
1223            Value::Int(n) => self.push_int(*n as i32),
1224            Value::Str(s) => self.push_str(s.clone()),
1225            Value::Element(elm) => self.push_element(elm.clone()),
1226            Value::List(items) => {
1227                for item in items {
1228                    self.push_call_arg_value(item);
1229                }
1230            }
1231        }
1232    }
1233
1234    fn run_user_cmd_inline_at_offset(
1235        &mut self,
1236        cmd_name: &str,
1237        offset: usize,
1238        return_pc: usize,
1239        end_offset: Option<usize>,
1240        _expected_return_pc: Option<usize>,
1241        ret_form: i32,
1242        call_args: &[Value],
1243        frame_action_proc: bool,
1244    ) -> Result<bool> {
1245        let base_depth = self.call_stack.len();
1246        let saved_halted = self.halted;
1247        let saved_scene_no = self.current_scene_no;
1248        let saved_pc = self.stream.get_prg_cntr();
1249        let saved_call_stack = self.call_stack.clone();
1250        let saved_caller_return = self
1251            .call_stack
1252            .last()
1253            .map(|caller| (caller.return_pc, caller.ret_form));
1254        let saved_int_stack = self.int_stack.clone();
1255        let saved_str_stack = self.str_stack.clone();
1256        let saved_element_points = self.element_points.clone();
1257        let saved_gosub_return_stack = self.gosub_return_stack.clone();
1258
1259        if let Some(caller) = self.call_stack.last_mut() {
1260            if std::env::var_os("SIGLUS_TRACE_CALL_RETURN_PC").is_some() {
1261                eprintln!(
1262                    "[SG_CALL_PC] inline set cmd={} depth={} saved_pc=0x{:x} return_pc=0x{:x} old=0x{:x}",
1263                    cmd_name,
1264                    base_depth,
1265                    saved_pc,
1266                    return_pc,
1267                    caller.return_pc
1268                );
1269            }
1270            caller.return_pc = return_pc;
1271            caller.ret_form = ret_form;
1272        }
1273        for arg in call_args {
1274            self.push_call_arg_value(arg);
1275        }
1276        self.call_stack.push(self.make_call_frame(
1277            self.cfg.fm_void,
1278            false,
1279            frame_action_proc,
1280            call_args.len(),
1281            None,
1282        ));
1283        self.stream.set_prg_cntr(offset)?;
1284
1285        if std::env::var_os("SIGLUS_TRACE_FRAME_ACTION_CALL").is_some() {
1286            eprintln!(
1287                "[SG_FRAME_ACTION_CALL] run cmd={} scene={:?} offset=0x{:x} return_pc=0x{:x} args={:?}",
1288                cmd_name,
1289                self.current_scene_no,
1290                offset,
1291                return_pc,
1292                call_args
1293            );
1294        }
1295
1296        let max_steps = std::env::var("SIGLUS_INLINE_USER_CMD_MAX_STEPS")
1297            .ok()
1298            .and_then(|s| s.parse::<u64>().ok())
1299            .unwrap_or(0);
1300        let mut steps: u64 = 0;
1301        let mut run_error = None;
1302        loop {
1303            if let Some(end) = end_offset {
1304                if self.stream.get_prg_cntr() >= end {
1305                    break;
1306                }
1307            }
1308            let wait_generation_before_step = self.ctx.wait.block_generation();
1309            let proc_generation_before_step = self.ctx.proc_generation();
1310            let running = match self.step_inner(false) {
1311                Ok(v) => v,
1312                Err(e) => {
1313                    run_error = Some(e);
1314                    break;
1315                }
1316            };
1317            if self.halted || !running {
1318                break;
1319            }
1320            if self.ctx.proc_generation() != proc_generation_before_step {
1321                break;
1322            }
1323            if self.ctx.wait.block_generation() != wait_generation_before_step && self.ctx.wait_poll() {
1324                break;
1325            }
1326            if self.call_stack.len() == base_depth {
1327                // Inline user commands are isolated script-proc calls.  Once
1328                // their temporary call frame has returned, control belongs
1329                // back to the outer VM even if the inner script's restored PC
1330                // is not the synthetic continuation we installed.  Continuing
1331                // here can execute data bytes after a nested gosub return.
1332                break;
1333            }
1334            steps = steps.saturating_add(1);
1335            if max_steps > 0 && steps >= max_steps {
1336                run_error = Some(anyhow!(
1337                    "inline user command exceeded SIGLUS_INLINE_USER_CMD_MAX_STEPS: cmd={}",
1338                    cmd_name
1339                ));
1340                break;
1341            }
1342        }
1343
1344        let captured_inline_return = if ret_form == self.cfg.fm_int || ret_form == self.cfg.fm_label {
1345            if self.int_stack.len() > saved_int_stack.len() {
1346                self.int_stack.last().copied().map(|v| Value::Int(v as i64))
1347            } else {
1348                None
1349            }
1350        } else if ret_form == self.cfg.fm_str {
1351            if self.str_stack.len() > saved_str_stack.len() {
1352                self.str_stack.last().cloned().map(Value::Str)
1353            } else {
1354                None
1355            }
1356        } else {
1357            None
1358        };
1359
1360        if self.current_scene_no == saved_scene_no {
1361            self.int_stack = saved_int_stack;
1362            self.str_stack = saved_str_stack;
1363            self.element_points = saved_element_points;
1364            self.gosub_return_stack = saved_gosub_return_stack;
1365            self.call_stack = saved_call_stack;
1366            self.halted = saved_halted;
1367            self.stream.set_prg_cntr(saved_pc)?;
1368        }
1369        if let (Some((return_pc, ret_form)), Some(caller)) =
1370            (saved_caller_return, self.call_stack.get_mut(base_depth.saturating_sub(1)))
1371        {
1372            if std::env::var_os("SIGLUS_TRACE_CALL_RETURN_PC").is_some() {
1373                eprintln!(
1374                    "[SG_CALL_PC] inline restore cmd={} depth={} return_pc=0x{:x} old=0x{:x}",
1375                    cmd_name,
1376                    base_depth,
1377                    return_pc,
1378                    caller.return_pc
1379                );
1380            }
1381            caller.return_pc = return_pc;
1382            caller.ret_form = ret_form;
1383        }
1384        if let Some(v) = captured_inline_return {
1385            self.ctx.stack.push(v);
1386        }
1387
1388        if let Some(e) = run_error {
1389            return Err(e);
1390        }
1391
1392        Ok(true)
1393    }
1394
1395    fn ensure_scene_pck_cache(&mut self) -> Result<()> {
1396        if self.scene_pck_cache.is_none() {
1397            #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1398            {
1399                let scene_pck_path = self.ctx.project_dir.join("Scene.pck");
1400                let bytes = crate::resource::read_file_bytes(&scene_pck_path)?;
1401                let exe = ["key.toml", "Key.toml"]
1402                    .iter()
1403                    .find_map(|name| {
1404                        let p = self.ctx.project_dir.join(name);
1405                        if !crate::resource::wasm_path_is_file(&p) {
1406                            return None;
1407                        }
1408                        let text = crate::resource::read_file_to_string(&p).ok()?;
1409                        siglus_assets::key_toml::parse_key_toml(&text)
1410                            .ok()
1411                            .and_then(|cfg| cfg.exe_key16)
1412                            .map(|v| v.to_vec())
1413                    });
1414                let opt = ScenePckDecodeOptions {
1415                    exe_angou_element: exe,
1416                    easy_angou_code: Some(siglus_assets::keys::SCENE_KEY.to_vec()),
1417                };
1418                self.scene_pck_cache = Some(ScenePck::load_and_rebuild_from_bytes(bytes, &opt)?);
1419            }
1420
1421            #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1422            {
1423                let scene_pck_path = find_scene_pck_in_project(&self.ctx.project_dir)?;
1424                let opt = ScenePckDecodeOptions::from_project_dir(&self.ctx.project_dir)?;
1425                self.scene_pck_cache = Some(ScenePck::load_and_rebuild(&scene_pck_path, &opt)?);
1426            }
1427        }
1428        Ok(())
1429    }
1430
1431    fn cached_scene_stream(&mut self, scene_no: usize) -> Result<SceneStream<'a>> {
1432        self.ensure_scene_pck_cache()?;
1433        if !self.scene_stream_cache.contains_key(&scene_no) {
1434            let chunk = {
1435                let pck = self
1436                    .scene_pck_cache
1437                    .as_ref()
1438                    .expect("scene pck cache initialized");
1439                pck.scn_data_slice(scene_no)?.to_vec()
1440            };
1441            let chunk_leaked: &'static [u8] = Box::leak(chunk.into_boxed_slice());
1442            let stream = SceneStream::new(chunk_leaked)?;
1443            self.scene_stream_cache.insert(scene_no, stream);
1444        }
1445        Ok(self
1446            .scene_stream_cache
1447            .get(&scene_no)
1448            .expect("scene stream cached")
1449            .clone())
1450    }
1451
1452    fn run_scene_user_cmd_inline_at_cached_scene_offset(
1453        &mut self,
1454        target_scene_no: usize,
1455        cmd_name: &str,
1456        target_offset: usize,
1457        call_args: &[Value],
1458        ret_form: i32,
1459        preserve_return_pc: bool,
1460        frame_action_proc: bool,
1461    ) -> Result<bool> {
1462        let target_stream = self.cached_scene_stream(target_scene_no)?;
1463        if target_offset > target_stream.scn.len() {
1464            bail!(
1465                "scene_pck: user command offset out of bounds: cmd={} scn_no={} offset=0x{:x} scn_len=0x{:x}",
1466                cmd_name,
1467                target_scene_no,
1468                target_offset,
1469                target_stream.scn.len()
1470            );
1471        }
1472        let (target_call_cmd_names, target_scene_name) = {
1473            let pck = self
1474                .scene_pck_cache
1475                .as_ref()
1476                .expect("scene pck cache initialized");
1477            (
1478                pck.inc_cmd_name_map.clone(),
1479                pck.find_scene_name(target_scene_no).map(ToOwned::to_owned),
1480            )
1481        };
1482
1483        let saved_stream = std::mem::replace(&mut self.stream, target_stream);
1484        let target_user_cmd_names = self.stream.scn_cmd_name_map.clone();
1485        let saved_user_cmd_names =
1486            std::mem::replace(&mut self.user_cmd_names, target_user_cmd_names);
1487        let saved_call_cmd_names =
1488            std::mem::replace(&mut self.call_cmd_names, target_call_cmd_names);
1489        let saved_current_scene_no = self.current_scene_no;
1490        let saved_current_scene_name = self.current_scene_name.clone();
1491        let saved_current_line_no = self.current_line_no;
1492        let saved_ctx_scene_no = self.ctx.current_scene_no;
1493        let saved_ctx_scene_name = self.ctx.current_scene_name.clone();
1494        let saved_ctx_line_no = self.ctx.current_line_no;
1495        let saved_halted = self.halted;
1496        let saved_user_props = self.enter_cross_scene_user_prop_scope();
1497
1498        self.current_scene_no = Some(target_scene_no);
1499        self.current_scene_name = target_scene_name;
1500        self.current_line_no = -1;
1501        self.ctx.current_scene_no = Some(target_scene_no as i64);
1502        self.ctx.current_scene_name = self.current_scene_name.clone();
1503        self.ctx.current_line_no = -1;
1504
1505        let target_return_pc = if preserve_return_pc {
1506            saved_stream.get_prg_cntr()
1507        } else {
1508            self.stream.scn.len()
1509        };
1510        let result = self.run_user_cmd_inline_at_offset(
1511            cmd_name,
1512            target_offset,
1513            target_return_pc,
1514            None,
1515            None,
1516            ret_form,
1517            call_args,
1518            frame_action_proc,
1519        );
1520
1521        self.stream = saved_stream;
1522        self.user_cmd_names = saved_user_cmd_names;
1523        self.call_cmd_names = saved_call_cmd_names;
1524        self.current_scene_no = saved_current_scene_no;
1525        self.current_scene_name = saved_current_scene_name;
1526        self.current_line_no = saved_current_line_no;
1527        self.ctx.current_scene_no = saved_ctx_scene_no;
1528        self.ctx.current_scene_name = saved_ctx_scene_name;
1529        self.ctx.current_line_no = saved_ctx_line_no;
1530        self.halted = saved_halted;
1531        self.restore_cross_scene_user_prop_scope(saved_user_props);
1532        result
1533    }
1534
1535    fn run_scene_user_cmd_inline(
1536        &mut self,
1537        scn_name: Option<&str>,
1538        cmd_name: &str,
1539        call_args: &[Value],
1540        ret_form: i32,
1541        frame_action_proc: bool,
1542    ) -> Result<bool> {
1543        if frame_action_proc {
1544            return self.run_scene_user_cmd_frame_action_proc(scn_name, cmd_name, call_args);
1545        }
1546
1547        let current_scene_no = self.current_scene_no;
1548        let is_current_scene = match scn_name {
1549            None => true,
1550            Some(name) if name.is_empty() => true,
1551            Some(name) => self
1552                .current_scene_name
1553                .as_deref()
1554                .map(|cur| cur.eq_ignore_ascii_case(name))
1555                .unwrap_or(false),
1556        };
1557
1558        // Original C_elm_frame_action::restruct resolves m_scn_no/m_cmd_no
1559        // against the loaded lexer.  The per-frame action path must not reload
1560        // and rebuild Scene.pck every frame.
1561        if is_current_scene {
1562            let Some(_target_scene_no) = current_scene_no else {
1563                return Ok(false);
1564            };
1565            let cmd_no = match self.user_cmd_names.iter().find_map(|(no, name)| {
1566                if name.eq_ignore_ascii_case(cmd_name) {
1567                    Some(*no as usize)
1568                } else {
1569                    None
1570                }
1571            }) {
1572                Some(v) => v,
1573                None => {
1574                    if std::env::var_os("SIGLUS_TRACE_FRAME_ACTION_CALL").is_some() {
1575                        eprintln!(
1576                            "[SG_FRAME_ACTION_CALL] current-scene user command not found: cmd={} scene={:?} scn_name={:?}",
1577                            cmd_name,
1578                            self.current_scene_no,
1579                            scn_name
1580                        );
1581                    }
1582                    return Ok(false);
1583                }
1584            };
1585            let offset = self.stream.scn_cmd_offset(cmd_no)?;
1586            let return_pc = self.stream.get_prg_cntr();
1587            return self.run_user_cmd_inline_at_offset(
1588                cmd_name,
1589                offset,
1590                return_pc,
1591                None,
1592                Some(return_pc),
1593                ret_form,
1594                call_args,
1595                frame_action_proc,
1596            );
1597        }
1598
1599        let Some(name) = scn_name.filter(|name| !name.is_empty()) else {
1600            return Ok(false);
1601        };
1602        self.ensure_scene_pck_cache()?;
1603        let Some(target_scene_no) = self
1604            .scene_pck_cache
1605            .as_ref()
1606            .expect("scene pck cache initialized")
1607            .find_scene_no(name)
1608        else {
1609            if std::env::var_os("SIGLUS_TRACE_FRAME_ACTION_CALL").is_some() {
1610                eprintln!(
1611                    "[SG_FRAME_ACTION_CALL] target scene not found: scn_name={} cmd={}",
1612                    name, cmd_name
1613                );
1614            }
1615            return Ok(false);
1616        };
1617
1618        let target_stream = self.cached_scene_stream(target_scene_no)?;
1619        let cmd_no = match target_stream
1620            .scn_cmd_name_map
1621            .iter()
1622            .find_map(|(no, name)| {
1623                if name.eq_ignore_ascii_case(cmd_name) {
1624                    Some(*no as usize)
1625                } else {
1626                    None
1627                }
1628            }) {
1629            Some(v) => v,
1630            None => {
1631                if std::env::var_os("SIGLUS_TRACE_FRAME_ACTION_CALL").is_some() {
1632                    eprintln!(
1633                        "[SG_FRAME_ACTION_CALL] user command not found: cmd={} target_scene={} scn_name={:?}",
1634                        cmd_name,
1635                        target_scene_no,
1636                        scn_name
1637                    );
1638                }
1639                return Ok(false);
1640            }
1641        };
1642        let offset = target_stream.scn_cmd_offset(cmd_no)?;
1643        self.run_scene_user_cmd_inline_at_cached_scene_offset(
1644            target_scene_no,
1645            cmd_name,
1646            offset,
1647            call_args,
1648            ret_form,
1649            false,
1650            frame_action_proc,
1651        )
1652    }
1653
1654    fn run_scene_user_cmd_frame_action_proc(
1655        &mut self,
1656        scn_name: Option<&str>,
1657        cmd_name: &str,
1658        call_args: &[Value],
1659    ) -> Result<bool> {
1660        let saved_exec = self.capture_interpreter_exec_state();
1661        let saved_scene_no = self.current_scene_no;
1662        let saved_scene_stack_len = self.scene_stack.len();
1663        let saved_call_depth = self.call_stack.len();
1664
1665        let current_scene_no = self.current_scene_no;
1666        let is_current_scene = match scn_name {
1667            None => true,
1668            Some(name) if name.is_empty() => true,
1669            Some(name) => self
1670                .current_scene_name
1671                .as_deref()
1672                .map(|cur| cur.eq_ignore_ascii_case(name))
1673                .unwrap_or(false),
1674        };
1675
1676        if is_current_scene {
1677            let Some(_) = current_scene_no else {
1678                return Ok(false);
1679            };
1680            let Some(cmd_no) = self.user_cmd_names.iter().find_map(|(no, name)| {
1681                if name.eq_ignore_ascii_case(cmd_name) {
1682                    Some(*no as usize)
1683                } else {
1684                    None
1685                }
1686            }) else {
1687                if std::env::var_os("SIGLUS_TRACE_FRAME_ACTION_CALL").is_some() {
1688                    eprintln!(
1689                        "[SG_FRAME_ACTION_CALL] current-scene user command not found: cmd={} scene={:?} scn_name={:?}",
1690                        cmd_name,
1691                        self.current_scene_no,
1692                        scn_name
1693                    );
1694                }
1695                return Ok(false);
1696            };
1697            let offset = self.stream.scn_cmd_offset(cmd_no)?;
1698            self.enter_current_scene_user_cmd_proc_at_offset(
1699                offset,
1700                self.cfg.fm_void,
1701                call_args,
1702                false,
1703                true,
1704            )?;
1705        } else {
1706            let Some(name) = scn_name.filter(|name| !name.is_empty()) else {
1707                return Ok(false);
1708            };
1709            self.ensure_scene_pck_cache()?;
1710            let Some(target_scene_no) = self
1711                .scene_pck_cache
1712                .as_ref()
1713                .expect("scene pck cache initialized")
1714                .find_scene_no(name)
1715            else {
1716                if std::env::var_os("SIGLUS_TRACE_FRAME_ACTION_CALL").is_some() {
1717                    eprintln!(
1718                        "[SG_FRAME_ACTION_CALL] target scene not found: scn_name={} cmd={}",
1719                        name, cmd_name
1720                    );
1721                }
1722                return Ok(false);
1723            };
1724
1725            let target_stream = self.cached_scene_stream(target_scene_no)?;
1726            let Some(cmd_no) = target_stream.scn_cmd_name_map.iter().find_map(|(no, name)| {
1727                if name.eq_ignore_ascii_case(cmd_name) {
1728                    Some(*no as usize)
1729                } else {
1730                    None
1731                }
1732            }) else {
1733                if std::env::var_os("SIGLUS_TRACE_FRAME_ACTION_CALL").is_some() {
1734                    eprintln!(
1735                        "[SG_FRAME_ACTION_CALL] user command not found: cmd={} target_scene={} scn_name={:?}",
1736                        cmd_name,
1737                        target_scene_no,
1738                        scn_name
1739                    );
1740                }
1741                return Ok(false);
1742            };
1743            let offset = target_stream.scn_cmd_offset(cmd_no)?;
1744            self.enter_scene_user_cmd_at_scene_offset_ex(
1745                target_scene_no,
1746                offset,
1747                call_args,
1748                self.cfg.fm_void,
1749                false,
1750                true,
1751            )?;
1752        }
1753
1754        if std::env::var_os("SIGLUS_TRACE_FRAME_ACTION_CALL").is_some() {
1755            eprintln!(
1756                "[SG_FRAME_ACTION_CALL] proc enter cmd={} scene={:?} depth={} args={:?}",
1757                cmd_name,
1758                self.current_scene_no,
1759                self.call_stack.len(),
1760                call_args
1761            );
1762        }
1763
1764        let mut completed_by_return = false;
1765        let mut stopped_at_proc_boundary = false;
1766        let mut stopped_at_wait_boundary = false;
1767        let mut run_error = None;
1768        let max_steps = std::env::var("SIGLUS_FRAME_ACTION_MAX_STEPS")
1769            .ok()
1770            .and_then(|s| s.parse::<u64>().ok())
1771            .unwrap_or(0);
1772        let mut steps: u64 = 0;
1773        loop {
1774            let wait_generation_before_step = self.ctx.wait.block_generation();
1775            let proc_generation_before_step = self.ctx.proc_generation();
1776            let running = match self.step_inner(false) {
1777                Ok(v) => v,
1778                Err(e) => {
1779                    run_error = Some(e);
1780                    break;
1781                }
1782            };
1783            if self.current_scene_no == saved_scene_no
1784                && self.scene_stack.len() == saved_scene_stack_len
1785                && self.call_stack.len() == saved_call_depth
1786            {
1787                completed_by_return = true;
1788                break;
1789            }
1790            if self.halted || !running {
1791                break;
1792            }
1793            if self.ctx.proc_generation() != proc_generation_before_step {
1794                stopped_at_proc_boundary = true;
1795                break;
1796            }
1797            if self.ctx.wait.block_generation() != wait_generation_before_step && self.ctx.wait_poll() {
1798                stopped_at_wait_boundary = true;
1799                break;
1800            }
1801            steps = steps.saturating_add(1);
1802            if max_steps > 0 && steps >= max_steps {
1803                run_error = Some(anyhow!(
1804                    "frame_action user command exceeded SIGLUS_FRAME_ACTION_MAX_STEPS: cmd={} scene={:?}",
1805                    cmd_name,
1806                    scn_name
1807                ));
1808                break;
1809            }
1810        }
1811
1812        // A frame-action user command is invoked from the frame phase, not from
1813        // the main SCRIPT proc.  Original Siglus runs it synchronously via
1814        // tnm_proc_script(), and CD_RETURN with frame_action_flag exits that
1815        // recursive script loop.  Even when the callback hits a VM error, the
1816        // caller lexer/call stack must not be left inside the callback body;
1817        // otherwise the next frame continues at the failed callback PC and can
1818        // run into command padding / CD_NONE.
1819        let restore_callback_lexer = self.current_scene_no == saved_scene_no;
1820        if restore_callback_lexer {
1821            if std::env::var_os("SIGLUS_TRACE_FRAME_ACTION_CALL").is_some() {
1822                eprintln!(
1823                    "[SG_FRAME_ACTION_CALL] proc exit cmd={} scene={:?} completed={} proc_boundary={} wait_boundary={} error={} restoring caller lexer state",
1824                    cmd_name,
1825                    scn_name,
1826                    completed_by_return,
1827                    stopped_at_proc_boundary,
1828                    stopped_at_wait_boundary,
1829                    run_error.is_some()
1830                );
1831            }
1832            self.restore_interpreter_exec_state(saved_exec);
1833        }
1834
1835        if let Some(e) = run_error {
1836            return Err(e);
1837        }
1838
1839        Ok(true)
1840    }
1841
1842    fn enter_current_scene_user_cmd_proc_at_offset(
1843        &mut self,
1844        offset: usize,
1845        ret_form: i32,
1846        call_args: &[Value],
1847        excall_proc: bool,
1848        frame_action_proc: bool,
1849    ) -> Result<bool> {
1850        let return_pc = self.stream.get_prg_cntr();
1851        let depth = self.call_stack.len();
1852        let Some(caller) = self.call_stack.last_mut() else {
1853            return Ok(false);
1854        };
1855        if std::env::var_os("SIGLUS_TRACE_CALL_RETURN_PC").is_some() {
1856            eprintln!(
1857                "[SG_CALL_PC] proc-call set depth={} offset=0x{:x} return_pc=0x{:x} old=0x{:x} frame_action={}",
1858                depth,
1859                offset,
1860                return_pc,
1861                caller.return_pc,
1862                frame_action_proc
1863            );
1864        }
1865        caller.return_pc = return_pc;
1866        caller.ret_form = ret_form;
1867        for arg in call_args {
1868            self.push_call_arg_value(arg);
1869        }
1870        self.call_stack.push(self.make_call_frame(
1871            self.cfg.fm_void,
1872            excall_proc,
1873            frame_action_proc,
1874            call_args.len(),
1875            None,
1876        ));
1877        self.stream.set_prg_cntr(offset)?;
1878        if excall_proc {
1879            self.mark_excall_script_proc_requested();
1880        }
1881        Ok(true)
1882    }
1883
1884    fn enter_scene_user_cmd_at_scene_offset_ex(
1885        &mut self,
1886        target_scene_no: usize,
1887        target_offset: usize,
1888        call_args: &[Value],
1889        ret_form: i32,
1890        ex_call_proc: bool,
1891        frame_action_proc: bool,
1892    ) -> Result<bool> {
1893        let target_stream = self.cached_scene_stream(target_scene_no)?;
1894        if target_offset > target_stream.scn.len() {
1895            bail!(
1896                "scene_pck: user command offset out of bounds: scn_no={} offset=0x{:x} scn_len=0x{:x}",
1897                target_scene_no,
1898                target_offset,
1899                target_stream.scn.len()
1900            );
1901        }
1902
1903        let saved = SceneExecFrame {
1904            stream: self.stream.clone(),
1905            user_cmd_names: self.user_cmd_names.clone(),
1906            call_cmd_names: self.call_cmd_names.clone(),
1907            int_stack: std::mem::take(&mut self.int_stack),
1908            str_stack: std::mem::take(&mut self.str_stack),
1909            element_points: std::mem::take(&mut self.element_points),
1910            call_stack: std::mem::take(&mut self.call_stack),
1911            gosub_return_stack: std::mem::take(&mut self.gosub_return_stack),
1912            user_props: self.enter_cross_scene_user_prop_scope(),
1913            current_scene_no: self.current_scene_no,
1914            current_scene_name: self.current_scene_name.clone(),
1915            current_line_no: self.current_line_no,
1916            ret_form,
1917            excall_proc: ex_call_proc,
1918        };
1919        self.scene_stack.push(saved);
1920
1921        self.stream = target_stream;
1922        self.user_cmd_names = self.stream.scn_cmd_name_map.clone();
1923        self.call_cmd_names = self
1924            .scene_pck_cache
1925            .as_ref()
1926            .expect("scene pck cache initialized")
1927            .inc_cmd_name_map
1928            .clone();
1929        self.current_scene_no = Some(target_scene_no);
1930        self.current_scene_name = self
1931            .scene_pck_cache
1932            .as_ref()
1933            .expect("scene pck cache initialized")
1934            .find_scene_name(target_scene_no)
1935            .map(ToOwned::to_owned);
1936        self.current_line_no = -1;
1937        self.ctx.current_scene_no = Some(target_scene_no as i64);
1938        self.ctx.current_scene_name = self.current_scene_name.clone();
1939        self.ctx.current_line_no = -1;
1940
1941        for arg in call_args {
1942            self.push_call_arg_value(arg);
1943        }
1944        self.call_stack.push(self.make_call_frame(
1945            self.cfg.fm_void,
1946            ex_call_proc,
1947            frame_action_proc,
1948            call_args.len(),
1949            None,
1950        ));
1951        self.stream.set_prg_cntr(target_offset)?;
1952        if ex_call_proc {
1953            self.mark_excall_script_proc_requested();
1954        }
1955        Ok(true)
1956    }
1957
1958    fn enter_scene_user_cmd_call(
1959        &mut self,
1960        scn_name: Option<&str>,
1961        cmd_name: &str,
1962        call_args: &[Value],
1963    ) -> Result<bool> {
1964        let current_scene_no = self.current_scene_no;
1965        self.ensure_scene_pck_cache()?;
1966
1967        let target_scene_no = match scn_name {
1968            Some(name) if !name.is_empty() => self
1969                .scene_pck_cache
1970                .as_ref()
1971                .expect("scene pck cache initialized")
1972                .find_scene_no(name)
1973                .or(current_scene_no),
1974            _ => current_scene_no,
1975        };
1976        let Some(target_scene_no) = target_scene_no else {
1977            return Ok(false);
1978        };
1979
1980        // C++ SET_BUTTON_CALL stores a scene-local user command name and resolves it
1981        // with Gp_lexer->get_user_cmd_no(scene_no, cmd_name). It must not prefer
1982        // global inc-command names here, because button callbacks are user commands
1983        // in the stored scene.
1984        if Some(target_scene_no) == self.current_scene_no {
1985            let Some(cmd_no) = self.user_cmd_names.iter().find_map(|(no, name)| {
1986                if name.eq_ignore_ascii_case(cmd_name) {
1987                    Some(*no as usize)
1988                } else {
1989                    None
1990                }
1991            }) else {
1992                if std::env::var_os("SG_DEBUG").is_some() {
1993                    eprintln!(
1994                        "[SG_DEBUG][BUTTON] user command not found for ex-call: scene={:?} cmd={}",
1995                        scn_name, cmd_name
1996                    );
1997                }
1998                return Ok(false);
1999            };
2000            let offset = self.stream.scn_cmd_offset(cmd_no)?;
2001            if std::env::var_os("SG_DEBUG").is_some() {
2002                eprintln!(
2003                    "[SG_DEBUG][BUTTON] enter local user command scene={:?} cmd={} cmd_no={} offset=0x{:x}",
2004                    scn_name,
2005                    cmd_name,
2006                    cmd_no,
2007                    offset
2008                );
2009            }
2010            return self.enter_current_scene_user_cmd_at_offset(offset, call_args);
2011        }
2012
2013        let target_stream = self.cached_scene_stream(target_scene_no)?;
2014        let Some(cmd_no) = target_stream
2015            .scn_cmd_name_map
2016            .iter()
2017            .find_map(|(no, name)| {
2018                if name.eq_ignore_ascii_case(cmd_name) {
2019                    Some(*no as usize)
2020                } else {
2021                    None
2022                }
2023            })
2024        else {
2025            if std::env::var_os("SG_DEBUG").is_some() {
2026                eprintln!(
2027                    "[SG_DEBUG][BUTTON] target user command not found for ex-call: target_scene={} scn_name={:?} cmd={}",
2028                    target_scene_no,
2029                    scn_name,
2030                    cmd_name
2031                );
2032            }
2033            return Ok(false);
2034        };
2035        let offset = target_stream.scn_cmd_offset(cmd_no)?;
2036        if std::env::var_os("SG_DEBUG").is_some() {
2037            eprintln!(
2038                "[SG_DEBUG][BUTTON] enter target user command target_scene={} scn_name={:?} cmd={} cmd_no={} offset=0x{:x}",
2039                target_scene_no,
2040                scn_name,
2041                cmd_name,
2042                cmd_no,
2043                offset
2044            );
2045        }
2046        self.enter_scene_user_cmd_at_scene_offset(target_scene_no, offset, call_args)
2047    }
2048
2049    fn enter_current_scene_user_cmd_at_offset(
2050        &mut self,
2051        offset: usize,
2052        call_args: &[Value],
2053    ) -> Result<bool> {
2054        self.enter_current_scene_user_cmd_proc_at_offset(
2055            offset,
2056            self.cfg.fm_void,
2057            call_args,
2058            true,
2059            false,
2060        )
2061    }
2062
2063    fn enter_scene_user_cmd_at_scene_offset(
2064        &mut self,
2065        target_scene_no: usize,
2066        target_offset: usize,
2067        call_args: &[Value],
2068    ) -> Result<bool> {
2069        self.enter_scene_user_cmd_at_scene_offset_ex(
2070            target_scene_no,
2071            target_offset,
2072            call_args,
2073            self.cfg.fm_void,
2074            true,
2075            false,
2076        )
2077    }
2078
2079    fn run_current_scene_user_cmd_inline(
2080        &mut self,
2081        cmd_name: &str,
2082        call_args: &[Value],
2083    ) -> Result<bool> {
2084        self.run_scene_user_cmd_inline(None, cmd_name, call_args, self.cfg.fm_void, false)
2085    }
2086
2087    fn run_scene_user_cmd_inline_at_scene_offset(
2088        &mut self,
2089        pck: &ScenePck,
2090        target_scene_no: usize,
2091        cmd_name: &str,
2092        target_offset: usize,
2093        call_args: &[Value],
2094        preserve_return_pc: bool,
2095        frame_action_proc: bool,
2096    ) -> Result<bool> {
2097        let chunk = pck.scn_data_slice(target_scene_no)?;
2098        let chunk_leaked: &'static [u8] = Box::leak(chunk.to_vec().into_boxed_slice());
2099        let target_stream: SceneStream<'a> = SceneStream::new(chunk_leaked)?;
2100        if target_offset > target_stream.scn.len() {
2101            bail!(
2102                "scene_pck: user command offset out of bounds: cmd={} scn_no={} offset=0x{:x} scn_len=0x{:x}",
2103                cmd_name,
2104                target_scene_no,
2105                target_offset,
2106                target_stream.scn.len()
2107            );
2108        }
2109
2110        let saved_stream = std::mem::replace(&mut self.stream, target_stream);
2111        let target_user_cmd_names = self.stream.scn_cmd_name_map.clone();
2112        let target_call_cmd_names = pck.inc_cmd_name_map.clone();
2113        let saved_user_cmd_names =
2114            std::mem::replace(&mut self.user_cmd_names, target_user_cmd_names);
2115        let saved_call_cmd_names =
2116            std::mem::replace(&mut self.call_cmd_names, target_call_cmd_names);
2117        let saved_current_scene_no = self.current_scene_no;
2118        let saved_current_scene_name = self.current_scene_name.clone();
2119        let saved_current_line_no = self.current_line_no;
2120        let saved_ctx_scene_no = self.ctx.current_scene_no;
2121        let saved_ctx_scene_name = self.ctx.current_scene_name.clone();
2122        let saved_ctx_line_no = self.ctx.current_line_no;
2123        let saved_halted = self.halted;
2124        let saved_user_props = self.enter_cross_scene_user_prop_scope();
2125
2126        self.current_scene_no = Some(target_scene_no);
2127        self.current_scene_name = pck.find_scene_name(target_scene_no).map(ToOwned::to_owned);
2128        self.current_line_no = -1;
2129        self.ctx.current_scene_no = Some(target_scene_no as i64);
2130        self.ctx.current_scene_name = self.current_scene_name.clone();
2131        self.ctx.current_line_no = -1;
2132
2133        let target_return_pc = if preserve_return_pc {
2134            saved_stream.get_prg_cntr()
2135        } else {
2136            self.stream.scn.len()
2137        };
2138        let result = self.run_user_cmd_inline_at_offset(
2139            cmd_name,
2140            target_offset,
2141            target_return_pc,
2142            None,
2143            None,
2144            self.cfg.fm_void,
2145            call_args,
2146            frame_action_proc,
2147        );
2148
2149        self.stream = saved_stream;
2150        self.user_cmd_names = saved_user_cmd_names;
2151        self.call_cmd_names = saved_call_cmd_names;
2152        self.current_scene_no = saved_current_scene_no;
2153        self.current_scene_name = saved_current_scene_name;
2154        self.current_line_no = saved_current_line_no;
2155        self.ctx.current_scene_no = saved_ctx_scene_no;
2156        self.ctx.current_scene_name = saved_ctx_scene_name;
2157        self.ctx.current_line_no = saved_ctx_line_no;
2158        self.halted = saved_halted;
2159        self.restore_cross_scene_user_prop_scope(saved_user_props);
2160        result
2161    }
2162
2163    fn collect_object_frame_action_work_recursive(
2164        obj: &crate::runtime::globals::ObjectState,
2165        stage_idx: i64,
2166        obj_idx: usize,
2167        object_chain: Vec<i32>,
2168        out: &mut Vec<FrameActionWork>,
2169    ) {
2170        let fa = &obj.frame_action;
2171        if !fa.cmd_name.is_empty() {
2172            let mut frame_action_chain = object_chain.clone();
2173            frame_action_chain.push(crate::runtime::forms::codes::elm_value::OBJECT_FRAME_ACTION);
2174            out.push(FrameActionWork {
2175                stage_idx,
2176                obj_idx,
2177                ch_idx: None,
2178                global_form_id: None,
2179                object_chain: Some(object_chain.clone()),
2180                frame_action_chain: Some(frame_action_chain),
2181                scn_name: fa.scn_name.clone(),
2182                cmd_name: fa.cmd_name.clone(),
2183                args: fa.args.clone(),
2184                count: fa.counter.get_count(),
2185                end_time: fa.end_time,
2186            });
2187        }
2188        for (ch_idx, ch) in obj.frame_action_ch.iter().enumerate() {
2189            if !ch.cmd_name.is_empty() {
2190                let mut frame_action_chain = object_chain.clone();
2191                frame_action_chain
2192                    .push(crate::runtime::forms::codes::elm_value::OBJECT_FRAME_ACTION_CH);
2193                frame_action_chain.push(crate::runtime::forms::codes::ELM_ARRAY);
2194                frame_action_chain.push(ch_idx as i32);
2195                out.push(FrameActionWork {
2196                    stage_idx,
2197                    obj_idx,
2198                    ch_idx: Some(ch_idx),
2199                    global_form_id: None,
2200                    object_chain: Some(object_chain.clone()),
2201                    frame_action_chain: Some(frame_action_chain),
2202                    scn_name: ch.scn_name.clone(),
2203                    cmd_name: ch.cmd_name.clone(),
2204                    args: ch.args.clone(),
2205                    count: ch.counter.get_count(),
2206                    end_time: ch.end_time,
2207                });
2208            }
2209        }
2210        for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
2211            let child_has_frame_action = !child.frame_action.cmd_name.is_empty()
2212                || child.frame_action_ch.iter().any(|ch| !ch.cmd_name.is_empty());
2213            let child_has_nested_work = !child.runtime.child_objects.is_empty();
2214            if child.used || child_has_frame_action || child_has_nested_work {
2215                let mut child_chain = object_chain.clone();
2216                child_chain.push(crate::runtime::forms::codes::elm_value::OBJECT_CHILD);
2217                child_chain.push(crate::runtime::forms::codes::ELM_ARRAY);
2218                child_chain.push(child_idx as i32);
2219                Self::collect_object_frame_action_work_recursive(
2220                    child,
2221                    stage_idx,
2222                    child_idx,
2223                    child_chain,
2224                    out,
2225                );
2226            }
2227        }
2228    }
2229
2230    fn object_child_from_chain_mut<'b>(
2231        mut obj: &'b mut crate::runtime::globals::ObjectState,
2232        object_chain: &[i32],
2233        mut pos: usize,
2234        elm_array: i32,
2235    ) -> Option<&'b mut crate::runtime::globals::ObjectState> {
2236        while pos + 2 < object_chain.len() {
2237            let op = object_chain[pos];
2238            if op != crate::runtime::forms::codes::elm_value::OBJECT_CHILD {
2239                break;
2240            }
2241            if object_chain[pos + 1] != elm_array
2242                && object_chain[pos + 1] != crate::runtime::forms::codes::ELM_ARRAY
2243            {
2244                return None;
2245            }
2246            let child_idx = object_chain[pos + 2].max(0) as usize;
2247            obj = obj.runtime.child_objects.get_mut(child_idx)?;
2248            pos += 3;
2249        }
2250        Some(obj)
2251    }
2252
2253    fn object_from_frame_action_chain_mut<'b>(
2254        objects: &'b mut [crate::runtime::globals::ObjectState],
2255        object_chain: &[i32],
2256        elm_array: i32,
2257    ) -> Option<&'b mut crate::runtime::globals::ObjectState> {
2258        if object_chain.len() < 6 || object_chain[1] != elm_array || object_chain[4] != elm_array {
2259            return None;
2260        }
2261        let obj = objects.get_mut(object_chain[5].max(0) as usize)?;
2262        Self::object_child_from_chain_mut(obj, object_chain, 6, elm_array)
2263    }
2264
2265    fn object_from_mwnd_frame_action_chain_mut<'b>(
2266        mwnds: &'b mut [crate::runtime::globals::MwndState],
2267        object_chain: &[i32],
2268        elm_array: i32,
2269    ) -> Option<&'b mut crate::runtime::globals::ObjectState> {
2270        if object_chain.len() < 9
2271            || object_chain[1] != elm_array
2272            || object_chain[4] != elm_array
2273            || object_chain[7] != elm_array
2274        {
2275            return None;
2276        }
2277        if object_chain[3] != crate::runtime::forms::codes::elm_value::STAGE_MWND {
2278            return None;
2279        }
2280        let mwnd_idx = object_chain[5].max(0) as usize;
2281        let selector = object_chain[6];
2282        let obj_idx = object_chain[8].max(0) as usize;
2283        let mwnd = mwnds.get_mut(mwnd_idx)?;
2284        if selector == crate::runtime::forms::codes::elm_value::MWND_BUTTON {
2285            let obj = mwnd.button_list.get_mut(obj_idx)?;
2286            return Self::object_child_from_chain_mut(obj, object_chain, 9, elm_array);
2287        }
2288        if selector == crate::runtime::forms::codes::elm_value::MWND_FACE {
2289            let obj = mwnd.face_list.get_mut(obj_idx)?;
2290            return Self::object_child_from_chain_mut(obj, object_chain, 9, elm_array);
2291        }
2292        if selector == crate::runtime::forms::codes::elm_value::MWND_OBJECT {
2293            let obj = mwnd.object_list.get_mut(obj_idx)?;
2294            return Self::object_child_from_chain_mut(obj, object_chain, 9, elm_array);
2295        }
2296        None
2297    }
2298
2299    fn with_frame_action_mut<R>(
2300        &mut self,
2301        item: &FrameActionWork,
2302        f: impl FnOnce(&mut crate::runtime::globals::ObjectFrameActionState) -> R,
2303    ) -> Option<R> {
2304        if item.stage_idx < 0 {
2305            let form_id = item.global_form_id?;
2306            if let Some(idx) = item.ch_idx {
2307                let list = self.ctx.globals.frame_action_lists.get_mut(&form_id)?;
2308                return list.get_mut(idx).map(f);
2309            }
2310            return self.ctx.globals.frame_actions.get_mut(&form_id).map(f);
2311        }
2312
2313        let chain = item.object_chain.as_ref()?;
2314        if chain.len() < 6 {
2315            return None;
2316        }
2317        let stage_idx = chain[2] as i64;
2318        let elm_array = self.ctx.ids.elm_array;
2319        let mut form_ids: Vec<u32> = self.ctx.globals.stage_forms.keys().copied().collect();
2320        form_ids.sort_unstable();
2321        for form_id in form_ids {
2322            let Some(st) = self.ctx.globals.stage_forms.get_mut(&form_id) else {
2323                continue;
2324            };
2325            let obj = if chain.get(3).copied()
2326                == Some(crate::runtime::forms::codes::elm_value::STAGE_MWND)
2327            {
2328                let Some(mwnds) = st.mwnd_lists.get_mut(&stage_idx) else {
2329                    continue;
2330                };
2331                let Some(obj) = Self::object_from_mwnd_frame_action_chain_mut(mwnds, chain, elm_array)
2332                else {
2333                    continue;
2334                };
2335                obj
2336            } else {
2337                let Some(objects) = st.object_lists.get_mut(&stage_idx) else {
2338                    continue;
2339                };
2340                let Some(obj) = Self::object_from_frame_action_chain_mut(objects, chain, elm_array)
2341                else {
2342                    continue;
2343                };
2344                obj
2345            };
2346            if let Some(idx) = item.ch_idx {
2347                return obj.frame_action_ch.get_mut(idx).map(f);
2348            }
2349            return Some(f(&mut obj.frame_action));
2350        }
2351        None
2352    }
2353
2354    fn begin_frame_action_finish(
2355        &mut self,
2356        item: &FrameActionWork,
2357    ) -> Option<(String, Vec<Value>)> {
2358        self.with_frame_action_mut(item, |fa| {
2359            if fa.cmd_name.is_empty() || fa.end_time < 0 {
2360                return None;
2361            }
2362            if fa.counter.get_count() < fa.end_time {
2363                return None;
2364            }
2365
2366            let cmd_name = fa.cmd_name.clone();
2367            let args = fa.args.clone();
2368            let final_count = if fa.end_time == -1 { 0 } else { fa.end_time };
2369            fa.counter.set_count(final_count);
2370            fa.scn_name.clear();
2371            fa.cmd_name.clear();
2372            fa.end_flag = true;
2373            Some((cmd_name, args))
2374        })?
2375    }
2376
2377    fn end_frame_action_finish(&mut self, item: &FrameActionWork) {
2378        let _ = self.with_frame_action_mut(item, |fa| {
2379            // Original C_elm_frame_action::finish only drops the end-action flag
2380            // after the callback. It must not wipe a new frame action that may
2381            // have been started by that callback.
2382            fa.end_flag = false;
2383        });
2384    }
2385
2386    fn make_frame_action_call_args(
2387        frame_action_chain: Option<&Vec<i32>>,
2388        object_chain: Option<&Vec<i32>>,
2389        args: &[Value],
2390    ) -> Vec<Value> {
2391        let mut call_args = Vec::with_capacity(args.len() + 2);
2392        if let Some(frame_action_chain) = frame_action_chain {
2393            call_args.push(Value::Element(frame_action_chain.clone()));
2394        }
2395        if let Some(object_chain) = object_chain {
2396            call_args.push(Value::Element(object_chain.clone()));
2397        }
2398        call_args.extend(args.iter().cloned());
2399        call_args
2400    }
2401
2402    fn runtime_slot_from_object_children(
2403        mut obj: &mut crate::runtime::globals::ObjectState,
2404        fallback_slot: usize,
2405        chain: &[i32],
2406        mut pos: usize,
2407        elm_array: i32,
2408        next_slot: &mut usize,
2409    ) -> usize {
2410        let object_child = crate::runtime::forms::codes::elm_value::OBJECT_CHILD;
2411        let mut slot = obj.runtime_slot_or(fallback_slot);
2412        while pos + 2 < chain.len() {
2413            if chain[pos] == object_child
2414                && (chain[pos + 1] == elm_array
2415                    || chain[pos + 1] == crate::runtime::forms::codes::ELM_ARRAY)
2416            {
2417                let child_idx = chain[pos + 2].max(0) as usize;
2418                if obj.runtime.child_objects.len() <= child_idx {
2419                    obj.runtime
2420                        .child_objects
2421                        .resize_with(child_idx + 1, crate::runtime::globals::ObjectState::default);
2422                }
2423                let child = &mut obj.runtime.child_objects[child_idx];
2424                slot = child.ensure_runtime_slot(next_slot);
2425                obj = child;
2426                pos += 3;
2427            } else {
2428                pos += 1;
2429            }
2430        }
2431        slot
2432    }
2433
2434    fn runtime_slot_from_object_chain(
2435        &mut self,
2436        stage_idx: i64,
2437        fallback_obj_idx: usize,
2438        chain: &[i32],
2439    ) -> usize {
2440        let stage_form = self.ctx.ids.form_global_stage;
2441        let elm_array = if self.ctx.ids.elm_array != 0 {
2442            self.ctx.ids.elm_array
2443        } else {
2444            crate::runtime::forms::codes::ELM_ARRAY
2445        };
2446
2447        let Some(st) = self.ctx.globals.stage_forms.get_mut(&stage_form) else {
2448            return fallback_obj_idx;
2449        };
2450        let next_slot = st
2451            .next_nested_object_slot
2452            .entry(stage_idx)
2453            .or_insert(100000);
2454
2455        if chain.get(3).copied() == Some(crate::runtime::forms::codes::elm_value::STAGE_MWND)
2456            && chain.len() >= 9
2457        {
2458            let mwnd_idx = chain.get(5).copied().unwrap_or(0).max(0) as usize;
2459            let selector = chain.get(6).copied().unwrap_or(0);
2460            let obj_idx = chain
2461                .get(8)
2462                .copied()
2463                .unwrap_or(fallback_obj_idx as i32)
2464                .max(0) as usize;
2465            let Some(mwnds) = st.mwnd_lists.get_mut(&stage_idx) else {
2466                return obj_idx;
2467            };
2468            let Some(mwnd) = mwnds.get_mut(mwnd_idx) else {
2469                return obj_idx;
2470            };
2471            if selector == crate::runtime::forms::codes::elm_value::MWND_BUTTON {
2472                let Some(obj) = mwnd.button_list.get_mut(obj_idx) else {
2473                    return obj_idx;
2474                };
2475                return Self::runtime_slot_from_object_children(
2476                    obj, obj_idx, chain, 9, elm_array, next_slot,
2477                );
2478            }
2479            if selector == crate::runtime::forms::codes::elm_value::MWND_FACE {
2480                let Some(obj) = mwnd.face_list.get_mut(obj_idx) else {
2481                    return obj_idx;
2482                };
2483                return Self::runtime_slot_from_object_children(
2484                    obj, obj_idx, chain, 9, elm_array, next_slot,
2485                );
2486            }
2487            if selector == crate::runtime::forms::codes::elm_value::MWND_OBJECT {
2488                let Some(obj) = mwnd.object_list.get_mut(obj_idx) else {
2489                    return obj_idx;
2490                };
2491                return Self::runtime_slot_from_object_children(
2492                    obj, obj_idx, chain, 9, elm_array, next_slot,
2493                );
2494            }
2495            return obj_idx;
2496        }
2497
2498        if chain.get(3).copied() == Some(crate::runtime::forms::codes::STAGE_ELM_BTNSELITEM)
2499            && chain.len() >= 9
2500            && chain.get(6).copied() == Some(crate::runtime::forms::codes::ELM_BTNSELITEM_OBJECT)
2501        {
2502            let item_idx = chain.get(5).copied().unwrap_or(0).max(0) as usize;
2503            let obj_idx = chain
2504                .get(8)
2505                .copied()
2506                .unwrap_or(fallback_obj_idx as i32)
2507                .max(0) as usize;
2508            let Some(items) = st.btnselitem_lists.get_mut(&stage_idx) else {
2509                return obj_idx;
2510            };
2511            let Some(item) = items.get_mut(item_idx) else {
2512                return obj_idx;
2513            };
2514            let Some(obj) = item.object_list.get_mut(obj_idx) else {
2515                return obj_idx;
2516            };
2517            return Self::runtime_slot_from_object_children(
2518                obj, obj_idx, chain, 9, elm_array, next_slot,
2519            );
2520        }
2521
2522        let top_idx = chain
2523            .get(5)
2524            .copied()
2525            .unwrap_or(fallback_obj_idx as i32)
2526            .max(0) as usize;
2527        let Some(list) = st.object_lists.get_mut(&stage_idx) else {
2528            return top_idx;
2529        };
2530        if top_idx >= list.len() {
2531            return top_idx;
2532        }
2533        let obj = &mut list[top_idx];
2534        Self::runtime_slot_from_object_children(obj, top_idx, chain, 6, elm_array, next_slot)
2535    }
2536
2537    fn set_frame_action_current_object(
2538        &mut self,
2539        item: &FrameActionWork,
2540    ) -> (Option<(i64, usize)>, Option<Vec<i32>>) {
2541        let prev_target = self.ctx.globals.current_stage_object;
2542        let prev_chain = self.ctx.globals.current_object_chain.clone();
2543        if let Some(chain) = item.object_chain.clone() {
2544            let top_idx = chain.get(5).copied().unwrap_or(item.obj_idx as i32).max(0) as usize;
2545            let runtime_slot = self.runtime_slot_from_object_chain(item.stage_idx, top_idx, &chain);
2546            self.ctx.globals.current_stage_object = Some((item.stage_idx, runtime_slot));
2547            self.ctx.globals.current_object_chain = Some(chain);
2548        } else {
2549            self.ctx.globals.current_stage_object = None;
2550            self.ctx.globals.current_object_chain = None;
2551        }
2552        (prev_target, prev_chain)
2553    }
2554
2555    fn restore_frame_action_current_object(
2556        &mut self,
2557        prev_target: Option<(i64, usize)>,
2558        prev_chain: Option<Vec<i32>>,
2559    ) {
2560        self.ctx.globals.current_stage_object = prev_target;
2561        self.ctx.globals.current_object_chain = prev_chain;
2562    }
2563
2564    fn frame_action_work_from_pending_finish(
2565        &self,
2566        pending: &PendingFrameActionFinish,
2567    ) -> FrameActionWork {
2568        let object_chain = pending.object_chain.clone();
2569        let (stage_idx, obj_idx) = object_chain
2570            .as_ref()
2571            .and_then(|chain| {
2572                if chain.len() >= 6 {
2573                    Some((chain[2] as i64, chain[5].max(0) as usize))
2574                } else {
2575                    None
2576                }
2577            })
2578            .unwrap_or((-1, usize::MAX));
2579
2580        let ch_idx = if let Some(chain) = object_chain.as_ref() {
2581            let base = chain.len();
2582            if pending.frame_action_chain.len() >= base + 3
2583                && pending.frame_action_chain[base]
2584                    == crate::runtime::forms::codes::elm_value::OBJECT_FRAME_ACTION_CH
2585                && pending.frame_action_chain[base + 1] == crate::runtime::forms::codes::ELM_ARRAY
2586            {
2587                Some(pending.frame_action_chain[base + 2].max(0) as usize)
2588            } else {
2589                None
2590            }
2591        } else if pending.frame_action_chain.len() >= 3
2592            && pending.frame_action_chain[1] == crate::runtime::forms::codes::ELM_ARRAY
2593        {
2594            Some(pending.frame_action_chain[2].max(0) as usize)
2595        } else {
2596            None
2597        };
2598
2599        let global_form_id = if object_chain.is_none() {
2600            pending
2601                .frame_action_chain
2602                .first()
2603                .copied()
2604                .map(|v| v as u32)
2605        } else {
2606            None
2607        };
2608
2609        FrameActionWork {
2610            stage_idx,
2611            obj_idx,
2612            ch_idx,
2613            global_form_id,
2614            object_chain,
2615            frame_action_chain: Some(pending.frame_action_chain.clone()),
2616            scn_name: pending.scn_name.clone(),
2617            cmd_name: pending.cmd_name.clone(),
2618            args: pending.args.clone(),
2619            count: pending.end_time,
2620            end_time: pending.end_time,
2621        }
2622    }
2623
2624    fn run_pending_frame_action_finish(&mut self, pending: PendingFrameActionFinish) -> Result<()> {
2625        if pending.cmd_name.is_empty() {
2626            return Ok(());
2627        }
2628        let item = self.frame_action_work_from_pending_finish(&pending);
2629        let final_count = if pending.end_time == -1 {
2630            0
2631        } else {
2632            pending.end_time
2633        };
2634        let _ = self.with_frame_action_mut(&item, |fa| {
2635            // C_elm_frame_action::finish clears the active command before invoking
2636            // the end action, sets the end-action flag, and leaves any new
2637            // frame-action state created by that callback intact.
2638            fa.scn_name.clear();
2639            fa.cmd_name.clear();
2640            fa.args = pending.args.clone();
2641            fa.end_time = pending.end_time;
2642            fa.counter.set_count(final_count);
2643            fa.end_flag = true;
2644        });
2645
2646        let call_args = Self::make_frame_action_call_args(
2647            item.frame_action_chain.as_ref(),
2648            item.object_chain.as_ref(),
2649            &pending.args,
2650        );
2651        let (prev_target, prev_chain) = self.set_frame_action_current_object(&item);
2652        let result = self.run_scene_user_cmd_inline(
2653            Some(&pending.scn_name),
2654            &pending.cmd_name,
2655            &call_args,
2656            self.cfg.fm_void,
2657            true,
2658        );
2659        self.restore_frame_action_current_object(prev_target, prev_chain);
2660
2661        let _ = self.with_frame_action_mut(&item, |fa| {
2662            fa.end_flag = false;
2663        });
2664        if let Err(e) = result {
2665            self.ctx.unknown.record_note(&format!(
2666                "frame_action.finish.failed:{}:{}:{e}",
2667                pending.scn_name, pending.cmd_name
2668            ));
2669        }
2670        Ok(())
2671    }
2672
2673    fn run_pending_button_action(&mut self, action: PendingButtonAction) -> Result<()> {
2674        // Original tona3/Siglus copies runtime input into script input before
2675        // frame_main_proc(), while object button actions are decided later from
2676        // the element/frame pass.  A scene or user command entered by a button
2677        // action must therefore not observe the same mouse down/up stock that
2678        // decided the button.  Clear edge stocks here while preserving held
2679        // state and mouse position.
2680        self.ctx.input.use_current();
2681        self.script_input_synced_this_frame = false;
2682
2683        match action.kind {
2684            PendingButtonActionKind::UserCall {
2685                scn_name,
2686                cmd_name,
2687                z_no,
2688            } => {
2689                if scn_name.is_empty() {
2690                    return Ok(());
2691                }
2692                if std::env::var_os("SG_DEBUG").is_some() {
2693                    eprintln!(
2694                        "[SG_DEBUG][BUTTON] run action scene={} cmd={} z_no={}",
2695                        scn_name, cmd_name, z_no
2696                    );
2697                }
2698                if !cmd_name.is_empty() {
2699                    let _ = self.enter_scene_user_cmd_call(Some(&scn_name), &cmd_name, &[])?;
2700                } else if z_no >= 0 {
2701                    self.farcall_scene_name_ex(
2702                        &scn_name,
2703                        z_no as i32,
2704                        self.cfg.fm_void,
2705                        true,
2706                        &[],
2707                    )?;
2708                }
2709            }
2710            PendingButtonActionKind::Syscom {
2711                sys_type,
2712                sys_type_opt,
2713                mode,
2714            } => {
2715                self.run_pending_button_syscom_action(sys_type, sys_type_opt, mode)?;
2716            }
2717        }
2718        Ok(())
2719    }
2720
2721    fn syscom_proc_trace_enabled() -> bool {
2722        std::env::var_os("SG_SYSCOM_PROC_TRACE").is_some()
2723            || std::env::var_os("SG_DEBUG").is_some()
2724    }
2725
2726    fn syscom_trace_state(&self) -> String {
2727        let st = &self.ctx.globals.syscom;
2728        let msgbk_form = self.ctx.ids.form_global_msgbk;
2729        let msgbk_count = self
2730            .ctx
2731            .globals
2732            .msgbk_forms
2733            .get(&msgbk_form)
2734            .map(|m| m.history.len())
2735            .unwrap_or(0);
2736        let msgbk_visible_count = self
2737            .ctx
2738            .globals
2739            .msgbk_forms
2740            .get(&msgbk_form)
2741            .map(|m| {
2742                m.history
2743                    .iter()
2744                    .filter(|entry| {
2745                        entry.pct_flag
2746                            || !entry.msg_str.is_empty()
2747                            || !entry.disp_name.is_empty()
2748                            || !entry.original_name.is_empty()
2749                            || !entry.koe_no_list.is_empty()
2750                    })
2751                    .count()
2752            })
2753            .unwrap_or(0);
2754        format!(
2755            "read_skip={} auto_mode={} hide_mwnd={} msg_back_open={} msg_back_enable={} pending_proc={:?} msgbk_form={} msgbk_count={} msgbk_visible_count={} mwnd_waiting={} mwnd_visible_chars={} mwnd_wait_len={} msg_chars={}",
2756            st.read_skip.onoff,
2757            st.auto_mode.onoff,
2758            st.hide_mwnd.onoff,
2759            st.msg_back_open,
2760            st.msg_back.check_enabled(),
2761            st.pending_proc,
2762            msgbk_form,
2763            msgbk_count,
2764            msgbk_visible_count,
2765            self.ctx.ui.message_waiting(),
2766            self.ctx.ui.message_visible_chars(),
2767            self.ctx.ui.message_wait_message_len(),
2768            self.ctx.ui.message_text().unwrap_or("").chars().count()
2769        )
2770    }
2771
2772    fn syscom_button_op(sys_type: i64) -> Option<(i32, &'static str)> {
2773        use crate::runtime::forms::codes::syscom_op;
2774        Some(match sys_type {
2775            1 => (syscom_op::CALL_SAVE_MENU, "CALL_SAVE_MENU"),
2776            2 => (syscom_op::CALL_LOAD_MENU, "CALL_LOAD_MENU"),
2777            3 => (syscom_op::SET_READ_SKIP_ONOFF_FLAG, "SET_READ_SKIP_ONOFF_FLAG"),
2778            4 => (syscom_op::SET_AUTO_MODE_ONOFF_FLAG, "SET_AUTO_MODE_ONOFF_FLAG"),
2779            5 => (syscom_op::RETURN_TO_SEL, "RETURN_TO_SEL"),
2780            6 => (syscom_op::SET_HIDE_MWND_ONOFF_FLAG, "SET_HIDE_MWND_ONOFF_FLAG"),
2781            7 => (syscom_op::OPEN_MSG_BACK, "OPEN_MSG_BACK"),
2782            8 => (syscom_op::REPLAY_KOE, "REPLAY_KOE"),
2783            9 => (syscom_op::QUICK_SAVE, "QUICK_SAVE"),
2784            10 => (syscom_op::QUICK_LOAD, "QUICK_LOAD"),
2785            11 => (syscom_op::CALL_CONFIG_MENU, "CALL_CONFIG_MENU"),
2786            12 => (syscom_op::SET_LOCAL_EXTRA_SWITCH_ONOFF_FLAG, "SET_LOCAL_EXTRA_SWITCH_ONOFF_FLAG"),
2787            13 => (syscom_op::SET_LOCAL_EXTRA_MODE_VALUE, "SET_LOCAL_EXTRA_MODE_VALUE"),
2788            14 => (syscom_op::SET_GLOBAL_EXTRA_SWITCH_ONOFF, "SET_GLOBAL_EXTRA_SWITCH_ONOFF"),
2789            15 => (syscom_op::SET_GLOBAL_EXTRA_MODE_VALUE, "SET_GLOBAL_EXTRA_MODE_VALUE"),
2790            _ => return None,
2791        })
2792    }
2793
2794    fn dispatch_syscom_button_op(&mut self, op: i32, params: &[Value]) -> Result<bool> {
2795        // In this VM, form dispatch is rooted at the GLOBAL.SYSCOM element id
2796        // (normally 63), not at the FM_SYSCOM type id (1600).  A normal script
2797        // call reaches syscom.rs as the element chain [GLOBAL_SYSCOM, op].
2798        // Button actions must use the same root or global::dispatch_form()
2799        // never reaches syscom::dispatch(), and the trace shows handled=false.
2800        let form_id = if self.ctx.ids.form_global_syscom != 0 {
2801            self.ctx.ids.form_global_syscom as i32
2802        } else {
2803            constants::global_form::SYSCOM as i32
2804        };
2805
2806        let saved_call = self.ctx.vm_call.take();
2807        let saved_stack_len = self.ctx.stack.len();
2808        self.ctx.vm_call = Some(runtime::VmCallMeta {
2809            element: vec![form_id, op],
2810            al_id: 0,
2811            ret_form: self.cfg.fm_void as i64,
2812        });
2813
2814        let result = runtime::dispatch_form_code(&mut self.ctx, form_id as u32, params);
2815
2816        self.ctx.vm_call = saved_call;
2817        self.ctx.stack.truncate(saved_stack_len);
2818        result
2819    }
2820
2821    fn run_pending_button_syscom_action(
2822        &mut self,
2823        sys_type: i64,
2824        sys_type_opt: i64,
2825        mode: i64,
2826    ) -> Result<()> {
2827        let trace = Self::syscom_proc_trace_enabled();
2828        let Some((op, op_name)) = Self::syscom_button_op(sys_type) else {
2829            if trace {
2830                eprintln!(
2831                    "[SYSCOM_PROC_TRACE] button sys_type={} sys_opt={} mode={} resolved=UNKNOWN before {}",
2832                    sys_type,
2833                    sys_type_opt,
2834                    mode,
2835                    self.syscom_trace_state()
2836                );
2837            }
2838            return Ok(());
2839        };
2840
2841        if matches!(sys_type, 1 | 9) {
2842            crate::runtime::forms::syscom::prepare_runtime_save_thumb_capture(&mut self.ctx);
2843        }
2844
2845        // This follows C_elm_object::check_button_action() in the original engine:
2846        // system buttons call the SYSCOM operation directly.  The form dispatcher
2847        // expects CommandContext::vm_call to carry the current element chain, so
2848        // constructing Value::Element in the argument list is not enough here.
2849        // Button actions also ignore command return values, so any values pushed
2850        // by the generic SYSCOM command handler are discarded after dispatch.
2851        let params: Vec<Value> = match sys_type {
2852            1 | 2 => Vec::new(),
2853            3 | 4 => vec![Value::Int(if mode == 0 { 1 } else { 0 })],
2854            6 => vec![Value::Int(1)],
2855            5 => vec![Value::Int(1), Value::Int(1), Value::Int(1)],
2856            7 | 8 | 11 => Vec::new(),
2857            9 => vec![Value::Int(sys_type_opt), Value::Int(1), Value::Int(1)],
2858            10 => vec![
2859                Value::Int(sys_type_opt),
2860                Value::Int(1),
2861                Value::Int(1),
2862                Value::Int(1),
2863            ],
2864            12 | 14 => vec![
2865                Value::Int(sys_type_opt),
2866                Value::Int(if mode == 0 { 1 } else { 0 }),
2867            ],
2868            13 | 15 => vec![Value::Int(sys_type_opt), Value::Int(mode + 1)],
2869            _ => Vec::new(),
2870        };
2871        if trace {
2872            eprintln!(
2873                "[SYSCOM_PROC_TRACE] button sys_type={} sys_opt={} mode={} resolved={}({}) params={:?} before {}",
2874                sys_type,
2875                sys_type_opt,
2876                mode,
2877                op_name,
2878                op,
2879                params,
2880                self.syscom_trace_state()
2881            );
2882        }
2883        let dispatch_result = self.dispatch_syscom_button_op(op, &params);
2884        if trace {
2885            let status = match dispatch_result.as_ref() {
2886                Ok(handled) => format!("ok handled={}", handled),
2887                Err(err) => format!("err={}", err),
2888            };
2889            eprintln!(
2890                "[SYSCOM_PROC_TRACE] after resolved={}({}) status={} {}",
2891                op_name,
2892                op,
2893                status,
2894                self.syscom_trace_state()
2895            );
2896        }
2897        dispatch_result?;
2898        Ok(())
2899    }
2900
2901    fn drain_pending_button_actions(&mut self) -> Result<()> {
2902        let mut budget = 64usize;
2903        while !self.ctx.globals.pending_button_actions.is_empty() {
2904            if budget == 0 {
2905                bail!("button action queue did not drain");
2906            }
2907            budget -= 1;
2908            let pending = std::mem::take(&mut self.ctx.globals.pending_button_actions);
2909            for action in pending {
2910                self.run_pending_button_action(action)?;
2911            }
2912        }
2913        Ok(())
2914    }
2915
2916    pub fn process_pending_button_actions(&mut self) -> Result<()> {
2917        self.drain_pending_button_actions()
2918    }
2919
2920    fn drain_pending_frame_action_finishes(&mut self) -> Result<()> {
2921        let mut budget = 64usize;
2922        while !self.ctx.globals.pending_frame_action_finishes.is_empty() {
2923            if budget == 0 {
2924                bail!("frame action finish queue did not drain");
2925            }
2926            budget -= 1;
2927            let pending = std::mem::take(&mut self.ctx.globals.pending_frame_action_finishes);
2928            for item in pending {
2929                self.run_pending_frame_action_finish(item)?;
2930            }
2931        }
2932        Ok(())
2933    }
2934
2935    pub fn tick_frame(&mut self) -> Result<()> {
2936        let trace = std::env::var_os("SG_TICK_TRACE").is_some()
2937            || std::env::var_os("SG_FRAME_ACTION_TRACE").is_some();
2938        if trace {
2939            eprintln!(
2940                "[SG_TICK_TRACE] tick_frame start blocked={} halted={} scene={:?}",
2941                self.is_blocked(),
2942                self.halted,
2943                self.current_scene_name()
2944            );
2945        }
2946        self.drain_pending_button_actions()?;
2947        if self.ctx.globals.syscom.pending_proc.is_some() {
2948            if trace || std::env::var_os("SG_DEBUG").is_some() {
2949                eprintln!(
2950                    "[SG_DEBUG][SYSCOM_PROC] stop frame tick before frame actions pending_proc={:?}",
2951                    self.ctx.globals.syscom.pending_proc
2952                );
2953            }
2954            return Ok(());
2955        }
2956        if self.ctx.globals.script.frame_action_time_stop_flag && trace {
2957            eprintln!(
2958                "[SG_TICK_TRACE] frame_action_time_stop_flag set; executing callbacks with frozen frame-action time"
2959            );
2960        }
2961
2962        // C++ C_tnm_eng::frame() advances element time before frame_action_proc().
2963        // FRAME_ACTION callbacks read the already advanced counter value, then
2964        // C_elm_frame_action::frame() performs the end check later in the same
2965        // frame.  Keep the same order here; otherwise callbacks such as the MWND
2966        // rotating circle animation repeatedly observe the previous count.
2967        self.ctx.tick_frame();
2968
2969        let mut work: Vec<FrameActionWork> = Vec::new();
2970        let mut global_form_ids: Vec<u32> =
2971            self.ctx.globals.frame_actions.keys().copied().collect();
2972        global_form_ids.sort_unstable();
2973        for form_id in global_form_ids {
2974            let Some(fa) = self.ctx.globals.frame_actions.get(&form_id) else {
2975                continue;
2976            };
2977            if !fa.cmd_name.is_empty() {
2978                work.push(FrameActionWork {
2979                    stage_idx: -1,
2980                    obj_idx: usize::MAX,
2981                    ch_idx: None,
2982                    global_form_id: Some(form_id),
2983                    object_chain: None,
2984                    frame_action_chain: Some(vec![form_id as i32]),
2985                    scn_name: fa.scn_name.clone(),
2986                    cmd_name: fa.cmd_name.clone(),
2987                    args: fa.args.clone(),
2988                    count: fa.counter.get_count(),
2989                    end_time: fa.end_time,
2990                });
2991            }
2992        }
2993        let mut global_list_form_ids: Vec<u32> = self
2994            .ctx
2995            .globals
2996            .frame_action_lists
2997            .keys()
2998            .copied()
2999            .collect();
3000        global_list_form_ids.sort_unstable();
3001        for form_id in global_list_form_ids {
3002            let Some(list) = self.ctx.globals.frame_action_lists.get(&form_id) else {
3003                continue;
3004            };
3005            for (idx, fa) in list.iter().enumerate() {
3006                if !fa.cmd_name.is_empty() {
3007                    work.push(FrameActionWork {
3008                        stage_idx: -1,
3009                        obj_idx: usize::MAX,
3010                        ch_idx: Some(idx),
3011                        global_form_id: Some(form_id),
3012                        object_chain: None,
3013                        frame_action_chain: Some(vec![
3014                            form_id as i32,
3015                            crate::runtime::forms::codes::ELM_ARRAY,
3016                            idx as i32,
3017                        ]),
3018                        scn_name: fa.scn_name.clone(),
3019                        cmd_name: fa.cmd_name.clone(),
3020                        args: fa.args.clone(),
3021                        count: fa.counter.get_count(),
3022                        end_time: fa.end_time,
3023                    });
3024                }
3025            }
3026        }
3027        let mut stage_form_ids: Vec<u32> = self.ctx.globals.stage_forms.keys().copied().collect();
3028        stage_form_ids.sort_unstable();
3029        for form_id in stage_form_ids {
3030            let Some(st) = self.ctx.globals.stage_forms.get(&form_id) else {
3031                continue;
3032            };
3033            let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
3034            stage_ids.sort_unstable();
3035            for stage_idx in stage_ids {
3036                let Some(objs) = st.object_lists.get(&stage_idx) else {
3037                    continue;
3038                };
3039                for (obj_idx, obj) in objs.iter().enumerate() {
3040                    if st.is_embedded_object_slot(stage_idx, obj_idx) {
3041                        continue;
3042                    }
3043                    let object_chain = vec![
3044                        self.ctx.ids.form_global_stage as i32,
3045                        self.ctx.ids.elm_array,
3046                        stage_idx as i32,
3047                        self.ctx.ids.stage_elm_object,
3048                        self.ctx.ids.elm_array,
3049                        obj_idx as i32,
3050                    ];
3051                    Self::collect_object_frame_action_work_recursive(
3052                        obj,
3053                        stage_idx,
3054                        obj_idx,
3055                        object_chain,
3056                        &mut work,
3057                    );
3058                }
3059            }
3060
3061            let mut mwnd_stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
3062            mwnd_stage_ids.sort_unstable();
3063            for stage_idx in mwnd_stage_ids {
3064                let Some(mwnds) = st.mwnd_lists.get(&stage_idx) else {
3065                    continue;
3066                };
3067                for (mwnd_idx, mwnd) in mwnds.iter().enumerate() {
3068                    for (obj_idx, obj) in mwnd.button_list.iter().enumerate() {
3069                        let object_chain = vec![
3070                            self.ctx.ids.form_global_stage as i32,
3071                            self.ctx.ids.elm_array,
3072                            stage_idx as i32,
3073                            crate::runtime::forms::codes::elm_value::STAGE_MWND,
3074                            self.ctx.ids.elm_array,
3075                            mwnd_idx as i32,
3076                            crate::runtime::forms::codes::elm_value::MWND_BUTTON,
3077                            self.ctx.ids.elm_array,
3078                            obj_idx as i32,
3079                        ];
3080                        Self::collect_object_frame_action_work_recursive(
3081                            obj,
3082                            stage_idx,
3083                            obj_idx,
3084                            object_chain,
3085                            &mut work,
3086                        );
3087                    }
3088                    for (obj_idx, obj) in mwnd.face_list.iter().enumerate() {
3089                        let object_chain = vec![
3090                            self.ctx.ids.form_global_stage as i32,
3091                            self.ctx.ids.elm_array,
3092                            stage_idx as i32,
3093                            crate::runtime::forms::codes::elm_value::STAGE_MWND,
3094                            self.ctx.ids.elm_array,
3095                            mwnd_idx as i32,
3096                            crate::runtime::forms::codes::elm_value::MWND_FACE,
3097                            self.ctx.ids.elm_array,
3098                            obj_idx as i32,
3099                        ];
3100                        Self::collect_object_frame_action_work_recursive(
3101                            obj,
3102                            stage_idx,
3103                            obj_idx,
3104                            object_chain,
3105                            &mut work,
3106                        );
3107                    }
3108                    for (obj_idx, obj) in mwnd.object_list.iter().enumerate() {
3109                        let object_chain = vec![
3110                            self.ctx.ids.form_global_stage as i32,
3111                            self.ctx.ids.elm_array,
3112                            stage_idx as i32,
3113                            crate::runtime::forms::codes::elm_value::STAGE_MWND,
3114                            self.ctx.ids.elm_array,
3115                            mwnd_idx as i32,
3116                            crate::runtime::forms::codes::elm_value::MWND_OBJECT,
3117                            self.ctx.ids.elm_array,
3118                            obj_idx as i32,
3119                        ];
3120                        Self::collect_object_frame_action_work_recursive(
3121                            obj,
3122                            stage_idx,
3123                            obj_idx,
3124                            object_chain,
3125                            &mut work,
3126                        );
3127                    }
3128                }
3129            }
3130        }
3131        if trace {
3132            eprintln!("[SG_TICK_TRACE] frame_action work items={}", work.len());
3133        }
3134        // C++ drives FRAME_ACTION from the engine proc loop independently of the
3135        // current script call depth. Do not stall object callbacks while a user
3136        // command/excall is active; MWND child setup relies on these callbacks.
3137        for item in work {
3138            if trace {
3139                eprintln!(
3140                    "[SG_TICK_TRACE] invoke stage={} obj={} ch={:?} global={:?} cmd={} count={} end_time={} args={:?}",
3141                    item.stage_idx,
3142                    item.obj_idx,
3143                    item.ch_idx,
3144                    item.global_form_id,
3145                    item.cmd_name,
3146                    item.count,
3147                    item.end_time,
3148                    item.args
3149                );
3150            }
3151            // Original order is C_tnm_eng::frame_action_proc() do_action() first,
3152            // then C_elm_frame_action::frame() checks for finish later in the frame.
3153            // This matters for end_time == 0 and for callbacks that replace their own
3154            // frame action.
3155            let call_args = Self::make_frame_action_call_args(
3156                item.frame_action_chain.as_ref(),
3157                item.object_chain.as_ref(),
3158                &item.args,
3159            );
3160            let (prev_target, prev_chain) = self.set_frame_action_current_object(&item);
3161            if let Err(e) = self.run_scene_user_cmd_inline(
3162                Some(&item.scn_name),
3163                &item.cmd_name,
3164                &call_args,
3165                self.cfg.fm_void,
3166                true,
3167            ) {
3168                self.ctx.unknown.record_note(&format!(
3169                    "frame_action.call.failed:{}:{}:{e}",
3170                    item.scn_name, item.cmd_name
3171                ));
3172            }
3173            self.restore_frame_action_current_object(prev_target, prev_chain);
3174
3175            if let Some((finish_cmd_name, finish_args)) = self.begin_frame_action_finish(&item) {
3176                if trace {
3177                    eprintln!(
3178                        "[SG_TICK_TRACE] finish stage={} obj={} ch={:?} global={:?} cmd={} args={:?}",
3179                        item.stage_idx,
3180                        item.obj_idx,
3181                        item.ch_idx,
3182                        item.global_form_id,
3183                        finish_cmd_name,
3184                        finish_args
3185                    );
3186                }
3187                let finish_call_args = Self::make_frame_action_call_args(
3188                    item.frame_action_chain.as_ref(),
3189                    item.object_chain.as_ref(),
3190                    &finish_args,
3191                );
3192                let (prev_target, prev_chain) = self.set_frame_action_current_object(&item);
3193                if let Err(e) = self.run_scene_user_cmd_inline(
3194                    Some(&item.scn_name),
3195                    &finish_cmd_name,
3196                    &finish_call_args,
3197                    self.cfg.fm_void,
3198                    true,
3199                ) {
3200                    self.ctx.unknown.record_note(&format!(
3201                        "frame_action.finish_call.failed:{}:{}:{e}",
3202                        item.scn_name, finish_cmd_name
3203                    ));
3204                }
3205                self.restore_frame_action_current_object(prev_target, prev_chain);
3206                self.end_frame_action_finish(&item);
3207            }
3208        }
3209        if trace {
3210            eprintln!(
3211                "[SG_TICK_TRACE] after frame-action callbacks blocked={} halted={}",
3212                self.is_blocked(),
3213                self.halted
3214            );
3215        }
3216        self.script_input_synced_this_frame = false;
3217        Ok(())
3218    }
3219
3220    pub fn restart_scene_name(&mut self, scene_name: &str, z_no: i32) -> Result<()> {
3221        let (stream, scene_no) = self.load_scene_stream(scene_name, z_no)?;
3222        self.ctx.reset_for_scene_restart();
3223        self.stream = stream;
3224        self.int_stack.clear();
3225        self.str_stack.clear();
3226        self.element_points.clear();
3227        self.call_stack.clear();
3228        self.call_stack.push(self.scene_base_call());
3229        self.gosub_return_stack.clear();
3230        self.user_props.clear();
3231        self.scene_stack.clear();
3232        self.save_point = None;
3233        self.ctx.local_save_snapshot = None;
3234        self.sel_point_stack.clear();
3235        self.current_scene_no = Some(scene_no);
3236        self.current_scene_name = Some(scene_name.to_string());
3237        self.current_line_no = -1;
3238        self.ctx.current_scene_no = Some(scene_no as i64);
3239        self.ctx.current_scene_name = Some(scene_name.to_string());
3240        self.ctx.current_line_no = -1;
3241        self.halted = false;
3242        self.delayed_ret_form = None;
3243        Ok(())
3244    }
3245
3246    fn make_resume_point(&self) -> VmResumePoint<'a> {
3247        VmResumePoint {
3248            stream: self.stream.clone(),
3249            user_cmd_names: self.user_cmd_names.clone(),
3250            call_cmd_names: self.call_cmd_names.clone(),
3251            int_stack: self.int_stack.clone(),
3252            str_stack: self.str_stack.clone(),
3253            element_points: self.element_points.clone(),
3254            call_stack: self.call_stack.clone(),
3255            gosub_return_stack: self.gosub_return_stack.clone(),
3256            user_props: self.user_props.clone(),
3257            current_scene_no: self.current_scene_no,
3258            current_scene_name: self.current_scene_name.clone(),
3259            current_line_no: self.current_line_no,
3260            globals: self.ctx.globals.clone(),
3261        }
3262    }
3263
3264    fn restore_resume_point(&mut self, point: VmResumePoint<'a>) {
3265        self.stream = point.stream;
3266        self.user_cmd_names = point.user_cmd_names;
3267        self.call_cmd_names = point.call_cmd_names;
3268        self.int_stack = point.int_stack;
3269        self.str_stack = point.str_stack;
3270        self.element_points = point.element_points;
3271        self.call_stack = point.call_stack;
3272        self.gosub_return_stack = point.gosub_return_stack;
3273        self.user_props = point.user_props;
3274        self.current_scene_no = point.current_scene_no;
3275        self.current_scene_name = point.current_scene_name;
3276        self.current_line_no = point.current_line_no;
3277        let mut restored_globals = point.globals;
3278        restored_globals.syscom.pending_proc = None;
3279        restored_globals.syscom.menu_open = false;
3280        restored_globals.syscom.menu_kind = None;
3281        restored_globals.syscom.msg_back_open = false;
3282        self.ctx.globals = restored_globals;
3283        self.ctx.current_scene_no = self.current_scene_no.map(|v| v as i64);
3284        self.ctx.current_scene_name = self.current_scene_name.clone();
3285        self.ctx.current_line_no = self.current_line_no as i64;
3286        self.ctx.wait = runtime::wait::VmWait::default();
3287        self.ctx.stack.clear();
3288        self.halted = false;
3289        self.delayed_ret_form = None;
3290    }
3291
3292    pub fn has_sel_point(&self) -> bool {
3293        !self.sel_point_stack.is_empty()
3294    }
3295
3296    pub fn restore_last_sel_point(&mut self) -> bool {
3297        let Some(point) = self.sel_point_stack.last().cloned() else {
3298            return false;
3299        };
3300        self.restore_resume_point(point);
3301        true
3302    }
3303
3304    pub fn step(&mut self) -> Result<bool> {
3305        self.step_inner(true)
3306    }
3307
3308    /// Reset the infinite-loop guard for one C++-style frame_main_proc pass.
3309    /// This is not a scheduling quota; it only preserves SIGLUS_VM_MAX_STEPS
3310    /// as a hard error if a script never reaches a proc/wait boundary.
3311    pub fn begin_script_proc_pump(&mut self) {
3312        self.steps = 0;
3313    }
3314
3315    /// Execute one standalone SCRIPT proc pass. Direct callers get a fresh
3316    /// infinite-loop guard, while the winit shell uses run_script_proc_continue()
3317    /// inside its C++-style frame_main_proc loop.
3318    pub fn run_script_proc(&mut self) -> Result<bool> {
3319        self.begin_script_proc_pump();
3320        self.run_script_proc_continue()
3321    }
3322
3323    /// Execute the current SCRIPT proc the same way the original engine's
3324    /// `tnm_proc_script()` does: keep stepping while the current proc is SCRIPT,
3325    /// and return only when a command changes the proc, enters a wait, returns,
3326    /// or stops the VM. There is no per-frame instruction quota here.
3327    pub fn run_script_proc_continue(&mut self) -> Result<bool> {
3328        if self.halted {
3329            return Ok(false);
3330        }
3331        if self.is_blocked() {
3332            return Ok(true);
3333        }
3334
3335        if !self.script_input_synced_this_frame {
3336            self.ctx.sync_script_input_from_runtime();
3337            self.ctx.input.next_frame();
3338            self.script_input_synced_this_frame = true;
3339        }
3340
3341        loop {
3342            let proc_generation_before = self.ctx.proc_generation();
3343            let running = self.step_inner(true)?;
3344            if !running || self.halted {
3345                return Ok(running);
3346            }
3347
3348            if self.is_blocked() {
3349                return Ok(true);
3350            }
3351            if self.ctx.proc_generation() != proc_generation_before {
3352                return Ok(true);
3353            }
3354        }
3355    }
3356
3357    #[allow(dead_code)]
3358    fn step_inner(&mut self, respect_wait: bool) -> Result<bool> {
3359        self.yield_safe_after_step = false;
3360        if self.halted {
3361            return Ok(false);
3362        }
3363
3364        // Normal scene execution is blocked by WAIT / WAIT_KEY.
3365        // Frame-action inline callbacks bypass this outer wait guard so the
3366        // frame phase can run object callbacks while the main script is waiting.
3367        if respect_wait {
3368            let blocked = self.ctx.wait_poll();
3369            if blocked {
3370                return Ok(true);
3371            }
3372        }
3373
3374        // If the main script yielded a delayed return, materialize it only when
3375        // the normal script pump resumes. Frame-action callbacks intentionally
3376        // bypass the outer wait guard, so letting them take this value would
3377        // resume the blocked statement before the C++ wait proc has produced
3378        // its return value.
3379        if respect_wait {
3380            let delayed = self
3381                .call_stack
3382                .last_mut()
3383                .and_then(|frame| frame.delayed_ret_form.take());
3384            if let Some(rf) = delayed {
3385                if self.ctx.stack.is_empty() {
3386                    self.push_default_for_ret(rf);
3387                } else {
3388                    self.take_ctx_return(rf)?;
3389                }
3390            }
3391        }
3392
3393        if self.cfg.max_steps > 0 && self.steps >= self.cfg.max_steps {
3394            let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
3395            let scene_no = self
3396                .current_scene_no
3397                .map(|v| v.to_string())
3398                .unwrap_or_else(|| "-".to_string());
3399            bail!(
3400                "VM reached SIGLUS_VM_MAX_STEPS={} (possible infinite loop) scene={} scene_no={} line={} pc=0x{:x}",
3401                self.cfg.max_steps,
3402                scene,
3403                scene_no,
3404                self.current_line_no,
3405                self.stream.get_prg_cntr()
3406            );
3407        }
3408        self.steps += 1;
3409
3410        let pc_before = self.stream.get_prg_cntr();
3411        let opcode = match self.stream.pop_u8() {
3412            Ok(v) => v,
3413            Err(_) => {
3414                if self.return_from_scene(Vec::new())? {
3415                    return Ok(true);
3416                }
3417                self.halted = true;
3418                return Ok(false);
3419            }
3420        };
3421
3422        self.vm_trace_opcode(pc_before, opcode, "before");
3423
3424        match opcode {
3425            CD_NL => {
3426                let line_no = self.stream.pop_i32()?;
3427                self.current_line_no = line_no;
3428                self.ctx.current_line_no = line_no as i64;
3429                self.yield_safe_after_step = true;
3430                // Compact element continuation is statement-local, but some title/menu code
3431                // keeps a base object element alive across `NL` boundaries and continues to use
3432                // compact child/property syntax on the following line. So we only drop the
3433                // continuation context when there is no live element on the VM stack anymore.
3434                if self.element_points.is_empty() {
3435                    self.ctx.globals.current_object_chain = None;
3436                    self.ctx.globals.current_stage_object = None;
3437                }
3438            }
3439
3440            CD_PUSH => {
3441                let form_code = self.stream.pop_i32()?;
3442                self.exec_push(form_code)?;
3443            }
3444            CD_POP => {
3445                let form_code = self.stream.pop_i32()?;
3446                self.exec_pop(form_code)?;
3447            }
3448            CD_COPY => {
3449                let form_code = self.stream.pop_i32()?;
3450                self.exec_copy(form_code)?;
3451            }
3452
3453            CD_ELM_POINT => {
3454                self.element_points.push(self.int_stack.len());
3455                self.vm_trace(
3456                    None,
3457                    format!("ELM_POINT push start={} ", self.int_stack.len()),
3458                );
3459            }
3460            CD_COPY_ELM => {
3461                self.exec_copy_element()?;
3462            }
3463
3464            CD_PROPERTY => {
3465                let elm = self.pop_element()?;
3466                self.vm_trace(None, format!("CD_PROPERTY elm={:?}", elm));
3467                self.exec_property(elm)?;
3468            }
3469            CD_DEC_PROP => {
3470                let form_code = self.stream.pop_i32()?;
3471                let prop_id = self.stream.pop_i32()?;
3472
3473                let size = if form_code == self.cfg.fm_intlist || form_code == self.cfg.fm_strlist {
3474                    self.pop_int()?.max(0) as usize
3475                } else {
3476                    0usize
3477                };
3478                let prop_element = vec![constants::elm::create(
3479                    constants::elm::OWNER_CALL_PROP,
3480                    0,
3481                    prop_id,
3482                )];
3483                let value = if form_code == self.cfg.fm_int {
3484                    CallPropValue::Int(0)
3485                } else if form_code == self.cfg.fm_str {
3486                    CallPropValue::Str(String::new())
3487                } else if form_code == self.cfg.fm_intlist {
3488                    CallPropValue::IntList(vec![0; size])
3489                } else if form_code == self.cfg.fm_strlist {
3490                    CallPropValue::StrList(vec![String::new(); size])
3491                } else {
3492                    CallPropValue::Element(prop_element.clone())
3493                };
3494
3495                let frame = self
3496                    .call_stack
3497                    .last_mut()
3498                    .ok_or_else(|| anyhow!("call stack underflow"))?;
3499                frame.user_props.push(CallProp {
3500                    prop_id,
3501                    form: form_code,
3502                    decl_size: size,
3503                    element: prop_element,
3504                    value,
3505                });
3506            }
3507            CD_ARG => {
3508                // Expand stack arguments into the current call's declared properties
3509                // (tnm_expand_arg_into_call_flag).
3510                let (frame_action_proc, actual_arg_cnt, forms): (bool, usize, Vec<i32>) = {
3511                    let frame = self
3512                        .call_stack
3513                        .last()
3514                        .ok_or_else(|| anyhow!("call stack underflow"))?;
3515                    (
3516                        frame.frame_action_proc,
3517                        frame.arg_cnt,
3518                        frame.user_props.iter().map(|p| p.form).collect(),
3519                    )
3520                };
3521
3522                if frame_action_proc {
3523                    if forms.first().copied() != Some(crate::runtime::forms::codes::FM_FRAMEACTION)
3524                    {
3525                        bail!("frame_action CD_ARG requires first argument to be FM_FRAMEACTION");
3526                    }
3527                    if actual_arg_cnt != forms.len() {
3528                        bail!(
3529                            "frame_action CD_ARG argument count mismatch: declared={} actual={}",
3530                            forms.len(),
3531                            actual_arg_cnt
3532                        );
3533                    }
3534                }
3535
3536                // Pop values in reverse order to match the original stack layout.
3537                let mut values: Vec<CallPropValue> = Vec::with_capacity(forms.len());
3538                for &form in forms.iter().rev() {
3539                    let v = if form == self.cfg.fm_int {
3540                        CallPropValue::Int(self.pop_int()?)
3541                    } else if form == self.cfg.fm_str {
3542                        CallPropValue::Str(self.pop_str()?)
3543                    } else {
3544                        CallPropValue::Element(self.pop_element()?)
3545                    };
3546                    values.push(v);
3547                }
3548                values.reverse();
3549
3550                let frame = self
3551                    .call_stack
3552                    .last_mut()
3553                    .ok_or_else(|| anyhow!("call stack underflow"))?;
3554                for (prop, v) in frame.user_props.iter_mut().zip(values.into_iter()) {
3555                    match (&v, prop.form) {
3556                        (CallPropValue::Int(_), f) if f == self.cfg.fm_int => {
3557                            prop.value = v;
3558                        }
3559                        (CallPropValue::Str(_), f) if f == self.cfg.fm_str => {
3560                            prop.value = v;
3561                        }
3562                        (CallPropValue::Element(e), _) => {
3563                            // C++ tnm_expand_arg_into_call_flag() writes all non-int/str
3564                            // arguments into user_prop_list[i].element directly.
3565                            prop.element = e.clone();
3566                            if matches!(
3567                                prop.form,
3568                                crate::runtime::forms::codes::FM_INTREF
3569                                    | crate::runtime::forms::codes::FM_STRREF
3570                                    | crate::runtime::forms::codes::FM_INTLISTREF
3571                                    | crate::runtime::forms::codes::FM_STRLISTREF
3572                                    | crate::runtime::forms::codes::FM_LIST
3573                            ) {
3574                                prop.value = CallPropValue::Element(e.clone());
3575                            }
3576                        }
3577                        _ => {
3578                            prop.value = v;
3579                        }
3580                    }
3581                }
3582            }
3583
3584            CD_GOTO => {
3585                let label_no = self.stream.pop_i32()?;
3586                self.sg_omv_trace(format!("GOTO label={} taken=true", label_no));
3587                self.stream.jump_to_label(label_no.max(0) as usize)?;
3588            }
3589            CD_GOTO_TRUE => {
3590                let label_no = self.stream.pop_i32()?;
3591                let before_tail_start = self.int_stack.len().saturating_sub(16);
3592                let before_tail = self.int_stack[before_tail_start..].to_vec();
3593                let cond = self.pop_int()?;
3594                let taken = cond != 0;
3595                self.sg_omv_trace(format!("GOTO_TRUE label={} cond={} taken={}", label_no, cond, taken));
3596                self.trace_cf_branch_goto(pc_before, "GOTO_TRUE", label_no, cond, taken, &before_tail);
3597                if taken {
3598                    self.stream.jump_to_label(label_no.max(0) as usize)?;
3599                }
3600            }
3601            CD_GOTO_FALSE => {
3602                let label_no = self.stream.pop_i32()?;
3603                let before_tail_start = self.int_stack.len().saturating_sub(16);
3604                let before_tail = self.int_stack[before_tail_start..].to_vec();
3605                let cond = self.pop_int()?;
3606                let taken = cond == 0;
3607                self.sg_omv_trace(format!("GOTO_FALSE label={} cond={} taken={}", label_no, cond, taken));
3608                self.trace_cf_branch_goto(pc_before, "GOTO_FALSE", label_no, cond, taken, &before_tail);
3609                if taken {
3610                    self.stream.jump_to_label(label_no.max(0) as usize)?;
3611                }
3612            }
3613            CD_GOSUB => {
3614                let label_no = self.stream.pop_i32()?;
3615                let _args = self.pop_arg_list()?;
3616                let return_pc = self.stream.get_prg_cntr();
3617                self.vm_trace(
3618                    Some(pc_before),
3619                    format!("GOSUB label={} return_pc=0x{return_pc:x}", label_no),
3620                );
3621
3622                // Save return info on the caller frame .
3623                let caller = self
3624                    .call_stack
3625                    .last_mut()
3626                    .ok_or_else(|| anyhow!("call stack underflow"))?;
3627                caller.return_pc = return_pc;
3628                caller.ret_form = self.cfg.fm_int;
3629                self.gosub_return_stack.push((return_pc, self.cfg.fm_int));
3630
3631                // Enter callee context.
3632                let scratch_args = self.call_scratch_from_args(&_args);
3633                let mut callee = self.make_call_frame(
3634                    self.cfg.fm_void,
3635                    false,
3636                    false,
3637                    _args.len(),
3638                    Some(scratch_args),
3639                );
3640                callee.return_override = Some((return_pc, self.cfg.fm_int));
3641                self.call_stack.push(callee);
3642
3643                self.stream.jump_to_label(label_no.max(0) as usize)?;
3644            }
3645            CD_GOSUBSTR => {
3646                let label_no = self.stream.pop_i32()?;
3647                let _args = self.pop_arg_list()?;
3648                let return_pc = self.stream.get_prg_cntr();
3649                self.vm_trace(
3650                    Some(pc_before),
3651                    format!("GOSUBSTR label={} return_pc=0x{return_pc:x}", label_no),
3652                );
3653
3654                let caller = self
3655                    .call_stack
3656                    .last_mut()
3657                    .ok_or_else(|| anyhow!("call stack underflow"))?;
3658                caller.return_pc = return_pc;
3659                caller.ret_form = self.cfg.fm_str;
3660                self.gosub_return_stack.push((return_pc, self.cfg.fm_str));
3661
3662                let scratch_args = self.call_scratch_from_args(&_args);
3663                let mut callee = self.make_call_frame(
3664                    self.cfg.fm_void,
3665                    false,
3666                    false,
3667                    _args.len(),
3668                    Some(scratch_args),
3669                );
3670                callee.return_override = Some((return_pc, self.cfg.fm_str));
3671                self.call_stack.push(callee);
3672
3673                self.stream.jump_to_label(label_no.max(0) as usize)?;
3674            }
3675            CD_RETURN => {
3676                let args = self.pop_arg_list()?;
3677                self.sg_omv_trace(format!("RETURN argc={} args={:?} call_depth={} scene_stack={}", args.len(), args, self.call_stack.len(), self.scene_stack.len()));
3678                if self.call_stack.len() == 1 {
3679                    if self.return_from_scene(args.clone())? {
3680                        return Ok(true);
3681                    }
3682                    self.halted = true;
3683                    return Ok(false);
3684                }
3685                if self.exec_return(args)? {
3686                    return Ok(false);
3687                }
3688            }
3689
3690            CD_ASSIGN => {
3691                let _left_form = self.stream.pop_i32()?;
3692                let right_form = self.stream.pop_i32()?;
3693                let al_id = self.stream.pop_i32()?;
3694                let rhs = self.pop_value_for_form(right_form)?;
3695                let elm = self.pop_element()?;
3696                self.exec_assign(elm, al_id, rhs)?;
3697            }
3698
3699            CD_OPERATE_1 => {
3700                let form_code = self.stream.pop_i32()?;
3701                let opr = self.stream.pop_u8()?;
3702                self.exec_operate_1(form_code, opr)?;
3703            }
3704            CD_OPERATE_2 => {
3705                let form_l = self.stream.pop_i32()?;
3706                let form_r = self.stream.pop_i32()?;
3707                let opr = self.stream.pop_u8()?;
3708                self.exec_operate_2(form_l, form_r, opr)?;
3709            }
3710            CD_COMMAND => {
3711                // CD_COMMAND reads: arg_list_id, arg_list, element, named_arg_cnt, named_arg_ids..., ret_form
3712                let arg_list_id = self.stream.pop_i32()?;
3713                let mut args = self.pop_arg_list()?;
3714                let elm = self.pop_element()?;
3715
3716                let named_arg_cnt = self.stream.pop_i32()?;
3717                if named_arg_cnt < 0 {
3718                    bail!("negative named_arg_cnt={named_arg_cnt}");
3719                }
3720
3721                let mut named_ids: Vec<i32> = Vec::with_capacity(named_arg_cnt as usize);
3722                for _ in 0..(named_arg_cnt as usize) {
3723                    named_ids.push(self.stream.pop_i32()?);
3724                }
3725
3726                if !named_ids.is_empty() {
3727                    let n = named_ids.len().min(args.len());
3728                    for a in 0..n {
3729                        let idx = args.len() - 1 - a;
3730                        let id = named_ids[a];
3731                        let v = std::mem::replace(&mut args[idx], crate::runtime::Value::Int(0));
3732                        args[idx] = crate::runtime::Value::NamedArg {
3733                            id,
3734                            value: Box::new(v),
3735                        };
3736                    }
3737                }
3738
3739                let ret_form = self.stream.pop_i32()?;
3740                if let Some(raw_head) = elm.first().copied() {
3741                    let form_id = self.canonical_runtime_form_id(raw_head as u32) as i32;
3742                    let op_id = if elm.len() >= 2 { elm[1] } else { arg_list_id };
3743                    self.sg_omv_trace_command("CD_COMMAND", &elm, form_id, op_id, arg_list_id, ret_form, &args);
3744                }
3745                let _ = self.ctx.take_read_flag_no_request();
3746                let block_generation = self.ctx.wait.block_generation();
3747                let proc_generation = self.ctx.proc_generation();
3748                self.exec_command(elm, arg_list_id, ret_form, &mut args)?;
3749                self.drain_runtime_save_load_requests()?;
3750                if self.ctx.take_read_flag_no_request() {
3751                    let read_flag_no = self.stream.pop_i32()?;
3752                    self.ctx.submit_read_flag_no(read_flag_no);
3753                }
3754                if self.ctx.proc_generation() != proc_generation {
3755                    return Ok(true);
3756                }
3757                if respect_wait
3758                    && self.ctx.wait.block_generation() != block_generation
3759                    && self.ctx.wait_poll()
3760                {
3761                    return Ok(true);
3762                }
3763            }
3764            CD_TEXT => {
3765                let rf_flag_no = self.stream.pop_i32()?;
3766                let text = self.pop_str()?;
3767                if !crate::runtime::forms::stage::cd_text_current_mwnd(
3768                    &mut self.ctx,
3769                    &text,
3770                    rf_flag_no as i64,
3771                ) {
3772                    self.ctx.ui.set_message(text);
3773                }
3774            }
3775            CD_NAME => {
3776                let name = self.pop_str()?;
3777                if !crate::runtime::forms::stage::cd_name_current_mwnd(&mut self.ctx, &name) {
3778                    self.ctx.ui.set_name(name);
3779                }
3780            }
3781            CD_SEL_BLOCK_START => {
3782                // Selection blocks are handled by higher-level UI commands.
3783                // Keep a marker to avoid breaking control flow.
3784            }
3785            CD_SEL_BLOCK_END => {
3786                // The original VM leaves a result on the int stack for certain selection constructs.
3787                // Default to 0 (first choice) if scripts expect a value.
3788                self.push_int(0);
3789            }
3790
3791            CD_EOF => {
3792                if self.return_from_scene(Vec::new())? {
3793                    return Ok(true);
3794                }
3795                self.halted = true;
3796                return Ok(false);
3797            }
3798
3799            CD_NONE => {
3800                // In the original engine this is treated as a fatal script error.
3801                // Stop execution and record it.
3802                *self.unknown_opcodes.entry(opcode).or_insert(0) += 1;
3803                let scn_cmd_context = self.vm_scn_cmd_context(pc_before);
3804                eprintln!(
3805                    "VM hit CD_NONE scene={} line={} pc=0x{:x} {} bytes={:02x?}; stopping",
3806                    self.current_scene_name.as_deref().unwrap_or("<none>"),
3807                    self.current_line_no,
3808                    pc_before,
3809                    scn_cmd_context,
3810                    &self.stream.scn[pc_before.saturating_sub(8)..self.stream.scn.len().min(pc_before + 16)]
3811                );
3812                self.halted = true;
3813                return Ok(false);
3814            }
3815
3816            other => {
3817                *self.unknown_opcodes.entry(other).or_insert(0) += 1;
3818                println!(
3819                    "VM unknown opcode=0x{other:02x} at pc=0x{:x}; stopping",
3820                    pc_before
3821                );
3822                self.halted = true;
3823                return Ok(false);
3824            }
3825        }
3826
3827        Ok(true)
3828    }
3829
3830    pub fn run(&mut self) -> Result<()> {
3831        while self.step()? {}
3832        Ok(())
3833    }
3834
3835    // ---------------------------------------------------------------------
3836    // Stack helpers
3837    // ---------------------------------------------------------------------
3838
3839    fn push_int(&mut self, v: i32) {
3840        self.int_stack.push(v);
3841        self.vm_trace(None, format!("push_int {}", v));
3842    }
3843
3844    fn pop_int(&mut self) -> Result<i32> {
3845        match self.int_stack.pop() {
3846            Some(v) => {
3847                self.vm_trace(None, format!("pop_int -> {}", v));
3848                Ok(v)
3849            }
3850            None => {
3851                self.vm_trace(None, "pop_int underflow");
3852                Err(anyhow!(
3853                    "int stack underflow: scene={} scene_no={} line={} pc=0x{:x}",
3854                    self.current_scene_name.as_deref().unwrap_or("<none>"),
3855                    self.current_scene_no
3856                        .map(|v| v.to_string())
3857                        .unwrap_or_else(|| "-".to_string()),
3858                    self.current_line_no,
3859                    self.stream.get_prg_cntr()
3860                ))
3861            }
3862        }
3863    }
3864
3865    fn peek_int(&self) -> Result<i32> {
3866        self.int_stack
3867            .last()
3868            .copied()
3869            .ok_or_else(|| anyhow!("int stack underflow"))
3870    }
3871
3872    fn push_str(&mut self, s: String) {
3873        let preview = if s.chars().count() > 48 {
3874            let mut tmp = s.chars().take(48).collect::<String>();
3875            tmp.push('…');
3876            tmp
3877        } else {
3878            s.clone()
3879        };
3880        self.str_stack.push(s);
3881        self.vm_trace(None, format!("push_str {:?}", preview));
3882    }
3883
3884    fn pop_str(&mut self) -> Result<String> {
3885        match self.str_stack.pop() {
3886            Some(v) => {
3887                let preview = if v.chars().count() > 48 {
3888                    let mut tmp = v.chars().take(48).collect::<String>();
3889                    tmp.push('…');
3890                    tmp
3891                } else {
3892                    v.clone()
3893                };
3894                self.vm_trace(None, format!("pop_str -> {:?}", preview));
3895                Ok(v)
3896            }
3897            None => {
3898                self.vm_trace(None, "pop_str underflow");
3899                Err(anyhow!("str stack underflow"))
3900            }
3901        }
3902    }
3903
3904    fn peek_str(&self) -> Result<String> {
3905        self.str_stack
3906            .last()
3907            .cloned()
3908            .ok_or_else(|| anyhow!("str stack underflow"))
3909    }
3910
3911    fn push_element(&mut self, elm: Vec<i32>) {
3912        self.element_points.push(self.int_stack.len());
3913        self.int_stack.extend_from_slice(&elm);
3914        self.vm_trace(None, format!("push_element {:?}", elm));
3915    }
3916
3917    fn pop_element(&mut self) -> Result<Vec<i32>> {
3918        let start = match self.element_points.pop() {
3919            Some(v) => v,
3920            None => {
3921                self.vm_trace(None, "pop_element underflow (missing ELM_POINT)");
3922                return Err(anyhow!("element stack underflow (missing ELM_POINT)"));
3923            }
3924        };
3925        if start > self.int_stack.len() {
3926            self.vm_trace(
3927                None,
3928                format!(
3929                    "pop_element invalid start={} len={}",
3930                    start,
3931                    self.int_stack.len()
3932                ),
3933            );
3934            bail!(
3935                "invalid element point start={start} len={}",
3936                self.int_stack.len()
3937            );
3938        }
3939        let elm = self.int_stack[start..].to_vec();
3940        self.int_stack.truncate(start);
3941        self.vm_trace(None, format!("pop_element -> {:?}", elm));
3942        Ok(elm)
3943    }
3944
3945    fn extract_array_index(&self, elm: &[i32]) -> Option<usize> {
3946        if elm.len() >= 3 && elm[1] == self.ctx.ids.elm_array {
3947            let idx = elm[2];
3948            if idx >= 0 {
3949                return Some(idx as usize);
3950            }
3951        }
3952        None
3953    }
3954
3955    fn user_prop_decl(&self, prop_id: u16) -> Option<(i32, usize)> {
3956        let prop_idx = prop_id as usize;
3957        if let Some(pck) = self.scene_pck_cache.as_ref() {
3958            if prop_idx < pck.inc_props.len() {
3959                let decl = &pck.inc_props[prop_idx];
3960                return Some((decl.form, decl.size.max(0) as usize));
3961            }
3962        } else if prop_idx < self.stream.header.scn_prop_cnt.max(0) as usize {
3963            // Some VM/unit-test entry points construct a SceneVm directly from
3964            // one scene chunk and do not eagerly load Scene.pck metadata.  In
3965            // that mode the bytecode still numbers the shared/inc properties
3966            // from zero, and this scene chunk carries the matching declarations
3967            // in its own prop table.  Treat them as authoritative until the
3968            // pack cache is available; otherwise scalar input/menu variables
3969            // are mis-created as generic lists and title clicks never latch.
3970            let off = (self.stream.header.scn_prop_list_ofs.max(0) as usize)
3971                .checked_add(prop_idx * 8)?;
3972            if off + 8 <= self.stream.chunk.len() {
3973                let form = i32::from_le_bytes(self.stream.chunk[off..off + 4].try_into().unwrap());
3974                let size =
3975                    i32::from_le_bytes(self.stream.chunk[off + 4..off + 8].try_into().unwrap());
3976                return Some((form, size.max(0) as usize));
3977            }
3978        }
3979
3980        let local_idx = prop_idx.saturating_sub(
3981            self.scene_pck_cache
3982                .as_ref()
3983                .map(|pck| pck.inc_props.len())
3984                .unwrap_or(0),
3985        );
3986        let list_ofs = self.stream.header.scn_prop_list_ofs.max(0) as usize;
3987        let cnt = self.stream.header.scn_prop_cnt.max(0) as usize;
3988        if local_idx < cnt {
3989            let off = list_ofs.checked_add(local_idx * 8)?;
3990            if off + 8 <= self.stream.chunk.len() {
3991                let form = i32::from_le_bytes(self.stream.chunk[off..off + 4].try_into().unwrap());
3992                let size =
3993                    i32::from_le_bytes(self.stream.chunk[off + 4..off + 8].try_into().unwrap());
3994                return Some((form, size.max(0) as usize));
3995            }
3996        }
3997        None
3998    }
3999
4000    fn default_user_prop_element(&self, prop_id: u16, _form: i32) -> Vec<i32> {
4001        let head = constants::elm::create(constants::elm::OWNER_USER_PROP, 0, prop_id as i32);
4002        vec![head]
4003    }
4004
4005    fn default_user_prop_slot_element(&self, prop_id: u16, idx: usize) -> Vec<i32> {
4006        vec![
4007            constants::elm::create(constants::elm::OWNER_USER_PROP, 0, prop_id as i32),
4008            self.ctx.ids.elm_array,
4009            idx as i32,
4010        ]
4011    }
4012
4013    fn default_user_prop_cell(&self, prop_id: u16) -> UserPropCell {
4014        let (form, size) = self
4015            .user_prop_decl(prop_id)
4016            .unwrap_or((self.cfg.fm_list, 0));
4017        let mut cell = UserPropCell::new(form, self.default_user_prop_element(prop_id, form));
4018        if form == self.cfg.fm_intlist {
4019            cell.int_list = vec![0; size];
4020        } else if form == self.cfg.fm_strlist {
4021            cell.str_list = vec![String::new(); size];
4022        } else if form == self.cfg.fm_list && size > 0 {
4023            let mut items = Vec::with_capacity(size);
4024            for i in 0..size {
4025                let mut slot = UserPropCell::new(
4026                    self.cfg.fm_list,
4027                    self.default_user_prop_slot_element(prop_id, i),
4028                );
4029                slot.form = self.cfg.fm_list;
4030                items.push(slot);
4031            }
4032            cell.list_items = items;
4033        }
4034        cell
4035    }
4036
4037    fn user_prop_cell_from_value(
4038        &self,
4039        rhs: Value,
4040        declared_form: i32,
4041        element: Vec<i32>,
4042        prop_id: Option<u16>,
4043    ) -> UserPropCell {
4044        let mut cell = UserPropCell::new(declared_form, element.clone());
4045        match rhs {
4046            Value::NamedArg { value, .. } => {
4047                return self.user_prop_cell_from_value(*value, declared_form, element, prop_id);
4048            }
4049            Value::Int(n) => {
4050                cell.form = self.cfg.fm_int;
4051                cell.int_value = n as i32;
4052            }
4053            Value::Str(s) => {
4054                cell.form = self.cfg.fm_str;
4055                cell.str_value = s;
4056            }
4057            Value::Element(e) => {
4058                cell.form = declared_form;
4059                cell.element = e;
4060            }
4061            Value::List(items) => {
4062                if declared_form == self.cfg.fm_intlist {
4063                    cell.form = self.cfg.fm_intlist;
4064                    cell.int_list = items
4065                        .into_iter()
4066                        .map(|item| item.as_i64().unwrap_or(0) as i32)
4067                        .collect();
4068                } else if declared_form == self.cfg.fm_strlist {
4069                    cell.form = self.cfg.fm_strlist;
4070                    cell.str_list = items
4071                        .into_iter()
4072                        .map(|item| item.as_str().unwrap_or("").to_string())
4073                        .collect();
4074                } else {
4075                    cell.form = self.cfg.fm_list;
4076                    let mut out = Vec::with_capacity(items.len());
4077                    for (idx, item) in items.into_iter().enumerate() {
4078                        let slot_element = if let Some(pid) = prop_id {
4079                            self.default_user_prop_slot_element(pid, idx)
4080                        } else {
4081                            vec![]
4082                        };
4083                        out.push(self.user_prop_cell_from_value(
4084                            item,
4085                            self.cfg.fm_list,
4086                            slot_element,
4087                            prop_id,
4088                        ));
4089                    }
4090                    cell.list_items = out;
4091                }
4092            }
4093        }
4094        cell
4095    }
4096
4097    fn consume_array_sub<'b>(&self, sub: &'b [i32]) -> Option<(usize, &'b [i32])> {
4098        if sub.len() >= 2 && self.call_array_marker(sub[0]) && sub[1] >= 0 {
4099            Some((sub[1] as usize, &sub[2..]))
4100        } else {
4101            None
4102        }
4103    }
4104
4105    fn intlist_bit_get(values: &[i32], bit: i32, index: usize) -> i32 {
4106        let word = values
4107            .get(index / (32 / bit as usize))
4108            .copied()
4109            .unwrap_or(0) as u32;
4110        let shift = (index % (32 / bit as usize)) * bit as usize;
4111        let mask = ((1u32 << bit) - 1) << shift;
4112        ((word & mask) >> shift) as i32
4113    }
4114
4115    fn intlist_dispatch_read(
4116        &mut self,
4117        values: &[i32],
4118        sub: &[i32],
4119        fallback_element: &[i32],
4120    ) -> Result<()> {
4121        use crate::runtime::forms::codes::{
4122            ELM_ARRAY, ELM_INTLIST_BIT, ELM_INTLIST_BIT16, ELM_INTLIST_BIT2, ELM_INTLIST_BIT4,
4123            ELM_INTLIST_BIT8, ELM_INTLIST_GET_SIZE,
4124        };
4125        let sub = if sub.len() == 1 && self.call_array_marker(sub[0]) {
4126            &[][..]
4127        } else {
4128            sub
4129        };
4130        if sub.is_empty() {
4131            self.push_element(fallback_element.to_vec());
4132            return Ok(());
4133        }
4134        if let Some((idx, rest)) = self.consume_array_sub(sub) {
4135            if !rest.is_empty() {
4136                bail!("unsupported chained intlist array access {:?}", sub);
4137            }
4138            self.push_int(values.get(idx).copied().unwrap_or(0));
4139            return Ok(());
4140        }
4141        match sub[0] {
4142            ELM_INTLIST_GET_SIZE => {
4143                self.push_int(values.len() as i32);
4144            }
4145            ELM_INTLIST_BIT | ELM_INTLIST_BIT2 | ELM_INTLIST_BIT4 | ELM_INTLIST_BIT8
4146            | ELM_INTLIST_BIT16 => {
4147                let bit = match sub[0] {
4148                    ELM_INTLIST_BIT => 1,
4149                    ELM_INTLIST_BIT2 => 2,
4150                    ELM_INTLIST_BIT4 => 4,
4151                    ELM_INTLIST_BIT8 => 8,
4152                    _ => 16,
4153                };
4154                if let Some((idx, rest)) = self.consume_array_sub(&sub[1..]) {
4155                    if !rest.is_empty() {
4156                        bail!("unsupported chained intlist bit access {:?}", sub);
4157                    }
4158                    self.push_int(Self::intlist_bit_get(values, bit, idx));
4159                } else {
4160                    let mut chained = fallback_element.to_vec();
4161                    chained.extend_from_slice(sub);
4162                    self.push_element(chained);
4163                }
4164            }
4165            _ => self.push_element(fallback_element.to_vec()),
4166        }
4167        Ok(())
4168    }
4169
4170    fn push_user_prop_cell_result(
4171        &mut self,
4172        cell: &UserPropCell,
4173        sub: &[i32],
4174        full_elm: &[i32],
4175    ) -> Result<()> {
4176        use crate::runtime::forms::codes::{
4177            ELM_STRLIST_GET_SIZE, FM_INTLISTREF, FM_INTREF, FM_STRLISTREF, FM_STRREF,
4178        };
4179
4180        let sub = if sub.len() == 1 && self.call_array_marker(sub[0]) {
4181            &[][..]
4182        } else {
4183            sub
4184        };
4185        if cell.form == self.cfg.fm_int && sub.is_empty() {
4186            self.push_int(cell.int_value);
4187            return Ok(());
4188        }
4189        if cell.form == self.cfg.fm_str {
4190            if sub.is_empty() {
4191                self.push_str(cell.str_value.clone());
4192            } else {
4193                self.call_prop_eval_str_op(&cell.str_value, sub[0], &[], 0)?;
4194            }
4195            return Ok(());
4196        }
4197        if cell.form == self.cfg.fm_intlist {
4198            return self.intlist_dispatch_read(&cell.int_list, sub, &cell.element);
4199        }
4200        if cell.form == self.cfg.fm_strlist {
4201            if sub.is_empty() {
4202                self.push_element(cell.element.clone());
4203            } else if let Some((idx, rest)) = self.consume_array_sub(sub) {
4204                let cur = cell.str_list.get(idx).cloned().unwrap_or_default();
4205                if rest.is_empty() {
4206                    self.push_str(cur);
4207                } else {
4208                    self.call_prop_eval_str_op(&cur, rest[0], &[], 0)?;
4209                }
4210            } else if sub[0] == ELM_STRLIST_GET_SIZE {
4211                self.push_int(cell.str_list.len() as i32);
4212            } else {
4213                self.push_element(cell.element.clone());
4214            }
4215            return Ok(());
4216        }
4217        if matches!(
4218            cell.form,
4219            FM_INTREF | FM_STRREF | FM_INTLISTREF | FM_STRLISTREF
4220        ) {
4221            self.push_element(cell.element.clone());
4222            return Ok(());
4223        }
4224        if let Some((idx, rest)) = self.consume_array_sub(sub) {
4225            let slot = if let Some(slot) = cell.list_items.get(idx) {
4226                slot.clone()
4227            } else {
4228                let mut tmp = UserPropCell::new(
4229                    self.cfg.fm_list,
4230                    self.default_user_prop_slot_element(elm_code::code(full_elm[0]), idx),
4231                );
4232                tmp.form = self.cfg.fm_list;
4233                tmp
4234            };
4235            return self.push_user_prop_cell_result(&slot, rest, full_elm);
4236        }
4237        self.push_element(cell.element.clone());
4238        Ok(())
4239    }
4240
4241    fn default_value_like(&self, v: &Value) -> Value {
4242        match v {
4243            Value::NamedArg { value, .. } => self.default_value_like(value),
4244            Value::Int(_) => Value::Int(0),
4245            Value::Str(_) => Value::Str(String::new()),
4246            Value::Element(_) => Value::Element(Vec::new()),
4247            Value::List(_) => Value::List(Vec::new()),
4248        }
4249    }
4250    fn call_scratch_from_args(&self, args: &[Value]) -> (Vec<i32>, Vec<String>) {
4251        let mut int_args = Self::blank_call_int_args();
4252        let mut str_args = Self::blank_call_str_args();
4253        let mut int_pos = 0usize;
4254        let mut str_pos = 0usize;
4255        for v in args {
4256            match v {
4257                Value::NamedArg { value, .. } => match value.as_ref() {
4258                    Value::Int(n) => {
4259                        if int_pos < int_args.len() {
4260                            int_args[int_pos] = *n as i32;
4261                            int_pos += 1;
4262                        }
4263                    }
4264                    Value::Str(s) => {
4265                        if str_pos < str_args.len() {
4266                            str_args[str_pos] = s.clone();
4267                            str_pos += 1;
4268                        }
4269                    }
4270                    _ => {}
4271                },
4272                Value::Int(n) => {
4273                    if int_pos < int_args.len() {
4274                        int_args[int_pos] = *n as i32;
4275                        int_pos += 1;
4276                    }
4277                }
4278                Value::Str(s) => {
4279                    if str_pos < str_args.len() {
4280                        str_args[str_pos] = s.clone();
4281                        str_pos += 1;
4282                    }
4283                }
4284                _ => {}
4285            }
4286        }
4287        (int_args, str_args)
4288    }
4289
4290    fn call_array_marker(&self, code: i32) -> bool {
4291        let mapped = self.ctx.ids.elm_array;
4292        code == crate::runtime::forms::codes::ELM_ARRAY || (mapped >= 0 && code == mapped)
4293    }
4294
4295    fn resolve_call_frame_index(&self, idx: i32) -> Option<usize> {
4296        if idx < 0 {
4297            return None;
4298        }
4299        let depth = self.call_stack.len();
4300        let rev = idx as usize;
4301        if rev >= depth {
4302            return None;
4303        }
4304        Some(depth - 1 - rev)
4305    }
4306
4307    fn current_call_frame_index(&self) -> Option<usize> {
4308        if self.call_stack.is_empty() {
4309            None
4310        } else {
4311            Some(self.call_stack.len() - 1)
4312        }
4313    }
4314
4315    fn find_call_prop_index_in_frame(&self, frame_idx: usize, call_prop_id: i32) -> Option<usize> {
4316        // C++ tnm_command_proc_call_prop() indexes the current call's
4317        // user_prop_list directly with the CALL_PROP code value:
4318        //   p_cur_call->user_prop_list[call_prop_id]
4319        // The stored C_elm_user_call_prop::prop_id is the declared property id
4320        // and is not the lookup key for CALL_PROP bytecode.
4321        if call_prop_id < 0 {
4322            return None;
4323        }
4324        let frame = self.call_stack.get(frame_idx)?;
4325        let idx = call_prop_id as usize;
4326        if idx < frame.user_props.len() {
4327            Some(idx)
4328        } else {
4329            None
4330        }
4331    }
4332
4333    fn call_prop_element(prop_id: i32) -> Vec<i32> {
4334        vec![constants::elm::create(constants::elm::OWNER_CALL_PROP, 0, prop_id)]
4335    }
4336
4337    fn call_prop_value_from_rhs(&self, rhs: &Value) -> (i32, CallPropValue) {
4338        match rhs {
4339            Value::NamedArg { value, .. } => self.call_prop_value_from_rhs(value),
4340            Value::Int(n) => (self.cfg.fm_int, CallPropValue::Int(*n as i32)),
4341            Value::Str(s) => (self.cfg.fm_str, CallPropValue::Str(s.clone())),
4342            Value::Element(e) => (self.cfg.fm_list, CallPropValue::Element(e.clone())),
4343            Value::List(_) => (self.cfg.fm_list, CallPropValue::Element(Vec::new())),
4344        }
4345    }
4346
4347    fn ensure_call_prop_index_for_assign(
4348        &mut self,
4349        frame_idx: usize,
4350        call_prop_id: i32,
4351        rhs: &Value,
4352    ) -> Result<usize> {
4353        if let Some(idx) = self.find_call_prop_index_in_frame(frame_idx, call_prop_id) {
4354            return Ok(idx);
4355        }
4356        if call_prop_id < 0 {
4357            bail!("negative CALL_PROP id {}", call_prop_id);
4358        }
4359
4360        // The original engine expects CALL_PROP ids to be dense list indexes
4361        // created by CD_DEC_PROP. If Rust reaches an assignment before a slot
4362        // exists, keep the same indexed layout rather than appending a slot with
4363        // a matching prop_id, because later CALL_PROP[0] must address slot 0.
4364        let (form, value) = self.call_prop_value_from_rhs(rhs);
4365        let target_idx = call_prop_id as usize;
4366        let frame = self
4367            .call_stack
4368            .get_mut(frame_idx)
4369            .ok_or_else(|| anyhow!("call stack frame out of range"))?;
4370        while frame.user_props.len() <= target_idx {
4371            let idx = frame.user_props.len() as i32;
4372            frame.user_props.push(CallProp {
4373                prop_id: idx,
4374                form: self.cfg.fm_list,
4375                decl_size: 0,
4376                element: Self::call_prop_element(idx),
4377                value: CallPropValue::Element(Self::call_prop_element(idx)),
4378            });
4379        }
4380        let prop = frame
4381            .user_props
4382            .get_mut(target_idx)
4383            .ok_or_else(|| anyhow!("CALL_PROP slot allocation failed"))?;
4384        prop.form = form;
4385        prop.element = Self::call_prop_element(call_prop_id);
4386        prop.value = value;
4387        Ok(target_idx)
4388    }
4389
4390    fn is_direct_value_form(&self, form: i32) -> bool {
4391        form == self.cfg.fm_int
4392            || form == self.cfg.fm_str
4393            || form == self.cfg.fm_intlist
4394            || form == self.cfg.fm_strlist
4395    }
4396
4397    fn call_prop_effective_element(&self, prop: &CallProp) -> Vec<i32> {
4398        match &prop.value {
4399            CallPropValue::Element(e) if !e.is_empty() => e.clone(),
4400            _ => prop.element.clone(),
4401        }
4402    }
4403
4404    fn compose_call_prop_tail(&self, prop: &CallProp, sub: &[i32]) -> Option<Vec<i32>> {
4405        if sub.is_empty() || self.is_direct_value_form(prop.form) {
4406            return None;
4407        }
4408        let mut element = self.call_prop_effective_element(prop);
4409        if element.is_empty() {
4410            return None;
4411        }
4412        element.extend_from_slice(sub);
4413        Some(element)
4414    }
4415
4416    fn compose_user_prop_tail(
4417        &self,
4418        prop_id: u16,
4419        cell: &UserPropCell,
4420        sub: &[i32],
4421    ) -> Option<Vec<i32>> {
4422        if sub.is_empty() || self.is_direct_value_form(cell.form) {
4423            return None;
4424        }
4425        if let Some((idx, rest)) = self.consume_array_sub(sub) {
4426            let slot = cell.list_items.get(idx)?;
4427            let default_slot = self.default_user_prop_slot_element(prop_id, idx);
4428            if slot.element.is_empty() || slot.element == default_slot {
4429                return None;
4430            }
4431            if rest.is_empty() || self.is_direct_value_form(slot.form) {
4432                return Some(slot.element.clone());
4433            }
4434            let mut element = slot.element.clone();
4435            element.extend_from_slice(rest);
4436            return Some(element);
4437        }
4438
4439        let default_root = self.default_user_prop_element(prop_id, cell.form);
4440        if cell.element.is_empty() || cell.element == default_root {
4441            return None;
4442        }
4443        let mut element = cell.element.clone();
4444        element.extend_from_slice(sub);
4445        Some(element)
4446    }
4447
4448    fn push_call_prop_result(
4449        &mut self,
4450        prop: &CallProp,
4451        sub: &[i32],
4452        full_elm: &[i32],
4453    ) -> Result<()> {
4454        use crate::runtime::forms::codes::{
4455            ELM_ARRAY, ELM_STRLIST_GET_SIZE, FM_INT, FM_INTLIST, FM_INTLISTREF, FM_INTREF, FM_STR,
4456            FM_STRLIST, FM_STRLISTREF, FM_STRREF,
4457        };
4458
4459        let sub = if sub.len() == 1 && self.call_array_marker(sub[0]) {
4460            &[][..]
4461        } else {
4462            sub
4463        };
4464        match prop.form {
4465            FM_INT if sub.is_empty() => {
4466                if let CallPropValue::Int(n) = &prop.value {
4467                    self.push_int(*n);
4468                } else {
4469                    bail!("CALL_PROP int storage mismatch for {:?}", full_elm);
4470                }
4471            }
4472            FM_STR if sub.is_empty() => {
4473                if let CallPropValue::Str(s) = &prop.value {
4474                    self.push_str(s.clone());
4475                } else {
4476                    bail!("CALL_PROP str storage mismatch for {:?}", full_elm);
4477                }
4478            }
4479            FM_STR => {
4480                if let CallPropValue::Str(s) = &prop.value {
4481                    self.call_prop_eval_str_op(s, sub[0], &[], 0)?;
4482                } else {
4483                    bail!("CALL_PROP str storage mismatch for {:?}", full_elm);
4484                }
4485            }
4486            FM_INTLIST => {
4487                if let CallPropValue::IntList(v) = &prop.value {
4488                    self.intlist_dispatch_read(v, sub, &prop.element)?;
4489                } else {
4490                    bail!("CALL_PROP intlist storage mismatch for {:?}", full_elm);
4491                }
4492            }
4493            FM_STRLIST => {
4494                if let CallPropValue::StrList(v) = &prop.value {
4495                    if sub.is_empty() {
4496                        self.push_element(prop.element.clone());
4497                    } else if let Some((idx, rest)) = self.consume_array_sub(sub) {
4498                        let current = v.get(idx).cloned().unwrap_or_default();
4499                        if rest.is_empty() {
4500                            self.push_str(current);
4501                        } else {
4502                            self.call_prop_eval_str_op(&current, rest[0], &[], 0)?;
4503                        }
4504                    } else if sub[0] == ELM_STRLIST_GET_SIZE {
4505                        self.push_int(v.len() as i32);
4506                    } else {
4507                        self.push_element(prop.element.clone());
4508                    }
4509                } else {
4510                    bail!("CALL_PROP strlist storage mismatch for {:?}", full_elm);
4511                }
4512            }
4513            FM_INTREF | FM_STRREF | FM_INTLISTREF | FM_STRLISTREF => {
4514                self.push_element(prop.element.clone());
4515            }
4516            _ if !sub.is_empty() => {
4517                self.push_element(prop.element.clone());
4518            }
4519            _ => {
4520                self.push_element(prop.element.clone());
4521            }
4522        }
4523        Ok(())
4524    }
4525
4526    fn str_display_width_char(ch: char) -> usize {
4527        if ch.is_ascii() {
4528            1
4529        } else {
4530            2
4531        }
4532    }
4533
4534    fn str_display_width(s: &str) -> usize {
4535        s.chars().map(Self::str_display_width_char).sum()
4536    }
4537
4538    fn str_left_len(s: &str, limit: usize) -> String {
4539        let mut width = 0usize;
4540        let mut out = String::new();
4541        for ch in s.chars() {
4542            let w = Self::str_display_width_char(ch);
4543            if width + w > limit {
4544                break;
4545            }
4546            width += w;
4547            out.push(ch);
4548        }
4549        out
4550    }
4551
4552    fn str_right_len(s: &str, limit: usize) -> String {
4553        let mut width = 0usize;
4554        let mut out: Vec<char> = Vec::new();
4555        for ch in s.chars().rev() {
4556            let w = Self::str_display_width_char(ch);
4557            if width + w > limit {
4558                break;
4559            }
4560            width += w;
4561            out.push(ch);
4562        }
4563        out.into_iter().rev().collect()
4564    }
4565
4566    fn str_mid_len(s: &str, start_width: usize, len_width: Option<usize>) -> String {
4567        let mut width = 0usize;
4568        let mut out = String::new();
4569        for ch in s.chars() {
4570            let ch_width = Self::str_display_width_char(ch);
4571            if width >= start_width {
4572                if let Some(limit) = len_width {
4573                    if Self::str_display_width(&out) + ch_width > limit {
4574                        break;
4575                    }
4576                }
4577                out.push(ch);
4578            }
4579            width += ch_width;
4580        }
4581        out
4582    }
4583
4584    fn call_prop_eval_str_op(
4585        &mut self,
4586        current: &str,
4587        op: i32,
4588        params: &[Value],
4589        al_id: i32,
4590    ) -> Result<()> {
4591        use crate::runtime::forms::codes::str_op;
4592        match op {
4593            str_op::UPPER => {
4594                self.push_str(current.chars().map(|c| c.to_ascii_uppercase()).collect())
4595            }
4596            str_op::LOWER => {
4597                self.push_str(current.chars().map(|c| c.to_ascii_lowercase()).collect())
4598            }
4599            str_op::CNT => self.push_int(current.chars().count() as i32),
4600            str_op::LEN => self.push_int(Self::str_display_width(current) as i32),
4601            str_op::LEFT => {
4602                let len = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4603                self.push_str(current.chars().take(len).collect());
4604            }
4605            str_op::LEFT_LEN => {
4606                let len = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4607                self.push_str(Self::str_left_len(current, len));
4608            }
4609            str_op::RIGHT => {
4610                let len = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4611                let total = current.chars().count();
4612                let start = total.saturating_sub(len);
4613                self.push_str(current.chars().skip(start).collect());
4614            }
4615            str_op::RIGHT_LEN => {
4616                let len = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4617                self.push_str(Self::str_right_len(current, len));
4618            }
4619            str_op::MID => {
4620                let start = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4621                if al_id == 0 || params.len() <= 1 {
4622                    self.push_str(current.chars().skip(start).collect());
4623                } else {
4624                    let len = params.get(1).and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4625                    self.push_str(current.chars().skip(start).take(len).collect());
4626                }
4627            }
4628            str_op::MID_LEN => {
4629                let start = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4630                let len = if al_id == 0 || params.len() <= 1 {
4631                    None
4632                } else {
4633                    Some(params.get(1).and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize)
4634                };
4635                self.push_str(Self::str_mid_len(current, start, len));
4636            }
4637            str_op::SEARCH => {
4638                let needle = params.first().and_then(|v| v.as_str()).unwrap_or("");
4639                let hay = current.to_ascii_lowercase();
4640                let needle = needle.to_ascii_lowercase();
4641                self.push_int(hay.find(&needle).map(|v| v as i32).unwrap_or(-1));
4642            }
4643            str_op::SEARCH_LAST => {
4644                let needle = params.first().and_then(|v| v.as_str()).unwrap_or("");
4645                let hay = current.to_ascii_lowercase();
4646                let needle = needle.to_ascii_lowercase();
4647                self.push_int(hay.rfind(&needle).map(|v| v as i32).unwrap_or(-1));
4648            }
4649            str_op::GET_CODE => {
4650                let pos = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4651                let code = current.chars().nth(pos).map(|c| c as i32).unwrap_or(-1);
4652                self.push_int(code);
4653            }
4654            str_op::TONUM => self.push_int(current.parse::<i32>().unwrap_or(0)),
4655            _ => bail!("unsupported CALL_PROP string op {}", op),
4656        }
4657        Ok(())
4658    }
4659
4660    fn assign_call_prop_result(prop: &mut CallProp, sub: &[i32], rhs: Value) -> Result<()> {
4661        use crate::runtime::forms::codes::{
4662            ELM_ARRAY, FM_INT, FM_INTLIST, FM_INTLISTREF, FM_INTREF, FM_STR, FM_STRLIST,
4663            FM_STRLISTREF, FM_STRREF,
4664        };
4665
4666        match prop.form {
4667            FM_INT if sub.is_empty() => match rhs {
4668                Value::Int(n) => {
4669                    prop.value = CallPropValue::Int(n as i32);
4670                }
4671                _ => bail!("unsupported CALL_PROP int assign sub={:?}", sub),
4672            },
4673            FM_STR if sub.is_empty() => match rhs {
4674                Value::Str(s) => {
4675                    prop.value = CallPropValue::Str(s);
4676                }
4677                _ => bail!("unsupported CALL_PROP str assign sub={:?}", sub),
4678            },
4679            FM_INTLIST if sub.len() >= 2 && sub[0] == ELM_ARRAY => match rhs {
4680                Value::Int(n) => {
4681                    let idx = sub[1].max(0) as usize;
4682                    let mut dst = match std::mem::replace(
4683                        &mut prop.value,
4684                        CallPropValue::IntList(Vec::new()),
4685                    ) {
4686                        CallPropValue::IntList(v) => v,
4687                        other => {
4688                            prop.value = other;
4689                            bail!("CALL_PROP intlist storage mismatch");
4690                        }
4691                    };
4692                    if dst.len() <= idx {
4693                        dst.resize(idx + 1, 0);
4694                    }
4695                    dst[idx] = n as i32;
4696                    prop.value = CallPropValue::IntList(dst);
4697                }
4698                _ => bail!("unsupported CALL_PROP intlist assign sub={:?}", sub),
4699            },
4700            FM_STRLIST if sub.len() >= 2 && sub[0] == ELM_ARRAY => match rhs {
4701                Value::Str(s) => {
4702                    let idx = sub[1].max(0) as usize;
4703                    let mut dst = match std::mem::replace(
4704                        &mut prop.value,
4705                        CallPropValue::StrList(Vec::new()),
4706                    ) {
4707                        CallPropValue::StrList(v) => v,
4708                        other => {
4709                            prop.value = other;
4710                            bail!("CALL_PROP strlist storage mismatch");
4711                        }
4712                    };
4713                    if dst.len() <= idx {
4714                        dst.resize_with(idx + 1, String::new);
4715                    }
4716                    dst[idx] = s;
4717                    prop.value = CallPropValue::StrList(dst);
4718                }
4719                _ => bail!("unsupported CALL_PROP strlist assign sub={:?}", sub),
4720            },
4721            FM_INTREF | FM_STRREF | FM_INTLISTREF | FM_STRLISTREF => match rhs {
4722                Value::Element(e) => {
4723                    prop.element = e.clone();
4724                    prop.value = CallPropValue::Element(e);
4725                }
4726                _ => bail!("unsupported CALL_PROP ref assign sub={:?}", sub),
4727            },
4728            _ => bail!(
4729                "unsupported call prop assign form={} sub={:?}",
4730                prop.form,
4731                sub
4732            ),
4733        }
4734        Ok(())
4735    }
4736
4737    fn exec_user_prop_list_init_command(
4738        &mut self,
4739        prop_id: u16,
4740        sub: &[i32],
4741        ret_form: i32,
4742    ) -> Result<bool> {
4743        use crate::runtime::forms::codes::{ELM_INTLIST_INIT, ELM_STRLIST_INIT};
4744
4745        if sub.len() != 1 {
4746            return Ok(false);
4747        }
4748        let (form, size) = self
4749            .user_prop_decl(prop_id)
4750            .unwrap_or((self.cfg.fm_list, 0));
4751        if form == self.cfg.fm_intlist && sub[0] == ELM_INTLIST_INIT {
4752            let mut cell = self
4753                .user_props
4754                .remove(&prop_id)
4755                .unwrap_or_else(|| self.default_user_prop_cell(prop_id));
4756            cell.form = form;
4757            cell.element = self.default_user_prop_element(prop_id, form);
4758            cell.int_list.clear();
4759            cell.int_list.resize(size, 0);
4760            self.user_props.insert(prop_id, cell);
4761            self.push_default_for_ret(ret_form);
4762            return Ok(true);
4763        }
4764        if form == self.cfg.fm_strlist && sub[0] == ELM_STRLIST_INIT {
4765            let mut cell = self
4766                .user_props
4767                .remove(&prop_id)
4768                .unwrap_or_else(|| self.default_user_prop_cell(prop_id));
4769            cell.form = form;
4770            cell.element = self.default_user_prop_element(prop_id, form);
4771            cell.str_list.clear();
4772            cell.str_list.resize_with(size, String::new);
4773            self.user_props.insert(prop_id, cell);
4774            self.push_default_for_ret(ret_form);
4775            return Ok(true);
4776        }
4777        Ok(false)
4778    }
4779
4780    fn exec_call_prop_command(
4781        &mut self,
4782        frame_idx: usize,
4783        prop_idx: usize,
4784        sub: &[i32],
4785        al_id: i32,
4786        ret_form: i32,
4787        args: &[Value],
4788    ) -> Result<()> {
4789        use crate::runtime::forms::codes::{
4790            ELM_ARRAY, ELM_INTLIST_BIT, ELM_INTLIST_BIT16, ELM_INTLIST_BIT2, ELM_INTLIST_BIT4,
4791            ELM_INTLIST_BIT8, ELM_INTLIST_CLEAR, ELM_INTLIST_GET_SIZE, ELM_INTLIST_INIT,
4792            ELM_INTLIST_RESIZE, ELM_INTLIST_SETS, ELM_STRLIST_GET_SIZE, ELM_STRLIST_INIT,
4793            ELM_STRLIST_RESIZE, FM_INT, FM_INTLIST, FM_INTLISTREF, FM_INTREF, FM_STR, FM_STRLIST,
4794            FM_STRLISTREF, FM_STRREF,
4795        };
4796
4797        let (form, decl_size, mut value, mut element) = {
4798            let prop = self
4799                .call_stack
4800                .get(frame_idx)
4801                .and_then(|f| f.user_props.get(prop_idx))
4802                .ok_or_else(|| anyhow!("call prop frame/index out of range"))?;
4803            (prop.form, prop.decl_size, prop.value.clone(), prop.element.clone())
4804        };
4805
4806        let mut write_back = false;
4807
4808        if !sub.is_empty() && !self.is_direct_value_form(form) {
4809            let mut composed = match &value {
4810                CallPropValue::Element(e) if !e.is_empty() => e.clone(),
4811                _ => element.clone(),
4812            };
4813            if !composed.is_empty() {
4814                composed.extend_from_slice(sub);
4815                let mut owned_args = args.to_vec();
4816                self.exec_command(composed, al_id, ret_form, &mut owned_args)?;
4817                return Ok(());
4818            }
4819        }
4820
4821        match form {
4822            FM_INT => {
4823                if sub.is_empty() {
4824                    if al_id == 0 {
4825                        match &value {
4826                            CallPropValue::Int(n) => self.push_int(*n),
4827                            _ => bail!("CALL_PROP int storage mismatch"),
4828                        }
4829                    } else {
4830                        let rhs = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
4831                        value = CallPropValue::Int(rhs);
4832                        write_back = true;
4833                        self.push_default_for_ret(ret_form);
4834                    }
4835                } else {
4836                    self.push_element(element.clone());
4837                }
4838            }
4839            FM_STR => {
4840                let current = match &value {
4841                    CallPropValue::Str(s) => s.clone(),
4842                    _ => bail!("CALL_PROP str storage mismatch"),
4843                };
4844                if sub.is_empty() {
4845                    if al_id == 0 {
4846                        self.push_str(current);
4847                    } else {
4848                        let rhs = args
4849                            .first()
4850                            .and_then(|v| v.as_str())
4851                            .unwrap_or("")
4852                            .to_string();
4853                        value = CallPropValue::Str(rhs);
4854                        write_back = true;
4855                        self.push_default_for_ret(ret_form);
4856                    }
4857                } else {
4858                    self.call_prop_eval_str_op(&current, sub[0], args, al_id)?;
4859                }
4860            }
4861            FM_INTLIST => {
4862                let mut list = match value {
4863                    CallPropValue::IntList(v) => v,
4864                    _ => bail!("CALL_PROP intlist storage mismatch"),
4865                };
4866                if sub.is_empty() {
4867                    self.push_element(element.clone());
4868                } else if sub.len() >= 2 && sub[0] == ELM_ARRAY {
4869                    let idx = sub[1].max(0) as usize;
4870                    if al_id == 0 {
4871                        self.push_int(list.get(idx).copied().unwrap_or(0));
4872                    } else {
4873                        let rhs = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
4874                        if list.len() <= idx {
4875                            list.resize(idx + 1, 0);
4876                        }
4877                        list[idx] = rhs;
4878                        write_back = true;
4879                        self.push_default_for_ret(ret_form);
4880                    }
4881                } else {
4882                    match sub[0] {
4883                        ELM_INTLIST_BIT | ELM_INTLIST_BIT2 | ELM_INTLIST_BIT4
4884                        | ELM_INTLIST_BIT8 | ELM_INTLIST_BIT16 => {
4885                            self.push_element(element.clone());
4886                        }
4887                        ELM_INTLIST_INIT => {
4888                            list.clear();
4889                            list.resize(decl_size, 0);
4890                            write_back = true;
4891                            self.push_default_for_ret(ret_form);
4892                        }
4893                        ELM_INTLIST_RESIZE => {
4894                            let new_len =
4895                                args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4896                            list.resize(new_len, 0);
4897                            write_back = true;
4898                            self.push_default_for_ret(ret_form);
4899                        }
4900                        ELM_INTLIST_GET_SIZE => self.push_int(list.len() as i32),
4901                        ELM_INTLIST_CLEAR => {
4902                            let start =
4903                                args.get(0).and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4904                            let end =
4905                                args.get(1).and_then(|v| v.as_i64()).unwrap_or(-1).max(-1) as isize;
4906                            let clear_value = if al_id == 0 {
4907                                0
4908                            } else {
4909                                args.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32
4910                            };
4911                            if !list.is_empty() && end >= 0 {
4912                                let end = usize::min(end as usize, list.len().saturating_sub(1));
4913                                for i in start..=end {
4914                                    if i < list.len() {
4915                                        list[i] = clear_value;
4916                                    }
4917                                }
4918                            }
4919                            write_back = true;
4920                            self.push_default_for_ret(ret_form);
4921                        }
4922                        ELM_INTLIST_SETS => {
4923                            let start =
4924                                args.get(0).and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4925                            for (off, v) in args.iter().skip(1).enumerate() {
4926                                let idx = start + off;
4927                                if list.len() <= idx {
4928                                    list.resize(idx + 1, 0);
4929                                }
4930                                list[idx] = v.as_i64().unwrap_or(0) as i32;
4931                            }
4932                            write_back = true;
4933                            self.push_default_for_ret(ret_form);
4934                        }
4935                        _ => bail!("unsupported CALL_PROP intlist op {:?}", sub),
4936                    }
4937                }
4938                value = CallPropValue::IntList(list);
4939            }
4940            FM_STRLIST => {
4941                let mut list = match value {
4942                    CallPropValue::StrList(v) => v,
4943                    _ => bail!("CALL_PROP strlist storage mismatch"),
4944                };
4945                if sub.is_empty() {
4946                    self.push_element(element.clone());
4947                } else if sub.len() >= 2 && sub[0] == ELM_ARRAY {
4948                    let idx = sub[1].max(0) as usize;
4949                    if list.len() <= idx {
4950                        list.resize_with(idx + 1, String::new);
4951                    }
4952                    if sub.len() == 2 {
4953                        if al_id == 0 {
4954                            self.push_str(list[idx].clone());
4955                        } else {
4956                            let rhs = args
4957                                .first()
4958                                .and_then(|v| v.as_str())
4959                                .unwrap_or("")
4960                                .to_string();
4961                            list[idx] = rhs;
4962                            write_back = true;
4963                            self.push_default_for_ret(ret_form);
4964                        }
4965                    } else {
4966                        let current = list[idx].clone();
4967                        self.call_prop_eval_str_op(&current, sub[2], args, al_id)?;
4968                    }
4969                } else {
4970                    match sub[0] {
4971                        ELM_STRLIST_INIT => {
4972                            list.clear();
4973                            list.resize_with(decl_size, String::new);
4974                            write_back = true;
4975                            self.push_default_for_ret(ret_form);
4976                        }
4977                        ELM_STRLIST_RESIZE => {
4978                            let new_len =
4979                                args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
4980                            list.resize_with(new_len, String::new);
4981                            write_back = true;
4982                            self.push_default_for_ret(ret_form);
4983                        }
4984                        ELM_STRLIST_GET_SIZE => self.push_int(list.len() as i32),
4985                        _ => bail!("unsupported CALL_PROP strlist op {:?}", sub),
4986                    }
4987                }
4988                value = CallPropValue::StrList(list);
4989            }
4990            FM_INTREF | FM_STRREF | FM_INTLISTREF | FM_STRLISTREF => {
4991                if sub.is_empty() {
4992                    if al_id == 0 {
4993                        if let CallPropValue::Element(e) = &value {
4994                            if e.is_empty() {
4995                                self.push_element(element.clone());
4996                            } else {
4997                                self.push_element(e.clone());
4998                            }
4999                        } else {
5000                            self.push_element(element.clone());
5001                        }
5002                    } else {
5003                        let rhs = args.first().cloned().unwrap_or(Value::Element(Vec::new()));
5004                        match rhs {
5005                            Value::Element(e) => {
5006                                element = e.clone();
5007                                value = CallPropValue::Element(e);
5008                                write_back = true;
5009                            }
5010                            _ => bail!("CALL_PROP ref assign requires element"),
5011                        }
5012                        self.push_default_for_ret(ret_form);
5013                    }
5014                } else if let CallPropValue::Element(e) = &value {
5015                    if e.is_empty() {
5016                        self.push_element(element.clone());
5017                    } else {
5018                        self.push_element(e.clone());
5019                    }
5020                } else {
5021                    self.push_element(element.clone());
5022                }
5023            }
5024            _ => {
5025                if !sub.is_empty() || al_id == 0 {
5026                    self.push_element(element.clone());
5027                } else {
5028                    bail!("unsupported CALL_PROP form {}", form);
5029                }
5030            }
5031        }
5032
5033        if write_back {
5034            let prop = self
5035                .call_stack
5036                .get_mut(frame_idx)
5037                .and_then(|f| f.user_props.get_mut(prop_idx))
5038                .ok_or_else(|| anyhow!("call prop frame/index out of range"))?;
5039            prop.value = value;
5040            prop.element = element;
5041        }
5042        Ok(())
5043    }
5044
5045    fn exec_call_property(&mut self, elm: &[i32]) -> Result<bool> {
5046        self.vm_trace(None, format!("exec_call_property elm={:?}", elm));
5047        use crate::runtime::forms::codes::{
5048            ELM_CALL_K, ELM_CALL_L, ELM_GLOBAL_CUR_CALL, ELM_INTLIST_GET_SIZE,
5049            ELM_STRLIST_GET_SIZE, FM_CALL, FM_CALLLIST,
5050        };
5051
5052        if elm.is_empty() {
5053            return Ok(false);
5054        }
5055        let head = elm[0];
5056        if head != FM_CALL && head != FM_CALLLIST && head != ELM_GLOBAL_CUR_CALL {
5057            return Ok(false);
5058        }
5059
5060        let current_idx = self
5061            .current_call_frame_index()
5062            .ok_or_else(|| anyhow!("call stack underflow"))?;
5063
5064        let tail: &[i32] = if head == FM_CALLLIST {
5065            if elm.len() < 3 || !self.call_array_marker(elm[1]) {
5066                bail!("malformed CALLLIST access: {:?}", elm);
5067            }
5068            self.resolve_call_frame_index(elm[2])
5069                .ok_or_else(|| anyhow!("CALLLIST index out of range: {}", elm[2]))?;
5070            &elm[3..]
5071        } else {
5072            &elm[1..]
5073        };
5074
5075        if tail.is_empty() {
5076            self.push_element(elm.to_vec());
5077            return Ok(true);
5078        }
5079
5080        match tail[0] {
5081            ELM_CALL_L => {
5082                let sub = &tail[1..];
5083                if sub.is_empty() {
5084                    self.push_element(elm.to_vec());
5085                } else if sub.len() >= 2 && self.call_array_marker(sub[0]) {
5086                    let idx = sub[1].max(0) as usize;
5087                    let v = self.call_stack[current_idx]
5088                        .int_args
5089                        .get(idx)
5090                        .copied()
5091                        .unwrap_or(0);
5092                    self.push_int(v);
5093                } else if sub[0] == ELM_INTLIST_GET_SIZE {
5094                    self.push_int(self.call_stack[current_idx].int_args.len() as i32);
5095                } else {
5096                    self.push_element(elm[..elm.len() - sub.len()].to_vec());
5097                }
5098                return Ok(true);
5099            }
5100            ELM_CALL_K => {
5101                let sub = &tail[1..];
5102                if sub.is_empty() {
5103                    self.push_element(elm.to_vec());
5104                } else if sub.len() >= 2 && self.call_array_marker(sub[0]) {
5105                    let idx = sub[1].max(0) as usize;
5106                    let v = self.call_stack[current_idx]
5107                        .str_args
5108                        .get(idx)
5109                        .cloned()
5110                        .unwrap_or_default();
5111                    if sub.len() == 2 {
5112                        self.push_str(v);
5113                    } else {
5114                        self.call_prop_eval_str_op(&v, sub[2], &[], 0)?;
5115                    }
5116                } else if sub[0] == ELM_STRLIST_GET_SIZE {
5117                    self.push_int(self.call_stack[current_idx].str_args.len() as i32);
5118                } else {
5119                    self.push_element(elm[..elm.len() - sub.len()].to_vec());
5120                }
5121                return Ok(true);
5122            }
5123            _ => {}
5124        }
5125
5126        if elm_code::owner(tail[0]) != elm_code::ELM_OWNER_CALL_PROP {
5127            bail!("invalid CALL property owner for {:?}", elm);
5128        }
5129
5130        let call_prop_id = elm_code::code(tail[0]) as i32;
5131        let prop_idx = self
5132            .find_call_prop_index_in_frame(current_idx, call_prop_id)
5133            .ok_or_else(|| anyhow!("missing CALL_PROP id={} for {:?}", call_prop_id, elm))?;
5134        let prop = self.call_stack[current_idx].user_props[prop_idx].clone();
5135        let sub = &tail[1..];
5136        if let Some(composed) = self.compose_call_prop_tail(&prop, sub) {
5137            self.exec_property(composed)?;
5138            return Ok(true);
5139        }
5140        self.push_call_prop_result(&prop, sub, elm)?;
5141        Ok(true)
5142    }
5143
5144    fn exec_call_assign(&mut self, elm: &[i32], al_id: i32, rhs: Value) -> Result<bool> {
5145        use crate::runtime::forms::codes::{
5146            ELM_CALL_K, ELM_CALL_L, ELM_GLOBAL_CUR_CALL, FM_CALL, FM_CALLLIST,
5147        };
5148
5149        if elm.is_empty() {
5150            return Ok(false);
5151        }
5152        let head = elm[0];
5153        if head != FM_CALL && head != FM_CALLLIST && head != ELM_GLOBAL_CUR_CALL {
5154            return Ok(false);
5155        }
5156
5157        let current_idx = match self.current_call_frame_index() {
5158            Some(v) => v,
5159            None => return Ok(true),
5160        };
5161
5162        let tail: &[i32] = if head == FM_CALLLIST {
5163            if elm.len() < 3 || !self.call_array_marker(elm[1]) {
5164                bail!("malformed CALLLIST assign: {:?}", elm);
5165            }
5166            self.resolve_call_frame_index(elm[2])
5167                .ok_or_else(|| anyhow!("CALLLIST index out of range: {}", elm[2]))?;
5168            &elm[3..]
5169        } else {
5170            &elm[1..]
5171        };
5172
5173        if tail.is_empty() {
5174            return Ok(true);
5175        }
5176
5177        match tail[0] {
5178            ELM_CALL_L => {
5179                let sub = &tail[1..];
5180                if sub.len() >= 2 && self.call_array_marker(sub[0]) {
5181                    let idx = sub[1].max(0) as usize;
5182                    if let Value::Int(n) = rhs {
5183                        let frame = &mut self.call_stack[current_idx];
5184                        if frame.int_args.len() <= idx {
5185                            frame.int_args.resize(idx + 1, 0);
5186                        }
5187                        frame.int_args[idx] = n as i32;
5188                    }
5189                }
5190                return Ok(true);
5191            }
5192            ELM_CALL_K => {
5193                let sub = &tail[1..];
5194                if sub.len() >= 2 && self.call_array_marker(sub[0]) {
5195                    let idx = sub[1].max(0) as usize;
5196                    if let Value::Str(s) = rhs {
5197                        let frame = &mut self.call_stack[current_idx];
5198                        if frame.str_args.len() <= idx {
5199                            frame.str_args.resize_with(idx + 1, String::new);
5200                        }
5201                        frame.str_args[idx] = s;
5202                    }
5203                }
5204                return Ok(true);
5205            }
5206            _ => {}
5207        }
5208
5209        if elm_code::owner(tail[0]) != elm_code::ELM_OWNER_CALL_PROP {
5210            bail!("invalid CALL assign owner for {:?}", elm);
5211        }
5212        let call_prop_id = elm_code::code(tail[0]) as i32;
5213        let sub = &tail[1..];
5214        let prop_idx = self.ensure_call_prop_index_for_assign(current_idx, call_prop_id, &rhs)?;
5215        let prop_for_compose = self.call_stack[current_idx].user_props[prop_idx].clone();
5216        if let Some(composed) = self.compose_call_prop_tail(&prop_for_compose, sub) {
5217            self.exec_assign(composed, al_id, rhs)?;
5218            return Ok(true);
5219        }
5220        let frame = &mut self.call_stack[current_idx];
5221        let prop = frame
5222            .user_props
5223            .get_mut(prop_idx)
5224            .ok_or_else(|| anyhow!("missing CALL_PROP slot assign id={}", call_prop_id))?;
5225        Self::assign_call_prop_result(prop, sub, rhs)?;
5226        Ok(true)
5227    }
5228
5229    fn exec_call_command(
5230        &mut self,
5231        elm: &[i32],
5232        al_id: i32,
5233        ret_form: i32,
5234        args: &[Value],
5235    ) -> Result<bool> {
5236        use crate::runtime::forms::codes::{
5237            ELM_ARRAY, ELM_CALL_K, ELM_CALL_L, ELM_GLOBAL_CUR_CALL, ELM_INTLIST_CLEAR, ELM_INTLIST_GET_SIZE,
5238            ELM_INTLIST_INIT, ELM_INTLIST_RESIZE, ELM_INTLIST_SETS, ELM_STRLIST_GET_SIZE,
5239            ELM_STRLIST_INIT, ELM_STRLIST_RESIZE, FM_CALL, FM_CALLLIST,
5240        };
5241
5242        if elm.is_empty() {
5243            return Ok(false);
5244        }
5245        let head = elm[0];
5246        if head != FM_CALL && head != FM_CALLLIST && head != ELM_GLOBAL_CUR_CALL {
5247            return Ok(false);
5248        }
5249
5250        let current_idx = match self.current_call_frame_index() {
5251            Some(v) => v,
5252            None => {
5253                self.push_default_for_ret(ret_form);
5254                return Ok(true);
5255            }
5256        };
5257
5258        let tail: &[i32] = if head == FM_CALLLIST {
5259            if elm.len() < 3 || !self.call_array_marker(elm[1]) {
5260                self.push_default_for_ret(ret_form);
5261                return Ok(true);
5262            }
5263            let Some(_selected_idx) = self.resolve_call_frame_index(elm[2]) else {
5264                self.push_default_for_ret(ret_form);
5265                return Ok(true);
5266            };
5267            &elm[3..]
5268        } else {
5269            &elm[1..]
5270        };
5271
5272        if tail.is_empty() {
5273            self.push_default_for_ret(ret_form);
5274            return Ok(true);
5275        }
5276
5277        match tail[0] {
5278            ELM_CALL_L => {
5279                let sub = &tail[1..];
5280                if sub.is_empty() {
5281                    self.push_default_for_ret(ret_form);
5282                    return Ok(true);
5283                }
5284                if sub.len() >= 2 && self.call_array_marker(sub[0]) {
5285                    let idx = sub[1].max(0) as usize;
5286                    if al_id == 1 {
5287                        let rhs = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
5288                        let frame = &mut self.call_stack[current_idx];
5289                        if frame.int_args.len() <= idx {
5290                            frame.int_args.resize(idx + 1, 0);
5291                        }
5292                        frame.int_args[idx] = rhs;
5293                        self.push_default_for_ret(ret_form);
5294                    } else {
5295                        let v = self.call_stack[current_idx]
5296                            .int_args
5297                            .get(idx)
5298                            .copied()
5299                            .unwrap_or(0);
5300                        self.push_int(v);
5301                    }
5302                    return Ok(true);
5303                }
5304                match sub[0] {
5305                    ELM_INTLIST_INIT => {
5306                        self.call_stack[current_idx].int_args = Self::blank_call_int_args();
5307                    }
5308                    ELM_INTLIST_RESIZE => {
5309                        let new_len =
5310                            args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5311                        self.call_stack[current_idx].int_args.resize(new_len, 0);
5312                    }
5313                    ELM_INTLIST_GET_SIZE => {
5314                        self.push_int(self.call_stack[current_idx].int_args.len() as i32);
5315                    }
5316                    ELM_INTLIST_CLEAR => {
5317                        let start =
5318                            args.get(0).and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5319                        let end =
5320                            args.get(1).and_then(|v| v.as_i64()).unwrap_or(-1).max(-1) as isize;
5321                        let value = if al_id == 0 {
5322                            0
5323                        } else {
5324                            args.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32
5325                        };
5326                        let frame = &mut self.call_stack[current_idx];
5327                        if !frame.int_args.is_empty() && end >= 0 {
5328                            let end =
5329                                usize::min(end as usize, frame.int_args.len().saturating_sub(1));
5330                            for i in start..=end {
5331                                if i < frame.int_args.len() {
5332                                    frame.int_args[i] = value;
5333                                }
5334                            }
5335                        }
5336                    }
5337                    ELM_INTLIST_SETS => {
5338                        let start =
5339                            args.get(0).and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5340                        let frame = &mut self.call_stack[current_idx];
5341                        for (off, v) in args.iter().skip(1).enumerate() {
5342                            let idx = start + off;
5343                            if frame.int_args.len() <= idx {
5344                                frame.int_args.resize(idx + 1, 0);
5345                            }
5346                            frame.int_args[idx] = v.as_i64().unwrap_or(0) as i32;
5347                        }
5348                    }
5349                    _ => self.push_default_for_ret(ret_form),
5350                }
5351                return Ok(true);
5352            }
5353            ELM_CALL_K => {
5354                let sub = &tail[1..];
5355                if sub.is_empty() {
5356                    self.push_default_for_ret(ret_form);
5357                    return Ok(true);
5358                }
5359                if sub.len() >= 2 && self.call_array_marker(sub[0]) {
5360                    let idx = sub[1].max(0) as usize;
5361                    if sub.len() == 2 {
5362                        if al_id == 1 {
5363                            let rhs = args
5364                                .first()
5365                                .and_then(|v| v.as_str())
5366                                .unwrap_or("")
5367                                .to_string();
5368                            let frame = &mut self.call_stack[current_idx];
5369                            if frame.str_args.len() <= idx {
5370                                frame.str_args.resize_with(idx + 1, String::new);
5371                            }
5372                            frame.str_args[idx] = rhs;
5373                            self.push_default_for_ret(ret_form);
5374                        } else {
5375                            let v = self.call_stack[current_idx]
5376                                .str_args
5377                                .get(idx)
5378                                .cloned()
5379                                .unwrap_or_default();
5380                            self.push_str(v);
5381                        }
5382                    } else {
5383                        let v = self.call_stack[current_idx]
5384                            .str_args
5385                            .get(idx)
5386                            .cloned()
5387                            .unwrap_or_default();
5388                        self.call_prop_eval_str_op(&v, sub[2], args, al_id)?;
5389                    }
5390                    return Ok(true);
5391                }
5392                match sub[0] {
5393                    ELM_STRLIST_INIT => {
5394                        self.call_stack[current_idx].str_args = Self::blank_call_str_args();
5395                    }
5396                    ELM_STRLIST_RESIZE => {
5397                        let new_len =
5398                            args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5399                        self.call_stack[current_idx]
5400                            .str_args
5401                            .resize_with(new_len, String::new);
5402                    }
5403                    ELM_STRLIST_GET_SIZE => {
5404                        self.push_int(self.call_stack[current_idx].str_args.len() as i32);
5405                    }
5406                    _ => self.push_default_for_ret(ret_form),
5407                }
5408                return Ok(true);
5409            }
5410            _ => {
5411                if elm_code::owner(tail[0]) == elm_code::ELM_OWNER_CALL_PROP {
5412                    let call_prop_id = elm_code::code(tail[0]) as i32;
5413                    let prop_idx = self
5414                        .find_call_prop_index_in_frame(current_idx, call_prop_id)
5415                        .ok_or_else(|| {
5416                            anyhow!(
5417                                "missing CALL_PROP command id={} for {:?}",
5418                                call_prop_id,
5419                                elm
5420                            )
5421                        })?;
5422                    self.exec_call_prop_command(
5423                        current_idx,
5424                        prop_idx,
5425                        &tail[1..],
5426                        al_id,
5427                        ret_form,
5428                        args,
5429                    )?;
5430                    return Ok(true);
5431                }
5432                bail!("unsupported CALL command chain {:?}", elm);
5433            }
5434        }
5435    }
5436
5437    fn push_property_value(&mut self, v: Value, array_idx: Option<usize>) {
5438        match v {
5439            Value::NamedArg { value, .. } => self.push_property_value(*value, array_idx),
5440            Value::Int(n) => self.push_int(n as i32),
5441            Value::Str(s) => self.push_str(s),
5442            Value::Element(elm) => self.push_element(elm),
5443            Value::List(items) => {
5444                if let Some(i) = array_idx {
5445                    if let Some(item) = items.get(i).cloned() {
5446                        self.push_property_value(item, None);
5447                    } else {
5448                        self.push_int(0);
5449                    }
5450                } else {
5451                    panic!("raw Value::List used as property result; expected runtime ref");
5452                }
5453            }
5454        }
5455    }
5456
5457    fn assign_user_prop(&mut self, prop_id: u16, array_idx: Option<usize>, rhs: Value) {
5458        let decl = self
5459            .user_prop_decl(prop_id)
5460            .unwrap_or((self.cfg.fm_list, 0));
5461        if let Some(i) = array_idx {
5462            let slot_element = self.default_user_prop_slot_element(prop_id, i);
5463            let default_entry = self.default_user_prop_cell(prop_id);
5464            let existing_form = self
5465                .user_props
5466                .get(&prop_id)
5467                .map(|e| e.form)
5468                .unwrap_or(default_entry.form);
5469            if existing_form == self.cfg.fm_intlist {
5470                let entry = self.user_props.entry(prop_id).or_insert(default_entry);
5471                if entry.int_list.len() <= i {
5472                    entry.int_list.resize(i + 1, 0);
5473                }
5474                entry.int_list[i] = rhs.as_i64().unwrap_or(0) as i32;
5475                return;
5476            }
5477            if existing_form == self.cfg.fm_strlist {
5478                let entry = self.user_props.entry(prop_id).or_insert(default_entry);
5479                if entry.str_list.len() <= i {
5480                    entry.str_list.resize_with(i + 1, String::new);
5481                }
5482                entry.str_list[i] = rhs.as_str().unwrap_or("").to_string();
5483                return;
5484            }
5485            let list_form = self.cfg.fm_list;
5486            let new_slot =
5487                self.user_prop_cell_from_value(rhs, list_form, slot_element, Some(prop_id));
5488            let head = constants::elm::create(constants::elm::OWNER_USER_PROP, 0, prop_id as i32);
5489            let elm_array = self.ctx.ids.elm_array;
5490            let entry = self.user_props.entry(prop_id).or_insert(default_entry);
5491            if entry.list_items.len() <= i {
5492                let cur = entry.list_items.len();
5493                entry
5494                    .list_items
5495                    .resize_with(i + 1, || UserPropCell::new(list_form, Vec::new()));
5496                for idx in cur..entry.list_items.len() {
5497                    entry.list_items[idx].form = list_form;
5498                    entry.list_items[idx].element = vec![head, elm_array, idx as i32];
5499                }
5500            }
5501            entry.list_items[i] = new_slot;
5502        } else {
5503            let element = self.default_user_prop_element(prop_id, decl.0);
5504            let cell = self.user_prop_cell_from_value(rhs, decl.0, element, Some(prop_id));
5505            self.user_props.insert(prop_id, cell);
5506        }
5507    }
5508
5509    fn exec_copy_element(&mut self) -> Result<()> {
5510        let start = match self.element_points.last().copied() {
5511            Some(v) => v,
5512            None => {
5513                self.vm_trace(None, "COPY_ELM missing prior ELM_POINT");
5514                return Err(anyhow!("COPY_ELM without a prior ELM_POINT"));
5515            }
5516        };
5517        if start > self.int_stack.len() {
5518            self.vm_trace(
5519                None,
5520                format!(
5521                    "COPY_ELM invalid start={} len={}",
5522                    start,
5523                    self.int_stack.len()
5524                ),
5525            );
5526            bail!(
5527                "invalid element point start={start} len={}",
5528                self.int_stack.len()
5529            );
5530        }
5531        let slice = self.int_stack[start..].to_vec();
5532        if Self::sg_mwnd_object_trace_enabled() && Self::sg_mwnd_chain_interesting(&slice) {
5533            self.sg_mwnd_object_trace(format!(
5534                "COPY_ELM slice={:?} before_current_chain={:?} before_current_stage_object={:?}",
5535                slice,
5536                self.ctx.globals.current_object_chain,
5537                self.ctx.globals.current_stage_object
5538            ));
5539        }
5540        self.element_points.push(self.int_stack.len());
5541        self.int_stack.extend_from_slice(&slice);
5542        self.vm_trace(None, format!("COPY_ELM copied {:?}", slice));
5543        Ok(())
5544    }
5545
5546    fn pop_value_for_form(&mut self, form_code: i32) -> Result<Value> {
5547        if form_code == self.cfg.fm_void {
5548            return Ok(Value::Int(0));
5549        }
5550        if form_code == self.cfg.fm_int {
5551            return Ok(Value::Int(self.pop_int()? as i64));
5552        }
5553        if form_code == self.cfg.fm_str {
5554            return Ok(Value::Str(self.pop_str()?));
5555        }
5556        if form_code == self.cfg.fm_label {
5557            return Ok(Value::Int(self.pop_int()? as i64));
5558        }
5559        if form_code == self.cfg.fm_list {
5560            let nested = self.pop_arg_list()?;
5561            return Ok(Value::List(nested));
5562        }
5563
5564        // Unknown form: treat as element.
5565        self.trace_unknown_form(form_code, "pop_value_for_form");
5566        Ok(Value::Element(self.pop_element()?))
5567    }
5568
5569    fn pop_arg_list(&mut self) -> Result<Vec<Value>> {
5570        let arg_cnt_i32 = self.stream.pop_i32()?;
5571        if arg_cnt_i32 < 0 {
5572            bail!("negative arg_cnt={arg_cnt_i32}");
5573        }
5574        let arg_cnt = arg_cnt_i32 as usize;
5575        let mut out: Vec<Value> = vec![Value::Int(0); arg_cnt];
5576
5577        // The original fills from the end (stack pop order).
5578        for i in (0..arg_cnt).rev() {
5579            let form_code = self.stream.pop_i32()?;
5580            let v = self.pop_value_for_form(form_code)?;
5581            out[i] = v;
5582        }
5583        Ok(out)
5584    }
5585
5586    fn exec_push(&mut self, form_code: i32) -> Result<()> {
5587        if form_code == self.cfg.fm_void {
5588            return Ok(());
5589        }
5590        if form_code == self.cfg.fm_int {
5591            let v = self.stream.pop_i32()?;
5592            self.push_int(v);
5593            return Ok(());
5594        }
5595        if form_code == self.cfg.fm_str {
5596            let s = self.stream.pop_str()?;
5597            self.push_str(s);
5598            return Ok(());
5599        }
5600
5601        // Other forms are not pushed by CD_PUSH in the fork.
5602        self.trace_unknown_form(form_code, "exec_push");
5603        Ok(())
5604    }
5605
5606    fn exec_pop(&mut self, form_code: i32) -> Result<()> {
5607        if form_code == self.cfg.fm_void {
5608            return Ok(());
5609        }
5610        if form_code == self.cfg.fm_int {
5611            let _ = self.pop_int()?;
5612            return Ok(());
5613        }
5614        if form_code == self.cfg.fm_str {
5615            let _ = self.pop_str()?;
5616            return Ok(());
5617        }
5618
5619        self.trace_unknown_form(form_code, "exec_pop");
5620        Ok(())
5621    }
5622
5623    fn exec_copy(&mut self, form_code: i32) -> Result<()> {
5624        if form_code == self.cfg.fm_void {
5625            return Ok(());
5626        }
5627        if form_code == self.cfg.fm_int {
5628            let v = self.peek_int()?;
5629            self.push_int(v);
5630            return Ok(());
5631        }
5632        if form_code == self.cfg.fm_str {
5633            let s = self.peek_str()?;
5634            self.push_str(s);
5635            return Ok(());
5636        }
5637
5638        // Original CD_COPY only handles scalar INT/STR forms.
5639        self.trace_unknown_form(form_code, "exec_copy");
5640        Ok(())
5641    }
5642
5643    // ---------------------------------------------------------------------
5644    // Command/Property dispatch bridging
5645    // ---------------------------------------------------------------------
5646
5647    fn canonical_runtime_form_id(&self, form_id: u32) -> u32 {
5648        let ids = &self.ctx.ids;
5649
5650        if constants::is_stage_global_form(form_id, ids.form_global_stage) {
5651            return constants::global_form::STAGE_ALT;
5652        }
5653        if constants::matches_form_id(form_id, ids.form_global_mov, constants::global_form::MOV) {
5654            return constants::global_form::MOV;
5655        }
5656        if constants::matches_form_id(form_id, ids.form_global_bgm, constants::global_form::BGM) {
5657            return constants::global_form::BGM;
5658        }
5659        if constants::matches_form_id(
5660            form_id,
5661            ids.form_global_bgm_table,
5662            constants::global_form::BGMTABLE,
5663        ) {
5664            return constants::global_form::BGMTABLE;
5665        }
5666        if constants::matches_form_id(form_id, ids.form_global_math, constants::global_form::MATH) {
5667            return constants::global_form::MATH;
5668        }
5669        if constants::matches_form_id(form_id, ids.form_global_pcm, constants::global_form::PCM) {
5670            return constants::global_form::PCM;
5671        }
5672        if constants::matches_form_id(
5673            form_id,
5674            ids.form_global_pcmch,
5675            constants::global_form::PCMCH,
5676        ) {
5677            return constants::global_form::PCMCH;
5678        }
5679        if constants::matches_form_id(form_id, ids.form_global_se, constants::global_form::SE) {
5680            return constants::global_form::SE;
5681        }
5682        if constants::matches_form_id(
5683            form_id,
5684            ids.form_global_pcm_event,
5685            constants::global_form::PCMEVENT,
5686        ) {
5687            return constants::global_form::PCMEVENT;
5688        }
5689        if constants::matches_form_id(
5690            form_id,
5691            ids.form_global_excall,
5692            constants::global_form::EXCALL,
5693        ) {
5694            return constants::global_form::EXCALL;
5695        }
5696        if constants::matches_form_id(
5697            form_id,
5698            ids.form_global_screen,
5699            constants::global_form::SCREEN,
5700        ) {
5701            return constants::global_form::SCREEN;
5702        }
5703        if constants::matches_form_id(
5704            form_id,
5705            ids.form_global_msgbk,
5706            constants::global_form::MSGBK,
5707        ) {
5708            return constants::global_form::MSGBK;
5709        }
5710        if constants::matches_form_id(
5711            form_id,
5712            ids.form_global_koe_st,
5713            constants::global_form::KOE_ST,
5714        ) {
5715            return constants::global_form::KOE_ST;
5716        }
5717        if constants::matches_form_id(form_id, ids.form_global_key, constants::global_form::KEY) {
5718            return constants::global_form::KEY;
5719        }
5720        if form_id == constants::global_form::COUNTER {
5721            return constants::global_form::COUNTER;
5722        }
5723        if constants::matches_form_id(
5724            form_id,
5725            ids.form_global_frame_action,
5726            constants::global_form::FRAME_ACTION,
5727        ) {
5728            return constants::global_form::FRAME_ACTION;
5729        }
5730        if form_id == constants::global_form::TIMEWAIT {
5731            return constants::global_form::TIMEWAIT;
5732        }
5733        if form_id == constants::global_form::TIMEWAIT_KEY {
5734            return constants::global_form::TIMEWAIT_KEY;
5735        }
5736
5737        form_id
5738    }
5739
5740
5741    fn sg_mwnd_object_trace_enabled() -> bool {
5742        std::env::var_os("SG_DEBUG").is_some()
5743    }
5744
5745    fn sg_mwnd_object_trace(&self, msg: impl AsRef<str>) {
5746        if Self::sg_mwnd_object_trace_enabled() {
5747            eprintln!("[SG_DEBUG][MWND_OBJECT_TRACE][VM] {}", msg.as_ref());
5748        }
5749    }
5750
5751    fn sg_mwnd_chain_interesting(elm: &[i32]) -> bool {
5752        elm.iter().any(|v| {
5753            *v == crate::runtime::forms::codes::STAGE_ELM_MWND
5754                || *v == crate::runtime::forms::codes::STAGE_ELM_BTNSELITEM
5755                || *v == crate::runtime::forms::codes::elm_value::MWND_OBJECT
5756                || *v == crate::runtime::forms::codes::elm_value::MWND_BUTTON
5757                || *v == crate::runtime::forms::codes::elm_value::MWND_FACE
5758                || *v == crate::runtime::forms::codes::ELM_BTNSELITEM_OBJECT
5759                || *v == crate::runtime::forms::codes::elm_value::OBJECT_CHILD
5760                || *v == crate::runtime::forms::codes::elm_value::OBJECT_CREATE
5761                || *v == crate::runtime::forms::codes::elm_value::OBJECT_CREATE_RECT
5762                || *v == crate::runtime::forms::codes::elm_value::OBJECT_CREATE_STRING
5763                || *v == crate::runtime::forms::codes::elm_value::OBJECT_FRAME_ACTION
5764                || *v == crate::runtime::forms::codes::elm_value::OBJECT_FRAME_ACTION_CH
5765        })
5766    }
5767
5768    fn is_global_indexed_list_head(&self, head: i32) -> bool {
5769        if head < 0 {
5770            return false;
5771        }
5772        let head = head as u32;
5773        crate::runtime::constants::global_form::INT_LIST_FORMS.contains(&head)
5774            || crate::runtime::constants::global_form::STR_LIST_FORMS.contains(&head)
5775    }
5776
5777    fn is_global_indexed_list_chain(&self, elm: &[i32]) -> bool {
5778        if elm.len() < 3 || !self.is_global_indexed_list_head(elm[0]) {
5779            return false;
5780        }
5781        elm[1] == self.ctx.ids.elm_array || elm[1] == crate::runtime::forms::codes::ELM_ARRAY
5782    }
5783
5784    fn current_object_chain_has_child_index(&self, child_idx: i32) -> bool {
5785        if child_idx < 0 {
5786            return false;
5787        }
5788        let Some(chain) = self.ctx.globals.current_object_chain.as_ref() else {
5789            return false;
5790        };
5791        let stage_form = if self.ctx.ids.form_global_stage != 0 {
5792            self.ctx.ids.form_global_stage as i32
5793        } else {
5794            crate::runtime::forms::codes::FORM_GLOBAL_STAGE as i32
5795        };
5796        let elm_array = if self.ctx.ids.elm_array != 0 {
5797            self.ctx.ids.elm_array
5798        } else {
5799            crate::runtime::forms::codes::ELM_ARRAY
5800        };
5801        let stage_object = if self.ctx.ids.stage_elm_object != 0 {
5802            self.ctx.ids.stage_elm_object
5803        } else {
5804            crate::runtime::forms::codes::STAGE_ELM_OBJECT
5805        };
5806        if chain.len() < 6
5807            || chain[0] != stage_form
5808            || chain[1] != elm_array
5809            || chain[2] < 0
5810        {
5811            return false;
5812        }
5813
5814        let stage_idx = chain[2] as i64;
5815        let Some(stage_state) = self.ctx.globals.stage_forms.get(&(stage_form as u32)) else {
5816            return false;
5817        };
5818
5819        fn descend_child_chain<'a>(
5820            mut obj: &'a crate::runtime::globals::ObjectState,
5821            chain: &[i32],
5822            mut pos: usize,
5823            elm_array: i32,
5824        ) -> Option<&'a crate::runtime::globals::ObjectState> {
5825            let object_child = crate::runtime::forms::codes::elm_value::OBJECT_CHILD;
5826            while pos + 2 < chain.len() {
5827                if chain[pos] == object_child && chain[pos + 1] == elm_array && chain[pos + 2] >= 0 {
5828                    let idx = chain[pos + 2] as usize;
5829                    obj = obj.runtime.child_objects.get(idx)?;
5830                    pos += 3;
5831                } else {
5832                    break;
5833                }
5834            }
5835            Some(obj)
5836        }
5837
5838        let current_obj = (|| -> Option<&crate::runtime::globals::ObjectState> {
5839            if chain[3] == stage_object {
5840                if chain[4] != elm_array || chain[5] < 0 {
5841                    return None;
5842                }
5843                let top_idx = chain[5] as usize;
5844                let list = stage_state.object_lists.get(&stage_idx)?;
5845                let obj = list.get(top_idx)?;
5846                descend_child_chain(obj, chain, 6, elm_array)
5847            } else if chain[3] == crate::runtime::forms::codes::STAGE_ELM_MWND {
5848                if chain.len() < 9
5849                    || chain[4] != elm_array
5850                    || chain[5] < 0
5851                    || chain[7] != elm_array
5852                    || chain[8] < 0
5853                {
5854                    return None;
5855                }
5856                let mwnd_idx = chain[5] as usize;
5857                let selector = chain[6];
5858                let obj_idx = chain[8] as usize;
5859                let mwnds = stage_state.mwnd_lists.get(&stage_idx)?;
5860                let mwnd = mwnds.get(mwnd_idx)?;
5861                let list = if selector == constants::MWND_BUTTON {
5862                    &mwnd.button_list
5863                } else if selector == constants::MWND_FACE {
5864                    &mwnd.face_list
5865                } else if selector == constants::MWND_OBJECT {
5866                    &mwnd.object_list
5867                } else {
5868                    return None;
5869                };
5870                let obj = list.get(obj_idx)?;
5871                descend_child_chain(obj, chain, 9, elm_array)
5872            } else if chain[3] == crate::runtime::forms::codes::STAGE_ELM_BTNSELITEM {
5873                if chain.len() < 9
5874                    || chain[4] != elm_array
5875                    || chain[5] < 0
5876                    || chain[7] != elm_array
5877                    || chain[8] < 0
5878                {
5879                    return None;
5880                }
5881                if chain[6] != crate::runtime::forms::codes::ELM_BTNSELITEM_OBJECT {
5882                    return None;
5883                }
5884                let item_idx = chain[5] as usize;
5885                let obj_idx = chain[8] as usize;
5886                let items = stage_state.btnselitem_lists.get(&stage_idx)?;
5887                let item = items.get(item_idx)?;
5888                let obj = item.object_list.get(obj_idx)?;
5889                descend_child_chain(obj, chain, 9, elm_array)
5890            } else {
5891                None
5892            }
5893        })();
5894
5895        let Some(current_obj) = current_obj else {
5896            return false;
5897        };
5898        (child_idx as usize) < current_obj.runtime.child_objects.len()
5899    }
5900
5901    fn object_array_property_op(&self, op: i32) -> bool {
5902        let ids = &self.ctx.ids;
5903        op == crate::runtime::forms::codes::elm_value::OBJECT_CHILD
5904            || (ids.obj_x_rep != 0 && op == ids.obj_x_rep)
5905            || (ids.obj_y_rep != 0 && op == ids.obj_y_rep)
5906            || (ids.obj_z_rep != 0 && op == ids.obj_z_rep)
5907            || (ids.obj_tr_rep != 0 && op == ids.obj_tr_rep)
5908            || (ids.obj_f != 0 && op == ids.obj_f)
5909            || (ids.obj_frame_action_ch != 0 && op == ids.obj_frame_action_ch)
5910    }
5911
5912    fn is_current_object_child_tail(&self, elm: &[i32]) -> bool {
5913        if elm.len() < 2 {
5914            return false;
5915        }
5916        if elm[0] < 0 {
5917            return false;
5918        }
5919        if elm[1] != self.ctx.ids.elm_array && elm[1] != crate::runtime::forms::codes::ELM_ARRAY {
5920            return false;
5921        }
5922        if self.object_array_property_op(elm[0]) {
5923            return false;
5924        }
5925        if elm.len() == 2 {
5926            return true;
5927        }
5928        if self.object_array_property_op(elm[2]) {
5929            return elm[2] == crate::runtime::forms::codes::elm_value::OBJECT_CHILD;
5930        }
5931        elm[2] == self.ctx.ids.elm_array
5932            || elm[2] == crate::runtime::forms::codes::ELM_ARRAY
5933            || elm[2] == crate::runtime::forms::codes::ELM_UP
5934            || self.compact_object_op_allowed(elm[2])
5935    }
5936
5937    fn global_indexed_list_must_dispatch_direct(&self, elm: &[i32]) -> bool {
5938        self.is_global_indexed_list_chain(elm) && !self.is_current_object_child_tail(elm)
5939    }
5940
5941    fn dispatch_global_indexed_list_property_direct(&mut self, elm: &[i32]) -> Result<bool> {
5942        if !self.global_indexed_list_must_dispatch_direct(elm) {
5943            return Ok(false);
5944        }
5945        let form_id = elm[0] as u32;
5946        self.ctx.vm_call = Some(runtime::VmCallMeta {
5947            element: elm.to_vec(),
5948            al_id: 0,
5949            ret_form: self.cfg.fm_int as i64,
5950        });
5951        if !runtime::dispatch_form_code(&mut self.ctx, form_id, &[])? {
5952            self.ctx.vm_call = None;
5953            bail!("unhandled global indexed-list property chain {:?}", elm);
5954        }
5955        self.ctx.vm_call = None;
5956        if let Some(v) = self.ctx.pop() {
5957            self.push_return_value_raw(v);
5958        } else {
5959            bail!("global indexed-list property returned no value: {:?}", elm);
5960        }
5961        Ok(true)
5962    }
5963
5964    fn dispatch_global_indexed_list_assign_direct(&mut self, elm: &[i32], al_id: i32, rhs: Value) -> Result<bool> {
5965        if !self.global_indexed_list_must_dispatch_direct(elm) {
5966            return Ok(false);
5967        }
5968        let form_id = elm[0] as u32;
5969        let args: Vec<Value> = vec![rhs];
5970        self.ctx.vm_call = Some(runtime::VmCallMeta {
5971            element: elm.to_vec(),
5972            al_id: al_id as i64,
5973            ret_form: 0,
5974        });
5975        if !runtime::dispatch_form_code(&mut self.ctx, form_id, &args)? {
5976            self.ctx.vm_call = None;
5977            bail!("unhandled global indexed-list assignment chain {:?}", elm);
5978        }
5979        self.ctx.vm_call = None;
5980        self.ctx.stack.clear();
5981        self.drain_pending_frame_action_finishes()?;
5982        Ok(true)
5983    }
5984
5985    fn dispatch_global_indexed_list_command_direct(
5986        &mut self,
5987        elm: &[i32],
5988        al_id: i32,
5989        ret_form: i32,
5990        args: &mut Vec<Value>,
5991    ) -> Result<bool> {
5992        if !self.global_indexed_list_must_dispatch_direct(elm) {
5993            return Ok(false);
5994        }
5995        let form_id = elm[0] as u32;
5996        self.ctx.vm_call = Some(runtime::VmCallMeta {
5997            element: elm.to_vec(),
5998            al_id: al_id as i64,
5999            ret_form: ret_form as i64,
6000        });
6001        if !runtime::dispatch_form_code(&mut self.ctx, form_id, args)? {
6002            self.ctx.vm_call = None;
6003            bail!("unhandled global indexed-list command chain {:?}", elm);
6004        }
6005        self.ctx.vm_call = None;
6006        self.drain_pending_frame_action_finishes()?;
6007        if ret_form != self.cfg.fm_void {
6008            self.take_ctx_return(ret_form)?;
6009        } else {
6010            self.ctx.stack.clear();
6011        }
6012        Ok(true)
6013    }
6014
6015
6016    fn try_parent_slot_property(&mut self, elm: &[i32]) -> bool {
6017        if elm.len() != 3 || elm[1] != self.ctx.ids.elm_array || elm[2] <= 0 {
6018            return false;
6019        }
6020        // Siglus object-child shorthand reuses the same compact `[slot, ELM_ARRAY, parent]`
6021        // shape as generic parent-slot access. When we are already inside an object chain,
6022        // prefer the object child interpretation so title/menu patno updates like
6023        // `front.object[0].[29] = ...` continue to drive the actual child objects instead
6024        // of disappearing into the generic parent-form property bags.
6025        if self.ctx.globals.current_object_chain.is_some() && self.compact_object_op_allowed(elm[0])
6026        {
6027            return false;
6028        }
6029        let parent_form = elm[2] as u32;
6030        let slot = elm[0];
6031        let ret_form = self
6032            .ctx
6033            .vm_call
6034            .as_ref()
6035            .map(|m| m.ret_form)
6036            .unwrap_or(self.cfg.fm_int as i64);
6037        if ret_form == self.cfg.fm_str as i64 {
6038            let value = self
6039                .ctx
6040                .globals
6041                .str_props
6042                .get(&parent_form)
6043                .and_then(|m| m.get(&slot))
6044                .cloned()
6045                .unwrap_or_default();
6046            self.push_str(value);
6047        } else if let Some(value) = self
6048            .ctx
6049            .globals
6050            .str_props
6051            .get(&parent_form)
6052            .and_then(|m| m.get(&slot))
6053            .cloned()
6054        {
6055            self.push_str(value);
6056        } else {
6057            let value = self
6058                .ctx
6059                .globals
6060                .int_props
6061                .get(&parent_form)
6062                .and_then(|m| m.get(&slot).copied())
6063                .unwrap_or(0);
6064            self.push_int(value as i32);
6065        }
6066        true
6067    }
6068
6069    fn compact_object_op_allowed(&self, op: i32) -> bool {
6070        op >= 0 && op <= 187
6071    }
6072
6073    fn compact_object_op_allowed_for_element(
6074        &self,
6075        elm: &[i32],
6076        allow_ambiguous_single_token_object_op: bool,
6077    ) -> bool {
6078        let Some(op) = elm.first().copied() else {
6079            return false;
6080        };
6081        if elm.len() == 1 && !allow_ambiguous_single_token_object_op {
6082            return false;
6083        }
6084        self.compact_object_op_allowed(op)
6085    }
6086
6087    fn current_object_has_child_index(&self, child_idx: i32) -> bool {
6088        if self.current_object_chain_has_child_index(child_idx) {
6089            return true;
6090        }
6091        if child_idx < 0 {
6092            return false;
6093        }
6094        let Some((stage_idx, obj_idx)) = self.ctx.globals.current_stage_object else {
6095            return false;
6096        };
6097        let stage_form = self.ctx.ids.form_global_stage;
6098        let Some(stage_state) = self.ctx.globals.stage_forms.get(&stage_form) else {
6099            return false;
6100        };
6101        let Some(list) = stage_state.object_lists.get(&stage_idx) else {
6102            return false;
6103        };
6104        let Some(obj) = list.get(obj_idx) else {
6105            return false;
6106        };
6107        (child_idx as usize) < obj.runtime.child_objects.len()
6108    }
6109
6110    fn try_compact_object_chain(
6111        &self,
6112        elm: &[i32],
6113        allow_ambiguous_single_token_object_op: bool,
6114    ) -> Option<Vec<i32>> {
6115        if elm.is_empty() {
6116            return None;
6117        }
6118
6119        let op = elm[0];
6120        if !self.compact_object_op_allowed_for_element(
6121            elm,
6122            allow_ambiguous_single_token_object_op,
6123        ) {
6124            return None;
6125        }
6126
6127        let elm_array = if self.ctx.ids.elm_array != 0 {
6128            self.ctx.ids.elm_array
6129        } else {
6130            crate::runtime::forms::codes::ELM_ARRAY
6131        };
6132        let stage_object = if self.ctx.ids.stage_elm_object != 0 {
6133            self.ctx.ids.stage_elm_object
6134        } else {
6135            crate::runtime::forms::codes::STAGE_ELM_OBJECT
6136        };
6137
6138        let looks_like_absolute_stage_alias_object = elm.len() >= 4
6139            && constants::is_stage_global_form(elm[0] as u32, self.ctx.ids.form_global_stage)
6140            && elm[1] == stage_object
6141            && (elm[2] == elm_array || elm[2] == crate::runtime::forms::codes::ELM_ARRAY);
6142
6143        if looks_like_absolute_stage_alias_object {
6144            return None;
6145        }
6146
6147        // Original command dispatch receives the complete element chain.  The
6148        // only compact form we keep for an ambient object context is the
6149        // explicit child shorthand used after an already-resolved OBJECT.  Do
6150        // not append arbitrary OBJECT op ids to current_object_chain here:
6151        // many unrelated forms share the same numeric element values.
6152        if let Some(prefix) = &self.ctx.globals.current_object_chain {
6153            if self.is_current_object_child_tail(elm) && self.current_object_has_child_index(elm[0]) {
6154                let mut synthetic = prefix.clone();
6155                synthetic.push(crate::runtime::forms::codes::elm_value::OBJECT_CHILD);
6156                synthetic.push(elm_array);
6157                synthetic.push(elm[0]);
6158                if elm.len() > 2 {
6159                    if elm[2] == crate::runtime::forms::codes::elm_value::OBJECT_CHILD {
6160                        synthetic.extend_from_slice(&elm[3..]);
6161                    } else {
6162                        synthetic.extend_from_slice(&elm[2..]);
6163                    }
6164                }
6165                if Self::sg_mwnd_object_trace_enabled()
6166                    && (Self::sg_mwnd_chain_interesting(elm)
6167                        || Self::sg_mwnd_chain_interesting(&synthetic))
6168                {
6169                    eprintln!(
6170                        "[SG_DEBUG][MWND_OBJECT_TRACE][VM] try_compact child-shorthand elm={:?} prefix={:?} synthetic={:?}",
6171                        elm,
6172                        prefix,
6173                        synthetic
6174                    );
6175                }
6176                return Some(synthetic);
6177            }
6178        }
6179
6180        // Explicit compact absolute form: [object_op, stage_no, ARRAY, obj_no, ...]
6181        // This still carries both the stage and object index, so it is not an
6182        // ambient-context guess.
6183        if elm.len() >= 4
6184            && elm[1] >= 0
6185            && (elm[2] == elm_array || elm[2] == crate::runtime::forms::codes::ELM_ARRAY)
6186            && elm[3] >= 0
6187        {
6188            let stage_idx = elm[1];
6189            if !(0..3).contains(&stage_idx) {
6190                return None;
6191            }
6192            let stage_form = if self.ctx.ids.form_global_stage != 0 {
6193                self.ctx.ids.form_global_stage as i32
6194            } else {
6195                crate::runtime::forms::codes::FORM_GLOBAL_STAGE as i32
6196            };
6197            let mut synthetic = vec![
6198                stage_form,
6199                elm_array,
6200                stage_idx,
6201                stage_object,
6202                elm_array,
6203                elm[3],
6204                op,
6205            ];
6206            if elm.len() > 4 {
6207                synthetic.extend_from_slice(&elm[4..]);
6208            }
6209            if Self::sg_mwnd_object_trace_enabled()
6210                && (Self::sg_mwnd_chain_interesting(elm)
6211                    || Self::sg_mwnd_chain_interesting(&synthetic))
6212            {
6213                eprintln!(
6214                    "[SG_DEBUG][MWND_OBJECT_TRACE][VM] try_compact absolute elm={:?} synthetic={:?}",
6215                    elm,
6216                    synthetic
6217                );
6218            }
6219            return Some(synthetic);
6220        }
6221
6222        None
6223    }
6224
6225    fn try_parent_slot_assign(&mut self, elm: &[i32], rhs: &Value) -> bool {
6226        if elm.len() != 3 || elm[1] != self.ctx.ids.elm_array || elm[2] <= 0 {
6227            return false;
6228        }
6229        // See try_parent_slot_property(): inside object chains this compact syntax is used for
6230        // object child operations, not generic parent-form slots.
6231        if self.ctx.globals.current_object_chain.is_some() && self.compact_object_op_allowed(elm[0])
6232        {
6233            return false;
6234        }
6235        let parent_form = elm[2] as u32;
6236        let slot = elm[0];
6237        match rhs {
6238            Value::Str(s) => {
6239                self.ctx
6240                    .globals
6241                    .str_props
6242                    .entry(parent_form)
6243                    .or_default()
6244                    .insert(slot, s.clone());
6245            }
6246            Value::Int(n) => {
6247                self.ctx
6248                    .globals
6249                    .int_props
6250                    .entry(parent_form)
6251                    .or_default()
6252                    .insert(slot, *n);
6253            }
6254            Value::NamedArg { value, .. } => return self.try_parent_slot_assign(elm, value),
6255            _ => return false,
6256        }
6257        true
6258    }
6259
6260    fn exec_property(&mut self, mut elm: Vec<i32>) -> Result<()> {
6261        if std::env::var_os("SG_TITLE_CHAIN_TRACE").is_some()
6262            && self.current_scene_name.as_deref() == Some("sys10_tt01")
6263            && matches!(elm.first().copied(), Some(83 | 84 | 24 | 25))
6264        {
6265            eprintln!(
6266                "[SG_TITLE_CHAIN_TRACE] line={} elm={:?} current_object_chain={:?} current_stage_object={:?}",
6267                self.current_line_no,
6268                elm,
6269                self.ctx.globals.current_object_chain,
6270                self.ctx.globals.current_stage_object
6271            );
6272        }
6273        self.vm_trace(None, format!("exec_property enter elm={:?}", elm));
6274        if elm.is_empty() {
6275            self.push_int(0);
6276            return Ok(());
6277        }
6278        // Call-local properties (declared by CD_DEC_PROP / populated by CD_ARG).
6279        if self.exec_call_property(&elm)? {
6280            self.vm_trace(
6281                None,
6282                format!("exec_property handled by call-property elm={:?}", elm),
6283            );
6284            return Ok(());
6285        }
6286
6287        let head = elm[0];
6288        let head_owner = elm_code::owner(head);
6289        if head_owner == elm_code::ELM_OWNER_CALL_PROP {
6290            let current_idx = self
6291                .current_call_frame_index()
6292                .ok_or_else(|| anyhow!("call stack underflow"))?;
6293            let call_prop_id = elm_code::code(head) as i32;
6294            let prop_idx = self
6295                .find_call_prop_index_in_frame(current_idx, call_prop_id)
6296                .ok_or_else(|| {
6297                    anyhow!("missing direct CALL_PROP id={} for {:?}", call_prop_id, elm)
6298                })?;
6299            let prop = self.call_stack[current_idx].user_props[prop_idx].clone();
6300            if let Some(composed) = self.compose_call_prop_tail(&prop, &elm[1..]) {
6301                self.exec_property(composed)?;
6302                self.vm_trace(
6303                    None,
6304                    format!("exec_property direct CALL_PROP composed elm={:?}", elm),
6305                );
6306                return Ok(());
6307            }
6308            self.push_call_prop_result(&prop, &elm[1..], &elm)?;
6309            self.vm_trace(
6310                None,
6311                format!("exec_property direct CALL_PROP elm={:?}", elm),
6312            );
6313            return Ok(());
6314        }
6315
6316        if head_owner == elm_code::ELM_OWNER_USER_PROP {
6317            let prop_id = elm_code::code(head);
6318            let cell = self
6319                .user_props
6320                .get(&prop_id)
6321                .cloned()
6322                .unwrap_or_else(|| self.default_user_prop_cell(prop_id));
6323            let array_idx = self.extract_array_index(&elm);
6324            self.trace_cf_condition_user_prop_read(
6325                self.stream.get_prg_cntr(),
6326                prop_id,
6327                array_idx,
6328                &cell,
6329                &elm,
6330            );
6331            self.push_user_prop_cell_result(&cell, &elm[1..], &elm)?;
6332            self.vm_trace(
6333                None,
6334                format!("exec_property direct USER_PROP elm={:?}", elm),
6335            );
6336            return Ok(());
6337        }
6338
6339        if head_owner != elm_code::ELM_OWNER_FORM {
6340            bail!(
6341                "unsupported property owner {} for element {:?}",
6342                head_owner,
6343                elm
6344            );
6345        }
6346
6347        if self.dispatch_global_indexed_list_property_direct(&elm)? {
6348            self.vm_trace(None, format!("exec_property handled by global indexed-list elm={:?}", elm));
6349            return Ok(());
6350        }
6351
6352        if self.try_parent_slot_property(&elm) {
6353            self.vm_trace(
6354                None,
6355                format!("exec_property handled by parent-slot elm={:?}", elm),
6356            );
6357            return Ok(());
6358        }
6359        if let Some(synthetic) = self.try_compact_object_chain(&elm, false) {
6360            self.vm_trace(
6361                None,
6362                format!(
6363                    "exec_property compact-object elm={:?} synthetic={:?}",
6364                    elm, synthetic
6365                ),
6366            );
6367            self.ctx.vm_call = Some(runtime::VmCallMeta {
6368                element: synthetic.clone(),
6369                al_id: 0,
6370                ret_form: self.cfg.fm_int as i64,
6371            });
6372            let form_id = self.canonical_runtime_form_id(synthetic[0] as u32);
6373            if !runtime::dispatch_form_code(&mut self.ctx, form_id, &[])? {
6374                self.ctx.vm_call = None;
6375                bail!(
6376                    "unhandled compact object property chain {:?} -> {:?}",
6377                    elm,
6378                    synthetic
6379                );
6380            }
6381            self.ctx.vm_call = None;
6382            self.update_compact_context_from_object_dispatch_chain(&synthetic);
6383            if let Some(v) = self.ctx.pop() {
6384                self.push_return_value_raw(v);
6385            } else {
6386                bail!("compact object property chain returned no value: {:?}", elm);
6387            }
6388            return Ok(());
6389        }
6390
6391        let form_id = self.canonical_runtime_form_id(head as u32);
6392        let args: Vec<Value> = Vec::new();
6393        self.ctx.vm_call = Some(runtime::VmCallMeta {
6394            element: elm.clone(),
6395            al_id: 0,
6396            ret_form: self.cfg.fm_int as i64,
6397        });
6398
6399        self.vm_trace(
6400            None,
6401            format!("exec_property dispatch form_id={} elm={:?}", form_id, elm),
6402        );
6403        if !runtime::dispatch_form_code(&mut self.ctx, form_id, &args)? {
6404            self.ctx.vm_call = None;
6405            bail!("unhandled form property chain {:?}", elm);
6406        }
6407
6408        self.ctx.vm_call = None;
6409        if let Some(v) = self.ctx.pop() {
6410            self.push_return_value_raw(v);
6411        } else {
6412            bail!("property chain returned no value: {:?}", elm);
6413        }
6414
6415        Ok(())
6416    }
6417
6418    fn exec_assign(&mut self, elm: Vec<i32>, al_id: i32, rhs: Value) -> Result<()> {
6419        if elm.is_empty() {
6420            return Ok(());
6421        }
6422
6423        self.trace_cgm_coord_assign(&elm, &rhs);
6424
6425        // Call-local property assignment.
6426        if self.exec_call_assign(&elm, al_id, rhs.clone())? {
6427            return Ok(());
6428        }
6429
6430        let head = elm[0];
6431        let head_owner = elm_code::owner(head);
6432        if head_owner == elm_code::ELM_OWNER_CALL_PROP {
6433            let current_idx = self
6434                .current_call_frame_index()
6435                .ok_or_else(|| anyhow!("call stack underflow"))?;
6436            let call_prop_id = elm_code::code(head) as i32;
6437            let prop_idx = self
6438                .find_call_prop_index_in_frame(current_idx, call_prop_id)
6439                .ok_or_else(|| {
6440                    anyhow!(
6441                        "missing direct CALL_PROP assign id={} for {:?}",
6442                        call_prop_id,
6443                        elm
6444                    )
6445                })?;
6446            let prop_for_compose = self.call_stack[current_idx].user_props[prop_idx].clone();
6447            if let Some(composed) = self.compose_call_prop_tail(&prop_for_compose, &elm[1..]) {
6448                self.exec_assign(composed, al_id, rhs)?;
6449                return Ok(());
6450            }
6451            let frame = self
6452                .call_stack
6453                .get_mut(current_idx)
6454                .ok_or_else(|| anyhow!("call stack underflow"))?;
6455            let prop = frame.user_props.get_mut(prop_idx).ok_or_else(|| {
6456                anyhow!("missing direct CALL_PROP slot assign id={}", call_prop_id)
6457            })?;
6458            Self::assign_call_prop_result(prop, &elm[1..], rhs)?;
6459            return Ok(());
6460        }
6461
6462        if head_owner == elm_code::ELM_OWNER_USER_PROP {
6463            let prop_id = elm_code::code(head);
6464            let array_idx = self.extract_array_index(&elm);
6465            let old_cell = self.user_props.get(&prop_id).cloned();
6466            self.assign_user_prop(prop_id, array_idx, rhs.clone());
6467            let new_cell = self.user_props.get(&prop_id);
6468            self.trace_cf_condition_user_prop_assign(
6469                self.stream.get_prg_cntr(),
6470                prop_id,
6471                array_idx,
6472                old_cell.as_ref(),
6473                new_cell,
6474                &rhs,
6475                &elm,
6476            );
6477            return Ok(());
6478        }
6479
6480        if head_owner != elm_code::ELM_OWNER_FORM {
6481            bail!(
6482                "unsupported assignment owner {} for element {:?}",
6483                head_owner,
6484                elm
6485            );
6486        }
6487
6488        if self.dispatch_global_indexed_list_assign_direct(&elm, al_id, rhs.clone())? {
6489            return Ok(());
6490        }
6491
6492        if self.try_parent_slot_assign(&elm, &rhs) {
6493            return Ok(());
6494        }
6495        if let Some(synthetic) = self.try_compact_object_chain(&elm, true) {
6496            self.vm_trace(
6497                None,
6498                format!(
6499                    "exec_assign compact-object elm={:?} synthetic={:?} al_id={} rhs={:?}",
6500                    elm, synthetic, al_id, rhs
6501                ),
6502            );
6503            let args: Vec<Value> = vec![rhs];
6504            self.ctx.vm_call = Some(runtime::VmCallMeta {
6505                element: synthetic.clone(),
6506                al_id: al_id as i64,
6507                ret_form: 0,
6508            });
6509            let form_id = self.canonical_runtime_form_id(synthetic[0] as u32);
6510            if !runtime::dispatch_form_code(&mut self.ctx, form_id, &args)? {
6511                self.ctx.vm_call = None;
6512                bail!(
6513                    "unhandled compact object assignment chain {:?} -> {:?}",
6514                    elm,
6515                    synthetic
6516                );
6517            }
6518            self.ctx.vm_call = None;
6519            self.update_compact_context_from_object_dispatch_chain(&synthetic);
6520            self.ctx.stack.clear();
6521            self.drain_pending_frame_action_finishes()?;
6522            return Ok(());
6523        }
6524
6525        let form_id = self.canonical_runtime_form_id(head as u32);
6526        self.vm_trace(
6527            None,
6528            format!(
6529                "exec_assign dispatch form_id={} elm={:?} al_id={} rhs={:?}",
6530                form_id, elm, al_id, rhs
6531            ),
6532        );
6533        let args: Vec<Value> = vec![rhs];
6534        if (std::env::var_os("SIGLUS_TRACE_VM_COMMANDS").is_some()) {
6535            eprintln!(
6536                "[vm form assign] form={} al_id={} elm={:?} rhs={:?}",
6537                form_id,
6538                al_id,
6539                elm,
6540                args.first()
6541            );
6542        }
6543        self.ctx.vm_call = Some(runtime::VmCallMeta {
6544            element: elm.clone(),
6545            al_id: al_id as i64,
6546            ret_form: 0,
6547        });
6548
6549        if !runtime::dispatch_form_code(&mut self.ctx, form_id, &args)? {
6550            self.ctx.vm_call = None;
6551            bail!("unhandled form assignment chain {:?}", elm);
6552        }
6553        self.ctx.vm_call = None;
6554        self.ctx.stack.clear();
6555        self.drain_pending_frame_action_finishes()?;
6556        Ok(())
6557    }
6558
6559    fn dispatch_owner_named_command(
6560        &mut self,
6561        owner: u8,
6562        raw_head: i32,
6563        ret_form: i32,
6564        args: &[Value],
6565    ) -> Result<bool> {
6566        let cmd_no = elm_code::code(raw_head) as u32;
6567        if owner == elm_code::ELM_OWNER_USER_CMD {
6568            // C++ tnm_command_proc_user_cmd() does not execute the user command
6569            // synchronously. It sets the caller ret_form, saves the call frame,
6570            // jumps the lexer to the user command, then pushes call arguments onto
6571            // the VM stack. The return value is pushed later by
6572            // tnm_command_proc_return(), immediately before the caller resumes.
6573            //
6574            // The previous Rust inline path restored the caller PC and tried to
6575            // synthesize a return value inside CD_COMMAND. That is not equivalent
6576            // for user commands that wait, switch proc, or otherwise return later,
6577            // and it causes the following instruction to pop from an empty int
6578            // stack. Enter the user command as an actual VM call instead.
6579            let inc_cmd_cnt = self.call_cmd_names.len() as u32;
6580            let local_cmd_no = if cmd_no < inc_cmd_cnt {
6581                let Some(name) = self.call_cmd_names.get(&cmd_no) else {
6582                    return Ok(false);
6583                };
6584                match self.user_cmd_names.iter().find_map(|(no, local_name)| {
6585                    if local_name.eq_ignore_ascii_case(name) {
6586                        Some(*no)
6587                    } else {
6588                        None
6589                    }
6590                }) {
6591                    Some(no) => no,
6592                    None => return Ok(false),
6593                }
6594            } else {
6595                cmd_no - inc_cmd_cnt
6596            };
6597
6598            let Some(name) = self.user_cmd_names.get(&local_cmd_no).cloned() else {
6599                self.sg_omv_trace(format!(
6600                    "USER_CMD unresolved raw_head={} cmd_no={} local_cmd_no={} inc_cmd_cnt={} ret_form={} argc={}",
6601                    raw_head,
6602                    cmd_no,
6603                    local_cmd_no,
6604                    inc_cmd_cnt,
6605                    ret_form,
6606                    args.len()
6607                ));
6608                return Ok(false);
6609            };
6610            let offset = self.stream.scn_cmd_offset(local_cmd_no as usize)?;
6611            self.sg_omv_trace(format!(
6612                "USER_CMD enter name={} raw_head={} cmd_no={} local_cmd_no={} offset=0x{:x} ret_form={} argc={} current_pc=0x{:x}",
6613                name,
6614                raw_head,
6615                cmd_no,
6616                local_cmd_no,
6617                offset,
6618                ret_form,
6619                args.len(),
6620                self.stream.get_prg_cntr()
6621            ));
6622            return self.enter_current_scene_user_cmd_proc_at_offset(
6623                offset,
6624                ret_form,
6625                args,
6626                false,
6627                false,
6628            );
6629        }
6630
6631        let name = match owner {
6632            o if o == elm_code::ELM_OWNER_CALL_CMD => self.call_cmd_names.get(&cmd_no).cloned(),
6633            _ => None,
6634        };
6635        let Some(name) = name else {
6636            return Ok(false);
6637        };
6638        runtime::dispatch_named_command(&mut self.ctx, &name, args)
6639    }
6640
6641    fn command_consumes_read_flag_no(&self, elm: &[i32]) -> bool {
6642        fn global_consumes(op: i32) -> bool {
6643            matches!(
6644                op,
6645                crate::runtime::forms::codes::elm_value::GLOBAL_PRINT
6646                    | crate::runtime::forms::codes::elm_value::GLOBAL_SEL
6647                    | crate::runtime::forms::codes::elm_value::GLOBAL_SEL_CANCEL
6648                    | crate::runtime::forms::codes::elm_value::GLOBAL_SELMSG
6649                    | crate::runtime::forms::codes::elm_value::GLOBAL_SELMSG_CANCEL
6650                    | crate::runtime::forms::codes::elm_value::GLOBAL_SELBTN
6651                    | crate::runtime::forms::codes::elm_value::GLOBAL_SELBTN_CANCEL
6652                    | crate::runtime::forms::codes::elm_value::GLOBAL_SELBTN_START
6653                    | crate::runtime::forms::codes::elm_value::GLOBAL_KOE
6654                    | crate::runtime::forms::codes::elm_value::GLOBAL_KOE_PLAY_WAIT
6655                    | crate::runtime::forms::codes::elm_value::GLOBAL_KOE_PLAY_WAIT_KEY
6656            )
6657        }
6658
6659        fn mwnd_consumes(op: i32) -> bool {
6660            matches!(
6661                op,
6662                crate::runtime::forms::codes::elm_value::MWND_PRINT
6663                    | crate::runtime::forms::codes::elm_value::MWND_SEL
6664                    | crate::runtime::forms::codes::elm_value::MWND_SEL_CANCEL
6665                    | crate::runtime::forms::codes::elm_value::MWND_SELMSG
6666                    | crate::runtime::forms::codes::elm_value::MWND_SELMSG_CANCEL
6667                    | crate::runtime::forms::codes::elm_value::MWND_KOE
6668                    | crate::runtime::forms::codes::elm_value::MWND_KOE_PLAY_WAIT
6669                    | crate::runtime::forms::codes::elm_value::MWND_KOE_PLAY_WAIT_KEY
6670            )
6671        }
6672
6673        // C++ consumes the read-flag integer inside the concrete command
6674        // handler after CD_COMMAND has read the command metadata. The Rust VM
6675        // has to make the same decision from the actual element chain. Do not
6676        // assume the chain is exactly [FORM, OP]: commands can arrive through
6677        // object, mwnd, and global aliases, so scan every adjacent form/op pair.
6678        for pair in elm.windows(2) {
6679            let form_id = self.canonical_runtime_form_id(pair[0] as u32) as i32;
6680            let op = pair[1];
6681            if form_id == crate::runtime::forms::codes::FM_GLOBAL && global_consumes(op) {
6682                return true;
6683            }
6684            if form_id == crate::runtime::forms::codes::FM_MWND && mwnd_consumes(op) {
6685                return true;
6686            }
6687        }
6688
6689        false
6690    }
6691
6692    fn exec_command(
6693        &mut self,
6694        elm: Vec<i32>,
6695        al_id: i32,
6696        ret_form: i32,
6697        args: &mut Vec<Value>,
6698    ) -> Result<()> {
6699        if elm.is_empty() {
6700            self.push_default_for_ret(ret_form);
6701            return Ok(());
6702        }
6703
6704        if self.exec_call_command(&elm, al_id, ret_form, args)? {
6705            return Ok(());
6706        }
6707
6708        let raw_head = elm[0];
6709        let owner = elm_code::owner(raw_head);
6710
6711        if owner == elm_code::ELM_OWNER_CALL_PROP {
6712            let current_idx = self
6713                .current_call_frame_index()
6714                .ok_or_else(|| anyhow!("call stack underflow"))?;
6715            let call_prop_id = elm_code::code(raw_head) as i32;
6716            let prop_idx = self
6717                .find_call_prop_index_in_frame(current_idx, call_prop_id)
6718                .ok_or_else(|| {
6719                    anyhow!("missing direct CALL_PROP command id={} for {:?}", call_prop_id, elm)
6720                })?;
6721            let prop = self.call_stack[current_idx].user_props[prop_idx].clone();
6722            if let Some(composed) = self.compose_call_prop_tail(&prop, &elm[1..]) {
6723                self.exec_command(composed, al_id, ret_form, args)?;
6724                return Ok(());
6725            }
6726            self.push_default_for_ret(ret_form);
6727            return Ok(());
6728        }
6729
6730        if owner == elm_code::ELM_OWNER_USER_PROP {
6731            let prop_id = elm_code::code(raw_head);
6732            if self.exec_user_prop_list_init_command(prop_id, &elm[1..], ret_form)? {
6733                return Ok(());
6734            }
6735            let cell = self
6736                .user_props
6737                .get(&prop_id)
6738                .cloned()
6739                .unwrap_or_else(|| self.default_user_prop_cell(prop_id));
6740            if let Some(composed) = self.compose_user_prop_tail(prop_id, &cell, &elm[1..]) {
6741                self.exec_command(composed, al_id, ret_form, args)?;
6742                return Ok(());
6743            }
6744            self.push_default_for_ret(ret_form);
6745            return Ok(());
6746        }
6747
6748        match owner {
6749            o if o == elm_code::ELM_OWNER_FORM => {
6750                // Suppress only the exact residual bare [GLOBAL.WIPE] command shape
6751                // observed at sys20_adv01 loop-increment sites. Real WIPE calls with
6752                // arguments still go through global.rs.
6753                if elm.len() == 1
6754                    && elm[0] == crate::runtime::forms::codes::elm_value::GLOBAL_WIPE
6755                    && args.is_empty()
6756                    && ret_form == self.cfg.fm_void
6757                {
6758                    self.vm_trace(None, "suppress bare residual GLOBAL.WIPE command".to_string());
6759                    return Ok(());
6760                }
6761
6762                if self.dispatch_global_indexed_list_command_direct(&elm, al_id, ret_form, args)? {
6763                    return Ok(());
6764                }
6765                if let Some(synthetic) = self.try_compact_object_chain(&elm, true) {
6766                    self.vm_trace(
6767                        None,
6768                        format!(
6769                            "exec_command compact-object elm={:?} synthetic={:?} al_id={} ret_form={} args={:?}",
6770                            elm, synthetic, al_id, ret_form, args
6771                        ),
6772                    );
6773                    if Self::sg_mwnd_object_trace_enabled()
6774                        && (Self::sg_mwnd_chain_interesting(&elm) || Self::sg_mwnd_chain_interesting(&synthetic))
6775                    {
6776                        self.sg_mwnd_object_trace(format!(
6777                            "exec_command compact elm={:?} synthetic={:?} al_id={} ret_form={} args={:?} current_chain={:?} current_stage_object={:?}",
6778                            elm,
6779                            synthetic,
6780                            al_id,
6781                            ret_form,
6782                            args,
6783                            self.ctx.globals.current_object_chain,
6784                            self.ctx.globals.current_stage_object
6785                        ));
6786                    }
6787                    self.ctx.vm_call = Some(runtime::VmCallMeta {
6788                        element: synthetic.clone(),
6789                        al_id: al_id as i64,
6790                        ret_form: ret_form as i64,
6791                    });
6792                    let form_id = self.canonical_runtime_form_id(synthetic[0] as u32) as i32;
6793                    let op_id = if synthetic.len() >= 2 { synthetic[1] } else { al_id };
6794                    self.sg_omv_trace_command(
6795                        "compact",
6796                        &synthetic,
6797                        form_id,
6798                        op_id,
6799                        al_id,
6800                        ret_form,
6801                        args,
6802                    );
6803                    if !runtime::dispatch_form_code(&mut self.ctx, form_id as u32, args)? {
6804                        self.ctx.vm_call = None;
6805                        bail!(
6806                            "unhandled compact object command chain {:?} -> {:?}",
6807                            elm,
6808                            synthetic
6809                        );
6810                    }
6811                    self.ctx.vm_call = None;
6812                    self.update_compact_context_from_object_dispatch_chain(&synthetic);
6813                    self.drain_pending_frame_action_finishes()?;
6814                    if ret_form != self.cfg.fm_void {
6815                        if !self.ctx.stack.is_empty() {
6816                            self.take_ctx_return(ret_form)?;
6817                            return Ok(());
6818                        }
6819                        if self.ctx.wait_poll() {
6820                            if let Some(frame) = self.call_stack.last_mut() {
6821                                frame.delayed_ret_form = Some(ret_form);
6822                            } else {
6823                                self.delayed_ret_form = Some(ret_form);
6824                            }
6825                            return Ok(());
6826                        }
6827                    }
6828                    self.take_ctx_return(ret_form)?;
6829                    return Ok(());
6830                }
6831
6832                let form_id = self.canonical_runtime_form_id(raw_head as u32) as i32;
6833                if self.exec_builtin_global_control(form_id, ret_form)? {
6834                    if ret_form != self.cfg.fm_void {
6835                        self.take_ctx_return(ret_form)?;
6836                    } else {
6837                        self.ctx.stack.clear();
6838                    }
6839                    return Ok(());
6840                }
6841                if self.exec_builtin_scene_form(&elm, form_id, al_id, ret_form, args)? {
6842                    return Ok(());
6843                }
6844
6845                let op_id = if elm.len() >= 2 { elm[1] } else { al_id };
6846                self.ctx.vm_call = Some(runtime::VmCallMeta {
6847                    element: elm.clone(),
6848                    al_id: al_id as i64,
6849                    ret_form: ret_form as i64,
6850                });
6851
6852                if (std::env::var_os("SIGLUS_TRACE_VM_COMMANDS").is_some()) {
6853                    let elm_tail = elm
6854                        .iter()
6855                        .map(|v| v.to_string())
6856                        .collect::<Vec<_>>()
6857                        .join(",");
6858                    let args_dbg = args
6859                        .iter()
6860                        .map(|v| format!("{v:?}"))
6861                        .collect::<Vec<_>>()
6862                        .join(", ");
6863                    eprintln!(
6864                        "[vm form cmd] form={} op={} argc={} ret_form={} al_id={} elm=[{}] args=[{}]",
6865                        form_id,
6866                        op_id,
6867                        args.len(),
6868                        ret_form,
6869                        al_id,
6870                        elm_tail,
6871                        args_dbg
6872                    );
6873                }
6874
6875                self.sg_omv_trace_command(
6876                    "dispatch",
6877                    &elm,
6878                    form_id,
6879                    op_id,
6880                    al_id,
6881                    ret_form,
6882                    args,
6883                );
6884
6885                if !runtime::dispatch_form_code(&mut self.ctx, form_id as u32, args)? {
6886                    self.ctx.vm_call = None;
6887                    bail!("unhandled form command chain {:?}", elm);
6888                }
6889                self.ctx.vm_call = None;
6890                self.drain_pending_frame_action_finishes()?;
6891            }
6892            o if o == elm_code::ELM_OWNER_USER_CMD || o == elm_code::ELM_OWNER_CALL_CMD => {
6893                if (std::env::var_os("SIGLUS_TRACE_VM_COMMANDS").is_some()) {
6894                    let cmd_no = elm_code::code(raw_head);
6895                    let elm_tail = elm
6896                        .iter()
6897                        .map(|v| v.to_string())
6898                        .collect::<Vec<_>>()
6899                        .join(",");
6900                    let args_dbg = args
6901                        .iter()
6902                        .map(|v| format!("{v:?}"))
6903                        .collect::<Vec<_>>()
6904                        .join(", ");
6905                    eprintln!(
6906                        "[vm owner cmd] owner={} cmd_no={} argc={} ret_form={} al_id={} elm=[{}] args=[{}]",
6907                        owner,
6908                        cmd_no,
6909                        args.len(),
6910                        ret_form,
6911                        al_id,
6912                        elm_tail,
6913                        args_dbg
6914                    );
6915                }
6916
6917                if !self.dispatch_owner_named_command(owner, raw_head, ret_form, args)? {
6918                    bail!("unhandled owner command chain {:?}", elm);
6919                }
6920                if owner == elm_code::ELM_OWNER_USER_CMD {
6921                    // USER_CMD has transferred control to the callee. Its return
6922                    // value will be materialized by CD_RETURN when that callee
6923                    // finishes, so CD_COMMAND must not consume ctx.stack now.
6924                    return Ok(());
6925                }
6926            }
6927            _ => {
6928                bail!("unsupported command owner {} for element {:?}", owner, elm);
6929            }
6930        }
6931
6932        if ret_form != self.cfg.fm_void {
6933            if !self.ctx.stack.is_empty() {
6934                self.take_ctx_return(ret_form)?;
6935                return Ok(());
6936            }
6937            if self.ctx.wait_poll() {
6938                if let Some(frame) = self.call_stack.last_mut() {
6939                    frame.delayed_ret_form = Some(ret_form);
6940                } else {
6941                    self.delayed_ret_form = Some(ret_form);
6942                }
6943                return Ok(());
6944            }
6945        }
6946
6947        self.take_ctx_return(ret_form)?;
6948        Ok(())
6949    }
6950
6951
6952    fn save_kind_to_original(kind: RuntimeSaveKind) -> Option<crate::original_save::SaveKind> {
6953        match kind {
6954            RuntimeSaveKind::Normal => Some(crate::original_save::SaveKind::Normal),
6955            RuntimeSaveKind::Quick => Some(crate::original_save::SaveKind::Quick),
6956            RuntimeSaveKind::End => Some(crate::original_save::SaveKind::End),
6957            RuntimeSaveKind::Inner => None,
6958        }
6959    }
6960
6961    fn configured_runtime_save_count(&self, quick: bool) -> usize {
6962        let keys: [&str; 2] = if quick {
6963            ["#QUICK_SAVE.CNT", "QUICK_SAVE.CNT"]
6964        } else {
6965            ["#SAVE.CNT", "SAVE.CNT"]
6966        };
6967        let default_count = if quick { 3 } else { 10 };
6968        self.ctx
6969            .tables
6970            .gameexe
6971            .as_ref()
6972            .and_then(|cfg| keys.iter().find_map(|key| cfg.get_usize(*key)))
6973            .unwrap_or(default_count)
6974            .min(10000)
6975    }
6976
6977    fn runtime_save_file_path(&self, kind: RuntimeSaveKind, index: usize) -> Option<std::path::PathBuf> {
6978        let save_kind = Self::save_kind_to_original(kind)?;
6979        let save_cnt = self.configured_runtime_save_count(false);
6980        let quick_cnt = self.configured_runtime_save_count(true);
6981        Some(crate::original_save::save_file_path_with_counts(
6982            &self.ctx.project_dir,
6983            save_cnt,
6984            quick_cnt,
6985            save_kind,
6986            index,
6987        ))
6988    }
6989
6990    fn stamp_slot_with_local_time(slot: &mut crate::runtime::globals::SaveSlotState) {
6991        let now = crate::platform_time::local_time_fields();
6992        slot.exist = true;
6993        slot.year = now.year as i64;
6994        slot.month = now.month as i64;
6995        slot.day = now.day as i64;
6996        // SYSTEMTIME.wDayOfWeek uses 0..6 with Sunday = 0.
6997        slot.weekday = now.weekday_sunday0 as i64;
6998        slot.hour = now.hour as i64;
6999        slot.minute = now.minute as i64;
7000        slot.second = now.second as i64;
7001        slot.millisecond = now.millisecond as i64;
7002    }
7003
7004    /// Build the slot record that ends up in the save file header and in the in-memory
7005    /// `save_slots` / `quick_save_slots` tables. Mirrors C++ `tnm_save_local_on_file`:
7006    /// timestamps come from `GetLocalTime` (i.e. "now"), while the textual fields
7007    /// (title / message / full_message / append_dir / append_name) come from the
7008    /// engine's m_local_save snapshot.
7009    ///
7010    /// Inner-save still pulls textual fields from live runtime state because the
7011    /// inner-save path here is the only consumer that doesn't go through SAVEPOINT.
7012    fn ensure_runtime_slot_for_save(&mut self, req: RuntimeSaveRequest) -> crate::runtime::globals::SaveSlotState {
7013        let mut slot = crate::runtime::globals::SaveSlotState::default();
7014        Self::stamp_slot_with_local_time(&mut slot);
7015        if let Some(snapshot) = self.ctx.local_save_snapshot.as_ref() {
7016            slot.title = snapshot.save_scene_title.clone();
7017            slot.message = snapshot.save_msg.clone();
7018            slot.full_message = if snapshot.save_full_msg.is_empty() {
7019                snapshot.save_msg.clone()
7020            } else {
7021                snapshot.save_full_msg.clone()
7022            };
7023            slot.append_dir = snapshot.append_dir.clone();
7024            slot.append_name = snapshot.append_name.clone();
7025        } else {
7026            // No snapshot exists (e.g. inner save before any SAVEPOINT). Fall back to
7027            // live runtime values so inner-save still records something meaningful.
7028            slot.title = self.ctx.globals.syscom.current_save_scene_title.clone();
7029            slot.message = self.ctx.globals.syscom.current_save_message.clone();
7030            slot.full_message = if self.ctx.globals.syscom.current_save_full_message.is_empty() {
7031                self.ctx.globals.syscom.current_save_message.clone()
7032            } else {
7033                self.ctx.globals.syscom.current_save_full_message.clone()
7034            };
7035            slot.append_dir = self.ctx.globals.append_dir.clone();
7036            slot.append_name = self.ctx.globals.append_name.clone();
7037        }
7038
7039        match req.kind {
7040            RuntimeSaveKind::Normal => {
7041                if self.ctx.globals.syscom.save_slots.len() <= req.index {
7042                    self.ctx.globals.syscom.save_slots.resize_with(req.index + 1, Default::default);
7043                }
7044                self.ctx.globals.syscom.save_slots[req.index] = slot.clone();
7045            }
7046            RuntimeSaveKind::Quick => {
7047                if self.ctx.globals.syscom.quick_save_slots.len() <= req.index {
7048                    self.ctx.globals.syscom.quick_save_slots.resize_with(req.index + 1, Default::default);
7049                }
7050                self.ctx.globals.syscom.quick_save_slots[req.index] = slot.clone();
7051            }
7052            RuntimeSaveKind::End | RuntimeSaveKind::Inner => {}
7053        }
7054        slot
7055    }
7056
7057    fn local_flag_count(&self) -> usize {
7058        self.ctx
7059            .tables
7060            .gameexe
7061            .as_ref()
7062            .and_then(|cfg| cfg.get_usize("#FLAG.CNT").or_else(|| cfg.get_usize("FLAG.CNT")))
7063            .unwrap_or(1000)
7064            .min(10000)
7065    }
7066
7067    fn mwnd_waku_btn_count(&self) -> usize {
7068        self.ctx
7069            .tables
7070            .gameexe
7071            .as_ref()
7072            .and_then(|cfg| cfg.get_usize("#WAKU.BTN.CNT").or_else(|| cfg.get_usize("WAKU.BTN.CNT")))
7073            .unwrap_or(8)
7074            .min(256)
7075    }
7076
7077    fn int_list_by_element(&self, elm: i32) -> &[i64] {
7078        self.ctx
7079            .globals
7080            .int_lists
7081            .get(&(elm as u32))
7082            .map(Vec::as_slice)
7083            .unwrap_or(&[])
7084    }
7085
7086    fn str_list_by_element(&self, elm: i32) -> &[String] {
7087        self.ctx
7088            .globals
7089            .str_lists
7090            .get(&(elm as u32))
7091            .map(Vec::as_slice)
7092            .unwrap_or(&[])
7093    }
7094
7095    fn build_cpp_local_data_pod(&self) -> Vec<u8> {
7096        let mut out = Vec::with_capacity(356);
7097        let script = &self.ctx.globals.script;
7098        let syscom = &self.ctx.globals.syscom;
7099        let push_i32 = |out: &mut Vec<u8>, v: i64| out.extend_from_slice(&(v as i32).to_le_bytes());
7100        let push_bool = |out: &mut Vec<u8>, v: bool| out.push(if v { 1 } else { 0 });
7101
7102        push_i32(&mut out, 0);
7103        push_i32(&mut out, 0);
7104        push_i32(&mut out, 0);
7105        push_i32(&mut out, 0);
7106        push_i32(&mut out, script.cursor_no);
7107
7108        push_bool(&mut out, syscom.syscom_menu_disable);
7109        push_bool(&mut out, script.hide_mwnd_disable);
7110        push_bool(&mut out, script.msg_back_disable);
7111        push_bool(&mut out, script.shortcut_disable);
7112
7113        push_bool(&mut out, script.skip_disable);
7114        push_bool(&mut out, script.ctrl_disable);
7115        push_bool(&mut out, script.not_stop_skip_by_click);
7116        push_bool(&mut out, script.not_skip_msg_by_click);
7117        push_bool(&mut out, script.skip_unread_message);
7118        push_bool(&mut out, script.auto_mode_flag);
7119        while out.len() % 4 != 0 { out.push(0); }
7120        push_i32(&mut out, script.auto_mode_moji_wait);
7121        push_i32(&mut out, script.auto_mode_min_wait);
7122        push_i32(&mut out, script.auto_mode_moji_cnt);
7123        push_i32(&mut out, script.mouse_cursor_hide_onoff);
7124        push_i32(&mut out, script.mouse_cursor_hide_time);
7125        push_i32(&mut out, 0);
7126
7127        push_i32(&mut out, script.msg_speed);
7128        push_bool(&mut out, script.msg_nowait);
7129        push_bool(&mut out, script.async_msg_mode);
7130        push_bool(&mut out, script.async_msg_mode_once);
7131        push_bool(&mut out, false);
7132        push_bool(&mut out, script.skip_trigger);
7133        push_bool(&mut out, script.koe_dont_stop_on_flag);
7134        push_bool(&mut out, script.koe_dont_stop_off_flag);
7135
7136        push_bool(&mut out, syscom.mwnd_btn_disable_all);
7137        push_bool(&mut out, syscom.mwnd_btn_touch_disable);
7138        push_bool(&mut out, script.mwnd_anime_on_flag);
7139        push_bool(&mut out, script.mwnd_anime_off_flag);
7140        push_bool(&mut out, script.mwnd_disp_off_flag);
7141
7142        push_bool(&mut out, script.msg_back_off);
7143        push_bool(&mut out, script.msg_back_disp_off);
7144        while out.len() % 4 != 0 { out.push(0); }
7145        push_i32(&mut out, script.font_bold);
7146        push_i32(&mut out, script.font_shadow);
7147
7148        push_bool(&mut out, script.cursor_disp_off);
7149        push_bool(&mut out, script.cursor_move_by_key_disable);
7150        for key in 0u16..=255u16 {
7151            push_bool(&mut out, script.key_disable.contains(&(key as u8)));
7152        }
7153
7154        push_bool(&mut out, script.quake_stop_flag);
7155        push_bool(&mut out, script.emote_mouth_stop_flag);
7156        push_bool(&mut out, self.ctx.globals.cg_table_off);
7157        push_bool(&mut out, script.bgmfade_flag);
7158        push_bool(&mut out, script.dont_set_save_point);
7159        push_bool(&mut out, script.ignore_r_flag);
7160        push_bool(&mut out, script.wait_display_vsync_off_flag);
7161
7162        push_bool(&mut out, script.time_stop_flag);
7163        push_bool(&mut out, script.counter_time_stop_flag);
7164        push_bool(&mut out, script.frame_action_time_stop_flag);
7165        push_bool(&mut out, script.stage_time_stop_flag);
7166        while out.len() % 4 != 0 { out.push(0); }
7167        debug_assert_eq!(out.len(), 356);
7168        out
7169    }
7170
7171    fn write_cpp_syscom_menu(&self, w: &mut crate::original_save::OriginalStreamWriter) {
7172        let base = w.position();
7173        let s = &self.ctx.globals.syscom;
7174        let push_ex = |w: &mut crate::original_save::OriginalStreamWriter, exist: bool, enable: bool| {
7175            w.push_bool(exist);
7176            w.push_bool(enable);
7177        };
7178        push_ex(w, s.read_skip.exist, s.read_skip.enable);
7179        push_ex(w, false, false);
7180        push_ex(w, s.auto_skip.exist, s.auto_skip.enable);
7181        push_ex(w, s.auto_mode.exist, s.auto_mode.enable);
7182        push_ex(w, s.hide_mwnd.exist, s.hide_mwnd.enable);
7183        push_ex(w, s.msg_back.exist, s.msg_back.enable);
7184        push_ex(w, s.save_feature.exist, s.save_feature.enable);
7185        push_ex(w, s.load_feature.exist, s.load_feature.enable);
7186        push_ex(w, s.return_to_sel.exist, s.return_to_sel.enable);
7187        push_ex(w, true, true);
7188        push_ex(w, false, false);
7189        push_ex(w, false, false);
7190        push_ex(w, s.return_to_menu.exist, s.return_to_menu.enable);
7191        push_ex(w, s.end_game.exist, s.end_game.enable);
7192        push_ex(w, true, true);
7193        for i in 0..4 {
7194            let sw = s.local_extra_switches.get(i).copied().unwrap_or(if i == 0 { s.local_extra_switch } else { runtime::globals::ToggleFeatureState::default() });
7195            w.push_bool(sw.exist);
7196            w.push_bool(sw.enable);
7197            w.push_bool(sw.onoff);
7198        }
7199        while (w.position() - base) % 4 != 0 { w.push_bool(false); }
7200        for i in 0..4 {
7201            let mode = s.local_extra_modes.get(i).copied().unwrap_or(if i == 0 { s.local_extra_mode } else { runtime::globals::ValueFeatureState::default() });
7202            w.push_bool(mode.exist);
7203            w.push_bool(mode.enable);
7204            w.push_padding(2);
7205            w.push_i32(mode.value as i32);
7206        }
7207    }
7208
7209    fn write_empty_counter_param(&self, w: &mut crate::original_save::OriginalStreamWriter) {
7210        w.push_bool(false);
7211        w.push_bool(false);
7212        w.push_bool(false);
7213        w.push_bool(false);
7214        w.push_i32(0);
7215        w.push_i32(0);
7216        w.push_i32(0);
7217        w.push_i32(0);
7218    }
7219
7220    fn write_empty_frame_action(&self, w: &mut crate::original_save::OriginalStreamWriter) {
7221        w.push_i32(0);
7222        w.push_str("");
7223        w.push_str("");
7224        w.push_i32(0);
7225        self.write_empty_counter_param(w);
7226    }
7227
7228    fn write_empty_btn_select(&self, w: &mut crate::original_save::OriginalStreamWriter) {
7229        w.push_i32(0);
7230        w.push_padding(112);
7231        w.push_bool(false);
7232        w.push_bool(false);
7233        w.push_bool(false);
7234        w.push_bool(false);
7235        w.push_bool(false);
7236        w.push_bool(false);
7237        w.push_str("");
7238        w.push_i32(0);
7239        w.push_i32(0);
7240    }
7241
7242    fn write_empty_stage(&self, w: &mut crate::original_save::OriginalStreamWriter) {
7243        w.push_empty_fixed_array();
7244        w.push_empty_fixed_array();
7245        w.push_empty_fixed_array();
7246        self.write_empty_btn_select(w);
7247        w.push_empty_fixed_array();
7248        w.push_empty_fixed_array();
7249        w.push_empty_fixed_array();
7250    }
7251
7252    fn write_empty_screen(&self, w: &mut crate::original_save::OriginalStreamWriter) {
7253        w.push_empty_fixed_array();
7254        w.push_padding(16);
7255        w.push_empty_fixed_array();
7256    }
7257
7258    fn write_empty_sound(&self, w: &mut crate::original_save::OriginalStreamWriter) {
7259        w.push_str("");
7260        w.push_i32(0);
7261        w.push_i32(0);
7262        w.push_bool(false);
7263        w.push_bool(false);
7264        w.push_i32(0);
7265        w.push_i32(0);
7266        w.push_empty_fixed_array();
7267        w.push_i32(0);
7268        w.push_str("");
7269    }
7270
7271    fn write_empty_msg_back(&self, w: &mut crate::original_save::OriginalStreamWriter) {
7272        w.push_i32(0);
7273        w.push_i32(0);
7274        w.push_i32(0);
7275        w.push_i32(0);
7276        w.push_bool(false);
7277    }
7278
7279    fn write_cpp_prop(&self, w: &mut crate::original_save::OriginalStreamWriter, prop_id: i32, cell: &UserPropCell) {
7280        w.push_i32(prop_id);
7281        w.push_i32(cell.form);
7282        w.push_i32(cell.int_value);
7283        w.push_str(&cell.str_value);
7284        w.push_element(&cell.element);
7285        w.push_extend_items(&cell.list_items, |w, item| self.write_cpp_prop(w, 0, item));
7286        w.push_i32(cell.list_items.len() as i32);
7287        if cell.form == self.cfg.fm_intlist {
7288            let vals: Vec<i64> = cell.int_list.iter().map(|v| *v as i64).collect();
7289            w.push_extend_i32_list(&vals);
7290        } else if cell.form == self.cfg.fm_strlist {
7291            w.push_extend_str_list(&cell.str_list);
7292        }
7293    }
7294
7295    fn read_cpp_prop(&self, rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<(i32, UserPropCell)> {
7296        let prop_id = rd.i32()?;
7297        let form = rd.i32()?;
7298        let int_value = rd.i32()?;
7299        let str_value = rd.string()?;
7300        let element = rd.element()?;
7301        let list_items = rd.extend_items(|rd| {
7302            let (_id, cell) = self.read_cpp_prop(rd)?;
7303            Ok(cell)
7304        })?;
7305        let _exp_cnt = rd.i32()?;
7306        let mut cell = UserPropCell::new(form, element);
7307        cell.int_value = int_value;
7308        cell.str_value = str_value;
7309        cell.list_items = list_items;
7310        if form == self.cfg.fm_intlist {
7311            cell.int_list = rd.extend_i32_list()?.into_iter().map(|v| v as i32).collect();
7312        } else if form == self.cfg.fm_strlist {
7313            cell.str_list = rd.extend_items(|rd| rd.string())?;
7314        }
7315        Ok((prop_id, cell))
7316    }
7317
7318    fn write_cpp_inc_prop_list(&self, w: &mut crate::original_save::OriginalStreamWriter) {
7319        let shared = self.shared_user_prop_count();
7320        let props: Vec<(i32, UserPropCell)> = (0..shared)
7321            .map(|idx| {
7322                let prop_id = idx as u16;
7323                let cell = self.user_props.get(&prop_id).cloned().unwrap_or_else(|| self.default_user_prop_cell(prop_id));
7324                (idx as i32, cell)
7325            })
7326            .collect();
7327        w.push_fixed_items(&props, |w, (id, cell)| self.write_cpp_prop(w, *id, cell));
7328    }
7329
7330    fn read_cpp_inc_prop_list(&mut self, rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<()> {
7331        let props = rd.fixed_items(|rd| self.read_cpp_prop(rd))?;
7332        for (idx, (_stored_id, cell)) in props.into_iter().enumerate() {
7333            self.user_props.insert(idx as u16, cell);
7334        }
7335        Ok(())
7336    }
7337
7338    fn write_cpp_current_scene_prop_lists(&self, w: &mut crate::original_save::OriginalStreamWriter) {
7339        let shared = self.shared_user_prop_count();
7340        let mut props: Vec<(i32, UserPropCell)> = Vec::new();
7341        let scene_prop_cnt = self.stream.header.scn_prop_cnt.max(0) as usize;
7342        for idx in 0..scene_prop_cnt {
7343            let prop_id = (shared + idx) as u16;
7344            if let Some(cell) = self.user_props.get(&prop_id).cloned() {
7345                props.push((idx as i32, cell));
7346            } else if let Some((_, _)) = self.user_prop_decl(prop_id) {
7347                props.push((idx as i32, self.default_user_prop_cell(prop_id)));
7348            }
7349        }
7350        if props.is_empty() {
7351            w.push_i32(0);
7352            return;
7353        }
7354        w.push_i32(1);
7355        w.push_str(self.current_scene_name.as_deref().unwrap_or(""));
7356        w.push_fixed_items(&props, |w, (id, cell)| self.write_cpp_prop(w, *id, cell));
7357    }
7358
7359    fn read_cpp_scene_prop_lists(&mut self, rd: &mut crate::original_save::OriginalStreamReader<'_>, current_scene_name: &str) -> Result<()> {
7360        let shared = self.shared_user_prop_count();
7361        let scene_prop_cnt = rd.i32()?.max(0) as usize;
7362        for _ in 0..scene_prop_cnt {
7363            let scene_name = rd.string()?;
7364            let props = rd.fixed_items(|rd| self.read_cpp_prop(rd))?;
7365            if scene_name == current_scene_name {
7366                for (idx, (_stored_id, cell)) in props.into_iter().enumerate() {
7367                    self.user_props.insert((shared + idx) as u16, cell);
7368                }
7369            }
7370        }
7371        Ok(())
7372    }
7373
7374    fn write_cpp_call_prop(&self, w: &mut crate::original_save::OriginalStreamWriter, prop: &CallProp) {
7375        w.push_i32(self.current_scene_no.unwrap_or(0) as i32);
7376        w.push_i32(prop.prop_id);
7377        let mut cell = UserPropCell::new(prop.form, prop.element.clone());
7378        match &prop.value {
7379            CallPropValue::Int(v) => cell.int_value = *v,
7380            CallPropValue::Str(v) => cell.str_value = v.clone(),
7381            CallPropValue::Element(v) => cell.element = v.clone(),
7382            CallPropValue::IntList(v) => cell.int_list = v.clone(),
7383            CallPropValue::StrList(v) => cell.str_list = v.clone(),
7384        }
7385        self.write_cpp_prop(w, prop.prop_id, &cell);
7386    }
7387
7388    fn read_cpp_call_prop(&self, rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<CallProp> {
7389        let _scn_no = rd.i32()?;
7390        let declared_prop_id = rd.i32()?;
7391        let (_stored_id, cell) = self.read_cpp_prop(rd)?;
7392        let value = if cell.form == self.cfg.fm_int {
7393            CallPropValue::Int(cell.int_value)
7394        } else if cell.form == self.cfg.fm_str {
7395            CallPropValue::Str(cell.str_value.clone())
7396        } else if cell.form == self.cfg.fm_intlist {
7397            CallPropValue::IntList(cell.int_list.clone())
7398        } else if cell.form == self.cfg.fm_strlist {
7399            CallPropValue::StrList(cell.str_list.clone())
7400        } else {
7401            CallPropValue::Element(cell.element.clone())
7402        };
7403        Ok(CallProp {
7404            prop_id: declared_prop_id,
7405            form: cell.form,
7406            decl_size: cell.int_list.len().max(cell.str_list.len()).max(cell.list_items.len()),
7407            element: cell.element,
7408            value,
7409        })
7410    }
7411
7412    fn write_cpp_call_frame(&self, w: &mut crate::original_save::OriginalStreamWriter, frame: &CallFrame) {
7413        let l: Vec<i64> = frame.int_args.iter().map(|v| *v as i64).collect();
7414        w.push_extend_i32_list(&l);
7415        w.push_extend_str_list(&frame.str_args);
7416        w.push_extend_items(&frame.user_props, |w, prop| self.write_cpp_call_prop(w, prop));
7417        let call_type = if frame.frame_action_proc { 3 } else if frame.return_pc != 0 { 1 } else { 0 };
7418        w.push_i32(call_type);
7419        w.push_i32(frame.ret_form);
7420        w.push_str(self.current_scene_name.as_deref().unwrap_or(""));
7421        w.push_i32(self.current_line_no);
7422        w.push_i32(frame.return_pc as i32);
7423    }
7424
7425    fn read_cpp_call_frame(&self, rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<CallFrame> {
7426        let int_args: Vec<i32> = rd.extend_i32_list()?.into_iter().map(|v| v as i32).collect();
7427        let str_args: Vec<String> = rd.extend_items(|rd| rd.string())?;
7428        let user_props = rd.extend_items(|rd| self.read_cpp_call_prop(rd))?;
7429        let call_type = rd.i32()?;
7430        let ret_form = rd.i32()?;
7431        let _scn_name = rd.string()?;
7432        let _line_no = rd.i32()?;
7433        let return_pc = rd.i32()?.max(0) as usize;
7434        Ok(CallFrame {
7435            return_pc,
7436            ret_form,
7437            return_override: None,
7438            excall_proc: false,
7439            frame_action_proc: call_type == 3,
7440            arg_cnt: 0,
7441            delayed_ret_form: None,
7442            user_props,
7443            int_args,
7444            str_args,
7445        })
7446    }
7447
7448
7449    fn save_i32(v: i64) -> i32 {
7450        v.clamp(i32::MIN as i64, i32::MAX as i64) as i32
7451    }
7452
7453    fn write_cpp_counter_param(&self, w: &mut crate::original_save::OriginalStreamWriter, c: &runtime::globals::Counter) {
7454        let (is_running, real_flag, frame_mode, frame_loop_flag, frame_start_value, frame_end_value, frame_time, cur_time) = c.save_parts();
7455        w.push_bool(is_running);
7456        w.push_bool(real_flag);
7457        w.push_bool(frame_mode);
7458        w.push_bool(frame_loop_flag);
7459        w.push_i32(Self::save_i32(frame_start_value));
7460        w.push_i32(Self::save_i32(frame_end_value));
7461        w.push_i32(Self::save_i32(frame_time));
7462        w.push_i32(Self::save_i32(cur_time));
7463    }
7464
7465    fn read_cpp_counter_param(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::Counter> {
7466        let is_running = rd.bool()?;
7467        let real_flag = rd.bool()?;
7468        let frame_mode = rd.bool()?;
7469        let frame_loop_flag = rd.bool()?;
7470        let frame_start_value = rd.i32()? as i64;
7471        let frame_end_value = rd.i32()? as i64;
7472        let frame_time = rd.i32()? as i64;
7473        let cur_time = rd.i32()? as i64;
7474        Ok(runtime::globals::Counter::from_save_parts(
7475            is_running,
7476            real_flag,
7477            frame_mode,
7478            frame_loop_flag,
7479            frame_start_value,
7480            frame_end_value,
7481            frame_time,
7482            cur_time,
7483        ))
7484    }
7485
7486    fn write_cpp_value_prop(w: &mut crate::original_save::OriginalStreamWriter, value: &Value) {
7487        use crate::runtime::forms::codes;
7488        w.push_i32(0);
7489        match value {
7490            Value::Str(s) => {
7491                w.push_i32(codes::FM_STR);
7492                w.push_i32(0);
7493                w.push_str(s);
7494            }
7495            Value::Int(v) => {
7496                w.push_i32(codes::FM_INT);
7497                w.push_i32(Self::save_i32(*v));
7498                w.push_str("");
7499            }
7500            _ => {
7501                w.push_i32(codes::FM_INT);
7502                w.push_i32(0);
7503                w.push_str("");
7504            }
7505        }
7506        w.push_empty_element();
7507        w.push_i32(0);
7508        w.push_i32(0);
7509    }
7510
7511    fn read_cpp_value_prop(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<Value> {
7512        use crate::runtime::forms::codes;
7513        let _id = rd.i32()?;
7514        let form = rd.i32()?;
7515        let int_value = rd.i32()?;
7516        let str_value = rd.string()?;
7517        let _element = rd.element()?;
7518        let _exp_list: Vec<Value> = rd.extend_items(|rd| Self::read_cpp_value_prop(rd))?;
7519        let _exp_cnt = rd.i32()?;
7520        if form == codes::FM_INTLIST {
7521            let _ = rd.extend_i32_list()?;
7522        } else if form == codes::FM_STRLIST {
7523            let _ = rd.extend_items(|rd| rd.string())?;
7524        }
7525        if form == codes::FM_STR {
7526            Ok(Value::Str(str_value))
7527        } else {
7528            Ok(Value::Int(int_value as i64))
7529        }
7530    }
7531
7532    fn write_cpp_frame_action(&self, w: &mut crate::original_save::OriginalStreamWriter, fa: &runtime::globals::ObjectFrameActionState) {
7533        w.push_i32(Self::save_i32(fa.end_time));
7534        w.push_str(&fa.scn_name);
7535        w.push_str(&fa.cmd_name);
7536        w.push_extend_items(&fa.args, |w, arg| Self::write_cpp_value_prop(w, arg));
7537        self.write_cpp_counter_param(w, &fa.counter);
7538    }
7539
7540    fn read_cpp_frame_action(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::ObjectFrameActionState> {
7541        let end_time = rd.i32()? as i64;
7542        let scn_name = rd.string()?;
7543        let cmd_name = rd.string()?;
7544        let args = rd.extend_items(|rd| Self::read_cpp_value_prop(rd))?;
7545        let counter = Self::read_cpp_counter_param(rd)?;
7546        Ok(runtime::globals::ObjectFrameActionState {
7547            scn_name,
7548            cmd_name,
7549            counter,
7550            end_time,
7551            real_time_flag: false,
7552            end_flag: false,
7553            args,
7554        })
7555    }
7556
7557    fn write_cpp_int_event_raw(w: &mut crate::original_save::OriginalStreamWriter, e: &runtime::int_event::IntEvent) {
7558        w.push_i32(e.def_value);
7559        w.push_i32(e.value);
7560        w.push_i32(e.cur_time);
7561        w.push_i32(e.end_time);
7562        w.push_i32(e.delay_time);
7563        w.push_i32(e.start_value);
7564        w.push_i32(e.cur_value);
7565        w.push_i32(e.end_value);
7566        w.push_i32(e.loop_type);
7567        w.push_i32(e.speed_type);
7568        w.push_i32(e.real_flag);
7569    }
7570
7571    fn read_cpp_int_event_raw(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::int_event::IntEvent> {
7572        let def_value = rd.i32()?;
7573        Ok(runtime::int_event::IntEvent {
7574            def_value,
7575            value: rd.i32()?,
7576            cur_time: rd.i32()?,
7577            end_time: rd.i32()?,
7578            delay_time: rd.i32()?,
7579            start_value: rd.i32()?,
7580            cur_value: rd.i32()?,
7581            end_value: rd.i32()?,
7582            loop_type: rd.i32()?,
7583            speed_type: rd.i32()?,
7584            real_flag: rd.i32()?,
7585        })
7586    }
7587
7588    fn write_cpp_save_event(w: &mut crate::original_save::OriginalStreamWriter, e: &runtime::int_event::IntEvent) {
7589        w.push_i32(e.loop_type);
7590        if e.loop_type != -1 {
7591            Self::write_cpp_int_event_raw(w, e);
7592        } else {
7593            w.push_i32(e.value);
7594        }
7595    }
7596
7597    fn read_cpp_save_event(rd: &mut crate::original_save::OriginalStreamReader<'_>, def_value: i32) -> Result<runtime::int_event::IntEvent> {
7598        let loop_type = rd.i32()?;
7599        if loop_type != -1 {
7600            let mut e = Self::read_cpp_int_event_raw(rd)?;
7601            e.loop_type = loop_type;
7602            Ok(e)
7603        } else {
7604            let mut e = runtime::int_event::IntEvent::new(def_value);
7605            e.loop_type = -1;
7606            e.value = rd.i32()?;
7607            e.cur_value = e.value;
7608            Ok(e)
7609        }
7610    }
7611
7612    fn write_cpp_int_event_extend_list(&self, w: &mut crate::original_save::OriginalStreamWriter, values: &[runtime::int_event::IntEvent]) {
7613        w.push_extend_items(values, |w, e| Self::write_cpp_int_event_raw(w, e));
7614    }
7615
7616    fn read_cpp_int_event_extend_list(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<Vec<runtime::int_event::IntEvent>> {
7617        rd.extend_items(|rd| Self::read_cpp_int_event_raw(rd))
7618    }
7619
7620    fn write_cpp_group(&self, w: &mut crate::original_save::OriginalStreamWriter, g: &runtime::globals::GroupState) {
7621        w.push_i32(Self::save_i32(g.order));
7622        w.push_i32(Self::save_i32(g.layer));
7623        w.push_i32(Self::save_i32(g.cancel_priority));
7624        w.push_i32(Self::save_i32(g.cancel_se_no));
7625        w.push_i32(Self::save_i32(g.decided_button_no));
7626        w.push_i32(Self::save_i32(g.result));
7627        w.push_i32(Self::save_i32(g.result_button_no));
7628        w.push_bool(g.started);
7629        w.push_bool(false);
7630        w.push_bool(g.wait_flag);
7631        w.push_bool(g.cancel_flag);
7632        w.push_empty_element();
7633    }
7634
7635    fn read_cpp_group(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::GroupState> {
7636        let mut g = runtime::globals::GroupState::default();
7637        g.order = rd.i32()? as i64;
7638        g.layer = rd.i32()? as i64;
7639        g.cancel_priority = rd.i32()? as i64;
7640        g.cancel_se_no = rd.i32()? as i64;
7641        g.decided_button_no = rd.i32()? as i64;
7642        g.result = rd.i32()? as i64;
7643        g.result_button_no = rd.i32()? as i64;
7644        g.started = rd.bool()?;
7645        let _pause_flag = rd.bool()?;
7646        g.wait_flag = rd.bool()?;
7647        g.cancel_flag = rd.bool()?;
7648        let _target_object = rd.element()?;
7649        Ok(g)
7650    }
7651
7652    fn write_cpp_object(&self, w: &mut crate::original_save::OriginalStreamWriter, obj: &runtime::globals::ObjectState) {
7653        let b = &obj.base;
7654        let ev = &obj.runtime.prop_events;
7655        w.push_i32(Self::save_i32(obj.object_type));
7656        w.push_i32(Self::save_i32(b.wipe_copy));
7657        w.push_i32(Self::save_i32(b.wipe_erase));
7658        w.push_i32(Self::save_i32(b.click_disable));
7659        // C_elm_object_param_filter: C_rect + C_argb.
7660        w.push_i32(0); w.push_i32(0); w.push_i32(0); w.push_i32(0); w.push_i32(0);
7661        // C_elm_object_param_string.
7662        w.push_i32(Self::save_i32(obj.string_param.moji_size));
7663        w.push_i32(Self::save_i32(obj.string_param.moji_space_x));
7664        w.push_i32(Self::save_i32(obj.string_param.moji_space_y));
7665        w.push_i32(Self::save_i32(obj.string_param.moji_cnt));
7666        w.push_i32(Self::save_i32(obj.string_param.moji_color));
7667        w.push_i32(Self::save_i32(obj.string_param.shadow_color));
7668        w.push_i32(Self::save_i32(obj.string_param.fuchi_color));
7669        w.push_i32(Self::save_i32(obj.string_param.shadow_mode));
7670        // C_elm_object_param_number.
7671        w.push_i32(Self::save_i32(obj.number_value));
7672        w.push_i32(Self::save_i32(obj.number_param.keta_max));
7673        w.push_i32(Self::save_i32(obj.number_param.disp_zero));
7674        w.push_i32(Self::save_i32(obj.number_param.disp_sign));
7675        w.push_i32(Self::save_i32(obj.number_param.tumeru_sign));
7676        w.push_i32(Self::save_i32(obj.number_param.space_mod));
7677        w.push_i32(Self::save_i32(obj.number_param.space));
7678        if obj.object_type == 4 {
7679            let wp = &obj.weather_param;
7680            for v in [wp.weather_type, wp.cnt, wp.pat_mode, wp.pat_no_00, wp.pat_no_01, wp.pat_time, wp.move_time_x, wp.move_time_y, wp.sin_time_x, wp.sin_time_y, wp.sin_power_x, wp.sin_power_y, wp.center_x, wp.center_y, wp.center_rotate, wp.appear_range, wp.zoom_min, wp.zoom_max] {
7681                w.push_i32(Self::save_i32(v));
7682            }
7683            Self::write_cpp_int_event_raw(w, &ev.color_add_r);
7684            Self::write_cpp_int_event_raw(w, &ev.color_add_g);
7685            Self::write_cpp_int_event_raw(w, &ev.color_add_b);
7686            for v in [b.mask_no, b.tonecurve_no, b.light_no, b.fog_use, b.culling, b.alpha_test, b.alpha_blend, b.blend, 0] {
7687                w.push_i32(Self::save_i32(v));
7688            }
7689        }
7690        w.push_i32(Self::save_i32(obj.thumb_save_no));
7691        w.push_bool(obj.movie.loop_flag);
7692        w.push_bool(obj.movie.auto_free_flag);
7693        w.push_bool(obj.movie.real_time_flag);
7694        w.push_bool(obj.movie.pause_flag);
7695        if obj.object_type == 10 {
7696            w.push_i32(Self::save_i32(obj.emote.width));
7697            w.push_i32(Self::save_i32(obj.emote.height));
7698            for _ in 0..8 { w.push_i32(0); }
7699            w.push_i32(0);
7700            w.push_i32(0);
7701            w.push_i32(Self::save_i32(obj.emote.rep_x));
7702            w.push_i32(Self::save_i32(obj.emote.rep_y));
7703        }
7704        if obj.button.enabled {
7705            w.push_i32(1);
7706            w.push_i32(Self::save_i32(obj.button.sys_type));
7707            w.push_i32(Self::save_i32(obj.button.sys_type_opt));
7708            w.push_i32(Self::save_i32(obj.button.action_no));
7709            w.push_i32(Self::save_i32(obj.button.se_no));
7710            w.push_i32(Self::save_i32(obj.button.button_no));
7711            w.push_empty_element();
7712            w.push_i32(if obj.button.push_keep { 1 } else { 0 });
7713            w.push_i32(Self::save_i32(obj.button.state));
7714            w.push_i32(Self::save_i32(obj.button.mode));
7715            w.push_i32(Self::save_i32(obj.button.cut_no));
7716            w.push_i32(-1);
7717            w.push_i32(-1);
7718            w.push_i32(Self::save_i32(obj.button.decided_action_z_no));
7719            w.push_i32(0);
7720            w.push_i32(if obj.button.alpha_test { 1 } else { 0 });
7721        } else {
7722            w.push_i32(0);
7723        }
7724        w.push_i32(Self::save_i32(b.disp));
7725        w.push_i32(Self::save_i32(b.patno));
7726        w.push_i32(Self::save_i32(b.order));
7727        w.push_i32(Self::save_i32(b.layer));
7728        w.push_i32(Self::save_i32(b.world));
7729        w.push_i32(Self::save_i32(b.child_sort_type));
7730        for e in [&ev.x, &ev.y, &ev.z, &ev.center_x, &ev.center_y, &ev.center_z, &ev.center_rep_x, &ev.center_rep_y, &ev.center_rep_z, &ev.scale_x, &ev.scale_y, &ev.scale_z, &ev.rotate_x, &ev.rotate_y, &ev.rotate_z] {
7731            Self::write_cpp_save_event(w, e);
7732        }
7733        w.push_i32(Self::save_i32(b.clip_use));
7734        for e in [&ev.clip_left, &ev.clip_top, &ev.clip_right, &ev.clip_bottom] { Self::write_cpp_save_event(w, e); }
7735        w.push_i32(Self::save_i32(b.src_clip_use));
7736        for e in [&ev.src_clip_left, &ev.src_clip_top, &ev.src_clip_right, &ev.src_clip_bottom, &ev.tr, &ev.mono, &ev.reverse, &ev.bright, &ev.dark, &ev.color_r, &ev.color_g, &ev.color_b, &ev.color_rate, &ev.color_add_r, &ev.color_add_g, &ev.color_add_b] {
7737            Self::write_cpp_save_event(w, e);
7738        }
7739        for v in [b.mask_no, b.tonecurve_no, b.light_no, b.fog_use, b.culling, b.alpha_test, b.alpha_blend, b.blend, 0] {
7740            w.push_i32(Self::save_i32(v));
7741        }
7742        self.write_cpp_int_event_extend_list(w, &obj.runtime.prop_event_lists.x_rep);
7743        self.write_cpp_int_event_extend_list(w, &obj.runtime.prop_event_lists.y_rep);
7744        self.write_cpp_int_event_extend_list(w, &obj.runtime.prop_event_lists.z_rep);
7745        self.write_cpp_int_event_extend_list(w, &obj.runtime.prop_event_lists.tr_rep);
7746        w.push_extend_i32_list(&obj.runtime.prop_lists.f);
7747        w.push_str(obj.file_name.as_deref().unwrap_or(""));
7748        w.push_str(obj.string_value.as_deref().unwrap_or(""));
7749        w.push_str(&obj.button.decided_action_scn_name);
7750        w.push_str(&obj.button.decided_action_cmd_name);
7751        if obj.object_type == 10 { for _ in 0..8 { w.push_str(""); } }
7752        self.write_cpp_frame_action(w, &obj.frame_action);
7753        w.push_extend_items(&obj.frame_action_ch, |w, fa| self.write_cpp_frame_action(w, fa));
7754        w.push_str(obj.gan_file.as_deref().unwrap_or(""));
7755        w.push_extend_items(&obj.runtime.child_objects, |w, child| self.write_cpp_object(w, child));
7756    }
7757
7758    fn read_cpp_object(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::ObjectState> {
7759        let mut obj = runtime::globals::ObjectState::default();
7760        obj.object_type = rd.i32()? as i64;
7761        obj.base.wipe_copy = rd.i32()? as i64;
7762        obj.base.wipe_erase = rd.i32()? as i64;
7763        obj.base.click_disable = rd.i32()? as i64;
7764        rd.skip(20)?;
7765        obj.string_param.moji_size = rd.i32()? as i64;
7766        obj.string_param.moji_space_x = rd.i32()? as i64;
7767        obj.string_param.moji_space_y = rd.i32()? as i64;
7768        obj.string_param.moji_cnt = rd.i32()? as i64;
7769        obj.string_param.moji_color = rd.i32()? as i64;
7770        obj.string_param.shadow_color = rd.i32()? as i64;
7771        obj.string_param.fuchi_color = rd.i32()? as i64;
7772        obj.string_param.shadow_mode = rd.i32()? as i64;
7773        obj.number_value = rd.i32()? as i64;
7774        obj.number_param.keta_max = rd.i32()? as i64;
7775        obj.number_param.disp_zero = rd.i32()? as i64;
7776        obj.number_param.disp_sign = rd.i32()? as i64;
7777        obj.number_param.tumeru_sign = rd.i32()? as i64;
7778        obj.number_param.space_mod = rd.i32()? as i64;
7779        obj.number_param.space = rd.i32()? as i64;
7780        if obj.object_type == 4 {
7781            obj.weather_param.weather_type = rd.i32()? as i64;
7782            obj.weather_param.cnt = rd.i32()? as i64;
7783            obj.weather_param.pat_mode = rd.i32()? as i64;
7784            obj.weather_param.pat_no_00 = rd.i32()? as i64;
7785            obj.weather_param.pat_no_01 = rd.i32()? as i64;
7786            obj.weather_param.pat_time = rd.i32()? as i64;
7787            obj.weather_param.move_time_x = rd.i32()? as i64;
7788            obj.weather_param.move_time_y = rd.i32()? as i64;
7789            obj.weather_param.sin_time_x = rd.i32()? as i64;
7790            obj.weather_param.sin_time_y = rd.i32()? as i64;
7791            obj.weather_param.sin_power_x = rd.i32()? as i64;
7792            obj.weather_param.sin_power_y = rd.i32()? as i64;
7793            obj.weather_param.center_x = rd.i32()? as i64;
7794            obj.weather_param.center_y = rd.i32()? as i64;
7795            obj.weather_param.center_rotate = rd.i32()? as i64;
7796            obj.weather_param.appear_range = rd.i32()? as i64;
7797            obj.weather_param.zoom_min = rd.i32()? as i64;
7798            obj.weather_param.zoom_max = rd.i32()? as i64;
7799            obj.runtime.prop_events.color_add_r = Self::read_cpp_int_event_raw(rd)?;
7800            obj.runtime.prop_events.color_add_g = Self::read_cpp_int_event_raw(rd)?;
7801            obj.runtime.prop_events.color_add_b = Self::read_cpp_int_event_raw(rd)?;
7802            obj.base.mask_no = rd.i32()? as i64;
7803            obj.base.tonecurve_no = rd.i32()? as i64;
7804            obj.base.light_no = rd.i32()? as i64;
7805            obj.base.fog_use = rd.i32()? as i64;
7806            obj.base.culling = rd.i32()? as i64;
7807            obj.base.alpha_test = rd.i32()? as i64;
7808            obj.base.alpha_blend = rd.i32()? as i64;
7809            obj.base.blend = rd.i32()? as i64;
7810            let _ = rd.i32()?;
7811        }
7812        obj.thumb_save_no = rd.i32()? as i64;
7813        obj.movie.loop_flag = rd.bool()?;
7814        obj.movie.auto_free_flag = rd.bool()?;
7815        obj.movie.real_time_flag = rd.bool()?;
7816        obj.movie.pause_flag = rd.bool()?;
7817        if obj.object_type == 10 {
7818            obj.emote.width = rd.i32()? as i64;
7819            obj.emote.height = rd.i32()? as i64;
7820            rd.skip(8 * 4)?;
7821            let _ = rd.i32()?;
7822            let _ = rd.i32()?;
7823            obj.emote.rep_x = rd.i32()? as i64;
7824            obj.emote.rep_y = rd.i32()? as i64;
7825        }
7826        let button_exist = rd.i32()? != 0;
7827        if button_exist {
7828            obj.button.enabled = true;
7829            obj.button.sys_type = rd.i32()? as i64;
7830            obj.button.sys_type_opt = rd.i32()? as i64;
7831            obj.button.action_no = rd.i32()? as i64;
7832            obj.button.se_no = rd.i32()? as i64;
7833            obj.button.button_no = rd.i32()? as i64;
7834            rd.skip_element()?;
7835            obj.button.push_keep = rd.i32()? != 0;
7836            obj.button.state = rd.i32()? as i64;
7837            obj.button.mode = rd.i32()? as i64;
7838            obj.button.cut_no = rd.i32()? as i64;
7839            let _ = rd.i32()?;
7840            let _ = rd.i32()?;
7841            obj.button.decided_action_z_no = rd.i32()? as i64;
7842            let _ = rd.i32()?;
7843            obj.button.alpha_test = rd.i32()? != 0;
7844        }
7845        obj.base.disp = rd.i32()? as i64;
7846        obj.base.patno = rd.i32()? as i64;
7847        obj.base.order = rd.i32()? as i64;
7848        obj.base.layer = rd.i32()? as i64;
7849        obj.base.world = rd.i32()? as i64;
7850        obj.base.child_sort_type = rd.i32()? as i64;
7851        obj.runtime.prop_events.x = Self::read_cpp_save_event(rd, 0)?;
7852        obj.runtime.prop_events.y = Self::read_cpp_save_event(rd, 0)?;
7853        obj.runtime.prop_events.z = Self::read_cpp_save_event(rd, 0)?;
7854        obj.runtime.prop_events.center_x = Self::read_cpp_save_event(rd, 0)?;
7855        obj.runtime.prop_events.center_y = Self::read_cpp_save_event(rd, 0)?;
7856        obj.runtime.prop_events.center_z = Self::read_cpp_save_event(rd, 0)?;
7857        obj.runtime.prop_events.center_rep_x = Self::read_cpp_save_event(rd, 0)?;
7858        obj.runtime.prop_events.center_rep_y = Self::read_cpp_save_event(rd, 0)?;
7859        obj.runtime.prop_events.center_rep_z = Self::read_cpp_save_event(rd, 0)?;
7860        obj.runtime.prop_events.scale_x = Self::read_cpp_save_event(rd, 1000)?;
7861        obj.runtime.prop_events.scale_y = Self::read_cpp_save_event(rd, 1000)?;
7862        obj.runtime.prop_events.scale_z = Self::read_cpp_save_event(rd, 1000)?;
7863        obj.runtime.prop_events.rotate_x = Self::read_cpp_save_event(rd, 0)?;
7864        obj.runtime.prop_events.rotate_y = Self::read_cpp_save_event(rd, 0)?;
7865        obj.runtime.prop_events.rotate_z = Self::read_cpp_save_event(rd, 0)?;
7866        obj.base.clip_use = rd.i32()? as i64;
7867        obj.runtime.prop_events.clip_left = Self::read_cpp_save_event(rd, 0)?;
7868        obj.runtime.prop_events.clip_top = Self::read_cpp_save_event(rd, 0)?;
7869        obj.runtime.prop_events.clip_right = Self::read_cpp_save_event(rd, 0)?;
7870        obj.runtime.prop_events.clip_bottom = Self::read_cpp_save_event(rd, 0)?;
7871        obj.base.src_clip_use = rd.i32()? as i64;
7872        obj.runtime.prop_events.src_clip_left = Self::read_cpp_save_event(rd, 0)?;
7873        obj.runtime.prop_events.src_clip_top = Self::read_cpp_save_event(rd, 0)?;
7874        obj.runtime.prop_events.src_clip_right = Self::read_cpp_save_event(rd, 0)?;
7875        obj.runtime.prop_events.src_clip_bottom = Self::read_cpp_save_event(rd, 0)?;
7876        obj.runtime.prop_events.tr = Self::read_cpp_save_event(rd, 255)?;
7877        obj.runtime.prop_events.mono = Self::read_cpp_save_event(rd, 0)?;
7878        obj.runtime.prop_events.reverse = Self::read_cpp_save_event(rd, 0)?;
7879        obj.runtime.prop_events.bright = Self::read_cpp_save_event(rd, 0)?;
7880        obj.runtime.prop_events.dark = Self::read_cpp_save_event(rd, 0)?;
7881        obj.runtime.prop_events.color_r = Self::read_cpp_save_event(rd, 0)?;
7882        obj.runtime.prop_events.color_g = Self::read_cpp_save_event(rd, 0)?;
7883        obj.runtime.prop_events.color_b = Self::read_cpp_save_event(rd, 0)?;
7884        obj.runtime.prop_events.color_rate = Self::read_cpp_save_event(rd, 0)?;
7885        obj.runtime.prop_events.color_add_r = Self::read_cpp_save_event(rd, 0)?;
7886        obj.runtime.prop_events.color_add_g = Self::read_cpp_save_event(rd, 0)?;
7887        obj.runtime.prop_events.color_add_b = Self::read_cpp_save_event(rd, 0)?;
7888        obj.base.mask_no = rd.i32()? as i64;
7889        obj.base.tonecurve_no = rd.i32()? as i64;
7890        obj.base.light_no = rd.i32()? as i64;
7891        obj.base.fog_use = rd.i32()? as i64;
7892        obj.base.culling = rd.i32()? as i64;
7893        obj.base.alpha_test = rd.i32()? as i64;
7894        obj.base.alpha_blend = rd.i32()? as i64;
7895        obj.base.blend = rd.i32()? as i64;
7896        let _flags = rd.i32()?;
7897        obj.runtime.prop_event_lists.x_rep = Self::read_cpp_int_event_extend_list(rd)?;
7898        obj.runtime.prop_event_lists.y_rep = Self::read_cpp_int_event_extend_list(rd)?;
7899        obj.runtime.prop_event_lists.z_rep = Self::read_cpp_int_event_extend_list(rd)?;
7900        obj.runtime.prop_event_lists.tr_rep = Self::read_cpp_int_event_extend_list(rd)?;
7901        obj.runtime.prop_lists.f = rd.extend_i32_list()?;
7902        let file_name = rd.string()?;
7903        obj.file_name = if file_name.is_empty() { None } else { Some(file_name) };
7904        let string_value = rd.string()?;
7905        obj.string_value = if string_value.is_empty() { None } else { Some(string_value) };
7906        obj.button.decided_action_scn_name = rd.string()?;
7907        obj.button.decided_action_cmd_name = rd.string()?;
7908        if obj.object_type == 10 { for _ in 0..8 { let _ = rd.string()?; } }
7909        obj.frame_action = Self::read_cpp_frame_action(rd)?;
7910        obj.frame_action_ch = rd.extend_items(|rd| Self::read_cpp_frame_action(rd))?;
7911        let gan_file = rd.string()?;
7912        obj.gan_file = if gan_file.is_empty() { None } else { Some(gan_file) };
7913        obj.runtime.child_objects = rd.extend_items(|rd| Self::read_cpp_object(rd))?;
7914        obj.used = obj.object_type != 0 || obj.file_name.is_some() || obj.string_value.is_some();
7915        Ok(obj)
7916    }
7917
7918    fn write_cpp_mwnd(&self, w: &mut crate::original_save::OriginalStreamWriter, m: &runtime::globals::MwndState) {
7919        w.push_i32(Self::save_i32(m.world));
7920        w.push_i32(Self::save_i32(m.layer));
7921        w.push_i32(if m.open { 1 } else { 0 });
7922        w.push_i32(Self::save_i32(m.window_pos.map(|p| p.0).unwrap_or(0)));
7923        w.push_i32(Self::save_i32(m.window_pos.map(|p| p.1).unwrap_or(0)));
7924        w.push_i32(Self::save_i32(m.window_size.map(|p| p.0).unwrap_or(0)));
7925        w.push_i32(Self::save_i32(m.window_size.map(|p| p.1).unwrap_or(0)));
7926        w.push_i32(Self::save_i32(m.open_anime_type));
7927        w.push_i32(Self::save_i32(m.open_anime_time));
7928        w.push_i32(Self::save_i32(m.close_anime_type));
7929        w.push_i32(Self::save_i32(m.close_anime_time));
7930        w.push_i64(0);
7931        w.push_bool(m.msg_block_started);
7932        w.push_bool(false);
7933        w.push_bool(m.open);
7934        w.push_bool(!m.name_text.is_empty());
7935        w.push_bool(m.clear_ready);
7936        w.push_padding(3);
7937        w.push_i32(0); w.push_i32(0); w.push_i32(0);
7938        w.push_bool(m.slide_msg);
7939        w.push_padding(3);
7940        w.push_i32(Self::save_i32(m.slide_time));
7941        w.push_i32(m.koe.map(|p| Self::save_i32(p.0)).unwrap_or(0));
7942        w.push_bool(m.koe.is_some());
7943        w.push_padding(3);
7944        w.push_i32(Self::save_i32(m.open_anime_type));
7945        w.push_i32(Self::save_i32(m.open_anime_time));
7946        w.push_i32(0);
7947        w.push_i32(Self::save_i32(m.close_anime_type));
7948        w.push_i32(Self::save_i32(m.close_anime_time));
7949        w.push_i32(0);
7950        w.push_i32(1);
7951        w.push_bool(false);
7952        w.push_padding(3);
7953        w.push_str(&m.msg_text);
7954        w.push_str(&m.waku_file);
7955        w.push_str(&m.name_text);
7956        w.push_i32(0);
7957        w.push_i32(0);
7958        w.push_i32(0);
7959        w.push_extend_items(&m.object_list, |w, obj| self.write_cpp_object(w, obj));
7960        w.push_extend_items(&m.button_list, |w, obj| self.write_cpp_object(w, obj));
7961        w.push_extend_items(&m.face_list, |w, obj| self.write_cpp_object(w, obj));
7962    }
7963
7964    fn read_cpp_mwnd(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::MwndState> {
7965        let mut m = runtime::globals::MwndState::default();
7966        m.world = rd.i32()? as i64;
7967        m.layer = rd.i32()? as i64;
7968        m.open = rd.i32()? != 0;
7969        let wx = rd.i32()? as i64;
7970        let wy = rd.i32()? as i64;
7971        m.window_pos = Some((wx, wy));
7972        let ww = rd.i32()? as i64;
7973        let wh = rd.i32()? as i64;
7974        m.window_size = Some((ww, wh));
7975        m.open_anime_type = rd.i32()? as i64;
7976        m.open_anime_time = rd.i32()? as i64;
7977        m.close_anime_type = rd.i32()? as i64;
7978        m.close_anime_time = rd.i32()? as i64;
7979        let _ = rd.i64()?;
7980        m.msg_block_started = rd.bool()?;
7981        let _ = rd.bool()?;
7982        m.open = rd.bool()?;
7983        let _ = rd.bool()?;
7984        m.clear_ready = rd.bool()?;
7985        rd.skip(3)?;
7986        let _ = rd.i32()?; let _ = rd.i32()?; let _ = rd.i32()?;
7987        m.slide_msg = rd.bool()?;
7988        rd.skip(3)?;
7989        m.slide_time = rd.i32()? as i64;
7990        let koe_no = rd.i32()? as i64;
7991        let koe_play = rd.bool()?;
7992        rd.skip(3)?;
7993        if koe_play { m.koe = Some((koe_no, 0)); }
7994        m.open_anime_type = rd.i32()? as i64;
7995        m.open_anime_time = rd.i32()? as i64;
7996        let _ = rd.i32()?;
7997        m.close_anime_type = rd.i32()? as i64;
7998        m.close_anime_time = rd.i32()? as i64;
7999        let _ = rd.i32()?;
8000        let _ = rd.i32()?;
8001        let _ = rd.bool()?;
8002        rd.skip(3)?;
8003        m.msg_text = rd.string()?;
8004        m.waku_file = rd.string()?;
8005        m.name_text = rd.string()?;
8006        let _ = rd.i32()?; let _ = rd.i32()?; let _ = rd.i32()?;
8007        m.object_list = rd.extend_items(|rd| Self::read_cpp_object(rd))?;
8008        m.button_list = rd.extend_items(|rd| Self::read_cpp_object(rd))?;
8009        m.face_list = rd.extend_items(|rd| Self::read_cpp_object(rd))?;
8010        Ok(m)
8011    }
8012
8013    fn write_cpp_world(&self, w: &mut crate::original_save::OriginalStreamWriter, world: &runtime::globals::WorldState) {
8014        w.push_i32(world.mode);
8015        for e in [&world.camera_eye_x, &world.camera_eye_y, &world.camera_eye_z, &world.camera_pint_x, &world.camera_pint_y, &world.camera_pint_z, &world.camera_up_x, &world.camera_up_y, &world.camera_up_z] {
8016            Self::write_cpp_int_event_raw(w, e);
8017        }
8018        for v in [world.camera_view_angle, world.mono, world.order, world.layer, world.wipe_copy, world.wipe_erase] { w.push_i32(v); }
8019    }
8020
8021    fn read_cpp_world(rd: &mut crate::original_save::OriginalStreamReader<'_>, world_no: i32) -> Result<runtime::globals::WorldState> {
8022        let mut world = runtime::globals::WorldState::new(world_no);
8023        world.mode = rd.i32()?;
8024        world.camera_eye_x = Self::read_cpp_int_event_raw(rd)?;
8025        world.camera_eye_y = Self::read_cpp_int_event_raw(rd)?;
8026        world.camera_eye_z = Self::read_cpp_int_event_raw(rd)?;
8027        world.camera_pint_x = Self::read_cpp_int_event_raw(rd)?;
8028        world.camera_pint_y = Self::read_cpp_int_event_raw(rd)?;
8029        world.camera_pint_z = Self::read_cpp_int_event_raw(rd)?;
8030        world.camera_up_x = Self::read_cpp_int_event_raw(rd)?;
8031        world.camera_up_y = Self::read_cpp_int_event_raw(rd)?;
8032        world.camera_up_z = Self::read_cpp_int_event_raw(rd)?;
8033        world.camera_view_angle = rd.i32()?;
8034        world.mono = rd.i32()?;
8035        world.order = rd.i32()?;
8036        world.layer = rd.i32()?;
8037        world.wipe_copy = rd.i32()?;
8038        world.wipe_erase = rd.i32()?;
8039        Ok(world)
8040    }
8041
8042    fn write_cpp_effect(&self, w: &mut crate::original_save::OriginalStreamWriter, e: &runtime::globals::ScreenEffectState) {
8043        for ev in [&e.x, &e.y, &e.z, &e.mono, &e.reverse, &e.bright, &e.dark, &e.color_r, &e.color_g, &e.color_b, &e.color_rate, &e.color_add_r, &e.color_add_g, &e.color_add_b] {
8044            Self::write_cpp_int_event_raw(w, ev);
8045        }
8046        for v in [e.begin_order, e.end_order, e.begin_layer, e.end_layer, e.wipe_copy, e.wipe_erase] { w.push_i32(v); }
8047    }
8048
8049    fn read_cpp_effect(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::ScreenEffectState> {
8050        let mut e = runtime::globals::ScreenEffectState::default();
8051        e.x = Self::read_cpp_int_event_raw(rd)?;
8052        e.y = Self::read_cpp_int_event_raw(rd)?;
8053        e.z = Self::read_cpp_int_event_raw(rd)?;
8054        e.mono = Self::read_cpp_int_event_raw(rd)?;
8055        e.reverse = Self::read_cpp_int_event_raw(rd)?;
8056        e.bright = Self::read_cpp_int_event_raw(rd)?;
8057        e.dark = Self::read_cpp_int_event_raw(rd)?;
8058        e.color_r = Self::read_cpp_int_event_raw(rd)?;
8059        e.color_g = Self::read_cpp_int_event_raw(rd)?;
8060        e.color_b = Self::read_cpp_int_event_raw(rd)?;
8061        e.color_rate = Self::read_cpp_int_event_raw(rd)?;
8062        e.color_add_r = Self::read_cpp_int_event_raw(rd)?;
8063        e.color_add_g = Self::read_cpp_int_event_raw(rd)?;
8064        e.color_add_b = Self::read_cpp_int_event_raw(rd)?;
8065        e.begin_order = rd.i32()?;
8066        e.end_order = rd.i32()?;
8067        e.begin_layer = rd.i32()?;
8068        e.end_layer = rd.i32()?;
8069        e.wipe_copy = rd.i32()?;
8070        e.wipe_erase = rd.i32()?;
8071        Ok(e)
8072    }
8073
8074    fn write_cpp_quake(&self, w: &mut crate::original_save::OriginalStreamWriter, q: &runtime::globals::ScreenQuakeState) {
8075        w.push_i32(q.quake_type);
8076        w.push_i32(q.vec);
8077        w.push_i32(q.power);
8078        w.push_i32(0);
8079        w.push_i32(0);
8080        w.push_i32(if q.ending { 1 } else { 0 });
8081        w.push_i32(0);
8082        w.push_i32(0);
8083        w.push_i32(0);
8084        w.push_i32(0);
8085        w.push_i32(q.center_x);
8086        w.push_i32(q.center_y);
8087        w.push_i32(q.begin_order);
8088        w.push_i32(q.end_order);
8089    }
8090
8091    fn read_cpp_quake(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::ScreenQuakeState> {
8092        let mut q = runtime::globals::ScreenQuakeState::default();
8093        q.quake_type = rd.i32()?;
8094        q.vec = rd.i32()?;
8095        q.power = rd.i32()?;
8096        let _cur_time = rd.i32()?;
8097        let _total_time = rd.i32()?;
8098        q.ending = rd.i32()? != 0;
8099        let _end_cur_time = rd.i32()?;
8100        let _end_total_time = rd.i32()?;
8101        let _cnt = rd.i32()?;
8102        let _end_cnt = rd.i32()?;
8103        q.center_x = rd.i32()?;
8104        q.center_y = rd.i32()?;
8105        q.begin_order = rd.i32()?;
8106        q.end_order = rd.i32()?;
8107        Ok(q)
8108    }
8109
8110    fn write_cpp_btn_select(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8111        w.push_i32(0);
8112        w.push_padding(112);
8113        w.push_bool(false);
8114        w.push_bool(false);
8115        w.push_bool(false);
8116        w.push_bool(false);
8117        w.push_bool(false);
8118        w.push_bool(false);
8119        w.push_str("");
8120        w.push_i32(0);
8121        w.push_i32(0);
8122    }
8123
8124    fn read_cpp_btn_select(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<()> {
8125        let _ = rd.i32()?;
8126        rd.skip(112)?;
8127        for _ in 0..6 { let _ = rd.bool()?; }
8128        let _ = rd.string()?;
8129        let _ = rd.i32()?;
8130        let _ = rd.i32()?;
8131        Ok(())
8132    }
8133
8134    fn write_cpp_stage(&self, w: &mut crate::original_save::OriginalStreamWriter, stage_idx: i64) {
8135        let form_id = self.ctx.ids.form_global_stage;
8136        let st = self.ctx.globals.stage_forms.get(&form_id);
8137        let empty_groups: Vec<runtime::globals::GroupState> = Vec::new();
8138        let empty_objects: Vec<runtime::globals::ObjectState> = Vec::new();
8139        let empty_mwnds: Vec<runtime::globals::MwndState> = Vec::new();
8140        let empty_worlds: Vec<runtime::globals::WorldState> = Vec::new();
8141        let empty_effects: Vec<runtime::globals::ScreenEffectState> = Vec::new();
8142        let empty_quakes: Vec<runtime::globals::ScreenQuakeState> = Vec::new();
8143        let groups = st.and_then(|s| s.group_lists.get(&stage_idx)).unwrap_or(&empty_groups);
8144        let objects = st.and_then(|s| s.object_lists.get(&stage_idx)).unwrap_or(&empty_objects);
8145        let mwnds = st.and_then(|s| s.mwnd_lists.get(&stage_idx)).unwrap_or(&empty_mwnds);
8146        let worlds = st.and_then(|s| s.world_lists.get(&stage_idx)).unwrap_or(&empty_worlds);
8147        let effects = st.and_then(|s| s.effect_lists.get(&stage_idx)).unwrap_or(&empty_effects);
8148        let quakes = st.and_then(|s| s.quake_lists.get(&stage_idx)).unwrap_or(&empty_quakes);
8149        w.push_fixed_items(groups, |w, g| self.write_cpp_group(w, g));
8150        w.push_fixed_items(objects, |w, obj| self.write_cpp_object(w, obj));
8151        w.push_fixed_items(mwnds, |w, m| self.write_cpp_mwnd(w, m));
8152        self.write_cpp_btn_select(w);
8153        w.push_fixed_items(worlds, |w, world| self.write_cpp_world(w, world));
8154        w.push_fixed_items(effects, |w, e| self.write_cpp_effect(w, e));
8155        w.push_fixed_items(quakes, |w, q| self.write_cpp_quake(w, q));
8156    }
8157
8158    fn read_cpp_stage(rd: &mut crate::original_save::OriginalStreamReader<'_>, stage_idx: i64) -> Result<runtime::globals::StageFormState> {
8159        let mut st = runtime::globals::StageFormState::default();
8160        st.initialized_from_gameexe = true;
8161        st.group_lists.insert(stage_idx, rd.fixed_items(|rd| Self::read_cpp_group(rd))?);
8162        st.object_lists.insert(stage_idx, rd.fixed_items(|rd| Self::read_cpp_object(rd))?);
8163        st.mwnd_lists.insert(stage_idx, rd.fixed_items(|rd| Self::read_cpp_mwnd(rd))?);
8164        Self::read_cpp_btn_select(rd)?;
8165        let mut world_no = 0i32;
8166        let worlds = rd.fixed_items(|rd| { let w = Self::read_cpp_world(rd, world_no); world_no += 1; w })?;
8167        st.world_lists.insert(stage_idx, worlds);
8168        st.effect_lists.insert(stage_idx, rd.fixed_items(|rd| Self::read_cpp_effect(rd))?);
8169        st.quake_lists.insert(stage_idx, rd.fixed_items(|rd| Self::read_cpp_quake(rd))?);
8170        Ok(st)
8171    }
8172
8173    fn write_cpp_screen(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8174        let form_id = self.ctx.ids.form_global_screen;
8175        let screen = self.ctx.globals.screen_forms.get(&form_id).cloned().unwrap_or_default();
8176        w.push_fixed_items(&screen.effect_list, |w, e| self.write_cpp_effect(w, e));
8177        w.push_i32(Self::save_i32(screen.shake.last_value));
8178        w.push_i64(0);
8179        w.push_i32(0);
8180        w.push_fixed_items(&screen.quake_list, |w, q| self.write_cpp_quake(w, q));
8181    }
8182
8183    fn read_cpp_screen(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::ScreenFormState> {
8184        let effect_list = rd.fixed_items(|rd| Self::read_cpp_effect(rd))?;
8185        let mut shake = runtime::globals::ScreenShakeState::default();
8186        shake.last_value = rd.i32()? as i64;
8187        let _ = rd.i64()?;
8188        let _ = rd.i32()?;
8189        let quake_list = rd.fixed_items(|rd| Self::read_cpp_quake(rd))?;
8190        Ok(runtime::globals::ScreenFormState { effect_list, quake_list, shake })
8191    }
8192
8193    const ORIGINAL_PCMCH_DEFAULT_CNT: usize = 16;
8194    const ORIGINAL_PCMCH_MAX_CNT: usize = 256;
8195
8196    fn original_pcmch_count(&self) -> usize {
8197        self.ctx
8198            .tables
8199            .gameexe
8200            .as_ref()
8201            .and_then(|cfg| cfg.get_usize("#PCMCH.CNT").or_else(|| cfg.get_usize("PCMCH.CNT")))
8202            .unwrap_or(Self::ORIGINAL_PCMCH_DEFAULT_CNT)
8203            .min(Self::ORIGINAL_PCMCH_MAX_CNT)
8204    }
8205
8206    fn write_cpp_pcmch_default(w: &mut crate::original_save::OriginalStreamWriter) {
8207        w.push_str("");
8208        w.push_str("");
8209        w.push_i32(-1);
8210        w.push_i32(-1);
8211        w.push_i32(0);
8212        w.push_i32(-1);
8213        w.push_i32(255);
8214        w.push_i32(0);
8215        w.push_bool(false);
8216        w.push_bool(false);
8217        w.push_bool(false);
8218        w.push_bool(false);
8219        w.push_bool(false);
8220    }
8221
8222    fn read_cpp_pcmch(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<()> {
8223        let _pcm_name = rd.string()?;
8224        let _bgm_name = rd.string()?;
8225        let _koe_no = rd.i32()?;
8226        let _se_no = rd.i32()?;
8227        let _volume_type = rd.i32()?;
8228        let _chara_no = rd.i32()?;
8229        let _volume = rd.i32()?;
8230        let _delay_time = rd.i32()?;
8231        let _loop_flag = rd.bool()?;
8232        let _bgm_fade_target_flag = rd.bool()?;
8233        let _bgm_fade2_target_flag = rd.bool()?;
8234        let _bgm_fade_source_flag = rd.bool()?;
8235        let _ready_flag = rd.bool()?;
8236        Ok(())
8237    }
8238
8239    fn write_cpp_sound(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8240        // C_elm_sound::save order: BGM, KOE, PCM, PCMCHLIST, SE, MOV.
8241        // The runtime currently does not retain all original sound parameters, so this writes
8242        // a structurally exact silent/default sound state instead of a truncated one.
8243        w.push_str("");
8244        w.push_i32(255);
8245        w.push_i32(0);
8246        w.push_bool(false);
8247        w.push_bool(false);
8248        w.push_i32(255);
8249        w.push_i32(255);
8250        let pcmch_defaults = vec![(); self.original_pcmch_count()];
8251        w.push_fixed_items(&pcmch_defaults, |w, _| Self::write_cpp_pcmch_default(w));
8252        w.push_i32(255);
8253        w.push_str("");
8254    }
8255
8256    fn read_cpp_sound(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<()> {
8257        let _bgm_regist_name = rd.string()?;
8258        let _bgm_volume = rd.i32()?;
8259        let _bgm_delay_time = rd.i32()?;
8260        let _bgm_loop_flag = rd.bool()?;
8261        let _bgm_pause_flag = rd.bool()?;
8262        let _koe_volume = rd.i32()?;
8263        let _pcm_volume = rd.i32()?;
8264        rd.fixed_items(|rd| Self::read_cpp_pcmch(rd))?;
8265        let _se_volume = rd.i32()?;
8266        let _mov_file_name = rd.string()?;
8267        Ok(())
8268    }
8269
8270    fn write_cpp_pcm_event(&self, w: &mut crate::original_save::OriginalStreamWriter, ev: &runtime::globals::PcmEventState) {
8271        let ty = if ev.random { 2 } else if ev.looped { 1 } else if ev.active { 0 } else { -1 };
8272        w.push_i32(ty);
8273        if ty == 1 || ty == 2 {
8274            w.push_i32(0);
8275            w.push_i32(ev.volume_type);
8276            w.push_i32(ev.chara_no);
8277            w.push_bool(ev.bgm_fade_target_flag);
8278            w.push_bool(ev.bgm_fade2_target_flag);
8279            w.push_bool(ev.bgm_fade2_source_flag);
8280            w.push_bool(ev.real_flag);
8281            w.push_bool(ev.time_type);
8282            w.push_extend_items(&ev.lines, |w, line| {
8283                w.push_str(&line.file_name);
8284                w.push_i32(line.min_time);
8285                w.push_i32(line.max_time);
8286                w.push_i32(line.probability);
8287            });
8288        }
8289    }
8290
8291    fn read_cpp_pcm_event(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::PcmEventState> {
8292        let ty = rd.i32()?;
8293        let mut ev = runtime::globals::PcmEventState::default();
8294        ev.active = ty >= 0;
8295        ev.looped = ty == 1;
8296        ev.random = ty == 2;
8297        if ty == 1 || ty == 2 {
8298            let _ = rd.i32()?;
8299            ev.volume_type = rd.i32()?;
8300            ev.chara_no = rd.i32()?;
8301            ev.bgm_fade_target_flag = rd.bool()?;
8302            ev.bgm_fade2_target_flag = rd.bool()?;
8303            ev.bgm_fade2_source_flag = rd.bool()?;
8304            ev.real_flag = rd.bool()?;
8305            ev.time_type = rd.bool()?;
8306            ev.lines = rd.extend_items(|rd| Ok(runtime::globals::PcmEventLine { file_name: rd.string()?, min_time: rd.i32()?, max_time: rd.i32()?, probability: rd.i32()? }))?;
8307        }
8308        Ok(ev)
8309    }
8310
8311    fn write_cpp_editbox(&self, w: &mut crate::original_save::OriginalStreamWriter, e: &runtime::globals::EditBoxState) {
8312        w.push_bool(e.created);
8313        w.push_i32(e.rect_x);
8314        w.push_i32(e.rect_y);
8315        w.push_i32(e.rect_w);
8316        w.push_i32(e.rect_h);
8317        w.push_i32(e.moji_size);
8318    }
8319
8320    fn read_cpp_editbox(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::EditBoxState> {
8321        let mut e = runtime::globals::EditBoxState::default();
8322        e.created = rd.bool()?;
8323        e.rect_x = rd.i32()?;
8324        e.rect_y = rd.i32()?;
8325        e.rect_w = rd.i32()?;
8326        e.rect_h = rd.i32()?;
8327        e.moji_size = rd.i32()?;
8328        e.text.clear();
8329        Ok(e)
8330    }
8331
8332    fn write_cpp_msg_back(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8333        let msgbk = self.ctx.globals.msgbk_forms.values().next().cloned().unwrap_or_default();
8334        w.push_i32(msgbk.history_cnt as i32);
8335        let count = msgbk.history_cnt.min(msgbk.history.len());
8336        for entry in msgbk.history.iter().take(count) {
8337            w.push_bool(entry.pct_flag);
8338            w.push_str(&entry.msg_str);
8339            w.push_str(&entry.original_name);
8340            w.push_str(&entry.disp_name);
8341            w.push_i32(entry.pct_pos_x);
8342            w.push_i32(entry.pct_pos_y);
8343            w.push_extend_i32_list(&entry.koe_no_list);
8344            w.push_extend_i32_list(&entry.chr_no_list);
8345            w.push_i32(Self::save_i32(entry.koe_play_no));
8346            w.push_str(&entry.debug_msg);
8347            w.push_i32(Self::save_i32(entry.scn_no));
8348            w.push_i32(Self::save_i32(entry.line_no));
8349            w.push_tid_zero();
8350            w.push_bool(entry.save_id_check_flag);
8351        }
8352        w.push_i32(msgbk.history_start_pos as i32);
8353        w.push_i32(msgbk.history_last_pos as i32);
8354        w.push_i32(msgbk.history_insert_pos as i32);
8355        w.push_i32(if msgbk.new_msg_flag { 1 } else { 0 });
8356    }
8357
8358    fn read_cpp_msg_back(rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<runtime::globals::MsgBackState> {
8359        let cnt = rd.i32()?.max(0) as usize;
8360        let mut st = runtime::globals::MsgBackState::default();
8361        st.history.clear();
8362        for _ in 0..cnt {
8363            let mut entry = runtime::globals::MsgBackEntry::default();
8364            entry.pct_flag = rd.bool()?;
8365            entry.msg_str = rd.string()?;
8366            entry.original_name = rd.string()?;
8367            entry.disp_name = rd.string()?;
8368            entry.pct_pos_x = rd.i32()?;
8369            entry.pct_pos_y = rd.i32()?;
8370            entry.koe_no_list = rd.extend_i32_list()?;
8371            entry.chr_no_list = rd.extend_i32_list()?;
8372            entry.koe_play_no = rd.i32()? as i64;
8373            entry.debug_msg = rd.string()?;
8374            entry.scn_no = rd.i32()? as i64;
8375            entry.line_no = rd.i32()? as i64;
8376            rd.skip(14)?;
8377            entry.save_id_check_flag = rd.bool()?;
8378            st.history.push(entry);
8379        }
8380        st.history_cnt = cnt;
8381        st.history_cnt_max = cnt.max(256);
8382        st.history_start_pos = rd.i32()?.max(0) as usize;
8383        st.history_last_pos = rd.i32()?.max(0) as usize;
8384        st.history_insert_pos = rd.i32()?.max(0) as usize;
8385        st.new_msg_flag = rd.i32()? != 0;
8386        if st.history.len() < st.history_cnt_max { st.history.resize_with(st.history_cnt_max, runtime::globals::MsgBackEntry::default); }
8387        Ok(st)
8388    }
8389
8390
8391    fn parse_cpp_tail_state(&mut self, rd: &mut crate::original_save::OriginalStreamReader<'_>, current_scene_name: &str) -> Result<Vec<CallFrame>> {
8392        self.read_cpp_inc_prop_list(rd)?;
8393        self.read_cpp_scene_prop_lists(rd, current_scene_name)?;
8394
8395        let counter_list = rd.fixed_items(|rd| Self::read_cpp_counter_param(rd))?;
8396        if !counter_list.is_empty() {
8397            self.ctx.globals.counter_lists.insert(crate::runtime::forms::codes::FORM_GLOBAL_COUNTER, counter_list);
8398        }
8399
8400        let frame_action = Self::read_cpp_frame_action(rd)?;
8401        self.ctx.globals.frame_actions.insert(self.ctx.ids.form_global_frame_action, frame_action);
8402
8403        let frame_action_ch = rd.fixed_items(|rd| Self::read_cpp_frame_action(rd))?;
8404        if !frame_action_ch.is_empty() {
8405            self.ctx.globals.frame_action_lists.insert(self.ctx.ids.form_global_frame_action_ch, frame_action_ch);
8406        }
8407
8408        let g00buf_files = rd.fixed_items(|rd| rd.string())?;
8409        self.ctx.globals.g00buf.clear();
8410        self.ctx.globals.g00buf_names.clear();
8411        self.ctx.globals.g00buf.resize(g00buf_files.len(), None);
8412        self.ctx.globals.g00buf_names.resize(g00buf_files.len(), None);
8413        for (idx, name) in g00buf_files.into_iter().enumerate() {
8414            if !name.is_empty() {
8415                self.ctx.globals.g00buf_names[idx] = Some(name.clone());
8416                if let Ok(img_id) = self.ctx.images.load_g00(&name, 0) {
8417                    self.ctx.globals.g00buf[idx] = Some(img_id);
8418                }
8419            }
8420        }
8421
8422        let masks = rd.fixed_items(|rd| {
8423            let x_event = Self::read_cpp_int_event_raw(rd)?;
8424            let y_event = Self::read_cpp_int_event_raw(rd)?;
8425            let name = rd.string()?;
8426            Ok(runtime::globals::MaskState {
8427                name: if name.is_empty() { None } else { Some(name) },
8428                x_event,
8429                y_event,
8430                extra_int: std::collections::HashMap::new(),
8431                script_events: std::collections::HashMap::new(),
8432            })
8433        })?;
8434        if !masks.is_empty() {
8435            self.ctx.globals.mask_lists.insert(self.ctx.ids.form_global_mask, runtime::globals::MaskListState { masks });
8436        }
8437
8438        let mut st = runtime::globals::StageFormState::default();
8439        let back = Self::read_cpp_stage(rd, 0)?;
8440        let front = Self::read_cpp_stage(rd, 1)?;
8441        st.initialized_from_gameexe = true;
8442        st.group_lists.extend(back.group_lists);
8443        st.object_lists.extend(back.object_lists);
8444        st.mwnd_lists.extend(back.mwnd_lists);
8445        st.world_lists.extend(back.world_lists);
8446        st.effect_lists.extend(back.effect_lists);
8447        st.quake_lists.extend(back.quake_lists);
8448        st.group_lists.extend(front.group_lists);
8449        st.object_lists.extend(front.object_lists);
8450        st.mwnd_lists.extend(front.mwnd_lists);
8451        st.world_lists.extend(front.world_lists);
8452        st.effect_lists.extend(front.effect_lists);
8453        st.quake_lists.extend(front.quake_lists);
8454        self.ctx.globals.stage_forms.insert(self.ctx.ids.form_global_stage, st);
8455
8456        let screen = Self::read_cpp_screen(rd)?;
8457        self.ctx.globals.screen_forms.insert(self.ctx.ids.form_global_screen, screen);
8458
8459        Self::read_cpp_sound(rd)?;
8460
8461        let pcm_events = rd.fixed_items(|rd| Self::read_cpp_pcm_event(rd))?;
8462        if !pcm_events.is_empty() {
8463            self.ctx.globals.pcm_event_lists.insert(self.ctx.ids.form_global_pcm_event, pcm_events);
8464        }
8465
8466        let editboxes = rd.fixed_items(|rd| Self::read_cpp_editbox(rd))?;
8467        if !editboxes.is_empty() {
8468            self.ctx.globals.editbox_lists.insert(self.ctx.ids.form_global_editbox, runtime::globals::EditBoxListState { boxes: editboxes });
8469        }
8470
8471        let call_cnt = rd.i32()?.max(0) as usize;
8472        let mut call_stack = Vec::with_capacity(call_cnt.max(1));
8473        for _ in 0..call_cnt {
8474            call_stack.push(self.read_cpp_call_frame(rd)?);
8475        }
8476        if call_stack.is_empty() {
8477            call_stack.push(self.scene_base_call());
8478        }
8479
8480        let msg_back = Self::read_cpp_msg_back(rd)?;
8481        self.ctx.globals.msgbk_forms.insert(self.ctx.ids.form_global_msgbk, msg_back);
8482
8483        self.ctx.globals.syscom.sel_save_stock_stream = rd.len_bytes()?;
8484        let inner_cnt = rd.i32()?.max(0) as usize;
8485        self.ctx.globals.syscom.inner_save_streams.clear();
8486        for _ in 0..inner_cnt {
8487            self.ctx.globals.syscom.inner_save_streams.push(rd.len_bytes()?);
8488        }
8489        self.ctx.globals.syscom.inner_save_exists = self.ctx.globals.syscom.inner_save_streams.iter().any(|s| !s.is_empty());
8490        let sel_save_cnt = rd.i32()?.max(0) as usize;
8491        self.ctx.globals.syscom.sel_save_ids.clear();
8492        for _ in 0..sel_save_cnt {
8493            self.ctx.globals.syscom.sel_save_ids.push(rd.tid()?);
8494        }
8495        Ok(call_stack)
8496    }
8497
8498
8499
8500    fn write_cpp_proc_record(
8501        w: &mut crate::original_save::OriginalStreamWriter,
8502        proc_type: i32,
8503        element: &[i32],
8504        option: i32,
8505    ) {
8506        w.push_i32(proc_type);
8507        w.push_element(element);
8508        w.push_i32(0);
8509        w.push_i32(0);
8510        w.push_bool(false);
8511        w.push_bool(false);
8512        w.push_bool(false);
8513        w.push_i32(option);
8514    }
8515
8516    fn write_cpp_runtime_proc_stack(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8517        // Do not fabricate C_tnm_proc states.  Until the real C++ proc element,
8518        // arg_list, return_value_flag and option are mirrored from runtime state,
8519        // only the script proc can be represented safely; other transient waits are
8520        // saved as NONE rather than writing a guessed proc_type.
8521        let proc_type = if matches!(self.ctx.last_proc_kind(), runtime::ProcKind::Script) { 1 } else { 0 };
8522        Self::write_cpp_proc_record(w, proc_type, &[], 0);
8523        w.push_i32(0);
8524    }
8525
8526    fn read_cpp_proc_record(&self, rd: &mut crate::original_save::OriginalStreamReader<'_>) -> Result<()> {
8527        let _proc_type = rd.i32()?;
8528        let _element = rd.element()?;
8529        let _arg_list_id = rd.i32()?;
8530        let _arg_list: Vec<()> = rd.extend_items(|rd| {
8531            let _ = self.read_cpp_prop(rd)?;
8532            Ok(())
8533        })?;
8534        let _key_skip_enable_flag = rd.bool()?;
8535        let _skip_disable_flag = rd.bool()?;
8536        let _return_value_flag = rd.bool()?;
8537        let _option = rd.i32()?;
8538        Ok(())
8539    }
8540
8541    fn cpp_mwnd_element(stage_idx: i64, mwnd_no: Option<usize>) -> Vec<i32> {
8542        let Some(no) = mwnd_no else {
8543            return Vec::new();
8544        };
8545        let stage_head = match stage_idx {
8546            0 => crate::runtime::forms::codes::ELM_GLOBAL_BACK,
8547            2 => crate::runtime::forms::codes::ELM_GLOBAL_NEXT,
8548            _ => crate::runtime::forms::codes::ELM_GLOBAL_FRONT,
8549        };
8550        vec![
8551            stage_head,
8552            crate::runtime::forms::codes::ELM_STAGE_MWND,
8553            crate::runtime::forms::codes::ELM_ARRAY,
8554            no as i32,
8555        ]
8556    }
8557
8558    fn decode_cpp_mwnd_element(elm: &[i32]) -> Option<(i64, usize)> {
8559        if elm.len() < 4 {
8560            return None;
8561        }
8562        let stage_idx = if elm[0] == crate::runtime::forms::codes::ELM_GLOBAL_BACK {
8563            0
8564        } else if elm[0] == crate::runtime::forms::codes::ELM_GLOBAL_FRONT {
8565            1
8566        } else if elm[0] == crate::runtime::forms::codes::ELM_GLOBAL_NEXT {
8567            2
8568        } else {
8569            return None;
8570        };
8571        if elm[1] != crate::runtime::forms::codes::ELM_STAGE_MWND {
8572            return None;
8573        }
8574        if elm[2] != crate::runtime::forms::codes::ELM_ARRAY {
8575            return None;
8576        }
8577        if elm[3] < 0 {
8578            return None;
8579        }
8580        Some((stage_idx, elm[3] as usize))
8581    }
8582
8583    fn apply_saved_current_mwnd_elements(
8584        &mut self,
8585        cur_mwnd: &[i32],
8586        cur_sel_mwnd: &[i32],
8587        last_mwnd: &[i32],
8588    ) {
8589        self.ctx.globals.current_mwnd_no = None;
8590        self.ctx.globals.current_sel_mwnd_no = None;
8591        self.ctx.globals.last_mwnd_no = None;
8592
8593        if let Some((stage, no)) = Self::decode_cpp_mwnd_element(cur_mwnd) {
8594            self.ctx.globals.current_mwnd_stage_idx = stage;
8595            self.ctx.globals.current_mwnd_no = Some(no);
8596        }
8597        if let Some((stage, no)) = Self::decode_cpp_mwnd_element(cur_sel_mwnd) {
8598            self.ctx.globals.current_sel_mwnd_stage_idx = stage;
8599            self.ctx.globals.current_sel_mwnd_no = Some(no);
8600        }
8601        if let Some((stage, no)) = Self::decode_cpp_mwnd_element(last_mwnd) {
8602            self.ctx.globals.last_mwnd_stage_idx = stage;
8603            self.ctx.globals.last_mwnd_no = Some(no);
8604        }
8605    }
8606
8607    fn current_local_save_id(&self) -> [u16; 7] {
8608        let now = crate::platform_time::local_time_fields();
8609        [
8610            now.year.clamp(0, u16::MAX as i32) as u16,
8611            now.month as u16,
8612            now.day as u16,
8613            now.hour as u16,
8614            now.minute as u16,
8615            now.second as u16,
8616            now.millisecond as u16,
8617        ]
8618    }
8619
8620    /// Mirror of C++ `C_tnm_eng::save_local()`. Captures the engine snapshot into
8621    /// `ctx.local_save_snapshot` so subsequent SAVE / QUICK_SAVE / END_SAVE invocations
8622    /// write the savepoint-time state, not whatever transient menu state happens to be
8623    /// live when the user picks a slot.
8624    fn build_local_save_snapshot(&mut self) {
8625        let local_stream = self.build_original_local_stream();
8626        let local_ex_stream = self.build_original_local_ex_stream();
8627        let snapshot = crate::runtime::LocalSaveSnapshot {
8628            save_id: self.current_local_save_id(),
8629            append_dir: self.ctx.globals.append_dir.clone(),
8630            append_name: self.ctx.globals.append_name.clone(),
8631            save_scene_title: self.ctx.globals.syscom.current_save_scene_title.clone(),
8632            save_msg: String::new(),
8633            save_full_msg: self.ctx.globals.syscom.current_save_full_message.clone(),
8634            local_stream,
8635            local_ex_stream,
8636            sel_saves: self
8637                .ctx
8638                .local_save_snapshot
8639                .as_ref()
8640                .map(|s| s.sel_saves.clone())
8641                .unwrap_or_default(),
8642        };
8643        self.ctx.local_save_snapshot = Some(snapshot);
8644    }
8645
8646    fn build_original_local_stream(&self) -> Vec<u8> {
8647        let mut w = crate::original_save::OriginalStreamWriter::new();
8648        let scene_name = self.current_scene_name.as_deref().unwrap_or("");
8649        let flag_cnt = self.local_flag_count();
8650        use crate::runtime::forms::codes;
8651
8652        w.push_str(scene_name);
8653        w.push_i32(self.current_line_no);
8654        w.push_i32(self.stream.get_prg_cntr() as i32);
8655
8656        self.write_cpp_runtime_proc_stack(&mut w);
8657        let cur_mwnd = Self::cpp_mwnd_element(
8658            self.ctx.globals.current_mwnd_stage_idx,
8659            self.ctx.globals.current_mwnd_no,
8660        );
8661        let cur_sel_mwnd = Self::cpp_mwnd_element(
8662            self.ctx.globals.current_sel_mwnd_stage_idx,
8663            self.ctx.globals.current_sel_mwnd_no,
8664        );
8665        let last_mwnd = Self::cpp_mwnd_element(
8666            self.ctx.globals.last_mwnd_stage_idx,
8667            self.ctx.globals.last_mwnd_no,
8668        );
8669        w.push_element(&cur_mwnd);
8670        w.push_element(&cur_sel_mwnd);
8671        w.push_element(&last_mwnd);
8672        w.push_str(&self.ctx.globals.syscom.current_save_scene_title);
8673        let current_full_message = if self.ctx.globals.syscom.current_save_full_message.is_empty() {
8674            self.ctx.globals.syscom.current_save_message.as_str()
8675        } else {
8676            self.ctx.globals.syscom.current_save_full_message.as_str()
8677        };
8678        w.push_str(current_full_message);
8679
8680        let btn_cnt = self.mwnd_waku_btn_count();
8681        for idx in 0..btn_cnt {
8682            w.push_bool(self.ctx.globals.syscom.mwnd_btn_disable.get(&(idx as i64)).copied().unwrap_or(false));
8683        }
8684        w.push_str(&self.ctx.globals.script.font_name);
8685        w.push_raw(&self.build_cpp_local_data_pod());
8686
8687        w.push_i32(self.int_stack.len() as i32);
8688        for v in &self.int_stack { w.push_i32(*v); }
8689        w.push_i32(self.str_stack.len() as i32);
8690        for s in &self.str_stack { w.push_str(s); }
8691        w.push_i32(self.element_points.len() as i32);
8692        for p in &self.element_points { w.push_i32(*p as i32); }
8693
8694        w.push_i32(self.ctx.globals.local_real_time.clamp(i32::MIN as i64, i32::MAX as i64) as i32);
8695        w.push_i32(self.ctx.globals.local_game_time.clamp(i32::MIN as i64, i32::MAX as i64) as i32);
8696        w.push_i32(self.ctx.globals.local_wipe_time.clamp(i32::MIN as i64, i32::MAX as i64) as i32);
8697        self.write_cpp_syscom_menu(&mut w);
8698
8699        let fog = &self.ctx.globals.fog_global;
8700        w.push_str(&fog.name);
8701        Self::write_cpp_int_event_raw(&mut w, &fog.x_event);
8702        w.push_i32(fog.near as i32);
8703        w.push_i32(fog.far as i32);
8704
8705        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_A), flag_cnt);
8706        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_B), flag_cnt);
8707        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_C), flag_cnt);
8708        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_D), flag_cnt);
8709        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_E), flag_cnt);
8710        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_F), flag_cnt);
8711        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_X), flag_cnt);
8712        w.push_fixed_str_list(self.str_list_by_element(codes::ELM_GLOBAL_S), flag_cnt);
8713        w.push_extend_i32_list(&self.ctx.globals.local_flag_h);
8714        w.push_extend_i32_list(&self.ctx.globals.local_flag_i);
8715        w.push_extend_i32_list(&self.ctx.globals.local_flag_j);
8716        w.push_fixed_str_list(self.str_list_by_element(codes::ELM_GLOBAL_NAMAE_LOCAL), 26 + 26 * 26);
8717
8718        self.write_cpp_inc_prop_list(&mut w);
8719        self.write_cpp_current_scene_prop_lists(&mut w);
8720
8721        let counter_list = self.ctx.globals.counter_lists.values().next().cloned().unwrap_or_default();
8722        w.push_fixed_items(&counter_list, |w, c| self.write_cpp_counter_param(w, c));
8723
8724        let frame_action = self.ctx.globals.frame_actions.values().next().cloned().unwrap_or_default();
8725        self.write_cpp_frame_action(&mut w, &frame_action);
8726
8727        let frame_action_ch = self.ctx.globals.frame_action_lists.values().next().cloned().unwrap_or_default();
8728        w.push_fixed_items(&frame_action_ch, |w, fa| self.write_cpp_frame_action(w, fa));
8729
8730        // Original C_elm_g00_buf::save writes the file name for each slot.
8731        w.push_fixed_items(&self.ctx.globals.g00buf_names, |w, name| w.push_str(name.as_deref().unwrap_or("")));
8732
8733        let mask_list = self.ctx.globals.mask_lists.values().next().map(|m| m.masks.clone()).unwrap_or_default();
8734        w.push_fixed_items(&mask_list, |w, m| {
8735            Self::write_cpp_int_event_raw(w, &m.x_event);
8736            Self::write_cpp_int_event_raw(w, &m.y_event);
8737            w.push_str(m.name.as_deref().unwrap_or(""));
8738        });
8739
8740        self.write_cpp_stage(&mut w, 0);
8741        self.write_cpp_stage(&mut w, 1);
8742        self.write_cpp_screen(&mut w);
8743        self.write_cpp_sound(&mut w);
8744
8745        let pcm_events = self.ctx.globals.pcm_event_lists.values().next().cloned().unwrap_or_default();
8746        w.push_fixed_items(&pcm_events, |w, ev| self.write_cpp_pcm_event(w, ev));
8747
8748        let editboxes = self.ctx.globals.editbox_lists.values().next().map(|e| e.boxes.clone()).unwrap_or_default();
8749        w.push_fixed_items(&editboxes, |w, e| self.write_cpp_editbox(w, e));
8750
8751        w.push_i32(self.call_stack.len() as i32);
8752        for frame in &self.call_stack {
8753            self.write_cpp_call_frame(&mut w, frame);
8754        }
8755        self.write_cpp_msg_back(&mut w);
8756
8757        w.push_len_bytes(&self.ctx.globals.syscom.sel_save_stock_stream);
8758        w.push_i32(self.ctx.globals.syscom.inner_save_streams.len() as i32);
8759        for stream in &self.ctx.globals.syscom.inner_save_streams {
8760            w.push_len_bytes(stream);
8761        }
8762        w.push_i32(self.ctx.globals.syscom.sel_save_ids.len() as i32);
8763        for tid in &self.ctx.globals.syscom.sel_save_ids {
8764            w.push_tid(tid);
8765        }
8766        w.into_inner()
8767    }
8768
8769    fn build_original_local_ex_stream(&self) -> Vec<u8> {
8770        let mut w = crate::original_save::OriginalStreamWriter::new();
8771        let s = &self.ctx.globals.syscom;
8772        for i in 0..4 {
8773            let sw = s.local_extra_switches.get(i).copied().unwrap_or(if i == 0 { s.local_extra_switch } else { runtime::globals::ToggleFeatureState::default() });
8774            w.push_bool(sw.exist);
8775            w.push_bool(sw.enable);
8776            w.push_bool(sw.onoff);
8777        }
8778        for i in 0..4 {
8779            let mode = s.local_extra_modes.get(i).copied().unwrap_or(if i == 0 { s.local_extra_mode } else { runtime::globals::ValueFeatureState::default() });
8780            w.push_bool(mode.exist);
8781            w.push_bool(mode.enable);
8782            w.push_padding(2);
8783            w.push_i32(mode.value as i32);
8784        }
8785        let out = w.into_inner();
8786        debug_assert_eq!(out.len(), 44);
8787        out
8788    }
8789
8790    fn parse_original_local_ex_stream(&mut self, local_ex_stream: &[u8]) -> Result<()> {
8791        if local_ex_stream.len() < 44 { return Ok(()); }
8792        let mut rd = crate::original_save::OriginalStreamReader::new(local_ex_stream);
8793        for i in 0..4 {
8794            self.ctx.globals.syscom.local_extra_switches[i].exist = rd.bool()?;
8795            self.ctx.globals.syscom.local_extra_switches[i].enable = rd.bool()?;
8796            self.ctx.globals.syscom.local_extra_switches[i].onoff = rd.bool()?;
8797        }
8798        for i in 0..4 {
8799            self.ctx.globals.syscom.local_extra_modes[i].exist = rd.bool()?;
8800            self.ctx.globals.syscom.local_extra_modes[i].enable = rd.bool()?;
8801            rd.skip(2)?;
8802            self.ctx.globals.syscom.local_extra_modes[i].value = rd.i32()? as i64;
8803        }
8804        self.ctx.globals.syscom.local_extra_switch = self.ctx.globals.syscom.local_extra_switches[0];
8805        self.ctx.globals.syscom.local_extra_mode = self.ctx.globals.syscom.local_extra_modes[0];
8806        Ok(())
8807    }
8808
8809    fn parse_original_local_stream(&mut self, local_stream: &[u8]) -> Result<RuntimeDiskSnapshot> {
8810        let mut rd = crate::original_save::OriginalStreamReader::new(local_stream);
8811        let flag_cnt = self.local_flag_count();
8812        use crate::runtime::forms::codes;
8813
8814        let scene_name = rd.string()?;
8815        let line_no = rd.i32()?;
8816        let pc = rd.i32()?;
8817
8818        self.read_cpp_proc_record(&mut rd)?;
8819        let proc_stack_cnt = rd.i32()?.max(0) as usize;
8820        for _ in 0..proc_stack_cnt { self.read_cpp_proc_record(&mut rd)?; }
8821        let cur_mwnd = rd.element()?;
8822        let cur_sel_mwnd = rd.element()?;
8823        let last_mwnd = rd.element()?;
8824        self.apply_saved_current_mwnd_elements(&cur_mwnd, &cur_sel_mwnd, &last_mwnd);
8825        self.ctx.globals.syscom.current_save_scene_title = rd.string()?;
8826        self.ctx.globals.syscom.current_save_full_message = rd.string()?;
8827        self.ctx.globals.syscom.current_save_message.clear();
8828
8829        let btn_cnt = self.mwnd_waku_btn_count();
8830        self.ctx.globals.syscom.mwnd_btn_disable.clear();
8831        for idx in 0..btn_cnt {
8832            if rd.bool()? {
8833                self.ctx.globals.syscom.mwnd_btn_disable.insert(idx as i64, true);
8834            }
8835        }
8836        self.ctx.globals.script.font_name = rd.string()?;
8837        rd.skip(356)?;
8838
8839        let int_cnt = rd.i32()?.max(0) as usize;
8840        let mut int_stack = Vec::with_capacity(int_cnt);
8841        for _ in 0..int_cnt { int_stack.push(rd.i32()?); }
8842        let str_cnt = rd.i32()?.max(0) as usize;
8843        let mut str_stack = Vec::with_capacity(str_cnt);
8844        for _ in 0..str_cnt { str_stack.push(rd.string()?); }
8845        let ep_cnt = rd.i32()?.max(0) as usize;
8846        let mut element_points = Vec::with_capacity(ep_cnt);
8847        for _ in 0..ep_cnt { element_points.push(rd.i32()?.max(0) as usize); }
8848
8849        self.ctx.globals.local_real_time = rd.i32()? as i64;
8850        self.ctx.globals.local_game_time = rd.i32()? as i64;
8851        self.ctx.globals.local_wipe_time = rd.i32()? as i64;
8852        rd.skip(76)?;
8853
8854        let fog_name = rd.string()?;
8855        let fog_x = Self::read_cpp_int_event_raw(&mut rd)?;
8856        let fog_near = rd.i32()?;
8857        let fog_far = rd.i32()?;
8858        self.ctx.globals.fog_global.name = fog_name;
8859        self.ctx.globals.fog_global.enabled = !self.ctx.globals.fog_global.name.is_empty();
8860        self.ctx.globals.fog_global.texture_image_id = None;
8861        if self.ctx.globals.fog_global.enabled {
8862            match self.ctx.images.load_g00(&self.ctx.globals.fog_global.name, 0) {
8863                Ok(id) => self.ctx.globals.fog_global.texture_image_id = Some(id),
8864                Err(e) => log::error!(
8865                    "load_local fog texture '{}' failed: {e}",
8866                    self.ctx.globals.fog_global.name
8867                ),
8868            }
8869        }
8870        self.ctx.globals.fog_global.x_event = fog_x;
8871        self.ctx.globals.fog_global.scroll_x = self.ctx.globals.fog_global.x_event.get_total_value() as f32;
8872        self.ctx.globals.fog_global.near = fog_near as f32;
8873        self.ctx.globals.fog_global.far = fog_far as f32;
8874
8875        let a = rd.fixed_i32_list()?;
8876        let b = rd.fixed_i32_list()?;
8877        let c = rd.fixed_i32_list()?;
8878        let d = rd.fixed_i32_list()?;
8879        let e = rd.fixed_i32_list()?;
8880        let f = rd.fixed_i32_list()?;
8881        let x = rd.fixed_i32_list()?;
8882        let s = rd.fixed_str_list()?;
8883        let h = rd.extend_i32_list()?;
8884        let i = rd.extend_i32_list()?;
8885        let j = rd.extend_i32_list()?;
8886        let namae_local = rd.fixed_str_list()?;
8887        self.ctx.globals.int_lists.insert(codes::ELM_GLOBAL_A as u32, resize_i64_vec(a, flag_cnt));
8888        self.ctx.globals.int_lists.insert(codes::ELM_GLOBAL_B as u32, resize_i64_vec(b, flag_cnt));
8889        self.ctx.globals.int_lists.insert(codes::ELM_GLOBAL_C as u32, resize_i64_vec(c, flag_cnt));
8890        self.ctx.globals.int_lists.insert(codes::ELM_GLOBAL_D as u32, resize_i64_vec(d, flag_cnt));
8891        self.ctx.globals.int_lists.insert(codes::ELM_GLOBAL_E as u32, resize_i64_vec(e, flag_cnt));
8892        self.ctx.globals.int_lists.insert(codes::ELM_GLOBAL_F as u32, resize_i64_vec(f, flag_cnt));
8893        self.ctx.globals.int_lists.insert(codes::ELM_GLOBAL_X as u32, resize_i64_vec(x, flag_cnt));
8894        self.ctx.globals.str_lists.insert(codes::ELM_GLOBAL_S as u32, resize_string_vec(s, flag_cnt));
8895        self.ctx.globals.local_flag_h = h;
8896        self.ctx.globals.local_flag_i = i;
8897        self.ctx.globals.local_flag_j = j;
8898        self.ctx.globals.str_lists.insert(codes::ELM_GLOBAL_NAMAE_LOCAL as u32, resize_string_vec(namae_local, 26 + 26 * 26));
8899
8900        let call_stack = self.parse_cpp_tail_state(&mut rd, &scene_name)?;
8901
8902        Ok(RuntimeDiskSnapshot {
8903            scene_name,
8904            scene_no: -1,
8905            line_no,
8906            pc,
8907            int_stack,
8908            str_stack,
8909            element_points,
8910            call_stack,
8911        })
8912    }
8913
8914    fn save_load_trace_enabled() -> bool {
8915        std::env::var_os("SG_SAVELOAD_TRACE").is_some()
8916    }
8917
8918    fn perform_runtime_save_request(&mut self, req: RuntimeSaveRequest) -> Result<()> {
8919        if req.kind == RuntimeSaveKind::Inner {
8920            // C++ `tnm_saveload_proc_create_inner_save` copies the current
8921            // `m_local_save` into the inner-save slot. It must not reserialize the
8922            // live runtime (which may be the save/load menu).
8923            let Some(snapshot) = self.ctx.local_save_snapshot.as_ref() else {
8924                log::error!(
8925                    "[SG_SAVELOAD] inner save dropped idx={}: no local_save snapshot",
8926                    req.index
8927                );
8928                return Ok(());
8929            };
8930            if Self::save_load_trace_enabled() {
8931                eprintln!("[SG_SAVELOAD_TRACE][VM] save inner idx={}", req.index);
8932            }
8933            if self.ctx.globals.syscom.inner_save_streams.len() <= req.index {
8934                self.ctx.globals.syscom.inner_save_streams.resize_with(req.index + 1, Vec::new);
8935            }
8936            self.ctx.globals.syscom.inner_save_streams[req.index] = snapshot.local_stream.clone();
8937            self.ctx.globals.syscom.inner_save_exists = true;
8938            return Ok(());
8939        }
8940
8941        // Normal / quick / end save mirror C++ `tnm_save_local_on_file`: bail out when
8942        // there is no snapshot (equivalent to `m_local_save.save_stream.empty()`).
8943        // Without this, picking a slot in the save menu would otherwise serialize the
8944        // menu itself - the bug we're fixing.
8945        if self.ctx.local_save_snapshot.is_none() {
8946            log::error!(
8947                "[SG_SAVELOAD] save dropped (kind={:?} idx={}): no local_save snapshot. \
8948                 SAVEPOINT has not fired in the current message block - either the script \
8949                 set dont_set_save_point or auto-SAVEPOINT wasn't reached yet. No file written.",
8950                req.kind, req.index
8951            );
8952            return Ok(());
8953        }
8954
8955        // Refresh local_ex_stream from the live runtime; mirrors C++ `save_local_ex()`
8956        // being called inside `tnm_save_local_on_file` right before writing.
8957        let refreshed_ex = self.build_original_local_ex_stream();
8958        if let Some(snapshot) = self.ctx.local_save_snapshot.as_mut() {
8959            snapshot.local_ex_stream = refreshed_ex;
8960        }
8961
8962        let slot = self.ensure_runtime_slot_for_save(req);
8963        let Some(path) = self.runtime_save_file_path(req.kind, req.index) else { return Ok(()); };
8964        if Self::save_load_trace_enabled() {
8965            eprintln!(
8966                "[SG_SAVELOAD_TRACE][VM] save begin kind={:?} idx={} path={} file_exists_before={}",
8967                req.kind,
8968                req.index,
8969                path.display(),
8970                path.exists()
8971            );
8972        }
8973        let snapshot = self
8974            .ctx
8975            .local_save_snapshot
8976            .as_ref()
8977            .expect("snapshot presence checked above");
8978        let env = crate::original_save::OriginalLocalSaveEnvelope {
8979            save_id: snapshot.save_id,
8980            append_dir: snapshot.append_dir.clone(),
8981            append_name: snapshot.append_name.clone(),
8982            title: snapshot.save_scene_title.clone(),
8983            message: snapshot.save_msg.clone(),
8984            full_message: snapshot.save_full_msg.clone(),
8985            local_stream: snapshot.local_stream.clone(),
8986            local_ex_stream: snapshot.local_ex_stream.clone(),
8987            sel_saves: snapshot.sel_saves.clone(),
8988        };
8989        crate::original_save::write_local_save_file(&path, &slot, &env)?;
8990        crate::runtime::forms::syscom::write_global_save(&self.ctx);
8991        if Self::save_load_trace_enabled() {
8992            eprintln!(
8993                "[SG_SAVELOAD_TRACE][VM] save written kind={:?} idx={} path={} bytes={}",
8994                req.kind,
8995                req.index,
8996                path.display(),
8997                std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0)
8998            );
8999        }
9000        if let Some(saved_slot) = crate::original_save::read_slot_from_path(&path) {
9001            match req.kind {
9002                RuntimeSaveKind::Normal => {
9003                    if self.ctx.globals.syscom.save_slots.len() <= req.index {
9004                        self.ctx.globals.syscom.save_slots.resize_with(req.index + 1, Default::default);
9005                    }
9006                    self.ctx.globals.syscom.save_slots[req.index] = saved_slot;
9007                }
9008                RuntimeSaveKind::Quick => {
9009                    if self.ctx.globals.syscom.quick_save_slots.len() <= req.index {
9010                        self.ctx.globals.syscom.quick_save_slots.resize_with(req.index + 1, Default::default);
9011                    }
9012                    self.ctx.globals.syscom.quick_save_slots[req.index] = saved_slot;
9013                }
9014                RuntimeSaveKind::End => {
9015                    self.ctx.globals.syscom.end_save_exists = true;
9016                }
9017                RuntimeSaveKind::Inner => {}
9018            }
9019        }
9020        if let Some(save_kind) = Self::save_kind_to_original(req.kind) {
9021            let save_no = crate::original_save::original_save_no(
9022                self.configured_runtime_save_count(false),
9023                self.configured_runtime_save_count(true),
9024                save_kind,
9025                req.index,
9026            );
9027            if Self::save_load_trace_enabled() {
9028                eprintln!(
9029                    "[SG_SAVELOAD_TRACE][VM] save thumb write kind={:?} idx={} original_save_no={}",
9030                    req.kind,
9031                    req.index,
9032                    save_no
9033                );
9034            }
9035            crate::runtime::forms::syscom::write_runtime_slot_thumb(&mut self.ctx, save_no);
9036        }
9037        Ok(())
9038    }
9039
9040    fn perform_runtime_load_request(&mut self, req: RuntimeLoadRequest) -> Result<()> {
9041        if Self::save_load_trace_enabled() {
9042            eprintln!("[SG_SAVELOAD_TRACE][VM] load begin kind={:?} idx={}", req.kind, req.index);
9043        }
9044        struct LoadedEnvelopeMeta {
9045            save_id: [u16; 7],
9046            append_dir: String,
9047            append_name: String,
9048            title: String,
9049            message: String,
9050            full_message: String,
9051            sel_saves: Vec<crate::original_save::OriginalLocalSaveEnvelope>,
9052        }
9053        let (local_stream, local_ex_stream, loaded_meta) = if req.kind == RuntimeSaveKind::Inner {
9054            let Some(stream) = self.ctx.globals.syscom.inner_save_streams.get(req.index).cloned() else { return Ok(()); };
9055            (stream, Vec::new(), None)
9056        } else {
9057            let Some(path) = self.runtime_save_file_path(req.kind, req.index) else { return Ok(()); };
9058            if Self::save_load_trace_enabled() {
9059                eprintln!(
9060                    "[SG_SAVELOAD_TRACE][VM] load read kind={:?} idx={} path={} file_exists={}",
9061                    req.kind,
9062                    req.index,
9063                    path.display(),
9064                    path.exists()
9065                );
9066            }
9067            let (_header, env) = crate::original_save::read_local_save_file(&path)?;
9068            let meta = LoadedEnvelopeMeta {
9069                save_id: env.save_id,
9070                append_dir: env.append_dir.clone(),
9071                append_name: env.append_name.clone(),
9072                title: env.title.clone(),
9073                message: env.message.clone(),
9074                full_message: env.full_message.clone(),
9075                sel_saves: env.sel_saves.clone(),
9076            };
9077            (env.local_stream, env.local_ex_stream, Some(meta))
9078        };
9079        if let Some(meta) = loaded_meta.as_ref() {
9080            let append_dir = meta.append_dir.clone();
9081            let append_name = meta.append_name.clone();
9082            self.ctx.globals.append_dir = append_dir.clone();
9083            self.ctx.globals.append_name = append_name;
9084            self.ctx.images.set_current_append_dir(append_dir.clone());
9085            self.ctx.movie.set_current_append_dir(append_dir.clone());
9086            self.ctx.bgm.set_current_append_dir(append_dir);
9087        }
9088        // VM-side equivalent of C++ `tnm_finish_local`: drop excall frames, sel
9089        // points, and the stale save point. The loaded scene re-establishes its
9090        // own context; without this, when the loaded scene eventually issues a
9091        // RETURN we'd pop back into the orphaned save/load menu excall frame.
9092        self.scene_stack.clear();
9093        self.sel_point_stack.clear();
9094        self.save_point = None;
9095        self.ctx.local_save_snapshot = None;
9096        self.ctx.begin_runtime_load_apply();
9097        let snapshot = self.parse_original_local_stream(&local_stream)?;
9098        self.parse_original_local_ex_stream(&local_ex_stream)?;
9099        // Mirror C++ `tnm_load_local_on_file` + tail of `load_local`: re-populate
9100        // `m_local_save` so the loaded scene can SAVE without first taking another
9101        // SAVEPOINT. C++ clears save_msg and copies save_full_msg = cur_full_message
9102        // after load_local; do the same here.
9103        if let Some(meta) = loaded_meta {
9104            self.ctx.local_save_snapshot = Some(crate::runtime::LocalSaveSnapshot {
9105                save_id: meta.save_id,
9106                append_dir: meta.append_dir,
9107                append_name: meta.append_name,
9108                save_scene_title: meta.title,
9109                save_msg: String::new(),
9110                save_full_msg: self.ctx.globals.syscom.current_save_full_message.clone(),
9111                local_stream: local_stream.clone(),
9112                local_ex_stream: local_ex_stream.clone(),
9113                sel_saves: meta.sel_saves,
9114            });
9115            // The header text from the loaded file (which represents the last
9116            // append'd-message state) takes precedence over what's left in
9117            // current_save_message after parse, so subsequent saves echo what the
9118            // user actually saw last.
9119            let snap = self.ctx.local_save_snapshot.as_ref().unwrap();
9120            self.ctx.globals.syscom.current_save_scene_title = snap.save_scene_title.clone();
9121        }
9122        if snapshot.scene_name.is_empty() {
9123            log::error!(
9124                "[SG_SAVELOAD] aborting load (kind={:?} idx={}): saved snapshot has empty scene_name. \
9125                 This save file is unusable; please delete it.",
9126                req.kind, req.index
9127            );
9128            return Ok(());
9129        }
9130        let (mut stream, scene_no) = self.load_scene_stream(&snapshot.scene_name, 0)?;
9131        stream.set_prg_cntr(snapshot.pc.max(0) as usize)?;
9132        self.stream = stream;
9133        self.int_stack = snapshot.int_stack;
9134        self.str_stack = snapshot.str_stack;
9135        self.element_points = snapshot.element_points;
9136        self.call_stack = snapshot.call_stack;
9137        if self.call_stack.is_empty() {
9138            self.call_stack.push(self.scene_base_call());
9139        }
9140        self.gosub_return_stack.clear();
9141        self.current_scene_no = if snapshot.scene_no >= 0 { Some(snapshot.scene_no as usize) } else { Some(scene_no) };
9142        self.current_scene_name = Some(snapshot.scene_name);
9143        self.current_line_no = snapshot.line_no;
9144        self.ctx.current_scene_no = self.current_scene_no.map(|v| v as i64);
9145        self.ctx.current_scene_name = self.current_scene_name.clone();
9146        self.ctx.current_line_no = self.current_line_no as i64;
9147        self.ctx.wait = runtime::wait::VmWait::default();
9148        self.halted = false;
9149        self.delayed_ret_form = None;
9150        // C++ `C_elm_stage::load` / `C_elm_mwnd::load` end by calling each
9151        // object's `restruct_type()` to rebuild the visible render side
9152        // (image asset + sprite binding + transform). Rust's gfx runtime is
9153        // not in the save format, so do the equivalent walk here: for every
9154        // Gfx-backed object whose `file_name` is set, rebuild its gfx state
9155        // from the loaded globals. Without this the loaded scene renders as
9156        // a blank canvas while the saved data is technically all there.
9157        self.restore_runtime_bindings_after_load();
9158        self.ctx.mark_runtime_load_completed();
9159        Ok(())
9160    }
9161
9162    /// Walk every PCT-style object the loaded snapshot put back into
9163    /// `globals.stage_forms` (BG, top-level on each stage, plus mwnd-embedded
9164    /// button/face/object lists) and ask the gfx runtime to re-bind a sprite
9165    /// and re-load its image. Also writes `backend = Gfx` back into globals so
9166    /// the render pipeline's backend-dispatch reaches the Gfx arm instead of
9167    /// skipping the object (the save format never serialized the backend tag,
9168    /// so every loaded object starts with `backend = None`).
9169    ///
9170    /// Equivalent to the `restruct_type()` tail of C++ `C_elm_object::load`,
9171    /// for the PCT / SAVE_THUMB / THUMB / CAPTURE family. Specialized backends
9172    /// (RECT, STRING, NUMBER, WEATHER, MESH, BILLBOARD, MOVIE, EMOTE) carry
9173    /// runtime sprite IDs that aren't in the save format and would need their
9174    /// own backend-specific restruct path - logged as warnings here so we know
9175    /// what's still missing.
9176    fn restore_runtime_bindings_after_load(&mut self) {
9177        struct RebuildTask {
9178            stage_idx: i64,
9179            path: String,
9180            runtime_slot: usize,
9181            obj_snapshot: runtime::globals::ObjectState,
9182        }
9183
9184        // PCT (2), SAVE_THUMB (8), THUMB (11), CAPTURE (10) - everything that
9185        // C++ `restruct_type` routes through a single-image Gfx pipeline. EMOTE
9186        // and MOVIE use specialized backends here, so they need their own
9187        // type-specific restruct path and are intentionally not handled as Gfx.
9188        fn needs_gfx_restore(obj: &runtime::globals::ObjectState) -> bool {
9189            if !obj.used {
9190                return false;
9191            }
9192            let has_file = obj.file_name.as_deref().map(|s| !s.is_empty()).unwrap_or(false);
9193            has_file && matches!(obj.object_type, 2 | 8 | 10 | 11)
9194        }
9195
9196        fn assign_child_slots_and_backend(
9197            obj: &mut runtime::globals::ObjectState,
9198            next_nested: &mut usize,
9199        ) {
9200            if needs_gfx_restore(obj) {
9201                obj.backend = runtime::globals::ObjectBackend::Gfx;
9202            }
9203            for child in &mut obj.runtime.child_objects {
9204                if child.nested_runtime_slot.is_none() {
9205                    child.nested_runtime_slot = Some(*next_nested);
9206                    *next_nested += 1;
9207                }
9208                assign_child_slots_and_backend(child, next_nested);
9209            }
9210        }
9211
9212        fn collect_rebuild_tasks(
9213            out: &mut Vec<RebuildTask>,
9214            stage_idx: i64,
9215            path: String,
9216            slot_hint: usize,
9217            obj: &runtime::globals::ObjectState,
9218        ) {
9219            let runtime_slot = obj.runtime_slot_or(slot_hint);
9220            if needs_gfx_restore(obj) {
9221                out.push(RebuildTask {
9222                    stage_idx,
9223                    path: path.clone(),
9224                    runtime_slot,
9225                    obj_snapshot: obj.clone(),
9226                });
9227            }
9228            for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
9229                collect_rebuild_tasks(
9230                    out,
9231                    stage_idx,
9232                    format!("{path}.child[{child_idx}]"),
9233                    child_idx,
9234                    child,
9235                );
9236            }
9237        }
9238
9239        // C++ load reconstructs every C_elm_object recursively via load/restruct_type.
9240        // Rust's save stream does not contain runtime slots, so rebuild the stable
9241        // slot assignment before re-binding sprites. Top-level STAGE.OBJECT keeps
9242        // its index slot; MWND internal roots use the embedded 200000+ range;
9243        // OBJECT.CHILD descendants use the nested 100000+ range.
9244        let stage_form_ids: Vec<u32> = self.ctx.globals.stage_forms.keys().copied().collect();
9245        for form_id in &stage_form_ids {
9246            let Some(stage_form) = self.ctx.globals.stage_forms.get_mut(form_id) else {
9247                continue;
9248            };
9249            let mut stage_ids: Vec<i64> = stage_form
9250                .object_lists
9251                .keys()
9252                .chain(stage_form.mwnd_lists.keys())
9253                .copied()
9254                .collect();
9255            stage_ids.sort_unstable();
9256            stage_ids.dedup();
9257
9258            for stage_idx in stage_ids {
9259                let mut next_nested = stage_form
9260                    .next_nested_object_slot
9261                    .get(&stage_idx)
9262                    .copied()
9263                    .unwrap_or(100000)
9264                    .max(100000);
9265                let mut next_embedded = stage_form
9266                    .next_embedded_object_slot
9267                    .get(&stage_idx)
9268                    .copied()
9269                    .unwrap_or(200000)
9270                    .max(200000);
9271                let existing_embedded = stage_form.embedded_object_slots.clone();
9272                let mut embedded_assignments: Vec<(String, usize)> = Vec::new();
9273                let mut alloc_embedded = |key: String| -> usize {
9274                    let full = format!("{stage_idx}:{key}");
9275                    if let Some(slot) = existing_embedded.get(&full).copied() {
9276                        return slot;
9277                    }
9278                    let slot = next_embedded;
9279                    next_embedded += 1;
9280                    embedded_assignments.push((full, slot));
9281                    slot
9282                };
9283
9284                if let Some(objs) = stage_form.object_lists.get_mut(&stage_idx) {
9285                    for obj in objs.iter_mut() {
9286                        assign_child_slots_and_backend(obj, &mut next_nested);
9287                    }
9288                }
9289                if let Some(mwnds) = stage_form.mwnd_lists.get_mut(&stage_idx) {
9290                    for (mwnd_idx, m) in mwnds.iter_mut().enumerate() {
9291                        for (i, obj) in m.button_list.iter_mut().enumerate() {
9292                            if obj.nested_runtime_slot.is_none() {
9293                                obj.nested_runtime_slot = Some(alloc_embedded(format!(
9294                                    "mwnd_button_{stage_idx}_{mwnd_idx}_{i}"
9295                                )));
9296                            }
9297                            assign_child_slots_and_backend(obj, &mut next_nested);
9298                        }
9299                        for (i, obj) in m.face_list.iter_mut().enumerate() {
9300                            if obj.nested_runtime_slot.is_none() {
9301                                obj.nested_runtime_slot = Some(alloc_embedded(format!(
9302                                    "mwnd_face_{stage_idx}_{mwnd_idx}_{i}"
9303                                )));
9304                            }
9305                            assign_child_slots_and_backend(obj, &mut next_nested);
9306                        }
9307                        for (i, obj) in m.object_list.iter_mut().enumerate() {
9308                            if obj.nested_runtime_slot.is_none() {
9309                                obj.nested_runtime_slot = Some(alloc_embedded(format!(
9310                                    "mwnd_object_{stage_idx}_{mwnd_idx}_{i}"
9311                                )));
9312                            }
9313                            assign_child_slots_and_backend(obj, &mut next_nested);
9314                        }
9315                    }
9316                }
9317
9318                for (key, slot) in embedded_assignments {
9319                    stage_form.embedded_object_slots.entry(key).or_insert(slot);
9320                }
9321                stage_form
9322                    .next_nested_object_slot
9323                    .insert(stage_idx, next_nested);
9324                stage_form
9325                    .next_embedded_object_slot
9326                    .insert(stage_idx, next_embedded);
9327            }
9328        }
9329
9330        let mut tasks: Vec<RebuildTask> = Vec::new();
9331        for form_id in &stage_form_ids {
9332            let Some(stage_form) = self.ctx.globals.stage_forms.get(form_id) else {
9333                continue;
9334            };
9335            let mut stage_ids: Vec<i64> = stage_form
9336                .object_lists
9337                .keys()
9338                .chain(stage_form.mwnd_lists.keys())
9339                .copied()
9340                .collect();
9341            stage_ids.sort_unstable();
9342            stage_ids.dedup();
9343            for stage_idx in stage_ids {
9344                if let Some(objs) = stage_form.object_lists.get(&stage_idx) {
9345                    for (obj_idx, obj) in objs.iter().enumerate() {
9346                        collect_rebuild_tasks(
9347                            &mut tasks,
9348                            stage_idx,
9349                            format!("stage[{stage_idx}].object[{obj_idx}]"),
9350                            obj_idx,
9351                            obj,
9352                        );
9353                    }
9354                }
9355                if let Some(mwnds) = stage_form.mwnd_lists.get(&stage_idx) {
9356                    for (mwnd_idx, m) in mwnds.iter().enumerate() {
9357                        for (i, obj) in m.button_list.iter().enumerate() {
9358                            collect_rebuild_tasks(
9359                                &mut tasks,
9360                                stage_idx,
9361                                format!("stage[{stage_idx}].mwnd[{mwnd_idx}].button[{i}]"),
9362                                i,
9363                                obj,
9364                            );
9365                        }
9366                        for (i, obj) in m.face_list.iter().enumerate() {
9367                            collect_rebuild_tasks(
9368                                &mut tasks,
9369                                stage_idx,
9370                                format!("stage[{stage_idx}].mwnd[{mwnd_idx}].face[{i}]"),
9371                                i,
9372                                obj,
9373                            );
9374                        }
9375                        for (i, obj) in m.object_list.iter().enumerate() {
9376                            collect_rebuild_tasks(
9377                                &mut tasks,
9378                                stage_idx,
9379                                format!("stage[{stage_idx}].mwnd[{mwnd_idx}].object[{i}]"),
9380                                i,
9381                                obj,
9382                            );
9383                        }
9384                    }
9385                }
9386            }
9387        }
9388
9389        let mut unsupported_count = 0usize;
9390        for form_id in &stage_form_ids {
9391            let Some(stage_form) = self.ctx.globals.stage_forms.get(form_id) else {
9392                continue;
9393            };
9394            for (_stage_idx, objs) in &stage_form.object_lists {
9395                for obj in objs {
9396                    if obj.used && obj.object_type != 0 && !needs_gfx_restore(obj) {
9397                        unsupported_count += 1;
9398                    }
9399                }
9400            }
9401        }
9402        if unsupported_count > 0 {
9403            log::warn!(
9404                "[SG_SAVELOAD] {unsupported_count} loaded top-level object(s) have type-specific backends whose runtime sprite IDs are not reconstructed by the Gfx restore path"
9405            );
9406        }
9407
9408        for task in tasks {
9409            if let Err(err) = self.ctx.gfx.restore_gfx_object_from_globals(
9410                &mut self.ctx.images,
9411                &mut self.ctx.layers,
9412                task.stage_idx,
9413                task.runtime_slot as i64,
9414                &task.obj_snapshot,
9415            ) {
9416                log::warn!(
9417                    "[SG_SAVELOAD] restore_gfx_object_from_globals path={} slot={} file={:?} failed: {err:#}",
9418                    task.path,
9419                    task.runtime_slot,
9420                    task.obj_snapshot.file_name
9421                );
9422            }
9423        }
9424    }
9425
9426    fn drain_runtime_save_load_requests(&mut self) -> Result<()> {
9427        // Auto SAVEPOINT must fire before any pending save in the same command
9428        // batch, so a SAVE issued from the script's first frame after a message
9429        // block start still has a snapshot to write.
9430        if self.ctx.take_pending_auto_savepoint() {
9431            self.build_local_save_snapshot();
9432        }
9433        if let Some(req) = self.ctx.take_runtime_save_request() {
9434            self.perform_runtime_save_request(req)?;
9435        }
9436        if let Some(req) = self.ctx.take_runtime_load_request() {
9437            self.perform_runtime_load_request(req)?;
9438        }
9439        Ok(())
9440    }
9441
9442    fn exec_return(&mut self, args: Vec<Value>) -> Result<bool> {
9443        // Pop callee frame.
9444        let Some(callee) = self.call_stack.pop() else {
9445            return Ok(false);
9446        };
9447
9448        // Return info is stored on the caller frame .
9449        let caller = match self.call_stack.last_mut() {
9450            Some(f) => f,
9451            None => {
9452                // No caller: treat as end.
9453                return Ok(false);
9454            }
9455        };
9456
9457        // C++ `tnm_scene_proc_gosub` persists the continuation on the caller
9458        // call frame (`save_call`), then `load_call` restores that caller.
9459        // Frame-action/user-command inline calls may run nested gosubs while a
9460        // script gosub is waiting, so the authoritative continuation must be
9461        // the caller frame here rather than any callee-local scratch state.
9462        let return_pc = caller.return_pc;
9463        let ret_form = caller.ret_form;
9464        if std::env::var_os("SIGLUS_TRACE_CALL_RETURN_PC").is_some() {
9465            eprintln!(
9466                "[SG_CALL_PC] return depth={} pc=0x{:x} ret_form={} override={:?} args={:?}",
9467                self.call_stack.len() + 1,
9468                return_pc,
9469                ret_form,
9470                callee.return_override,
9471                args
9472            );
9473        }
9474        self.stream.set_prg_cntr(return_pc)?;
9475
9476        match ret_form {
9477            f if f == self.cfg.fm_int => {
9478                let v = args.get(0).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
9479                self.push_int(v);
9480            }
9481            f if f == self.cfg.fm_str => {
9482                let s = args
9483                    .get(0)
9484                    .and_then(|v| v.as_str().map(|s| s.to_string()))
9485                    .unwrap_or_default();
9486                self.push_str(s);
9487            }
9488            _ => {
9489                // Ignore.
9490            }
9491        }
9492
9493        if callee.excall_proc {
9494            self.mark_excall_script_proc_pop_requested();
9495        }
9496
9497        Ok(callee.frame_action_proc)
9498    }
9499
9500    fn scene_base_call(&self) -> CallFrame {
9501        self.make_call_frame(self.cfg.fm_void, false, false, 0, None)
9502    }
9503
9504    fn load_scene_stream(
9505        &mut self,
9506        scene_name: &str,
9507        z_no: i32,
9508    ) -> Result<(SceneStream<'a>, usize)> {
9509        self.ensure_scene_pck_cache()?;
9510        let scene_no = self
9511            .scene_pck_cache
9512            .as_ref()
9513            .expect("scene pck cache initialized")
9514            .find_scene_no(scene_name)
9515            .ok_or_else(|| anyhow!("scene not found: {}", scene_name))?;
9516        let mut stream = self.cached_scene_stream(scene_no)?;
9517        self.sg_omv_trace(format!(
9518            "load_scene_stream resolved target={} scene_no={} z={} initial_pc=0x{:x} scn_len=0x{:x}",
9519            scene_name,
9520            scene_no,
9521            z_no,
9522            stream.get_prg_cntr(),
9523            stream.scn.len()
9524        ));
9525        self.call_cmd_names = self
9526            .scene_pck_cache
9527            .as_ref()
9528            .expect("scene pck cache initialized")
9529            .inc_cmd_name_map
9530            .clone();
9531        self.user_cmd_names = stream.scn_cmd_name_map.clone();
9532        match stream.jump_to_z_label(z_no.max(0) as usize) {
9533            Ok(()) => {
9534                self.sg_omv_trace(format!(
9535                    "load_scene_stream entered target={} scene_no={} z={} target_pc=0x{:x} user_cmd_cnt={} call_cmd_cnt={}",
9536                    scene_name,
9537                    scene_no,
9538                    z_no,
9539                    stream.get_prg_cntr(),
9540                    stream.scn_cmd_name_map.len(),
9541                    self.call_cmd_names.len()
9542                ));
9543            }
9544            Err(e) => {
9545                self.sg_omv_trace(format!(
9546                    "load_scene_stream failed target={} scene_no={} z={} error={}",
9547                    scene_name,
9548                    scene_no,
9549                    z_no,
9550                    e
9551                ));
9552                return Err(e);
9553            }
9554        }
9555        Ok((stream, scene_no))
9556    }
9557
9558    fn jump_to_scene_name(&mut self, scene_name: &str, z_no: i32) -> Result<()> {
9559        self.sg_omv_trace(format!("scene_jump target={} z={}", scene_name, z_no));
9560        let (stream, scene_no) = self.load_scene_stream(scene_name, z_no)?;
9561        self.stream = stream;
9562        self.current_scene_no = Some(scene_no);
9563        self.current_scene_name = Some(scene_name.to_string());
9564        self.current_line_no = -1;
9565        self.ctx.current_scene_no = Some(scene_no as i64);
9566        self.ctx.current_scene_name = Some(scene_name.to_string());
9567        self.ctx.current_line_no = -1;
9568        self.sg_omv_trace(format!(
9569            "scene_jump_entered target={} scene_no={} z={} pc=0x{:x}",
9570            scene_name,
9571            scene_no,
9572            z_no,
9573            self.stream.get_prg_cntr()
9574        ));
9575        Ok(())
9576    }
9577
9578    fn farcall_scene_name_ex(
9579        &mut self,
9580        scene_name: &str,
9581        z_no: i32,
9582        ret_form: i32,
9583        ex_call_proc: bool,
9584        scratch_source_args: &[Value],
9585    ) -> Result<()> {
9586        self.sg_omv_trace(format!(
9587            "scene_farcall target={} z={} ret_form={} ex_call_proc={} scratch_argc={}",
9588            scene_name,
9589            z_no,
9590            ret_form,
9591            ex_call_proc,
9592            scratch_source_args.len()
9593        ));
9594        self.trace_cf_branch_farcall(
9595            self.stream.get_prg_cntr(),
9596            scene_name,
9597            z_no,
9598            ret_form,
9599            ex_call_proc,
9600            scratch_source_args,
9601        );
9602        if (scene_name == "sys20_adv00" && matches!(z_no, 10 | 13 | 17))
9603            || (scene_name == "sys20_adv01" && z_no == 0)
9604        {
9605            let args_dbg = scratch_source_args
9606                .iter()
9607                .map(|v| format!("{v:?}"))
9608                .collect::<Vec<_>>()
9609                .join(", ");
9610            self.sg_cgm_coord_trace(format!(
9611                "farcall target={} z={} ret_form={} ex_call_proc={} argc={} args=[{}]",
9612                scene_name,
9613                z_no,
9614                ret_form,
9615                ex_call_proc,
9616                scratch_source_args.len(),
9617                args_dbg
9618            ));
9619        }
9620        let saved = SceneExecFrame {
9621            stream: self.stream.clone(),
9622            user_cmd_names: self.user_cmd_names.clone(),
9623            call_cmd_names: self.call_cmd_names.clone(),
9624            int_stack: std::mem::take(&mut self.int_stack),
9625            str_stack: std::mem::take(&mut self.str_stack),
9626            element_points: std::mem::take(&mut self.element_points),
9627            call_stack: std::mem::take(&mut self.call_stack),
9628            gosub_return_stack: std::mem::take(&mut self.gosub_return_stack),
9629            user_props: self.enter_cross_scene_user_prop_scope(),
9630            current_scene_no: self.current_scene_no,
9631            current_scene_name: self.current_scene_name.clone(),
9632            current_line_no: self.current_line_no,
9633            ret_form,
9634            excall_proc: ex_call_proc,
9635        };
9636        self.scene_stack.push(saved);
9637        let (stream, scene_no) = self.load_scene_stream(scene_name, z_no)?;
9638        self.stream = stream;
9639        let scratch_args = self.call_scratch_from_args(scratch_source_args);
9640        self.call_stack.push(self.make_call_frame(
9641            self.cfg.fm_void,
9642            false,
9643            false,
9644            scratch_source_args.len(),
9645            Some(scratch_args),
9646        ));
9647        self.current_scene_no = Some(scene_no);
9648        self.current_scene_name = Some(scene_name.to_string());
9649        self.current_line_no = -1;
9650        self.ctx.current_scene_no = Some(scene_no as i64);
9651        self.ctx.current_scene_name = Some(scene_name.to_string());
9652        self.ctx.current_line_no = -1;
9653        self.sg_omv_trace(format!(
9654            "scene_farcall_entered target={} scene_no={} z={} pc=0x{:x} call_depth={} scene_stack={}",
9655            scene_name,
9656            scene_no,
9657            z_no,
9658            self.stream.get_prg_cntr(),
9659            self.call_stack.len(),
9660            self.scene_stack.len()
9661        ));
9662        if ex_call_proc {
9663            self.mark_excall_script_proc_requested();
9664        }
9665        Ok(())
9666    }
9667
9668    fn return_from_scene(&mut self, args: Vec<Value>) -> Result<bool> {
9669        let Some(saved) = self.scene_stack.pop() else {
9670            return Ok(false);
9671        };
9672        self.sg_omv_trace(format!(
9673            "scene_return restore_scene={:?} restore_line={} ret_form={} args={:?}",
9674            saved.current_scene_name,
9675            saved.current_line_no,
9676            saved.ret_form,
9677            args
9678        ));
9679        self.stream = saved.stream;
9680        self.int_stack = saved.int_stack;
9681        self.str_stack = saved.str_stack;
9682        self.element_points = saved.element_points;
9683        self.call_stack = saved.call_stack;
9684        self.gosub_return_stack = saved.gosub_return_stack;
9685        self.restore_cross_scene_user_prop_scope(saved.user_props);
9686        self.current_scene_no = saved.current_scene_no;
9687        self.current_scene_name = saved.current_scene_name;
9688        self.current_line_no = saved.current_line_no;
9689        self.ctx.current_scene_no = self.current_scene_no.map(|v| v as i64);
9690        self.ctx.current_scene_name = self.current_scene_name.clone();
9691        self.ctx.current_line_no = self.current_line_no as i64;
9692        self.user_cmd_names = saved.user_cmd_names;
9693        self.call_cmd_names = saved.call_cmd_names;
9694        let was_excall_proc = saved.excall_proc;
9695
9696        match saved.ret_form {
9697            f if f == self.cfg.fm_int || f == self.cfg.fm_label => {
9698                let v = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
9699                self.push_int(v);
9700            }
9701            f if f == self.cfg.fm_str => {
9702                let s = args
9703                    .first()
9704                    .and_then(|v| v.as_str().map(|s| s.to_string()))
9705                    .unwrap_or_default();
9706                self.push_str(s);
9707            }
9708            _ => {}
9709        }
9710        if was_excall_proc {
9711            self.mark_excall_script_proc_pop_requested();
9712        }
9713        if self.cf_branch_trace_interesting_line() {
9714            self.sg_cf_branch_trace(
9715                self.stream.get_prg_cntr(),
9716                format!("kind=RETURN_RESTORED ret_form={} args={:?}", saved.ret_form, args),
9717            );
9718        }
9719        self.sg_omv_trace(format!(
9720            "scene_return_restored scene={:?} scene_no={:?} line={} pc=0x{:x} call_depth={} scene_stack={}",
9721            self.current_scene_name,
9722            self.current_scene_no,
9723            self.current_line_no,
9724            self.stream.get_prg_cntr(),
9725            self.call_stack.len(),
9726            self.scene_stack.len()
9727        ));
9728        Ok(true)
9729    }
9730
9731    fn exec_builtin_global_control(&mut self, form_id: i32, ret_form: i32) -> Result<bool> {
9732        match form_id {
9733            constants::elm_value::GLOBAL_SAVEPOINT => {
9734                // C++ `ELM_GLOBAL_SAVEPOINT` temporarily pushes 1 before
9735                // `tnm_set_save_point()` and then replaces it with return 0.
9736                // A later load resumes from the saved stream with that 1 still
9737                // on the int stack, allowing scripts to distinguish "loaded from
9738                // this SAVEPOINT" from normal forward execution.
9739                self.int_stack.push(1);
9740                self.save_point = Some(self.make_resume_point());
9741                self.build_local_save_snapshot();
9742                let _ = self.int_stack.pop();
9743                if ret_form != self.cfg.fm_void {
9744                    self.ctx.stack.push(Value::Int(0));
9745                }
9746                Ok(true)
9747            }
9748            constants::elm_value::GLOBAL_CLEAR_SAVEPOINT => {
9749                self.save_point = None;
9750                self.ctx.local_save_snapshot = None;
9751                Ok(true)
9752            }
9753            constants::elm_value::GLOBAL_CHECK_SAVEPOINT => {
9754                let has = self
9755                    .ctx
9756                    .local_save_snapshot
9757                    .as_ref()
9758                    .map(|s| !s.local_stream.is_empty())
9759                    .unwrap_or(false);
9760                self.ctx.stack.push(Value::Int(if has { 1 } else { 0 }));
9761                Ok(true)
9762            }
9763            constants::elm_value::GLOBAL_SELPOINT => {
9764                let point = self.make_resume_point();
9765                self.sel_point_stack.clear();
9766                self.sel_point_stack.push(point);
9767                Ok(true)
9768            }
9769            constants::elm_value::GLOBAL_CLEAR_SELPOINT => {
9770                self.sel_point_stack.clear();
9771                Ok(true)
9772            }
9773            constants::elm_value::GLOBAL_CHECK_SELPOINT => {
9774                self.ctx
9775                    .stack
9776                    .push(Value::Int(if self.has_sel_point() { 1 } else { 0 }));
9777                Ok(true)
9778            }
9779            constants::elm_value::GLOBAL_STACK_SELPOINT => {
9780                let point = self.make_resume_point();
9781                self.sel_point_stack.push(point);
9782                Ok(true)
9783            }
9784            constants::elm_value::GLOBAL_DROP_SELPOINT => {
9785                let _ = self.sel_point_stack.pop();
9786                Ok(true)
9787            }
9788            _ => Ok(false),
9789        }
9790    }
9791
9792    fn exec_builtin_scene_form(
9793        &mut self,
9794        elm: &[i32],
9795        form_id: i32,
9796        al_id: i32,
9797        ret_form: i32,
9798        args: &[Value],
9799    ) -> Result<bool> {
9800        const FORM_GLOBAL_JUMP: i32 = crate::runtime::forms::codes::elm_value::GLOBAL_JUMP;
9801        const FORM_GLOBAL_FARCALL: i32 = crate::runtime::forms::codes::elm_value::GLOBAL_FARCALL;
9802        const FORM_GLOBAL_SYSCOM: i32 = crate::runtime::forms::codes::FORM_GLOBAL_SYSCOM as i32;
9803        const FORM_SYSCOM: i32 = crate::runtime::forms::codes::FM_SYSCOM;
9804        const ELM_SYSCOM_CALL_EX: i32 = crate::runtime::forms::codes::elm_value::SYSCOM_CALL_EX;
9805        if (form_id == FORM_GLOBAL_SYSCOM || form_id == FORM_SYSCOM)
9806            && elm.get(1).copied() == Some(ELM_SYSCOM_CALL_EX)
9807        {
9808            self.sg_omv_trace_command("builtin", elm, form_id, ELM_SYSCOM_CALL_EX, al_id, self.cfg.fm_void, args);
9809            let scene_name = args.get(0).and_then(|v| v.as_str()).unwrap_or("");
9810            let z_no = if al_id == 1 {
9811                args.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32
9812            } else {
9813                0
9814            };
9815            let scratch_args = if al_id == 1 && args.len() > 2 {
9816                &args[2..]
9817            } else {
9818                &[]
9819            };
9820            self.farcall_scene_name_ex(scene_name, z_no, self.cfg.fm_void, true, scratch_args)?;
9821            self.ctx.request_proc_boundary(runtime::ProcKind::Script);
9822            self.ctx.stack.clear();
9823            return Ok(true);
9824        }
9825        if form_id == FORM_GLOBAL_JUMP {
9826            self.sg_omv_trace_command("builtin", &[], form_id, form_id, al_id, ret_form, args);
9827            let scene_name = args.get(0).and_then(|v| v.as_str()).unwrap_or("");
9828            let z_no = if al_id >= 1 {
9829                args.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32
9830            } else {
9831                0
9832            };
9833            if !scene_name.is_empty() {
9834                self.jump_to_scene_name(scene_name, z_no)?;
9835            }
9836            return Ok(true);
9837        }
9838        if form_id == FORM_GLOBAL_FARCALL {
9839            self.sg_omv_trace_command("builtin", &[], form_id, form_id, al_id, ret_form, args);
9840            let scene_name = args.get(0).and_then(|v| v.as_str()).unwrap_or("");
9841            let z_no = if al_id >= 1 {
9842                args.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32
9843            } else {
9844                0
9845            };
9846            if !scene_name.is_empty() {
9847                self.farcall_scene_name_ex(
9848                    scene_name,
9849                    z_no,
9850                    self.cfg.fm_int,
9851                    false,
9852                    if al_id >= 1 && args.len() > 2 {
9853                        &args[2..]
9854                    } else {
9855                        &[]
9856                    },
9857                )?;
9858            } else {
9859                self.push_default_for_ret(self.cfg.fm_int);
9860            }
9861            return Ok(true);
9862        }
9863        Ok(false)
9864    }
9865
9866    fn take_ctx_return(&mut self, ret_form: i32) -> Result<()> {
9867        if ret_form == self.cfg.fm_void {
9868            self.ctx.stack.clear();
9869            return Ok(());
9870        }
9871
9872        let v = self.ctx.pop();
9873        match ret_form {
9874            f if f == self.cfg.fm_int || f == self.cfg.fm_label => match v {
9875                Some(Value::Int(n)) => self.push_int(n as i32),
9876                Some(Value::NamedArg { value, .. }) => match *value {
9877                    Value::Int(n) => self.push_int(n as i32),
9878                    _ => bail!("non-int ctx return for form {}", ret_form),
9879                },
9880                Some(_) => bail!("non-int ctx return for form {}", ret_form),
9881                None => bail!(
9882                    "missing ctx return int for form {}: scene={} scene_no={} line={} pc=0x{:x} vm_call={:?}",
9883                    ret_form,
9884                    self.current_scene_name.as_deref().unwrap_or("<none>"),
9885                    self.current_scene_no
9886                        .map(|v| v.to_string())
9887                        .unwrap_or_else(|| "-".to_string()),
9888                    self.current_line_no,
9889                    self.stream.get_prg_cntr(),
9890                    self.ctx.vm_call
9891                ),
9892            },
9893            f if f == self.cfg.fm_str => match v {
9894                Some(Value::Str(s)) => self.push_str(s),
9895                Some(Value::NamedArg { value, .. }) => match *value {
9896                    Value::Str(s) => self.push_str(s),
9897                    _ => bail!("non-str ctx return for form {}", ret_form),
9898                },
9899                Some(_) => bail!("non-str ctx return for form {}", ret_form),
9900                None => bail!("missing ctx return str for form {}", ret_form),
9901            },
9902            f if f == self.cfg.fm_list => match v {
9903                Some(Value::Element(elm)) => self.push_element(elm),
9904                Some(Value::NamedArg { value, .. }) => match *value {
9905                    Value::Element(elm) => self.push_element(elm),
9906                    _ => bail!("non-element ctx return for FM_LIST"),
9907                },
9908                Some(Value::List(_)) => {
9909                    bail!("FM_LIST ctx return used raw Value::List; expected element reference")
9910                }
9911                Some(_) => bail!("non-element ctx return for FM_LIST"),
9912                None => bail!("missing ctx return element for FM_LIST"),
9913            },
9914            _ => match v {
9915                Some(Value::Element(elm)) => self.push_element(elm),
9916                Some(Value::NamedArg { value, .. }) => match *value {
9917                    Value::Element(elm) => self.push_element(elm),
9918                    _ => bail!("non-element ctx return for form {}", ret_form),
9919                },
9920                Some(_) => bail!("non-element ctx return for form {}", ret_form),
9921                None => bail!("missing ctx return element for form {}", ret_form),
9922            },
9923        }
9924        Ok(())
9925    }
9926
9927    fn push_default_for_ret(&mut self, ret_form: i32) {
9928        if ret_form == self.cfg.fm_int || ret_form == self.cfg.fm_label {
9929            self.push_int(0);
9930        } else if ret_form == self.cfg.fm_str {
9931            self.push_str(String::new());
9932        }
9933    }
9934
9935    fn update_compact_context_from_element(&mut self, elm: &[i32]) {
9936        let stage_form = if self.ctx.ids.form_global_stage != 0 {
9937            self.ctx.ids.form_global_stage as i32
9938        } else {
9939            crate::runtime::forms::codes::FORM_GLOBAL_STAGE as i32
9940        };
9941        let elm_array = if self.ctx.ids.elm_array != 0 {
9942            self.ctx.ids.elm_array
9943        } else {
9944            crate::runtime::forms::codes::ELM_ARRAY
9945        };
9946        let stage_object = if self.ctx.ids.stage_elm_object != 0 {
9947            self.ctx.ids.stage_elm_object
9948        } else {
9949            crate::runtime::forms::codes::STAGE_ELM_OBJECT
9950        };
9951        let stage_mwnd = crate::runtime::forms::codes::STAGE_ELM_MWND;
9952        let stage_btnselitem = crate::runtime::forms::codes::STAGE_ELM_BTNSELITEM;
9953
9954        fn is_array_token(token: i32, elm_array: i32) -> bool {
9955            token == elm_array || token == crate::runtime::forms::codes::ELM_ARRAY
9956        }
9957
9958        fn object_chain_tail_is_plain_object_ref(
9959            elm: &[i32],
9960            mut pos: usize,
9961            elm_array: i32,
9962        ) -> bool {
9963            let object_child = crate::runtime::forms::codes::elm_value::OBJECT_CHILD;
9964            while pos + 2 < elm.len()
9965                && elm[pos] == object_child
9966                && is_array_token(elm[pos + 1], elm_array)
9967            {
9968                if elm[pos + 2] < 0 {
9969                    return false;
9970                }
9971                pos += 3;
9972            }
9973            pos == elm.len()
9974        }
9975
9976        let resolved = if elm.len() >= 6
9977            && elm[0] == stage_form
9978            && is_array_token(elm[1], elm_array)
9979            && elm[2] >= 0
9980            && elm[3] == stage_object
9981            && is_array_token(elm[4], elm_array)
9982            && elm[5] >= 0
9983            && object_chain_tail_is_plain_object_ref(elm, 6, elm_array)
9984        {
9985            Some((elm[2] as i64, elm[5] as usize))
9986        } else if elm.len() >= 9
9987            && elm[0] == stage_form
9988            && is_array_token(elm[1], elm_array)
9989            && elm[2] >= 0
9990            && elm[3] == stage_mwnd
9991            && is_array_token(elm[4], elm_array)
9992            && elm[5] >= 0
9993            && matches!(
9994                elm[6],
9995                crate::runtime::forms::codes::elm_value::MWND_OBJECT
9996                    | crate::runtime::forms::codes::elm_value::MWND_BUTTON
9997                    | crate::runtime::forms::codes::elm_value::MWND_FACE
9998            )
9999            && is_array_token(elm[7], elm_array)
10000            && elm[8] >= 0
10001            && object_chain_tail_is_plain_object_ref(elm, 9, elm_array)
10002        {
10003            Some((elm[2] as i64, elm[8] as usize))
10004        } else if elm.len() >= 9
10005            && elm[0] == stage_form
10006            && is_array_token(elm[1], elm_array)
10007            && elm[2] >= 0
10008            && elm[3] == stage_btnselitem
10009            && is_array_token(elm[4], elm_array)
10010            && elm[5] >= 0
10011            && elm[6] == crate::runtime::forms::codes::ELM_BTNSELITEM_OBJECT
10012            && is_array_token(elm[7], elm_array)
10013            && elm[8] >= 0
10014            && object_chain_tail_is_plain_object_ref(elm, 9, elm_array)
10015        {
10016            Some((elm[2] as i64, elm[8] as usize))
10017        } else {
10018            None
10019        };
10020
10021        let Some((stage_idx, fallback_obj_idx)) = resolved else {
10022            return;
10023        };
10024        let runtime_slot = self.runtime_slot_from_object_chain(stage_idx, fallback_obj_idx, elm);
10025        let prev_chain = self.ctx.globals.current_object_chain.clone();
10026        let prev_stage_object = self.ctx.globals.current_stage_object;
10027        self.ctx.globals.current_object_chain = Some(elm.to_vec());
10028        self.ctx.globals.current_stage_object = Some((stage_idx, runtime_slot));
10029        if Self::sg_mwnd_object_trace_enabled() && Self::sg_mwnd_chain_interesting(elm) {
10030            self.sg_mwnd_object_trace(format!(
10031                "update_compact_context elm={:?} resolved_stage={} fallback_idx={} runtime_slot={} prev_chain={:?} prev_stage_object={:?}",
10032                elm,
10033                stage_idx,
10034                fallback_obj_idx,
10035                runtime_slot,
10036                prev_chain,
10037                prev_stage_object
10038            ));
10039        }
10040    }
10041
10042    fn update_compact_context_from_object_dispatch_chain(&mut self, elm: &[i32]) {
10043        if elm.is_empty() {
10044            return;
10045        }
10046        let stage_form = if self.ctx.ids.form_global_stage != 0 {
10047            self.ctx.ids.form_global_stage as i32
10048        } else {
10049            crate::runtime::forms::codes::FORM_GLOBAL_STAGE as i32
10050        };
10051        let elm_array = if self.ctx.ids.elm_array != 0 {
10052            self.ctx.ids.elm_array
10053        } else {
10054            crate::runtime::forms::codes::ELM_ARRAY
10055        };
10056        let stage_object = if self.ctx.ids.stage_elm_object != 0 {
10057            self.ctx.ids.stage_elm_object
10058        } else {
10059            crate::runtime::forms::codes::STAGE_ELM_OBJECT
10060        };
10061        let stage_mwnd = crate::runtime::forms::codes::STAGE_ELM_MWND;
10062        let stage_btnselitem = crate::runtime::forms::codes::STAGE_ELM_BTNSELITEM;
10063        let object_child = crate::runtime::forms::codes::elm_value::OBJECT_CHILD;
10064
10065        fn is_array_token(token: i32, elm_array: i32) -> bool {
10066            token == elm_array || token == crate::runtime::forms::codes::ELM_ARRAY
10067        }
10068
10069        let mut pos = if elm.len() >= 6
10070            && elm[0] == stage_form
10071            && is_array_token(elm[1], elm_array)
10072            && elm[2] >= 0
10073            && elm[3] == stage_object
10074            && is_array_token(elm[4], elm_array)
10075            && elm[5] >= 0
10076        {
10077            6usize
10078        } else if elm.len() >= 9
10079            && elm[0] == stage_form
10080            && is_array_token(elm[1], elm_array)
10081            && elm[2] >= 0
10082            && elm[3] == stage_mwnd
10083            && is_array_token(elm[4], elm_array)
10084            && elm[5] >= 0
10085            && matches!(
10086                elm[6],
10087                crate::runtime::forms::codes::elm_value::MWND_OBJECT
10088                    | crate::runtime::forms::codes::elm_value::MWND_BUTTON
10089                    | crate::runtime::forms::codes::elm_value::MWND_FACE
10090            )
10091            && is_array_token(elm[7], elm_array)
10092            && elm[8] >= 0
10093        {
10094            9usize
10095        } else if elm.len() >= 9
10096            && elm[0] == stage_form
10097            && is_array_token(elm[1], elm_array)
10098            && elm[2] >= 0
10099            && elm[3] == stage_btnselitem
10100            && is_array_token(elm[4], elm_array)
10101            && elm[5] >= 0
10102            && elm[6] == crate::runtime::forms::codes::ELM_BTNSELITEM_OBJECT
10103            && is_array_token(elm[7], elm_array)
10104            && elm[8] >= 0
10105        {
10106            9usize
10107        } else {
10108            return;
10109        };
10110
10111        while pos + 2 < elm.len()
10112            && elm[pos] == object_child
10113            && is_array_token(elm[pos + 1], elm_array)
10114            && elm[pos + 2] >= 0
10115        {
10116            pos += 3;
10117        }
10118
10119        let object_ref = elm[..pos].to_vec();
10120        if Self::sg_mwnd_object_trace_enabled() && Self::sg_mwnd_chain_interesting(elm) {
10121            self.sg_mwnd_object_trace(format!(
10122                "update_context_from_dispatch elm={:?} object_ref={:?} pos={}",
10123                elm,
10124                object_ref,
10125                pos
10126            ));
10127        }
10128        self.update_compact_context_from_element(&object_ref);
10129    }
10130
10131    fn push_return_value_raw(&mut self, v: Value) {
10132        match v {
10133            Value::NamedArg { value, .. } => self.push_return_value_raw(*value),
10134            Value::Int(n) => self.push_int(n as i32),
10135            Value::Str(s) => self.push_str(s),
10136            Value::Element(elm) => {
10137                self.update_compact_context_from_element(&elm);
10138                self.push_element(elm);
10139            }
10140            Value::List(_) => {
10141                panic!("raw Value::List reached push_return_value_raw; expected runtime ref");
10142            }
10143        }
10144    }
10145
10146    // ---------------------------------------------------------------------
10147    // Arithmetic / comparisons
10148    // ---------------------------------------------------------------------
10149
10150    fn exec_operate_1(&mut self, form_code: i32, opr: u8) -> Result<()> {
10151        if form_code != self.cfg.fm_int {
10152            self.trace_unknown_form(form_code, "exec_operate_1");
10153            self.push_int(0);
10154            return Ok(());
10155        }
10156
10157        let v = self.pop_int()?;
10158        let out = match opr {
10159            OP_PLUS => v,
10160            OP_MINUS => v.wrapping_neg(),
10161            OP_TILDE => !v,
10162            _ => v,
10163        };
10164        if self.cf_condition_trace_interesting_line() {
10165            self.sg_cf_condition_trace(
10166                self.stream.get_prg_cntr(),
10167                format!(
10168                    "kind=OPERATE_1 op={} in={} out={}",
10169                    Self::cf_condition_op_name(opr),
10170                    v,
10171                    out
10172                ),
10173            );
10174        }
10175        self.push_int(out);
10176        Ok(())
10177    }
10178
10179    fn exec_operate_2(&mut self, form_l: i32, form_r: i32, opr: u8) -> Result<()> {
10180        // int/int
10181        if form_l == self.cfg.fm_int && form_r == self.cfg.fm_int {
10182            let r = self.pop_int()?;
10183            let l = self.pop_int()?;
10184            let out = self.calc_int_int(l, r, opr);
10185            if self.cf_condition_trace_interesting_line() {
10186                self.sg_cf_condition_trace(
10187                    self.stream.get_prg_cntr(),
10188                    format!(
10189                        "kind=OPERATE_2 op={} left={} right={} out={}",
10190                        Self::cf_condition_op_name(opr),
10191                        l,
10192                        r,
10193                        out
10194                    ),
10195                );
10196            }
10197            self.push_int(out);
10198            return Ok(());
10199        }
10200
10201        // str/int
10202        if form_l == self.cfg.fm_str && form_r == self.cfg.fm_int {
10203            let r = self.pop_int()?;
10204            let l = self.pop_str()?;
10205            let out = self.calc_str_int(l, r, opr);
10206            self.push_str(out);
10207            return Ok(());
10208        }
10209
10210        // str/str
10211        if form_l == self.cfg.fm_str && form_r == self.cfg.fm_str {
10212            let r = self.pop_str()?;
10213            let l = self.pop_str()?;
10214            let out = self.calc_str_str(l, r, opr);
10215            match out {
10216                Value::Int(n) => self.push_int(n as i32),
10217                Value::Str(s) => self.push_str(s),
10218                _ => {
10219                    self.push_int(0);
10220                }
10221            }
10222            return Ok(());
10223        }
10224
10225        // Unknown combo.
10226        self.trace_unknown_form(form_l, "exec_operate_2.left");
10227        self.trace_unknown_form(form_r, "exec_operate_2.right");
10228        self.push_int(0);
10229        Ok(())
10230    }
10231
10232    fn calc_int_int(&mut self, l: i32, r: i32, opr: u8) -> i32 {
10233        match opr {
10234            OP_PLUS => l.wrapping_add(r),
10235            OP_MINUS => l.wrapping_sub(r),
10236            OP_MULTIPLE => l.wrapping_mul(r),
10237            OP_DIVIDE => {
10238                if r == 0 {
10239                    0
10240                } else {
10241                    l.wrapping_div(r)
10242                }
10243            }
10244            OP_AMARI => {
10245                if r == 0 {
10246                    0
10247                } else {
10248                    l.wrapping_rem(r)
10249                }
10250            }
10251
10252            OP_EQUAL => (l == r) as i32,
10253            OP_NOT_EQUAL => (l != r) as i32,
10254            OP_GREATER => (l > r) as i32,
10255            OP_GREATER_EQUAL => (l >= r) as i32,
10256            OP_LESS => (l < r) as i32,
10257            OP_LESS_EQUAL => (l <= r) as i32,
10258
10259            OP_LOGICAL_OR => ((l != 0) || (r != 0)) as i32,
10260            OP_LOGICAL_AND => ((l != 0) && (r != 0)) as i32,
10261
10262            OP_OR => l | r,
10263            OP_AND => l & r,
10264            OP_HAT => l ^ r,
10265            OP_SL => l.wrapping_shl((r as u32) & 31),
10266            OP_SR => l.wrapping_shr((r as u32) & 31),
10267            OP_SR3 => ((l as u32).wrapping_shr((r as u32) & 31)) as i32,
10268
10269            _ => 0,
10270        }
10271    }
10272
10273    fn calc_str_int(&mut self, s: String, n: i32, opr: u8) -> String {
10274        match opr {
10275            OP_MULTIPLE => {
10276                if n <= 0 {
10277                    return String::new();
10278                }
10279                let mut out = String::new();
10280                for _ in 0..(n as usize) {
10281                    out.push_str(&s);
10282                }
10283                out
10284            }
10285            _ => s,
10286        }
10287    }
10288
10289    fn calc_str_str(&mut self, l: String, r: String, opr: u8) -> Value {
10290        match opr {
10291            OP_PLUS => Value::Str(format!("{l}{r}")),
10292            OP_EQUAL | OP_NOT_EQUAL | OP_GREATER | OP_GREATER_EQUAL | OP_LESS | OP_LESS_EQUAL => {
10293                // The original engine lowercases for comparisons.
10294                let ll = l.to_lowercase();
10295                let rr = r.to_lowercase();
10296                let cmp = ll.cmp(&rr);
10297                let b = match opr {
10298                    OP_EQUAL => cmp == std::cmp::Ordering::Equal,
10299                    OP_NOT_EQUAL => cmp != std::cmp::Ordering::Equal,
10300                    OP_GREATER => cmp == std::cmp::Ordering::Greater,
10301                    OP_GREATER_EQUAL => cmp != std::cmp::Ordering::Less,
10302                    OP_LESS => cmp == std::cmp::Ordering::Less,
10303                    OP_LESS_EQUAL => cmp != std::cmp::Ordering::Greater,
10304                    _ => false,
10305                };
10306                Value::Int(b as i64)
10307            }
10308            _ => Value::Int(0),
10309        }
10310    }
10311}